Difference between revisions of "Argo Bubbles"

From The Official Visionaire Studio: Adventure Game Engine Wiki
Line 15: Line 15:
 
|}
 
|}
 
<hr>
 
<hr>
 
  
 
== Instructions ==
 
== Instructions ==
  
1. Add the [[#Main_Script|main script]] to the Visionaire Studio Script Editor & set the script as a definition script.<br/>
+
1. Add the [[#Main_Script|main script]] to the Visionaire Studio Script Editor & set the script as a definition script.
 
 
<pdf width="1070" height="500">File:Creating_Speech_Bubbles_with_the_Argo_Bubbles_Script.pdf</pdf>
 
  
 
2. Create the bubble graphics & add them into the '''gui''' subfolder of your projects root folder - if the '''gui''' folder doesn't already exist, create it.
 
2. Create the bubble graphics & add them into the '''gui''' subfolder of your projects root folder - if the '''gui''' folder doesn't already exist, create it.
Line 29: Line 26:
 
Since the dimensions (width, height) of a speech bubble depend on the length of the text and the number of lines, there are limitations to what the bubbles can look like. It is not possible with this script to have elliptical or fancy-shaped bubbles. They need to be more or less rectangular, but may have rounded or chamfered corners. That's because the script splits the main bubble graphic into 9 tiles which are individually scaled and then recombined to match the size of the text.
 
Since the dimensions (width, height) of a speech bubble depend on the length of the text and the number of lines, there are limitations to what the bubbles can look like. It is not possible with this script to have elliptical or fancy-shaped bubbles. They need to be more or less rectangular, but may have rounded or chamfered corners. That's because the script splits the main bubble graphic into 9 tiles which are individually scaled and then recombined to match the size of the text.
  
[[File:Argo_bubbles_ninerect.png|800px]]
+
<div style="text-align:center;margin:30px 0">[[File:Argo_bubbles_ninerect.png|1000px]]</div>
 +
 
 +
* The four corners (1, 3, 7, 9) will keep their look and size<br/>
 +
* The horizontal borders (2, 8) will get stretched/shrinked to the required width<br/>
 +
* The vertical borders (4, 6) will get stretched/shrinked to the required height<br/>
 +
* The center tile (5) is stretched/shrinked horizontally and vertically to fill the bubble<br/>
  
◻️ The four corners (1, 3, 7, 9) will keep their look and size<br/>
 
◻️ The horizontal borders (2, 8) will get stretched/shrinked to the required width<br/>
 
◻️ The vertical borders (4, 6) will get stretched/shrinked to the required height<br/>
 
◻️ The center tile (5) is stretched/shrinked horizontally and vertically to fill the bubble<br/>
 
  
 
You usually want to have a pointer attached to the bubble, pointing to the character who is currently speaking. Depending on the direction the character is facing, the script either uses the right-pointing or the left-pointing pointer graphics. So you should at least provide two graphic files.
 
You usually want to have a pointer attached to the bubble, pointing to the character who is currently speaking. Depending on the direction the character is facing, the script either uses the right-pointing or the left-pointing pointer graphics. So you should at least provide two graphic files.
Line 40: Line 38:
 
You also have the option of positioning the speech bubble above the character (that's the default, where the pointer is attached to the bottom of the bubble) or below the character's head (with the pointer attached to the top of the bubble and pointing upwards). If you make use of both possibilities (through the custom bubble style option of the script), you must provide four different pointers.
 
You also have the option of positioning the speech bubble above the character (that's the default, where the pointer is attached to the bottom of the bubble) or below the character's head (with the pointer attached to the top of the bubble and pointing upwards). If you make use of both possibilities (through the custom bubble style option of the script), you must provide four different pointers.
  
[[File:Argo_bubbles_pointers.png|1000px]]
+
<div style="text-align:center;margin:30px 0">[[File:Argo_bubbles_pointers.png|1000px]]</div>
  
 
However, you don't have to use pointers at all. Just set all pointer path options to an empty string <span class="inlinecode">" "</span> to get a bubble without pointer.
 
However, you don't have to use pointers at all. Just set all pointer path options to an empty string <span class="inlinecode">" "</span> to get a bubble without pointer.
Line 50: Line 48:
 
The last options are related to the events which the bubble creation is bound to: "textStarted" and "textStopped". Remember that you can register each type of event handler only once in a Visionaire project. So if you have already registered an event handler for the "textStarted" and/or the "textStopped" event, you can't do it again in this bubble script. Instead you have to put all your functions that use the same event handler into one function and bind that one to the event. The Argo Bubble script offers a convenient way to do this without messing up the main code.
 
The last options are related to the events which the bubble creation is bound to: "textStarted" and "textStopped". Remember that you can register each type of event handler only once in a Visionaire project. So if you have already registered an event handler for the "textStarted" and/or the "textStopped" event, you can't do it again in this bubble script. Instead you have to put all your functions that use the same event handler into one function and bind that one to the event. The Argo Bubble script offers a convenient way to do this without messing up the main code.
  
 +
== Tutorial ==
 +
For a deeper understanding of how the speech bubbles are created and how they replace the text displayed
 +
by Visionaire, please have a look at the tutorial (it is included in the zip file). You don't have to read it, if you just want to use the script in your project. Everything you need to know is explained in the script.
 +
 +
<pdf width="1070" height="500">File:Creating_Speech_Bubbles_with_the_Argo_Bubbles_Script.pdf</pdf>
  
 
== Main Script ==
 
== Main Script ==
Line 63: Line 66:
  
 
Author:          The Argonauts
 
Author:          The Argonauts
Version:        2.1.1
+
Version:        2.1.2
Date:            2022-09-02
+
Date:            2022-09-09
 
Play our games:  https://the-argonauts.itch.io/
 
Play our games:  https://the-argonauts.itch.io/
  
Line 71: Line 74:
  
  
For a description on how to use it, please visit the official Visionaire wiki (download includes a demo):
+
For a description on how to use it, please visit the official Visionaire wiki (download includes a demo project and a tutorial): https://wiki.visionaire-tracker.net/wiki/Compiled_Index_of_Lua_Scripts_for_Visionaire_Studio
https://wiki.visionaire-tracker.net/wiki/Compiled_Index_of_Lua_Scripts_for_Visionaire_Studio
 
  
  
Line 95: Line 97:
 
-- DEFAULT BUBBLE STYLE
 
-- DEFAULT BUBBLE STYLE
 
local default_style = {
 
local default_style = {
   align_h = "center", -- horizontal alignment of bubble in relation to character (center, left, right, char_facing)
+
   align_h = "center", -- horizontal alignment of bubble in relation to character (center|left|right|char_facing)
   align_v = "top", -- vertical alignment of bubble in relation to character (top, bottom), defines position of pointer
+
   align_v = "top", -- vertical alignment of bubble in relation to character (top|bottom), defines position of pointer
   bubble_offset_x = 0, -- horizontal bubble offset
+
   bubble_offset_x = 0, -- horizontal bubble offset (positive: move in facing direction)
   bubble_offset_y = -25, -- vertical bubble offset
+
   bubble_offset_y = -25, -- vertical bubble offset (positive: move downwards)
 
   color = 0xffffff, -- color tint for bubble and pointer (set to white for none: 0xffffff)
 
   color = 0xffffff, -- color tint for bubble and pointer (set to white for none: 0xffffff)
  
  linesgap = 3, -- gap between lines of text, as defined in font
+
   text_align = "center", -- alignment of multi-line text inside the bubble (center|left|right)
   text_align = "center", -- alignment of multi-line text inside the bubble (center, left, right)
 
 
   padding = {
 
   padding = {
 
     top = 15,
 
     top = 15,
Line 117: Line 118:
 
    
 
    
 
   -- Set the "file_pointer..." paths to "" for bubbles without pointers
 
   -- Set the "file_pointer..." paths to "" for bubbles without pointers
   file_pointer_bottom_right = "vispath:gui/bubble_pointer_br.png", -- path to right facing pointer at bubble bottom
+
   file_pointer_bottom_right = "vispath:gui/bubble_pointer_br.png", -- path to -> pointer at bubble bottom
   file_pointer_bottom_left = "vispath:gui/bubble_pointer_bl.png", -- path to left facing pointer at bubble bottom
+
   file_pointer_bottom_left = "vispath:gui/bubble_pointer_bl.png", -- path to <- pointer at bubble bottom
   pointer_bottom_right_offset_x = -20, -- x offset of right facing pointer at bubble bottom
+
   pointer_bottom_right_offset_x = 20, -- x offset of -> pointer at bubble bottom (positive: move in facing direction)
   pointer_bottom_left_offset_x = 0, -- x offset of left facing pointer at bubble bottom
+
   pointer_bottom_left_offset_x = 0, -- x offset of <- pointer at bubble bottom (positive: move in facing direction)
   pointer_bottom_offset_y = 3, -- y offset of pointer at bubble bottom (positive numbers for moving inwards)
+
   pointer_bottom_offset_y = 3, -- y offset of pointer at bubble bottom (positive: move inwards)
 
    
 
    
   file_pointer_top_right = "", -- path to right facing pointer at bubble top
+
   file_pointer_top_right = "", -- path to -> pointer at bubble top
   file_pointer_top_left = "", -- path to left facing pointer at bubble top
+
   file_pointer_top_left = "", -- path to <- pointer at bubble top
   pointer_top_right_offset_x = 0, -- x offset of right facing pointer at bubble top
+
   pointer_top_right_offset_x = 0, -- x offset of -> pointer at top (positive: move in facing direction)
   pointer_top_left_offset_x = 0, -- x offset of left facing pointer at bubble top
+
   pointer_top_left_offset_x = 0, -- x offset of <- pointer at top (positive: move in facing direction)
   pointer_top_offset_y = 0 -- y offset of pointer at bubble top (positive numbers for moving inwards)
+
   pointer_top_offset_y = 0 -- y offset of pointer at bubble top (positive: move inwards)
 
}
 
}
  
Line 145: Line 146:
 
     align_h = "left",
 
     align_h = "left",
 
     align_v = "bottom",
 
     align_v = "bottom",
     bubble_offset_x = -40,
+
     bubble_offset_x = 40,
 
     bubble_offset_y = 80,
 
     bubble_offset_y = 80,
 
     file_pointer_top_right = "vispath:gui/bubble_pointer_tr.png",
 
     file_pointer_top_right = "vispath:gui/bubble_pointer_tr.png",
Line 209: Line 210:
 
   end
 
   end
 
end
 
end
 
 
  
 
-- EVENT HANDLERS
 
-- EVENT HANDLERS
Line 218: Line 217:
 
   -- Add current text to the bubbles table and prevent displaying
 
   -- Add current text to the bubbles table and prevent displaying
 
   if text.Owner:getId().tableId == eCharacters then
 
   if text.Owner:getId().tableId == eCharacters then
     bubbles[text:getId().id] = {Text = text.CurrentText, Owner = text.Owner:getName(), Background = text.Background}
+
     bubbles[text:getId().id] = {
 +
      Text = text.CurrentText,
 +
      Owner = text.Owner:getName(),
 +
      Background = text.Background
 +
    }
  
 
     text.CurrentText = ""
 
     text.CurrentText = ""
Line 270: Line 273:
 
   local char = Characters[val.Owner]
 
   local char = Characters[val.Owner]
 
   local pos = graphics.getCharacterTextPosition(char)
 
   local pos = graphics.getCharacterTextPosition(char)
  local char_facing = "right"
 
  if char.Direction > 90 and char.Direction < 270 then
 
    char_facing = "left"
 
  end
 
  graphics.font = char.Font
 
  
 
   -- Use default bubble style or custom style depending on talking character
 
   -- Use default bubble style or custom style depending on talking character
Line 289: Line 287:
 
   end
 
   end
  
   -- Calculate the text dimensions (width, height)
+
  -- Facing direction (if bubble alignment is set to "left" or "right", the actual facing direction is irrelevant)
 +
  local char_facing = "right"
 +
 
 +
  if bubble_style.align_h == "left" then
 +
    char_facing = "left"
 +
  elseif bubble_style.align_h ~= "right" then
 +
    if char.Direction > 90 and char.Direction < 270 then
 +
      char_facing = "left"
 +
    end
 +
  end
 +
 
 +
   -- Calculate the text and bubble dimensions (width, height)
 
   local txt = val.Text:gsub("<br/"..">", "\n")
 
   local txt = val.Text:gsub("<br/"..">", "\n")
 
   local lines = graphics.performLinebreaks(txt)
 
   local lines = graphics.performLinebreaks(txt)
   local dim = {x = 0, y = 0}
+
   local text_dim = {x = 0, y = 0}
 +
  local bubble_dim = {x = 0, y = 0}
  
   for k,v in ipairs(lines) do  
+
   for k, line in ipairs(lines) do  
     local tempdim = graphics.fontDimension(v)
+
     local tempdim = graphics.fontDimension(line)
     if dim.x < tempdim.x then dim.x = tempdim.x end  
+
   
 +
     if text_dim.x < tempdim.x then
 +
      text_dim.x = tempdim.x
 +
    end  
 
   end
 
   end
  
   dim.y = #lines * (char.Font.Size + bubble_style.linesgap) - bubble_style.linesgap
+
   text_dim.y = #lines * (char.Font.Size + char.Font.VerticalLetterSpacing) - char.Font.VerticalLetterSpacing
 +
 
 +
  bubble_dim.x = text_dim.x + bubble_style.padding.right + bubble_style.padding.left
 +
  bubble_dim.y = text_dim.y + bubble_style.padding.top + bubble_style.padding.bottom
  
 
   -- Calculate the bubble position
 
   -- Calculate the bubble position
   if bubble_style.align_h == "left" or (bubble_style.align_h == "char_facing" and char_facing == "left") then
+
   if bubble_style.align_h == "right" or (bubble_style.align_h == "char_facing" and char_facing == "right") then
     pos.x = pos.x - game.ScrollPosition.x - dim.x - bubble_style.padding.right - bubble_style.padding.left + bubble_style.bubble_offset_x
+
     pos.x = pos.x - game.ScrollPosition.x
   elseif bubble_style.align_h == "right" or (bubble_style.align_h == "char_facing" and char_facing == "right") then
+
   elseif bubble_style.align_h == "left" or (bubble_style.align_h == "char_facing" and char_facing == "left") then
     pos.x = pos.x - game.ScrollPosition.x + bubble_style.bubble_offset_x
+
     pos.x = pos.x - game.ScrollPosition.x - bubble_dim.x
 
   else -- center
 
   else -- center
     pos.x = pos.x - game.ScrollPosition.x - (dim.x + bubble_style.padding.right + bubble_style.padding.left) / 2 + bubble_style.bubble_offset_x
+
     pos.x = pos.x - game.ScrollPosition.x - bubble_dim.x / 2
 +
  end
 +
 
 +
  if char_facing == "left" then
 +
    pos.x = pos.x - bubble_style.bubble_offset_x
 +
  else -- right
 +
    pos.x = pos.x + bubble_style.bubble_offset_x
 
   end
 
   end
  
 
   if pos.x < min_distance then
 
   if pos.x < min_distance then
 
     pos.x = min_distance
 
     pos.x = min_distance
   elseif pos.x > game.WindowResolution.x - dim.x - bubble_style.padding.right - bubble_style.padding.left - min_distance then
+
   elseif pos.x > game.WindowResolution.x - bubble_dim.x - min_distance then
     pos.x = game.WindowResolution.x - dim.x - bubble_style.padding.right - bubble_style.padding.left - min_distance
+
     pos.x = game.WindowResolution.x - bubble_dim.x - min_distance
 
   end
 
   end
  
Line 319: Line 341:
 
     pos.y = pos.y - game.ScrollPosition.y + bubble_style.bubble_offset_y
 
     pos.y = pos.y - game.ScrollPosition.y + bubble_style.bubble_offset_y
 
   else -- top
 
   else -- top
     pos.y = pos.y - game.ScrollPosition.y - (dim.y + bubble_style.padding.top + bubble_style.padding.bottom) + bubble_style.bubble_offset_y
+
     pos.y = pos.y - game.ScrollPosition.y - bubble_dim.y + bubble_style.bubble_offset_y
 
   end
 
   end
  
 
   if pos.y < min_distance then
 
   if pos.y < min_distance then
 
     pos.y = min_distance
 
     pos.y = min_distance
   elseif pos.y > game.WindowResolution.y - dim.y - bubble_style.padding.top - bubble_style.padding.bottom - min_distance then
+
   elseif pos.y > game.WindowResolution.y - bubble_dim.y - min_distance then
     pos.y = game.WindowResolution.y - dim.y - bubble_style.padding.top - bubble_style.padding.bottom - min_distance
+
     pos.y = game.WindowResolution.y - bubble_dim.y - min_distance
 
   end
 
   end
  
Line 333: Line 355:
 
     x = pos.x,
 
     x = pos.x,
 
     y = pos.y,
 
     y = pos.y,
     width = dim.x + bubble_style.padding.right + bubble_style.padding.left,
+
     width = bubble_dim.x,
     height = dim.y + bubble_style.padding.top + bubble_style.padding.bottom
+
     height = bubble_dim.y
 
   }
 
   }
 
   local nine_rect = {
 
   local nine_rect = {
Line 348: Line 370:
 
   -- Get pointer graphic
 
   -- Get pointer graphic
 
   local pointer = nil
 
   local pointer = nil
   local plus = 0
+
   local pointer_offset = 0
 
 
  -- If bubble alignment is set to "left" or "right", the actual facing direction is irrelevant
 
  if bubble_style.align_h == "left" then
 
    char_facing = "left"
 
  elseif bubble_style.align_h == "right" then
 
    char_facing = "right"
 
  end
 
  
 
   if char_facing == "left" then
 
   if char_facing == "left" then
Line 363: Line 378:
 
       if bubble_style.file_pointer_top_right ~= nil and bubble_style.file_pointer_top_right ~= "" then
 
       if bubble_style.file_pointer_top_right ~= nil and bubble_style.file_pointer_top_right ~= "" then
 
         pointer = graphics.loadFromFile(bubble_style.file_pointer_top_right)
 
         pointer = graphics.loadFromFile(bubble_style.file_pointer_top_right)
         plus = bubble_style.pointer_top_right_offset_x
+
         pointer_offset = -bubble_style.pointer_top_right_offset_x
 
       end
 
       end
 
     else
 
     else
Line 369: Line 384:
 
       if bubble_style.file_pointer_bottom_right ~= nil and bubble_style.file_pointer_bottom_right ~= "" then
 
       if bubble_style.file_pointer_bottom_right ~= nil and bubble_style.file_pointer_bottom_right ~= "" then
 
         pointer = graphics.loadFromFile(bubble_style.file_pointer_bottom_right)
 
         pointer = graphics.loadFromFile(bubble_style.file_pointer_bottom_right)
         plus = bubble_style.pointer_bottom_right_offset_x
+
         pointer_offset = -bubble_style.pointer_bottom_right_offset_x
 
       end
 
       end
 
     end
 
     end
Line 378: Line 393:
 
       if bubble_style.file_pointer_top_left ~= nil and bubble_style.file_pointer_top_left ~= "" then
 
       if bubble_style.file_pointer_top_left ~= nil and bubble_style.file_pointer_top_left ~= "" then
 
         pointer = graphics.loadFromFile(bubble_style.file_pointer_top_left)
 
         pointer = graphics.loadFromFile(bubble_style.file_pointer_top_left)
         plus = bubble_style.pointer_top_left_offset_x
+
         pointer_offset = bubble_style.pointer_top_left_offset_x
 
       end
 
       end
 
     else
 
     else
Line 384: Line 399:
 
       if bubble_style.file_pointer_bottom_left ~= nil and bubble_style.file_pointer_bottom_left ~= "" then
 
       if bubble_style.file_pointer_bottom_left ~= nil and bubble_style.file_pointer_bottom_left ~= "" then
 
         pointer = graphics.loadFromFile(bubble_style.file_pointer_bottom_left)
 
         pointer = graphics.loadFromFile(bubble_style.file_pointer_bottom_left)
         plus = bubble_style.pointer_bottom_left_offset_x
+
         pointer_offset = bubble_style.pointer_bottom_left_offset_x
 
       end
 
       end
 
     end
 
     end
Line 392: Line 407:
 
   if pointer ~= nil then
 
   if pointer ~= nil then
 
     -- Calculate pointer position
 
     -- Calculate pointer position
     local pointer_x = 0
+
     local pointer_pos = {x = 0, y = 0}
    local pointer_y = 0
 
  
     if bubble_style.align_h == "left" or (bubble_style.align_h == "char_facing" and char_facing == "left") then
+
     if bubble_style.align_h == "right" or (bubble_style.align_h == "char_facing" and char_facing == "right") then
       -- if bubble is aligned left, pointer is positioned rightmost
+
       -- if bubble is aligned right, pointer is positioned leftmost
       pointer_x = pos.x + dim.x + bubble_style.padding.right + bubble_style.padding.left - pointer.width + plus
+
       pointer_pos.x = pos.x + pointer_offset
  
 
       -- Fix position, if you're too close to the edge
 
       -- Fix position, if you're too close to the edge
       if pointer_x > char.Position.x - game.ScrollPosition.x + plus then
+
       if pointer_pos.x < char.Position.x - game.ScrollPosition.x + pointer_offset then
         pointer_x = char.Position.x - game.ScrollPosition.x + plus
+
         pointer_pos.x = char.Position.x - game.ScrollPosition.x + pointer_offset
 
       end
 
       end
     elseif bubble_style.align_h == "right" or (bubble_style.align_h == "char_facing" and char_facing == "right") then
+
     elseif bubble_style.align_h == "left" or (bubble_style.align_h == "char_facing" and char_facing == "left") then
       -- if bubble is aligned right, pointer is positioned leftmost
+
       -- if bubble is aligned left, pointer is positioned rightmost
       pointer_x = pos.x + plus
+
       pointer_pos.x = pos.x + bubble_dim.x - pointer.width + pointer_offset
  
 
       -- Fix position, if you're too close to the edge
 
       -- Fix position, if you're too close to the edge
       if pointer_x < char.Position.x - game.ScrollPosition.x + plus then
+
       if pointer_pos.x > char.Position.x - game.ScrollPosition.x + pointer_offset then
         pointer_x = char.Position.x - game.ScrollPosition.x + plus
+
         pointer_pos.x = char.Position.x - game.ScrollPosition.x + pointer_offset
 
       end
 
       end
 
     else -- center
 
     else -- center
 
       -- if bubble is aligned center, pointer is positioned at character
 
       -- if bubble is aligned center, pointer is positioned at character
       pointer_x = char.Position.x - game.ScrollPosition.x + plus
+
       pointer_pos.x = char.Position.x - game.ScrollPosition.x + pointer_offset
 
     end
 
     end
  
 
     if bubble_style.align_v == "bottom" then
 
     if bubble_style.align_v == "bottom" then
 
       -- pointer on top when bubble aligned to bottom
 
       -- pointer on top when bubble aligned to bottom
       pointer_y = pos.y - pointer.height + bubble_style.pointer_top_offset_y
+
       pointer_pos.y = pos.y - pointer.height + bubble_style.pointer_top_offset_y
 
     else
 
     else
 
       -- pointer on bottom when bubble aligned to top
 
       -- pointer on bottom when bubble aligned to top
       pointer_y = pos.y + dim.y + bubble_style.padding.top + bubble_style.padding.bottom - bubble_style.pointer_bottom_offset_y
+
       pointer_pos.y = pos.y + bubble_dim.y - bubble_style.pointer_bottom_offset_y
 
     end
 
     end
  
     pointer.position = {x = pointer_x, y = pointer_y}
+
     pointer.position = {x = pointer_pos.x, y = pointer_pos.y}
  
 
     -- Draw the pointer
 
     -- Draw the pointer
Line 431: Line 445:
  
 
   -- Draw the text line by line
 
   -- Draw the text line by line
   for k,v in ipairs(lines) do  
+
  graphics.font = char.Font
     local tempdim = graphics.fontDimension(v)
+
 
     local text_x = 0
+
   for k, line in ipairs(lines) do  
 +
     local tempdim = graphics.fontDimension(line)
 +
     local text_pos = {x = 0, y = 0}
  
 
     if bubble_style.text_align == "left" then
 
     if bubble_style.text_align == "left" then
       text_x = math.floor(pos.x + bubble_style.padding.left)
+
       text_pos.x = pos.x + bubble_style.padding.left
 
     elseif bubble_style.text_align == "right" then
 
     elseif bubble_style.text_align == "right" then
       text_x = math.floor(pos.x + bubble_style.padding.left + dim.x - tempdim.x)
+
       text_pos.x = pos.x + bubble_style.padding.left + text_dim.x - tempdim.x
 
     else -- center
 
     else -- center
       text_x = math.floor(pos.x + bubble_style.padding.left + dim.x / 2 - tempdim.x / 2)
+
       text_pos.x = pos.x + bubble_style.padding.left + (text_dim.x - tempdim.x) / 2
 
     end
 
     end
 
+
   
     graphics.drawFont(v,
+
     text_pos.y = pos.y + bubble_style.padding.top + (k - 1) * (char.Font.Size + char.Font.VerticalLetterSpacing)
      text_x,
+
      math.floor(pos.y + bubble_style.padding.top + (k - 1) * (char.Font.Size + bubble_style.linesgap)),
+
    graphics.drawFont(line,
 +
      math.floor(text_pos.x),
 +
      math.floor(text_pos.y),
 
       1.0
 
       1.0
 
     )
 
     )
Line 464: Line 482:
 
! style="text-align:left" | Name !! style="text-align:left" | Description
 
! style="text-align:left" | Name !! style="text-align:left" | Description
 
|-
 
|-
| [[media:argo_bubbles_2.1.1.zip|argo_bubbles_2.1.1.zip]] || A working example of the script in action. ''Visionaire Studio 5.1.9.2+'' required to run the included .ved file(s).
+
| [[media:argo_bubbles_2.1.2.zip|argo_bubbles_2.1.2.zip]] || A working example of the script in action. ''Visionaire Studio 5.1.9.2+'' required to run the included .ved file(s).
 
|}{{toc}}
 
|}{{toc}}

Revision as of 14:42, 9 September 2022

Name Type By
Argo Speech Bubbles Definition The Argonauts

This script allows you to turn texts spoken by characters into speech bubbles. It has been developed for Visionaire Studio 5.x.


Quick note: The "Argo Bubbles" script is based on the "Advanced Speechbubble Script" by Sebastian aka turbomodus.

Instructions

1. Add the main script to the Visionaire Studio Script Editor & set the script as a definition script.

2. Create the bubble graphics & add them into the gui subfolder of your projects root folder - if the gui folder doesn't already exist, create it.

The font used in the speech bubbles is the regular font defined in Visionaire for a specific character, so you may have to adjust that to match your bubble design. The bubble itself is made of up to five graphic files: one for the main bubble and optionally up to four different pointers (for different directions).

Since the dimensions (width, height) of a speech bubble depend on the length of the text and the number of lines, there are limitations to what the bubbles can look like. It is not possible with this script to have elliptical or fancy-shaped bubbles. They need to be more or less rectangular, but may have rounded or chamfered corners. That's because the script splits the main bubble graphic into 9 tiles which are individually scaled and then recombined to match the size of the text.

Argo bubbles ninerect.png
  • The four corners (1, 3, 7, 9) will keep their look and size
  • The horizontal borders (2, 8) will get stretched/shrinked to the required width
  • The vertical borders (4, 6) will get stretched/shrinked to the required height
  • The center tile (5) is stretched/shrinked horizontally and vertically to fill the bubble


You usually want to have a pointer attached to the bubble, pointing to the character who is currently speaking. Depending on the direction the character is facing, the script either uses the right-pointing or the left-pointing pointer graphics. So you should at least provide two graphic files.

You also have the option of positioning the speech bubble above the character (that's the default, where the pointer is attached to the bottom of the bubble) or below the character's head (with the pointer attached to the top of the bubble and pointing upwards). If you make use of both possibilities (through the custom bubble style option of the script), you must provide four different pointers.

Argo bubbles pointers.png

However, you don't have to use pointers at all. Just set all pointer path options to an empty string " " to get a bubble without pointer.

3. The script offers several options to adjust the speech bubbles to your needs. The options are commented with explanations inside the script.

One main feature is the ability to define as many bubble styles as you like. You could have a different bubble color for each character, for example. Or depending on where a NPC stands, that character might need a different positioning of the bubble than the default one. You can even define multiple styles for the same character. It is then possible to choose the appropriate style by setting a Visionaire value. Please read the comments inside the script on how to achieve this.

The last options are related to the events which the bubble creation is bound to: "textStarted" and "textStopped". Remember that you can register each type of event handler only once in a Visionaire project. So if you have already registered an event handler for the "textStarted" and/or the "textStopped" event, you can't do it again in this bubble script. Instead you have to put all your functions that use the same event handler into one function and bind that one to the event. The Argo Bubble script offers a convenient way to do this without messing up the main code.

Tutorial

For a deeper understanding of how the speech bubbles are created and how they replace the text displayed by Visionaire, please have a look at the tutorial (it is included in the zip file). You don't have to read it, if you just want to use the script in your project. Everything you need to know is explained in the script.

Main Script

--[[

Argo Bubbles
------------
Speech bubble script for Visionaire Studio 5.

Author:          The Argonauts
Version:         2.1.2
Date:            2022-09-09
Play our games:  https://the-argonauts.itch.io/

based on (with permission):
Advanced Speechbubble Script [v1.1] (10/13/2018) -- Written by TURBOMODUS


For a description on how to use it, please visit the official Visionaire wiki (download includes a demo project and a tutorial): https://wiki.visionaire-tracker.net/wiki/Compiled_Index_of_Lua_Scripts_for_Visionaire_Studio



MIT License

Copyright 2022 The Argonauts

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

]]


-- GENERAL SETTINGS
local min_distance = 15 -- minimum distance a bubble has to keep from edge of screen


-- DEFAULT BUBBLE STYLE
local default_style = {
  align_h = "center", -- horizontal alignment of bubble in relation to character (center|left|right|char_facing)
  align_v = "top", -- vertical alignment of bubble in relation to character (top|bottom), defines position of pointer
  bubble_offset_x = 0, -- horizontal bubble offset (positive: move in facing direction)
  bubble_offset_y = -25, -- vertical bubble offset (positive: move downwards)
  color = 0xffffff, -- color tint for bubble and pointer (set to white for none: 0xffffff)

  text_align = "center", -- alignment of multi-line text inside the bubble (center|left|right)
  padding = {
    top = 15,
    right = 20,
    bottom = 12,
    left = 20
  }, -- distance between text block and outer edge of bubble
  
  file_bubble = "vispath:gui/bubble.png", -- path to ninerect bubble graphic
  ninerect_x = 20, -- width of top left corner in ninerect bubble graphic
  ninerect_y = 15, -- height of top left corner in ninerect bubble graphic
  ninerect_width = 30, -- width of center part of ninerect bubble graphic
  ninerect_height = 20, -- height of center part of ninerect bubble graphic
  
  -- Set the "file_pointer..." paths to "" for bubbles without pointers
  file_pointer_bottom_right = "vispath:gui/bubble_pointer_br.png", -- path to -> pointer at bubble bottom
  file_pointer_bottom_left = "vispath:gui/bubble_pointer_bl.png", -- path to <- pointer at bubble bottom
  pointer_bottom_right_offset_x = 20, -- x offset of -> pointer at bubble bottom (positive: move in facing direction)
  pointer_bottom_left_offset_x = 0, -- x offset of <- pointer at bubble bottom (positive: move in facing direction)
  pointer_bottom_offset_y = 3, -- y offset of pointer at bubble bottom (positive: move inwards)
  
  file_pointer_top_right = "", -- path to -> pointer at bubble top
  file_pointer_top_left = "", -- path to <- pointer at bubble top
  pointer_top_right_offset_x = 0, -- x offset of -> pointer at top (positive: move in facing direction)
  pointer_top_left_offset_x = 0, -- x offset of <- pointer at top (positive: move in facing direction)
  pointer_top_offset_y = 0 -- y offset of pointer at bubble top (positive: move inwards)
}


-- CUSTOM BUBBLE STYLES
--[[
Add custom bubble styles to the "custom_styles" table. You can override all properties of the default style. Undefined properties will fallback to default.

Each custom bubble style is defined for a specific character. Add the name of the character as an additional "char" property. If you want the character to use the default style by default and use the custom style only in certain situations (or have several custom styles for him), read the following paragraph.

You can define multiple bubble styles per character. Add a Visionaire value called "argo_bubble" to the character to set the desired bubble style in-game. Counting starts with 1. If you set this value to a number that doesn't correspond to a custom style (e.g. 0), the default style will be used. If you don't add this value at all, the first bubble style definition for this character will be used if present. If you have only one custom style defined for a character and want to use only that, you don't need to add the Visionaire value.

Example:
local custom_styles = {
  {
    char = "Hero",
    align_h = "left",
    align_v = "bottom",
    bubble_offset_x = 40,
    bubble_offset_y = 80,
    file_pointer_top_right = "vispath:gui/bubble_pointer_tr.png",
    pointer_top_offset_y = 10
  },
  {
    ... next style definition here
  }
}

]]

local custom_styles = {}


-- BIND TO HANDLERS
-- Set to false, if you are using the "textStarted" and "textStopped" event handlers in another script.
-- You'll then have to call "show_argo_bubble(text)" AND "destroy_argo_bubble(text)" over there.
local ab_bind_to_handlers = true

-- OPTIONAL: CALL EXTERNAL FUNCTIONS FOR THE "textStarted" EVENT HANDLER
function ab_on_text_started(text)
  -- add your functions here

end

-- OPTIONAL: CALL EXTERNAL FUNCTIONS FOR THE "textStopped" EVENT HANDLER
function ab_on_text_stopped(text)
  -- add your functions here

end





--------------------------------------------------------
-- USUALLY NO NEED TO CHANGE ANYTHING BELOW THIS LINE --
--------------------------------------------------------


-- Build new custom styles table "c_styles" and fill up with default values
local c_styles = {}

if custom_styles ~= nil then
  for key, styles in pairs(custom_styles) do
    local num_char_styles = 1

    if c_styles[ styles["char"] ] ~= nil then
      num_char_styles = #c_styles[ styles["char"] ] + 1
      c_styles[ styles["char"] ][num_char_styles] = {}
    else
      c_styles[ styles["char"] ] = {{}}
    end

    for k, v in pairs(default_style) do
      c_styles[ styles["char"] ][num_char_styles][k] = v
    end

    for k, v in pairs(styles) do
      c_styles[ styles["char"] ][num_char_styles][k] = v
    end
  end
end

-- EVENT HANDLERS
local bubbles = {}

function show_argo_bubble(text)
  -- Add current text to the bubbles table and prevent displaying
  if text.Owner:getId().tableId == eCharacters then
    bubbles[text:getId().id] = {
      Text = text.CurrentText,
      Owner = text.Owner:getName(),
      Background = text.Background
    }

    text.CurrentText = ""
  end

  -- Call external functions that use the "textStarted" event handler
  if ab_on_text_started ~= nil then
    ab_on_text_started(text)
  end
end

function destroy_argo_bubble(text)
  -- Remove current text from bubbles table
  bubbles[text:getId().id] = nil

  -- Call external functions that use the "textStopped" event handler
  if ab_on_text_stopped ~= nil then
    ab_on_text_stopped(text)
  end
end

if ab_bind_to_handlers then
  registerEventHandler("textStarted","show_argo_bubble")
  registerEventHandler("textStopped","destroy_argo_bubble")
end


-- DRAW FUNCTIONS

-- Draw background texts from bubbles table below interfaces (and cursors)
function bubble_below_interface()
  for key, val in pairs(bubbles) do
    if val.Background then
      create_bubble(key, val)
    end
  end
end

-- Draw non-background texts from bubbles table above interfaces
function bubble_above_interface()
  for key, val in pairs(bubbles) do
    if not val.Background then
      create_bubble(key, val)
    end
  end
end

-- Main bubble function
function create_bubble(key, val)
  -- Get talking character
  local char = Characters[val.Owner]
  local pos = graphics.getCharacterTextPosition(char)

  -- Use default bubble style or custom style depending on talking character
  local bubble_style = default_style

  if c_styles ~= nil and c_styles[char.name] ~= nil then
    if Characters[char.name].Values["argo_bubble"] ~= nil then
      if c_styles[char.name][ Characters[char.name].Values["argo_bubble"].Int ] ~= nil then
        bubble_style = c_styles[char.name][ Characters[char.name].Values["argo_bubble"].Int ]
      end
    else
      bubble_style = c_styles[char.name][1]
    end
  end

  -- Facing direction (if bubble alignment is set to "left" or "right", the actual facing direction is irrelevant)
  local char_facing = "right"
  
  if bubble_style.align_h == "left" then
    char_facing = "left"
  elseif bubble_style.align_h ~= "right" then
    if char.Direction > 90 and char.Direction < 270 then
      char_facing = "left"
    end
  end

  -- Calculate the text and bubble dimensions (width, height)
  local txt = val.Text:gsub("<br/"..">", "\n")
  local lines = graphics.performLinebreaks(txt)
  local text_dim = {x = 0, y = 0}
  local bubble_dim = {x = 0, y = 0}

  for k, line in ipairs(lines) do 
    local tempdim = graphics.fontDimension(line)
    
    if text_dim.x < tempdim.x then
      text_dim.x = tempdim.x
    end 
  end

  text_dim.y = #lines * (char.Font.Size + char.Font.VerticalLetterSpacing) - char.Font.VerticalLetterSpacing

  bubble_dim.x = text_dim.x + bubble_style.padding.right + bubble_style.padding.left
  bubble_dim.y = text_dim.y + bubble_style.padding.top + bubble_style.padding.bottom

  -- Calculate the bubble position
  if bubble_style.align_h == "right" or (bubble_style.align_h == "char_facing" and char_facing == "right") then
    pos.x = pos.x - game.ScrollPosition.x
  elseif bubble_style.align_h == "left" or (bubble_style.align_h == "char_facing" and char_facing == "left") then
    pos.x = pos.x - game.ScrollPosition.x - bubble_dim.x
  else -- center
    pos.x = pos.x - game.ScrollPosition.x - bubble_dim.x / 2
  end

  if char_facing == "left" then
    pos.x = pos.x - bubble_style.bubble_offset_x
  else -- right
    pos.x = pos.x + bubble_style.bubble_offset_x
  end

  if pos.x < min_distance then
    pos.x = min_distance
  elseif pos.x > game.WindowResolution.x - bubble_dim.x - min_distance then
    pos.x = game.WindowResolution.x - bubble_dim.x - min_distance
  end

  if bubble_style.align_v == "bottom" then
    pos.y = pos.y - game.ScrollPosition.y + bubble_style.bubble_offset_y
  else -- top
    pos.y = pos.y - game.ScrollPosition.y - bubble_dim.y + bubble_style.bubble_offset_y
  end

  if pos.y < min_distance then
    pos.y = min_distance
  elseif pos.y > game.WindowResolution.y - bubble_dim.y - min_distance then
    pos.y = game.WindowResolution.y - bubble_dim.y - min_distance
  end

  -- Get bubble graphic and define ninerect geometry
  local sprite = graphics.loadFromFile(bubble_style.file_bubble)
  local dest_rect = {
    x = pos.x,
    y = pos.y,
    width = bubble_dim.x,
    height = bubble_dim.y
  }
  local nine_rect = {
    x = bubble_style.ninerect_x,
    y = bubble_style.ninerect_y,
    width = bubble_style.ninerect_width,
    height = bubble_style.ninerect_height
  }

  -- Draw the bubble
  graphics.drawSpriteWithNineRect(sprite, dest_rect, nine_rect, bubble_style.color, 1.0)

  -- Get pointer graphic
  local pointer = nil
  local pointer_offset = 0

  if char_facing == "left" then
    -- right pointer when char facing left
    if bubble_style.align_v == "bottom" then
      -- top pointer when bubble aligned to bottom
      if bubble_style.file_pointer_top_right ~= nil and bubble_style.file_pointer_top_right ~= "" then
        pointer = graphics.loadFromFile(bubble_style.file_pointer_top_right)
        pointer_offset = -bubble_style.pointer_top_right_offset_x
      end
    else
      -- bottom pointer when bubble aligned to top
      if bubble_style.file_pointer_bottom_right ~= nil and bubble_style.file_pointer_bottom_right ~= "" then
        pointer = graphics.loadFromFile(bubble_style.file_pointer_bottom_right)
        pointer_offset = -bubble_style.pointer_bottom_right_offset_x
      end
    end
  else
    -- left pointer when char facing right
    if bubble_style.align_v == "bottom" then
      -- top pointer when bubble aligned to bottom
      if bubble_style.file_pointer_top_left ~= nil and bubble_style.file_pointer_top_left ~= "" then
        pointer = graphics.loadFromFile(bubble_style.file_pointer_top_left)
        pointer_offset = bubble_style.pointer_top_left_offset_x
      end
    else
      -- bottom pointer when bubble aligned to top
      if bubble_style.file_pointer_bottom_left ~= nil and bubble_style.file_pointer_bottom_left ~= "" then
        pointer = graphics.loadFromFile(bubble_style.file_pointer_bottom_left)
        pointer_offset = bubble_style.pointer_bottom_left_offset_x
      end
    end
  end

  -- Continue with pointer, if graphic has been defined
  if pointer ~= nil then
    -- Calculate pointer position
    local pointer_pos = {x = 0, y = 0}

    if bubble_style.align_h == "right" or (bubble_style.align_h == "char_facing" and char_facing == "right") then
      -- if bubble is aligned right, pointer is positioned leftmost
      pointer_pos.x = pos.x + pointer_offset

      -- Fix position, if you're too close to the edge
      if pointer_pos.x < char.Position.x - game.ScrollPosition.x + pointer_offset then
        pointer_pos.x = char.Position.x - game.ScrollPosition.x + pointer_offset
      end
    elseif bubble_style.align_h == "left" or (bubble_style.align_h == "char_facing" and char_facing == "left") then
      -- if bubble is aligned left, pointer is positioned rightmost
      pointer_pos.x = pos.x + bubble_dim.x - pointer.width + pointer_offset

      -- Fix position, if you're too close to the edge
      if pointer_pos.x > char.Position.x - game.ScrollPosition.x + pointer_offset then
        pointer_pos.x = char.Position.x - game.ScrollPosition.x + pointer_offset
      end
    else -- center
      -- if bubble is aligned center, pointer is positioned at character
      pointer_pos.x = char.Position.x - game.ScrollPosition.x + pointer_offset
    end

    if bubble_style.align_v == "bottom" then
      -- pointer on top when bubble aligned to bottom
      pointer_pos.y = pos.y - pointer.height + bubble_style.pointer_top_offset_y
    else
      -- pointer on bottom when bubble aligned to top
      pointer_pos.y = pos.y + bubble_dim.y - bubble_style.pointer_bottom_offset_y
    end

    pointer.position = {x = pointer_pos.x, y = pointer_pos.y}

    -- Draw the pointer
    graphics.drawSprite(pointer, 1.0, bubble_style.color)
  end

  -- Draw the text line by line
  graphics.font = char.Font

  for k, line in ipairs(lines) do 
    local tempdim = graphics.fontDimension(line)
    local text_pos = {x = 0, y = 0}

    if bubble_style.text_align == "left" then
      text_pos.x = pos.x + bubble_style.padding.left
    elseif bubble_style.text_align == "right" then
      text_pos.x = pos.x + bubble_style.padding.left + text_dim.x - tempdim.x
    else -- center
      text_pos.x = pos.x + bubble_style.padding.left + (text_dim.x - tempdim.x) / 2
    end
    
    text_pos.y = pos.y + bubble_style.padding.top + (k - 1) * (char.Font.Size + char.Font.VerticalLetterSpacing)
 
    graphics.drawFont(line,
      math.floor(text_pos.x),
      math.floor(text_pos.y),
      1.0
    )
  end
end -- end of bubble function



-- ADD DRAW FUNCTIONS TO THE RENDERING
graphics.addDrawFunc("bubble_below_interface()", 0)
graphics.addDrawFunc("bubble_above_interface()", 1)


Resources

Name Description
argo_bubbles_2.1.2.zip A working example of the script in action. Visionaire Studio 5.1.9.2+ required to run the included .ved file(s).