Difference between revisions of "Argo Bubbles"

From The Official Visionaire Studio: Adventure Game Engine Wiki
Line 3: Line 3:
 
! style="text-align:left" | Name !! style="text-align:left" | Type !! style="text-align:left" | By
 
! style="text-align:left" | Name !! style="text-align:left" | Type !! style="text-align:left" | By
 
|-
 
|-
| Argo Speech Bubbles || Definition || The Argonauts
+
| Argo Bubbles - Dynamic stylized speech bubbles || Definition || The Argonauts
 
|}
 
|}
  
Line 10: Line 10:
 
{| class="toccolours mw-collapsible mw-collapsed ts qntoggle"
 
{| class="toccolours mw-collapsible mw-collapsed ts qntoggle"
 
|-
 
|-
| colspan="2" | ''Quick note: The "Argo Bubbles" script is based on the "Advanced Speechbubble Script" by Sebastian aka [https://www.youtube.com/c/turbomodus turbomodus].''
+
| colspan="2" | ''The "Argo Bubbles" script is based on the "Advanced Speechbubble Script" by Sebastian aka [https://www.youtube.com/c/turbomodus turbomodus].''
 
|-
 
|-
 
| style="text-align:center;vertical-align:middle;" | <youtube width="400">wV6Fh_baPkk</youtube> || style="text-align:center;" | <youtube width="400">dn_X6OsozZI</youtube>
 
| style="text-align:center;vertical-align:middle;" | <youtube width="400">wV6Fh_baPkk</youtube> || style="text-align:center;" | <youtube width="400">dn_X6OsozZI</youtube>
Line 19: Line 19:
 
== Instructions ==
 
== Instructions ==
  
1. Add the [[#Main_Script|main script]] to the Visionaire Studio Script Editor & set the script as a definition script.
+
[[File:Argo_Bubbles_-_Example.png|thumb]]
  
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.
+
1. Add the [[#Main_Script|main script]] to the Visionaire Studio Script Editor and set the script as a definition script.
  
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).
+
2. Create the bubble graphics and add them to your project folder or any subfolder.
  
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.
+
3. Adjust the settings at the top of the script (between "DEFINE YOUR SETTINGS HERE" and "USUALLY NO NEED TO CHANGE ANYTHING BELOW THIS LINE").
  
<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/>
+
== Description ==
* 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 script is extensively commented, giving you all the information you need to make use of its features. If anything is unclear, you may find the answer in this description.
  
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.
+
=== How the Argo Bubbles are created ===
  
<div style="text-align:center;margin:30px 0">[[File:Argo_bubbles_pointers.png|1000px]]</div>
+
* Display of the regular character text is prevented. The text is stored internally to get integrated in the speech bubble. The font used in the bubble will stay the same though - it is the regular font defined in the editor for a specific character. Choose a font for the character that fits your speech bubble design.
 +
* The main bubble graphic is cut into nine pieces which are individually stretched and then reassembled to match the calculated size of the bubble (see the graphic below). This "ninerect" technique ensures that the corners don't get distorted, but leads to limitations to what the speech 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.
 +
* You usually want to have a pointer attached to the bubble, pointing at the character who is currently speaking. Depending on your alignment settings and on the direction the character is facing, the script selects one of up to four pointer graphics you can provide and places it accordingly. Pointers are optional though.
 +
* You have the option to add an additional image to the bubble. It is intended for a portrait picture of the character, but you can choose any image you like, of course.
  
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.
 
  
3. The script offers several options to adjust the speech bubbles to your needs. The options are commented with explanations inside the script.
+
[[File:Argo_Bubbles_-_How_the_bubbles_are_created.png|thumb|center|800px]]
  
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.
+
=== Settings ===
 +
 
 +
==== General settings ====
 +
 
 +
* <span class="inlinecode">enable_shader_viewport_adjustments :</span> If set to true, the script will take viewport manipulations by Visionaire's Shader Toolkit into account. If you don't make use of the Shader Toolkit, you can set this option to false.
 +
 
 +
 
 +
==== Bubble style ====
 +
 
 +
* <span class="inlinecode">align_h:</span> The horizontal alignment of the bubble in relation to the character. Possible values are (center|left|right|char_facing). The "char_facing" value results in a left/right aligned bubble, respectively, depending on the character's facing direction.
 +
* <span class="inlinecode">align_v:</span> The vertical alignment of the bubble in relation to the character. Possible values are (top|bottom). This setting also defines the vertical positioning of the bubble pointer: it is attached to the bottom of the bubble, if the bubble is top-aligned, and vice versa.
 +
* <span class="inlinecode">flip_v_align_on_edge:</span> If set to true, a top-aligned bubble will automatically turn into a bottom-aligned bubble (and vice versa) when the character gets too close to the top (bottom) edge of the screen.
 +
* <span class="inlinecode">bubble_offset_x:</span> The horizontal offset of the bubble from the initial position centered above the character's head. Positive values move the bubble in the character's facing direction.
 +
* <span class="inlinecode">bubble_top_offset_y:</span> The vertical offset of the bubble for top-aligned bubbles. Positive values move the bubble up.
 +
* <span class="inlinecode">bubble_bottom_offset_y:</span> The vertical offset of the bubble for bottom-aligned bubbles. Positive values move the bubble down.
 +
* <span class="inlinecode">adjust_v_offset_to_char_scale:</span> If set to true, the current character scaling is taken into account when applying the vertical bubble offset. The offset (as defined in "bubble_top_offset_y" or "bubble_bottom_offset_y") will be reduced/increased according to the character scale.
 +
* <span class="inlinecode">color:</span> Color tint for the bubble (and pointer). Please note that this option takes the color in hexadecimal BGR notation (instead of RGB), prepended with "0x". The default value of 0xffffff (white) results in no tint.
 +
* <span class="inlinecode">text_align_h:</span> The horizontal text alignment inside the bubble. Possible values are (center|left|right). This option has a visible effect only if it's multiline text and/or if the bubble has a fixed width.
 +
* <span class="inlinecode">text_align_v:</span> The vertical text alignment inside the bubble. Possible values are (center|top|bottom).  This option has a visible effect only if the bubble has a fixed height.
 +
* <span class="inlinecode">padding:</span> The distance between the text block and the outer edge of the bubble, defined in a table {"top", "right", "bottom", "left"}.
 +
* <span class="inlinecode">fixed_width:</span> The fixed width of the bubble. Set the value to 0 to not set a fixed width.
 +
* <span class="inlinecode">fixed_height:</span> The fixed height of the bubble. Set the value to 0 to not set a fixed height.
 +
* <span class="inlinecode">fixed_pos:</span> A fixed position on the screen to show the bubble at, defined in a table {"x", "y"}. That position will serve as the root point of the position calculation (it substitutes the character's position), so it depends on your alignment settings where the bubble will be placed. Don't set "align_h" to "char_facing" when using the fixed position, because the bubble position would change depending on the character's facing direction.
 +
* <span class="inlinecode">expand_fixed_dims:</span> If set to true, a fixed bubble width/height will expand if the text doesn't fit in the fix-sized bubble. The fixed width/height will act like a min-width/min-height setting.
 +
* <span class="inlinecode">file_bubble:</span> The path to the main bubble graphic. The graphic needs to be suited for the "ninerect" technique. The filepath must be relative to the project root folder and be prepended with "vispath:".
 +
* <span class="inlinecode">ninerect:</span> The measurements used to cut the "ninerect" bubble graphic, defined in a table {"x", "y", "width, "height"}. The x/y values define the width/height of the top left tile of the "ninerect", the width/height values define the width/height of the center tile (see the graphic below).
 +
* <span class="inlinecode">file_portrait:</span> The path to a portrait image. The filepath must be relative to the project root folder and be prepended with "vispath:". Set the value to "" (empty string) for a bubble without a portrait image.
 +
* <span class="inlinecode">portrait_align_h:</span> The horizontal alignment of the portrait image in the bubble. Possible values are (left|right). There will be no padding between the portrait and the edge of the bubble; the padding setting only applies to the text. Add transparent areas to your portrait image to adjust the position (see the graphic below).
 +
* <span class="inlinecode">portrait_align_v:</span> The vertical alignment of the portrait image in the bubble. Possible values are (top|center|bottom). There will be no padding between the portrait and the edge of the bubble; the padding setting only applies to the text. Add transparent areas to your portrait image to adjust the position (see the graphic below).
 +
* <span class="inlinecode">file_pointer_bottom_right:</span> The path to the down-right-pointing pointer graphic. The filepath must be relative to the project root folder and be prepended with "vispath:". Set the value to "" (empty string) for a bubble without a pointer.
 +
* <span class="inlinecode">file_pointer_bottom_left:</span> The path to the down-left-pointing pointer graphic. The filepath must be relative to the project root folder and be prepended with "vispath:". Set the value to "" (empty string) for a bubble without a pointer.
 +
* <span class="inlinecode">pointer_bottom_right_offset_x:</span> The horizontal offset of the down-right-pointing pointer. Positive values move the pointer in the character's facing direction.
 +
* <span class="inlinecode">pointer_bottom_left_offset_x:</span> The horizontal offset of the down-left-pointing pointer. Positive values move the pointer in the character's facing direction.
 +
* <span class="inlinecode">pointer_bottom_offset_y:</span> The vertical offset of the down-pointing pointer. Positive values move the pointer inwards.
 +
* <span class="inlinecode">file_pointer_top_right:</span> The path to the up-right-pointing pointer graphic. The filepath must be relative to the project root folder and be prepended with "vispath:". Set the value to "" (empty string) for a bubble without a pointer.
 +
* <span class="inlinecode">file_pointer_top_left:</span> The path to the up-left-pointing pointer graphic. The filepath must be relative to the project root folder and be prepended with "vispath:". Set the value to "" (empty string) for a bubble without a pointer.
 +
* <span class="inlinecode">pointer_top_right_offset_x:</span> The horizontal offset of the up-right-pointing pointer. Positive values move the pointer in the character's facing direction.
 +
* <span class="inlinecode">pointer_top_left_offset_x:</span> The horizontal offset of the up-left-pointing pointer. Positive values move the pointer in the character's facing direction.
 +
* <span class="inlinecode">pointer_top_offset_y:</span> The vertical offset of the up-pointing pointer. Positive values move the pointer inwards.
 +
* <span class="inlinecode">pointer_min_distance:</span> The minimum distance a pointer has to keep from the edge of the bubble (see the graphic below).
 +
* <span class="inlinecode">min_distance:</span> The minimum distance the bubble has to keep from the edge of the screen.
 +
 
 +
 
 +
[[File:Argo_Bubbles_-_Style_settings.png|thumb|center|800px]]
 +
 
 +
 
 +
=== Work with multiple speech bubble styles ===
 +
 
 +
A main feature of the "Argo Bubbles" script is the ability to define as many bubble styles as you like. You could have a different bubble color/design 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 and switch between them during the game. The default bubble style is defined in the "default_style" variable.
 +
 
 +
If you would like to add more 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 a character is meant to use only a single style throughout the game (either the default style or a custom one), you don't have to do anything else.
 +
* If you want the character to switch styles during the game, add a Visionaire value called "argo_bubble" to the character. By changing that value you can select the style to be active. A value of 0 selects the default style. A value of 1 selects the first custom style defined for that character, a value of 2 the second custom style for that character etc. You can define as many custom styles per character as you like - just add them to the "custom_styles" table.
  
  
 
== Tutorial ==
 
== Tutorial ==
For a deeper understanding of how the speech bubbles are created and how they replace the text displayed
+
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 <u>don't</u> have to read it, if you just want to use the script in your project. Be aware that the tutorial was created for script version 2.1.2. The basic idea is still the same, but the code has changed a lot since then.
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.
 
<hr>
 
<pdf>File:Creating_Speech_Bubbles_with_the_Argo_Bubbles_Script.pdf</pdf>
 
  
  
Line 69: Line 118:
 
                 with contributions by Mulewa
 
                 with contributions by Mulewa
 
                 and some lines of code taken from the Visionaire Shader Toolkit by Simon Scheckel
 
                 and some lines of code taken from the Visionaire Shader Toolkit by Simon Scheckel
Version:        2.1.5
+
Version:        2.2.0
Date:            2023-02-16
+
Date:            2023-05-07
 
Play our games:  https://the-argonauts.itch.io/
 
Play our games:  https://the-argonauts.itch.io/
  
Line 76: Line 125:
 
Advanced Speechbubble Script [v1.1] (10/13/2018) -- Written by TURBOMODUS
 
Advanced Speechbubble Script [v1.1] (10/13/2018) -- Written by TURBOMODUS
  
 
+
For a detailed description, please visit the official Visionaire wiki: https://wiki.visionaire-tracker.net/wiki/Argo_Bubbles
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
 
  
  
Line 103: Line 151:
 
--[[ GENERAL SETTINGS
 
--[[ GENERAL SETTINGS
  
* min_distance: minimum distance a bubble has to keep from edge of screen
 
 
* enable_shader_viewport_adjustments: enables viewport adjustments (zoom, scale) when using Visionaire's shader toolkit
 
* enable_shader_viewport_adjustments: enables viewport adjustments (zoom, scale) when using Visionaire's shader toolkit
* flip_v_align_on_edge: turns top-aligned bubble into bottom-aligned bubble when too close to the upper edge of screen (and vice versa)
 
 
]]
 
]]
  
local min_distance = 15
 
 
local enable_shader_viewport_adjustments = true
 
local enable_shader_viewport_adjustments = true
local flip_v_align_on_edge = true
 
  
  
Line 117: Line 161:
 
* align_h: horizontal alignment of bubble in relation to character (center|left|right|char_facing)
 
* align_h: horizontal alignment of bubble in relation to character (center|left|right|char_facing)
 
* align_v: vertical alignment of bubble in relation to character (top|bottom), defines position of pointer
 
* align_v: vertical alignment of bubble in relation to character (top|bottom), defines position of pointer
 +
* flip_v_align_on_edge: turns top-aligned bubble into bottom-aligned bubble when too close to edge of screen (and vice versa)
 
* bubble_offset_x: horizontal bubble offset (positive: move in facing direction)
 
* bubble_offset_x: horizontal bubble offset (positive: move in facing direction)
 
* bubble_top_offset_y: vertical bubble offset for top-aligned bubbles (positive: move up)
 
* bubble_top_offset_y: vertical bubble offset for top-aligned bubbles (positive: move up)
Line 122: Line 167:
 
* adjust_v_offset_to_char_scale: adjusts the vertical offset according to current character scaling
 
* adjust_v_offset_to_char_scale: adjusts the vertical offset according to current character scaling
 
* color: color tint for bubble and pointer in BGR notation, not RGB (set to white for none: 0xffffff)
 
* color: color tint for bubble and pointer in BGR notation, not RGB (set to white for none: 0xffffff)
* text_align: alignment of multi-line text inside the bubble (center|left|right)
+
* text_align_h: horizontal alignment inside the bubble (center|left|right)
 +
* text_align_v: vertical alignment inside the bubble (center|top|bottom)
 
* padding: distance between text block and outer edge of bubble
 
* padding: distance between text block and outer edge of bubble
 +
* fixed_width: fixed width of the bubble (set to 0 for variable width)
 +
* fixed_height: fixed height of the bubble (set to 0 for variable height)
 +
* fixed_pos: fixed position of bubble on screen, not related to character (set to false to not define a fixed position)
 +
* expand_fixed_dims: if true, fixed width/height will expand if the text is wider/higher
 
* file_bubble: path to ninerect bubble graphic
 
* file_bubble: path to ninerect bubble graphic
* ninerect_x: width of top left corner in ninerect bubble graphic
+
* ninerect: x/y = width/height of top left corner in ninerect bubble graphic, width/height = width/height of center part
* ninerect_y: height of top left corner in ninerect bubble graphic
+
* file_portrait: path to a portrait image (set to "" for bubble without portrait)
* ninerect_width: width of center part of ninerect bubble graphic
+
* portrait_align_h: horizontal alignment of the portrait image (left|right)
* ninerect_height: height of center part of ninerect bubble graphic
+
* portrait_align_v: vertical alignment of the portrait image (top|center|bottom)
 
* file_pointer_bottom_right: path to right-facing pointer at bubble bottom (set to "" for bubble without pointer)
 
* file_pointer_bottom_right: path to right-facing pointer at bubble bottom (set to "" for bubble without pointer)
 
* file_pointer_bottom_left: path to left-facing pointer at bubble bottom (set to "" for bubble without pointer)
 
* file_pointer_bottom_left: path to left-facing pointer at bubble bottom (set to "" for bubble without pointer)
Line 139: Line 189:
 
* pointer_top_left_offset_x: x offset of left-facing pointer at top (positive: move in facing direction)
 
* pointer_top_left_offset_x: x offset of left-facing pointer at top (positive: move in facing direction)
 
* pointer_top_offset_y: y offset of pointer at bubble top (positive: move inwards)
 
* pointer_top_offset_y: y offset of pointer at bubble top (positive: move inwards)
 +
* pointer_min_distance: minimum distance a pointer has to keep from edge of bubble
 +
* min_distance: minimum distance a bubble has to keep from edge of screen
 
]]
 
]]
  
Line 144: Line 196:
 
   align_h = "center",
 
   align_h = "center",
 
   align_v = "top",
 
   align_v = "top",
 +
  flip_v_align_on_edge = true,
 
   bubble_offset_x = 0,
 
   bubble_offset_x = 0,
 
   bubble_top_offset_y = 25,
 
   bubble_top_offset_y = 25,
Line 149: Line 202:
 
   adjust_v_offset_to_char_scale = true,
 
   adjust_v_offset_to_char_scale = true,
 
   color = 0xffffff,
 
   color = 0xffffff,
   text_align = "center",
+
   text_align_h = "center",
 +
  text_align_v = "center",
 
   padding = { top = 15, right = 20, bottom = 12, left = 20 },
 
   padding = { top = 15, right = 20, bottom = 12, left = 20 },
 +
  fixed_width = 0,
 +
  fixed_height = 0,
 +
  fixed_pos = false,
 +
  expand_fixed_dims = true,
 
   file_bubble = "vispath:gui/bubble.png",
 
   file_bubble = "vispath:gui/bubble.png",
   ninerect_x = 20,
+
   ninerect = { x = 20, y = 15, width = 30, height = 20 },
   ninerect_y = 15,
+
   file_portrait = "",
   ninerect_width = 30,
+
   portrait_align_h = "left",
   ninerect_height = 20,
+
   portrait_align_v = "top",
 
   file_pointer_bottom_right = "vispath:gui/bubble_pointer_br.png",
 
   file_pointer_bottom_right = "vispath:gui/bubble_pointer_br.png",
 
   file_pointer_bottom_left = "vispath:gui/bubble_pointer_bl.png",
 
   file_pointer_bottom_left = "vispath:gui/bubble_pointer_bl.png",
Line 165: Line 223:
 
   pointer_top_right_offset_x = 0,
 
   pointer_top_right_offset_x = 0,
 
   pointer_top_left_offset_x = 0,
 
   pointer_top_left_offset_x = 0,
   pointer_top_offset_y = 0
+
   pointer_top_offset_y = 0,
 +
  pointer_min_distance = 15,
 +
  min_distance = 15
 
}
 
}
  
  
--[[ CUSTOM BUBBLE STYLE(S)
+
--[[ CUSTOM BUBBLE STYLES
  
 
Optional: Add custom bubble styles to the "custom_styles" table. You can override all properties of the default style. Undefined properties will fallback to default.
 
Optional: 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.
+
Each custom bubble style is defined for a specific character. Add the name of the character as an additional "char" property. If a character is meant to use only a single style throughout the game (either the default style or a custom one), you don't have to do anything else.
  
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.
+
If you want the character to switch styles during the game, add a Visionaire value called "argo_bubble" to the character. By changing that value you can select the style to be active. A value of 0 selects the default style. A value of 1 selects the first custom style defined for that character, a value of 2 the second custom style for that character etc. You can define as many custom styles per character as you like.
  
 
Example:
 
Example:
Line 200: Line 260:
 
--[[ ADVANCED SETTINGS
 
--[[ ADVANCED SETTINGS
  
* classic_mode: the classic mode uses the "textStarted" event handler for displaying the speech bubbles; the new mode (classic_mode = false) uses the "textRender" hook function and supports custom pause wildcards in the middle of texts (e.g. "Some text<p2>More text", "Hello<p>world"); if you need the "textRender" hook elsewhere in your project, you may switch back to classic mode
+
* classic_mode: the classic mode uses the "textStarted" event handler for displaying the speech bubbles; the new mode (classic_mode = false) uses the "textRender" hook function and supports custom pause wildcards in the middle of texts (e.g. "Text<p2>Text", "Hello<p>world"); if you need the "textRender" hook elsewhere in your project, you may switch back to classic mode
 
* ab_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.
 
* ab_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.
 
* ab_on_text_started: call external functions for the "textStarted" event handler
 
* ab_on_text_started: call external functions for the "textStarted" event handler
Line 251: Line 311:
 
   end
 
   end
 
end
 
end
 +
  
  
Line 260: Line 321:
 
function show_argo_bubble(text)
 
function show_argo_bubble(text)
 
   if text.Owner:getId().tableId == eCharacters then
 
   if text.Owner:getId().tableId == eCharacters then
     -- Add current character text to the bubbles table
+
     -- Add current character text to the bubbles table, if not already there
     bubbles[text:getId().id] = {
+
     if bubbles[text:getId().id] == nil then
      Text = text.CurrentText,
+
      bubbles[text:getId().id] = {
      Owner = text.Owner:getName(),
+
        Text = text.CurrentText,
      Background = text.Background
+
        Owner = text.Owner:getName(),
    }
+
        Background = text.Background
      
+
      }
 +
     end
 +
 
 
     -- override the default text rendering
 
     -- override the default text rendering
 
     return true
 
     return true
Line 273: Line 336:
 
   return false
 
   return false
 
end
 
end
 +
 +
  
 
-- Classic mode: create bubbles on "textStarted" event
 
-- Classic mode: create bubbles on "textStarted" event
Line 295: Line 360:
 
   end
 
   end
 
end
 
end
 +
 +
  
 
function destroy_argo_bubble(text)
 
function destroy_argo_bubble(text)
Line 305: Line 372:
 
   end
 
   end
 
end
 
end
 +
 +
  
 
-- Call this function to immediately destroy all bubbles
 
-- Call this function to immediately destroy all bubbles
Line 310: Line 379:
 
   bubbles = {}
 
   bubbles = {}
 
end
 
end
 +
 +
  
 
-- Bind to event handlers and register hook function
 
-- Bind to event handlers and register hook function
Line 320: Line 391:
 
   registerHookFunction("textRender", "show_argo_bubble")
 
   registerHookFunction("textRender", "show_argo_bubble")
 
end
 
end
 +
  
  
Line 332: Line 404:
 
   end
 
   end
 
end
 
end
 +
 +
  
 
-- Draw non-background texts from bubbles table above interfaces
 
-- Draw non-background texts from bubbles table above interfaces
Line 341: Line 415:
 
   end
 
   end
 
end
 
end
 +
 +
  
 
-- Main bubble function
 
-- Main bubble function
Line 348: Line 424:
 
   local pos = graphics.getCharacterTextPosition(char)
 
   local pos = graphics.getCharacterTextPosition(char)
  
   -- Shader toolkit viewport adjustments (not fully supported yet)
+
   -- Get bubble style
 +
  local bubble_style = get_style(key, val, char)
 +
 
 +
  -- Get shader toolkit viewport adjustments
 +
  local shader_adjusted_viewport = get_viewport_adjustments(bubble_style)
 +
 
 +
  -- 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
 +
 
 +
  -- Get text and bubble dimensions
 +
  local text_and_bubble_dims = get_text_and_dims(key, val, char, bubble_style)
 +
 
 +
  local text_dim = text_and_bubble_dims.text_dim
 +
  local bubble_dim = text_and_bubble_dims.bubble_dim
 +
 
 +
  -- Get the bubble position
 +
  pos = get_bubble_position(pos, bubble_style, bubble_dim, char, char_facing, shader_adjusted_viewport)
 +
 
 +
  -- Get bubble graphic and define ninerect geometry
 +
  local bubble_sprite
 +
  local dest_rect = { x = pos.x, y = pos.y, width = bubble_dim.x, height = bubble_dim.y }
 +
 
 +
  -- Take sprite from bubbles table, if cached
 +
  if val.bubble_sprite ~= nil then
 +
    bubble_sprite = val.bubble_sprite
 +
  else
 +
    bubble_sprite = graphics.loadFromFile(bubble_style.file_bubble)
 +
 
 +
    -- Cache bubble sprite
 +
    bubbles[key].bubble_sprite = bubble_sprite
 +
  end
 +
 
 +
  -- Draw the bubble
 +
  graphics.drawSpriteWithNineRect(bubble_sprite, dest_rect, bubble_style.ninerect, bubble_style.color, 1.0)
 +
 
 +
  -- Get portrait graphic
 +
  local portrait_sprite = nil
 +
 
 +
  -- Take sprite from bubbles table, if cached
 +
  if val.portrait_sprite ~= nil then
 +
    portrait_sprite = val.portrait_sprite
 +
  elseif bubble_style.file_portrait ~= nil and bubble_style.file_portrait ~= "" then
 +
    portrait_sprite = graphics.loadFromFile(bubble_style.file_portrait)
 +
   
 +
    -- Cache bubble sprite
 +
    bubbles[key].portrait_sprite = portrait_sprite
 +
  end
 +
 
 +
  -- Continue with portrait, if graphic has been defined
 +
  if portrait_sprite ~= nil then
 +
    -- Get position of portrait image
 +
    portrait_sprite.position = get_portrait_position(pos, bubble_style, bubble_dim, portrait_sprite)
 +
 
 +
    -- Draw the portrait
 +
    graphics.drawSprite(portrait_sprite, 1.0)
 +
  end
 +
 
 +
  -- Get pointer graphic
 +
  local pointer_sprite = nil
 +
  local pointer_offset = 0
 +
 
 +
  if char_facing == "left" then
 +
    -- right pointer when char facing left
 +
    if (bubble_style.align_v_temp ~= nil and bubble_style.align_v_temp == "bottom") or (bubble_style.align_v_temp == nil and 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_sprite = 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_sprite = 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_temp ~= nil and bubble_style.align_v_temp == "bottom") or (bubble_style.align_v_temp == nil and 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_sprite = 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_sprite = 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_sprite ~= nil then
 +
    -- Get position of pointer image
 +
    pointer_sprite.position = get_pointer_position(pos, bubble_style, bubble_dim, pointer_offset, char, char_facing, shader_adjusted_viewport, pointer_sprite)
 +
 
 +
    -- Draw the pointer
 +
    graphics.drawSprite(pointer_sprite, 1.0, bubble_style.color)
 +
  end
 +
 
 +
  -- Draw the text
 +
  -- Calculate vertical alignment of text inside bubble (only if fixed height and not at the top)
 +
  local text_box_pos_y = pos.y + bubble_style.padding.top
 +
   
 +
  if bubble_style.fixed_height ~= nil then
 +
    if bubble_style.text_align_v == "center" then
 +
      text_box_pos_y = pos.y + bubble_style.padding.top + (bubble_dim.y - bubble_style.padding.top - bubble_style.padding.bottom - text_dim.y) / 2
 +
    elseif bubble_style.text_align_v == "bottom" then
 +
      text_box_pos_y = pos.y + bubble_dim.y - bubble_style.padding.bottom - text_dim.y
 +
    end
 +
  end
 +
 
 +
  -- Draw the text line by line
 +
  for k, line in ipairs(text_and_bubble_dims.lines) do
 +
    local tempdim = graphics.fontDimension(line)
 +
    local text_pos = {x = 0, y = 0}
 +
 
 +
    if bubble_style.text_align_h == "left" then
 +
      text_pos.x = pos.x + bubble_style.padding.left
 +
    elseif bubble_style.text_align_h == "right" then
 +
      text_pos.x = pos.x + bubble_dim.x - bubble_style.padding.right - tempdim.x
 +
    else -- center
 +
      text_pos.x = pos.x + bubble_style.padding.left + (bubble_dim.x - bubble_style.padding.left - bubble_style.padding.right - tempdim.x) / 2
 +
    end
 +
   
 +
    text_pos.y = text_box_pos_y + (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
 +
 
 +
 
 +
 
 +
-- Get style for the current bubble
 +
function get_style(key, val, char)
 +
  local bubble_style = default_style
 +
 
 +
  -- Take style from bubbles table, if cached
 +
  if val.style ~= nil then
 +
    bubble_style = val.style
 +
  else
 +
    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
 +
 
 +
    -- Cache style
 +
    bubbles[key].style = bubble_style
 +
  end
 +
 
 +
  return bubble_style
 +
end
 +
 
 +
 
 +
 
 +
-- Get shader toolkit viewport adjustments (probably not fully supported yet)
 +
function get_viewport_adjustments(bubble_style)
 
   local shader_adjusted_viewport = {x = 0, y = 0, scale = 1}
 
   local shader_adjusted_viewport = {x = 0, y = 0, scale = 1}
 
    
 
    
   if enable_shader_viewport_adjustments and shader_newViewport ~= nil then
+
   if enable_shader_viewport_adjustments and shader_newViewport ~= nil and bubble_style.fixed_pos == false then
 
     if shader_viewportTransition > 0.0 and shader_viewportTransition < 1.0 then
 
     if shader_viewportTransition > 0.0 and shader_viewportTransition < 1.0 then
 
       shader_adjusted_viewport.scale = (1.0 - shader_viewportTransition) * shader_oldViewport.scale + shader_viewportTransition * shader_newViewport.scale
 
       shader_adjusted_viewport.scale = (1.0 - shader_viewportTransition) * shader_oldViewport.scale + shader_viewportTransition * shader_newViewport.scale
Line 377: Line 628:
 
   end
 
   end
  
   -- Use default bubble style or custom style depending on talking character
+
   return shader_adjusted_viewport
  local bubble_style = default_style
+
end
  
  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)
+
-- Calculate text and bubble dimensions
   local txt = val.Text:gsub("<br/"..">", "\n")
+
function get_text_and_dims(key, val, char, bubble_style)
   local lines = graphics.performLinebreaks(txt)
+
   local txt = ""
 +
   local lines = {}
 
   local text_dim = {x = 0, y = 0}
 
   local text_dim = {x = 0, y = 0}
 
   local bubble_dim = {x = 0, y = 0}
 
   local bubble_dim = {x = 0, y = 0}
Line 409: Line 642:
 
   graphics.font = char.Font
 
   graphics.font = char.Font
  
   for k, line in ipairs(lines) do  
+
   -- Take from bubbles table, if cached
    local tempdim = graphics.fontDimension(line)
+
  if val.text_dim ~= nil then
   
+
    txt = val.txt
    if text_dim.x < tempdim.x then
+
    lines = val.lines
       text_dim.x = tempdim.x
+
    text_dim = val.text_dim
     end  
+
    bubble_dim = val.bubble_dim
 +
  else
 +
    txt = val.Text:gsub("<br/"..">", "\n")
 +
    lines = graphics.performLinebreaks(txt)
 +
 
 +
    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
 +
 
 +
    if bubble_style.fixed_width > 0 then
 +
      if not bubble_style.expand_fixed_dims or bubble_style.fixed_width > bubble_dim.x then
 +
        bubble_dim.x = bubble_style.fixed_width
 +
      end
 +
    end
 +
 
 +
     if bubble_style.fixed_height > 0 then
 +
      if not bubble_style.expand_fixed_dims or bubble_style.fixed_height > bubble_dim.y then
 +
        bubble_dim.y = bubble_style.fixed_height
 +
      end
 +
    end
 +
 
 +
    -- Cache dimensions
 +
    bubbles[key].txt = txt
 +
    bubbles[key].lines = lines
 +
    bubbles[key].text_dim = text_dim
 +
    bubbles[key].bubble_dim = bubble_dim
 
   end
 
   end
  
   text_dim.y = #lines * (char.Font.Size + char.Font.VerticalLetterSpacing) - char.Font.VerticalLetterSpacing
+
   return { lines = lines, text_dim = text_dim, bubble_dim = bubble_dim }
 +
end
  
  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
 +
function get_bubble_position(pos, bubble_style, bubble_dim, char, char_facing, shader_adjusted_viewport)
 +
  -- Change reference point for bubble with fixed position
 +
  if bubble_style.fixed_pos ~= false then
 +
    pos.x = bubble_style.fixed_pos.x + game.ScrollPosition.x
 +
    pos.y = bubble_style.fixed_pos.y + game.ScrollPosition.y
 +
  end
 +
 
 
   -- X position
 
   -- X position
 
   if bubble_style.align_h == "right" or (bubble_style.align_h == "char_facing" and char_facing == "right") then
 
   if bubble_style.align_h == "right" or (bubble_style.align_h == "char_facing" and char_facing == "right") then
Line 438: Line 712:
 
   end
 
   end
  
   if pos.x < min_distance then
+
   if pos.x < bubble_style.min_distance then
     pos.x = min_distance
+
     pos.x = bubble_style.min_distance
   elseif pos.x > game.WindowResolution.x - bubble_dim.x - min_distance then
+
   elseif pos.x > game.WindowResolution.x - bubble_dim.x - bubble_style.min_distance then
     pos.x = game.WindowResolution.x - bubble_dim.x - min_distance
+
     pos.x = game.WindowResolution.x - bubble_dim.x - bubble_style.min_distance
 
   end
 
   end
  
Line 449: Line 723:
  
 
   if bubble_style.adjust_v_offset_to_char_scale then
 
   if bubble_style.adjust_v_offset_to_char_scale then
     char_scale = (char.ScaleFactor / 100)
+
     char_scale = (char.ScaleFactor / 100) * shader_adjusted_viewport.scale
  
 
     if char.Scale then
 
     if char.Scale then
Line 462: Line 736:
 
   end
 
   end
  
   if temp_y < min_distance then
+
   if temp_y < bubble_style.min_distance then
     if flip_v_align_on_edge then
+
     if bubble_style.flip_v_align_on_edge and bubble_style.fixed_pos == false then
 
       bubble_style.align_v_temp = "bottom"
 
       bubble_style.align_v_temp = "bottom"
 
       pos.y = (pos.y - game.ScrollPosition.y - shader_adjusted_viewport.y) * shader_adjusted_viewport.scale + math.floor(bubble_style.bubble_bottom_offset_y * char_scale)
 
       pos.y = (pos.y - game.ScrollPosition.y - shader_adjusted_viewport.y) * shader_adjusted_viewport.scale + math.floor(bubble_style.bubble_bottom_offset_y * char_scale)
 
     else
 
     else
       pos.y = min_distance
+
       pos.y = bubble_style.min_distance
 
     end
 
     end
   elseif temp_y > game.WindowResolution.y - bubble_dim.y - min_distance then
+
   elseif temp_y > game.WindowResolution.y - bubble_dim.y - bubble_style.min_distance then
     if flip_v_align_on_edge then
+
     if bubble_style.flip_v_align_on_edge and bubble_style.fixed_pos == false then
 
       bubble_style.align_v_temp = "top"
 
       bubble_style.align_v_temp = "top"
 
       pos.y = (pos.y - game.ScrollPosition.y - shader_adjusted_viewport.y) * shader_adjusted_viewport.scale - bubble_dim.y - math.floor(bubble_style.bubble_top_offset_y * char_scale)  
 
       pos.y = (pos.y - game.ScrollPosition.y - shader_adjusted_viewport.y) * shader_adjusted_viewport.scale - bubble_dim.y - math.floor(bubble_style.bubble_top_offset_y * char_scale)  
 
     else
 
     else
       pos.y = game.WindowResolution.y - bubble_dim.y - min_distance
+
       pos.y = game.WindowResolution.y - bubble_dim.y - bubble_style.min_distance
 
     end
 
     end
 
   else
 
   else
Line 481: Line 755:
 
   end
 
   end
  
   -- Get bubble graphic and define ninerect geometry
+
   return pos
  local sprite = graphics.loadFromFile(bubble_style.file_bubble)
+
end
  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
+
-- Calculate position of portrait image
    -- right pointer when char facing left
+
function get_portrait_position(pos, bubble_style, bubble_dim, portrait_sprite)
    if (bubble_style.align_v_temp ~= nil and bubble_style.align_v_temp == "bottom") or (bubble_style.align_v_temp == nil and bubble_style.align_v == "bottom") then
+
  local portrait_pos = { x = pos.x, y = pos.y }
      -- top pointer when bubble aligned to bottom
+
 
      if bubble_style.file_pointer_top_right ~= nil and bubble_style.file_pointer_top_right ~= "" then
+
  if bubble_style.portrait_align_h == "right" then
        pointer = graphics.loadFromFile(bubble_style.file_pointer_top_right)
+
    portrait_pos.x = pos.x + bubble_dim.x - portrait_sprite.width
        pointer_offset = -bubble_style.pointer_top_right_offset_x
+
  end
      end
+
 
    else
+
  if bubble_style.portrait_align_v == "center" then
      -- bottom pointer when bubble aligned to top
+
    portrait_pos.y = pos.y + (bubble_dim.y - portrait_sprite.height) / 2
      if bubble_style.file_pointer_bottom_right ~= nil and bubble_style.file_pointer_bottom_right ~= "" then
+
   elseif bubble_style.portrait_align_v == "bottom" then
        pointer = graphics.loadFromFile(bubble_style.file_pointer_bottom_right)
+
    portrait_pos.y = pos.y + bubble_dim.y - portrait_sprite.height
        pointer_offset = -bubble_style.pointer_bottom_right_offset_x
 
      end
 
    end
 
   else
 
    -- left pointer when char facing right
 
    if (bubble_style.align_v_temp ~= nil and bubble_style.align_v_temp == "bottom") or (bubble_style.align_v_temp == nil and 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
 
   end
 +
 
 +
  return portrait_pos
 +
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
+
-- Calculate position of pointer image
      -- if bubble is aligned right, pointer is positioned leftmost
+
function get_pointer_position(pos, bubble_style, bubble_dim, pointer_offset, char, char_facing, shader_adjusted_viewport, pointer_sprite)
      pointer_pos.x = pos.x + pointer_offset
+
  -- 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
  
      -- Adjust position, if you're too close to the edge
+
    -- Adjust position, if bubble is too close to the edge
 +
    if bubble_style.fixed_pos == false then
 
       if pointer_pos.x < (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset then
 
       if pointer_pos.x < (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset then
         pointer_pos.x = (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset
+
         pointer_pos.x = (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset
 
       end
 
       end
     elseif bubble_style.align_h == "left" or (bubble_style.align_h == "char_facing" and char_facing == "left") then
+
     end
      -- if bubble is aligned left, pointer is positioned rightmost
+
  elseif bubble_style.align_h == "left" or (bubble_style.align_h == "char_facing" and char_facing == "left") then
      pointer_pos.x = pos.x + bubble_dim.x - pointer.width + pointer_offset
+
    -- if bubble is aligned left, pointer is positioned rightmost
 +
    pointer_pos.x = pos.x + bubble_dim.x - pointer_sprite.width + pointer_offset
  
      -- Adjust position, if you're too close to the edge
+
    -- Adjust position, if bubble is too close to the edge
 +
    if bubble_style.fixed_pos == false then
 
       if pointer_pos.x > (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset then
 
       if pointer_pos.x > (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset then
         pointer_pos.x = (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset
+
         pointer_pos.x = (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset
 
       end
 
       end
     else -- center
+
     end
      -- if bubble is aligned center, pointer is positioned at character
+
  else -- center
 +
    -- if bubble is aligned center, pointer is positioned at character
 +
    if bubble_style.fixed_pos == false then
 
       pointer_pos.x = (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset
 
       pointer_pos.x = (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset
 +
    else
 +
      pointer_pos.x = pos.x + bubble_dim.x / 2
 
     end
 
     end
 +
  end
  
    -- Adjust position, if character is too close to the edge
+
  -- Adjust position, if character is too close to the edge
 +
  if bubble_style.fixed_pos == false then
 
     if pointer_pos.x < pos.x - pointer_offset then
 
     if pointer_pos.x < pos.x - pointer_offset then
 
       pointer_pos.x = pos.x - pointer_offset
 
       pointer_pos.x = pos.x - pointer_offset
     elseif pointer_pos.x > pos.x + bubble_dim.x - pointer.width - pointer_offset then
+
     elseif pointer_pos.x > pos.x + bubble_dim.x - pointer_sprite.width - pointer_offset then
       pointer_pos.x = pos.x + bubble_dim.x - pointer.width - pointer_offset
+
       pointer_pos.x = pos.x + bubble_dim.x - pointer_sprite.width - pointer_offset
 
     end
 
     end
 
+
   
     if (bubble_style.align_v_temp ~= nil and bubble_style.align_v_temp == "bottom") or (bubble_style.align_v_temp == nil and bubble_style.align_v == "bottom") then
+
     if pointer_pos.x < pos.x + bubble_style.pointer_min_distance then
      -- pointer on top when bubble aligned to bottom
+
       pointer_pos.x = pos.x + bubble_style.pointer_min_distance
       pointer_pos.y = pos.y - pointer.height + bubble_style.pointer_top_offset_y
+
     elseif pointer_pos.x > pos.x + bubble_dim.x - pointer_sprite.width - bubble_style.pointer_min_distance then
     else
+
       pointer_pos.x = pos.x + bubble_dim.x - pointer_sprite.width - bubble_style.pointer_min_distance
      -- pointer on bottom when bubble aligned to top
 
       pointer_pos.y = pos.y + bubble_dim.y - bubble_style.pointer_bottom_offset_y
 
 
     end
 
     end
 +
  end
  
    pointer.position = {x = pointer_pos.x, y = pointer_pos.y}
+
  if (bubble_style.align_v_temp ~= nil and bubble_style.align_v_temp == "bottom") or (bubble_style.align_v_temp == nil and bubble_style.align_v == "bottom") then
 
+
    -- pointer on top when bubble aligned to bottom
     -- Draw the pointer
+
    pointer_pos.y = pos.y - pointer_sprite.height + bubble_style.pointer_top_offset_y
     graphics.drawSprite(pointer, 1.0, bubble_style.color)
+
  else
 +
     -- pointer on bottom when bubble aligned to top
 +
     pointer_pos.y = pos.y + bubble_dim.y - bubble_style.pointer_bottom_offset_y
 
   end
 
   end
  
   -- Draw the text line by line
+
   return pointer_pos
  for k, line in ipairs(lines) do
+
end
    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
 
  
  
Line 618: Line 852:
 
! 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.5.zip|argo_bubbles_2.1.5.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.2.0.zip|argo_bubbles_2.2.0.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 00:47, 7 May 2023

Name Type By
Argo Bubbles - Dynamic stylized 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.


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


Instructions

Argo Bubbles - Example.png

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

2. Create the bubble graphics and add them to your project folder or any subfolder.

3. Adjust the settings at the top of the script (between "DEFINE YOUR SETTINGS HERE" and "USUALLY NO NEED TO CHANGE ANYTHING BELOW THIS LINE").


Description

The script is extensively commented, giving you all the information you need to make use of its features. If anything is unclear, you may find the answer in this description.


How the Argo Bubbles are created

  • Display of the regular character text is prevented. The text is stored internally to get integrated in the speech bubble. The font used in the bubble will stay the same though - it is the regular font defined in the editor for a specific character. Choose a font for the character that fits your speech bubble design.
  • The main bubble graphic is cut into nine pieces which are individually stretched and then reassembled to match the calculated size of the bubble (see the graphic below). This "ninerect" technique ensures that the corners don't get distorted, but leads to limitations to what the speech 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.
  • You usually want to have a pointer attached to the bubble, pointing at the character who is currently speaking. Depending on your alignment settings and on the direction the character is facing, the script selects one of up to four pointer graphics you can provide and places it accordingly. Pointers are optional though.
  • You have the option to add an additional image to the bubble. It is intended for a portrait picture of the character, but you can choose any image you like, of course.


Argo Bubbles - How the bubbles are created.png


Settings

General settings

  • enable_shader_viewport_adjustments : If set to true, the script will take viewport manipulations by Visionaire's Shader Toolkit into account. If you don't make use of the Shader Toolkit, you can set this option to false.


Bubble style

  • align_h: The horizontal alignment of the bubble in relation to the character. Possible values are (center|left|right|char_facing). The "char_facing" value results in a left/right aligned bubble, respectively, depending on the character's facing direction.
  • align_v: The vertical alignment of the bubble in relation to the character. Possible values are (top|bottom). This setting also defines the vertical positioning of the bubble pointer: it is attached to the bottom of the bubble, if the bubble is top-aligned, and vice versa.
  • flip_v_align_on_edge: If set to true, a top-aligned bubble will automatically turn into a bottom-aligned bubble (and vice versa) when the character gets too close to the top (bottom) edge of the screen.
  • bubble_offset_x: The horizontal offset of the bubble from the initial position centered above the character's head. Positive values move the bubble in the character's facing direction.
  • bubble_top_offset_y: The vertical offset of the bubble for top-aligned bubbles. Positive values move the bubble up.
  • bubble_bottom_offset_y: The vertical offset of the bubble for bottom-aligned bubbles. Positive values move the bubble down.
  • adjust_v_offset_to_char_scale: If set to true, the current character scaling is taken into account when applying the vertical bubble offset. The offset (as defined in "bubble_top_offset_y" or "bubble_bottom_offset_y") will be reduced/increased according to the character scale.
  • color: Color tint for the bubble (and pointer). Please note that this option takes the color in hexadecimal BGR notation (instead of RGB), prepended with "0x". The default value of 0xffffff (white) results in no tint.
  • text_align_h: The horizontal text alignment inside the bubble. Possible values are (center|left|right). This option has a visible effect only if it's multiline text and/or if the bubble has a fixed width.
  • text_align_v: The vertical text alignment inside the bubble. Possible values are (center|top|bottom). This option has a visible effect only if the bubble has a fixed height.
  • padding: The distance between the text block and the outer edge of the bubble, defined in a table {"top", "right", "bottom", "left"}.
  • fixed_width: The fixed width of the bubble. Set the value to 0 to not set a fixed width.
  • fixed_height: The fixed height of the bubble. Set the value to 0 to not set a fixed height.
  • fixed_pos: A fixed position on the screen to show the bubble at, defined in a table {"x", "y"}. That position will serve as the root point of the position calculation (it substitutes the character's position), so it depends on your alignment settings where the bubble will be placed. Don't set "align_h" to "char_facing" when using the fixed position, because the bubble position would change depending on the character's facing direction.
  • expand_fixed_dims: If set to true, a fixed bubble width/height will expand if the text doesn't fit in the fix-sized bubble. The fixed width/height will act like a min-width/min-height setting.
  • file_bubble: The path to the main bubble graphic. The graphic needs to be suited for the "ninerect" technique. The filepath must be relative to the project root folder and be prepended with "vispath:".
  • ninerect: The measurements used to cut the "ninerect" bubble graphic, defined in a table {"x", "y", "width, "height"}. The x/y values define the width/height of the top left tile of the "ninerect", the width/height values define the width/height of the center tile (see the graphic below).
  • file_portrait: The path to a portrait image. The filepath must be relative to the project root folder and be prepended with "vispath:". Set the value to "" (empty string) for a bubble without a portrait image.
  • portrait_align_h: The horizontal alignment of the portrait image in the bubble. Possible values are (left|right). There will be no padding between the portrait and the edge of the bubble; the padding setting only applies to the text. Add transparent areas to your portrait image to adjust the position (see the graphic below).
  • portrait_align_v: The vertical alignment of the portrait image in the bubble. Possible values are (top|center|bottom). There will be no padding between the portrait and the edge of the bubble; the padding setting only applies to the text. Add transparent areas to your portrait image to adjust the position (see the graphic below).
  • file_pointer_bottom_right: The path to the down-right-pointing pointer graphic. The filepath must be relative to the project root folder and be prepended with "vispath:". Set the value to "" (empty string) for a bubble without a pointer.
  • file_pointer_bottom_left: The path to the down-left-pointing pointer graphic. The filepath must be relative to the project root folder and be prepended with "vispath:". Set the value to "" (empty string) for a bubble without a pointer.
  • pointer_bottom_right_offset_x: The horizontal offset of the down-right-pointing pointer. Positive values move the pointer in the character's facing direction.
  • pointer_bottom_left_offset_x: The horizontal offset of the down-left-pointing pointer. Positive values move the pointer in the character's facing direction.
  • pointer_bottom_offset_y: The vertical offset of the down-pointing pointer. Positive values move the pointer inwards.
  • file_pointer_top_right: The path to the up-right-pointing pointer graphic. The filepath must be relative to the project root folder and be prepended with "vispath:". Set the value to "" (empty string) for a bubble without a pointer.
  • file_pointer_top_left: The path to the up-left-pointing pointer graphic. The filepath must be relative to the project root folder and be prepended with "vispath:". Set the value to "" (empty string) for a bubble without a pointer.
  • pointer_top_right_offset_x: The horizontal offset of the up-right-pointing pointer. Positive values move the pointer in the character's facing direction.
  • pointer_top_left_offset_x: The horizontal offset of the up-left-pointing pointer. Positive values move the pointer in the character's facing direction.
  • pointer_top_offset_y: The vertical offset of the up-pointing pointer. Positive values move the pointer inwards.
  • pointer_min_distance: The minimum distance a pointer has to keep from the edge of the bubble (see the graphic below).
  • min_distance: The minimum distance the bubble has to keep from the edge of the screen.


Argo Bubbles - Style settings.png


Work with multiple speech bubble styles

A main feature of the "Argo Bubbles" script is the ability to define as many bubble styles as you like. You could have a different bubble color/design 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 and switch between them during the game. The default bubble style is defined in the "default_style" variable.

If you would like to add more 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 a character is meant to use only a single style throughout the game (either the default style or a custom one), you don't have to do anything else.
  • If you want the character to switch styles during the game, add a Visionaire value called "argo_bubble" to the character. By changing that value you can select the style to be active. A value of 0 selects the default style. A value of 1 selects the first custom style defined for that character, a value of 2 the second custom style for that character etc. You can define as many custom styles per character as you like - just add them to the "custom_styles" table.


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. Be aware that the tutorial was created for script version 2.1.2. The basic idea is still the same, but the code has changed a lot since then.


Main Script

--[[

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

Authors:         The Argonauts
                 with contributions by Mulewa
                 and some lines of code taken from the Visionaire Shader Toolkit by Simon Scheckel
Version:         2.2.0
Date:            2023-05-07
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 detailed description, please visit the official Visionaire wiki: https://wiki.visionaire-tracker.net/wiki/Argo_Bubbles



MIT License

Copyright 2023 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.
]]





-------------------------------
-- DEFINE YOUR SETTINGS HERE --
-------------------------------


--[[ GENERAL SETTINGS

* enable_shader_viewport_adjustments: enables viewport adjustments (zoom, scale) when using Visionaire's shader toolkit
]]

local enable_shader_viewport_adjustments = true


--[[ DEFAULT BUBBLE STYLE

* align_h: horizontal alignment of bubble in relation to character (center|left|right|char_facing)
* align_v: vertical alignment of bubble in relation to character (top|bottom), defines position of pointer
* flip_v_align_on_edge: turns top-aligned bubble into bottom-aligned bubble when too close to edge of screen (and vice versa)
* bubble_offset_x: horizontal bubble offset (positive: move in facing direction)
* bubble_top_offset_y: vertical bubble offset for top-aligned bubbles (positive: move up)
* bubble_bottom_offset_y: vertical bubble offset for bottom-aligned bubbles (positive: move down)
* adjust_v_offset_to_char_scale: adjusts the vertical offset according to current character scaling
* color: color tint for bubble and pointer in BGR notation, not RGB (set to white for none: 0xffffff)
* text_align_h: horizontal alignment inside the bubble (center|left|right)
* text_align_v: vertical alignment inside the bubble (center|top|bottom)
* padding: distance between text block and outer edge of bubble
* fixed_width: fixed width of the bubble (set to 0 for variable width)
* fixed_height: fixed height of the bubble (set to 0 for variable height)
* fixed_pos: fixed position of bubble on screen, not related to character (set to false to not define a fixed position)
* expand_fixed_dims: if true, fixed width/height will expand if the text is wider/higher
* file_bubble: path to ninerect bubble graphic
* ninerect: x/y = width/height of top left corner in ninerect bubble graphic, width/height = width/height of center part
* file_portrait: path to a portrait image (set to "" for bubble without portrait)
* portrait_align_h: horizontal alignment of the portrait image (left|right)
* portrait_align_v: vertical alignment of the portrait image (top|center|bottom)
* file_pointer_bottom_right: path to right-facing pointer at bubble bottom (set to "" for bubble without pointer)
* file_pointer_bottom_left: path to left-facing pointer at bubble bottom (set to "" for bubble without pointer)
* pointer_bottom_right_offset_x: x offset of right-facing pointer at bubble bottom (positive: move in facing direction)
* pointer_bottom_left_offset_x: x offset of left-facing pointer at bubble bottom (positive: move in facing direction)
* pointer_bottom_offset_y: y offset of pointer at bubble bottom (positive: move inwards)
* file_pointer_top_right: path to right-facing pointer at bubble top (set to "" for bubble without pointer)
* file_pointer_top_left: path to left-facing pointer at bubble top (set to "" for bubble without pointer)
* pointer_top_right_offset_x: x offset of right-facing pointer at top (positive: move in facing direction)
* pointer_top_left_offset_x: x offset of left-facing pointer at top (positive: move in facing direction)
* pointer_top_offset_y: y offset of pointer at bubble top (positive: move inwards)
* pointer_min_distance: minimum distance a pointer has to keep from edge of bubble
* min_distance: minimum distance a bubble has to keep from edge of screen
]]

local default_style = {
  align_h = "center",
  align_v = "top",
  flip_v_align_on_edge = true,
  bubble_offset_x = 0,
  bubble_top_offset_y = 25,
  bubble_bottom_offset_y = 70,
  adjust_v_offset_to_char_scale = true,
  color = 0xffffff,
  text_align_h = "center",
  text_align_v = "center",
  padding = { top = 15, right = 20, bottom = 12, left = 20 },
  fixed_width = 0,
  fixed_height = 0,
  fixed_pos = false,
  expand_fixed_dims = true,
  file_bubble = "vispath:gui/bubble.png",
  ninerect = { x = 20, y = 15, width = 30, height = 20 },
  file_portrait = "",
  portrait_align_h = "left",
  portrait_align_v = "top",
  file_pointer_bottom_right = "vispath:gui/bubble_pointer_br.png",
  file_pointer_bottom_left = "vispath:gui/bubble_pointer_bl.png",
  pointer_bottom_right_offset_x = 20,
  pointer_bottom_left_offset_x = 0,
  pointer_bottom_offset_y = 3,
  file_pointer_top_right = "",
  file_pointer_top_left = "",
  pointer_top_right_offset_x = 0,
  pointer_top_left_offset_x = 0,
  pointer_top_offset_y = 0,
  pointer_min_distance = 15,
  min_distance = 15
}


--[[ CUSTOM BUBBLE STYLES

Optional: 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 a character is meant to use only a single style throughout the game (either the default style or a custom one), you don't have to do anything else.

If you want the character to switch styles during the game, add a Visionaire value called "argo_bubble" to the character. By changing that value you can select the style to be active. A value of 0 selects the default style. A value of 1 selects the first custom style defined for that character, a value of 2 the second custom style for that character etc. You can define as many custom styles per character as you like.

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

]]

local custom_styles = {}


--[[ ADVANCED SETTINGS

* classic_mode: the classic mode uses the "textStarted" event handler for displaying the speech bubbles; the new mode (classic_mode = false) uses the "textRender" hook function and supports custom pause wildcards in the middle of texts (e.g. "Text<p2>Text", "Hello<p>world"); if you need the "textRender" hook elsewhere in your project, you may switch back to classic mode
* ab_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.
* ab_on_text_started: call external functions for the "textStarted" event handler
* ab_on_text_stopped: call external functions for the "textStopped" event handler
]]

local classic_mode = false
local ab_bind_to_handlers = true

function ab_on_text_started(text)
  -- add your functions here

end

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



-- CALL FUNCTIONS

local bubbles = {}

-- New mode: create bubbles in "textRender" function
function show_argo_bubble(text)
  if text.Owner:getId().tableId == eCharacters then
    -- Add current character text to the bubbles table, if not already there
    if bubbles[text:getId().id] == nil then
      bubbles[text:getId().id] = {
        Text = text.CurrentText,
        Owner = text.Owner:getName(),
        Background = text.Background
      }
    end

    -- override the default text rendering
    return true
  end
  
  return false
end



-- Classic mode: create bubbles on "textStarted" event
function show_argo_bubble_classic(text)
  if classic_mode then
    if text.Owner:getId().tableId == eCharacters then
      -- Add current character text to the bubbles table
      bubbles[text:getId().id] = {
        Text = text.CurrentText,
        Owner = text.Owner:getName(),
        Background = text.Background
      }

      -- prevent displaying of text
      text.CurrentText = ""
    end
  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



-- Call this function to immediately destroy all bubbles
function kill_argo_bubbles()
  bubbles = {}
end



-- Bind to event handlers and register hook function
if ab_bind_to_handlers then
  registerEventHandler("textStarted","show_argo_bubble_classic")
  registerEventHandler("textStopped","destroy_argo_bubble")
end

if not classic_mode then
  registerHookFunction("textRender", "show_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)

  -- Get bubble style
  local bubble_style = get_style(key, val, char)
  
  -- Get shader toolkit viewport adjustments
  local shader_adjusted_viewport = get_viewport_adjustments(bubble_style)

  -- 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

  -- Get text and bubble dimensions
  local text_and_bubble_dims = get_text_and_dims(key, val, char, bubble_style)
  
  local text_dim = text_and_bubble_dims.text_dim
  local bubble_dim = text_and_bubble_dims.bubble_dim

   -- Get the bubble position
  pos = get_bubble_position(pos, bubble_style, bubble_dim, char, char_facing, shader_adjusted_viewport)

  -- Get bubble graphic and define ninerect geometry
  local bubble_sprite
  local dest_rect = { x = pos.x, y = pos.y, width = bubble_dim.x, height = bubble_dim.y }
  
  -- Take sprite from bubbles table, if cached
  if val.bubble_sprite ~= nil then
    bubble_sprite = val.bubble_sprite
  else
    bubble_sprite = graphics.loadFromFile(bubble_style.file_bubble)

    -- Cache bubble sprite
    bubbles[key].bubble_sprite = bubble_sprite
  end

  -- Draw the bubble
  graphics.drawSpriteWithNineRect(bubble_sprite, dest_rect, bubble_style.ninerect, bubble_style.color, 1.0)

  -- Get portrait graphic
  local portrait_sprite = nil

  -- Take sprite from bubbles table, if cached
  if val.portrait_sprite ~= nil then
    portrait_sprite = val.portrait_sprite
  elseif bubble_style.file_portrait ~= nil and bubble_style.file_portrait ~= "" then
    portrait_sprite = graphics.loadFromFile(bubble_style.file_portrait)
    
    -- Cache bubble sprite
    bubbles[key].portrait_sprite = portrait_sprite
  end
  
  -- Continue with portrait, if graphic has been defined
  if portrait_sprite ~= nil then
    -- Get position of portrait image
    portrait_sprite.position = get_portrait_position(pos, bubble_style, bubble_dim, portrait_sprite)

    -- Draw the portrait
    graphics.drawSprite(portrait_sprite, 1.0)
  end

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

  if char_facing == "left" then
    -- right pointer when char facing left
    if (bubble_style.align_v_temp ~= nil and bubble_style.align_v_temp == "bottom") or (bubble_style.align_v_temp == nil and 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_sprite = 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_sprite = 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_temp ~= nil and bubble_style.align_v_temp == "bottom") or (bubble_style.align_v_temp == nil and 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_sprite = 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_sprite = 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_sprite ~= nil then
    -- Get position of pointer image
    pointer_sprite.position = get_pointer_position(pos, bubble_style, bubble_dim, pointer_offset, char, char_facing, shader_adjusted_viewport, pointer_sprite)

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

  -- Draw the text
  -- Calculate vertical alignment of text inside bubble (only if fixed height and not at the top)
  local text_box_pos_y = pos.y + bubble_style.padding.top
    
  if bubble_style.fixed_height ~= nil then
    if bubble_style.text_align_v == "center" then
      text_box_pos_y = pos.y + bubble_style.padding.top + (bubble_dim.y - bubble_style.padding.top - bubble_style.padding.bottom - text_dim.y) / 2
    elseif bubble_style.text_align_v == "bottom" then
      text_box_pos_y = pos.y + bubble_dim.y - bubble_style.padding.bottom - text_dim.y
    end
  end

  -- Draw the text line by line
  for k, line in ipairs(text_and_bubble_dims.lines) do 
    local tempdim = graphics.fontDimension(line)
    local text_pos = {x = 0, y = 0}

    if bubble_style.text_align_h == "left" then
      text_pos.x = pos.x + bubble_style.padding.left
    elseif bubble_style.text_align_h == "right" then
      text_pos.x = pos.x + bubble_dim.x - bubble_style.padding.right - tempdim.x
    else -- center
      text_pos.x = pos.x + bubble_style.padding.left + (bubble_dim.x - bubble_style.padding.left - bubble_style.padding.right - tempdim.x) / 2
    end
    
    text_pos.y = text_box_pos_y + (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



-- Get style for the current bubble
function get_style(key, val, char)
  local bubble_style = default_style
  
  -- Take style from bubbles table, if cached
  if val.style ~= nil then
    bubble_style = val.style
  else
    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

    -- Cache style
    bubbles[key].style = bubble_style
  end

  return bubble_style
end



-- Get shader toolkit viewport adjustments (probably not fully supported yet)
function get_viewport_adjustments(bubble_style)
  local shader_adjusted_viewport = {x = 0, y = 0, scale = 1}
  
  if enable_shader_viewport_adjustments and shader_newViewport ~= nil and bubble_style.fixed_pos == false then
    if shader_viewportTransition > 0.0 and shader_viewportTransition < 1.0 then
      shader_adjusted_viewport.scale = (1.0 - shader_viewportTransition) * shader_oldViewport.scale + shader_viewportTransition * shader_newViewport.scale

      local startCenter = {
        x = shader_oldViewport.x + c_res.x / shader_oldViewport.scale * shader_interpolationPoint.x,
        y = shader_oldViewport.y + c_res.y / shader_oldViewport.scale * shader_interpolationPoint.y
      }

      local endCenter = {
        x = shader_newViewport.x + c_res.x / shader_newViewport.scale * shader_interpolationPoint.x,
        y = shader_newViewport.y + c_res.y / shader_newViewport.scale * shader_interpolationPoint.y
      }

      local interpolatedCenter = {
        x = startCenter.x * (1.0 - shader_viewportTransition) + endCenter.x * shader_viewportTransition,
        y = startCenter.y * (1.0 - shader_viewportTransition) + endCenter.y * shader_viewportTransition
      }

      shader_adjusted_viewport.x = interpolatedCenter.x - c_res.x / shader_scale * shader_interpolationPoint.x
      shader_adjusted_viewport.y = interpolatedCenter.y - c_res.y / shader_scale * shader_interpolationPoint.y
    else
      shader_adjusted_viewport = shader_newViewport
    end
  end

  return shader_adjusted_viewport
end



 -- Calculate text and bubble dimensions
function get_text_and_dims(key, val, char, bubble_style)
  local txt = ""
  local lines = {}
  local text_dim = {x = 0, y = 0}
  local bubble_dim = {x = 0, y = 0}

  graphics.font = char.Font

  -- Take from bubbles table, if cached
  if val.text_dim ~= nil then
    txt = val.txt
    lines = val.lines
    text_dim = val.text_dim
    bubble_dim = val.bubble_dim
  else
    txt = val.Text:gsub("<br/"..">", "\n")
    lines = graphics.performLinebreaks(txt)

    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

    if bubble_style.fixed_width > 0 then
      if not bubble_style.expand_fixed_dims or bubble_style.fixed_width > bubble_dim.x then
        bubble_dim.x = bubble_style.fixed_width
      end
    end
   
    if bubble_style.fixed_height > 0 then
      if not bubble_style.expand_fixed_dims or bubble_style.fixed_height > bubble_dim.y then
        bubble_dim.y = bubble_style.fixed_height
      end
    end

    -- Cache dimensions
    bubbles[key].txt = txt
    bubbles[key].lines = lines
    bubbles[key].text_dim = text_dim
    bubbles[key].bubble_dim = bubble_dim
  end

  return { lines = lines, text_dim = text_dim, bubble_dim = bubble_dim }
end


  
-- Calculate the bubble position
function get_bubble_position(pos, bubble_style, bubble_dim, char, char_facing, shader_adjusted_viewport)
  -- Change reference point for bubble with fixed position
  if bubble_style.fixed_pos ~= false then
    pos.x = bubble_style.fixed_pos.x + game.ScrollPosition.x
    pos.y = bubble_style.fixed_pos.y + game.ScrollPosition.y
  end
  
  -- X 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 - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale
  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 - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale - bubble_dim.x
  else -- center
    pos.x = (pos.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale - 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 < bubble_style.min_distance then
    pos.x = bubble_style.min_distance
  elseif pos.x > game.WindowResolution.x - bubble_dim.x - bubble_style.min_distance then
    pos.x = game.WindowResolution.x - bubble_dim.x - bubble_style.min_distance
  end

  -- Y position
  local temp_y = 0
  local char_scale = 1

  if bubble_style.adjust_v_offset_to_char_scale then
    char_scale = (char.ScaleFactor / 100) * shader_adjusted_viewport.scale

    if char.Scale then
      char_scale = char_scale * (char.Size / 100)
    end
  end

  if bubble_style.align_v == "bottom" then
    temp_y = (pos.y - game.ScrollPosition.y - shader_adjusted_viewport.y) * shader_adjusted_viewport.scale + math.floor(bubble_style.bubble_bottom_offset_y * char_scale)
  else -- top
    temp_y = (pos.y - game.ScrollPosition.y - shader_adjusted_viewport.y) * shader_adjusted_viewport.scale - bubble_dim.y - math.floor(bubble_style.bubble_top_offset_y * char_scale)
  end

  if temp_y < bubble_style.min_distance then
    if bubble_style.flip_v_align_on_edge and bubble_style.fixed_pos == false then
      bubble_style.align_v_temp = "bottom"
      pos.y = (pos.y - game.ScrollPosition.y - shader_adjusted_viewport.y) * shader_adjusted_viewport.scale + math.floor(bubble_style.bubble_bottom_offset_y * char_scale)
    else
      pos.y = bubble_style.min_distance
    end
  elseif temp_y > game.WindowResolution.y - bubble_dim.y - bubble_style.min_distance then
    if bubble_style.flip_v_align_on_edge and bubble_style.fixed_pos == false then
      bubble_style.align_v_temp = "top"
      pos.y = (pos.y - game.ScrollPosition.y - shader_adjusted_viewport.y) * shader_adjusted_viewport.scale - bubble_dim.y - math.floor(bubble_style.bubble_top_offset_y * char_scale) 
    else
      pos.y = game.WindowResolution.y - bubble_dim.y - bubble_style.min_distance
    end
  else
    pos.y = temp_y
    bubble_style.align_v_temp = nil
  end

  return pos
end



-- Calculate position of portrait image
function get_portrait_position(pos, bubble_style, bubble_dim, portrait_sprite)
  local portrait_pos = { x = pos.x, y = pos.y }
  
  if bubble_style.portrait_align_h == "right" then
    portrait_pos.x = pos.x + bubble_dim.x - portrait_sprite.width
  end
  
  if bubble_style.portrait_align_v == "center" then
    portrait_pos.y = pos.y + (bubble_dim.y - portrait_sprite.height) / 2
  elseif bubble_style.portrait_align_v == "bottom" then
    portrait_pos.y = pos.y + bubble_dim.y - portrait_sprite.height
  end
  
  return portrait_pos
end



-- Calculate position of pointer image
function get_pointer_position(pos, bubble_style, bubble_dim, pointer_offset, char, char_facing, shader_adjusted_viewport, pointer_sprite)
  -- 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

    -- Adjust position, if bubble is too close to the edge
    if bubble_style.fixed_pos == false then
      if pointer_pos.x < (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset then
        pointer_pos.x =  (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset
      end
    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_sprite.width + pointer_offset

    -- Adjust position, if bubble is too close to the edge
    if bubble_style.fixed_pos == false then
      if pointer_pos.x > (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset then
        pointer_pos.x =  (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset
      end
    end
  else -- center
    -- if bubble is aligned center, pointer is positioned at character
    if bubble_style.fixed_pos == false then
      pointer_pos.x = (char.Position.x - game.ScrollPosition.x - shader_adjusted_viewport.x) * shader_adjusted_viewport.scale + pointer_offset
    else
      pointer_pos.x = pos.x + bubble_dim.x / 2
    end
  end

  -- Adjust position, if character is too close to the edge
  if bubble_style.fixed_pos == false then
    if pointer_pos.x < pos.x - pointer_offset then
      pointer_pos.x = pos.x - pointer_offset
    elseif pointer_pos.x > pos.x + bubble_dim.x - pointer_sprite.width - pointer_offset then
      pointer_pos.x = pos.x + bubble_dim.x - pointer_sprite.width - pointer_offset
    end
    
    if pointer_pos.x < pos.x + bubble_style.pointer_min_distance then
      pointer_pos.x = pos.x + bubble_style.pointer_min_distance
    elseif pointer_pos.x > pos.x + bubble_dim.x - pointer_sprite.width - bubble_style.pointer_min_distance then
      pointer_pos.x = pos.x + bubble_dim.x - pointer_sprite.width - bubble_style.pointer_min_distance
    end
  end

  if (bubble_style.align_v_temp ~= nil and bubble_style.align_v_temp == "bottom") or (bubble_style.align_v_temp == nil and bubble_style.align_v == "bottom") then
    -- pointer on top when bubble aligned to bottom
    pointer_pos.y = pos.y - pointer_sprite.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

  return pointer_pos
end



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


Resources

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