Add dialogue support.

This commit is contained in:
Major-
2015-02-25 16:38:19 +00:00
parent 612ed89ba7
commit 9e5f454aa5
2 changed files with 442 additions and 0 deletions
+426
View File
@@ -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
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<plugin>
<id>dialogue</id>
<version>0.1</version>
<name>Dialogue</name>
<description>Adds dialogue support.</description>
<authors>
<author>Major</author>
</authors>
<scripts>
<script>dialogue.rb</script>
</scripts>
<dependencies>
<dependency>util</dependency>
</dependencies>
</plugin>