From 9e5f454aa5aad163705c50e720db82d823d48f33 Mon Sep 17 00:00:00 2001 From: Major- Date: Wed, 25 Feb 2015 16:38:19 +0000 Subject: [PATCH] Add dialogue support. --- data/plugins/dialogue/dialogue.rb | 426 ++++++++++++++++++++++++++++++ data/plugins/dialogue/plugin.xml | 16 ++ 2 files changed, 442 insertions(+) create mode 100644 data/plugins/dialogue/dialogue.rb create mode 100644 data/plugins/dialogue/plugin.xml diff --git a/data/plugins/dialogue/dialogue.rb b/data/plugins/dialogue/dialogue.rb new file mode 100644 index 00000000..301cf771 --- /dev/null +++ b/data/plugins/dialogue/dialogue.rb @@ -0,0 +1,426 @@ +require 'java' + +java_import 'org.apollo.game.model.inter.dialogue.DialogueAdapter' +java_import 'org.apollo.game.message.impl.CloseInterfaceMessage' +java_import 'org.apollo.game.message.impl.SetWidgetItemModelMessage' +java_import 'org.apollo.game.message.impl.SetWidgetNpcModelMessage' +java_import 'org.apollo.game.message.impl.SetWidgetPlayerModelMessage' +java_import 'org.apollo.game.message.impl.SetWidgetTextMessage' + +# Defines a dialogue, with the specified name and block. +def dialogue(name, &block) + raise 'Dialogues must have a name and block.' if (name.nil? || block.nil?) + + dialogue = Dialogue.new(name) + dialogue.instance_eval(&block) + dialogue.wrap + DIALOGUES[name] = dialogue +end + +# Defines an opening (i.e. conversation starter) dialogue, which hooks into the chain. +# Allows for a lambda prerequisite to be passed, which takes one argument the player; if the prerequisite evaluates to false, the dialogue will not be opened. +def opening_dialogue(name, prerequisite=nil, &block) + dialogue = dialogue(name, &block) + npc = dialogue.npc + raise 'Npc cannot be null when opening a dialogue.' if npc.nil? + + on :message, :first_npc_action, npc do |ctx, player, event| + player.open_dialogue(name) if (prerequisite.nil? || prerequisite.call(player)) + end +end + +# Declares an emote, with the specified name and id. +def declare_emote(name, id) + EMOTES[name] = id +end + + + +private + +# The hash of dialogue names to dialogues. +DIALOGUES = {} + +# The hash of emote names to ids. +EMOTES = {} + +# The maximum amount of lines of text that can be displayed on a dialogue. +MAXIMUM_LINE_COUNT = 4 + +# The maximum amount of options that can be displayed on a dialogue. +MAXIMUM_OPTION_COUNT = 5 + +# The maximum width of a line, in characters. +MAXIMUM_LINE_WIDTH = 55 + +# The possible types of a dialogue. +DIALOGUE_TYPES = [ :message_with_item, :message_with_model, :npc_speech, :options, :player_speech, :text ] + +# A type of dialogue. +class Dialogue + attr_reader :emote, :name, :media, :options, :text, :title, :type + + # Initializes the Dialogue. + def initialize(name) + @name = name.to_s + @text = [] + @options = [] + end + + # Closes the dialogue interface when the player clicks the 'Click here to continue...' text. + def close + continue(:close => true) + end + + # Defines the event that occurs when a player clicks the 'Click here to continue...' text. + def continue(type=nil, &block) + raise 'Cannot add a continue event on a dialogue with options.' unless @options.size.zero? + raise 'Must declare either a type or a block for a continue event.' if (type.nil? && block.nil?) + + @options << (block.nil? ? get_next_dialogue(type) : block) + end + + # Sets the emote performed by the dialogue head. + def emote(emote=nil) + raise 'Can only perform an emote on :player_speech or :npc_speech dialogues.' unless [ :npc_speech, :player_speech ].include?(@type) + @emote = EMOTES[emote] if emote.kind_of?(Symbol) + @emote + end + + # Gets the media of this dialogue. + def media() + case @type + when :message_with_item then @item + when :npc_speech then @npc + when :message_with_model then @model + else raise "Cannot get media for #{@type}." + end + end + + # Sets the id of the item displayed. + def item(item=nil, scale=nil) + unless item.nil? + raise 'Can only display an item on :message_with_item dialogues.' unless @type == :message_with_item + @item = item + @item_scale = scale + end + + @item + end + + # Sets the id of the model displayed. + def model(model=nil) + unless model.nil? + raise 'Can only display a model on :message_with_model dialogues.' unless @type == :message_with_model + @model = model + end + + @model + end + + # Sets the id of the npc displayed. + def npc(npc=nil) + raise 'Can only display an npc on :npc_speech dialogues.' unless @type == :npc_speech + @npc = lookup_npc(npc) unless npc.nil? + @npc + end + + # Defines an option, displaying the specified message. + def option(message, type) + raise 'Can only display options on an :options dialogue.' unless @type == :options + raise "Cannot display more than #{MAXIMUM_OPTION_COUNT} options on a dialogue." unless @options.size < MAXIMUM_OPTION_COUNT + + @options[text.size] = get_next_dialogue(type) + @text << message + end + + # Gets the array of options. + def options + @options.dup + end + + # Appends a message to the text list. + def text(*message) + unless message.nil? + @text.concat(message) + end + + @text + end + + # Sets the title of the dialogue. + def title(title=nil) + @title = title unless title.nil? + @title + end + + # Sets the type of dialogue. + def type(type=nil) + unless type.nil? + verify_dialogue_type(type) + @type = type + end + + @type + end + + # Wraps text in this Dialogue, inserting extra Dialogues in the chain if necessary. + def wrap + lines = [] + next if @type == :options + + text = @text[0] + segments = []# text.chars.each_slice(MAXIMUM_LINE_WIDTH).map(&:join) # Split text into array of strings with length <= 60. + previous = 0; index = MAXIMUM_LINE_WIDTH + + while index < text.length + index -= 1 until text[index] == ' ' + segments << text[previous..index] + previous = index + index += MAXIMUM_LINE_WIDTH + end + segments << text[previous..text.length] + + if (segments.size <= MAXIMUM_LINE_COUNT) + lines.concat(segments) + @text = @text.drop(1) + insert_copy(@text) if @text.size > 0 + else + remaining = MAXIMUM_LINE_COUNT - segments.size + lines.concat(segments.first(remaining)) + insert_copy(segments.drop(remaining).join().concat(@text.drop(1))) + end + + @text = lines + end + + + # Copies the value of every variable from the specified Dialogue, optionally updating the text array. + def copy_from(dialogue, text=nil) + @emote = dialogue.emote + @item = dialogue.item + @model = dialogue.model + @npc = dialogue.npc + @options = dialogue.options + @text = if text.nil? then dialogue.text.dup else text.dup end + @type = dialogue.type + end + + private + + # Inserts a copy of this Dialogue into the chain, but with different text. + def insert_copy(text) + name = @name + index = name.index('-auto-inserted-') + + id = if index.nil? then 0 else name[name.rindex('-')..-1].to_i + 1 end + index ||= -1 + name = "#{name[0..index]}-auto-inserted-#{id}" + + dialogue = Dialogue.new(name) + dialogue.copy_from(self, text.dup) + dialogue.wrap() + + DIALOGUES[name] = dialogue + @options[0] = ->(player) { send_dialogue(player, dialogue) } + end + + # Decodes the next dialogue interface from the hash, returning a proc. + def get_next_dialogue(hash) + hash.keys.each do |key| + case key + when :close + return ->(player) { player.send(CloseInterfaceMessage.new) } + when :dialogue + return ->(player) { send_dialogue(player, lookup_dialogue(hash[key])) } + else raise "Unrecognised dialogue continue type #{key}." + end + end + end + +end + +# The existing Player class. +class Player + + # Opens the dialogue with the specified name. + def open_dialogue(name) + dialogue = lookup_dialogue(name) + send_dialogue(self, dialogue) + end + +end + + + +# Gets a Dialogue using the name it was registered with. +def lookup_dialogue(name) + dialogue = DIALOGUES[name] + raise "No dialogue named #{name.to_s}." if dialogue.nil? + + dialogue +end + +# Sends the specified dialogue. +def send_dialogue(player, dialogue) + type = dialogue.type + + case type + when :message_with_item then send_item_dialogue(player, dialogue) + when :message_with_model then send_model_dialogue(player, dialogue) + when :npc_speech then send_npc_dialogue(player, dialogue) + when :options then send_options_dialogue(player, dialogue) + when :player_speech then send_player_dialogue(player, dialogue) + when :text then send_text_dialogue(player, dialogue) + else raise "Unrecognised dialogue type #{type}." + end +end + +# The dialogue interface ids for dialogues that only display text, ordered by line count. +TEXT_DIALOGUE_IDS = [ 356, 359, 363, 368, 374 ] + +# The dialogue interface ids for dialogues that display the head of the player, ordered by line count. +PLAYER_DIALOGUE_IDS = [ 968, 973, 979, 986 ] + +# The dialogue interface ids for dialogues that display the head of an npc, ordered by line count. +NPC_DIALOGUE_IDS = [ 4882, 4887, 4893, 4900 ] + +# The dialogue interface ids for option dialogues, ordered by (option_count - 1) +OPTIONS_DIALOGUE_IDS = [ 2459, 2469, 2480, 2492 ] + +# Sends a dialogue displaying only text. +def send_text_dialogue(player, dialogue) + title = dialogue.title + send_generic_dialogue(player, dialogue, title, TEXT_DIALOGUE_IDS) +end + +# Sends a dialogue displaying the player's head. +def send_player_dialogue(player, dialogue) + send_generic_dialogue(player, dialogue, PLAYERS_DIALOGUE_IDS, ->(id) { SetWidgetPlayerModelMessage.new(id + 1) }) +end + +# Sends a dialogue displaying the head of an npc. +def send_npc_dialogue(player, dialogue) + npc = dialogue.npc + name = NpcDefinition.lookup(npc).name.to_s + name = "" if (name.nil? || name == "null") + + send_generic_dialogue(player, dialogue, name, NPC_DIALOGUE_IDS, ->(id) { SetWidgetNpcModelMessage.new(id + 1, npc)}) +end + + +# Sends a dialogue displaying an event. +def send_generic_dialogue(player, dialogue, title, ids, event=nil) + text = dialogue.text + dialogue_id = ids[text.size - 1] + player.send(event.call(dialogue_id)) unless event.nil? + + set_text(player, dialogue_title_id(dialogue_id), title) + + text.each_index { |index| set_text(player, dialogue_text_id(dialogue_id, index), text[index]) } + player.interface_set.open_dialogue(ContinueDialogueAdapter.new(player, dialogue.options[0]), dialogue_id) # TODO listener!!! +end + + +# Sends an options dialogue interface. +def send_options_dialogue(player, dialogue) + options = dialogue.options + size = options.size + raise 'Illegal options count: must be between 2 and 5, inclusive.' unless (2..5).include?(size) + + text = dialogue.text + dialogue_id = OPTIONS_DIALOGUE_IDS[size - 1] + + question = dialogue.title + set_text(player, dialogue_question_id(dialogue_id), question) + + text.each_index { |index| set_text(player, dialogue_option_id(dialogue_id, index), text[index]) } + player.interface_set.open_dialogue(OptionDialogueAdapter.new(player, options), dialogue_id) # TODO listener!!! +end + + +# A DialogueAdapter for dialogues with a 'Click here to continue...' message. +class ContinueDialogueAdapter < DialogueAdapter + + # Creates the ContinueDialogueAdadpter. + def initialize(player, continue) + super() + @player = player + @continue = continue + end + + # Executes the 'continue' lambda when the player clicks the 'Click here to continue...' message. + def continued() + @continue.call(@player) + end + +end + + +# A DialogueAdapter for dialogues with a set of options that can be selected. +class OptionDialogueAdapter < DialogueAdapter + + # Creates the OptionDialogueAdadpter. + def initialize(player, options) + super() + @player = player + @options = options.dup + end + + # Executes an option. + def button_clicked(button) + option = OPTIONS_DIALOGUE_IDS.find_index(button) + options[option].call(@player) + end + +end + + +# Gets the widget id of the question, for an options dialogue interface. +def dialogue_question_id(id) + id + 1 +end + +# Gets the widget id of a dialogue option. +def dialogue_option_id(id, option) + id + 1 + option +end + +# Gets the widget id of a dialogue text line. +def dialogue_text_id(id, line) + id + 3 + line +end + +# Gets the widget id of a dialogue title. +def dialogue_title_id(id) + id + 2 +end + +# Sets the text of a widget. +def set_text(player, id, message) + player.send(SetWidgetTextMessage.new(id, message)) +end + +# Verifies that the dialogue type exists. +def verify_dialogue_type(type) + raise "Unrecognised dialogue type #{type}, expected one of #{DIALOGUE_TYPES}." unless DIALOGUE_TYPES.include?(type) +end + +# The spacing of each character glyph, for the font used for dialogue. TODO decode the font from the cache. +GLYPH_SPACING = [ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 7, 14, 9, 12, 12, 4, 5, + 5, 10, 8, 4, 8, 4, 7, 9, 7, 9, 8, 8, 8, 9, 7, 9, 9, 4, 5, 7, + 9, 7, 9, 14, 9, 8, 8, 8, 7, 7, 9, 8, 6, 8, 8, 7, 10, 9, 9, 8, + 9, 8, 8, 6, 9, 8, 10, 8, 8, 8, 6, 7, 6, 9, 10, 5, 8, 8, 7, 8, + 8, 7, 8, 8, 4, 7, 7, 4, 10, 8, 8, 8, 8, 6, 8, 6, 8, 8, 9, 8, + 8, 8, 6, 4, 6, 12, 3, 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 8, 11, 8, 8, 4, 8, 7, 12, 6, 7, 9, 5, 12, 5, 6, 10, 6, 6, 6, + 8, 8, 4, 5, 5, 6, 7, 11, 11, 11, 9, 9, 9, 9, 9, 9, 9, 13, 8, 8, + 8, 8, 8, 4, 4, 5, 4, 8, 9, 9, 9, 9, 9, 9, 8, 10, 9, 9, 9, 9, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 13, 6, 8, 8, 8, 8, 4, 4, 5, 4, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 ] + +def get_width(char) + +end \ No newline at end of file diff --git a/data/plugins/dialogue/plugin.xml b/data/plugins/dialogue/plugin.xml new file mode 100644 index 00000000..667266b9 --- /dev/null +++ b/data/plugins/dialogue/plugin.xml @@ -0,0 +1,16 @@ + + + dialogue + 0.1 + Dialogue + Adds dialogue support. + + Major + + + + + + util + + \ No newline at end of file