This repository has been archived by the owner. It is now read-only.
Permalink
Cannot retrieve contributors at this time
541 lines (453 sloc)
15.6 KB
| local 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". | |
| vertical_position = "auto", -- "auto", "top" or "bottom". | |
| skip_mode = nil, -- "none", "current", "all" or "unchanged". | |
| icon_index = nil, -- Index of the 16x16 icon in hud/dialog_icons.png or nil. | |
| info = nil, -- Parameter passed to start_dialog(). | |
| skipped = false, -- Whether the player skipped the dialog. | |
| selected_answer = nil, -- Selected answer (1 or 2) 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 3 visible lines. | |
| line_surfaces = {}, -- Array of the 3 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 3 visible lines have shown all 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, | |
| box_img = nil, | |
| icons_img = nil, | |
| end_lines_sprite = nil, | |
| box_dst_position = nil, -- Destination coordinates of the dialog box. | |
| question_dst_position = nil, -- Destination coordinates of the question icon. | |
| icon_dst_position = nil, -- Destination coordinates of the icon. | |
| } | |
| -- Constants. | |
| local nb_visible_lines = 3 -- 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 | |
| -- Initializes the dialog box system. | |
| function game:initialize_dialog_box() | |
| game.dialog_box = dialog_box | |
| -- Initialize dialog box data. | |
| 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", | |
| } | |
| end | |
| dialog_box.dialog_surface = sol.surface.create(sol.video.get_quest_size()) | |
| dialog_box.dialog_surface:set_transparency_color{0, 0, 0} | |
| dialog_box.box_img = sol.surface.create("hud/dialog_box.png") | |
| dialog_box.icons_img = sol.surface.create("hud/dialog_icons.png") | |
| dialog_box.end_lines_sprite = sol.sprite.create("hud/dialog_box_message_end") | |
| game:set_dialog_style("box") | |
| end | |
| -- Exits the dialog box system. | |
| function game:quit_dialog_box() | |
| if dialog_box ~= nil then | |
| if game:is_dialog_enabled() then | |
| sol.menu.stop(dialog_box) | |
| end | |
| game.dialog_box = nil | |
| 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) | |
| sol.menu.stop(dialog_box) | |
| dialog_box.dialog = nil | |
| dialog_box.info = nil | |
| end | |
| -- Sets the style of the dialog box for subsequent dialogs. | |
| -- style must be one of: | |
| -- - "box" (default): Usual dialog box. | |
| -- - "empty": No decoration. | |
| function game:set_dialog_style(style) | |
| dialog_box.style = style | |
| if style == "box" then | |
| -- Make the dialog box slightly transparent. | |
| dialog_box.dialog_surface:set_opacity(216) | |
| end | |
| end | |
| -- Sets the vertical position of the dialog box for subsequent dialogs. | |
| -- vertical_position must be one of: | |
| -- - "auto" (default): Choose automatically so that the hero is not hidden. | |
| -- - "top": Top of the screen. | |
| -- - "bottom": Botton of the screen. | |
| function game:set_dialog_position(vertical_position) | |
| dialog_box.vertical_position = vertical_position | |
| 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 dialog_box:has_more_lines() | |
| or dialog_box.dialog.next ~= nil | |
| or dialog_box.selected_answer ~= nil then | |
| dialog_box.end_lines_sprite:set_animation("next") | |
| game:set_custom_command_effect("action", "next") | |
| else | |
| dialog_box.end_lines_sprite:set_animation("last") | |
| game:set_custom_command_effect("action", "return") | |
| end | |
| game:set_custom_command_effect("attack", nil) | |
| 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.icon_index = nil | |
| self.skip_mode = "none" | |
| self.char_delay = char_delays["fast"] | |
| self.selected_answer = nil | |
| -- Determine the position of the dialog box on the screen. | |
| local map = game:get_map() | |
| local camera_x, camera_y, camera_width, camera_height = map:get_camera_position() | |
| local top = false | |
| if self.vertical_position == "top" then | |
| top = true | |
| elseif self.vertical_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 x = camera_width / 2 - 110 | |
| local y = top and 32 or (camera_height - 96) | |
| if self.style == "empty" then | |
| y = y + (top and -24 or 24) | |
| end | |
| self.box_dst_position = { x = x, y = y } | |
| self.question_dst_position = { x = x + 18, y = y + 27 } | |
| self.icon_dst_position = { x = x + 18, y = y + 22 } | |
| self:show_dialog() | |
| end | |
| -- The dialog box is being closed. | |
| function dialog_box:on_finished() | |
| -- Remove overriden command effects. | |
| game:set_custom_command_effect("action", nil) | |
| game:set_custom_command_effect("attack", nil) | |
| 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 = self.style ~= "empty" | |
| if dialog.skip ~= nil then | |
| -- The skip mode changes for this dialog. | |
| self.skip_mode = dialog.skip | |
| end | |
| if dialog.icon ~= nil then | |
| -- The icon changes for this dialog ("-1" means none). | |
| if dialog.icon == "-1" then | |
| self.icon_index = nil | |
| else | |
| self.icon_index = dialog.icon | |
| end | |
| end | |
| if dialog.question == "1" then | |
| -- This dialog is a question. | |
| self.selected_answer = 1 -- The answer will be 1 or 2. | |
| end | |
| -- Start displaying text. | |
| self:show_more_lines() | |
| end | |
| -- Returns whether there are more lines remaining to display after the current | |
| -- 3 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 3 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 | |
| if self.selected_answer ~= 2 then | |
| -- No question or first answer | |
| next_dialog_id = self.dialog.next | |
| else | |
| -- Second answer. | |
| next_dialog_id = self.dialog.next2 | |
| end | |
| if next_dialog_id ~= nil and next_dialog_id ~= "_unknown" then | |
| -- Show the next dialog. | |
| self.first = false | |
| self.selected_answer = nil | |
| self.dialog = sol.language.get_dialog(next_dialog_id) | |
| self:show_dialog() | |
| else | |
| -- Finish the dialog, returning the answer or nil if there was no question. | |
| local status = self.selected_answer | |
| -- Conform to the built-in handling of shop items. | |
| if self.dialog.id == "_shop.question" then | |
| -- The engine expects a boolean answer after the "do you want to buy" | |
| -- shop item dialog. | |
| status = self.selected_answer == 1 | |
| end | |
| game:stop_dialog(status) | |
| end | |
| end | |
| -- Starts showing a new group of 3 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. | |
| 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 | |
| -- Prepare the 3 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 | |
| -- - 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"] | |
| 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 3 lines, shows them immediately. | |
| -- If the 3 lines were already finished, the next group of 3 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 | |
| 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_answer ~= nil | |
| and not self:has_more_lines() | |
| and self:is_full() then | |
| sol.audio.play_sound("cursor") | |
| self.selected_answer = 3 - self.selected_answer -- Switch between 1 and 2. | |
| self.question_dst_position.y = self.box_dst_position.y + | |
| (self.selected_answer == 1 and 27 or 40) | |
| 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:fill_color{0, 0, 0} | |
| if self.style == "empty" then | |
| -- Draw a dark rectangle. | |
| dst_surface:fill_color({0, 0, 0}, x, y, 220, 60) | |
| else | |
| -- Draw the dialog box. | |
| self.box_img:draw_region(0, 0, box_width, box_height, self.dialog_surface, x, y) | |
| end | |
| -- Draw the text. | |
| local text_x = x + (self.icon_index == nil and 16 or 48) | |
| local text_y = y - 1 | |
| for i = 1, nb_visible_lines do | |
| text_y = text_y + 13 | |
| if self.selected_answer ~= nil | |
| and i == nb_visible_lines - 1 | |
| and not self:has_more_lines() then | |
| -- The last two lines are the answer to a question. | |
| text_x = text_x + 24 | |
| end | |
| self.line_surfaces[i]:draw(self.dialog_surface, text_x, text_y) | |
| end | |
| -- Draw the icon. | |
| if self.icon_index ~= nil then | |
| local row, column = math.floor(self.icon_index / 10), self.icon_index % 10 | |
| self.icons_img:draw_region(16 * column, 16 * row, 16, 16, | |
| self.dialog_surface, | |
| self.icon_dst_position.x, self.icon_dst_position.y) | |
| self.question_dst_position.x = x + 50 | |
| else | |
| self.question_dst_position.x = x + 18 | |
| end | |
| -- Draw the question arrow. | |
| if self.selected_answer ~= nil | |
| and self:is_full() | |
| and not self:has_more_lines() then | |
| self.box_img:draw_region(96, 60, 8, 8, self.dialog_surface, | |
| self.question_dst_position.x, self.question_dst_position.y) | |
| end | |
| -- Draw the end message arrow. | |
| if self:is_full() then | |
| self.end_lines_sprite:draw(self.dialog_surface, x + 103, y + 56) | |
| end | |
| -- Final blit. | |
| self.dialog_surface:draw(dst_surface) | |
| end | |