Alttp Dialog box + sample quest bug

Started by Zefk, December 14, 2016, 05:47:08 PM

Previous topic - Next topic
The mouse contol function seems to make the alttp dialog box script not work.

This function is the issue. The alttp dialog box works without it.
Code ( lua) Select
  function game:on_started()
    -- HUD menu.
    local hud = require("scripts/menus/hud")
    sol.menu.start(game, hud)
    hud:create(game)

    -- Mouse control.
    local mouse_control = require("scripts/menus/mouse_control")
    sol.menu.start(game, mouse_control)
    mouse_control:create(game, hud)

    local hero = game:get_hero()
    hero:set_tunic_sprite_id("main_heroes/eldran")
  end


Full code:
Code ( lua) Select
require("scripts/menus/alttp_dialog_box")
local game_manager = {}

-- Starts the game from the given savegame file,
-- initializing it if necessary.
function game_manager:start_game()

  local exists = sol.game.exists("save1.dat")
  local game = sol.game.load("save1.dat")
  if not exists then
    -- Initialize a new savegame.
    game:set_max_life(12)
    game:set_life(game:get_max_life())
    game:set_ability("lift", 2)
    game:set_ability("sword", 1)
    game:set_starting_location("first_map") -- Starting location.
  end

  function game:on_started()
    -- HUD menu.
    local hud = require("scripts/menus/hud")
    sol.menu.start(game, hud)
    hud:create(game)

    -- Mouse control.
    local mouse_control = require("scripts/menus/mouse_control")
    sol.menu.start(game, mouse_control)
    mouse_control:create(game, hud)

    local hero = game:get_hero()
    hero:set_tunic_sprite_id("main_heroes/eldran")
  end
  game:start()
 
  local hero = game:get_hero()
  hero:set_tunic_sprite_id("main_heroes/eldran")
end

return game_manager

Can we have a look on the ALTTP dialog box script ?

This might be because the script need to be initialized.
Any errors ?

December 15, 2016, 12:55:19 AM #2 Last Edit: December 15, 2016, 04:33:14 AM by Zefk
Alttp dialog script:

No errors....

Code ( lua) Select
-- Script that creates an ALTTP-looking dialog box for games.
--
-- Usage:
-- require("scripts/menus/alttp_dialog_box")
--
-- You really have nothing more to do:
-- the dialog box will automatically be used in games.
--
-- To customize the dialog box, call
-- local dialog_box = game:get_dialog_box()
-- to get it and then it provides the following functions:
--
-- - dialog_box:set_style(style):
--   Sets the style of the dialog box for subsequent dialogs.
--   style must be one of:
--   - "box" (default): Usual dialog box.
--   - "empty": No decoration.
--
-- - dialog_box:set_position(position):
--   Sets the vertical position of the dialog box for subsequent dialogs.
--   position must be one of:
--   - "auto": Choose automatically so that the hero is not hidden.
--   - "top": Top of the screen.
--   - "bottom" (default): Bottom of the screen.
--   - a table with x and y integer fields.
--
-- - dialog_box:get_bounding_box():
--   Returns the coordinates on screen and the size of the dialog box.
--   This also works when the dialog box is inactive: in this case it
--   returns the bounding box it would have if it was activated now.

local dialog_box_manager = {}

require("scripts/multi_events")

-- Creates and sets up a dialog box for the specified game.
local function create_dialog_box(game)

  local dialog_box = {

    -- Dialog box properties.
    dialog = nil,                -- Dialog being displayed or nil.
    first = true,                -- Whether this is the first dialog of a sequence.
    style = nil,                 -- "box" or "empty".
    position = "bottom",         -- "auto", "top", "bottom" or an x,y table.
    skip_mode = nil,             -- "none", "current", "all" or "unchanged".
    info = nil,                  -- Parameter passed to start_dialog().
    skipped = false,             -- Whether the player skipped the dialog.
    choices = {},                -- Whether there is a choice on each line. If yes,
                                 -- the value is the char index of the cursor.
    selected_choice = nil,       -- Selected line (1 is the first one) or nil if there is no question.

    -- Displaying text gradually.
    next_line = nil,             -- Next line to display or nil.
    line_it = nil,               -- Iterator over of all lines of the dialog.
    lines = {},                  -- Array of the text of the visible lines.
    line_surfaces = {},          -- Array of the nb_visible_lines text surfaces.
    line_index = nil,            -- Line currently being shown.
    char_index = nil,            -- Next character to show in the current line.
    char_delay = nil,            -- Delay between two characters in milliseconds.
    full = false,                -- Whether the visible lines have shown all their content.
    need_letter_sound = false,   -- Whether a sound should be played with the next character.
    gradual = true,              -- Whether text is displayed gradually.

    -- Graphics.
    dialog_surface = nil,        -- Intermediate surface where we draw the dialog.
    box_img = nil,               -- Image of the dialog box frame.
    box_dst_position = nil,      -- Destination coordinates of the dialog box.
    choice_cursor_img = nil,     -- Icon representing the selected choice in a question.
    choice_cursor_dst_position = -- Destination coordinates of the cursor icon.
        nil,
  }

  -- Constants.
  local nb_visible_lines = 4     -- Maximum number of lines in the dialog box.
  local char_delays = {
    slow = 60,
    medium = 40,
    fast = 20  -- Default.
  }
  local letter_sound_delay = 100
  local box_width = 220
  local box_height = 60

  -- Initialize dialog box data.
  local dialog_font, dialog_font_size = "alttp", nil
  for i = 1, nb_visible_lines do
    dialog_box.lines[i] = ""
    dialog_box.line_surfaces[i] = sol.text_surface.create{
      horizontal_alignment = "left",
      vertical_alignment = "top",
      font = dialog_font,
      font_size = dialog_font_size,
    }
  end
  dialog_box.dialog_surface = sol.surface.create(sol.video.get_quest_size())
  dialog_box.box_img = sol.surface.create("hud/dialog_box.png")
  dialog_box.choice_cursor_img = sol.text_surface.create{
    horizontal_alignment = "left",
    vertical_alignment = "top",
    font = dialog_font,
      font_size = dialog_font_size,
    text = ">",
  }

  -- Exits the dialog box system.
  function dialog_box:quit()
    if sol.menu.is_started(dialog_box) then
      sol.menu.stop(dialog_box)
    end
  end

  -- Called by the engine when a dialog starts.
  function game:on_dialog_started(dialog, info)

    dialog_box.dialog = dialog
    dialog_box.info = info
    sol.menu.start(game, dialog_box)
  end

  -- Called by the engine when a dialog finishes.
  function game:on_dialog_finished(dialog)

    if sol.menu.is_started(dialog_box) then
      sol.menu.stop(dialog_box)
    end
    dialog_box.dialog = nil
    dialog_box.info = nil
  end

  -- Determines the position of the dialog box on the screen.
  local function compute_position()

    local map = game:get_map()
    local camera_x, camera_y, camera_width, camera_height = map:get_camera():get_bounding_box()
    local top = false
    if dialog_box.position == "top" then
      top = true
    elseif dialog_box.position == "auto" then
      local hero_x, hero_y = map:get_entity("hero"):get_position()
      if hero_y >= camera_y + (camera_height / 2 + 10) then
        top = true
      end
    end

    -- Set the coordinates of graphic objects.
    local box_width, box_height = dialog_box.box_img:get_size()
    local x = camera_width / 2 - box_width / 2
    local y = top and x or (camera_height - x - box_height)

    if type(dialog_box.position) == "table" then
      -- Custom position.
      dialog_box.box_dst_position = dialog_box.position
    else
      dialog_box.box_dst_position = { x = x, y = y }
    end
  end

  -- Returns the dialog box.
  function game:get_dialog_box()
    return dialog_box
  end

  -- See the doc in the header comment.
  function dialog_box:set_style(style)

    dialog_box.style = style
  end

  -- See the doc in the header comment.
  function dialog_box:set_position(position)
    dialog_box.position = position
  end

  -- See the doc in the header comment.
  function dialog_box:get_bounding_box()
    compute_position()
    local width, height = self.box_img:get_size()
    return self.box_dst_position.x, self.box_dst_position.y, width, height
  end

  local function repeat_show_character()

    dialog_box:check_full()
    while not dialog_box:is_full()
        and dialog_box.char_index > #dialog_box.lines[dialog_box.line_index] do
      -- The current line is finished.
      dialog_box.char_index = 1
      dialog_box.line_index = dialog_box.line_index + 1
      dialog_box:check_full()
    end

    if not dialog_box:is_full() then
      dialog_box:add_character()
    else
      sol.audio.play_sound("message_end")
      if game.set_custom_command_effect ~= nil then
        if dialog_box:has_more_lines()
            or dialog_box.dialog.next ~= nil
            or dialog_box.selected_choice ~= nil then
          game:set_custom_command_effect("action", "next")
        else
          game:set_custom_command_effect("action", "return")
        end
        game:set_custom_command_effect("attack", nil)
      end
    end
  end

  -- The first dialog of a sequence starts.
  function dialog_box:on_started()

    -- Set the initial properties.
    -- Subsequent dialogs in the same sequence do not reset them.
    self.skip_mode = "current"
    self.char_delay = char_delays["fast"]
    self.selected_choice = nil

    compute_position()
    self.choice_cursor_dst_position = { x = 0, y = 0 }

    self:show_dialog()
  end

  -- The dialog box is being closed.
  function dialog_box:on_finished()

    -- Remove overriden command effects.
    if game.set_custom_command_effect ~= nil then
      game:set_custom_command_effect("action", nil)
      game:set_custom_command_effect("attack", nil)
    end
  end

  -- A dialog starts (not necessarily the first one of its sequence).
  function dialog_box:show_dialog()

    -- Initialize this dialog.
    local dialog = self.dialog

    local text = dialog.text
    if dialog_box.info ~= nil then
      -- There is a "$v" sequence to substitute.
      text = text:gsub("%$v", dialog_box.info)
    end
    -- Split the text in lines.
    text = text:gsub("\r\n", "\n"):gsub("\r", "\n")
    self.line_it = text:gmatch("([^\n]*)\n")  -- Each line including empty ones.

    self.next_line = self.line_it()
    self.line_index = 1
    self.char_index = 1
    self.skipped = false
    self.full = false
    self.need_letter_sound = true
    self.selected_choice = nil

    if dialog.skip ~= nil then
      -- The skip mode changes for this dialog.
      self.skip_mode = dialog.skip
    end

    -- Start displaying text.
    self:show_more_lines()
  end

  -- Returns whether there are more lines remaining to display after the current
  -- group of nb_visible_lines lines.
  function dialog_box:has_more_lines()
    return self.next_line ~= nil
  end

  -- Updates the result of is_full().
  function dialog_box:check_full()
    if self.line_index >= nb_visible_lines
        and self.char_index > #self.lines[nb_visible_lines] then
      self.full = true
    else
      self.full = false
    end
  end

  -- Returns whether all current lines of the dialog box are entirely
  -- displayed.
  function dialog_box:is_full()
    return self.full
  end

  -- Shows the next dialog of the sequence.
  -- Closes the dialog box if there is no next dialog.
  function dialog_box:show_next_dialog()

    local next_dialog_id = self.dialog.next

    if next_dialog_id ~= nil then
      -- Show the next dialog.
      self.first = false
      self.selected_choice = nil
      self.dialog = sol.language.get_dialog(next_dialog_id)
      self:show_dialog()
    else
      -- Finish the dialog, returning the choice or nil if there was no question.
      local status = self.selected_choice

      -- Conform to the built-in handling of shop treasures.
      if self.dialog.id == "_shop.question" then
        -- The engine expects a boolean answer after the "do you want to buy"
        -- shop treasure dialog.
        status = self.selected_choice == 2  -- "Yes" is on the second line.
      end

      game:stop_dialog(status)
    end
  end

  -- Starts showing a new group of nb_visible_lines lines in the dialog.
  -- Shows the next dialog (if any) if there are no remaining lines.
  function dialog_box:show_more_lines()

    self.gradual = true

    if not self:has_more_lines() then
      self:show_next_dialog()
      return
    end

    -- Hide the action icon and change the sword icon.
    if game.set_custom_command_effect ~= nil then
      game:set_custom_command_effect("action", nil)
      if self.skip_mode ~= "none" then
        game:set_custom_command_effect("attack", "skip")
        game:set_custom_command_effect("action", "next")
      else
        game:set_custom_command_effect("attack", nil)
      end
    end

    -- Prepare the lines.
    for i = 1, nb_visible_lines do
      self.line_surfaces[i]:set_text("")
      if self:has_more_lines() then
        self.lines[i] = self.next_line
        self.next_line = self.line_it()
      else
        self.lines[i] = ""
      end
    end
    self.line_index = 1
    self.char_index = 1

    if self.gradual then
      sol.timer.start(self, self.char_delay, repeat_show_character)
    end
  end

  -- Adds the next character to the dialog box.
  -- If this is a special character (like $0, $v, etc.),
  -- the corresponding action is performed.
  function dialog_box:add_character()

    local line = self.lines[self.line_index]
    local current_char = line:sub(self.char_index, self.char_index)
    if current_char == "" then
      error("No remaining character to add on this line")
    end
    self.char_index = self.char_index + 1
    local additional_delay = 0
    local text_surface = self.line_surfaces[self.line_index]

    -- Special characters:
    -- - $1, $2 and $3: slow, medium and fast
    -- - $0: pause
    -- - $v: variable
    -- - $?: cursor for a choice
    -- - space: don't add the delay
    -- - 110xxxx: multibyte character

    local special = false
    if current_char == "$" then
      -- Special character.

      special = true
      current_char = line:sub(self.char_index, self.char_index)
      self.char_index = self.char_index + 1

      if current_char == "0" then
        -- Pause.
        additional_delay = 1000

      elseif current_char == "1" then
        -- Slow.
        self.char_delay = char_delays["slow"]

      elseif current_char == "2" then
        -- Medium.
        self.char_delay = char_delays["medium"]

      elseif current_char == "3" then
        -- Fast.
        self.char_delay = char_delays["fast"]

      elseif current_char == "?" then
        -- Cursor for a choice.
        self:add_choice(self.line_index, self.char_index - 2)
        current_char = " "
        special = false
      else
        -- Not a special char, actually.
        text_surface:set_text(text_surface:get_text() .. "$")
        special = false
      end
    end

    if not special then
      -- Normal character to be displayed.
      text_surface:set_text(text_surface:get_text() .. current_char)

      -- If this is a multibyte character, also add the next byte.
      local byte = current_char:byte()
      if byte >= 192 and byte < 224 then
        -- The first byte is 110xxxxx: the character is stored with
        -- two bytes (utf-8).
        current_char = line:sub(self.char_index, self.char_index)
        self.char_index = self.char_index + 1
        text_surface:set_text(text_surface:get_text() .. current_char)
      end

      if current_char == " " then
        -- Remove the delay for whitespace characters.
        additional_delay = -self.char_delay
      end
    end

    if not special and current_char ~= nil and self.need_letter_sound then
      -- Play a letter sound sometimes.
      sol.audio.play_sound("message_letter")
      self.need_letter_sound = false
      sol.timer.start(self, letter_sound_delay, function()
        self.need_letter_sound = true
      end)
    end

    if self.gradual then
      sol.timer.start(self, self.char_delay + additional_delay, repeat_show_character)
    end
  end

  -- Stops displaying gradually the current lines,
  -- shows them immediately.
  -- If the lines were already finished, the next group of lines starts
  -- (if any).
  function dialog_box:show_all_now()

    if self:is_full() then
      self:show_more_lines()
    else
      self.gradual = false
      -- Check the end of the current line.
      self:check_full()
      while not self:is_full() do

        while not self:is_full()
            and self.char_index > #self.lines[self.line_index] do
          self.char_index = 1
          self.line_index = self.line_index + 1
          self:check_full()
        end

        if not self:is_full() then
          self:add_character()
        end
        self:check_full()
      end
    end
  end

  -- Marks that a line contains a selectable choice.
  -- A cursor will be displayed at the specified index when this
  -- line is selected.
  function dialog_box:add_choice(line_index, char_index)

    self.choices[line_index] = char_index
    if self.selected_choice == nil then
      self:set_selected_choice(line_index)
    end
  end

  function dialog_box:set_selected_choice(line_index)

    self.selected_choice = line_index

    if line_index ~= nil then
      self.choice_cursor_dst_position.x = self.box_dst_position.x + self.choices[line_index] * 6
      self.choice_cursor_dst_position.y = self.box_dst_position.y - 8 + line_index * 16
    end
  end

  function dialog_box:on_command_pressed(command)

    if command == "action" then

      -- Display more lines.
      if self:is_full() then
        self:show_more_lines()
      elseif self.skip_mode ~= "none" then
        self:show_all_now()
      end

    elseif command == "attack" then

      -- Attempt to skip the dialog.
      if self.skip_mode == "all" then
        self.skipped = true
        game:stop_dialog("skipped")
      elseif self:is_full() then
        self:show_more_lines()
      elseif self.skip_mode == "current" then
        self:show_all_now()
      end

    elseif command == "up" or command == "down" then

      if self.selected_choice ~= nil
          and not self:has_more_lines()
          and self:is_full() then

        sol.audio.play_sound("cursor")
        local line_index = self.selected_choice

        if command == "down" then
          -- Move the cursor downwards.
          repeat
            line_index = line_index % nb_visible_lines + 1
          until self.choices[line_index] ~= nil
        else
          -- Move the cursor upwards.
          repeat
            line_index = (line_index - 2) % nb_visible_lines + 1
          until self.choices[line_index] ~= nil
        end
        self:set_selected_choice(line_index)
      end
    end

    -- Don't propagate the event to anything below the dialog box.
    return true
  end

  function dialog_box:on_draw(dst_surface)

    local x, y = self.box_dst_position.x, self.box_dst_position.y

    self.dialog_surface:clear()

    if self.style == "box" then
      -- Draw the dialog box.
      self.box_img:draw(self.dialog_surface, x, y)
    end

    -- Draw the text.
    local text_x = x + 8
    local text_y = y + 8
    for i = 1, nb_visible_lines do
      self.line_surfaces[i]:draw(self.dialog_surface, text_x, text_y)
      text_y = text_y + 16
    end

    -- Draw the answer arrow.
    if self.selected_choice ~= nil then
      self.choice_cursor_img:draw(self.dialog_surface,
          self.choice_cursor_dst_position.x, self.choice_cursor_dst_position.y)
    end

    -- Final blit.
    self.dialog_surface:draw(dst_surface)
  end

  dialog_box:set_style("box")
end

-- Set up the dialog box on any game that starts.
local game_meta = sol.main.get_metatable("game")
game_meta:register_event("on_started", create_dialog_box)

return dialog_box_manager


The problem is that the scripts from the sample quest are not yet as modular as more recent scripts I made. There is an issue about that: https://github.com/solarus-games/solarus-sample-quest/issues/1

To be more specific, the sample quest scripts do not use the multi_events script (yet), so they don't mix well with scripts from the ALTTP pack like the ALTTP dialog box. I will improve that soon, but here is a solution in the meantime:

In your game_manager script, add require("scripts/multi_events") at the beginning, and then replace
Code (lua) Select

function game:on_started()
  ...
end

by
Code (lua) Select

game:register_event("on_started", function()
  ...
end)

Then the ALTTP dialog box should work well with the sample quest :)

Thank you Christopho! It worked perfectly.

Here is the code for anyone that wants it.

Code ( lua) Select
game:register_event("on_started", function()
    -- HUD menu.
    local hud = require("scripts/menus/hud")
    sol.menu.start(game, hud)
    hud:create(game)

    -- Mouse control.
    local mouse_control = require("scripts/menus/mouse_control")
    sol.menu.start(game, mouse_control)
    mouse_control:create(game, hud)

    local hero = game:get_hero()
    hero:set_tunic_sprite_id("main_heroes/eldran")
end)