--[[

Argo Bubbles
------------
This script turns character texts into speech bubbles. It allows defining differently styled bubbles for each character, or even different speech bubble styles for the same character to switch between during the game.

Authors:         The Argonauts
                 (with contributions by Mulewa and some lines of code
                 taken from the Visionaire Shader Toolkit by Simon Scheckel)
Version:         2.3.1
Date:            2024-06-01
Requirements:    Visionaire Studio 5.3 or higher
License:         MIT License (details at the bottom of the script)
Based on:        Advanced Speechbubble Script [v1.1] (10/13/2018), written by Sebastian/TURBOMODUS (with permission)
Instructions:    https://wiki.visionaire-tracker.net/wiki/Argo_Bubbles

Argonauts games: https://the-argonauts.itch.io/

]]



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

-- Detailed description of the settings:
-- https://wiki.visionaire-tracker.net/wiki/Argo_Bubbles#Settings
-- or in the "readme.txt" file coming with this script


-- GENERAL SETTINGS
local enable_shader_viewport_adjustments = true


-- DEFAULT BUBBLE STYLE
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
local custom_styles = {}


-- ADVANCED SETTINGS
local ab_bind_to_handler = true

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 = {}
local killed = {}

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

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



function destroy_argo_bubble(text)
  -- Remove current text from bubbles table
  bubbles[text.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()
  -- Since we use the render hook, the bubbles table gets quickly filled again;
  -- so we store the ids of texts to be killed in a separate table
  for key, val in pairs(bubbles) do
    killed[key] = key
  end
  
  bubbles = {}
end



-- Register hook function and bind to event handler
registerHookFunction("textRender", "show_argo_bubble")

if ab_bind_to_handler then
  registerEventHandler("textStopped","destroy_argo_bubble")
end





-- DRAW FUNCTIONS

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



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



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

  -- 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 object
  local text_pos = {x = 0, y = text_box_pos_y}
  local alignment = 2
  
  if bubble_style.text_align_h == "left" then
    text_pos.x = pos.x + bubble_style.padding.left
    alignment = 0
  elseif bubble_style.text_align_h == "right" then
    text_pos.x = pos.x + bubble_dim.x - bubble_style.padding.right
    alignment = 1
  else -- center
    text_pos.x = pos.x + bubble_style.padding.left + (bubble_dim.x - bubble_style.padding.left - bubble_style.padding.right) / 2
  end

  graphics.drawTextObject(val.Object, text_pos.x, text_pos.y, alignment)
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

    -- TrueType Font
    local letter_h = char.Font.Size
    
    -- Bitmap Font
    if not char.Font.TrueTypeFont then
      letter_h = char.Font.Letters[1].height
    end

    text_dim.y = #lines * (letter_h + 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)



--[[

MIT License

Copyright 2024 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.

]]