Merge branch 'animations' into dev

This commit is contained in:
Maruno17
2025-01-16 22:56:15 +00:00
275 changed files with 40954 additions and 26 deletions

View File

@@ -0,0 +1,140 @@
#===============================================================================
# Container module for control classes.
#===============================================================================
module UIControls; end
#===============================================================================
#
#===============================================================================
module UIControls::StyleMixin
COLOR_SCHEMES = {
:dark => {
:background_color => Color.new(32, 32, 32),
:text_color => Color.white,
:disabled_text_color => Color.new(96, 96, 96),
:line_color => Color.white,
:disabled_fill_color => Color.new(128, 128, 128),
:hover_color => Color.new(64, 80, 80),
:capture_color => Color.new(224, 32, 96),
:highlight_color => Color.new(160, 128, 16),
# Sidebars
# :delete_icon_color => Color.new(248, 96, 96), # Unchanged
# Checkbox
:checked_color => Color.new(32, 160, 32),
:unchecked_color => Color.new(160, 160, 160),
# ParticleList
# :position_line_color => Color.new(248, 96, 96), # Unchanged
:after_end_bg_color => Color.new(80, 80, 80),
:se_background_color => Color.new(160, 160, 160),
:property_background_color => Color.new(96, 96, 96),
# ParticleList and Canvas
:focus_colors => {
:foreground => Color.new(80, 112, 248), # Blue
:midground => Color.new(80, 112, 248), # Blue
:background => Color.new(80, 112, 248), # Blue
:user => Color.new(32, 192, 32), # Green
:target => Color.new(192, 32, 32), # Red
:user_and_target => Color.new(192, 192, 32), # Yellow
:user_side_foreground => Color.new(80, 208, 208), # Cyan
:user_side_background => Color.new(80, 208, 208), # Cyan
:target_side_foreground => Color.new(80, 208, 208), # Cyan
:target_side_background => Color.new(80, 208, 208) # Cyan
}
}
}
FOCUS_COLORS = {
:foreground => Color.new(128, 160, 248), # Blue
:midground => Color.new(128, 160, 248), # Blue
:background => Color.new(128, 160, 248), # Blue
:user => Color.new(64, 224, 64), # Green
:target => Color.new(224, 64, 64), # Red
:user_and_target => Color.new(224, 224, 64), # Yellow
:user_side_foreground => Color.new(128, 224, 224), # Cyan
:user_side_background => Color.new(128, 224, 224), # Cyan
:target_side_foreground => Color.new(128, 224, 224), # Cyan
:target_side_background => Color.new(128, 224, 224) # Cyan
}
def color_scheme_options
return {
:light => _INTL("Light"),
:dark => _INTL("Dark")
}
end
#-----------------------------------------------------------------------------
def background_color
return get_color_scheme_color_for_element(:background_color, Color.white)
end
def semi_transparent_color
return get_color_scheme_color_for_element(:semi_transparent_color, Color.new(0, 0, 0, 128))
end
#-----------------------------------------------------------------------------
def text_color
return get_color_scheme_color_for_element(:text_color, Color.black)
end
def disabled_text_color
return get_color_scheme_color_for_element(:disabled_text_color, Color.new(160, 160, 160))
end
def text_size
return 18 # Default is 22 if size isn't explicitly set
end
def line_color
return get_color_scheme_color_for_element(:line_color, Color.black)
end
def delete_icon_color
return get_color_scheme_color_for_element(:delete_icon_color, Color.new(248, 96, 96))
end
#-----------------------------------------------------------------------------
def disabled_fill_color
return get_color_scheme_color_for_element(:disabled_fill_color, Color.gray)
end
def hover_color
return get_color_scheme_color_for_element(:hover_color, Color.new(224, 255, 255))
end
def capture_color
return get_color_scheme_color_for_element(:capture_color, Color.new(255, 64, 128))
end
def highlight_color
return get_color_scheme_color_for_element(:highlight_color, Color.new(224, 192, 32))
end
#-----------------------------------------------------------------------------
def color_scheme=(value)
return if @color_scheme == value
@color_scheme = value
self.bitmap.font.color = text_color
self.bitmap.font.size = text_size
invalidate if self.respond_to?(:invalidate)
end
def get_color_scheme_color_for_element(element, default)
if COLOR_SCHEMES[@color_scheme] && COLOR_SCHEMES[@color_scheme][element]
return COLOR_SCHEMES[@color_scheme][element]
end
return default
end
def focus_color(focus)
if COLOR_SCHEMES[@color_scheme] && COLOR_SCHEMES[@color_scheme][:focus_colors] &&
COLOR_SCHEMES[@color_scheme][:focus_colors][focus]
return COLOR_SCHEMES[@color_scheme][:focus_colors][focus]
end
return FOCUS_COLORS[focus] || Color.magenta
end
end

View File

@@ -0,0 +1,247 @@
#===============================================================================
# Controls are arranged in a list in self's bitmap. Each control is given an
# area of size "self's bitmap's width" x LINE_SPACING to draw itself in.
#===============================================================================
class UIControls::ControlsContainer
attr_reader :x, :y
attr_accessor :label_offset_x, :label_offset_y
attr_reader :controls
attr_reader :values
attr_reader :visible
attr_reader :viewport
LINE_SPACING = 28
OFFSET_FROM_LABEL_X = 100
OFFSET_FROM_LABEL_Y = 0
include UIControls::StyleMixin
def initialize(x, y, width, height, right_margin = 0)
self.color_scheme = :light
@viewport = Viewport.new(x, y, width, height)
@viewport.z = 99999
@x = x
@y = y
@width = width
@height = height
@right_margin = right_margin
@label_offset_x = OFFSET_FROM_LABEL_X
@label_offset_y = OFFSET_FROM_LABEL_Y
@controls = []
@row_count = 0
@pixel_offset = 0
@captured = nil
@visible = true
end
def dispose
@controls.each { |c| c[1]&.dispose }
@controls.clear
@viewport.dispose
end
#-----------------------------------------------------------------------------
def visible=(value)
@visible = value
@controls.each { |c| c[1].visible = value }
repaint if @visible
end
def color_scheme=(value)
return if @color_scheme == value
@color_scheme = value
if @controls
@controls.each { |c| c[1].color_scheme = value }
repaint
end
end
#-----------------------------------------------------------------------------
def busy?
return !@captured.nil?
end
def changed?
return !@values.nil?
end
def clear_changed
@values = nil
end
def get_control(id)
ret = nil
@controls.each do |c|
ret = c[1] if c[0] == id
break if ret
end
return ret
end
#-----------------------------------------------------------------------------
def add_label(id, label, has_label = false)
id = (id.to_s + "_label").to_sym if !has_label
add_control(id, UIControls::Label.new(*control_size(has_label), @viewport, label), has_label)
end
def add_labelled_label(id, label, text)
add_label(id, label)
add_label(id, text, true)
end
def add_header_label(id, label)
ctrl = UIControls::Label.new(*control_size, @viewport, label)
ctrl.header = true
add_control(id, ctrl)
end
def add_checkbox(id, value, has_label = false)
add_control(id, UIControls::Checkbox.new(*control_size(has_label), @viewport, value), has_label)
end
def add_labelled_checkbox(id, label, value)
add_label(id, label)
add_checkbox(id, value, true)
end
def add_text_box(id, value, has_label = false)
add_control(id, UIControls::TextBox.new(*control_size(has_label), @viewport, value), has_label)
end
def add_labelled_text_box(id, label, value)
add_label(id, label)
add_text_box(id, value, true)
end
def add_number_slider(id, min_value, max_value, value, has_label = false)
add_control(id, UIControls::NumberSlider.new(*control_size(has_label), @viewport, min_value, max_value, value), has_label)
end
def add_labelled_number_slider(id, label, min_value, max_value, value)
add_label(id, label)
add_number_slider(id, min_value, max_value, value, true)
end
def add_number_text_box(id, min_value, max_value, value, has_label = false)
add_control(id, UIControls::NumberTextBox.new(*control_size(has_label), @viewport, min_value, max_value, value), has_label)
end
def add_labelled_number_text_box(id, label, min_value, max_value, value)
add_label(id, label)
add_number_text_box(id, min_value, max_value, value, true)
end
def add_button(id, button_text, has_label = false)
add_control(id, UIControls::Button.new(*control_size(has_label), @viewport, button_text), has_label)
end
def add_labelled_button(id, label, button_text)
add_label(id, label)
add_button(id, button_text, true)
end
def add_list(id, rows, options, has_label = false)
size = control_size(has_label)
size[0] -= 8
size[1] = rows * UIControls::List::ROW_HEIGHT
add_control(id, UIControls::List.new(*size, @viewport, options), has_label, rows)
end
def add_labelled_list(id, label, rows, options)
add_label(id, label)
add_list(id, rows, options, true)
end
def add_dropdown_list(id, options, value, has_label = false)
add_control(id, UIControls::DropdownList.new(*control_size(has_label), @viewport, options, value), has_label)
end
def add_labelled_dropdown_list(id, label, options, value)
add_label(id, label)
add_dropdown_list(id, options, value, true)
end
def add_text_box_dropdown_list(id, options, value, has_label = false)
add_control(id, UIControls::TextBoxDropdownList.new(*control_size(has_label), @viewport, options, value), has_label)
end
def add_labelled_text_box_dropdown_list(id, label, options, value)
add_label(id, label)
add_text_box_dropdown_list(id, options, value, true)
end
#-----------------------------------------------------------------------------
def repaint
@controls.each { |ctrl| ctrl[1].repaint }
end
def refresh; end
#-----------------------------------------------------------------------------
def update
return if !@visible
# Update controls
if @captured
@captured.update
@captured = nil if !@captured.busy?
else
@controls.each do |ctrl|
ctrl[1].update
@captured = ctrl[1] if ctrl[1].busy?
end
end
# Check for updated controls
@controls.each do |ctrl|
next if !ctrl[1].changed?
@values ||= {}
@values[ctrl[0]] = ctrl[1].value
ctrl[1].clear_changed
end
# Redraw controls if needed
repaint
end
#-----------------------------------------------------------------------------
def control_size(has_label = false)
if has_label
return @width - @label_offset_x - @right_margin, LINE_SPACING - @label_offset_y
end
return @width, LINE_SPACING
end
def next_control_position(add_offset = false)
row_x = 0
row_x += @label_offset_x if add_offset
row_y = @row_count * LINE_SPACING
row_y += @label_offset_y - LINE_SPACING if add_offset
row_y += @pixel_offset
return row_x, row_y
end
def add_control_at(id, control, x, y)
control.x = x
control.y = y
control.color_scheme = @color_scheme
control.set_interactive_rects
@controls.push([id, control])
repaint
end
def add_control(id, control, add_offset = false, rows = 1)
ctrl_x, ctrl_y = next_control_position(add_offset)
ctrl_x += 4 if control.is_a?(UIControls::List)
add_control_at(id, control, ctrl_x, ctrl_y)
increment_row_count(rows) if !add_offset
@pixel_offset -= (LINE_SPACING - UIControls::List::ROW_HEIGHT) * (rows - 1) if control.is_a?(UIControls::List)
end
def increment_row_count(count)
@row_count += count
end
end

View File

@@ -0,0 +1,206 @@
#===============================================================================
#
#===============================================================================
class UIControls::BaseControl < BitmapSprite
attr_reader :value
attr_accessor :disabled
TEXT_OFFSET_Y = 5
include UIControls::StyleMixin
def initialize(width, height, viewport)
super(width, height, viewport)
self.color_scheme = :light
@disabled = false
@hover_area = nil # Is a symbol from the keys for @interactions if the mouse is hovering over that interaction
@captured_area = nil # Is a symbol from the keys for @interactions (or :none) if this control is clicked in
clear_changed
invalidate
end
#-----------------------------------------------------------------------------
def width
return self.bitmap.width
end
def height
return self.bitmap.height
end
def visible=(value)
super
@captured_area = nil if !self.visible
end
#-----------------------------------------------------------------------------
def mouse_pos
mouse_coords = Mouse.getMousePos
return nil, nil if !mouse_coords
ret_x = mouse_coords[0] - self.viewport.rect.x - self.x
ret_y = mouse_coords[1] - self.viewport.rect.y - self.y
return ret_x, ret_y
end
def mouse_in_control?
return false if !@interactions || @interactions.empty?
mouse_x, mouse_y = mouse_pos
return false if !mouse_x || !mouse_y
return @interactions.any? { |area, rect| rect.contains?(mouse_x, mouse_y) }
end
def disabled?
return @disabled
end
def disable
return if disabled?
@disabled = true
@hover_area = nil
invalidate
end
def enable
return if !disabled?
@disabled = false
invalidate
end
def invalid?
return @invalid
end
# Marks that the control must be redrawn to reflect current logic.
def invalidate
@invalid = true
end
# Makes the control no longer invalid. Called after repainting.
def validate
@invalid = false
end
def busy?
return self.visible && !@captured_area.nil?
end
def changed?
return @changed
end
def set_changed
@changed = true
end
def clear_changed
@changed = false
end
#-----------------------------------------------------------------------------
def set_interactive_rects
@interactions = {}
end
#-----------------------------------------------------------------------------
def draw_text(this_bitmap, text_x, text_y, this_text)
text_size = this_bitmap.text_size(this_text.to_s)
this_bitmap.draw_text(text_x, text_y, text_size.width, text_size.height, this_text.to_s, 0)
end
def draw_text_centered(this_bitmap, text_x, text_y, wid, this_text)
text_size = this_bitmap.text_size(this_text.to_s)
this_bitmap.draw_text(text_x, text_y, wid, text_size.height, this_text.to_s, 1)
end
# Redraws the control only if it is invalid.
def repaint
return if !invalid?
refresh
validate
end
def refresh
self.bitmap.clear
draw_area_highlight
end
def draw_area_highlight
return if !@interactions || @interactions.empty?
if !@captured_area || @hover_area == @captured_area
# Draw mouse hover over area highlight
rect = @interactions[@hover_area]
self.bitmap.fill_rect(rect.x, rect.y, rect.width, rect.height, hover_color) if rect
elsif @captured_area
# Draw captured area highlight
rect = @interactions[@captured_area]
self.bitmap.fill_rect(rect.x, rect.y, rect.width, rect.height, capture_color) if rect
end
end
#-----------------------------------------------------------------------------
# This method is only called if the mouse is in the game window and this
# control has interactive elements.
def on_mouse_press
return if !@interactions || @interactions.empty?
return if @captured_area
@captured_area = nil
mouse_x, mouse_y = mouse_pos
return if !mouse_x || !mouse_y
@interactions.each_pair do |area, rect|
next if !rect.contains?(mouse_x, mouse_y)
@captured_area = area
invalidate
break
end
end
def on_mouse_release
@captured_area = nil
invalidate
end
def update_hover_highlight
# Remove the hover highlight if there are no interactions for this control
# or if the mouse is off-screen
mouse_x, mouse_y = mouse_pos
if !@interactions || @interactions.empty? || !mouse_x || !mouse_y
invalidate if @hover_area
@hover_area = nil
return
end
# Check each interactive area for whether the mouse is hovering over it, and
# set @hover_area accordingly
in_area = false
@interactions.each_pair do |area, rect|
next if !rect.contains?(mouse_x, mouse_y)
invalidate if @hover_area != area
@hover_area = area
in_area = true
break
end
if !in_area
invalidate if @hover_area
@hover_area = nil
end
end
# Updates the logic on the control, invalidating it if necessary.
def update
return if !self.visible
return if disabled? && !busy? # This control still works if it becomes disabled while using it
update_hover_highlight
# Detect a mouse press/release
if @interactions && !@interactions.empty?
if Input.trigger?(Input::MOUSELEFT)
on_mouse_press
elsif busy? && Input.release?(Input::MOUSELEFT)
on_mouse_release
end
end
end
end

View File

@@ -0,0 +1,43 @@
#===============================================================================
#
#===============================================================================
class UIControls::Label < UIControls::BaseControl
attr_reader :text
def initialize(width, height, viewport, text)
super(width, height, viewport)
@text = text
@header = false
end
#-----------------------------------------------------------------------------
def text=(value)
@text = value
refresh
end
def header=(val)
@header = val
refresh
end
def text_width
return self.bitmap.text_size(@text).width
end
#-----------------------------------------------------------------------------
def refresh
super
if @header
draw_text_centered(self.bitmap, 0, TEXT_OFFSET_Y, width, @text)
# Draw underline
text_size = self.bitmap.text_size(@text)
self.bitmap.fill_rect((width - text_size.width) / 2, TEXT_OFFSET_Y + text_size.height,
text_size.width, 1, line_color)
else
draw_text(self.bitmap, 4, TEXT_OFFSET_Y, @text)
end
end
end

View File

@@ -0,0 +1,82 @@
#===============================================================================
# NOTE: Strictly speaking, this is a toggle switch and not a checkbox.
#===============================================================================
class UIControls::Checkbox < UIControls::BaseControl
CHECKBOX_X = 2
CHECKBOX_WIDTH = 40
CHECKBOX_HEIGHT = 24
CHECKBOX_FILL_SIZE = CHECKBOX_HEIGHT - 4
def initialize(width, height, viewport, value = false)
super(width, height, viewport)
@value = value
end
#-----------------------------------------------------------------------------
def value=(new_value)
return if @value == new_value
@value = new_value
invalidate
end
def checked_color
return get_color_scheme_color_for_element(:checked_color, Color.new(48, 192, 48))
end
def unchecked_color
return get_color_scheme_color_for_element(:unchecked_color, Color.gray)
end
#-----------------------------------------------------------------------------
def set_interactive_rects
@checkbox_rect = Rect.new(CHECKBOX_X, (height - CHECKBOX_HEIGHT) / 2,
CHECKBOX_WIDTH, CHECKBOX_HEIGHT)
@interactions = {
:checkbox => @checkbox_rect
}
end
#-----------------------------------------------------------------------------
def refresh
super
# Draw disabled colour
if disabled?
self.bitmap.fill_rect(@checkbox_rect.x, @checkbox_rect.y,
@checkbox_rect.width, @checkbox_rect.height,
disabled_fill_color)
end
# Draw checkbox outline
self.bitmap.outline_rect(@checkbox_rect.x, @checkbox_rect.y,
@checkbox_rect.width, @checkbox_rect.height,
line_color)
# Draw checkbox fill
box_x = (@value) ? @checkbox_rect.width - CHECKBOX_FILL_SIZE - 2 : 2
if disabled?
box_color = disabled_text_color
else
box_color = (@value) ? checked_color : unchecked_color
end
self.bitmap.fill_rect(@checkbox_rect.x + box_x, @checkbox_rect.y + 2,
CHECKBOX_FILL_SIZE, CHECKBOX_FILL_SIZE, box_color)
self.bitmap.outline_rect(@checkbox_rect.x + box_x, @checkbox_rect.y + 2,
CHECKBOX_FILL_SIZE, CHECKBOX_FILL_SIZE, line_color)
end
#-----------------------------------------------------------------------------
def on_mouse_release
return if !@captured_area # Wasn't captured to begin with
# Change this control's value
if @captured_area == :checkbox
mouse_x, mouse_y = mouse_pos
if mouse_x && mouse_y && @interactions[@captured_area].contains?(mouse_x, mouse_y)
@value = !@value # The actual change of this control's value
set_changed
end
end
super # Make this control not busy again
end
end

View File

@@ -0,0 +1,318 @@
#===============================================================================
#
#===============================================================================
class UIControls::TextBox < UIControls::BaseControl
attr_accessor :box_width
TEXT_BOX_X = 2
TEXT_BOX_WIDTH = 200
TEXT_BOX_HEIGHT = 24
TEXT_BOX_PADDING = 4 # Gap between sides of text box and text
def initialize(width, height, viewport, value = "")
super(width, height, viewport)
@value = value
@box_width = TEXT_BOX_WIDTH
@cursor_pos = -1
@display_pos = 0
@cursor_timer = nil
@cursor_shown = false
@blacklist = []
end
#-----------------------------------------------------------------------------
def value
return @value.dup
end
def value=(new_value)
return if @value.to_s == new_value.to_s
@value = new_value.to_s.dup
invalidate
end
def insert_char(ch)
@value.insert(@cursor_pos, ch)
@cursor_pos += 1
@cursor_timer = System.uptime
@cursor_shown = true
invalidate
end
def delete_at(index)
@value = @value.to_s
@value.slice!(index)
@cursor_pos -= 1 if @cursor_pos > index
@cursor_timer = System.uptime
@cursor_shown = true
invalidate
end
def cursor_pos=(val)
@cursor_pos = val
reset_display_pos
@cursor_timer = System.uptime
@cursor_shown = true
invalidate
end
def set_blacklist(*list)
@blacklist = list
invalidate
end
#-----------------------------------------------------------------------------
def get_cursor_index_from_mouse_position
char_widths = []
@value.to_s.length.times { |i| char_widths[i] = self.bitmap.text_size(@value.to_s[i]).width }
mouse_x, mouse_y = mouse_pos
mouse_x -= @text_box_rect.x + TEXT_BOX_PADDING
return 0 if mouse_x < 0
(@display_pos...char_widths.length).each do |i|
mouse_x -= char_widths[i]
if mouse_x <= 0
return (mouse_x.abs >= char_widths[i] / 2) ? i : i + 1
end
end
return @value.to_s.length
end
def disabled?
val = (@value.respond_to?("strip!")) ? @value.strip : @value
return true if @blacklist.include?(val)
return super
end
def busy?
return @cursor_pos >= 0 if @captured_area == :text_box
return super
end
#-----------------------------------------------------------------------------
def set_interactive_rects
@text_box_rect = Rect.new(TEXT_BOX_X, (height - TEXT_BOX_HEIGHT) / 2,
[@box_width, width - (TEXT_BOX_X * 2)].min, TEXT_BOX_HEIGHT)
@interactions = {
:text_box => @text_box_rect
}
end
def reset_interaction
@cursor_pos = -1
@display_pos = 0
@cursor_timer = nil
@initial_value = nil
Input.text_input = false
invalidate
end
def reset_display_pos
box_width = @text_box_rect.width - (TEXT_BOX_PADDING * 2)
char_widths = []
@value.to_s.length.times { |i| char_widths[i] = self.bitmap.text_size(@value.to_s[i]).width }
# Text isn't wider than the box
if char_widths.sum <= box_width
return false if @display_pos == 0
@display_pos = 0
return true
end
display_pos_changed = false
# Ensure the cursor hasn't gone off the left side of the text box
if @cursor_pos < @display_pos
@display_pos = @cursor_pos
display_pos_changed = true
end
# Ensure the cursor hasn't gone off the right side of the text box
if @cursor_pos > @display_pos
loop do
cursor_x = 0
(@display_pos...@cursor_pos).each do |i|
cursor_x += char_widths[i] if char_widths[i]
end
break if cursor_x < box_width
@display_pos += 1
display_pos_changed = true
break if @display_pos == @cursor_pos
end
end
# Ensure there isn't empty space on the right if the text can be moved to
# the right to fill it
if @display_pos > 0
cursor_x = 0
(@display_pos...char_widths.length).each do |i|
cursor_x += char_widths[i] if char_widths[i]
end
loop do
cursor_x += char_widths[@display_pos - 1]
break if cursor_x >= box_width
@display_pos -= 1
display_pos_changed = true
break if @display_pos == 0
end
end
return display_pos_changed
end
#-----------------------------------------------------------------------------
def draw_area_highlight
return if @captured_area == :text_box && (@hover_area == @captured_area || !Input.press?(Input::MOUSELEFT))
super
end
def draw_cursor(cursor_x)
return if !@cursor_shown || @cursor_pos < 0
cursor_y_offset = ((height - TEXT_BOX_HEIGHT) / 2) + 2
cursor_height = height - (cursor_y_offset * 2)
bitmap.fill_rect(cursor_x, cursor_y_offset, 2, cursor_height, text_color)
end
def refresh
super
# Draw disabled colour
if disabled?
self.bitmap.fill_rect(@text_box_rect.x, @text_box_rect.y,
@text_box_rect.width, @text_box_rect.height,
disabled_fill_color)
end
# Draw text box outline
self.bitmap.outline_rect(@text_box_rect.x, @text_box_rect.y,
@text_box_rect.width, @text_box_rect.height,
line_color)
# Draw value
char_x = @text_box_rect.x + TEXT_BOX_PADDING
last_char_index = @display_pos
(@value.to_s.length - @display_pos).times do |i|
char = @value.to_s[@display_pos + i]
char_width = self.bitmap.text_size(char).width
cannot_display_next_char = char_x + char_width > @text_box_rect.x + @text_box_rect.width - TEXT_BOX_PADDING
draw_text(self.bitmap, char_x, TEXT_OFFSET_Y, char) if !cannot_display_next_char
# Draw cursor
draw_cursor(char_x - 1) if @display_pos + i == @cursor_pos
break if cannot_display_next_char
last_char_index = @display_pos + i
char_x += char_width
end
# Draw cursor at end
draw_cursor(char_x - 1) if @cursor_pos == @value.to_s.length
# Draw left/right arrows to indicate more text beyond the text box sides
arrow_color = (disabled?) ? disabled_text_color : text_color
if @display_pos > 0
bitmap.fill_rect(@text_box_rect.x, (height / 2) - 4, 1, 8, background_color)
5.times do |i|
bitmap.fill_rect(@text_box_rect.x - 2 + i, (height / 2) - (i + 1), 1, 2 * (i + 1), arrow_color)
end
end
if last_char_index < @value.to_s.length - 1
bitmap.fill_rect(@text_box_rect.x + @text_box_rect.width - 1, (height / 2) - 4, 1, 8, background_color)
5.times do |i|
bitmap.fill_rect(@text_box_rect.x + @text_box_rect.width + 1 - i, (height / 2) - (i + 1), 1, 2 * (i + 1), arrow_color)
end
end
end
#-----------------------------------------------------------------------------
def on_mouse_press
@captured_area = nil
super
if @captured_area == :text_box
# Clicked into the text box; put the text cursor in there
@cursor_pos = get_cursor_index_from_mouse_position
@cursor_timer = System.uptime
invalidate
else
@value.strip! if @value.respond_to?("strip!")
@value = @initial_value if disabled?
set_changed if @initial_value && @value != @initial_value
reset_interaction
end
end
def on_mouse_release
return if !@captured_area # Wasn't captured to begin with
# Start text entry if clicked and released mouse button in the text box
if @captured_area == :text_box
mouse_x, mouse_y = mouse_pos
if mouse_x && mouse_y && @interactions[@captured_area].contains?(mouse_x, mouse_y)
@initial_value = @value.clone
Input.text_input = true
invalidate
return # This control is still captured
end
end
# Released mouse button outside of text box, or initially clicked outside of
# text box; end interaction with this control
@value.strip! if @value.respond_to?("strip!")
@value = @initial_value if disabled?
set_changed if @initial_value && @value != @initial_value
reset_interaction
super # Make this control not busy again
end
def update_special_inputs
# Left/right to move cursor
if Input.triggerex?(:LEFT) || Input.repeatex?(:LEFT)
self.cursor_pos = @cursor_pos - 1 if @cursor_pos > 0
elsif Input.triggerex?(:RIGHT) || Input.repeatex?(:RIGHT)
self.cursor_pos = @cursor_pos + 1 if @cursor_pos < @value.to_s.length
end
# Home/End to jump to start/end of the text
if Input.triggerex?(:HOME) || Input.repeatex?(:HOME)
self.cursor_pos = 0
elsif Input.triggerex?(:END) || Input.repeatex?(:END)
self.cursor_pos = @value.to_s.length
end
# Backspace/Delete to remove text
if Input.triggerex?(:BACKSPACE) || Input.repeatex?(:BACKSPACE)
delete_at(@cursor_pos - 1) if @cursor_pos > 0
elsif Input.triggerex?(:DELETE) || Input.repeatex?(:DELETE)
delete_at(@cursor_pos) if @cursor_pos < @value.to_s.length
end
# Return/Escape to end text input (Escape undoes the change)
if Input.triggerex?(:RETURN) || Input.repeatex?(:RETURN) ||
Input.triggerex?(:KP_ENTER) || Input.repeatex?(:KP_ENTER)
@value.strip! if @value.respond_to?("strip!")
@value = @initial_value if disabled?
set_changed if @initial_value && @value != @initial_value
reset_interaction
@captured_area = nil
elsif Input.triggerex?(:ESCAPE) || Input.repeatex?(:ESCAPE)
@value = @initial_value if @initial_value
reset_interaction
@captured_area = nil
end
end
def update_text_entry
ret = false
Input.gets.each_char do |ch|
insert_char(ch)
ret = true
end
return ret
end
def update
return if !self.visible
super
# Make the cursor flash
if @captured_area == :text_box
cursor_to_show = ((System.uptime - @cursor_timer) / 0.35).to_i.even?
if cursor_to_show != @cursor_shown
@cursor_shown = cursor_to_show
invalidate
end
old_cursor_pos = @cursor_pos
# Update cursor movement, deletions and ending text input
update_special_inputs
return if @cursor_pos != old_cursor_pos || !busy?
# Detect character input and add them to @value
char_inserted = update_text_entry
invalidate if reset_display_pos || char_inserted
end
end
end

View File

@@ -0,0 +1,128 @@
#===============================================================================
#
#===============================================================================
class UIControls::NumberSlider < UIControls::BaseControl
attr_reader :min_value
attr_reader :max_value
PLUS_MINUS_SIZE = 16
SLIDER_PADDING = 6 # Gap between sides of interactive area for slider and drawn slider bar
MINUS_X = 0
SLIDER_X = MINUS_X + PLUS_MINUS_SIZE + SLIDER_PADDING
SLIDER_LENGTH = 128
PLUS_X = SLIDER_X + SLIDER_LENGTH + SLIDER_PADDING
VALUE_X = PLUS_X + PLUS_MINUS_SIZE + 5
def initialize(width, height, viewport, min_value, max_value, value)
super(width, height, viewport)
@min_value = min_value
@max_value = max_value
self.value = value
end
#-----------------------------------------------------------------------------
def value=(new_value)
old_val = @value
@value = new_value.to_i.clamp(self.min_value, self.max_value)
self.invalidate if @value != old_val
end
def min_value=(new_min)
return if new_min == @min_value
@min_value = new_min
@value = @value.clamp(self.min_value, self.max_value)
self.invalidate
end
def max_value=(new_max)
return if new_max == @max_value
@max_value = new_max
@value = @value.clamp(self.min_value, self.max_value)
self.invalidate
end
#-----------------------------------------------------------------------------
def set_interactive_rects
@slider_rect = Rect.new(SLIDER_X - SLIDER_PADDING, (self.height - PLUS_MINUS_SIZE) / 2, SLIDER_LENGTH + (SLIDER_PADDING * 2), PLUS_MINUS_SIZE)
@minus_rect = Rect.new(MINUS_X, (self.height - PLUS_MINUS_SIZE) / 2, PLUS_MINUS_SIZE, PLUS_MINUS_SIZE)
@plus_rect = Rect.new(PLUS_X, (self.height - PLUS_MINUS_SIZE) / 2, PLUS_MINUS_SIZE, PLUS_MINUS_SIZE)
@interactions = {
:slider => @slider_rect,
:minus => @minus_rect,
:plus => @plus_rect
}
end
#-----------------------------------------------------------------------------
def draw_area_highlight
# Don't want to ever highlight the slider with the capture color, because
# the mouse doesn't need to be on the slider to change this control's value
if @captured_area == :slider
rect = @interactions[@captured_area]
self.bitmap.fill_rect(rect.x, rect.y, rect.width, rect.height, hover_color) if rect
else
super
end
end
def refresh
super
button_color = (disabled?) ? disabled_text_color : text_color
# Draw minus button
self.bitmap.fill_rect(@minus_rect.x + 2, @minus_rect.y + (@minus_rect.height / 2) - 2, @minus_rect.width - 4, 4, button_color)
# Draw slider bar
self.bitmap.fill_rect(SLIDER_X, (self.height / 2) - 1, SLIDER_LENGTH, 2, text_color)
# Draw notches on slider bar
5.times do |i|
self.bitmap.fill_rect(SLIDER_X - 1 + (i * SLIDER_LENGTH / 4), (self.height / 2) - 2, 2, 4, text_color)
end
# Draw slider knob
fraction = (self.value - self.min_value) / (self.max_value.to_f - self.min_value)
knob_x = (SLIDER_LENGTH * fraction).to_i
self.bitmap.fill_rect(SLIDER_X + knob_x - 4, (self.height / 2) - 6, 8, 12, button_color)
# Draw plus button
self.bitmap.fill_rect(@plus_rect.x + 2, @plus_rect.y + (@plus_rect.height / 2) - 2, @plus_rect.width - 4, 4, button_color)
self.bitmap.fill_rect(@plus_rect.x + (@plus_rect.width / 2) - 2, @plus_rect.y + 2, 4, @plus_rect.height - 4, button_color)
# Draw value text
draw_text(self.bitmap, VALUE_X, TEXT_OFFSET_Y, self.value.to_s)
end
#-----------------------------------------------------------------------------
def on_mouse_press
super
@initial_value = @value if @captured_area
end
def on_mouse_release
return if !@captured_area # Wasn't captured to begin with
set_changed if @initial_value && @value != @initial_value
@initial_value = nil
super
end
def update
return if !self.visible
super
case @captured_area
when :minus
# Constant decrement of value while pressing the minus button
if @hover_area == @captured_area && Input.repeat?(Input::MOUSELEFT)
self.value -= 1
end
when :plus
# Constant incrementing of value while pressing the plus button
if @hover_area == @captured_area && Input.repeat?(Input::MOUSELEFT)
self.value += 1
end
when :slider
# Constant updating of value depending on mouse's x position
mouse_x, mouse_y = mouse_pos
return if !mouse_x || !mouse_y
self.value = lerp(self.min_value, self.max_value + (self.max_value & 1), SLIDER_LENGTH, mouse_x - SLIDER_X)
end
end
end

View File

@@ -0,0 +1,145 @@
#===============================================================================
#
#===============================================================================
class UIControls::NumberTextBox < UIControls::TextBox
attr_reader :min_value
attr_reader :max_value
PLUS_MINUS_SIZE = 16
CONTROL_PADDING = 2 # Gap between buttons and text box
MINUS_X = 0
TEXT_BOX_X = MINUS_X + PLUS_MINUS_SIZE + CONTROL_PADDING
TEXT_BOX_WIDTH = 64
TEXT_BOX_HEIGHT = 24
PLUS_X = TEXT_BOX_X + TEXT_BOX_WIDTH + CONTROL_PADDING
def initialize(width, height, viewport, min_value, max_value, value)
super(width, height, viewport, value)
@min_value = min_value
@max_value = max_value
self.value = value
end
#-----------------------------------------------------------------------------
def value=(new_value)
old_val = @value.to_i
@value = new_value.to_i.clamp(self.min_value, self.max_value)
invalidate if @value != old_val
end
def min_value=(new_min)
return if new_min == @min_value
@min_value = new_min
@value = @value.to_i.clamp(self.min_value, self.max_value)
invalidate
end
def max_value=(new_max)
return if new_max == @max_value
@max_value = new_max
@value = @value.to_i.clamp(self.min_value, self.max_value)
invalidate
end
def insert_char(ch, index = -1)
@value = @value.to_s.insert((index >= 0) ? index : @cursor_pos, ch)
@cursor_pos += 1
@cursor_timer = System.uptime
@cursor_shown = true
invalidate
end
#-----------------------------------------------------------------------------
def set_interactive_rects
@text_box_rect = Rect.new(TEXT_BOX_X, (height - TEXT_BOX_HEIGHT) / 2,
TEXT_BOX_WIDTH, TEXT_BOX_HEIGHT)
@minus_rect = Rect.new(MINUS_X, (self.height - PLUS_MINUS_SIZE) / 2, PLUS_MINUS_SIZE, PLUS_MINUS_SIZE)
@plus_rect = Rect.new(PLUS_X, (self.height - PLUS_MINUS_SIZE) / 2, PLUS_MINUS_SIZE, PLUS_MINUS_SIZE)
@interactions = {
:text_box => @text_box_rect,
:minus => @minus_rect,
:plus => @plus_rect
}
end
def reset_interaction
super
self.value = @value # Turn value back into a number and clamp it
end
#-----------------------------------------------------------------------------
def refresh
super
button_color = (disabled?) ? disabled_text_color : text_color
# Draw minus button
self.bitmap.fill_rect(@minus_rect.x + 2, @minus_rect.y + (@minus_rect.height / 2) - 2, @minus_rect.width - 4, 4, button_color)
# Draw plus button
self.bitmap.fill_rect(@plus_rect.x + 2, @plus_rect.y + (@plus_rect.height / 2) - 2, @plus_rect.width - 4, 4, button_color)
self.bitmap.fill_rect(@plus_rect.x + (@plus_rect.width / 2) - 2, @plus_rect.y + 2, 4, @plus_rect.height - 4, button_color)
end
#-----------------------------------------------------------------------------
def on_mouse_press
@captured_area = nil
super
if @captured_area == :text_box
# Clicked into the text box; put the text cursor in there
@cursor_pos = get_cursor_index_from_mouse_position
@cursor_timer = System.uptime
invalidate
elsif @captured_area
@initial_value = @value
else
reset_interaction
set_changed if @initial_value && @value != @initial_value
end
end
def update_text_entry
ret = false
Input.gets.each_char do |ch|
case ch
when "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
if (@value.to_s == "-0" && @cursor_pos > 1) ||
(@value.to_s == "0" && @cursor_pos > 0)
@value = @value.to_s.chop
@cursor_pos -= 1
end
insert_char(ch)
ret = true
when "-", "+"
@value = @value.to_s
if @value[0] == "-"
delete_at(0) # Remove the negative sign
ret = true
elsif ch == "-"
insert_char(ch, 0) # Add a negative sign at the start
ret = true
end
next
end
end
return ret
end
def update
return if !self.visible
super
case @captured_area
when :minus
# Constant decrement of value while pressing the minus button
if @hover_area == @captured_area && Input.repeat?(Input::MOUSELEFT)
self.value -= 1
end
when :plus
# Constant incrementing of value while pressing the plus button
if @hover_area == @captured_area && Input.repeat?(Input::MOUSELEFT)
self.value += 1
end
end
end
end

View File

@@ -0,0 +1,122 @@
#===============================================================================
#
#===============================================================================
class UIControls::Button < UIControls::BaseControl
BUTTON_X = 2
BUTTON_Y = 2
BUTTON_PADDING = 10 # Used when @fixed_size is false
BUTTON_HEIGHT = 28 # Used when @fixed_size is false
TEXT_BASE_OFFSET_Y = 18 # Text is centred vertically in the button
def initialize(width, height, viewport, text = "")
super(width, height, viewport)
@text = text
@fixed_size = false
@highlight = false
end
#-----------------------------------------------------------------------------
def set_fixed_size
@fixed_size = true
end
def set_text(val)
return if @text == val
@text = val
set_interactive_rects if !@fixed_size
invalidate
end
#-----------------------------------------------------------------------------
def disabled?
return highlighted? || super
end
def set_changed
@value = true
super
end
def clear_changed
@value = false
super
end
def highlighted?
return @highlight
end
def set_highlighted
return if highlighted?
@highlight = true
invalidate
end
def set_not_highlighted
return if !highlighted?
@highlight = false
invalidate
end
#-----------------------------------------------------------------------------
def set_interactive_rects
@interactions&.clear
button_width = (@fixed_size) ? width - (BUTTON_X * 2) : self.bitmap.text_size(@text).width + (BUTTON_PADDING * 2)
button_height = (@fixed_size) ? height - (2 * BUTTON_Y) : BUTTON_HEIGHT
button_height = [button_height, height - (2 * BUTTON_Y)].min
@button_rect = Rect.new(BUTTON_X, (height - button_height) / 2, button_width, button_height)
@interactions = {
:button => @button_rect
}
end
#-----------------------------------------------------------------------------
def refresh
super
if highlighted?
# Draw highligted colour
self.bitmap.fill_rect(@button_rect.x, @button_rect.y,
@button_rect.width, @button_rect.height,
highlight_color)
elsif disabled?
# Draw disabled colour
self.bitmap.fill_rect(@button_rect.x, @button_rect.y,
@button_rect.width, @button_rect.height,
disabled_fill_color)
end
# Draw button outline
self.bitmap.outline_rect(@button_rect.x, @button_rect.y,
@button_rect.width, @button_rect.height,
line_color)
# Draw inner grey ring that shows this is a button rather than a text box
if !disabled?
shade = line_color.clone
shade.alpha = (shade.red > 128) ? 160 : 64
self.bitmap.outline_rect(@button_rect.x + 2, @button_rect.y + 2,
@button_rect.width - 4, @button_rect.height - 4,
shade, 1)
end
# Draw button text
draw_text_centered(self.bitmap, @button_rect.x,
@button_rect.y + (@button_rect.height - TEXT_BASE_OFFSET_Y) / 2,
@button_rect.width, @text)
end
#-----------------------------------------------------------------------------
def on_mouse_release
return if !@captured_area # Wasn't captured to begin with
# Change this control's value
if @captured_area == :button
mouse_x, mouse_y = mouse_pos
if mouse_x && mouse_y && @interactions[@captured_area].contains?(mouse_x, mouse_y)
set_changed
end
end
super # Make this control not busy again
end
end

View File

@@ -0,0 +1,38 @@
#===============================================================================
#
#===============================================================================
class UIControls::BitmapButton < UIControls::Button
BUTTON_PADDING = 4
def initialize(x, y, viewport, button_bitmap, disabled_bitmap = nil)
super(button_bitmap.width + (BUTTON_PADDING * 2), button_bitmap.height + (BUTTON_PADDING * 2), viewport)
self.x = x
self.y = y
@button_bitmap = button_bitmap
@disabled_bitmap = disabled_bitmap
end
#-----------------------------------------------------------------------------
def set_interactive_rects
@interactions&.clear
@button_rect = Rect.new(0, 0, width, height)
@interactions = {
:button => @button_rect
}
end
#-----------------------------------------------------------------------------
def refresh
super
# Draw button bitmap
if @disabled_bitmap && disabled?
self.bitmap.blt(BUTTON_PADDING, BUTTON_PADDING, @disabled_bitmap,
Rect.new(0, 0, @disabled_bitmap.width, @disabled_bitmap.height))
else
self.bitmap.blt(BUTTON_PADDING, BUTTON_PADDING, @button_bitmap,
Rect.new(0, 0, @button_bitmap.width, @button_bitmap.height))
end
end
end

View File

@@ -0,0 +1,278 @@
#===============================================================================
#
#===============================================================================
class UIControls::List < UIControls::BaseControl
BORDER_THICKNESS = 2
ROW_HEIGHT = 24
TEXT_PADDING_X = 4
TEXT_OFFSET_Y = 3
def initialize(width, height, viewport, values = [])
super(width, height, viewport)
@scrollbar = UIControls::Scrollbar.new(
width - UIControls::Scrollbar::SLIDER_WIDTH - BORDER_THICKNESS, BORDER_THICKNESS,
height - (BORDER_THICKNESS * 2), viewport
)
@scrollbar.color_scheme = @color_scheme
@scrollbar.set_interactive_rects
@scrollbar.range = ROW_HEIGHT
@scrollbar.z = self.z + 1
@rows_count = (height / ROW_HEIGHT).floor # Number of rows visible at once
@top_row = 0
@selected = -1
self.values = values
end
def dispose
@scrollbar.dispose
@scrollbar = nil
super
end
#-----------------------------------------------------------------------------
def x=(new_val)
super(new_val)
@scrollbar.x = new_val + width - UIControls::Scrollbar::SLIDER_WIDTH - BORDER_THICKNESS
end
def y=(new_val)
super(new_val)
@scrollbar.y = new_val + BORDER_THICKNESS
end
def z=(new_val)
super(new_val)
@scrollbar.z = new_val + 1
end
def visible=(new_val)
super
@scrollbar.visible = new_val
end
def color_scheme=(value)
return if @color_scheme == value
@color_scheme = value
self.bitmap.font.color = text_color
self.bitmap.font.size = text_size
@scrollbar&.color_scheme = value
invalidate if self.respond_to?(:invalidate)
end
# Each value in @values is an array: [id, text].
def values=(new_vals)
@values = new_vals
set_interactive_rects
@scrollbar.range = [@values.length, 1].max * ROW_HEIGHT
if @scrollbar.visible
self.top_row = (@scrollbar.position.to_f / ROW_HEIGHT).round
else
self.top_row = 0
end
self.selected = -1 if @selected >= @values.length
invalidate
end
# Returns the ID of the selected row.
def value
return nil if @selected < 0
if @values.is_a?(Array)
return (@values[@selected].is_a?(Array)) ? @values[@selected][0] : @selected
elsif @values.is_a?(Hash)
return @values.keys[@selected]
end
return nil
end
def top_row=(val)
old_val = @top_row
@top_row = val
if @scrollbar.visible
@top_row = @top_row.clamp(0, @values.length - @rows_count)
else
@top_row = 0
end
invalidate if @top_row != old_val
end
def selected=(val)
return if @selected == val
@selected = val
invalidate
end
#-----------------------------------------------------------------------------
def mouse_in_control?
mouse_x, mouse_y = mouse_pos
return false if !mouse_x || !mouse_y
return true if Rect.new(0, 0, width, height).contains?(mouse_x, mouse_y)
return true if @scrollbar.mouse_in_control?
return false
end
def busy?
return !@captured_area.nil?
end
#-----------------------------------------------------------------------------
def set_interactive_rects
@interactions = {}
@values.length.times do |i|
@interactions[i] = Rect.new(
BORDER_THICKNESS, BORDER_THICKNESS + (ROW_HEIGHT * i),
width - (BORDER_THICKNESS * 2), ROW_HEIGHT
)
end
end
#-----------------------------------------------------------------------------
def draw_area_highlight
# If a row is captured, it will automatically be selected and the selection
# colour will be drawn over the highlight. There's no point drawing a
# highlight at all if anything is captured.
return if @captured_area
# Draw mouse hover over row highlight
rect = @interactions[@hover_area]
if rect
rect_y = rect.y
rect_y -= @top_row * ROW_HEIGHT if @hover_area.is_a?(Integer)
self.bitmap.fill_rect(rect.x, rect_y, rect.width, rect.height, hover_color)
end
end
def repaint
@scrollbar.repaint if @scrollbar.invalid?
super if invalid?
end
def refresh
super
# Draw control outline
self.bitmap.outline_rect(0, 0, width, height, line_color)
# Draw text options
@values.each_with_index do |val, i|
next if i < @top_row || i >= @top_row + @rows_count
if @selected == i
self.bitmap.fill_rect(
@interactions[i].x,
@interactions[i].y - (@top_row * ROW_HEIGHT),
@interactions[i].width, @interactions[i].height,
highlight_color
)
end
txt = (val.is_a?(Array)) ? val[1] : val.to_s
old_text_color = self.bitmap.font.color
if txt[/^\\c\[([0-9]+)\]/i]
text_colors = [
[ 0, 112, 248], [120, 184, 232], # 1 Blue
[232, 32, 16], [248, 168, 184], # 2 Red
[ 96, 176, 72], [174, 208, 144], # 3 Green
[ 72, 216, 216], [168, 224, 224], # 4 Cyan
[208, 56, 184], [232, 160, 224], # 5 Magenta
[232, 208, 32], [248, 232, 136], # 6 Yellow
[160, 160, 168], [208, 208, 216], # 7 Gray
[240, 240, 248], [200, 200, 208], # 8 White
[114, 64, 232], [184, 168, 224], # 9 Purple
[248, 152, 24], [248, 200, 152], # 10 Orange
MessageConfig::DARK_TEXT_MAIN_COLOR,
MessageConfig::DARK_TEXT_SHADOW_COLOR, # 11 Dark default
MessageConfig::LIGHT_TEXT_MAIN_COLOR,
MessageConfig::LIGHT_TEXT_SHADOW_COLOR # 12 Light default
]
self.bitmap.font.color = Color.new(*text_colors[2 * ($1.to_i - 1)])
txt = txt.gsub(/^\\c\[[0-9]+\]/i, "")
end
draw_text(self.bitmap,
@interactions[i].x + TEXT_PADDING_X,
@interactions[i].y + TEXT_OFFSET_Y - (@top_row * ROW_HEIGHT),
txt)
self.bitmap.font.color = old_text_color
end
end
#-----------------------------------------------------------------------------
def on_mouse_press
@captured_area = nil
mouse_x, mouse_y = mouse_pos
return if !mouse_x || !mouse_y
return if @scrollbar.visible && (@scrollbar.busy? || mouse_x >= @scrollbar.x - self.x)
# Check for mouse presses on rows
mouse_y += @top_row * ROW_HEIGHT
@interactions.each_pair do |area, rect|
next if !area.is_a?(Integer) || area < @top_row || area >= @top_row + @rows_count
next if !rect.contains?(mouse_x, mouse_y)
@captured_area = area
invalidate
break
end
end
def on_mouse_release
return if !@captured_area # Wasn't captured to begin with
set_changed
super
end
def update_hover_highlight
# Remove the hover highlight if there are no interactions for this control
# or if the mouse is off-screen
mouse_x, mouse_y = mouse_pos
if !@interactions || @interactions.empty? || !mouse_x || !mouse_y
invalidate if @hover_area
@hover_area = nil
return
end
# Don't update the highlight if the mouse is using the scrollbar
if @scrollbar.visible && (@scrollbar.busy? || mouse_x >= @scrollbar.x - self.x)
invalidate if @hover_area
@hover_area = nil
return
end
# Check each interactive area for whether the mouse is hovering over it, and
# set @hover_area accordingly
in_area = false
mouse_y += @top_row * ROW_HEIGHT
@interactions.each_pair do |area, rect|
next if !area.is_a?(Integer) || area < @top_row || area >= @top_row + @rows_count
next if !rect.contains?(mouse_x, mouse_y)
invalidate if @hover_area != area
@hover_area = area
in_area = true
break
end
if !in_area
invalidate if @hover_area
@hover_area = nil
end
end
def update
return if !self.visible
@scrollbar.update
super
# Refresh the list's position if changed by moving the scrollbar
self.top_row = (@scrollbar.position.to_f / ROW_HEIGHT).round
# Set the selected row to the row the mouse is over, if clicked on
if @captured_area
@selected = @hover_area if @hover_area.is_a?(Integer)
elsif @hover_area
wheel_v = Input.scroll_v
scroll_dist = UIControls::Scrollbar::SCROLL_DISTANCE
scroll_dist /= 2 if @values.length / @rows_count > 20 # Arbitrary 20
if wheel_v > 0 # Scroll up
@scrollbar.slider_top -= scroll_dist
elsif wheel_v < 0 # Scroll down
@scrollbar.slider_top += scroll_dist
end
if wheel_v != 0
self.top_row = (@scrollbar.position.to_f / ROW_HEIGHT).round
update_hover_highlight
end
end
end
end

View File

@@ -0,0 +1,167 @@
#===============================================================================
#
#===============================================================================
class UIControls::DropdownList < UIControls::BaseControl
attr_accessor :box_width
attr_accessor :max_rows
TEXT_BOX_X = 2
TEXT_BOX_WIDTH = 200
TEXT_BOX_HEIGHT = 24
TEXT_BOX_PADDING = 4 # Gap between sides of text box and text
MAX_LIST_ROWS = 10
# NOTE: options is a hash: keys are symbols, values are display names.
def initialize(width, height, viewport, options, value)
super(width, height, viewport)
@options = options
@value = value
@box_width = TEXT_BOX_WIDTH
@toggling_dropdown_list = false
@max_rows = MAX_LIST_ROWS
end
def dispose
remove_dropdown_menu
super
end
#-----------------------------------------------------------------------------
def values=(new_vals)
@options = new_vals
@dropdown_menu.values = @options if @dropdown_menu
end
def value=(new_value)
return if @value == new_value
@value = new_value
invalidate
end
#-----------------------------------------------------------------------------
def busy?
return true if @dropdown_menu || @toggling_dropdown_list
return super
end
#-----------------------------------------------------------------------------
def set_interactive_rects
@button_rect = Rect.new(TEXT_BOX_X, (height - TEXT_BOX_HEIGHT) / 2,
[@box_width, width - (TEXT_BOX_X * 2)].min, TEXT_BOX_HEIGHT)
@interactions = {
:button => @button_rect
}
end
def make_dropdown_menu
menu_height = (UIControls::List::ROW_HEIGHT * [@options.length, @max_rows].min) + (UIControls::List::BORDER_THICKNESS * 2)
# Draw menu's background
@dropdown_menu_bg = BitmapSprite.new(@button_rect.width, menu_height, self.viewport)
@dropdown_menu_bg.x = self.x + @button_rect.x
@dropdown_menu_bg.y = self.y + @button_rect.y + @button_rect.height
@dropdown_menu_bg.z = self.z + 1
@dropdown_menu_bg.bitmap.font.color = text_color
@dropdown_menu_bg.bitmap.font.size = text_size
@dropdown_menu_bg.bitmap.fill_rect(0, 0, @dropdown_menu_bg.width, @dropdown_menu_bg.height, background_color)
# Create menu
@dropdown_menu = UIControls::List.new(@button_rect.width, menu_height, self.viewport, @options)
@dropdown_menu.x = @dropdown_menu_bg.x
@dropdown_menu.y = @dropdown_menu_bg.y
@dropdown_menu.z = self.z + 2
@dropdown_menu.color_scheme = @color_scheme
@dropdown_menu.set_interactive_rects
@dropdown_menu.repaint
end
def remove_dropdown_menu
@dropdown_menu_bg&.dispose
@dropdown_menu_bg = nil
@dropdown_menu&.dispose
@dropdown_menu = nil
@captured_area = nil
end
#-----------------------------------------------------------------------------
def draw_area_highlight
return if @captured_area == :button
super
end
def refresh
@dropdown_menu&.refresh
super
# Draw disabled colour
if disabled?
self.bitmap.fill_rect(@button_rect.x, @button_rect.y,
@button_rect.width, @button_rect.height,
disabled_fill_color)
end
# Draw button outline
self.bitmap.outline_rect(@button_rect.x, @button_rect.y,
@button_rect.width, @button_rect.height,
line_color)
# Draw value
draw_text(self.bitmap, @button_rect.x + TEXT_BOX_PADDING, TEXT_OFFSET_Y, @options[@value] || "???")
# Draw down arrow
arrow_area_x = @button_rect.x + @button_rect.width - @button_rect.height + 1
arrow_area_width = @button_rect.height - 2
arrow_color = (disabled?) ? disabled_text_color : text_color
self.bitmap.fill_rect(arrow_area_x, @button_rect.y + 1, arrow_area_width, arrow_area_width,
(@hover_area && @captured_area != :button) ? hover_color : background_color)
6.times do |i|
self.bitmap.fill_rect(arrow_area_x + (arrow_area_width / 2) - 5 + i,
@button_rect.y + (arrow_area_width / 2) - 1 + i,
11 - (2 * i), 1, arrow_color)
end
end
#-----------------------------------------------------------------------------
def on_mouse_press
if @dropdown_menu
if !@dropdown_menu.mouse_in_control?
remove_dropdown_menu
@toggling_dropdown_list = true
end
else
@captured_area = nil
super
if @captured_area == :button
make_dropdown_menu
@toggling_dropdown_list = true
end
end
end
def on_mouse_release
return if !@captured_area && !@dropdown_menu && !@toggling_dropdown_list
if @toggling_dropdown_list
@toggling_dropdown_list = false
return
end
if @dropdown_menu
if @dropdown_menu.changed?
new_val = @dropdown_menu.value
if new_val && new_val != @value
@value = new_val
set_changed
end
remove_dropdown_menu
super # Make this control not busy again
elsif !@dropdown_menu.mouse_in_control?
remove_dropdown_menu
super # Make this control not busy again
end
end
end
def update
@dropdown_menu&.update
@dropdown_menu&.repaint
super
end
end

View File

@@ -0,0 +1,213 @@
#===============================================================================
# Also known as a Combo Box.
# NOTE: This control lets you type in whatever text you want. The dropdown list
# only offers autocomplete-like suggestions, but you don't need to match
# any of them.
#===============================================================================
class UIControls::TextBoxDropdownList < UIControls::TextBox
attr_accessor :max_rows
TEXT_BOX_WIDTH = 200 - TEXT_BOX_HEIGHT
BUTTON_X = TEXT_BOX_X + TEXT_BOX_WIDTH
BUTTON_WIDTH = TEXT_BOX_HEIGHT
BUTTON_HEIGHT = TEXT_BOX_HEIGHT
MAX_LIST_ROWS = 8
def initialize(width, height, viewport, options, value = "")
super(width, height, viewport, value)
@box_width = TEXT_BOX_WIDTH
@options = options
@toggling_dropdown_list = false
@max_rows = MAX_LIST_ROWS
end
def dispose
remove_dropdown_menu
super
end
#-----------------------------------------------------------------------------
def values=(new_vals)
@options = new_vals
@dropdown_menu.values = @options if @dropdown_menu
end
#-----------------------------------------------------------------------------
def busy?
return true if @dropdown_menu || @toggling_dropdown_list
return super
end
#-----------------------------------------------------------------------------
def set_interactive_rects
@text_box_rect = Rect.new(TEXT_BOX_X, (height - TEXT_BOX_HEIGHT) / 2,
[@box_width, width - (TEXT_BOX_X * 2) - BUTTON_WIDTH].min, TEXT_BOX_HEIGHT)
@button_rect = Rect.new(BUTTON_X, @text_box_rect.y, BUTTON_WIDTH, BUTTON_HEIGHT)
@interactions = {
:text_box => @text_box_rect,
:button => @button_rect
}
end
def make_dropdown_menu
menu_height = (UIControls::List::ROW_HEIGHT * [@options.length, @max_rows].min) + (UIControls::List::BORDER_THICKNESS * 2)
# Draw menu's background
@dropdown_menu_bg = BitmapSprite.new(@text_box_rect.width + @button_rect.width, menu_height, self.viewport)
@dropdown_menu_bg.x = self.x + @text_box_rect.x
@dropdown_menu_bg.y = self.y + @text_box_rect.y + @text_box_rect.height
@dropdown_menu_bg.z = self.z + 1
@dropdown_menu_bg.bitmap.fill_rect(0, 0, @dropdown_menu_bg.width, @dropdown_menu_bg.height, background_color)
# Create menu
@dropdown_menu = UIControls::List.new(@text_box_rect.width + @button_rect.width, menu_height, self.viewport, @options)
@dropdown_menu.x = @dropdown_menu_bg.x
@dropdown_menu.y = @dropdown_menu_bg.y
@dropdown_menu.z = self.z + 2
@dropdown_menu.color_scheme = @color_scheme
@dropdown_menu.set_interactive_rects
@dropdown_menu.repaint
end
def remove_dropdown_menu
@dropdown_menu_bg&.dispose
@dropdown_menu_bg = nil
@dropdown_menu&.dispose
@dropdown_menu = nil
@captured_area = nil
@applied_filter = false
end
#-----------------------------------------------------------------------------
def draw_area_highlight
highlight_color = nil
if @captured_area == :text_box && !@hover_area && Input.press?(Input::MOUSELEFT)
highlight_color = capture_color
elsif !@captured_area && [:text_box, :button].include?(@hover_area)
# Draw mouse hover over area highlight
highlight_color = hover_color
end
return if !highlight_color
[:text_box, :button].each do |area|
rect = @interactions[area]
self.bitmap.fill_rect(rect.x, rect.y, rect.width, rect.height, highlight_color) if rect
end
end
def refresh
@dropdown_menu&.refresh
super
# Draw disabled colour in button
if disabled?
self.bitmap.fill_rect(@button_rect.x, @button_rect.y,
@button_rect.width, @button_rect.height,
disabled_fill_color)
end
# Draw button outline
self.bitmap.outline_rect(@button_rect.x, @button_rect.y,
@button_rect.width, @button_rect.height,
line_color)
# Draw down arrow
arrow_area_x = @button_rect.x + @button_rect.width - @button_rect.height + 1
arrow_area_width = @button_rect.height - 2
arrow_color = (disabled?) ? disabled_text_color : text_color
# self.bitmap.fill_rect(arrow_area_x, @button_rect.y + 1, arrow_area_width, arrow_area_width,
# (@hover_area && @captured_area != :button) ? hover_color : background_color)
6.times do |i|
self.bitmap.fill_rect(arrow_area_x + (arrow_area_width / 2) - 5 + i,
@button_rect.y + (arrow_area_width / 2) - 1 + i,
11 - (2 * i), 1, arrow_color)
end
end
#-----------------------------------------------------------------------------
def on_mouse_press
mouse_x, mouse_y = mouse_pos
return if !mouse_x || !mouse_y
if @dropdown_menu
if @text_box_rect.contains?(mouse_x, mouse_y)
# Clicked into the text box; put the text cursor in there
@captured_area = :text_box
@cursor_pos = get_cursor_index_from_mouse_position
@cursor_timer = System.uptime
invalidate
elsif !@dropdown_menu.mouse_in_control?
@value.strip! if @value.respond_to?("strip!")
@value = @initial_value.dup if disabled?
set_changed if @initial_value && @value != @initial_value
reset_interaction
remove_dropdown_menu
@toggling_dropdown_list = true
end
else
@captured_area = nil
super
if @captured_area
make_dropdown_menu
@toggling_dropdown_list = true
end
end
end
def on_mouse_release
return if !@captured_area && !@dropdown_menu && !@toggling_dropdown_list
if @toggling_dropdown_list
@toggling_dropdown_list = false
mouse_x, mouse_y = mouse_pos
if mouse_x && mouse_y && @interactions[:text_box].contains?(mouse_x, mouse_y)
@initial_value = @value.dup
Input.text_input = true
invalidate
end
return
end
if @dropdown_menu
if @dropdown_menu.changed?
new_val = @dropdown_menu.value
new_val = @options[new_val] if new_val.is_a?(Integer)
if new_val && new_val != @value
self.value = new_val
set_changed
end
@value.strip! if @value.respond_to?("strip!")
reset_interaction
remove_dropdown_menu
@captured_area = nil
elsif @captured_area
mouse_x, mouse_y = mouse_pos
if mouse_x && mouse_y && @interactions[:text_box].contains?(mouse_x, mouse_y)
@captured_area = :text_box
@initial_value = @value.dup
Input.text_input = true
end
elsif !mouse_in_control? && !@dropdown_menu.mouse_in_control?
@value.strip! if @value.respond_to?("strip!")
self.value = @initial_value if disabled?
set_changed if @initial_value && @value != @initial_value
reset_interaction
remove_dropdown_menu
@captured_area = nil
end
else
super
end
invalidate
end
def update
@dropdown_menu&.update
@dropdown_menu&.repaint
super
# Filter the dropdown menu options based on @value if it changes
if @dropdown_menu && @initial_value && (@applied_filter || @value != @initial_value)
filtered_options = @options.select do |key, val|
key.downcase.include?(@value.downcase) || val.downcase.include?(@value.downcase)
end
@dropdown_menu.values = filtered_options
@applied_filter = true
end
end
end

View File

@@ -0,0 +1,159 @@
#===============================================================================
#
#===============================================================================
class UIControls::Scrollbar < UIControls::BaseControl
attr_reader :slider_top
SLIDER_WIDTH = 16
WIDTH_PADDING = 0
SCROLL_DISTANCE = 16
def initialize(x, y, size, viewport, horizontal = false, always_visible = false)
if horizontal
super(size, SLIDER_WIDTH, viewport)
else
super(SLIDER_WIDTH, size, viewport)
end
self.x = x
self.y = y
@horizontal = horizontal # Is vertical if not horizontal
@tray_size = size # Number of pixels the scrollbar can move around in
@slider_size = size
@range = size # Total distance of the area this scrollbar is for
@slider_top = 0 # Top pixel within @size of the scrollbar
@always_visible = always_visible
self.visible = @always_visible
end
#-----------------------------------------------------------------------------
# Range is the total size of the large area that the scrollbar is able to
# show part of.
def range=(new_val)
raise "Can't set a scrollbar's range to 0!" if new_val == 0
@range = new_val
@slider_size = (@tray_size * [@tray_size.to_f / @range, 1].min).round
if @horizontal
@slider.width = @slider_size
else # Vertical
@slider.height = @slider_size
end
self.slider_top = @slider_top
self.visible = (@always_visible || @range > @tray_size)
invalidate
end
def slider_top=(new_val)
old_val = @slider_top
@slider_top = new_val.clamp(0, @tray_size - @slider_size)
if @horizontal
@slider.x = @slider_top
else # Vertical
@slider.y = @slider_top
end
invalidate if @slider_top != old_val
end
def position
return 0 if @range <= @tray_size
return (@range - @tray_size) * @slider_top / (@tray_size - @slider_size)
end
def minimum?
return @slider_top <= 0
end
def maximum?
return @slider_top >= @tray_size - @slider_size
end
#-----------------------------------------------------------------------------
def set_interactive_rects
@interactions = {}
if @horizontal
@slider = Rect.new(@slider_top, WIDTH_PADDING, @slider_size, height - (WIDTH_PADDING * 2))
else # Vertical
@slider = Rect.new(WIDTH_PADDING, @slider_top, width - (WIDTH_PADDING * 2), @slider_size)
end
@interactions[:slider] = @slider
@slider_tray = Rect.new(0, 0, width, height)
@interactions[:slider_tray] = @slider_tray
end
#-----------------------------------------------------------------------------
def refresh
super
return if !self.visible
# Draw the tray
self.bitmap.fill_rect(@slider_tray.x, @slider_tray.y, @slider_tray.width, @slider_tray.height, background_color)
# Draw the slider
if @slider_size < @tray_size && !disabled?
if @captured_area == :slider || (!@captured_area && @hover_area == :slider)
bar_color = hover_color
else
bar_color = text_color
end
self.bitmap.fill_rect(@slider.x, @slider.y, @slider.width, @slider.height, bar_color)
end
end
#-----------------------------------------------------------------------------
def on_mouse_press
@captured_area = nil
mouse_x, mouse_y = mouse_pos
return if !mouse_x || !mouse_y
# Check for mouse presses on slider/slider tray
@interactions.each_pair do |area, rect|
next if !rect.contains?(mouse_x, mouse_y)
@captured_area = area
if area == :slider
if @horizontal
@slider_mouse_offset = mouse_x - rect.x
else
@slider_mouse_offset = mouse_y - rect.y
end
end
invalidate
break
end
end
def on_mouse_release
super if @captured_area
end
def update
return if !self.visible
super
if @captured_area == :slider
mouse_x, mouse_y = mouse_pos
return if !mouse_x || !mouse_y
long_coord = (@horizontal) ? mouse_x : mouse_y
self.slider_top = long_coord - @slider_mouse_offset
elsif @captured_area == :slider_tray
if Input.repeat?(Input::MOUSELEFT) && @hover_area == :slider_tray
mouse_x, mouse_y = mouse_pos
return if !mouse_x || !mouse_y
long_coord = (@horizontal) ? mouse_x : mouse_y
if long_coord < @slider_top
self.slider_top = @slider_top - ((@tray_size - @slider_size) / 4.0).ceil
else
self.slider_top = @slider_top + ((@tray_size - @slider_size) / 4.0).ceil
end
end
elsif !disabled?
mouse_x, mouse_y = mouse_pos
if mouse_x && mouse_y && @interactions[:slider_tray].contains?(mouse_x, mouse_y)
wheel_v = Input.scroll_v
if wheel_v > 0 # Scroll up
self.slider_top -= SCROLL_DISTANCE
elsif wheel_v < 0 # Scroll down
self.slider_top += SCROLL_DISTANCE
end
end
end
end
end

View File

@@ -0,0 +1,177 @@
class Bitmap
def outline_rect(x, y, width, height, color, thickness = 1)
fill_rect(x, y, width, thickness, color)
fill_rect(x, y, thickness, height, color)
fill_rect(x, y + height - thickness, width, thickness, color)
fill_rect(x + width - thickness, y, thickness, height, color)
end
# Draws a series of concentric outline_rects around the defined area. From
# inside to outside, the color of each ring alternates.
def border_rect(x, y, width, height, thickness, color1, color2)
thickness.times do |i|
col = (i.even?) ? color1 : color2
outline_rect(x - i - 1, y - i - 1, width + (i * 2) + 2, height + (i * 2) + 2, col)
end
end
def fill_diamond(x, y, radius, color)
((radius * 2) + 1).times do |i|
height = (i <= radius) ? (i * 2) + 1 : (((radius * 2) - i) * 2) + 1
fill_rect(x - radius + i, y - ((height - 1) / 2), 1, height, color)
end
end
def draw_interpolation_line(x, y, width, height, gradient, type, color)
start_x = x
end_x = x + width - 1
start_y = (gradient) ? y + height - 1 : y
end_y = (gradient) ? y : y + height - 1
case type
when :linear
# NOTE: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
dx = end_x - start_x
dy = -((end_y - start_y).abs)
error = dx + dy
draw_x = start_x
draw_y = start_y
loop do
fill_rect(draw_x, draw_y, 1, 1, color)
break if draw_x == end_x && draw_y == end_y
e2 = 2 * error
if e2 >= dy
break if draw_x == end_x
error += dy
draw_x += 1
end
if e2 <= dx
break if draw_y == end_y
error += dx
draw_y += (gradient) ? -1 : 1
end
end
when :ease_in, :ease_out, :ease_both # Quadratic
start_y = y + height - 1
end_y = y
points = []
(width + 1).times do |frame|
x = frame / width.to_f
case type
when :ease_in
points[frame] = (end_y - start_y) * x * x
when :ease_out
points[frame] = (end_y - start_y) * (1 - ((1 - x) * (1 - x)))
when :ease_both
if x < 0.5
points[frame] = (end_y - start_y) * x * x * 2
else
points[frame] = (end_y - start_y) * (1 - (((-2 * x) + 2) * ((-2 * x) + 2) / 2))
end
end
points[frame] = points[frame].round
end
width.times do |frame|
line_y = points[frame]
if frame == 0
line_height = 1
else
line_height = [(points[frame] - points[frame - 1]).abs, 1].max
end
if !gradient # Going down
line_y = -(height - 1) - line_y - line_height + 1
end
fill_rect(start_x + frame, start_y + line_y, 1, line_height, color)
end
else
raise _INTL("Unknown interpolation type {1}.", type)
end
end
end
#===============================================================================
# Fixed Compiler.pbWriteCsvRecord to make it detect enums first, allowing enum
# values to be turned into symbols/booleans/whatever instead of just numbers.
#===============================================================================
module Compiler
module_function
def pbWriteCsvRecord(record, file, schema)
rec = (record.is_a?(Array)) ? record.flatten : [record]
start = (["*", "^"].include?(schema[1][0, 1])) ? 1 : 0
index = -1
loop do
(start...schema[1].length).each do |i|
index += 1
value = rec[index]
if schema[1][i, 1][/[A-Z]/] # Optional
# Check the rest of the values for non-nil things
later_value_found = false
(index...rec.length).each do |j|
later_value_found = true if !rec[j].nil?
break if later_value_found
end
if !later_value_found
start = -1
break
end
end
file.write(",") if index > 0
next if value.nil?
case schema[1][i, 1]
when "e", "E" # Enumerable
enumer = schema[2 + i - start]
case enumer
when Array
file.write(enumer[value])
when Symbol, String
mod = Object.const_get(enumer.to_sym)
file.write(getConstantName(mod, value))
when Module
file.write(getConstantName(enumer, value))
when Hash
enumer.each_key do |key|
next if enumer[key] != value
file.write(key)
break
end
end
when "y", "Y" # Enumerable or integer
enumer = schema[2 + i - start]
case enumer
when Array
file.write((enumer[value].nil?) ? value : enumer[value])
when Symbol, String
mod = Object.const_get(enumer.to_sym)
file.write(getConstantNameOrValue(mod, value))
when Module
file.write(getConstantNameOrValue(enumer, value))
when Hash
has_enum = false
enumer.each_key do |key|
next if enumer[key] != value
file.write(key)
has_enum = true
break
end
file.write(value) if !has_enum
end
else
if value.is_a?(String)
file.write((schema[1][i, 1].downcase == "q") ? value : csvQuote(value))
elsif value.is_a?(Symbol)
file.write(csvQuote(value.to_s))
elsif value == true
file.write("true")
elsif value == false
file.write("false")
else
file.write(value.inspect)
end
end
end
break if start > 0 && index >= rec.length - 1
break if start <= 0
end
return record
end
end

View File

@@ -0,0 +1,403 @@
module GameData
class Animation
attr_reader :type # :move, :opp_move, :common, :opp_common
attr_reader :move # Either the move's ID or the common animation's name (both are strings)
attr_reader :version # Hit number
attr_reader :name # Shown in the sublist; cosmetic only
attr_reader :no_user # Whether there is no "User" particle (false by default)
attr_reader :no_target # Whether there is no "Target" particle (false by default)
attr_reader :ignore # Whether the animation can't be played in battle
attr_reader :flags
attr_reader :pbs_path # Whole path minus "PBS/Animations/" at start and ".txt" at end
attr_reader :particles
DATA = {}
DATA_FILENAME = "animations.dat"
OPTIONAL = true
# NOTE: All mentions of focus types can be found by searching for
# :user_and_target, plus there's :foreground in PARTICLE_DEFAULT_VALUES
# below.
FOCUS_TYPES = {
"Foreground" => :foreground,
"Midground" => :midground,
"Background" => :background,
"User" => :user,
"Target" => :target,
"UserAndTarget" => :user_and_target,
"UserSideForeground" => :user_side_foreground,
"UserSideBackground" => :user_side_background,
"TargetSideForeground" => :target_side_foreground,
"TargetSideBackground" => :target_side_background
}
FOCUS_TYPES_WITH_USER = [
:user, :user_and_target, :user_side_foreground, :user_side_background
]
FOCUS_TYPES_WITH_TARGET = [
:target, :user_and_target, :target_side_foreground, :target_side_background
]
INTERPOLATION_TYPES = {
"None" => :none,
"Linear" => :linear,
"EaseIn" => :ease_in,
"EaseBoth" => :ease_both,
"EaseOut" => :ease_out
}
USER_AND_TARGET_SEPARATION = [200, -200, -100] # x, y, z (from user to target)
SPAWNER_TYPES = {
"None" => :none,
"RandomDirection" => :random_direction,
"RandomDirectionGravity" => :random_direction_gravity,
"RandomUpDirectionGravity" => :random_up_direction_gravity
}
ANGLE_OVERRIDES = {
"None" => :none,
"InitialAngleToFocus" => :initial_angle_to_focus,
"AlwaysPointAtFocus" => :always_point_at_focus
}
# Properties that apply to the animation in general, not to individual
# particles. They don't change during the animation.
SCHEMA = {
"SectionName" => [:id, "esU", {"Move" => :move, "OppMove" => :opp_move,
"Common" => :common, "OppCommon" => :opp_common}],
"Name" => [:name, "s"],
"NoUser" => [:no_user, "b"],
"NoTarget" => [:no_target, "b"],
"Ignore" => [:ignore, "b"],
"Particle" => [:particles, "s"] # Is a subheader line like <text>
}
# For individual particles. Any property whose schema begins with "^" can
# change during the animation.
SUB_SCHEMA = {
# These properties cannot be changed partway through the animation.
# NOTE: "Name" isn't a property here, because the particle's name comes
# from the "Particle" property above.
"Graphic" => [:graphic, "s"],
"Focus" => [:focus, "e", FOCUS_TYPES],
"FoeInvertX" => [:foe_invert_x, "b"],
"FoeInvertY" => [:foe_invert_y, "b"],
"FoeFlip" => [:foe_flip, "b"],
"Spawner" => [:spawner, "e", SPAWNER_TYPES],
"SpawnQuantity" => [:spawn_quantity, "v"],
"RandomFrameMax" => [:random_frame_max, "u"],
"AngleOverride" => [:angle_override, "e", ANGLE_OVERRIDES],
# All properties below are "SetXYZ" or "MoveXYZ". "SetXYZ" has the
# keyframe and the value, and "MoveXYZ" has the keyframe, duration and the
# value. All have "^" in their schema. "SetXYZ" is turned into "MoveXYZ"
# when compiling by inserting a duration (second value) of 0.
"SetFrame" => [:frame, "^uu"], # Frame within the graphic if it's a spritesheet
"MoveFrame" => [:frame, "^uuuE", nil, nil, nil, INTERPOLATION_TYPES],
"SetBlending" => [:blending, "^uu"], # 0, 1 or 2
"SetFlip" => [:flip, "^ub"],
"SetX" => [:x, "^ui"],
"MoveX" => [:x, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES],
"SetY" => [:y, "^ui"],
"MoveY" => [:y, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES],
"SetZ" => [:z, "^ui"],
"MoveZ" => [:z, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES],
"SetZoomX" => [:zoom_x, "^uu"],
"MoveZoomX" => [:zoom_x, "^uuuE", nil, nil, nil, INTERPOLATION_TYPES],
"SetZoomY" => [:zoom_y, "^uu"],
"MoveZoomY" => [:zoom_y, "^uuuE", nil, nil, nil, INTERPOLATION_TYPES],
"SetAngle" => [:angle, "^ui"],
"MoveAngle" => [:angle, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES],
"SetVisible" => [:visible, "^ub"],
"SetOpacity" => [:opacity, "^uu"],
"MoveOpacity" => [:opacity, "^uuuE", nil, nil, nil, INTERPOLATION_TYPES],
"SetColorRed" => [:color_red, "^ui"],
"MoveColorRed" => [:color_red, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES],
"SetColorGreen" => [:color_green, "^ui"],
"MoveColorGreen" => [:color_green, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES],
"SetColorBlue" => [:color_blue, "^ui"],
"MoveColorBlue" => [:color_blue, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES],
"SetColorAlpha" => [:color_alpha, "^ui"],
"MoveColorAlpha" => [:color_alpha, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES],
"SetToneRed" => [:tone_red, "^ui"],
"MoveToneRed" => [:tone_red, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES],
"SetToneGreen" => [:tone_green, "^ui"],
"MoveToneGreen" => [:tone_green, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES],
"SetToneBlue" => [:tone_blue, "^ui"],
"MoveToneBlue" => [:tone_blue, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES],
"SetToneGray" => [:tone_gray, "^ui"],
"MoveToneGray" => [:tone_gray, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES],
# These properties are specifically for the "SE" particle.
"Play" => [:se, "^usUU"], # Filename, volume, pitch
"PlayUserCry" => [:user_cry, "^uUU"], # Volume, pitch
"PlayTargetCry" => [:target_cry, "^uUU"] # Volume, pitch
}
PARTICLE_DEFAULT_VALUES = {
:name => "",
:graphic => "",
:focus => :foreground,
:foe_invert_x => false,
:foe_invert_y => false,
:foe_flip => false,
:spawner => :none,
:spawn_quantity => 1,
:random_frame_max => 0,
:angle_override => :none
}
# NOTE: Particles are invisible until their first command, and automatically
# become visible then. "User" and "Target" are visible from the start,
# though.
PARTICLE_KEYFRAME_DEFAULT_VALUES = {
:frame => 0,
:blending => 0,
:flip => false,
:x => 0,
:y => 0,
:z => 0,
:zoom_x => 100,
:zoom_y => 100,
:angle => 0,
:visible => false,
:opacity => 255,
:color_red => 0,
:color_green => 0,
:color_blue => 0,
:color_alpha => 0,
:tone_red => 0,
:tone_green => 0,
:tone_blue => 0,
:tone_gray => 0,
:se => nil,
:user_cry => nil,
:target_cry => nil
}
def self.property_display_name(property)
return {
:frame => _INTL("Frame"),
:blending => _INTL("Blending"),
:flip => _INTL("Flip"),
:x => _INTL("X"),
:y => _INTL("Y"),
:z => _INTL("Priority"),
:zoom_x => _INTL("Zoom X"),
:zoom_y => _INTL("Zoom Y"),
:angle => _INTL("Angle"),
:visible => _INTL("Visible"),
:opacity => _INTL("Opacity"),
:color_red => _INTL("Color Red"),
:color_green => _INTL("Color Green"),
:color_blue => _INTL("Color Blue"),
:color_alpha => _INTL("Color Alpha"),
:tone_red => _INTL("Tone Red"),
:tone_green => _INTL("Tone Green"),
:tone_blue => _INTL("Tone Blue"),
:tone_gray => _INTL("Tone Gray")
}[property] || property.to_s.capitalize
end
def self.property_can_interpolate?(property)
return false if !property
SUB_SCHEMA.each_value do |prop|
return true if prop[0] == property && prop[5] && prop[5] == INTERPOLATION_TYPES
end
return false
end
@@cmd_to_pbs_name = nil # Used for writing animation PBS files
extend ClassMethodsIDNumbers
include InstanceMethods
singleton_class.alias_method(:__new_anim__load, :load) unless singleton_class.method_defined?(:__new_anim__load)
def self.load
__new_anim__load if FileTest.exist?("Data/#{self::DATA_FILENAME}")
end
def self.sub_schema
return SUB_SCHEMA
end
def self.register(hash, id_num = -1)
DATA[(id_num >= 0) ? id_num : DATA.keys.length] = self.new(hash)
end
def self.new_hash(anim_type = 0, move = nil)
ret = {}
ret[:type] = (anim_type == 0) ? :move : :common
ret[:move] = (anim_type == 0) ? "STRUGGLE" : "Shiny"
ret[:move] = move if !move.nil?
ret[:version] = 0
ret[:name] = _INTL("New animation")
ret[:no_user] = false
ret[:no_target] = false
ret[:ignore] = false
ret[:particles] = [
{:name => "User", :focus => :user, :graphic => "USER"},
{:name => "Target", :focus => :target, :graphic => "TARGET"},
{:name => "SE"}
]
ret[:flags] = []
ret[:pbs_path] = "New animation"
return ret
end
def initialize(hash)
# NOTE: hash has an :id entry, but it's unused here.
@type = hash[:type]
@move = hash[:move]
@version = hash[:version] || 0
@name = hash[:name]
@no_user = hash[:no_user] || false
@no_target = hash[:no_target] || false
@ignore = hash[:ignore] || false
@particles = hash[:particles] || []
@flags = hash[:flags] || []
@pbs_path = hash[:pbs_path] || @move
end
# Returns a clone of the animation in a hash format, the same as created by
# the Compiler. This hash can be passed into self.register.
def clone_as_hash
ret = {}
ret[:type] = @type
ret[:move] = @move
ret[:version] = @version
ret[:name] = @name
ret[:no_user] = @no_user
ret[:no_target] = @no_target
ret[:ignore] = @ignore
ret[:particles] = [] # Clone the @particles array, which is nested hashes and arrays
@particles.each do |particle|
new_p = {}
particle.each_pair do |key, val|
if val.is_a?(Array)
new_p[key] = []
val.each { |cmd| new_p[key].push(cmd.clone) }
else
new_p[key] = val
end
end
ret[:particles].push(new_p)
end
ret[:flags] = @flags.clone
ret[:pbs_path] = @pbs_path
return ret
end
def inspect
ret = super.chop + ": "
case @type
when :move then ret += _INTL("[Move]")
when :opp_move then ret += _INTL("[Foe Move]")
when :common then ret += _INTL("[Common]")
when :opp_common then ret += _INTL("[Foe Common]")
else
raise _INTL("Unknown animation type.")
end
case @type
when :move, :opp_move
move_data = GameData::Move.try_get(@move)
move_name = (move_data) ? move_data.name : @move
ret += " " + move_name
when :common, :opp_common
ret += " " + @move
end
ret += " (" + @version.to_s + ")" if @version > 0
ret += " - " + @name if @name
return ret
end
def move_animation?
return [:move, :opp_move].include?(@type)
end
def common_animation?
return [:common, :opp_common].include?(@type)
end
def opposing_animation?
return [:opp_move, :opp_common].include?(@type)
end
alias __new_anim__get_property_for_PBS get_property_for_PBS unless method_defined?(:__new_anim__get_property_for_PBS)
def get_property_for_PBS(key)
ret = __new_anim__get_property_for_PBS(key)
case key
when "SectionName"
ret = [@type, @move]
ret.push(@version) if @version > 0
end
return ret
end
def get_particle_property_for_PBS(key, index = 0)
ret = nil
ret = @particles[index][SUB_SCHEMA[key][0]] if SUB_SCHEMA[key]
ret = nil if ret == false || (ret.is_a?(Array) && ret.length == 0) || ret == ""
case key
when "Graphic", "Focus"
# The User and Target particles have hardcoded graphics/foci, so they
# don't need writing to PBS
ret = nil if ["User", "Target"].include?(@particles[index][:name])
when "Spawner"
ret = nil if ret == :none
when "SpawnQuantity"
ret = nil if @particles[index][:spawner].nil? || @particles[index][:spawner] == :none
ret = nil if ret && ret <= 1
when "RandomFrameMax"
ret = nil if ret == 0
when "AngleOverride"
ret = nil if ret == :none
ret = nil if !FOCUS_TYPES_WITH_USER.include?(@particles[index][:focus]) &&
!FOCUS_TYPES_WITH_TARGET.include?(@particles[index][:focus])
when "AllCommands"
# Get translations of all properties to their names as seen in PBS
# animation files
if !@@cmd_to_pbs_name
@@cmd_to_pbs_name = {}
SUB_SCHEMA.each_pair do |key, val|
@@cmd_to_pbs_name[val[0]] ||= []
@@cmd_to_pbs_name[val[0]].push([key, val[1].length])
end
# For each property translation, put "SetXYZ" before "MoveXYZ"
@@cmd_to_pbs_name.each_value do |val|
val.sort! { |a, b| a[1] <=> b[1] }
val.map! { |a| a[0] }
end
end
# Gather all commands into a single array
ret = []
@particles[index].each_pair do |key, val|
next if !val.is_a?(Array)
val.each do |cmd|
new_cmd = cmd.clone
if @particles[index][:name] != "SE" && new_cmd[1] > 0
new_cmd.pop if new_cmd.last == :linear # This is the default
ret.push([@@cmd_to_pbs_name[key][1]] + new_cmd) # ["MoveXYZ", keyframe, duration, value, interpolation]
else
case key
when :se
new_cmd[4] = nil if new_cmd[4] == 100 # Pitch
new_cmd[3] = nil if new_cmd[4].nil? && new_cmd[3] == 100 # Volume
when :user_cry, :target_cry
new_cmd[3] = nil if new_cmd[3] == 100 # Pitch
new_cmd[2] = nil if new_cmd[3].nil? && new_cmd[2] == 100 # Volume
end
ret.push([@@cmd_to_pbs_name[key][0]] + new_cmd) # ["SetXYZ", keyframe, duration, value]
end
end
end
# Sort the array of commands by keyframe order, then by duration, then
# by the order they're defined in SUB_SCHEMA
ret.sort! do |a, b|
if a[1] == b[1]
if a[2] == b[2]
next SUB_SCHEMA.keys.index(a[0]) <=> SUB_SCHEMA.keys.index(b[0])
else
next a[2] <=> b[2] # Sort by duration
end
else
next a[1] <=> b[1] # Sort by keyframe
end
end
end
return ret
end
end
end

View File

@@ -0,0 +1,373 @@
module Compiler
module_function
def compile_battle_animations(*paths)
GameData::Animation::DATA.clear
schema = GameData::Animation.schema
sub_schema = GameData::Animation.sub_schema
idx = 0
# Read from PBS file(s)
Console.echo_li(_INTL("Compiling animation PBS files..."))
paths.each do |path|
file_name = path.gsub(/^PBS\/Animations\//, "").gsub(/.txt$/, "")
data_hash = nil
current_particle = nil
section_name = nil
section_line = nil
# Read each line of the animation PBS file at a time and compile it as an
# animation property
pbCompilerEachPreppedLine(path) do |line, line_no|
echo "." if idx % 100 == 0
idx += 1
Graphics.update if idx % 500 == 0
FileLineData.setSection(section_name, nil, section_line)
if line[/^\s*\[\s*(.+)\s*\]\s*$/]
# New section [anim_type, name]
section_name = $~[1]
section_line = line
if data_hash
validate_compiled_animation(data_hash)
GameData::Animation.register(data_hash)
end
FileLineData.setSection(section_name, nil, section_line)
# Construct data hash
data_hash = {
:pbs_path => file_name
}
data_hash[schema["SectionName"][0]] = get_csv_record(section_name.clone, schema["SectionName"])
data_hash[schema["Particle"][0]] = []
current_particle = nil
elsif line[/^\s*<\s*(.+)\s*>\s*$/]
# New subsection [particle_name]
value = get_csv_record($~[1], schema["Particle"])
current_particle = {
:name => value
}
data_hash[schema["Particle"][0]].push(current_particle)
elsif line[/^\s*(\w+)\s*=\s*(.*)$/]
# XXX=YYY lines
if !data_hash
raise _INTL("Expected a section at the beginning of the file.") + "\n" + FileLineData.linereport
end
key = $~[1]
if schema[key] # Property of the animation
value = get_csv_record($~[2], schema[key])
if schema[key][1][0] == "^"
value = nil if value.is_a?(Array) && value.empty?
data_hash[schema[key][0]] ||= []
data_hash[schema[key][0]].push(value) if value
else
value = nil if value.is_a?(Array) && value.empty?
data_hash[schema[key][0]] = value
end
elsif sub_schema[key] # Property of a particle
if !current_particle
raise _INTL("Particle hasn't been defined yet!") + "\n" + FileLineData.linereport
end
value = get_csv_record($~[2], sub_schema[key])
if sub_schema[key][1][0] == "^"
value = nil if value.is_a?(Array) && value.empty?
current_particle[sub_schema[key][0]] ||= []
current_particle[sub_schema[key][0]].push(value) if value
else
value = nil if value.is_a?(Array) && value.empty?
current_particle[sub_schema[key][0]] = value
end
end
end
end
# Add last animation's data to records
if data_hash
FileLineData.setSection(section_name, nil, section_line)
validate_compiled_animation(data_hash)
GameData::Animation.register(data_hash)
end
end
validate_all_compiled_animations
process_pbs_file_message_end
# Save all data
GameData::Animation.save
end
def validate_compiled_animation(hash)
# Split anim_type, move/common_name, version into their own values
hash[:type] = hash[:id][0]
hash[:move] = hash[:id][1]
hash[:version] = hash[:id][2] || 0
# Ensure there is at most one each of "User", "Target" and "SE" particles
["User", "Target", "SE"].each do |type|
next if hash[:particles].count { |particle| particle[:name] == type } <= 1
raise _INTL("Animation has more than 1 \"{1}\" particle, which isn't allowed.", type) + "\n" + FileLineData.linereport
end
# Ensure there is no "User" particle if "NoUser" is set
if hash[:particles].any? { |particle| particle[:name] == "User" } && hash[:no_user]
raise _INTL("Can't define a \"User\" particle and also set property \"NoUser\" to true.") + "\n" + FileLineData.linereport
end
# Ensure there is no "Target" particle if "NoTarget" is set
if hash[:particles].any? { |particle| particle[:name] == "Target" } && hash[:no_target]
raise _INTL("Can't define a \"Target\" particle and also set property \"NoTarget\" to true.") + "\n" + FileLineData.linereport
end
# Create "User", "Target" and "SE" particles if they don't exist but should
if hash[:particles].none? { |particle| particle[:name] == "User" } && !hash[:no_user]
hash[:particles].push({:name => "User"})
end
if hash[:particles].none? { |particle| particle[:name] == "Target" } && !hash[:no_target]
hash[:particles].push({:name => "Target"})
end
if hash[:particles].none? { |particle| particle[:name] == "SE" }
hash[:particles].push({:name => "SE"})
end
# Go through each particle in turn
hash[:particles].each do |particle|
# Ensure the "Play"-type commands are exclusive to the "SE" particle, and
# that the "SE" particle has no other commands
if particle[:name] == "SE"
particle.keys.each do |property|
next if [:name, :se, :user_cry, :target_cry].include?(property)
raise _INTL("Particle \"{1}\" has a command that isn't a \"Play\"-type command.",
particle[:name]) + "\n" + FileLineData.linereport
end
else
if particle[:se]
raise _INTL("Particle \"{1}\" has a \"Play\" command but shouldn't.",
particle[:name]) + "\n" + FileLineData.linereport
elsif particle[:user_cry]
raise _INTL("Particle \"{1}\" has a \"PlayUserCry\" command but shouldn't.",
particle[:name]) + "\n" + FileLineData.linereport
elsif particle[:target_cry]
raise _INTL("Particle \"{1}\" has a \"PlayTargetCry\" command but shouldn't.",
particle[:name]) + "\n" + FileLineData.linereport
end
end
# Ensure all particles have a default focus if not given
if !particle[:focus] && particle[:name] != "SE"
case particle[:name]
when "User" then particle[:focus] = :user
when "Target" then particle[:focus] = :target
else particle[:focus] = GameData::Animation::PARTICLE_DEFAULT_VALUES[:focus]
end
end
# Ensure user/target particles have a default graphic if not given
if !particle[:graphic] && particle[:name] != "SE"
case particle[:name]
when "User" then particle[:graphic] = "USER"
when "Target" then particle[:graphic] = "TARGET"
end
end
# If the animation doesn't involve a user, ensure that particles don't
# have a focus/graphic that involves a user, and that the animation
# doesn't play a user's cry
if hash[:no_user]
if GameData::Animation::FOCUS_TYPES_WITH_USER.include?(particle[:focus])
raise _INTL("Particle \"{1}\" can't have a \"Focus\" that involves a user if property \"NoUser\" is set to true.",
particle[:name]) + "\n" + FileLineData.linereport
end
if ["USER", "USER_OPP", "USER_FRONT", "USER_BACK"].include?(particle[:graphic])
raise _INTL("Particle \"{1}\" can't have a \"Graphic\" that involves a user if property \"NoUser\" is set to true.",
particle[:name]) + "\n" + FileLineData.linereport
end
if particle[:name] == "SE" && particle[:user_cry] && !particle[:user_cry].empty?
raise _INTL("Animation can't play the user's cry if property \"NoUser\" is set to true.") + "\n" + FileLineData.linereport
end
end
# If the animation doesn't involve a target, ensure that particles don't
# have a focus/graphic that involves a target, and that the animation
# doesn't play a target's cry
if hash[:no_target]
if GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus])
raise _INTL("Particle \"{1}\" can't have a \"Focus\" that involves a target if property \"NoTarget\" is set to true.",
particle[:name]) + "\n" + FileLineData.linereport
end
if ["TARGET", "TARGET_OPP", "TARGET_FRONT", "TARGET_BACK"].include?(particle[:graphic])
raise _INTL("Particle \"{1}\" can't have a \"Graphic\" that involves a target if property \"NoTarget\" is set to true.",
particle[:name]) + "\n" + FileLineData.linereport
end
if particle[:name] == "SE" && particle[:target_cry] && !particle[:target_cry].empty?
raise _INTL("Animation can't play the target's cry if property \"NoTarget\" is set to true.") + "\n" + FileLineData.linereport
end
end
# Ensure that none of the particle's "alter something if focus is a
# battler on the foe's side" properties are set if the particle doesn't
# have such a focus
if GameData::Animation::FOCUS_TYPES_WITH_USER.include?(particle[:focus]) == GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus])
if particle[:foe_invert_x]
raise _INTL("Particle \"{1}\" can't set \"FoeInvertX\" if its focus isn't exactly 1 thing.",
particle[:name]) + "\n" + FileLineData.linereport
end
if particle[:foe_invert_y]
raise _INTL("Particle \"{1}\" can't set \"FoeInvertY\" if its focus isn't exactly 1 thing.",
particle[:name]) + "\n" + FileLineData.linereport
end
if particle[:foe_flip]
raise _INTL("Particle \"{1}\" can't set \"FoeFlip\" if its focus isn't exactly 1 thing.",
particle[:name]) + "\n" + FileLineData.linereport
end
end
# Ensure that only particles that have an entity as a focus can have a
# smart angle
if (particle[:angle_override] || :none) != :none &&
!GameData::Animation::FOCUS_TYPES_WITH_USER.include?(particle[:focus]) &&
!GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus])
raise _INTL("Particle \"{1}\" can't set \"AngleOverride\" if its focus isn't a specific thing(s).",
particle[:name]) + "\n" + FileLineData.linereport
end
# Ensure that a particle with a user's/target's graphic doesn't have any
# :frame commands
if !["User", "Target", "SE"].include?(particle[:name]) &&
["USER", "USER_OPP", "USER_FRONT", "USER_BACK",
"TARGET", "TARGET_OPP", "TARGET_FRONT", "TARGET_BACK"].include?(particle[:graphic]) &&
particle[:frame] && !particle[:frame].empty?
raise _INTL("Particle \"{1}\" can't have any \"Frame\" commands if its graphic is a Pokémon's sprite.",
particle[:name]) + "\n" + FileLineData.linereport
end
# Ensure that the same SE isn't played twice in the same frame
if particle[:name] == "SE"
[:se, :user_cry, :target_cry].each do |property|
next if !particle[property]
files_played = []
particle[property].each do |play|
files_played[play[0]] ||= []
if files_played[play[0]].include?(play[1])
case property
when :se
raise _INTL("SE \"{1}\" should not play twice in the same frame ({2}).", play[1], play[0]) + "\n" + FileLineData.linereport
when :user_cry
raise _INTL("User's cry should not play twice in the same frame ({1}).", play[0]) + "\n" + FileLineData.linereport
when :target_cry
raise _INTL("Target's cry should not play twice in the same frame ({1}).", play[0]) + "\n" + FileLineData.linereport
end
end
files_played[play[0]].push(play[1])
end
end
end
# Convert all "SetXYZ" particle commands to "MoveXYZ" by giving them a
# duration of 0 (even ones that can't have a "MoveXYZ" command)
GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES.keys.each do |prop|
next if !particle[prop]
particle[prop].each do |cmd|
cmd.insert(1, 0) if cmd.length == 2 || particle[:name] == "SE"
# Give default interpolation value of :linear to any "MoveXYZ" command
# that doesn't have one already
cmd.push(:linear) if cmd[1] > 0 && cmd.length < 4
end
end
# Sort each particle's commands by their keyframe and duration
particle.keys.each do |key|
next if !particle[key].is_a?(Array)
particle[key].sort! { |a, b| a[0] == b[0] ? a[1] == b[1] ? 0 : a[1] <=> b[1] : a[0] <=> b[0] }
next if particle[:name] == "SE"
# Check for any overlapping particle commands
last_frame = -1
last_set_frame = -1
particle[key].each do |cmd|
if last_frame > cmd[0]
raise _INTL("Animation has overlapping commands for the {1} property.",
key.to_s.capitalize) + "\n" + FileLineData.linereport
end
if cmd[1] == 0 && last_set_frame >= cmd[0]
raise _INTL("Animation has multiple \"Set\" commands in the same keyframe for the {1} property.",
key.to_s.capitalize) + "\n" + FileLineData.linereport
end
last_frame = cmd[0] + cmd[1]
last_set_frame = cmd[0] if cmd[1] == 0
end
end
# Ensure valid values for "SetBlending" commands
if particle[:blending]
particle[:blending].each do |blend|
next if blend[2] <= 2
raise _INTL("Invalid blend value: {1} (must be 0, 1 or 2).\n{2}",
blend[2], FileLineData.linereport)
end
end
end
end
def validate_all_compiled_animations; end
end
#===============================================================================
# Hook into the regular Compiler to also compile animation PBS files.
# This is a separate Compiler that runs after the regular one.
#===============================================================================
module Compiler
module_function
def get_animation_pbs_files_to_compile
ret = []
if FileTest.directory?("PBS/Animations")
Dir.all("PBS/Animations", "**/**.txt").each { |file| ret.push(file) }
end
return ret
end
class << self
if !method_defined?(:__new_anims_main)
alias_method :__new_anims_main, :main
end
end
def main
__new_anims_main
return if !$DEBUG
begin
Console.echo_h1(_INTL("Checking new animations data"))
must_compile = false
data_file = "animations.dat"
text_files = get_animation_pbs_files_to_compile
latest_data_time = 0
latest_text_time = 0
# Check data file for its latest modify time
if FileTest.exist?("Data/" + data_file)
begin
File.open("Data/#{data_file}") do |file|
latest_data_time = [latest_data_time, file.mtime.to_i].max
end
rescue SystemCallError
must_compile = true
end
else
must_compile = true if text_files.length > 0
end
# Check PBS files for their latest modify time
text_files.each do |filepath|
begin
File.open(filepath) do |file|
latest_text_time = [latest_text_time, file.mtime.to_i].max
end
rescue SystemCallError
end
end
# Decide to compile if a PBS file was edited more recently than the .dat file
must_compile |= (latest_text_time >= latest_data_time)
# Should recompile if holding Ctrl
Input.update
must_compile = true if $full_compile || Input.press?(Input::CTRL)
# Delete old data file in preparation for recompiling
if must_compile
begin
File.delete("Data/#{data_file}") if FileTest.exist?("Data/#{data_file}")
rescue SystemCallError
end
# Recompile all data
compile_battle_animations(*text_files)
else
Console.echoln_li(_INTL("New animations data were not compiled"))
end
echoln ""
rescue Exception
e = $!
raise e if e.class.to_s == "Reset" || e.is_a?(Reset) || e.is_a?(SystemExit)
pbPrintException(e)
begin
File.delete("Data/#{data_file}") if FileTest.exist?("Data/#{data_file}")
rescue SystemCallError
end
raise Reset.new if e.is_a?(Hangup)
raise SystemExit.new if e.is_a?(RuntimeError)
raise "Unknown exception when compiling animations."
end
end
end

View File

@@ -0,0 +1,124 @@
module Compiler
module_function
def write_all_battle_animations
# Delete all existing .txt files in the PBS/Animations/ folder
files_to_delete = get_animation_pbs_files_to_compile
files_to_delete.each { |path| File.delete(path) }
# Get all files that need writing
paths = []
GameData::Animation.each { |anim| paths.push(anim.pbs_path) if !paths.include?(anim.pbs_path) }
idx = 0
# Write each file in turn
paths.each do |path|
Graphics.update if idx % 500 == 0
idx += 1
write_battle_animation_file(path)
end
end
def write_battle_animation_file(path)
schema = GameData::Animation.schema
sub_schema = GameData::Animation.sub_schema
write_pbs_file_message_start(path)
# Create all subfolders needed
dirs = ("PBS/Animations/" + path).split("/")
dirs.pop # Remove the filename
dirs.length.times do |i|
dir_string = dirs[0..i].join("/")
if !FileTest.directory?(dir_string)
Dir.mkdir(dir_string) rescue nil
end
end
# Write file
File.open("PBS/Animations/" + path + ".txt", "wb") do |f|
add_PBS_header_to_file(f)
# Write each element in turn
GameData::Animation.each do |element|
next if element.pbs_path != path
f.write("\#-------------------------------\r\n")
if schema["SectionName"]
f.write("[")
pbWriteCsvRecord(element.get_property_for_PBS("SectionName"), f, schema["SectionName"])
f.write("]\r\n")
else
f.write("[#{element.id}]\r\n")
end
# Write each animation property
schema.each_key do |key|
next if ["SectionName", "Particle"].include?(key)
val = element.get_property_for_PBS(key)
next if val.nil?
f.write(sprintf("%s = ", key))
pbWriteCsvRecord(val, f, schema[key])
f.write("\r\n")
end
# Write each particle in turn
element.particles.each_with_index do |particle, i|
# Write header
f.write("<" + particle[:name] + ">")
f.write("\r\n")
# Write one-off particle properties
sub_schema.each_pair do |key, val|
next if val[1][0] == "^"
val = element.get_particle_property_for_PBS(key, i)
next if val.nil?
f.write(sprintf(" %s = ", key))
pbWriteCsvRecord(val, f, sub_schema[key])
f.write("\r\n")
end
# Write particle commands (in keyframe order)
cmds = element.get_particle_property_for_PBS("AllCommands", i)
cmds.each do |cmd|
if cmd[2] == 0 # Duration of 0
f.write(sprintf(" %s = ", cmd[0]))
new_cmd = cmd[1..-1]
new_cmd.delete_at(1)
pbWriteCsvRecord(new_cmd, f, sub_schema[cmd[0]])
f.write("\r\n")
else # Has a duration
f.write(sprintf(" %s = ", cmd[0]))
pbWriteCsvRecord(cmd[1..-1], f, sub_schema[cmd[0]])
f.write("\r\n")
end
end
end
end
end
process_pbs_file_message_end
end
end
#===============================================================================
# Hook into the regular Compiler to also write all animation PBS files.
#===============================================================================
module Compiler
module_function
class << self
if !method_defined?(:__new_anims__write_all)
alias_method :__new_anims__write_all, :write_all
end
end
def write_all
__new_anims__write_all
Console.echo_h1(_INTL("Writing all animation PBS files"))
write_all_battle_animations
echoln ""
Console.echo_h2(_INTL("Successfully rewrote all animation PBS files"), text: :green)
end
end
#===============================================================================
# Debug menu function for writing all animation PBS files. Shouldn't need to be
# used, but it's here if you want it.
#===============================================================================
MenuHandlers.add(:debug_menu, :create_animation_pbs_files, {
"name" => _INTL("Write all animation PBS files"),
"parent" => :files_menu,
"description" => _INTL("Write all animation PBS files."),
"effect" => proc {
Compiler.write_all_battle_animations
}
})

View File

@@ -0,0 +1,428 @@
module AnimationConverter
NO_USER_COMMON_ANIMATIONS = [
"Hail", "HarshSun", "HeavyRain", "Rain", "Sandstorm", "Sun", "ShadowSky",
"Rainbow", "RainbowOpp", "SeaOfFire", "SeaOfFireOpp", "Swamp", "SwampOpp"
]
HAS_TARGET_COMMON_ANIMATIONS = ["LeechSeed", "ParentalBond"]
module_function
def convert_old_animations_to_new
list = pbLoadBattleAnimations
raise "No animations found." if !list || list.length == 0
last_move = nil # For filename purposes
last_version = 0
last_type = :move
list.each do |anim|
next if !anim.name || anim.name == "" || anim.length <= 1
# Get folder and filename for new PBS file
folder = "Example anims/"
folder += (anim.name[/^Common:/]) ? "Common/" : "Move/"
filename = anim.name.gsub(/^Common:/, "")
filename.gsub!(/^Move:/, "")
filename.gsub!(/^OppMove:/, "")
# Update record of move and version
type = :move
if anim.name[/^Common:/]
type = :common
elsif anim.name[/^OppMove:/]
type = :opp_move
elsif anim.name[/^Move:/]
type = :move
end
if filename == anim.name
last_version += 1
type = last_type
pbs_path = folder + last_move
else
last_move = filename
last_version = 0
last_type = type
pbs_path = folder + filename
end
last_move = filename if !last_move
# Generate basic animaiton properties
new_anim = {
:type => type,
:move => last_move,
:version => last_version,
:name => "Example anim",
:particles => [],
:pbs_path => pbs_path
}
# Decide whether the animation involves a user or target
has_user = true
has_target = true
if new_anim[:type] == :common
if NO_USER_COMMON_ANIMATIONS.include?(new_anim[:move])
has_user = false
has_target = false
elsif !HAS_TARGET_COMMON_ANIMATIONS.include?(new_anim[:move])
has_target = false
end
else
move_data = GameData::Move.try_get(new_anim[:move])
if move_data
target_data = GameData::Target.get(move_data.target)
has_target = false if target_data.num_targets == 0 && target_data.id != :None
end
end
new_anim[:no_user] = true if !has_user
new_anim[:no_target] = true if !has_target
add_frames_to_new_anim_hash(anim, new_anim)
add_bg_fg_commands_to_new_anim_hash(anim, new_anim)
add_se_commands_to_new_anim_hash(anim, new_anim)
new_anim[:particles].compact!
GameData::Animation.register(new_anim)
Compiler.write_battle_animation_file(new_anim[:pbs_path])
end
end
#-----------------------------------------------------------------------------
def add_frames_to_new_anim_hash(anim, hash)
# Lookup array for particle index using cel index
index_lookup = []
max_index = -1
# Set up previous frame's values
default_frame = []
default_frame[AnimFrame::X] = -999
default_frame[AnimFrame::Y] = -999
default_frame[AnimFrame::ZOOMX] = 100
default_frame[AnimFrame::ZOOMY] = 100
default_frame[AnimFrame::BLENDTYPE] = 0
default_frame[AnimFrame::ANGLE] = 0
default_frame[AnimFrame::OPACITY] = 255
default_frame[AnimFrame::COLORRED] = 0
default_frame[AnimFrame::COLORGREEN] = 0
default_frame[AnimFrame::COLORBLUE] = 0
default_frame[AnimFrame::COLORALPHA] = 0
default_frame[AnimFrame::TONERED] = 0
default_frame[AnimFrame::TONEGREEN] = 0
default_frame[AnimFrame::TONEBLUE] = 0
default_frame[AnimFrame::TONEGRAY] = 0
default_frame[AnimFrame::PATTERN] = 0
default_frame[AnimFrame::PRIORITY] = 0 # 0=back, 1=front, 2=behind focus, 3=before focus
default_frame[AnimFrame::VISIBLE] = 1 # Boolean
default_frame[AnimFrame::MIRROR] = 0 # Boolean
default_frame[AnimFrame::FOCUS] = 4 # 1=target, 2=user, 3=user and target, 4=screen
default_frame[99] = "Examples/" + anim.graphic
last_frame_values = []
anim_graphic = anim.graphic
anim_graphic.gsub!(".", " ")
anim_graphic.gsub!(" ", " ")
# Go through each frame
anim.length.times do |frame_num|
frame = anim[frame_num]
had_particles = []
changed_particles = []
frame.each_with_index do |cel, i|
next if !cel
next if i == 0 && hash[:no_user]
next if i == 1 && hash[:no_target]
# If the particle from the previous frame for this cel had a different
# focus, start a new particle.
if i > 1 && frame_num > 0 && index_lookup[i] && index_lookup[i] >= 0 &&
last_frame_values[index_lookup[i]]
this_graphic = (cel[AnimFrame::PATTERN] == -1) ? "USER" : (cel[AnimFrame::PATTERN] == -2) ? "TARGET" : "Examples/" + anim_graphic
this_graphic.gsub!(".", " ")
this_graphic.gsub!(" ", "")
focus = cel[AnimFrame::FOCUS]
focus = 2 if (focus == 1 || focus == 3) && hash[:no_target]
focus = 0 if (focus == 2 || focus == 3) && hash[:no_user]
if last_frame_values[index_lookup[i]][AnimFrame::FOCUS] != focus ||
last_frame_values[index_lookup[i]][99] != this_graphic # Graphic
index_lookup[i] = -1
end
end
# Get the particle index for this cel
if !index_lookup[i] || index_lookup[i] < 0
max_index += 1
index_lookup[i] = max_index
end
idx = index_lookup[i]
had_particles.push(idx)
# i=0 for "User", i=1 for "Target"
hash[:particles][idx] ||= { :name => "Particle #{idx}" }
particle = hash[:particles][idx]
last_frame = last_frame_values[idx] || default_frame.clone
# User and target particles have specific names
if i == 0
particle[:name] = "User"
elsif i == 1
particle[:name] = "Target"
else
# Set graphic
case cel[AnimFrame::PATTERN]
when -1 # User's sprite
particle[:graphic] = "USER"
last_frame[99] = "USER"
when -2 # Target's sprite
particle[:graphic] = "TARGET"
last_frame[99] = "TARGET"
else
particle[:graphic] = "Examples/" + anim_graphic
last_frame[99] = "Examples/" + anim_graphic
end
end
# Set focus for non-User/non-Target
if i > 1
focus = cel[AnimFrame::FOCUS]
focus = 2 if (focus == 1 || focus == 3) && hash[:no_target]
focus = 0 if (focus == 2 || focus == 3) && hash[:no_user]
particle[:focus] = [:foreground, :target, :user, :user_and_target, :foreground][focus]
last_frame[AnimFrame::FOCUS] = focus
end
# Copy commands across
[
[AnimFrame::X, :x],
[AnimFrame::Y, :y],
[AnimFrame::ZOOMX, :zoom_x],
[AnimFrame::ZOOMY, :zoom_y],
[AnimFrame::BLENDTYPE, :blending],
[AnimFrame::ANGLE, :angle],
[AnimFrame::OPACITY, :opacity],
[AnimFrame::COLORRED, :color_red],
[AnimFrame::COLORGREEN, :color_green],
[AnimFrame::COLORBLUE, :color_blue],
[AnimFrame::COLORALPHA, :color_alpha],
[AnimFrame::TONERED, :tone_red],
[AnimFrame::TONEGREEN, :tone_green],
[AnimFrame::TONEBLUE, :tone_blue],
[AnimFrame::TONEGRAY, :tone_gray],
[AnimFrame::PATTERN, :frame],
[AnimFrame::PRIORITY, :z],
[AnimFrame::VISIBLE, :visible], # Boolean
[AnimFrame::MIRROR, :flip], # Boolean
].each do |property|
next if cel[property[0]] == last_frame[property[0]]
particle[property[1]] ||= []
val = cel[property[0]].to_i
case property[1]
when :x
case cel[AnimFrame::FOCUS]
when 1 # :target
val -= Battle::Scene::FOCUSTARGET_X
when 2 # :user
val -= Battle::Scene::FOCUSUSER_X
when 3 # :user_and_target
user_x = Battle::Scene::FOCUSUSER_X
target_x = Battle::Scene::FOCUSTARGET_X
if hash[:type] == :opp_move
user_x = Battle::Scene::FOCUSTARGET_X
target_x = Battle::Scene::FOCUSUSER_X
end
fraction = (val - user_x).to_f / (target_x - user_x)
val = (fraction * GameData::Animation::USER_AND_TARGET_SEPARATION[0]).to_i
end
if cel[AnimFrame::FOCUS] != particle[:focus]
pseudo_focus = cel[AnimFrame::FOCUS]
# Was focused on target, now focused on user
pseudo_focus = 2 if [1, 3].include?(pseudo_focus) && hash[:no_target]
# Was focused on user, now focused on screen
val += Battle::Scene::FOCUSUSER_X if [2, 3].include?(pseudo_focus) && hash[:no_user]
end
when :y
case cel[AnimFrame::FOCUS]
when 1 # :target
val -= Battle::Scene::FOCUSTARGET_Y
when 2 # :user
val -= Battle::Scene::FOCUSUSER_Y
when 3 # :user_and_target
user_y = Battle::Scene::FOCUSUSER_Y
target_y = Battle::Scene::FOCUSTARGET_Y
if hash[:type] == :opp_move
user_y = Battle::Scene::FOCUSTARGET_Y
target_y = Battle::Scene::FOCUSUSER_Y
end
fraction = (val - user_y).to_f / (target_y - user_y)
val = (fraction * GameData::Animation::USER_AND_TARGET_SEPARATION[1]).to_i
end
if cel[AnimFrame::FOCUS] != particle[:focus]
pseudo_focus = cel[AnimFrame::FOCUS]
# Was focused on target, now focused on user
pseudo_focus = 2 if [1, 3].include?(pseudo_focus) && hash[:no_target]
# Was focused on user, now focused on screen
val += Battle::Scene::FOCUSUSER_Y if [2, 3].include?(pseudo_focus) && hash[:no_user]
end
when :visible, :flip
val = (val == 1) # Boolean
when :z
next if i <= 1 # User or target
case val
when 0 then val = -50 + i # Back
when 1 then val = 25 + i # Front
when 2 then val = -25 + i # Behind focus
when 3 then val = i # Before focus
end
when :frame
next if val < 0 # -1 is user, -2 is target
end
particle[property[1]].push([frame_num, 0, val])
last_frame[property[0]] = cel[property[0]]
changed_particles.push(idx) if !changed_particles.include?(idx)
end
# Remember this cel's values at this frame
last_frame_values[idx] = last_frame
end
# End every particle lifetime that didn't have a corresponding cel this
# frame
hash[:particles].each_with_index do |particle, idx|
next if !particle || had_particles.include?(idx)
next if ["User", "Target"].include?(particle[:name])
if last_frame_values[idx][AnimFrame::VISIBLE] == 1
particle[:visible] ||= []
particle[:visible].push([frame_num, 0, false])
changed_particles.push(idx) if !changed_particles.include?(idx)
end
last_frame_values[idx][AnimFrame::VISIBLE] = 0
next if !index_lookup.include?(idx)
lookup_idx = index_lookup.index(idx)
index_lookup[lookup_idx] = -1
end
# Add a dummy command in the last frame if that frame doesn't have any
# commands (this makes all visible particles invisible)
if frame_num == anim.length - 1 && changed_particles.empty?
hash[:particles].each_with_index do |particle, idx|
next if !particle || ["User", "Target"].include?(particle[:name])
next if last_frame_values[idx][AnimFrame::VISIBLE] == 0
particle[:visible] ||= []
particle[:visible].push([frame_num + 1, 0, false])
end
end
end
if hash[:particles].any? { |particle| particle[:name] == "User" }
user_particle = hash[:particles].select { |particle| particle[:name] == "User" }[0]
user_particle[:focus] = :user
end
if hash[:particles].any? { |particle| particle[:name] == "Target" }
target_particle = hash[:particles].select { |particle| particle[:name] == "Target" }[0]
target_particle[:focus] = :target
end
end
#-----------------------------------------------------------------------------
def add_bg_fg_commands_to_new_anim_hash(anim, new_anim)
bg_particle = { :name => "Background", :focus => :background }
fg_particle = { :name => "Foreground", :focus => :foreground }
first_bg_frame = 999
first_fg_frame = 999
anim.timing.each do |cmd|
case cmd.timingType
when 1, 2, 3, 4 # BG graphic (set, move/recolour), FG graphic (set, move/recolour)
is_bg = (cmd.timingType <= 2)
particle = (is_bg) ? bg_particle : fg_particle
duration = (cmd.timingType == 2) ? cmd.duration : 0
added = false
if cmd.name && cmd.name != ""
particle[:graphic] ||= []
particle[:graphic].push([cmd.frame, duration, cmd.name])
added = true
end
if cmd.colorRed
particle[:color_red] ||= []
particle[:color_red].push([cmd.frame, duration, cmd.colorRed])
added = true
end
if cmd.colorGreen
particle[:color_green] ||= []
particle[:color_green].push([cmd.frame, duration, cmd.colorGreen])
added = true
end
if cmd.colorBlue
particle[:color_blue] ||= []
particle[:color_blue].push([cmd.frame, duration, cmd.colorBlue])
added = true
end
if cmd.colorAlpha
particle[:color_alpha] ||= []
particle[:color_alpha].push([cmd.frame, duration, cmd.colorAlpha])
added = true
end
if cmd.opacity
particle[:opacity] ||= []
particle[:opacity].push([cmd.frame, duration, cmd.opacity])
added = true
end
if cmd.bgX
particle[:x] ||= []
particle[:x].push([cmd.frame, duration, cmd.bgX])
added = true
end
if cmd.bgY
particle[:y] ||= []
particle[:y].push([cmd.frame, duration, cmd.bgY])
added = true
end
if added
if is_bg
first_bg_frame = [first_bg_frame, cmd.frame].min
else
first_fg_frame = [first_fg_frame, cmd.frame].min
end
end
end
end
if bg_particle.keys.length > 2
if !bg_particle[:graphic]
particle[:graphic] ||= []
particle[:graphic].push([first_bg_frame, 0, "black_screen"])
end
new_anim[:particles].push(bg_particle)
end
if fg_particle.keys.length > 2
if !fg_particle[:graphic]
particle[:graphic] ||= []
particle[:graphic].push([first_fg_frame, 0, "black_screen"])
end
new_anim[:particles].push(fg_particle)
end
end
def add_se_commands_to_new_anim_hash(anim, new_anim)
anim.timing.each do |cmd|
next if cmd.timingType != 0 # Play SE
particle = new_anim[:particles].last
if particle[:name] != "SE"
particle = { :name => "SE" }
new_anim[:particles].push(particle)
end
# Add command
if cmd.name && cmd.name != ""
particle[:se] ||= []
particle[:se].push([cmd.frame, 0, cmd.name, cmd.volume, cmd.pitch])
else # Play user's cry
particle[:user_cry] ||= []
particle[:user_cry].push([cmd.frame, 0, cmd.volume, cmd.pitch])
end
end
end
end
#===============================================================================
# Add to Debug menu.
#===============================================================================
# MenuHandlers.add(:debug_menu, :convert_anims, {
# "name" => "Convert old animation to PBS files",
# "parent" => :main,
# "description" => "This is just for the sake of having lots of example animation PBS files.",
# "effect" => proc {
# AnimationConverter.convert_old_animations_to_new
# }
# })

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,386 @@
#===============================================================================
#
#===============================================================================
class AnimationEditor
def create_pop_up_window(width, height, ret = nil)
if !ret
ret = BitmapSprite.new(width + (BORDER_THICKNESS * 2),
height + (BORDER_THICKNESS * 2), @pop_up_viewport)
ret.x = (WINDOW_WIDTH - ret.width) / 2
ret.y = (WINDOW_HEIGHT - ret.height) / 2
ret.z = -1
end
ret.bitmap.clear
ret.bitmap.font.color = text_color
ret.bitmap.font.size = text_size
# Draw pop-up box border
ret.bitmap.border_rect(BORDER_THICKNESS, BORDER_THICKNESS, width, height,
BORDER_THICKNESS, background_color, line_color)
# Fill pop-up box with white
ret.bitmap.fill_rect(BORDER_THICKNESS, BORDER_THICKNESS, width, height, background_color)
return ret
end
#-----------------------------------------------------------------------------
def message(text, *options)
@pop_up_bg_bitmap.visible = true
msg_bitmap = create_pop_up_window(MESSAGE_BOX_WIDTH, MESSAGE_BOX_HEIGHT)
# Draw text
text_size = msg_bitmap.bitmap.text_size(text)
msg_bitmap.bitmap.draw_text(0, (msg_bitmap.height / 2) - MESSAGE_BOX_BUTTON_HEIGHT,
msg_bitmap.width, text_size.height, text, 1)
# Create buttons
buttons = []
options.each_with_index do |option, i|
btn = UIControls::Button.new(MESSAGE_BOX_BUTTON_WIDTH, MESSAGE_BOX_BUTTON_HEIGHT, @pop_up_viewport, option[1])
btn.x = msg_bitmap.x + (msg_bitmap.width - (MESSAGE_BOX_BUTTON_WIDTH * options.length)) / 2
btn.x += MESSAGE_BOX_BUTTON_WIDTH * i
btn.y = msg_bitmap.y + msg_bitmap.height - MESSAGE_BOX_BUTTON_HEIGHT - MESSAGE_BOX_SPACING
btn.set_fixed_size
btn.color_scheme = @color_scheme
btn.set_interactive_rects
buttons.push([option[0], btn])
end
# Interaction loop
ret = nil
captured = nil
loop do
Graphics.update
Input.update
if captured
captured.update
captured = nil if !captured.busy?
else
buttons.each do |btn|
btn[1].update
captured = btn[1] if btn[1].busy?
end
end
buttons.each do |btn|
next if !btn[1].changed?
ret = btn[0]
break
end
ret = :cancel if Input.triggerex?(:ESCAPE)
break if ret
buttons.each { |btn| btn[1].repaint }
end
# Dispose and return
buttons.each { |btn| btn[1].dispose }
buttons.clear
msg_bitmap.dispose
@pop_up_bg_bitmap.visible = false
return ret
end
def confirm_message(text)
return message(text, [:yes, _INTL("Yes")], [:no, _INTL("No")]) == :yes
end
#-----------------------------------------------------------------------------
def edit_animation_properties
# Show pop-up window
@pop_up_bg_bitmap.visible = true
bg_bitmap = create_pop_up_window(ANIM_PROPERTIES_WIDTH, ANIM_PROPERTIES_HEIGHT)
anim_properties = @components[:animation_properties]
anim_properties.visible = true
# Set control values
case @anim[:type]
when :move, :opp_move
anim_properties.get_control(:type).value = :move
when :common, :opp_common
anim_properties.get_control(:type).value = :common
end
anim_properties.get_control(:opp_variant).value = ([:opp_move, :opp_common].include?(@anim[:type]))
anim_properties.get_control(:version).value = @anim[:version] || 0
anim_properties.get_control(:name).value = @anim[:name] || ""
anim_properties.get_control(:pbs_path).value = (@anim[:pbs_path] || "unsorted") + ".txt"
anim_properties.get_control(:has_user).value = !@anim[:no_user]
anim_properties.get_control(:has_target).value = !@anim[:no_target]
anim_properties.get_control(:usable).value = !(@anim[:ignore] || false)
refresh_component(:animation_properties) # This sets the :move control's value
# Interaction loop
ret = nil
loop do
Graphics.update
Input.update
anim_properties.update
if anim_properties.changed?
break if anim_properties.values.keys.include?(:close)
anim_properties.values.each_pair do |property, value|
apply_changed_value(:animation_properties, property, value)
end
anim_properties.clear_changed
end
break if !anim_properties.busy? && Input.triggerex?(:ESCAPE)
anim_properties.repaint
end
# Dispose and return
bg_bitmap.dispose
@pop_up_bg_bitmap.visible = false
anim_properties.clear_changed
anim_properties.visible = false
end
#-----------------------------------------------------------------------------
def edit_editor_settings
# Show pop-up window
@pop_up_bg_bitmap.visible = true
bg_bitmap = create_pop_up_window(ANIM_PROPERTIES_WIDTH, ANIM_PROPERTIES_HEIGHT)
editor_settings = @components[:editor_settings]
editor_settings.visible = true
# Set control values
refresh_component(:editor_settings)
editor_settings.get_control(:color_scheme).value = @settings[:color_scheme] || :light
editor_settings.get_control(:side_size_1).value = @settings[:side_sizes][0]
editor_settings.get_control(:side_size_2).value = @settings[:side_sizes][1]
editor_settings.get_control(:user_index).value = @settings[:user_index]
editor_settings.get_control(:target_indices).value = @settings[:target_indices].join(",")
editor_settings.get_control(:user_opposes).value = @settings[:user_opposes]
editor_settings.get_control(:canvas_bg).value = @settings[:canvas_bg]
editor_settings.get_control(:user_sprite_name).value = @settings[:user_sprite_name]
editor_settings.get_control(:target_sprite_name).value = @settings[:target_sprite_name]
# Interaction loop
ret = nil
loop do
Graphics.update
Input.update
editor_settings.update
if editor_settings.changed?
break if editor_settings.values.keys.include?(:close)
editor_settings.values.each_pair do |property, value|
apply_changed_value(:editor_settings, property, value)
create_pop_up_window(ANIM_PROPERTIES_WIDTH, ANIM_PROPERTIES_HEIGHT, bg_bitmap)
end
editor_settings.clear_changed
end
break if !editor_settings.busy? && Input.triggerex?(:ESCAPE)
editor_settings.repaint
end
# Dispose and return
bg_bitmap.dispose
@pop_up_bg_bitmap.visible = false
editor_settings.clear_changed
editor_settings.visible = false
end
#-----------------------------------------------------------------------------
# Generates a list of all files in the given folder and its subfolders which
# have a file extension that matches one in exts. Removes any files from the
# list whose filename is the same as one in blacklist (case insensitive).
def get_all_files_in_folder(folder, exts, blacklist = [])
ret = []
Dir.all(folder).each do |f|
next if !exts.include?(File.extname(f))
file = f.sub(folder + "/", "")
ret.push([file.sub(File.extname(file), ""), file])
end
ret.delete_if { |f| blacklist.any? { |add| add.upcase == f[0].upcase } }
ret.sort! { |a, b| a[0].downcase <=> b[0].downcase }
return ret
end
def choose_graphic_file(selected)
selected ||= ""
sprite_folder = "Graphics/Battle animations"
# Show pop-up window
@pop_up_bg_bitmap.visible = true
bg_bitmap = create_pop_up_window(GRAPHIC_CHOOSER_WINDOW_WIDTH, GRAPHIC_CHOOSER_WINDOW_HEIGHT)
graphic_chooser = @components[:graphic_chooser]
graphic_chooser.visible = true
# Draw box around list control
list = graphic_chooser.get_control(:list)
# Get a list of files
files = get_all_files_in_folder(
sprite_folder, [".png", ".jpg", ".jpeg"],
["USER", "USER_OPP", "USER_FRONT", "USER_BACK", "TARGET", "TARGET_OPP", "TARGET_FRONT", "TARGET_BACK"]
)
if !@anim[:no_target]
files.prepend(
["TARGET", _INTL("[[Target's sprite]]")],
["TARGET_OPP", _INTL("[[Target's other side sprite]]")],
["TARGET_FRONT", _INTL("[[Target's front sprite]]")],
["TARGET_BACK", _INTL("[[Target's back sprite]]")]
)
end
if !@anim[:no_user]
files.prepend(
["USER", _INTL("[[User's sprite]]")],
["USER_OPP", _INTL("[[User's other side sprite]]")],
["USER_FRONT", _INTL("[[User's front sprite]]")],
["USER_BACK", _INTL("[[User's back sprite]]")]
)
end
idx = 0
files.each_with_index do |f, i|
next if f[0] != selected
idx = i
break
end
# Set control values
list.values = files
list.selected = idx
# Create sprite preview
bg_bitmap.bitmap.outline_rect(BORDER_THICKNESS + list.x + list.width + 6,
BORDER_THICKNESS + list.y,
GRAPHIC_CHOOSER_PREVIEW_SIZE + 4, GRAPHIC_CHOOSER_PREVIEW_SIZE + 4,
line_color)
preview_sprite = Sprite.new(@pop_up_viewport)
preview_sprite.x = graphic_chooser.x + list.x + list.width + 8 + (GRAPHIC_CHOOSER_PREVIEW_SIZE / 2)
preview_sprite.y = graphic_chooser.y + list.y + 2 + (GRAPHIC_CHOOSER_PREVIEW_SIZE / 2)
preview_bitmap = nil
set_preview_graphic = lambda do |sprite, filename|
preview_bitmap&.dispose
folder = sprite_folder + "/"
fname = filename
if ["USER", "USER_BACK", "USER_FRONT", "USER_OPP",
"TARGET", "TARGET_FRONT", "TARGET_BACK", "TARGET_OPP"].include?(filename)
chunks = filename.split("_")
fname = (chunks[0] == "USER") ? @settings[:user_sprite_name].to_s : @settings[:target_sprite_name].to_s
case chunks[1] || ""
when "", "OPP"
if (chunks[0] == "USER") ^ (chunks[1] == "OPP") # xor
folder = (@settings[:user_opposes]) ? "Graphics/Pokemon/Front/" : "Graphics/Pokemon/Back/"
else
folder = (@settings[:user_opposes]) ? "Graphics/Pokemon/Back/" : "Graphics/Pokemon/Front/"
end
when "FRONT"
folder = "Graphics/Pokemon/Front/"
when "BACK"
folder = "Graphics/Pokemon/Back/"
end
end
preview_bitmap = AnimatedBitmap.new(folder + fname)
bg_bitmap.bitmap.fill_rect(BORDER_THICKNESS + list.x + list.width + 8, BORDER_THICKNESS + list.y + 2,
GRAPHIC_CHOOSER_PREVIEW_SIZE, GRAPHIC_CHOOSER_PREVIEW_SIZE,
background_color)
next if !preview_bitmap
sprite.bitmap = preview_bitmap.bitmap
zoom = [[GRAPHIC_CHOOSER_PREVIEW_SIZE.to_f / preview_bitmap.width,
GRAPHIC_CHOOSER_PREVIEW_SIZE.to_f / preview_bitmap.height].min, 1.0].min
sprite.zoom_x = sprite.zoom_y = zoom
sprite.ox = sprite.width / 2
sprite.oy = sprite.height / 2
bg_bitmap.bitmap.fill_rect(BORDER_THICKNESS + sprite.x - graphic_chooser.x - (sprite.width * sprite.zoom_x / 2).round,
BORDER_THICKNESS + sprite.y - graphic_chooser.y - (sprite.height * sprite.zoom_y / 2).round,
sprite.width * sprite.zoom_x, sprite.height * sprite.zoom_y,
Color.magenta)
end
set_preview_graphic.call(preview_sprite, list.value)
# Interaction loop
ret = nil
loop do
Graphics.update
Input.update
graphic_chooser.update
if graphic_chooser.changed?
graphic_chooser.values.each_pair do |ctrl, value|
case ctrl
when :ok
ret = list.value
when :cancel
ret = selected
when :list
set_preview_graphic.call(preview_sprite, list.value)
end
graphic_chooser.clear_changed
end
break if ret
graphic_chooser.repaint
end
if !graphic_chooser.busy? && Input.triggerex?(:ESCAPE)
ret = selected
break
end
end
# Dispose and return
bg_bitmap.dispose
preview_sprite.dispose
preview_bitmap&.dispose
@pop_up_bg_bitmap.visible = false
graphic_chooser.clear_changed
graphic_chooser.visible = false
return ret
end
#-----------------------------------------------------------------------------
def choose_audio_file(selected, volume = 100, pitch = 100)
selected ||= ""
audio_folder = "Audio/SE/Anim"
# Show pop-up window
@pop_up_bg_bitmap.visible = true
bg_bitmap = create_pop_up_window(AUDIO_CHOOSER_WINDOW_WIDTH, AUDIO_CHOOSER_WINDOW_HEIGHT)
audio_chooser = @components[:audio_chooser]
audio_chooser.visible = true
# Draw box around list control
list = audio_chooser.get_control(:list)
# Get a list of files
files = get_all_files_in_folder(audio_folder, [".wav", ".ogg", ".mp3", ".wma"], ["USER", "TARGET"])
files.prepend(["TARGET", _INTL("[[Target's cry]]")]) if !@anim[:no_target]
files.prepend(["USER", _INTL("[[User's cry]]")]) if !@anim[:no_user]
idx = 0
files.each_with_index do |f, i|
next if f[0] != selected
idx = i
break
end
# Set control values
list.values = files
list.selected = idx
audio_chooser.get_control(:volume).value = volume
audio_chooser.get_control(:pitch).value = pitch
# Interaction loop
ret = nil
cancel = false
loop do
Graphics.update
Input.update
audio_chooser.update
if audio_chooser.changed?
audio_chooser.values.each_pair do |ctrl, value|
case ctrl
when :ok
ret = list.value
when :cancel
ret = selected
cancel = true
when :play
vol = audio_chooser.get_control(:volume).value
ptch = audio_chooser.get_control(:pitch).value
case list.value
when "USER"
Pokemon.play_cry(@settings[:user_sprite_name])
when "TARGET"
Pokemon.play_cry(@settings[:target_sprite_name])
else
pbSEPlay(RPG::AudioFile.new("Anim/" + list.value, vol, ptch))
end
when :stop
pbSEStop
end
audio_chooser.clear_changed
end
break if ret
audio_chooser.repaint
end
if !audio_chooser.busy? && Input.triggerex?(:ESCAPE)
ret = selected
cancel = true
break
end
end
vol = (cancel) ? volume : audio_chooser.get_control(:volume).value
ptch = (cancel) ? pitch : audio_chooser.get_control(:pitch).value
# Dispose and return
bg_bitmap.dispose
@pop_up_bg_bitmap.visible = false
audio_chooser.clear_changed
audio_chooser.visible = false
return [ret, vol, ptch]
end
end

View File

@@ -0,0 +1,678 @@
#===============================================================================
#
#===============================================================================
module AnimationEditor::SidePanes
@@panes = {}
@@properties = {}
def self.is_side_pane?(pane)
return @@panes.keys.include?(pane)
end
def self.add_pane(symbol, hash)
@@panes[symbol] = hash
end
def self.add_property(pane, symbol, hash)
@@properties[pane] ||= {}
@@properties[pane][symbol] = hash
end
def self.each_pane
@@panes.each_pair { |pane, hash| yield pane, hash }
end
def self.each_property(pane)
return if !@@properties[pane]
@@properties[pane].each_pair do |property, hash|
yield property, hash
end
end
def self.get_pane(pane)
return @@panes[pane]
end
def self.get_property(pane, property)
return nil if !@@properties[pane] || !@@properties[pane][property]
return @@properties[pane][property]
end
def self.remove_pane(pane)
@@panes.remove(pane)
@@properties.remove(pane)
end
def self.remove_property(pane, property)
@@properties[pane]&.remove(property)
end
end
#===============================================================================
#
#===============================================================================
AnimationEditor::SidePanes.add_pane(:commands_pane, {
:deletable_properties => AnimationEditor::DELETABLE_COMMAND_PANE_PROPERTIES,
:set_visible => proc { |editor, anim, keyframe, particle_index|
next keyframe >= 0 && particle_index >= 0 &&
anim[:particles][particle_index] &&
anim[:particles][particle_index][:name] != "SE" &&
editor.property_pane == :commands_pane
},
:apply_value => proc { |property, value, editor|
particle = editor.anim[:particles][editor.particle_index]
prop = property
if property.to_s[/_delete$/]
prop = property.to_s.sub(/_delete$/, "").to_sym
new_cmds = AnimationEditor::ParticleDataHelper.delete_command(particle, prop, editor.keyframe)
else
new_cmds = AnimationEditor::ParticleDataHelper.add_command(particle, property, editor.keyframe, value)
end
if new_cmds
particle[prop] = new_cmds
else
particle.delete(prop)
end
editor.components[:particle_list].change_particle_commands(editor.particle_index)
editor.components[:play_controls].duration = editor.components[:particle_list].duration
editor.refresh_component(:commands_pane)
editor.refresh_component(:canvas)
}
})
AnimationEditor::SidePanes.add_pane(:color_tone_pane, {
:deletable_properties => AnimationEditor::DELETABLE_COLOR_TONE_PANE_PROPERTIES,
:set_visible => proc { |editor, anim, keyframe, particle_index|
next keyframe >= 0 && particle_index >= 0 &&
anim[:particles][particle_index] &&
anim[:particles][particle_index][:name] != "SE" &&
editor.property_pane == :color_tone_pane
},
:apply_value => proc { |property, value, editor|
particle = editor.anim[:particles][editor.particle_index]
prop = property
if property.to_s[/_delete$/]
prop = property.to_s.sub(/_delete$/, "").to_sym
new_cmds = AnimationEditor::ParticleDataHelper.delete_command(particle, prop, editor.keyframe)
else
new_cmds = AnimationEditor::ParticleDataHelper.add_command(particle, property, editor.keyframe, value)
end
if new_cmds
particle[prop] = new_cmds
else
particle.delete(prop)
end
editor.components[:particle_list].change_particle_commands(editor.particle_index)
editor.components[:play_controls].duration = editor.components[:particle_list].duration
editor.refresh_component(:color_tone_pane)
editor.refresh_component(:canvas)
}
})
# NOTE: Doesn't need an :apply_value proc.
AnimationEditor::SidePanes.add_pane(:se_pane, {
:set_visible => proc { |editor, anim, keyframe, particle_index|
next keyframe >= 0 && particle_index >= 0 &&
anim[:particles][particle_index] &&
anim[:particles][particle_index][:name] == "SE"
}
})
AnimationEditor::SidePanes.add_pane(:particle_pane, {
:unchanging_properties => true,
:set_visible => proc { |editor, anim, keyframe, particle_index|
next keyframe < 0 && particle_index >= 0
},
:apply_value => proc { |property, value, editor|
particle = editor.anim[:particles][editor.particle_index]
new_cmds = AnimationEditor::ParticleDataHelper.set_property(particle, property, value)
editor.components[:particle_list].change_particle(editor.particle_index)
editor.refresh_component(:particle_pane)
editor.refresh_component(:canvas)
}
})
#===============================================================================
#
#===============================================================================
AnimationEditor::SidePanes.add_property(:commands_pane, :header, {
:new => proc { |pane, editor|
pane.add_header_label(:header, _INTL("Edit particle at keyframe"))
}
})
AnimationEditor::SidePanes.add_property(:commands_pane, :tab_buttons, {
:new => proc { |pane, editor|
editor.add_side_pane_tab_buttons(:commands_pane, pane)
}
})
AnimationEditor::SidePanes.add_property(:commands_pane, :x, {
:new => proc { |pane, editor|
pane.add_labelled_number_text_box(:x, _INTL("X"), -999, 999, 0)
}
})
AnimationEditor::SidePanes.add_property(:commands_pane, :y, {
:new => proc { |pane, editor|
pane.add_labelled_number_text_box(:y, _INTL("Y"), -999, 999, 0)
}
})
AnimationEditor::SidePanes.add_property(:commands_pane, :z, {
:new => proc { |pane, editor|
pane.add_labelled_number_slider(:z, _INTL("Priority"), -50, 50, 0)
},
:refresh_value => proc { |control, editor|
# Set an appropriate range for the priority (z) property depending on the
# particle's focus
case editor.anim[:particles][editor.particle_index][:focus]
when :user_and_target
control.min_value = GameData::Animation::USER_AND_TARGET_SEPARATION[2] - 50
control.max_value = 50
else
control.min_value = -50
control.max_value = 50
end
}
})
AnimationEditor::SidePanes.add_property(:commands_pane, :frame, {
:new => proc { |pane, editor|
pane.add_labelled_number_text_box(:frame, _INTL("Frame"), 0, 99, 0)
},
:refresh_value => proc { |control, editor|
# Disable the "Frame" control if the particle's graphic is predefined to be
# the user's or target's sprite
graphic = editor.anim[:particles][editor.particle_index][:graphic]
if ["USER", "USER_OPP", "USER_FRONT", "USER_BACK",
"TARGET", "TARGET_OPP", "TARGET_FRONT", "TARGET_BACK"].include?(graphic)
control.disable
else
control.enable
end
}
})
AnimationEditor::SidePanes.add_property(:commands_pane, :visible, {
:new => proc { |pane, editor|
pane.add_labelled_checkbox(:visible, _INTL("Visible"), true)
}
})
AnimationEditor::SidePanes.add_property(:commands_pane, :opacity, {
:new => proc { |pane, editor|
pane.add_labelled_number_slider(:opacity, _INTL("Opacity"), 0, 255, 255)
}
})
AnimationEditor::SidePanes.add_property(:commands_pane, :zoom_x, {
:new => proc { |pane, editor|
pane.add_labelled_number_text_box(:zoom_x, _INTL("Zoom X"), 0, 1000, 100)
}
})
AnimationEditor::SidePanes.add_property(:commands_pane, :zoom_y, {
:new => proc { |pane, editor|
pane.add_labelled_number_text_box(:zoom_y, _INTL("Zoom Y"), 0, 1000, 100)
}
})
AnimationEditor::SidePanes.add_property(:commands_pane, :angle, {
:new => proc { |pane, editor|
pane.add_labelled_number_text_box(:angle, _INTL("Angle"), -1080, 1080, 0)
}
})
AnimationEditor::SidePanes.add_property(:commands_pane, :flip, {
:new => proc { |pane, editor|
pane.add_labelled_checkbox(:flip, _INTL("Flip"), false)
}
})
AnimationEditor::SidePanes.add_property(:commands_pane, :blending, {
:new => proc { |pane, editor|
pane.add_labelled_dropdown_list(:blending, _INTL("Blending"), {
0 => _INTL("None"),
1 => _INTL("Additive"),
2 => _INTL("Subtractive")
}, 0)
}
})
#===============================================================================
#
#===============================================================================
AnimationEditor::SidePanes.add_property(:color_tone_pane, :header, {
:new => proc { |pane, editor|
pane.add_header_label(:header, _INTL("Edit particle at keyframe"))
}
})
AnimationEditor::SidePanes.add_property(:color_tone_pane, :tab_buttons, {
:new => proc { |pane, editor|
editor.add_side_pane_tab_buttons(:color_tone_pane, pane)
}
})
AnimationEditor::SidePanes.add_property(:color_tone_pane, :color_red, {
:new => proc { |pane, editor|
pane.add_labelled_number_slider(:color_red, _INTL("Color Red"), 0, 255, 0)
}
})
AnimationEditor::SidePanes.add_property(:color_tone_pane, :color_green, {
:new => proc { |pane, editor|
pane.add_labelled_number_slider(:color_green, _INTL("Color Green"), 0, 255, 0)
}
})
AnimationEditor::SidePanes.add_property(:color_tone_pane, :color_blue, {
:new => proc { |pane, editor|
pane.add_labelled_number_slider(:color_blue, _INTL("Color Blue"), 0, 255, 0)
}
})
AnimationEditor::SidePanes.add_property(:color_tone_pane, :color_alpha, {
:new => proc { |pane, editor|
pane.add_labelled_number_slider(:color_alpha, _INTL("Color Alpha"), 0, 255, 0)
}
})
AnimationEditor::SidePanes.add_property(:color_tone_pane, :tone_red, {
:new => proc { |pane, editor|
pane.add_labelled_number_slider(:tone_red, _INTL("Tone Red"), -255, 255, 0)
}
})
AnimationEditor::SidePanes.add_property(:color_tone_pane, :tone_green, {
:new => proc { |pane, editor|
pane.add_labelled_number_slider(:tone_green, _INTL("Tone Green"), -255, 255, 0)
}
})
AnimationEditor::SidePanes.add_property(:color_tone_pane, :tone_blue, {
:new => proc { |pane, editor|
pane.add_labelled_number_slider(:tone_blue, _INTL("Tone Blue"), -255, 255, 0)
}
})
AnimationEditor::SidePanes.add_property(:color_tone_pane, :tone_gray, {
:new => proc { |pane, editor|
pane.add_labelled_number_slider(:tone_gray, _INTL("Tone Gray"), 0, 255, 0)
}
})
#===============================================================================
#
#===============================================================================
AnimationEditor::SidePanes.add_property(:se_pane, :header, {
:new => proc { |pane, editor|
pane.add_header_label(:header, _INTL("Edit sound effects at keyframe"))
}
})
AnimationEditor::SidePanes.add_property(:se_pane, :list, {
:new => proc { |pane, editor|
size = pane.control_size
size[0] -= 6
size[1] = (UIControls::List::ROW_HEIGHT * 5) + (UIControls::List::BORDER_THICKNESS * 2) # 5 rows
list = UIControls::List.new(*size, pane.viewport, [])
pane.add_control_at(:list, list, 3, 28)
},
:refresh_value => proc { |control, editor|
se_particle = editor.anim[:particles].select { |particle| particle[:name] == "SE" }[0]
keyframe = editor.keyframe
# Populate list of files
list = []
se_particle.each_pair do |property, values|
next if !values.is_a?(Array)
values.each do |val|
next if val[0] != keyframe
text = AnimationEditor::ParticleDataHelper.get_se_display_text(property, val)
case property
when :user_cry then list.push(["USER", text])
when :target_cry then list.push(["TARGET", text])
when :se then list.push([val[2], text])
end
end
end
list.sort! { |a, b| a[1].downcase <=> b[1].downcase }
control.values = list
},
:apply_value => proc { |value, editor|
editor.refresh_component(:se_pane)
}
})
AnimationEditor::SidePanes.add_property(:se_pane, :add, {
:new => proc { |pane, editor|
button_height = UIControls::ControlsContainer::LINE_SPACING
button = UIControls::Button.new(101, button_height, pane.viewport, _INTL("Add"))
button.set_fixed_size
pane.add_control_at(:add, button, 1, 154)
},
:apply_value => proc { |value, editor|
new_file, new_volume, new_pitch = editor.choose_audio_file("", 100, 100)
if new_file != ""
particle = editor.anim[:particles][editor.particle_index]
AnimationEditor::ParticleDataHelper.add_se_command(particle, editor.keyframe, new_file, new_volume, new_pitch)
editor.components[:particle_list].change_particle_commands(editor.particle_index)
editor.components[:play_controls].duration = editor.components[:particle_list].duration
editor.refresh_component(:se_pane)
end
}
})
AnimationEditor::SidePanes.add_property(:se_pane, :edit, {
:new => proc { |pane, editor|
button_height = UIControls::ControlsContainer::LINE_SPACING
button = UIControls::Button.new(100, button_height, pane.viewport, _INTL("Edit"))
button.set_fixed_size
pane.add_control_at(:edit, button, 102, 154)
},
:refresh_value => proc { |control, editor|
has_se = AnimationEditor::ParticleDataHelper.has_se_command_at?(editor.anim[:particles], editor.keyframe)
list = editor.components[:se_pane].get_control(:list)
if has_se && list.value
control.enable
else
control.disable
end
},
:apply_value => proc { |value, editor|
particle = editor.anim[:particles][editor.particle_index]
list = editor.components[:se_pane].get_control(:list)
old_file = list.value
old_volume, old_pitch = AnimationEditor::ParticleDataHelper.get_se_values_from_filename_and_frame(particle, editor.keyframe, old_file)
if old_file
new_file, new_volume, new_pitch = editor.choose_audio_file(old_file, old_volume, old_pitch)
if new_file != old_file || new_volume != old_volume || new_pitch != old_pitch
AnimationEditor::ParticleDataHelper.delete_se_command(particle, editor.keyframe, old_file)
AnimationEditor::ParticleDataHelper.add_se_command(particle, editor.keyframe, new_file, new_volume, new_pitch)
editor.components[:particle_list].change_particle_commands(editor.particle_index)
editor.components[:play_controls].duration = editor.components[:particle_list].duration
editor.refresh_component(:se_pane)
end
end
}
})
AnimationEditor::SidePanes.add_property(:se_pane, :delete, {
:new => proc { |pane, editor|
button_height = UIControls::ControlsContainer::LINE_SPACING
button = UIControls::Button.new(101, button_height, pane.viewport, _INTL("Delete"))
button.set_fixed_size
pane.add_control_at(:delete, button, 202, 154)
},
:refresh_value => proc { |control, editor|
has_se = AnimationEditor::ParticleDataHelper.has_se_command_at?(editor.anim[:particles], editor.keyframe)
list = editor.components[:se_pane].get_control(:list)
if has_se && list.value
control.enable
else
control.disable
end
},
:apply_value => proc { |value, editor|
particle = editor.anim[:particles][editor.particle_index]
list = editor.components[:se_pane].get_control(:list)
old_file = list.value
if old_file
AnimationEditor::ParticleDataHelper.delete_se_command(particle, editor.keyframe, old_file)
editor.components[:particle_list].change_particle_commands(editor.particle_index)
editor.components[:play_controls].duration = editor.components[:particle_list].duration
editor.refresh_component(:se_pane)
end
}
})
#===============================================================================
#
#===============================================================================
AnimationEditor::SidePanes.add_property(:particle_pane, :header, {
:new => proc { |pane, editor|
pane.add_header_label(:header, _INTL("Edit particle properties"))
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :name, {
:new => proc { |pane, editor|
pane.add_labelled_text_box(:name, _INTL("Name"), "")
pane.get_control(:name).set_blacklist("", "User", "Target", "SE")
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :graphic_name, {
:new => proc { |pane, editor|
pane.add_labelled_label(:graphic_name, _INTL("Graphic"), "")
},
:refresh_value => proc { |control, editor|
graphic_name = editor.anim[:particles][editor.particle_index][:graphic]
graphic_override_names = {
"USER" => _INTL("[[User's sprite]]"),
"USER_OPP" => _INTL("[[User's other side sprite]]"),
"USER_FRONT" => _INTL("[[User's front sprite]]"),
"USER_BACK" => _INTL("[[User's back sprite]]"),
"TARGET" => _INTL("[[Target's sprite]]"),
"TARGET_OPP" => _INTL("[[Target's other side sprite]]"),
"TARGET_FRONT" => _INTL("[[Target's front sprite]]"),
"TARGET_BACK" => _INTL("[[Target's back sprite]]"),
}
graphic_name = graphic_override_names[graphic_name] if graphic_override_names[graphic_name]
control.text = graphic_name
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :graphic, {
:new => proc { |pane, editor|
pane.add_labelled_button(:graphic, "", _INTL("Change"))
},
:refresh_value => proc { |control, editor|
if ["User", "Target"].include?(editor.anim[:particles][editor.particle_index][:name])
control.disable
else
control.enable
end
},
:apply_value => proc { |value, editor|
p_index = editor.particle_index
new_file = editor.choose_graphic_file(editor.anim[:particles][p_index][:graphic])
if editor.anim[:particles][p_index][:graphic] != new_file
editor.anim[:particles][p_index][:graphic] = new_file
if ["USER", "USER_BACK", "USER_FRONT", "USER_OPP",
"TARGET", "TARGET_FRONT", "TARGET_BACK", "TARGET_OPP"].include?(new_file)
editor.anim[:particles][p_index].delete(:frame)
editor.components[:particle_list].set_particles(editor.anim[:particles])
editor.refresh_component(:particle_list)
end
editor.refresh_component(:particle_pane)
editor.refresh_component(:canvas)
end
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :focus, {
:new => proc { |pane, editor|
pane.add_labelled_dropdown_list(:focus, _INTL("Focus"), {}, :undefined)
},
:refresh_value => proc { |control, editor|
if ["User", "Target"].include?(editor.anim[:particles][editor.particle_index][:name])
control.disable
else
control.enable
end
focus_values = {
:foreground => _INTL("Foreground"),
:midground => _INTL("Midground"),
:background => _INTL("Background"),
:user => _INTL("User"),
:target => _INTL("Target"),
:user_and_target => _INTL("User and target"),
:user_side_foreground => _INTL("In front of user's side"),
:user_side_background => _INTL("Behind user's side"),
:target_side_foreground => _INTL("In front of target's side"),
:target_side_background => _INTL("Behind target's side")
}
if editor.anim[:no_user]
GameData::Animation::FOCUS_TYPES_WITH_USER.each { |f| focus_values.delete(f) }
end
if editor.anim[:no_target]
GameData::Animation::FOCUS_TYPES_WITH_TARGET.each { |f| focus_values.delete(f) }
end
control.values = focus_values
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :random_frame_max, {
:new => proc { |pane, editor|
pane.add_labelled_number_text_box(:random_frame_max, _INTL("Rand. frame"), 0, 99, 0)
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :spawner, {
:new => proc { |pane, editor|
values = {
:none => _INTL("None"),
:random_direction => _INTL("Random direction"),
:random_direction_gravity => _INTL("Random dir. with gravity"),
:random_up_direction_gravity => _INTL("Random up dir. gravity")
}
pane.add_labelled_dropdown_list(:spawner, _INTL("Spawner"), values, :none)
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :spawn_quantity, {
:new => proc { |pane, editor|
pane.add_labelled_number_text_box(:spawn_quantity, _INTL("Spawn qty"), 1, 99, 1)
},
:refresh_value => proc { |control, editor|
spawner = editor.anim[:particles][editor.particle_index][:spawner]
if !spawner || spawner == :none
control.disable
else
control.enable
end
},
:apply_value => proc { |value, editor|
AnimationEditor::SidePanes.get_pane(:particle_pane)[:apply_value].call(:spawn_quantity, value, editor)
editor.components[:particle_list].change_particle_commands(editor.particle_index)
editor.components[:play_controls].duration = editor.components[:particle_list].duration
editor.refresh
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :angle_override, {
:new => proc { |pane, editor|
values = {
:none => _INTL("None"),
:initial_angle_to_focus => _INTL("Initial angle to focus"),
:always_point_at_focus => _INTL("Always point at focus")
}
pane.add_labelled_dropdown_list(:angle_override, _INTL("Smart angle"), values, :none)
},
:refresh_value => proc { |control, editor|
focus = editor.anim[:particles][editor.particle_index][:focus]
if !GameData::Animation::FOCUS_TYPES_WITH_USER.include?(focus) &&
!GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(focus)
editor.anim[:particles][editor.particle_index][:angle_override] = :none
control.value = :none
control.disable
else
control.enable
end
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :opposing_label, {
:new => proc { |pane, editor|
pane.add_label(:opposing_label, _INTL("If on opposing side..."))
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :foe_invert_x, {
:new => proc { |pane, editor|
pane.add_labelled_checkbox(:foe_invert_x, _INTL("Invert X"), false)
},
:refresh_value => proc { |control, editor|
focus = editor.anim[:particles][editor.particle_index][:focus]
if GameData::Animation::FOCUS_TYPES_WITH_USER.include?(focus) == GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(focus)
control.disable
else
control.enable
end
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :foe_invert_y, {
:new => proc { |pane, editor|
pane.add_labelled_checkbox(:foe_invert_y, _INTL("Invert Y"), false)
},
:refresh_value => proc { |control, editor|
focus = editor.anim[:particles][editor.particle_index][:focus]
if GameData::Animation::FOCUS_TYPES_WITH_USER.include?(focus) == GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(focus)
control.disable
else
control.enable
end
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :foe_flip, {
:new => proc { |pane, editor|
pane.add_labelled_checkbox(:foe_flip, _INTL("Flip sprite"), false)
},
:refresh_value => proc { |control, editor|
focus = editor.anim[:particles][editor.particle_index][:focus]
if GameData::Animation::FOCUS_TYPES_WITH_USER.include?(focus) == GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(focus)
control.disable
else
control.enable
end
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :duplicate, {
:new => proc { |pane, editor|
pane.add_button(:duplicate, _INTL("Duplicate this particle"))
},
:refresh_value => proc { |control, editor|
if editor.anim[:particles][editor.particle_index][:name] == "SE"
control.disable
else
control.enable
end
},
:apply_value => proc { |value, editor|
p_index = editor.particle_index
AnimationEditor::ParticleDataHelper.duplicate_particle(editor.anim[:particles], p_index)
editor.components[:particle_list].add_particle(p_index + 1)
editor.components[:particle_list].set_particles(editor.anim[:particles])
editor.components[:particle_list].particle_index = p_index + 1
editor.refresh
}
})
AnimationEditor::SidePanes.add_property(:particle_pane, :delete, {
:new => proc { |pane, editor|
pane.add_button(:delete, _INTL("Delete this particle"))
},
:refresh_value => proc { |control, editor|
if ["User", "Target", "SE"].include?(editor.anim[:particles][editor.particle_index][:name])
control.disable
else
control.enable
end
},
:apply_value => proc { |value, editor|
if editor.confirm_message(_INTL("Are you sure you want to delete this particle?"))
p_index = editor.particle_index
AnimationEditor::ParticleDataHelper.delete_particle(editor.anim[:particles], p_index)
editor.components[:particle_list].delete_particle(p_index)
editor.components[:particle_list].set_particles(editor.anim[:particles])
p_index = editor.particle_index
editor.components[:particle_list].keyframe = 0 if editor.anim[:particles][p_index][:name] == "SE"
editor.refresh
end
}
})

View File

@@ -0,0 +1,445 @@
#===============================================================================
#
#===============================================================================
class AnimationEditor::AnimationSelector
BORDER_THICKNESS = 4
LABEL_OFFSET_X = -4 # Position of label relative to what they're labelling
LABEL_OFFSET_Y = -32
QUIT_BUTTON_WIDTH = 80
QUIT_BUTTON_HEIGHT = 30
TYPE_BUTTONS_X = 2
TYPE_BUTTONS_Y = 62
TYPE_BUTTON_WIDTH = 100
TYPE_BUTTON_HEIGHT = 48
LIST_BORDER_PADDING = (UIControls::List::BORDER_THICKNESS * 2)
MOVES_LIST_X = TYPE_BUTTONS_X + TYPE_BUTTON_WIDTH + 2
MOVES_LIST_Y = TYPE_BUTTONS_Y + 2
MOVES_LIST_WIDTH = 200 + LIST_BORDER_PADDING
MOVES_LIST_HEIGHT = AnimationEditor::WINDOW_HEIGHT - MOVES_LIST_Y - LIST_BORDER_PADDING
MOVES_LIST_HEIGHT = (((MOVES_LIST_HEIGHT - LIST_BORDER_PADDING) / UIControls::List::ROW_HEIGHT) * UIControls::List::ROW_HEIGHT)
MOVES_LIST_HEIGHT += LIST_BORDER_PADDING
ANIMATIONS_LIST_X = MOVES_LIST_X + MOVES_LIST_WIDTH + 4
ANIMATIONS_LIST_Y = MOVES_LIST_Y
ANIMATIONS_LIST_WIDTH = 300 + LIST_BORDER_PADDING
ANIMATIONS_LIST_HEIGHT = MOVES_LIST_HEIGHT
ACTION_BUTTON_WIDTH = 200
ACTION_BUTTON_HEIGHT = 48
ACTION_BUTTON_X = ANIMATIONS_LIST_X + ANIMATIONS_LIST_WIDTH + 2
ACTION_BUTTON_Y = TYPE_BUTTONS_Y + ((ANIMATIONS_LIST_HEIGHT - (ACTION_BUTTON_HEIGHT * 3)) / 2) + 4
FILTER_BOX_WIDTH = ACTION_BUTTON_WIDTH
FILTER_BOX_HEIGHT = UIControls::TextBox::TEXT_BOX_HEIGHT
FILTER_BOX_X = ACTION_BUTTON_X
FILTER_BOX_Y = MOVES_LIST_Y
# Pop-up window
MESSAGE_BOX_WIDTH = AnimationEditor::WINDOW_WIDTH * 3 / 4
MESSAGE_BOX_HEIGHT = 160
MESSAGE_BOX_BUTTON_WIDTH = 150
MESSAGE_BOX_BUTTON_HEIGHT = 32
MESSAGE_BOX_SPACING = 16
include AnimationEditor::SettingsMixin
include UIControls::StyleMixin
def initialize
load_settings
@animation_type = 0 # 0=move, 1=common
@filter_text = ""
@quit = false
generate_full_lists
initialize_viewports
initialize_bitmaps
initialize_controls
self.color_scheme = @settings[:color_scheme]
refresh
end
def initialize_viewports
@viewport = Viewport.new(0, 0, AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT)
@viewport.z = 99999
@pop_up_viewport = Viewport.new(0, 0, AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT)
@pop_up_viewport.z = @viewport.z + 50
end
def initialize_bitmaps
# Background
@screen_bitmap = BitmapSprite.new(AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT, @viewport)
# Semi-transparent black overlay to dim the screen while a pop-up window is open
@pop_up_bg_bitmap = BitmapSprite.new(AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT, @pop_up_viewport)
@pop_up_bg_bitmap.z = -100
@pop_up_bg_bitmap.visible = false
# Draw in these bitmaps
draw_editor_background
end
def initialize_controls
@components = UIControls::ControlsContainer.new(0, 0, AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT)
# Quit button
btn = UIControls::Button.new(QUIT_BUTTON_WIDTH, QUIT_BUTTON_HEIGHT, @viewport, _INTL("Quit"))
btn.set_fixed_size
@components.add_control_at(:quit, btn, 0, 0)
# New button
btn = UIControls::Button.new(QUIT_BUTTON_WIDTH, QUIT_BUTTON_HEIGHT, @viewport, _INTL("New"))
btn.set_fixed_size
@components.add_control_at(:new, btn, QUIT_BUTTON_WIDTH, 0)
# Type label
label = UIControls::Label.new(TYPE_BUTTON_WIDTH, TYPE_BUTTON_HEIGHT, @viewport, _INTL("Anim types"))
label.header = true
@components.add_control_at(:type_label, label, TYPE_BUTTONS_X + LABEL_OFFSET_X + 4, TYPE_BUTTONS_Y + LABEL_OFFSET_Y + 4)
# Animation type toggle buttons
[[:moves, _INTL("Moves")], [:commons, _INTL("Common")]].each_with_index do |val, i|
btn = UIControls::Button.new(TYPE_BUTTON_WIDTH, TYPE_BUTTON_HEIGHT, @viewport, val[1])
btn.set_fixed_size
@components.add_control_at(val[0], btn, TYPE_BUTTONS_X, TYPE_BUTTONS_Y + (i * TYPE_BUTTON_HEIGHT))
end
# Moves list label
label = UIControls::Label.new(MOVES_LIST_WIDTH, TYPE_BUTTON_HEIGHT, @viewport, _INTL("Moves"))
label.header = true
@components.add_control_at(:moves_label, label, MOVES_LIST_X + LABEL_OFFSET_X, MOVES_LIST_Y + LABEL_OFFSET_Y)
# Moves list
list = UIControls::List.new(MOVES_LIST_WIDTH, MOVES_LIST_HEIGHT, @viewport, [])
@components.add_control_at(:moves_list, list, MOVES_LIST_X, MOVES_LIST_Y)
# Animations list label
label = UIControls::Label.new(ANIMATIONS_LIST_WIDTH, TYPE_BUTTON_HEIGHT, @viewport, _INTL("Animations"))
label.header = true
@components.add_control_at(:animations_label, label, ANIMATIONS_LIST_X + LABEL_OFFSET_X, ANIMATIONS_LIST_Y + LABEL_OFFSET_Y)
# Animations list
list = UIControls::List.new(ANIMATIONS_LIST_WIDTH, ANIMATIONS_LIST_HEIGHT, @viewport, [])
@components.add_control_at(:animations_list, list, ANIMATIONS_LIST_X, ANIMATIONS_LIST_Y)
# Edit, Copy and Delete buttons
[[:edit, _INTL("Edit animation")], [:copy, _INTL("Copy animation")], [:delete, _INTL("Delete animation")]].each_with_index do |val, i|
btn = UIControls::Button.new(ACTION_BUTTON_WIDTH, ACTION_BUTTON_HEIGHT, @viewport, val[1])
btn.set_fixed_size
@components.add_control_at(val[0], btn, ACTION_BUTTON_X, ACTION_BUTTON_Y + (i * ACTION_BUTTON_HEIGHT))
end
# Filter text box
text_box = UIControls::TextBox.new(FILTER_BOX_WIDTH, FILTER_BOX_HEIGHT, @viewport, "")
@components.add_control_at(:filter, text_box, FILTER_BOX_X, FILTER_BOX_Y)
# Filter text box label
label = UIControls::Label.new(FILTER_BOX_WIDTH, TYPE_BUTTON_HEIGHT, @viewport, _INTL("Filter text"))
label.header = true
@components.add_control_at(:filter_label, label, FILTER_BOX_X + LABEL_OFFSET_X, FILTER_BOX_Y + LABEL_OFFSET_Y)
end
def dispose
@screen_bitmap.dispose
@pop_up_bg_bitmap.dispose
@components.dispose
@viewport.dispose
@pop_up_viewport.dispose
end
#-----------------------------------------------------------------------------
def color_scheme=(value)
return if @color_scheme == value
@color_scheme = value
draw_editor_background
@components.color_scheme = value
refresh
end
#-----------------------------------------------------------------------------
def draw_editor_background
# Fill the whole screen with white
@screen_bitmap.bitmap.fill_rect(0, 0, AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT, background_color)
# Make the pop-up background semi-transparent
@pop_up_bg_bitmap.bitmap.fill_rect(0, 0, AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT, semi_transparent_color)
end
#-----------------------------------------------------------------------------
def create_pop_up_window(width, height)
ret = BitmapSprite.new(width + (BORDER_THICKNESS * 2),
height + (BORDER_THICKNESS * 2), @pop_up_viewport)
ret.x = (AnimationEditor::WINDOW_WIDTH - ret.width) / 2
ret.y = (AnimationEditor::WINDOW_HEIGHT - ret.height) / 2
ret.z = -1
ret.bitmap.font.color = text_color
ret.bitmap.font.size = text_size
# Draw pop-up box border
ret.bitmap.border_rect(BORDER_THICKNESS, BORDER_THICKNESS, width, height,
BORDER_THICKNESS, background_color, line_color)
# Fill pop-up box with white
ret.bitmap.fill_rect(BORDER_THICKNESS, BORDER_THICKNESS, width, height, background_color)
return ret
end
#-----------------------------------------------------------------------------
def message(text, *options)
@pop_up_bg_bitmap.visible = true
msg_bitmap = create_pop_up_window(MESSAGE_BOX_WIDTH, MESSAGE_BOX_HEIGHT)
# Draw text
text_size = msg_bitmap.bitmap.text_size(text)
msg_bitmap.bitmap.draw_text(0, (msg_bitmap.height / 2) - MESSAGE_BOX_BUTTON_HEIGHT,
msg_bitmap.width, text_size.height, text, 1)
# Create buttons
buttons = []
options.each_with_index do |option, i|
btn = UIControls::Button.new(MESSAGE_BOX_BUTTON_WIDTH, MESSAGE_BOX_BUTTON_HEIGHT, @pop_up_viewport, option[1])
btn.x = msg_bitmap.x + (msg_bitmap.width - (MESSAGE_BOX_BUTTON_WIDTH * options.length)) / 2
btn.x += MESSAGE_BOX_BUTTON_WIDTH * i
btn.y = msg_bitmap.y + msg_bitmap.height - MESSAGE_BOX_BUTTON_HEIGHT - MESSAGE_BOX_SPACING
btn.set_fixed_size
btn.color_scheme = @color_scheme
btn.set_interactive_rects
buttons.push([option[0], btn])
end
# Interaction loop
ret = nil
captured = nil
loop do
Graphics.update
Input.update
if captured
captured.update
captured = nil if !captured.busy?
else
buttons.each do |btn|
btn[1].update
captured = btn[1] if btn[1].busy?
end
end
buttons.each do |btn|
next if !btn[1].changed?
ret = btn[0]
break
end
ret = :cancel if Input.triggerex?(:ESCAPE)
break if ret
buttons.each { |btn| btn[1].repaint }
end
# Dispose and return
buttons.each { |btn| btn[1].dispose }
buttons.clear
msg_bitmap.dispose
@pop_up_bg_bitmap.visible = false
return ret
end
def confirm_message(text)
return message(text, [:yes, _INTL("Yes")], [:no, _INTL("No")]) == :yes
end
#-----------------------------------------------------------------------------
def generate_full_lists
@full_move_animations = {}
@full_common_animations = {}
GameData::Animation.keys.each do |id|
anim = GameData::Animation.get(id)
name = ""
name += "\\c[2]" if anim.ignore
name += _INTL("[Foe]") + " " if anim.opposing_animation?
name += "[#{anim.version}]" + " " if anim.version > 0
name += (anim.name || anim.move)
if anim.move_animation?
move_name = GameData::Move.try_get(anim.move)&.name || anim.move
@full_move_animations[anim.move] ||= []
@full_move_animations[anim.move].push([id, name, move_name])
elsif anim.common_animation?
@full_common_animations[anim.move] ||= []
@full_common_animations[anim.move].push([id, name])
end
end
@full_move_animations.values.each do |val|
val.sort! { |a, b| a[1] <=> b[1] }
end
@full_common_animations.values.each do |val|
val.sort! { |a, b| a[1] <=> b[1] }
end
apply_list_filter
end
def apply_list_filter
# Apply filter
if @filter_text == ""
@move_animations = @full_move_animations.clone
@common_animations = @full_common_animations.clone
else
filter = @filter_text.downcase
@move_animations.clear
@full_move_animations.each_pair do |move, anims|
anims.each do |anim|
next if !anim[1].downcase.include?(filter) && !anim[2].downcase.include?(filter)
@move_animations[move] ||= []
@move_animations[move].push(anim)
end
end
@common_animations.clear
@full_common_animations.each_pair do |common, anims|
anims.each do |anim|
next if !anim[1].downcase.include?(filter) && !common.downcase.include?(filter)
@common_animations[common] ||= []
@common_animations[common].push(anim)
end
end
end
# Create move list from the filtered results
@move_list = []
@move_animations.each_pair do |move_id, anims|
@move_list.push([move_id, anims[0][2]])
end
@move_list.uniq!
@move_list.sort!
# Create common list from the filtered results
@common_list = []
@common_animations.each_pair do |move_id, anims|
@common_list.push([move_id, move_id])
end
@common_list.uniq!
@common_list.sort!
end
def selected_move_animations
val = @components.get_control(:moves_list).value
return [] if !val
return @move_animations[val] if @animation_type == 0
return @common_animations[val] if @animation_type == 1
return []
end
def selected_animation_id
return @components.get_control(:animations_list).value
end
#-----------------------------------------------------------------------------
def refresh
# Put the correct list into the moves list
case @animation_type
when 0
@components.get_control(:moves).set_highlighted
@components.get_control(:commons).set_not_highlighted
@components.get_control(:moves_list).values = @move_list
@components.get_control(:moves_label).text = _INTL("Moves")
when 1
@components.get_control(:moves).set_not_highlighted
@components.get_control(:commons).set_highlighted
@components.get_control(:moves_list).values = @common_list
@components.get_control(:moves_label).text = _INTL("Common animations")
end
# Put the correct list into the animations list
@components.get_control(:animations_list).values = selected_move_animations
# Enable/disable buttons depending on what is selected
if @components.get_control(:animations_list).value
@components.get_control(:edit).enable
@components.get_control(:copy).enable
@components.get_control(:delete).enable
else
@components.get_control(:edit).disable
@components.get_control(:copy).disable
@components.get_control(:delete).disable
end
end
#-----------------------------------------------------------------------------
def apply_button_press(button)
case button
when :quit
@quit = true
return # Don't need to refresh the screen
when :new
new_anim = GameData::Animation.new_hash(@animation_type, @components.get_control(:moves_list).value)
new_id = GameData::Animation.keys.max + 1
screen = AnimationEditor.new(new_id, new_anim)
screen.run
generate_full_lists
when :moves
@animation_type = 0
@components.get_control(:moves_list).selected = -1
@components.get_control(:animations_list).selected = -1
when :commons
@animation_type = 1
@components.get_control(:moves_list).selected = -1
@components.get_control(:animations_list).selected = -1
when :edit
anim_id = selected_animation_id
if anim_id
screen = AnimationEditor.new(anim_id, GameData::Animation.get(anim_id).clone_as_hash)
screen.run
load_settings
self.color_scheme = @settings[:color_scheme]
generate_full_lists
end
when :copy
anim_id = selected_animation_id
if anim_id
new_anim = GameData::Animation.get(anim_id).clone_as_hash
new_anim[:name] += " " + _INTL("(copy)") if !nil_or_empty?(new_anim[:name])
new_id = GameData::Animation.keys.max + 1
screen = AnimationEditor.new(new_id, new_anim)
screen.run
generate_full_lists
end
when :delete
anim_id = selected_animation_id
if anim_id && confirm_message(_INTL("Are you sure you want to delete this animation?"))
pbs_path = GameData::Animation.get(anim_id).pbs_path
GameData::Animation::DATA.delete(anim_id)
if GameData::Animation::DATA.any? { |_key, anim| anim.pbs_path == pbs_path }
Compiler.write_battle_animation_file(pbs_path)
elsif FileTest.exist?("PBS/Animations/" + pbs_path + ".txt")
File.delete("PBS/Animations/" + pbs_path + ".txt")
end
generate_full_lists
end
end
refresh
end
def update
@components.update
if @components.changed?
@components.values.each_pair do |property, value|
apply_button_press(property)
end
@components.clear_changed
end
# Detect change to filter text
filter_ctrl = @components.get_control(:filter)
if filter_ctrl.value != @filter_text
@filter_text = filter_ctrl.value
apply_list_filter
refresh
end
end
def run
Input.text_input = false
loop do
Graphics.update
Input.update
update
break if !@components.busy? && @quit
end
dispose
end
end
#===============================================================================
# Add to Debug menu.
#===============================================================================
MenuHandlers.add(:debug_menu, :use_pc, {
"name" => _INTL("New Animation Editor"),
"parent" => :main,
"description" => _INTL("Open the new animation editor."),
"effect" => proc {
Graphics.resize_screen(AnimationEditor::WINDOW_WIDTH, AnimationEditor::WINDOW_HEIGHT)
pbSetResizeFactor(1)
screen = AnimationEditor::AnimationSelector.new
screen.run
Graphics.resize_screen(Settings::SCREEN_WIDTH, Settings::SCREEN_HEIGHT)
pbSetResizeFactor($PokemonSystem.screensize)
$game_map&.autoplay
}
})

View File

@@ -0,0 +1,574 @@
#===============================================================================
#
#===============================================================================
module AnimationEditor::ParticleDataHelper
module_function
def get_keyframe_particle_value(particle, property, frame)
if !GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES.include?(property)
raise _INTL("Couldn't get default value for property {1} for particle {2}.",
property, particle[:name])
end
ret = [GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES[property], false]
if particle[property]
# NOTE: The commands are already in keyframe order, so we can just run
# through them in order, applying their changes until we reach
# frame.
particle[property].each do |cmd|
break if cmd[0] > frame # Command is in the future; no more is needed
break if cmd[0] == frame && cmd[1] > 0 # Start of a "MoveXYZ" command; won't have changed yet
if cmd[0] + cmd[1] <= frame # Command has finished; use its end value
ret[0] = cmd[2]
next
end
# In a "MoveXYZ" command; need to interpolate
ret[0] = AnimationPlayer::Helper.interpolate(
(cmd[3] || :linear), ret[0], cmd[2], cmd[1], cmd[0], frame
)
ret[1] = true # Interpolating
break
end
end
# NOTE: Particles are assumed to be not visible at the start of the
# animation, and automatically become visible when the particle has
# its first command. This does not apply to the "User" and "Target"
# particles, which start the animation visible.
if property == :visible
first_cmd = (["User", "Target"].include?(particle[:name])) ? 0 : -1
first_visible_cmd = -1
particle.each_pair do |prop, value|
next if !value.is_a?(Array) || value.empty?
first_cmd = value[0][0] if first_cmd < 0 || first_cmd > value[0][0]
first_visible_cmd = value[0][0] if prop == :visible && (first_visible_cmd < 0 || first_visible_cmd > value[0][0])
end
ret[0] = true if first_cmd >= 0 && first_cmd <= frame &&
(first_visible_cmd < 0 || frame < first_visible_cmd)
end
return ret
end
def get_all_keyframe_particle_values(particle, frame)
ret = {}
GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES.each_pair do |prop, default|
ret[prop] = get_keyframe_particle_value(particle, prop, frame)
end
return ret
end
def get_all_particle_values(particle)
ret = {}
GameData::Animation::PARTICLE_DEFAULT_VALUES.each_pair do |prop, default|
ret[prop] = particle[prop] || default
end
return ret
end
# Used to determine which keyframes the particle is visible in, which is
# indicated in the timeline by a coloured bar. 0=not visible, 1=visible,
# 2=visible because of spawner delay.
# NOTE: Particles are assumed to be not visible at the start of the
# animation, and automatically become visible when the particle has
# its first command. This does not apply to the "User" and "Target"
# particles, which start the animation visible. They do NOT become
# invisible automatically after their last command.
def get_timeline_particle_visibilities(particle, duration)
if !GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES.include?(:visible)
raise _INTL("Couldn't get default value for property {1} for particle {2}.",
property, particle[:name])
end
value = GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES[:visible] ? 1 : 0
value = 1 if ["User", "Target", "SE"].include?(particle[:name])
ret = []
if !["User", "Target", "SE"].include?(particle[:name])
earliest = duration
particle.each_pair do |prop, value|
next if !value.is_a?(Array) || value.empty?
earliest = value[0][0] if earliest > value[0][0]
end
ret[earliest] = 1
end
if particle[:visible]
particle[:visible].each { |cmd| ret[cmd[0]] = (cmd[2]) ? 1 : 0 }
end
duration.times do |i|
value = ret[i] if !ret[i].nil?
ret[i] = value
end
qty = particle[:spawn_quantity] || 1 if particle[:spawner] && particle[:spawner] != :none
if (particle[:spawner] || :none) != :none
qty = particle[:spawn_quantity] || 1
delay = AnimationPlayer::Helper.get_particle_delay(particle, qty - 1)
if delay > 0
count = -1
duration.times do |i|
if ret[i] == 1 # Visible
count = 0
elsif ret[i] == 0 && count >= 0 && count < delay # Not visible and within delay
ret[i] = 2
count += 1
end
end
end
end
return ret
end
#-----------------------------------------------------------------------------
# Returns an array indicating where command diamonds and duration lines should
# be drawn in the AnimationEditor::ParticleList.
def get_particle_commands_timeline(particle)
ret = []
durations = []
particle.each_pair do |property, values|
next if !values.is_a?(Array) || values.empty?
values.each do |cmd|
ret[cmd[0]] = true
if cmd[1] > 0
ret[cmd[0] + cmd[1]] = true
durations.push([cmd[0], cmd[1]])
end
end
end
return ret, durations
end
# Returns an array, whose indexes are keyframes, where the values in the array
# are commands. A keyframe's value can be one of these:
# 0 - SetXYZ
# [+/- duration, interpolation type] --- MoveXYZ (duration's sign is whether
# it makes the value higher or lower)
def get_particle_property_commands_timeline(particle, property, commands)
return nil if !commands || commands.empty?
if particle[:name] == "SE"
ret = []
commands.each { |cmd| ret[cmd[0]] = 0 }
return ret
end
if !GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES.include?(property)
raise _INTL("No default value for property {1} in PARTICLE_KEYFRAME_DEFAULT_VALUES.", property)
end
ret = []
val = GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES[property]
commands.each do |cmd|
if cmd[1] > 0 # MoveXYZ
dur = cmd[1]
dur *= -1 if cmd[2] < val
ret[cmd[0]] = [dur, cmd[3] || :linear]
ret[cmd[0] + cmd[1]] = 0
else # SetXYZ
ret[cmd[0]] = 0
end
val = cmd[2] # New actual value
end
return ret
end
#-----------------------------------------------------------------------------
def set_property(particle, property, value)
particle[property] = value
end
def has_command_at?(particle, property, frame)
return particle[property]&.any? { |cmd| (cmd[0] == frame) || (cmd[0] + cmd[1] == frame) }
end
def has_se_command_at?(particles, frame)
ret = false
se_particle = particles.select { |particle| particle[:name] == "SE" }[0]
if se_particle
se_particle.each_pair do |property, values|
next if !values.is_a?(Array) || values.empty?
ret = values.any? { |value| value[0] == frame }
break if ret
end
end
return ret
end
def add_command(particle, property, frame, value)
# Return a new set of commands if there isn't one
if !particle[property] || particle[property].empty?
return [[frame, 0, value]]
end
# Find all relevant commands
set_now = nil
move_ending_now = nil
move_overlapping_now = nil
particle[property].each do |cmd|
if cmd[1] == 0
set_now = cmd if cmd[0] == frame
else
move_ending_now = cmd if cmd[0] + cmd[1] == frame
move_overlapping_now = cmd if cmd[0] < frame && cmd[0] + cmd[1] > frame
end
end
new_command_needed = true
# Replace existing command at frame if it has a duration of 0
if set_now
set_now[2] = value
new_command_needed = false
end
# If a command has a duration >0 and ends at frame, replace its value
if move_ending_now
move_ending_now[2] = value
new_command_needed = false
end
return particle[property] if !new_command_needed
# Add a new command
new_cmd = [frame, 0, value]
particle[property].push(new_cmd)
# If the new command interrupts an interpolation, split that interpolation
if move_overlapping_now
end_frame = move_overlapping_now[0] + move_overlapping_now[1]
new_cmd[1] = end_frame - frame # Duration
new_cmd[2] = move_overlapping_now[2] # Value
new_cmd[3] = move_overlapping_now[3] # Interpolation type
move_overlapping_now[1] = frame - move_overlapping_now[0] # Duration
move_overlapping_now[2] = value # Value
end
# Sort and return the commands
particle[property].sort! { |a, b| a[0] == b[0] ? a[1] == b[1] ? 0 : a[1] <=> b[1] : a[0] <=> b[0] }
return particle[property]
end
# Cases:
# * SetXYZ - delete it
# * MoveXYZ start - turn into a SetXYZ at the end point
# * MoveXYZ end - delete it (this may happen to remove the start diamond too)
# * MoveXYZ end and start - merge both together (use first's type)
# * SetXYZ and MoveXYZ start - delete SetXYZ (leave MoveXYZ alone)
# * SetXYZ and MoveXYZ end - (unlikely) delete both
# * SetXYZ and MoveXYZ start and end - (unlikely) delete SetXYZ, merge Moves together
def delete_command(particle, property, frame, full_delete = false)
# Find all relevant commands
set_now = nil
move_ending_now = nil
move_starting_now = nil
set_at_end_of_move_starting_now = nil
particle[property].each do |cmd|
if cmd[1] == 0
set_now = cmd if cmd[0] == frame
else
move_starting_now = cmd if cmd[0] == frame
move_ending_now = cmd if cmd[0] + cmd[1] == frame
end
end
if move_starting_now
particle[property].each do |cmd|
set_at_end_of_move_starting_now = cmd if cmd[1] == 0 && cmd[0] == move_starting_now[0] + move_starting_now[1]
end
end
# Delete SetXYZ if it is at frame
particle[property].delete(set_now) if set_now
# Edit/delete MoveXYZ commands starting/ending at frame
if move_ending_now && move_starting_now # Merge both MoveXYZ commands
move_ending_now[1] += move_starting_now[1] # Duration
move_ending_now[2] = move_starting_now[2] # Value
particle[property].delete(move_starting_now)
elsif move_ending_now # Delete MoveXYZ ending now
particle[property].delete(move_ending_now)
elsif move_starting_now && (full_delete || !set_now) # Turn into SetXYZ at its end point
if set_at_end_of_move_starting_now
particle[property].delete(move_starting_now)
else
move_starting_now[0] += move_starting_now[1]
move_starting_now[1] = 0
move_starting_now[3] = nil
move_starting_now.compact!
end
end
return (particle[property].empty?) ? nil : particle[property]
end
def optimize_all_particles(particles)
particles.each do |particle|
next if particle[:name] == "SE"
particle.each_pair do |key, cmds|
next if !cmds.is_a?(Array) || cmds.empty?
particle[key] = optimize_commands(particle, key)
end
end
end
# Removes commands for the particle's given property if they don't make a
# difference. Returns the resulting set of commands.
def optimize_commands(particle, property)
# Split particle[property] into values and interpolation arrays
set_points = [] # All SetXYZ commands (the values thereof)
end_points = [] # End points of MoveXYZ commands (the values thereof)
interps = [] # Interpolation type from a keyframe to the next point
if particle && particle[property]
particle[property].each do |cmd|
if cmd[1] == 0 # SetXYZ
set_points[cmd[0]] = cmd[2]
else
interps[cmd[0]] = cmd[3] || :linear
end_points[cmd[0] + cmd[1]] = cmd[2]
end
end
end
# For visibility only, set the keyframe with the first command (of any kind)
# to be visible, unless the command being added overwrites it. Also figure
# out the first keyframe that has a command, and the first keyframe that has
# a non-visibility command (used below).
if property == :visible
first_cmd = (["User", "Target", "SE"].include?(particle[:name])) ? 0 : -1
first_non_visible_cmd = -1
particle.each_pair do |prop, value|
next if !value.is_a?(Array) || value.empty?
first_cmd = value[0][0] if first_cmd < 0 || first_cmd > value[0][0]
next if prop == :visible
first_non_visible_cmd = value[0][0] if first_non_visible_cmd < 0 || first_non_visible_cmd > value[0][0]
end
set_points[first_cmd] = true if first_cmd >= 0 && set_points[first_cmd].nil?
end
# Convert points and interps back into particle[property]
ret = []
if !GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES.include?(property)
raise _INTL("Couldn't get default value for property {1}.", property)
end
val = GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES[property]
length = [set_points.length, end_points.length].max
length.times do |i|
if !set_points[i].nil?
if property == :visible && first_cmd >= 0 && i == first_cmd &&
first_non_visible_cmd >= 0 && i == first_non_visible_cmd
ret.push([i, 0, set_points[i]]) if !set_points[i]
elsif set_points[i] != val
ret.push([i, 0, set_points[i]])
end
val = set_points[i]
end
if interps[i] && interps[i] != :none
((i + 1)..length).each do |j|
next if set_points[j].nil? && end_points[j].nil?
if set_points[j].nil?
break if end_points[j] == val
ret.push([i, j - i, end_points[j], interps[i]])
val = end_points[j]
end_points[j] = nil
else
break if set_points[j] == val
ret.push([i, j - i, set_points[j], interps[i]])
val = set_points[j]
set_points[j] = nil
end
break
end
end
end
return (ret.empty?) ? nil : ret
end
# SetXYZ at frame
# - none: Do nothing.
# - interp: Add MoveXYZ (calc duration/value at end).
# MoveXYZ at frame
# - none: Turn into two SetXYZ (MoveXYZ's value for end point, calc value
# for start point).
# - interp: Change type.
# SetXYZ and MoveXYZ at frame
# - none: Turn MoveXYZ into SetXYZ at the end point.
# - interp: Change MoveXYZ's type.
# End of earlier MoveXYZ (or nothing) at frame
# - none: Do nothing.
# - interp: Add MoveXYZ (calc duration/value at end).
def set_interpolation(particle, property, frame, type)
# Find relevant command
set_now = nil
move_starting_now = nil
particle[property].each do |cmd|
next if cmd[0] != frame
set_now = cmd if cmd[1] == 0
move_starting_now = cmd if cmd[1] != 0
end
if move_starting_now
# If a MoveXYZ command exists at frame, amend it
if type == :none
old_end_point = move_starting_now[0] + move_starting_now[1]
old_value = move_starting_now[2]
# Turn the MoveXYZ command into a SetXYZ (or just delete it if a SetXYZ
# already exists at frame)
if set_now
particle[property].delete(move_starting_now)
else
move_starting_now[1] = 0
move_starting_now[2] = get_keyframe_particle_value(particle, property, frame)[0]
move_starting_now[3] = nil
move_starting_now.compact!
end
# Add a new SetXYZ at the end of the (former) interpolation
add_command(particle, property, old_end_point, old_value)
else
# Simply change the type
move_starting_now[3] = type
end
elsif type != :none
# If no MoveXYZ command exists at frame, make one (if type isn't :none)
particle[property].each do |cmd| # Assumes commands are sorted by keyframe
next if cmd[0] <= frame
val_at_end = get_keyframe_particle_value(particle, property, cmd[0])[0]
particle[property].push([frame, cmd[0] - frame, val_at_end, type])
particle[property].sort! { |a, b| a[0] == b[0] ? a[1] == b[1] ? 0 : a[1] <=> b[1] : a[0] <=> b[0] }
break
end
end
return particle[property]
end
#-----------------------------------------------------------------------------
def get_se_display_text(property, value)
ret = ""
case property
when :user_cry
ret += _INTL("[[User's cry]]")
when :target_cry
ret += _INTL("[[Target's cry]]")
when :se
ret += value[2]
else
raise _INTL("Unhandled property {1} for SE particle found.", property)
end
volume = (property == :se) ? value[3] : value[2]
ret += " " + _INTL("(volume: {1})", volume) if volume && volume != 100
pitch = (property == :se) ? value[4] : value[3]
ret += " " + _INTL("(pitch: {1})", pitch) if pitch && pitch != 100
return ret
end
# Returns the volume and pitch of the SE to be played at the given frame
# of the given filename.
def get_se_values_from_filename_and_frame(particle, frame, filename)
return nil if !filename
case filename
when "USER", "TARGET"
property = (filename == "USER") ? :user_cry : :target_cry
slot = particle[property].select { |s| s[0] == frame }[0]
return nil if !slot
return slot[2] || 100, slot[3] || 100
else
slot = particle[:se].select { |s| s[0] == frame && s[2] == filename }[0]
return nil if !slot
return slot[3] || 100, slot[4] || 100
end
return nil
end
# Deletes an existing command that plays the same filename at the same frame,
# and adds the new one.
def add_se_command(particle, frame, filename, volume, pitch)
delete_se_command(particle, frame, filename)
case filename
when "USER", "TARGET"
property = (filename == "USER") ? :user_cry : :target_cry
particle[property] ||= []
particle[property].push([frame, 0, (volume == 100) ? nil : volume, (pitch == 100) ? nil : pitch])
particle[property].sort! { |a, b| a[0] <=> b[0] }
else
particle[:se] ||= []
particle[:se].push([frame, 0, filename, (volume == 100) ? nil : volume, (pitch == 100) ? nil : pitch])
particle[:se].sort! { |a, b| a[0] <=> b[0] }
particle[:se].sort! { |a, b| (a[0] == b[0]) ? a[2].downcase <=> b[2].downcase : a[0] <=> b[0] }
end
end
# Deletes an existing SE-playing command at the given frame of the given
# filename.
def delete_se_command(particle, frame, filename)
case filename
when "USER", "TARGET"
property = (filename == "USER") ? :user_cry : :target_cry
return if !particle[property] || particle[property].empty?
particle[property].delete_if { |s| s[0] == frame }
particle.delete(property) if particle[property].empty?
else
return if !particle[:se] || particle[:se].empty?
particle[:se].delete_if { |s| s[0] == frame && s[2] == filename }
particle.delete(:se) if particle[:se].empty?
end
end
#-----------------------------------------------------------------------------
# Inserts an empty frame at the given frame. Delays all commands at or after
# the given frame by 1, and increases the duration of all commands that
# overlap the given frame.
def insert_frame(particle, frame)
particle.each_pair do |property, values|
next if !values.is_a?(Array) || values.empty?
values.each do |cmd|
if cmd[0] >= frame
cmd[0] += 1
elsif cmd[0] < frame && cmd[0] + cmd[1] > frame
cmd[1] += 1
end
end
end
end
# Removes a frame at the given frame. Deletes all commands in that frame, then
# brings all commands after the given frame earlier by 1, and reduces the
# duration of all commands that overlap the given frame.
def remove_frame(particle, frame)
particle.keys.each do |property|
next if !particle[property].is_a?(Array) || particle[property].empty?
delete_command(particle, property, frame, true)
end
particle.delete_if { |property, values| values.is_a?(Array) && values.empty? }
particle.each_pair do |key, values|
next if !values.is_a?(Array) || values.empty?
values.each do |cmd|
if cmd[0] > frame
cmd[0] -= 1
elsif cmd[0] < frame && cmd[0] + cmd[1] > frame
cmd[1] -= 1
end
end
end
end
#-----------------------------------------------------------------------------
# Creates a new particle and inserts it at index. If there is a particle above
# the new one, the new particle will inherit its focus; otherwise it gets a
# default focus of :foreground.
def add_particle(particles, index)
new_particle = GameData::Animation::PARTICLE_DEFAULT_VALUES.clone
new_particle[:name] = _INTL("New particle")
if index > 0 && index <= particles.length && particles[index - 1][:name] != "SE"
new_particle[:focus] = particles[index - 1][:focus]
end
index = particles.length if index < 0
particles.insert(index, new_particle)
end
# Copies the particle at index and inserts the copy immediately after that
# index. This assumes the original particle can be copied, i.e. isn't "SE".
def duplicate_particle(particles, index)
new_particle = {}
particles[index].each_pair do |key, value|
if value.is_a?(Array)
new_particle[key] = []
value.each { |cmd| new_particle[key].push(cmd.clone) }
else
new_particle[key] = value.clone
end
end
new_particle[:name] += " (copy)"
particles.insert(index + 1, new_particle)
end
def swap_particles(particles, index1, index2)
particles[index1], particles[index2] = particles[index2], particles[index1]
end
# Deletes the particle at the given index. This assumes the particle can be
# deleted, i.e. isn't "User"/"Target"/"SE".
def delete_particle(particles, index)
particles[index] = nil
particles.compact!
end
end

View File

@@ -0,0 +1,779 @@
#===============================================================================
# NOTE: z values:
# -200 = backdrop.
# -199 = side bases
# -198 = battler shadows.
# 0 +/-50 = background focus, foe side background.
# 500, 400, 300... = foe trainers.
# 900, 800, 700... +/-50 = foe battlers.
# 1000 +/-50 = foe side foreground, player side background.
# 1100, 1200, 1300... +/-50 = player battlers.
# 1500, 1600, 1700... = player trainers.
# 2000 +/-50 = player side foreground, foreground focus.
# 9999+ = UI
#===============================================================================
class AnimationEditor::Canvas < Sprite
attr_reader :sprites # Only used while playing the animation
attr_reader :values
FRAME_SIZE = 48
PARTICLE_FRAME_COLOR = Color.new(0, 0, 0, 64)
include UIControls::StyleMixin
def initialize(viewport, anim, settings)
super(viewport)
@anim = anim
@settings = settings
@keyframe = 0
@display_keyframe = 0
@selected_particle = -2
@captured = nil
@user_coords = []
@target_coords = []
initialize_background
initialize_battlers
initialize_particle_sprites
initialize_particle_frames
refresh
end
def initialize_background
self.z = -200
# NOTE: The background graphic is self.bitmap.
player_base_pos = Battle::Scene.pbBattlerPosition(0)
@player_base = IconSprite.new(*player_base_pos, viewport)
@player_base.z = -199
foe_base_pos = Battle::Scene.pbBattlerPosition(1)
@foe_base = IconSprite.new(*foe_base_pos, viewport)
@foe_base.z = -199
@message_bar_sprite = Sprite.new(viewport)
@message_bar_sprite.z = 9999
end
def initialize_battlers
@battler_sprites = []
end
def initialize_particle_sprites
@particle_sprites = []
end
def initialize_particle_frames
# Frame for selected particle
@sel_frame_bitmap = Bitmap.new(FRAME_SIZE, FRAME_SIZE)
@sel_frame_bitmap.outline_rect(0, 0, @sel_frame_bitmap.width, @sel_frame_bitmap.height, PARTICLE_FRAME_COLOR)
@sel_frame_bitmap.outline_rect(2, 2, @sel_frame_bitmap.width - 4, @sel_frame_bitmap.height - 4, PARTICLE_FRAME_COLOR)
@sel_frame_sprite = Sprite.new(viewport)
@sel_frame_sprite.bitmap = @sel_frame_bitmap
@sel_frame_sprite.z = 99999
@sel_frame_sprite.ox = @sel_frame_bitmap.width / 2
@sel_frame_sprite.oy = @sel_frame_bitmap.height / 2
# Frame for other particles
@frame_bitmap = Bitmap.new(FRAME_SIZE, FRAME_SIZE)
@frame_bitmap.outline_rect(1, 1, @frame_bitmap.width - 2, @frame_bitmap.height - 2, PARTICLE_FRAME_COLOR)
@battler_frame_sprites = []
@frame_sprites = []
end
def dispose
@user_bitmap_front&.dispose
@user_bitmap_back&.dispose
@target_bitmap_front&.dispose
@target_bitmap_back&.dispose
@sel_frame_bitmap&.dispose
@frame_bitmap&.dispose
@player_base.dispose
@foe_base.dispose
@message_bar_sprite.dispose
@battler_sprites.each { |s| s.dispose if s && !s.disposed? }
@battler_sprites.clear
@particle_sprites.each do |s|
if s.is_a?(Array)
s.each { |s2| s2.dispose if s2 && !s2.disposed? }
else
s.dispose if s && !s.disposed?
end
end
@particle_sprites.clear
@battler_frame_sprites.each { |s| s.dispose if s && !s.disposed? }
@battler_frame_sprites.clear
@frame_sprites.each do |s|
if s.is_a?(Array)
s.each { |s2| s2.dispose if s2 && !s2.disposed? }
else
s.dispose if s && !s.disposed?
end
end
@frame_sprites.clear
@sel_frame_sprite&.dispose
super
end
#-----------------------------------------------------------------------------
# Returns whether the user is on the foe's (non-player's) side.
def sides_swapped?
return @settings[:user_opposes] || [:opp_move, :opp_common].include?(@anim[:type])
end
# index is a battler index (even for player's side, odd for foe's side)
def side_size(index)
side = index % 2
side = (side + 1) % 2 if sides_swapped?
return @settings[:side_sizes][side]
end
def user_index
ret = @settings[:user_index]
ret += 1 if sides_swapped?
return ret
end
def target_indices
ret = @settings[:target_indices].clone
if sides_swapped?
ret.length.times do |i|
ret[i] += (ret[i].even?) ? 1 : -1
end
end
return ret
end
def first_target_index
return target_indices.compact[0]
end
def position_empty?(index)
return false if !@anim[:no_user] && user_index == index
return false if !@anim[:no_target] && target_indices.include?(index)
return true
end
def show_particle_sprite?(index)
return false if index < 0 || index >= @anim[:particles].length
particle = @anim[:particles][index]
return false if !particle || particle[:name] == "SE"
return false if particle[:spawner] && particle[:spawner] != :none
return true
end
def color_scheme=(value)
return if @color_scheme == value
@color_scheme = value
self.bitmap.font.color = text_color
self.bitmap.font.size = text_size
refresh
end
def selected_particle=(val)
return if @selected_particle == val
@selected_particle = val
refresh_particle_frame
end
def keyframe=(val)
return if @keyframe == val
@keyframe = val
return if val < 0
@display_keyframe = val
refresh
end
def mouse_pos
mouse_coords = Mouse.getMousePos
return nil, nil if !mouse_coords
ret_x = mouse_coords[0] - self.viewport.rect.x - self.x
ret_y = mouse_coords[1] - self.viewport.rect.y - self.y
return nil, nil if ret_x < 0 || ret_x >= self.viewport.rect.width ||
ret_y < 0 || ret_y >= self.viewport.rect.height
return ret_x, ret_y
end
def mouse_in_sprite?(sprite, mouse_x, mouse_y)
return false if mouse_x < sprite.x - sprite.ox
return false if mouse_x >= sprite.x - sprite.ox + sprite.width
return false if mouse_y < sprite.y - sprite.oy
return false if mouse_y >= sprite.y - sprite.oy + sprite.height
return true
end
#-----------------------------------------------------------------------------
def busy?
return !@captured.nil?
end
def changed?
return !@values.nil?
end
def clear_changed
@values = nil
end
#-----------------------------------------------------------------------------
def prepare_to_play_animation
@sprites = {}
# Populate @sprites with sprites that are present during battle, and reset
# their x/y/z values so the animation player knows where they start
idx = user_index
particle_idx = @anim[:particles].index { |particle| particle[:name] == "User" }
if particle_idx
@sprites["pokemon_#{idx}"] = @battler_sprites[idx]
@battler_sprites[idx].x = @user_coords[0]
@battler_sprites[idx].y = @user_coords[1]
offset_xy = AnimationPlayer::Helper.get_xy_offset(@anim[:particles][particle_idx], @battler_sprites[idx])
focus_z = AnimationPlayer::Helper.get_z_focus(@anim[:particles][particle_idx], idx, idx)
@battler_sprites[idx].x += offset_xy[0]
@battler_sprites[idx].y += offset_xy[1]
AnimationPlayer::Helper.apply_z_focus_to_sprite(@battler_sprites[idx], 0, focus_z)
end
particle_idx = @anim[:particles].index { |particle| particle[:name] == "Target" }
if particle_idx
target_indices.each do |idx|
@sprites["pokemon_#{idx}"] = @battler_sprites[idx]
@battler_sprites[idx].x = @target_coords[idx][0]
@battler_sprites[idx].y = @target_coords[idx][1]
if particle_idx
offset_xy = AnimationPlayer::Helper.get_xy_offset(@anim[:particles][particle_idx], @battler_sprites[idx])
focus_z = AnimationPlayer::Helper.get_z_focus(@anim[:particles][particle_idx], idx, idx)
else
offset_xy = [0, @battler_sprites[idx].bitmap.height / 2]
focus_z = 1000 + ((100 * ((idx / 2) + 1)) * (idx.even? ? 1 : -1))
end
@battler_sprites[idx].x += offset_xy[0]
@battler_sprites[idx].y += offset_xy[1]
AnimationPlayer::Helper.apply_z_focus_to_sprite(@battler_sprites[idx], 0, focus_z)
end
end
hide_all_sprites
@sel_frame_sprite.visible = false
@playing = true
end
def end_playing_animation
@sprites.clear
@sprites = nil
@playing = false
refresh
end
#-----------------------------------------------------------------------------
def refresh_bg_graphics
return if @bg_name && @bg_name == @settings[:canvas_bg]
@bg_name = @settings[:canvas_bg]
core_name = @bg_name.sub(/_eve$/, "").sub(/_night$/, "")
if pbResolveBitmap("Graphics/Battlebacks/" + @bg_name + "_bg")
self.bitmap = RPG::Cache.load_bitmap("Graphics/Battlebacks/", @bg_name + "_bg")
else
self.bitmap = RPG::Cache.load_bitmap("Graphics/Battlebacks/", core_name + "_bg")
end
if pbResolveBitmap("Graphics/Battlebacks/" + @bg_name + "_base0")
@player_base.setBitmap("Graphics/Battlebacks/" + @bg_name + "_base0")
else
@player_base.setBitmap("Graphics/Battlebacks/" + core_name + "_base0")
end
@player_base.ox = @player_base.bitmap.width / 2
@player_base.oy = @player_base.bitmap.height
if pbResolveBitmap("Graphics/Battlebacks/" + @bg_name + "_base1")
@foe_base.setBitmap("Graphics/Battlebacks/" + @bg_name + "_base1")
else
@foe_base.setBitmap("Graphics/Battlebacks/" + core_name + "_base1")
end
@foe_base.ox = @foe_base.bitmap.width / 2
@foe_base.oy = @foe_base.bitmap.height / 2
if pbResolveBitmap("Graphics/Battlebacks/" + @bg_name + "_message")
@message_bar_sprite.bitmap = RPG::Cache.load_bitmap("Graphics/Battlebacks/", @bg_name + "_message")
else
@message_bar_sprite.bitmap = RPG::Cache.load_bitmap("Graphics/Battlebacks/", core_name + "_message")
end
@message_bar_sprite.y = Settings::SCREEN_HEIGHT - @message_bar_sprite.height
end
def create_frame_sprite(index, sub_index = -1)
if sub_index >= 0
if @frame_sprites[index].is_a?(Array)
return if @frame_sprites[index][sub_index] && !@frame_sprites[index][sub_index].disposed?
else
@frame_sprites[index].dispose if @frame_sprites[index] && !@frame_sprites[index].disposed?
@frame_sprites[index] = []
end
else
if @frame_sprites[index].is_a?(Array)
@frame_sprites[index].each { |s| s.dispose if s && !s.disposed? }
@frame_sprites[index] = nil
else
return if @frame_sprites[index] && !@frame_sprites[index].disposed?
end
end
sprite = Sprite.new(viewport)
sprite.bitmap = @frame_bitmap
sprite.z = 99998
sprite.ox = @frame_bitmap.width / 2
sprite.oy = @frame_bitmap.height / 2
if sub_index >= 0
@frame_sprites[index] ||= []
@frame_sprites[index][sub_index] = sprite
else
@frame_sprites[index] = sprite
end
end
def ensure_battler_sprites
should_ensure = @sides_swapped.nil? || @sides_swapped != sides_swapped? ||
@settings_user_index.nil? || @settings_user_index != @settings[:user_index] ||
@settings_target_indices.nil? || @settings_target_indices != @settings[:target_indices]
if should_ensure || !@side_size0 || @side_size0 != side_size(0)
@battler_sprites.each_with_index { |s, i| s.dispose if i.even? && s && !s.disposed? }
@battler_frame_sprites.each_with_index { |s, i| s.dispose if i.even? && s && !s.disposed? }
@side_size0 = side_size(0)
@side_size0.times do |i|
next if user_index != i * 2 && !target_indices.include?(i * 2)
@battler_sprites[i * 2] = Sprite.new(self.viewport)
frame_sprite = Sprite.new(viewport)
frame_sprite.bitmap = @frame_bitmap
frame_sprite.z = 99998
frame_sprite.ox = @frame_bitmap.width / 2
frame_sprite.oy = @frame_bitmap.height / 2
@battler_frame_sprites[i * 2] = frame_sprite
end
end
if should_ensure || !@side_size1 || @side_size1 != side_size(1)
@battler_sprites.each_with_index { |s, i| s.dispose if i.odd? && s && !s.disposed? }
@battler_frame_sprites.each_with_index { |s, i| s.dispose if i.odd? && s && !s.disposed? }
@side_size1 = side_size(1)
@side_size1.times do |i|
next if user_index != (i * 2) + 1 && !target_indices.include?((i * 2) + 1)
@battler_sprites[(i * 2) + 1] = Sprite.new(self.viewport)
frame_sprite = Sprite.new(viewport)
frame_sprite.bitmap = @frame_bitmap
frame_sprite.z = 99998
frame_sprite.ox = @frame_bitmap.width / 2
frame_sprite.oy = @frame_bitmap.height / 2
@battler_frame_sprites[(i * 2) + 1] = frame_sprite
end
end
if should_ensure
@sides_swapped = sides_swapped?
@settings_user_index = @settings[:user_index]
@settings_target_indices = @settings[:target_indices].clone
end
end
def refresh_battler_graphics
if !@user_sprite_name || !@user_sprite_name || @user_sprite_name != @settings[:user_sprite_name]
@user_sprite_name = @settings[:user_sprite_name]
@user_bitmap_front_name = GameData::Species.front_sprite_filename(@user_sprite_name)
@user_bitmap_back_name = GameData::Species.back_sprite_filename(@user_sprite_name)
@user_bitmap_front&.dispose
@user_bitmap_back&.dispose
@user_bitmap_front = RPG::Cache.load_bitmap("", @user_bitmap_front_name)
@user_bitmap_back = RPG::Cache.load_bitmap("", @user_bitmap_back_name)
end
if !@target_bitmap_front || !@target_sprite_name || @target_sprite_name != @settings[:target_sprite_name]
@target_sprite_name = @settings[:target_sprite_name]
@target_bitmap_front_name = GameData::Species.front_sprite_filename(@target_sprite_name)
@target_bitmap_back_name = GameData::Species.back_sprite_filename(@target_sprite_name)
@target_bitmap_front&.dispose
@target_bitmap_back&.dispose
@target_bitmap_front = RPG::Cache.load_bitmap("", @target_bitmap_front_name)
@target_bitmap_back = RPG::Cache.load_bitmap("", @target_bitmap_back_name)
end
end
def refresh_battler_positions
user_idx = user_index
@user_coords = recalculate_battler_position(
user_idx, side_size(user_idx), @user_sprite_name,
(user_idx.even?) ? @user_bitmap_back : @user_bitmap_front
)
target_indices.each do |target_idx|
@target_coords[target_idx] = recalculate_battler_position(
target_idx, side_size(target_idx), @target_sprite_name,
(target_idx.even?) ? @target_bitmap_back : @target_bitmap_front
)
end
end
def recalculate_battler_position(index, size, sprite_name, btmp)
spr = Sprite.new(self.viewport)
spr.x, spr.y = Battle::Scene.pbBattlerPosition(index, size)
data = GameData::Species.get_species_form(sprite_name, 0) # Form 0
data.apply_metrics_to_sprite(spr, index) if data
return [spr.x, spr.y - (btmp.height / 2)]
end
def create_particle_sprite(index, target_idx = -1)
if target_idx >= 0
if @particle_sprites[index].is_a?(Array)
return if @particle_sprites[index][target_idx] && !@particle_sprites[index][target_idx].disposed?
else
@particle_sprites[index].dispose if @particle_sprites[index] && !@particle_sprites[index].disposed?
@particle_sprites[index] = []
end
@particle_sprites[index][target_idx] = Sprite.new(self.viewport)
create_frame_sprite(index, target_idx)
else
if @particle_sprites[index].is_a?(Array)
@particle_sprites[index].each { |s| s.dispose if s && !s.disposed? }
@particle_sprites[index] = nil
else
return if @particle_sprites[index] && !@particle_sprites[index].disposed?
end
@particle_sprites[index] = Sprite.new(self.viewport)
create_frame_sprite(index)
end
end
def get_sprite_and_frame(index, target_idx = -1)
return if !show_particle_sprite?(index)
spr = nil
frame = nil
particle = @anim[:particles][index]
case particle[:name]
when "User"
spr = @battler_sprites[user_index]
raise _INTL("Sprite for particle {1} not found somehow (battler index {2}).",
particle[:name], user_index) if !spr
frame = @battler_frame_sprites[user_index]
when "Target"
spr = @battler_sprites[target_idx]
raise _INTL("Sprite for particle {1} not found somehow (battler index {2}).",
particle[:name], target_idx) if !spr
frame = @battler_frame_sprites[target_idx]
else
create_particle_sprite(index, target_idx)
if target_idx >= 0
spr = @particle_sprites[index][target_idx]
frame = @frame_sprites[index][target_idx]
else
spr = @particle_sprites[index]
frame = @frame_sprites[index]
end
end
return spr, frame
end
def refresh_sprite(index, target_idx = -1)
particle = @anim[:particles][index]
return if !show_particle_sprite?(index)
relative_to_index = -1
if particle[:focus] != :user_and_target
if GameData::Animation::FOCUS_TYPES_WITH_USER.include?(particle[:focus])
relative_to_index = user_index
elsif GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus])
relative_to_index = target_idx
end
end
# Get sprite
spr, frame = get_sprite_and_frame(index, target_idx)
# Calculate all values of particle at the current keyframe
values = AnimationEditor::ParticleDataHelper.get_all_keyframe_particle_values(particle, @display_keyframe)
values.each_pair do |property, val|
values[property] = val[0]
end
# Set visibility
spr.visible = values[:visible]
frame.visible = spr.visible
return if !spr.visible
# Set opacity
spr.opacity = values[:opacity]
# Set coordinates
base_x = values[:x]
base_y = values[:y]
if relative_to_index >= 0 && relative_to_index.odd?
base_x *= -1 if particle[:foe_invert_x]
base_y *= -1 if particle[:foe_invert_y]
end
focus_xy = AnimationPlayer::Helper.get_xy_focus(particle, user_index, target_idx,
@user_coords, @target_coords[target_idx])
AnimationPlayer::Helper.apply_xy_focus_to_sprite(spr, :x, base_x, focus_xy)
AnimationPlayer::Helper.apply_xy_focus_to_sprite(spr, :y, base_y, focus_xy)
# Set graphic and ox/oy (may also alter y coordinate)
AnimationPlayer::Helper.set_bitmap_and_origin(particle, spr, user_index, target_idx,
[@user_bitmap_front_name, @user_bitmap_back_name],
[@target_bitmap_front_name, @target_bitmap_back_name])
offset_xy = AnimationPlayer::Helper.get_xy_offset(particle, spr)
spr.x += offset_xy[0]
spr.y += offset_xy[1]
# Set frame
spr.src_rect.x = values[:frame].floor * spr.src_rect.width
# Set z (priority)
focus_z = AnimationPlayer::Helper.get_z_focus(particle, user_index, target_idx)
AnimationPlayer::Helper.apply_z_focus_to_sprite(spr, values[:z], focus_z)
# Set various other properties
spr.zoom_x = values[:zoom_x] / 100.0
spr.zoom_y = values[:zoom_y] / 100.0
case particle[:angle_override]
when :initial_angle_to_focus
target_x = (focus_xy.length == 2) ? focus_xy[1][0] : focus_xy[0][0]
target_x += offset_xy[0]
target_y = (focus_xy.length == 2) ? focus_xy[1][1] : focus_xy[0][1]
target_y += offset_xy[1]
spr.angle = AnimationPlayer::Helper.initial_angle_between(particle, focus_xy, offset_xy)
when :always_point_at_focus
target_x = (focus_xy.length == 2) ? focus_xy[1][0] : focus_xy[0][0]
target_x += offset_xy[0]
target_y = (focus_xy.length == 2) ? focus_xy[1][1] : focus_xy[0][1]
target_y += offset_xy[1]
spr.angle = AnimationPlayer::Helper.angle_between(spr.x, spr.y, target_x, target_y)
else
spr.angle = 0
end
spr.angle += values[:angle]
spr.mirror = values[:flip]
spr.mirror = !spr.mirror if relative_to_index >= 0 && relative_to_index.odd? && particle[:foe_flip]
spr.blend_type = values[:blending]
# Set color and tone
spr.color.set(values[:color_red], values[:color_green], values[:color_blue], values[:color_alpha])
spr.tone.set(values[:tone_red], values[:tone_green], values[:tone_blue], values[:tone_gray])
# Position frame over spr
frame.x = spr.x
frame.y = spr.y
case particle[:graphic]
when "USER", "USER_OPP", "USER_FRONT", "USER_BACK",
"TARGET", "TARGET_OPP", "TARGET_FRONT", "TARGET_BACK"
# Offset battler frames because they aren't around the battler's position
frame.y -= spr.bitmap.height / 2
end
end
def refresh_particle(index)
one_per_side = [:target_side_foreground, :target_side_background].include?(@anim[:particles][index][:focus])
sides_covered = []
target_indices.each do |target_idx|
next if one_per_side && sides_covered.include?(target_idx % 2)
refresh_sprite(index, target_idx)
sides_covered.push(target_idx % 2)
end
end
def refresh_particle_frame
return if !show_particle_sprite?(@selected_particle)
frame_color = focus_color(@anim[:particles][@selected_particle][:focus])
@sel_frame_bitmap.outline_rect(1, 1, @sel_frame_bitmap.width - 2, @sel_frame_bitmap.height - 2, frame_color)
update_selected_particle_frame
end
def hide_all_sprites
[@battler_sprites, @battler_frame_sprites].each do |sprites|
sprites.each { |s| s.visible = false if s && !s.disposed? }
end
[@particle_sprites, @frame_sprites].each do |sprites|
sprites.each do |s|
if s.is_a?(Array)
s.each { |s2| s2.visible = false if s2 && !s2.disposed? }
else
s.visible = false if s && !s.disposed?
end
end
end
end
def refresh
refresh_bg_graphics
ensure_battler_sprites
refresh_battler_graphics
refresh_battler_positions
hide_all_sprites
@anim[:particles].each_with_index do |particle, i|
if GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus])
refresh_particle(i) # Because there can be multiple targets
else
refresh_sprite(i) if show_particle_sprite?(i)
end
end
refresh_particle_frame # Intentionally after refreshing particles
end
#-----------------------------------------------------------------------------
def on_mouse_press
mouse_x, mouse_y = mouse_pos
return if !mouse_x || !mouse_y
# Check if mouse is over particle frame
if @sel_frame_sprite.visible &&
mouse_x >= @sel_frame_sprite.x - @sel_frame_sprite.ox &&
mouse_x < @sel_frame_sprite.x - @sel_frame_sprite.ox + @sel_frame_sprite.width &&
mouse_y >= @sel_frame_sprite.y - @sel_frame_sprite.oy &&
mouse_y < @sel_frame_sprite.y - @sel_frame_sprite.oy + @sel_frame_sprite.height
if @keyframe >= 0
@captured = [@sel_frame_sprite.x, @sel_frame_sprite.y,
@sel_frame_sprite.x - mouse_x, @sel_frame_sprite.y - mouse_y]
end
return
end
# Find closest particle to mouse
nearest_index = -1
nearest_distance = -1
@battler_frame_sprites.each_with_index do |sprite, index|
next if !sprite || !sprite.visible
next if !mouse_in_sprite?(sprite, mouse_x, mouse_y)
dist = (sprite.x - mouse_x) ** 2 + (sprite.y - mouse_y) ** 2
next if nearest_distance >= 0 && nearest_distance < dist
if index == user_index
nearest_index = @anim[:particles].index { |particle| particle[:name] == "User" }
else
nearest_index = @anim[:particles].index { |particle| particle[:name] == "Target" }
end
nearest_distance = dist
end
@frame_sprites.each_with_index do |sprite, index|
sprites = (sprite.is_a?(Array)) ? sprite : [sprite]
sprites.each do |spr|
next if !spr || !spr.visible
next if !mouse_in_sprite?(spr, mouse_x, mouse_y)
dist = (spr.x - mouse_x) ** 2 + (spr.y - mouse_y) ** 2
next if nearest_distance >= 0 && nearest_distance < dist
nearest_index = index
nearest_distance = dist
end
end
return if nearest_index < 0
@values = { :particle_index => nearest_index }
end
def on_mouse_release
@captured = nil
end
def update_input
if Input.trigger?(Input::MOUSELEFT)
on_mouse_press
elsif busy? && Input.release?(Input::MOUSELEFT)
on_mouse_release
end
end
def update_particle_moved
return if !busy?
mouse_x, mouse_y = mouse_pos
return if !mouse_x || !mouse_y
new_canvas_x = mouse_x + @captured[2]
new_canvas_y = mouse_y + @captured[3]
return if @captured[0] == new_canvas_x && @captured[1] == new_canvas_y
particle = @anim[:particles][@selected_particle]
if GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus])
sprite, frame = get_sprite_and_frame(@selected_particle, first_target_index)
else
sprite, frame = get_sprite_and_frame(@selected_particle)
end
# Check if moved horizontally
if @captured[0] != new_canvas_x
new_canvas_pos = mouse_x + @captured[2]
new_pos = new_canvas_x
case particle[:focus]
when :foreground, :midground, :background
when :user
new_pos -= @user_coords[0]
when :target
new_pos -= @target_coords[first_target_index][0]
when :user_and_target
user_pos = @user_coords
target_pos = @target_coords[first_target_index]
distance = GameData::Animation::USER_AND_TARGET_SEPARATION
new_pos -= user_pos[0]
new_pos *= distance[0]
new_pos /= target_pos[0] - user_pos[0]
when :user_side_foreground, :user_side_background
base_coords = Battle::Scene.pbBattlerPosition(user_index)
new_pos -= base_coords[0]
when :target_side_foreground, :target_side_background
base_coords = Battle::Scene.pbBattlerPosition(first_target_index)
new_pos -= base_coords[0]
end
relative_to_index = -1
if particle[:focus] != :user_and_target
if GameData::Animation::FOCUS_TYPES_WITH_USER.include?(particle[:focus])
relative_to_index = user_index
elsif GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus])
relative_to_index = first_target_index
end
end
new_pos *= -1 if relative_to_index >= 0 && relative_to_index.odd? && particle[:foe_invert_x]
@values ||= {}
@values[:x] = new_pos
@captured[0] = new_canvas_x
sprite.x = new_canvas_x
end
# Check if moved vertically
if @captured[1] != new_canvas_y
new_pos = new_canvas_y
case particle[:focus]
when :foreground, :midground, :background
when :user
new_pos -= @user_coords[1]
when :target
new_pos -= @target_coords[first_target_index][1]
when :user_and_target
user_pos = @user_coords
target_pos = @target_coords[first_target_index]
distance = GameData::Animation::USER_AND_TARGET_SEPARATION
new_pos -= user_pos[1]
new_pos *= distance[1]
new_pos /= target_pos[1] - user_pos[1]
when :user_side_foreground, :user_side_background
base_coords = Battle::Scene.pbBattlerPosition(user_index)
new_pos -= base_coords[1]
when :target_side_foreground, :target_side_background
base_coords = Battle::Scene.pbBattlerPosition(first_target_index)
new_pos -= base_coords[1]
end
relative_to_index = -1
if particle[:focus] != :user_and_target
if GameData::Animation::FOCUS_TYPES_WITH_USER.include?(particle[:focus])
relative_to_index = user_index
elsif GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus])
relative_to_index = first_target_index
end
end
new_pos *= -1 if relative_to_index >= 0 && relative_to_index.odd? && particle[:foe_invert_y]
@values ||= {}
@values[:y] = new_pos
@captured[1] = new_canvas_y
sprite.y = new_canvas_y
end
end
def update_selected_particle_frame
if !show_particle_sprite?(@selected_particle)
@sel_frame_sprite.visible = false
return
end
case @anim[:particles][@selected_particle][:name]
when "User"
target = @battler_sprites[user_index]
raise _INTL("Sprite for particle \"{1}\" not found somehow.",
@anim[:particles][@selected_particle][:name]) if !target
when "Target"
target = @battler_sprites[target_indices[0]]
raise _INTL("Sprite for particle \"{1}\" not found somehow.",
@anim[:particles][@selected_particle][:name]) if !target
else
target = @particle_sprites[@selected_particle]
target = target[first_target_index] if target&.is_a?(Array)
end
if !target || !target.visible
@sel_frame_sprite.visible = false
return
end
@sel_frame_sprite.visible = true
@sel_frame_sprite.x = target.x
@sel_frame_sprite.y = target.y
case @anim[:particles][@selected_particle][:graphic]
when "USER", "USER_OPP", "USER_FRONT", "USER_BACK",
"TARGET", "TARGET_OPP", "TARGET_FRONT", "TARGET_BACK"
# Offset battler frames because they aren't around the battler's position
@sel_frame_sprite.y -= target.bitmap.height / 2
end
end
def update
update_input
update_particle_moved
update_selected_particle_frame
end
end

View File

@@ -0,0 +1,206 @@
#===============================================================================
#
#===============================================================================
class AnimationEditor::PlayControls < UIControls::ControlsContainer
attr_reader :slowdown, :looping
ROW_HEIGHT = 28
PLAY_BUTTON_X = 231
PLAY_BUTTON_Y = 3
PLAY_BUTTON_SIZE = 42
LOOP_BUTTON_X = PLAY_BUTTON_X + PLAY_BUTTON_SIZE + 12
LOOP_BUTTON_Y = 16
LOOP_BUTTON_SIZE = 16
# NOTE: Slowdown label is centered horizontally over the buttons.
SLOWDOWN_LABEL_Y = 0
SLOWDOWN_BUTTON_X = 1
SLOWDOWN_BUTTON_Y = ROW_HEIGHT - 1
SLOWDOWN_BUTTON_WIDTH = 32
SLOWDOWN_BUTTON_SPACING = -3
# NOTE: Duration label and value are centered horizontally on DURATION_TEXT_X.
DURATION_TEXT_X = 464
DURATION_LABEL_Y = SLOWDOWN_LABEL_Y
DURATION_VALUE_Y = ROW_HEIGHT
SLOWDOWN_FACTORS = [1, 2, 4, 6, 8]
def initialize(x, y, width, height, viewport)
super(x, y, width, height)
@viewport.z = viewport.z + 10
generate_button_bitmaps
@duration = 0
@slowdown = SLOWDOWN_FACTORS[0]
@looping = false
end
def dispose
@bitmaps.each_value { |b| b&.dispose }
@bitmaps.clear
super
end
#-----------------------------------------------------------------------------
def generate_button_bitmaps
@bitmaps = {} if !@bitmaps
icon_color = text_color
@bitmaps[:play_button] = Bitmap.new(PLAY_BUTTON_SIZE, PLAY_BUTTON_SIZE) if !@bitmaps[:play_button]
@bitmaps[:play_button].clear
(PLAY_BUTTON_SIZE - 10).times do |j|
@bitmaps[:play_button].fill_rect(11, j + 5, (j >= (PLAY_BUTTON_SIZE - 10) / 2) ? PLAY_BUTTON_SIZE - j - 4 : j + 7, 1, icon_color)
end
@bitmaps[:stop_button] = Bitmap.new(PLAY_BUTTON_SIZE, PLAY_BUTTON_SIZE) if !@bitmaps[:stop_button]
@bitmaps[:stop_button].clear
@bitmaps[:stop_button].fill_rect(8, 8, PLAY_BUTTON_SIZE - 16, PLAY_BUTTON_SIZE - 16, icon_color)
# Loop button
@bitmaps[:play_once_button] = Bitmap.new(LOOP_BUTTON_SIZE, LOOP_BUTTON_SIZE) if !@bitmaps[:play_once_button]
@bitmaps[:play_once_button].clear
@bitmaps[:play_once_button].fill_rect(1, 7, 11, 2, icon_color)
@bitmaps[:play_once_button].fill_rect(8, 5, 2, 6, icon_color)
@bitmaps[:play_once_button].fill_rect(10, 6, 1, 4, icon_color)
@bitmaps[:play_once_button].fill_rect(13, 1, 2, 14, icon_color)
@bitmaps[:looping_button] = Bitmap.new(LOOP_BUTTON_SIZE, LOOP_BUTTON_SIZE) if !@bitmaps[:looping_button]
@bitmaps[:looping_button].clear
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1,
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0].each_with_index do |val, i|
next if val == 0
@bitmaps[:looping_button].fill_rect(1 + (i % 14), 1 + (i / 14), 1, 1, icon_color)
end
end
def add_play_controls
# Slowdown label
duration_label = UIControls::Label.new(200, ROW_HEIGHT, self.viewport, _INTL("Slowdown factor"))
duration_label.x = SLOWDOWN_BUTTON_X + (SLOWDOWN_FACTORS.length * (SLOWDOWN_BUTTON_WIDTH + SLOWDOWN_BUTTON_SPACING) / 2)
duration_label.x -= (duration_label.text_width / 2) + 5
duration_label.y = SLOWDOWN_LABEL_Y
@controls.push([:slowdown_label, duration_label])
# Slowdown factor buttons
SLOWDOWN_FACTORS.each_with_index do |value, i|
button = UIControls::Button.new(SLOWDOWN_BUTTON_WIDTH, ROW_HEIGHT, self.viewport, value.to_s)
button.set_fixed_size
button.x = SLOWDOWN_BUTTON_X + ((SLOWDOWN_BUTTON_WIDTH + SLOWDOWN_BUTTON_SPACING) * i)
button.y = SLOWDOWN_BUTTON_Y
button.set_interactive_rects
button.set_highlighted if value == @slowdown
@controls.push([("slowdown" + value.to_s).to_sym, button])
end
# Play button
play_button = UIControls::BitmapButton.new(PLAY_BUTTON_X, PLAY_BUTTON_Y, self.viewport, @bitmaps[:play_button])
play_button.set_interactive_rects
play_button.disable
@controls.push([:play, play_button])
# Stop button
stop_button = UIControls::BitmapButton.new(PLAY_BUTTON_X, PLAY_BUTTON_Y, self.viewport, @bitmaps[:stop_button])
stop_button.set_interactive_rects
stop_button.visible = false
@controls.push([:stop, stop_button])
# Loop buttons
loop_button = UIControls::BitmapButton.new(LOOP_BUTTON_X, LOOP_BUTTON_Y, self.viewport, @bitmaps[:play_once_button])
loop_button.set_interactive_rects
loop_button.visible = false if @looping
@controls.push([:loop, loop_button])
unloop_button = UIControls::BitmapButton.new(LOOP_BUTTON_X, LOOP_BUTTON_Y, self.viewport, @bitmaps[:looping_button])
unloop_button.set_interactive_rects
unloop_button.visible = false if !@looping
@controls.push([:unloop, unloop_button])
# Duration label
duration_label = UIControls::Label.new(200, ROW_HEIGHT, self.viewport, _INTL("Duration"))
duration_label.x = DURATION_TEXT_X - (duration_label.text_width / 2)
duration_label.y = DURATION_LABEL_Y
@controls.push([:duration_label, duration_label])
# Duration value
duration_value = UIControls::Label.new(200, ROW_HEIGHT, self.viewport, _INTL("{1}s", 0.0))
duration_value.x = DURATION_TEXT_X - (duration_value.text_width / 2)
duration_value.y = DURATION_VALUE_Y
@controls.push([:duration_value, duration_value])
end
#-----------------------------------------------------------------------------
def duration=(new_val)
return if @duration == new_val
@duration = new_val
if @duration == 0
get_control(:play).disable
else
get_control(:play).enable
end
ctrl = get_control(:duration_value)
ctrl.text = _INTL("{1}s", @duration / 20.0)
ctrl.x = DURATION_TEXT_X - (ctrl.text_width / 2)
refresh
end
def color_scheme=(value)
return if @color_scheme == value
@color_scheme = value
generate_button_bitmaps
if @controls
@controls.each { |c| c[1].color_scheme = value }
repaint
end
end
#-----------------------------------------------------------------------------
def prepare_to_play_animation
get_control(:play).visible = false
get_control(:stop).visible = true
@controls.each { |ctrl| ctrl[1].disable if ctrl[0] != :stop }
end
def end_playing_animation
get_control(:stop).visible = false
get_control(:play).visible = true
@controls.each { |ctrl| ctrl[1].enable }
end
#-----------------------------------------------------------------------------
def update
super
if @values
@values.keys.each do |key|
case key
when :loop
get_control(:loop).visible = false
get_control(:unloop).visible = true
@looping = true
@values.delete(key)
when :unloop
get_control(:unloop).visible = false
get_control(:loop).visible = true
@looping = false
@values.delete(key)
else
if key.to_s[/slowdown/]
# A slowdown button was pressed; apply its effect now
@slowdown = key.to_s.sub("slowdown", "").to_i
@controls.each do |ctrl|
next if !ctrl[0].to_s[/slowdown\d+/]
if ctrl[0].to_s.sub("slowdown", "").to_i == @slowdown
ctrl[1].set_highlighted
else
ctrl[1].set_not_highlighted
end
end
@values.delete(key)
end
end
end
@values = nil if @values.empty?
end
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
#===============================================================================
#
#===============================================================================
class AnimationEditor::MenuBar < UIControls::ControlsContainer
MENU_BUTTON_WIDTH = 80
SETTINGS_BUTTON_WIDTH = 30
NAME_BUTTON_WIDTH = 400 # The animation's name
def initialize(x, y, width, height, viewport)
super(x, y, width, height)
@viewport.z = viewport.z + 10 # So that it appears over the canvas
end
#-----------------------------------------------------------------------------
def add_button(id, button_text)
ctrl = UIControls::Button.new(MENU_BUTTON_WIDTH, @height, @viewport, button_text)
ctrl.set_fixed_size
add_control(id, ctrl)
end
def add_settings_button(id, bitmap)
ctrl = UIControls::BitmapButton.new(0, 0, @viewport, bitmap)
add_control_at(id, ctrl, @width - SETTINGS_BUTTON_WIDTH + 2, 2)
end
def add_name_button(id, button_text)
ctrl = UIControls::Button.new(NAME_BUTTON_WIDTH, @height, @viewport, button_text)
ctrl.set_fixed_size
add_control_at(id, ctrl, @width - ctrl.width - SETTINGS_BUTTON_WIDTH, 0)
end
def anim_name=(val)
ctrl = get_control(:name)
ctrl.set_text(val) if ctrl
end
#-----------------------------------------------------------------------------
private
def next_control_position(add_offset = false)
row_x = @row_count * MENU_BUTTON_WIDTH
row_y = 0
return row_x, row_y
end
end

View File

@@ -0,0 +1,313 @@
#===============================================================================
#
#===============================================================================
class AnimationPlayer
attr_accessor :looping
attr_accessor :slowdown # 1 = normal speed, 2 = half speed, 3 = one third speed, etc.
# animation is either a GameData::Animation or a hash made from one.
# user is a Battler, or nil
# targets is an array of Battlers, or nil
def initialize(animation, user, targets, scene)
@animation = animation
@user = user
@targets = targets
@scene = scene
@viewport = @scene.viewport
@sprites = @scene.sprites
initialize_battler_sprite_names
initialize_battler_coordinates
@looping = false
@slowdown = 1
@timer_start = nil
@anim_sprites = [] # Each is a ParticleSprite
@spawner_sprites = []
@duration = total_duration
end
# Doesn't actually create any sprites; just gathers them into a more useful array
def initialize_battler_sprite_names
@battler_sprites = []
if @user
pkmn = @user.pokemon
@battler_sprites[@user.index] = []
@battler_sprites[@user.index].push(GameData::Species.front_sprite_filename(
pkmn.species, pkmn.form, pkmn.gender))
@battler_sprites[@user.index].push(GameData::Species.back_sprite_filename(
pkmn.species, pkmn.form, pkmn.gender))
end
if @targets
@targets.each do |target|
pkmn = target.pokemon
@battler_sprites[target.index] = []
@battler_sprites[target.index].push(GameData::Species.front_sprite_filename(
pkmn.species, pkmn.form, pkmn.gender))
@battler_sprites[target.index].push(GameData::Species.back_sprite_filename(
pkmn.species, pkmn.form, pkmn.gender))
end
end
end
def initialize_battler_coordinates
@user_coords = nil
if @user
sprite = @sprites["pokemon_#{@user.index}"]
@user_coords = [sprite.x, sprite.y - (sprite.bitmap.height / 2)]
end
@target_coords = []
if @targets
@targets.each do |target|
sprite = @sprites["pokemon_#{target.index}"]
@target_coords[target.index] = [sprite.x, sprite.y - (sprite.bitmap.height / 2)]
end
end
end
def dispose
@anim_sprites.each { |particle| particle.dispose }
@anim_sprites.clear
end
#-----------------------------------------------------------------------------
def particles
return (@animation.is_a?(GameData::Animation)) ? @animation.particles : @animation[:particles]
end
# Return value is in seconds.
def total_duration
ret = AnimationPlayer::Helper.get_duration(particles) / 20.0
ret *= slowdown
return ret
end
#-----------------------------------------------------------------------------
def set_up_particle(particle, target_idx = -1, instance = 0)
particle_sprite = AnimationPlayer::ParticleSprite.new
# Get/create a sprite
sprite = nil
case particle[:name]
when "User"
sprite = @sprites["pokemon_#{@user.index}"]
particle_sprite.set_as_battler_sprite
when "Target"
sprite = @sprites["pokemon_#{target_idx}"]
particle_sprite.set_as_battler_sprite
when "SE"
# Intentionally no sprite created
else
sprite = Sprite.new(@viewport)
end
particle_sprite.sprite = sprite if sprite
# Set sprite's graphic and ox/oy
if sprite
AnimationPlayer::Helper.set_bitmap_and_origin(particle, sprite, @user&.index, target_idx,
@battler_sprites[@user&.index || -1], @battler_sprites[target_idx])
end
# Calculate x/y/z focus values and additional x/y modifier and pass them all
# to particle_sprite
focus_xy = AnimationPlayer::Helper.get_xy_focus(particle, @user&.index, target_idx,
@user_coords, @target_coords[target_idx])
offset_xy = AnimationPlayer::Helper.get_xy_offset(particle, sprite)
focus_z = AnimationPlayer::Helper.get_z_focus(particle, @user&.index, target_idx)
particle_sprite.focus_xy = focus_xy
particle_sprite.offset_xy = offset_xy
particle_sprite.focus_z = focus_z
# Set whether properties should be modified if the particle's target is on
# the opposing side
relative_to_index = -1
if GameData::Animation::FOCUS_TYPES_WITH_USER.include?(particle[:focus])
relative_to_index = @user.index
elsif GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus])
relative_to_index = target_idx
end
if relative_to_index >= 0 && relative_to_index.odd? && particle[:focus] != :user_and_target
particle_sprite.foe_invert_x = particle[:foe_invert_x]
particle_sprite.foe_invert_y = particle[:foe_invert_y]
particle_sprite.foe_flip = particle[:foe_flip]
end
particle_sprite.base_angle = 0
case particle[:angle_override]
when :initial_angle_to_focus
target_x = (focus_xy.length == 2) ? focus_xy[1][0] : focus_xy[0][0]
target_x += offset_xy[0]
target_y = (focus_xy.length == 2) ? focus_xy[1][1] : focus_xy[0][1]
target_y += offset_xy[1]
particle_sprite.base_angle = AnimationPlayer::Helper.initial_angle_between(particle, focus_xy, offset_xy)
when :always_point_at_focus
particle_sprite.angle_override = particle[:angle_override] if relative_to_index >= 0
end
# Find earliest command and add a "make visible" command then
delay = AnimationPlayer::Helper.get_particle_delay(particle, instance)
if sprite && !particle_sprite.battler_sprite?
first_cmd = AnimationPlayer::Helper.get_first_command_frame(particle)
particle_sprite.add_set_process(:visible, (first_cmd + delay) * slowdown, true) if first_cmd >= 0
# Apply random frame
if particle[:random_frame_max] && particle[:random_frame_max] > 0
particle_sprite.add_set_process(:frame, (first_cmd + delay) * slowdown, rand(particle[:random_frame_max] + 1))
end
end
# Add all commands
spawner_type = particle[:spawner] || :none
regular_properties_skipped = AnimationPlayer::Helper::PROPERTIES_SET_BY_SPAWNER[spawner_type] || []
particle.each_pair do |property, cmds|
next if !cmds.is_a?(Array) || cmds.empty?
next if regular_properties_skipped.include?(property)
cmds.each do |cmd|
if cmd[1] == 0
if sprite
particle_sprite.add_set_process(property, (cmd[0] + delay) * slowdown, cmd[2])
else
# SE particle
filename = nil
case property
when :user_cry
filename = GameData::Species.cry_filename_from_pokemon(@user.pokemon) if @user
when :target_cry
# NOTE: If there are multiple targets, only the first one's cry
# will be played.
if @targets && !@targets.empty?
filename = GameData::Species.cry_filename_from_pokemon(@targets.first.pokemon)
end
else
filename = "Anim/" + cmd[2]
end
particle_sprite.add_set_process(property, (cmd[0] + delay) * slowdown, [filename, cmd[3], cmd[4]]) if filename
end
else
particle_sprite.add_move_process(property, (cmd[0] + delay) * slowdown, cmd[1] * slowdown, cmd[2], cmd[3] || :linear)
end
end
end
# Finish up
@anim_sprites.push(particle_sprite)
@spawner_sprites.push([particle_sprite, particle, target_idx, instance, delay]) if spawner_type != :none
end
def add_spawner_commands(particle_sprite, particle, target_idx, instance, delay, add_as_spawner = true)
spawner_type = particle[:spawner] || :none
return if spawner_type == :none
@spawner_sprites.push([particle_sprite, particle, target_idx, instance, delay]) if add_as_spawner
life_start = AnimationPlayer::Helper.get_first_command_frame(particle)
life_end = AnimationPlayer::Helper.get_last_command_frame(particle)
life_end = AnimationPlayer::Helper.get_duration(particles) if life_end < 0
lifetime = life_end - life_start
case spawner_type
when :random_direction, :random_direction_gravity, :random_up_direction_gravity
if spawner_type == :random_up_direction_gravity
angle = 30 + rand(120)
else
angle = rand(360)
angle = rand(360) if angle >= 180 && spawner_type == :random_direction_gravity # Prefer upwards angles
end
speed = rand(200, 400)
start_x_speed = speed * Math.cos(angle * Math::PI / 180)
start_y_speed = -speed * Math.sin(angle * Math::PI / 180)
start_x = (start_x_speed * 0.05) + rand(-8, 8)
start_y = (start_y_speed * 0.05) + rand(-8, 8)
# Set initial positions
[:x, :y].each do |property|
particle_sprite.delete_processes(property)
if particle[property] && !particle[property].empty?
offset = (property == :x) ? start_x : start_y
particle[property].each do |cmd|
next if cmd[1] > 0
particle_sprite.add_set_process(property, (cmd[0] + delay) * slowdown, cmd[2] + offset)
break
end
end
end
# Set movements
particle_sprite.add_move_process(:x,
(life_start + delay) * slowdown, lifetime * slowdown,
start_x + (start_x_speed * lifetime / 20.0), :linear)
if [:random_direction_gravity, :random_up_direction_gravity].include?(spawner_type)
particle_sprite.add_move_process(:y,
(life_start + delay) * slowdown, lifetime * slowdown,
[start_y_speed / slowdown, AnimationPlayer::Helper::GRAVITY_STRENGTH.to_f / (slowdown * slowdown)], :gravity)
else
particle_sprite.add_move_process(:y,
(life_start + delay) * slowdown, lifetime * slowdown,
start_y + (start_y_speed * lifetime / 20.0), :linear)
end
end
end
# Creates sprites and ParticleSprites, and sets sprite properties that won't
# change during the animation.
def set_up
particles.each do |particle|
qty = 1
qty = particle[:spawn_quantity] || 1 if particle[:spawner] && particle[:spawner] != :none
qty.times do |i|
if GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus]) && @targets
one_per_side = [:target_side_foreground, :target_side_background].include?(particle[:focus])
sides_covered = []
@targets.each do |target|
next if one_per_side && sides_covered.include?(target.index % 2)
set_up_particle(particle, target.index, i)
sides_covered.push(target.index % 2)
end
else
set_up_particle(particle, -1, i)
end
end
end
reset_anim_sprites
end
# Sets the initial properties of all sprites, and marks all processes as not
# yet started.
def reset_anim_sprites
@anim_sprites.each { |particle| particle.reset_processes }
# Randomise spawner particle properties
@spawner_sprites.each { |spawner| add_spawner_commands(*spawner, false) }
end
#-----------------------------------------------------------------------------
def start
@timer_start = System.uptime
end
def playing?
return !@timer_start.nil?
end
def finish
@timer_start = nil
@finished = true
end
def finished?
return @finished
end
def can_continue_battle?
return finished?
end
#-----------------------------------------------------------------------------
def update
return if !playing?
if @need_reset
reset_anim_sprites
start
@need_reset = false
end
time_now = System.uptime
elapsed = time_now - @timer_start
# Update all particles/sprites
@anim_sprites.each { |particle| particle.update(elapsed) }
# Finish or loop the animation
if elapsed >= @duration * @slowdown
if looping
@need_reset = true
else
finish
end
end
end
end

View File

@@ -0,0 +1,176 @@
#===============================================================================
# NOTE: This assumes that processes are added (for a given property) in the
# order they happen.
#===============================================================================
class AnimationPlayer::ParticleSprite
attr_accessor :sprite
attr_accessor :focus_xy, :offset_xy, :focus_z
attr_accessor :base_angle, :angle_override
attr_accessor :foe_invert_x, :foe_invert_y, :foe_flip
FRAMES_PER_SECOND = 20.0
def initialize
@processes = []
@sprite = nil
@battler_sprite = false
initialize_values
end
def initialize_values
@values = GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES.clone
end
def dispose
return if battler_sprite? || !@sprite || @sprite.disposed?
@sprite.bitmap&.dispose
@sprite.dispose
end
#-----------------------------------------------------------------------------
def set_as_battler_sprite
@battler_sprite = true
@values[:visible] = true
end
def battler_sprite?
return @battler_sprite
end
#-----------------------------------------------------------------------------
def add_set_process(property, frame, value)
add_move_process(property, frame, 0, value, :none)
end
def add_move_process(property, start_frame, duration, value, interpolation = :linear)
# First nil is progress (nil = not started, true = running, false = finished)
# Second nil is start value (set when the process starts running)
@processes.push([property, start_frame, duration, value, interpolation, nil, nil])
end
def delete_processes(property)
@processes.delete_if { |process| process[0] == property }
end
# Sets sprite's initial For looping purposes.
def reset_processes
initialize_values
set_as_battler_sprite if battler_sprite? # Start battler sprites as visible
@values.each_pair { |property, value| update_sprite_property(property, value) }
@processes.each { |process| process[5] = nil }
end
#-----------------------------------------------------------------------------
def start_process(process)
return if !process[5].nil?
process[6] = @values[process[0]]
process[5] = true
end
def update_process_value(process, elapsed_time)
# SetXYZ
if process[2] == 0
@values[process[0]] = process[3]
process[5] = false # Mark process as finished
return
end
# MoveXYZ
@values[process[0]] = AnimationPlayer::Helper.interpolate(
process[4], process[6], process[3], process[2] / FRAMES_PER_SECOND,
process[1] / FRAMES_PER_SECOND, elapsed_time
)
if elapsed_time >= (process[1] + process[2]) / FRAMES_PER_SECOND
process[5] = false # Mark process as finished
end
end
def update_sprite(changed_properties)
changed_properties.uniq!
changed_properties.each do |property|
update_sprite_property(property, @values[property])
end
end
def update_sprite_property(property, value)
if !@sprite
pbSEPlay(*value) if [:se, :user_cry, :target_cry].include?(property) && value
return
end
case property
when :frame then @sprite.src_rect.x = value.floor * @sprite.src_rect.width
when :blending then @sprite.blend_type = value
when :flip
@sprite.mirror = value
@sprite.mirror = !@sprite.mirror if @foe_flip
when :x
value = value.round
value *= -1 if @foe_invert_x
AnimationPlayer::Helper.apply_xy_focus_to_sprite(@sprite, :x, value, @focus_xy)
@sprite.x += @offset_xy[0]
update_angle_pointing_at_focus
when :y
value = value.round
value *= -1 if @foe_invert_y
AnimationPlayer::Helper.apply_xy_focus_to_sprite(@sprite, :y, value, @focus_xy)
@sprite.y += @offset_xy[1]
update_angle_pointing_at_focus
when :z
AnimationPlayer::Helper.apply_z_focus_to_sprite(@sprite, value, @focus_z)
when :zoom_x then @sprite.zoom_x = value / 100.0
when :zoom_y then @sprite.zoom_y = value / 100.0
when :angle
if @angle_override == :always_point_at_focus
update_angle_pointing_at_focus
@sprite.angle += value
else
@sprite.angle = value + (@base_angle || 0)
end
when :visible then @sprite.visible = value
when :opacity then @sprite.opacity = value
when :color_red then @sprite.color.red = value
when :color_green then @sprite.color.green = value
when :color_blue then @sprite.color.blue = value
when :color_alpha then @sprite.color.alpha = value
when :tone_red then @sprite.tone.red = value
when :tone_green then @sprite.tone.green = value
when :tone_blue then @sprite.tone.blue = value
when :tone_gray then @sprite.tone.gray = value
end
end
# This assumes vertically up is an angle of 0, and the angle increases
# anticlockwise.
def update_angle_pointing_at_focus
return if @angle_override != :always_point_at_focus
# Get coordinates
sprite_x = @sprite.x
sprite_y = @sprite.y
target_x = (@focus_xy.length == 2) ? @focus_xy[1][0] : @focus_xy[0][0]
target_x += @offset_xy[0]
target_y = (@focus_xy.length == 2) ? @focus_xy[1][1] : @focus_xy[0][1]
target_y += @offset_xy[1]
@sprite.angle = AnimationPlayer::Helper.angle_between(sprite_x, sprite_y, target_x, target_y)
@sprite.angle += (@base_angle || 0)
end
def update(elapsed_time)
frame = (elapsed_time * FRAMES_PER_SECOND).floor
changed_properties = []
@processes.each do |process|
# Skip processes that aren't due to start yet
next if process[1] > frame
# Skip processes that have already fully happened
next if process[5] == false
# Mark process as running if it isn't already
start_process(process)
# Update process's value
update_process_value(process, elapsed_time)
changed_properties.push(process[0]) # Record property as having changed
end
# Apply changed values to sprite
update_sprite(changed_properties) if !changed_properties.empty?
end
end

View File

@@ -0,0 +1,297 @@
#===============================================================================
# Methods used by both AnimationPlayer and AnimationEditor::Canvas.
#===============================================================================
module AnimationPlayer::Helper
PROPERTIES_SET_BY_SPAWNER = {
:random_direction => [:x, :y],
:random_direction_gravity => [:x, :y],
:random_up_direction_gravity => [:x, :y]
}
GRAVITY_STRENGTH = 500
BATTLE_MESSAGE_BAR_HEIGHT = 96 # NOTE: You shouldn't need to change this.
module_function
# Returns the duration of the animation in frames (1/20ths of a second).
def get_duration(particles)
ret = 0
particles.each do |particle|
particle.each_pair do |property, value|
next if !value.is_a?(Array) || value.empty?
max = value.last[0] + value.last[1] # Keyframe + duration
# Particle spawners can delay their particles; account for this
if (particle[:spawner] || :none) != :none
max += get_particle_delay(particle, (particle[:spawn_quantity] || 1) - 1)
end
ret = max if ret < max
end
end
return ret
end
# Returns the frame that the particle has its earliest command.
def get_first_command_frame(particle)
ret = -1
particle.each_pair do |property, cmds|
next if !cmds.is_a?(Array) || cmds.empty?
cmds.each do |cmd|
ret = cmd[0] if ret < 0 || ret > cmd[0]
end
end
return (ret >= 0) ? ret : 0
end
# Returns the frame that the particle has (the end of) its latest command.
def get_last_command_frame(particle)
ret = -1
particle.each_pair do |property, cmds|
next if !cmds.is_a?(Array) || cmds.empty?
cmds.each do |cmd|
ret = cmd[0] + cmd[1] if ret < cmd[0] + cmd[1]
end
end
return ret
end
# For spawner particles
def get_particle_delay(particle, instance)
case particle[:spawner] || :none
when :random_direction, :random_direction_gravity, :random_up_direction_gravity
return instance / 4
end
return 0
end
#-----------------------------------------------------------------------------
def get_xy_focus(particle, user_index, target_index, user_coords, target_coords)
ret = nil
case particle[:focus]
when :foreground, :midground, :background
when :user
ret = [user_coords.clone]
when :target
ret = [target_coords.clone]
when :user_and_target
ret = [user_coords.clone, target_coords.clone]
when :user_side_foreground, :user_side_background
ret = [Battle::Scene.pbBattlerPosition(user_index)]
when :target_side_foreground, :target_side_background
ret = [Battle::Scene.pbBattlerPosition(target_index)]
end
return ret
end
def get_xy_offset(particle, sprite)
ret = [0, 0]
case particle[:graphic]
when "USER", "USER_OPP", "USER_FRONT", "USER_BACK",
"TARGET", "TARGET_OPP", "TARGET_FRONT", "TARGET_BACK"
ret[1] += sprite.bitmap.height / 2 if sprite
end
return ret
end
# property is :x or :y.
def apply_xy_focus_to_sprite(sprite, property, value, focus)
result = value
coord_idx = (property == :x) ? 0 : 1
if focus
if focus.length == 2
distance = GameData::Animation::USER_AND_TARGET_SEPARATION
result = focus[0][coord_idx] + ((value.to_f / distance[coord_idx]) * (focus[1][coord_idx] - focus[0][coord_idx])).to_i
else
result = value + focus[0][coord_idx]
end
end
case property
when :x then sprite.x = result
when :y then sprite.y = result
end
end
#-----------------------------------------------------------------------------
# Returns either a number or an array of two numbers.
def get_z_focus(particle, user_index, target_index)
ret = 0
case particle[:focus]
when :foreground
ret = 2000
when :midground
ret = 1000
when :background
# NOTE: No change.
when :user
ret = 1000 + ((100 * ((user_index / 2) + 1)) * (user_index.even? ? 1 : -1))
when :target
ret = 1000 + ((100 * ((target_index / 2) + 1)) * (target_index.even? ? 1 : -1))
when :user_and_target
user_pos = 1000 + ((100 * ((user_index / 2) + 1)) * (user_index.even? ? 1 : -1))
target_pos = 1000 + ((100 * ((target_index / 2) + 1)) * (target_index.even? ? 1 : -1))
ret = [user_pos, target_pos]
when :user_side_foreground, :target_side_foreground
this_idx = (particle[:focus] == :user_side_foreground) ? user_index : target_index
ret = 1000
ret += 1000 if this_idx.even? # On player's side
when :user_side_background, :target_side_background
this_idx = (particle[:focus] == :user_side_background) ? user_index : target_index
ret = 1000 if this_idx.even? # On player's side
end
return ret
end
def apply_z_focus_to_sprite(sprite, z, focus)
if focus.is_a?(Array)
distance = GameData::Animation::USER_AND_TARGET_SEPARATION[2]
if z >= 0
if focus[0] > focus[1]
sprite.z = focus[0] + z
else
sprite.z = focus[0] - z
end
elsif z <= distance
if focus[0] > focus[1]
sprite.z = focus[1] + z + distance
else
sprite.z = focus[1] - z + distance
end
else
sprite.z = focus[0] + ((z.to_f / distance) * (focus[1] - focus[0])).to_i
end
elsif focus
sprite.z = z + focus
else
sprite.z = z
end
end
#-----------------------------------------------------------------------------
def angle_between(x1, y1, x2, y2)
diff_x = x1 - x2
diff_y = y1 - y2
ret = Math.atan(diff_x.to_f / diff_y) * 180 / Math::PI
ret += 180 if diff_y < 0
return ret
end
def initial_angle_between(particle, focus, offset)
x1 = 0
y1 = 0
x2 = (focus.length == 2) ? focus[1][0] : focus[0][0]
y2 = (focus.length == 2) ? focus[1][1] : focus[0][1]
[:x, :y].each do |property|
next if !particle[property]
particle[property].each do |cmd|
break if cmd[1] > 0
if property == :x
x1 = cmd[2]
else
y1 = cmd[2]
end
break
end
end
if focus
if focus.length == 2
distance = GameData::Animation::USER_AND_TARGET_SEPARATION
x1 = focus[0][0] + ((x1.to_f / distance[0]) * (focus[1][0] - focus[0][0])).to_i
y1 = focus[0][1] + ((y1.to_f / distance[1]) * (focus[1][1] - focus[0][1])).to_i
else
x1 += focus[0][0]
y1 += focus[0][1]
end
end
x1 += offset[0]
y1 += offset[1]
return angle_between(x1, y1, x2, y2)
end
#-----------------------------------------------------------------------------
# user_sprites, target_sprites = [front sprite, back sprite]
def set_bitmap_and_origin(particle, sprite, user_index, target_index, user_sprites, target_sprites)
return if sprite&.is_a?(Battle::Scene::BattlerSprite)
case particle[:graphic]
when "USER", "USER_OPP", "USER_FRONT", "USER_BACK",
"TARGET", "TARGET_OPP", "TARGET_FRONT", "TARGET_BACK"
filename = nil
case particle[:graphic]
when "USER"
filename = (user_index.even?) ? user_sprites[1] : user_sprites[0]
when "USER_OPP"
filename = (user_index.even?) ? user_sprites[0] : user_sprites[1]
when "USER_FRONT"
filename = user_sprites[0]
when "USER_BACK"
filename = user_sprites[1]
when "TARGET"
filename = (target_index.even?) ? target_sprites[1] : target_sprites[0]
when "TARGET_OPP"
filename = (target_index.even?) ? target_sprites[0] : target_sprites[1]
when "TARGET_FRONT"
filename = target_sprites[0]
when "TARGET_BACK"
filename = target_sprites[1]
end
sprite.bitmap = RPG::Cache.load_bitmap("", filename)
sprite.ox = sprite.bitmap.width / 2
sprite.oy = sprite.bitmap.height
else
sprite.bitmap = RPG::Cache.load_bitmap("Graphics/Battle animations/", particle[:graphic])
sprite.src_rect.set(0, 0, sprite.bitmap.width, sprite.bitmap.height)
if [:foreground, :midground, :background].include?(particle[:focus]) &&
sprite.bitmap.width >= Settings::SCREEN_WIDTH &&
sprite.bitmap.height >= Settings::SCREEN_HEIGHT - BATTLE_MESSAGE_BAR_HEIGHT
sprite.ox = 0
sprite.oy = 0
elsif sprite.bitmap.width > sprite.bitmap.height * 2
sprite.src_rect.set(0, 0, sprite.bitmap.height, sprite.bitmap.height)
sprite.ox = sprite.bitmap.height / 2
sprite.oy = sprite.bitmap.height / 2
else
sprite.ox = sprite.bitmap.width / 2
sprite.oy = sprite.bitmap.height / 2
end
if particle[:graphic][/\[\s*bottom\s*\]\s*$/i] # [bottom] at end of filename
sprite.oy = sprite.bitmap.height
end
end
end
#-----------------------------------------------------------------------------
def interpolate(interpolation, start_val, end_val, duration, start_time, now)
case interpolation
when :linear
return lerp(start_val, end_val, duration, start_time, now).to_i
when :ease_in # Quadratic
ret = start_val
x = (now - start_time) / duration.to_f
ret += (end_val - start_val) * x * x
return ret.round
when :ease_out # Quadratic
ret = start_val
x = (now - start_time) / duration.to_f
ret += (end_val - start_val) * (1 - ((1 - x) * (1 - x)))
return ret.round
when :ease_both # Quadratic
ret = start_val
x = (now - start_time) / duration.to_f
if x < 0.5
ret += (end_val - start_val) * x * x * 2
else
ret += (end_val - start_val) * (1 - (((-2 * x) + 2) * ((-2 * x) + 2) / 2))
end
return ret.round
when :gravity # Used by particle spawner
# end_val is [initial speed, gravity]
# s = ut + 1/2 at^2
t = now - start_time
ret = start_val + (end_val[0] * t) + (end_val[1] * t * t / 2)
return ret.round
end
raise _INTL("Unknown interpolation method {1}.", interpolation)
end
end

View File

@@ -0,0 +1,191 @@
#===============================================================================
#
#===============================================================================
class Battle::Scene
ANIMATION_DEFAULTS = [:TACKLE, :DEFENSECURL] # With target, without target
ANIMATION_DEFAULTS_FOR_TYPE_CATEGORY = {
:NORMAL => [:TACKLE, :SONICBOOM, :DEFENSECURL, :BODYSLAM, nil, :TAILWHIP],
:FIGHTING => [:MACHPUNCH, :AURASPHERE, :BULKUP, nil, nil, nil],
:FLYING => [:WINGATTACK, :GUST, :ROOST, nil, :AIRCUTTER, :FEATHERDANCE],
:POISON => [:POISONSTING, :SLUDGE, :ACIDARMOR, nil, :ACID, :POISONPOWDER],
:GROUND => [:SANDTOMB, :MUDSLAP, :MUDSPORT, :EARTHQUAKE, :EARTHPOWER, :SANDATTACK],
:ROCK => [:ROCKTHROW, :POWERGEM, :ROCKPOLISH, :ROCKSLIDE, nil, :SANDSTORM],
:BUG => [:TWINEEDLE, :BUGBUZZ, :QUIVERDANCE, nil, :STRUGGLEBUG, :STRINGSHOT],
:GHOST => [:ASTONISH, :SHADOWBALL, :GRUDGE, nil, nil, :CONFUSERAY],
:STEEL => [:IRONHEAD, :MIRRORSHOT, :IRONDEFENSE, nil, nil, :METALSOUND],
:FIRE => [:FIREPUNCH, :EMBER, :SUNNYDAY, nil, :INCINERATE, :WILLOWISP],
:WATER => [:CRABHAMMER, :WATERGUN, :AQUARING, nil, :SURF, :WATERSPORT],
:GRASS => [:VINEWHIP, :RAZORLEAF, :COTTONGUARD, nil, nil, :SPORE],
:ELECTRIC => [:THUNDERPUNCH, :THUNDERSHOCK, :CHARGE, nil, :DISCHARGE, :THUNDERWAVE],
:PSYCHIC => [:ZENHEADBUTT, :CONFUSION, :CALMMIND, nil, :SYNCHRONOISE, :MIRACLEEYE],
:ICE => [:ICEPUNCH, :ICEBEAM, :MIST, :AVALANCHE, :POWDERSNOW, :HAIL],
:DRAGON => [:DRAGONCLAW, :DRAGONRAGE, :DRAGONDANCE, nil, :TWISTER, nil],
:DARK => [:KNOCKOFF, :DARKPULSE, :HONECLAWS, nil, :SNARL, :EMBARGO],
:FAIRY => [:TACKLE, :FAIRYWIND, :MOONLIGHT, nil, :DAZZLINGGLEAM, :SWEETKISS]
}
#-----------------------------------------------------------------------------
def pbAnimation(move_id, user, targets, version = 0)
anims = find_move_animation(move_id, version, user&.index)
return if !anims || anims.empty?
if anims[0].is_a?(GameData::Animation) # New animation
pbSaveShadows do
# NOTE: anims.sample is a random valid animation.
play_better_animation(anims.sample, user, targets)
end
else # Old animation
anim = anims[0]
target = (targets.is_a?(Array)) ? targets[0] : targets
animations = pbLoadBattleAnimations
return if !animations
pbSaveShadows do
if anims[1] # On opposing side and using OppMove animation
pbAnimationCore(animations[anim], target, user, true)
else # On player's side, and/or using Move animation
pbAnimationCore(animations[anim], user, target)
end
end
end
end
alias __newanims__pbCommonAnimation pbCommonAnimation unless method_defined?(:__newanims__pbCommonAnimation)
def pbCommonAnimation(anim_name, user = nil, target = nil)
return if nil_or_empty?(anim_name)
anims = try_get_better_common_animation(anim_name, user.index)
if anims
# NOTE: anims.sample is a random valid animation.
play_better_animation(anims.sample, user, target)
else
__newanims__pbCommonAnimation(anim_name, user, target)
end
end
#-----------------------------------------------------------------------------
# Returns an array of GameData::Animation if a new animation(s) is found.
# Return [animation index, shouldn't be flipped] if an old animation is found.
def find_move_animation(move_id, version, user_index)
# Get animation
anims = find_move_animation_for_move(move_id, version, user_index)
return anims if anims
# Get information to decide which default animation to try
move_data = GameData::Move.get(move_id)
target_data = GameData::Target.get(move_data.target)
move_type = move_data.type
default_idx = move_data.category
default_idx += 3 if target_data.num_targets > 1 ||
(target_data.num_targets > 0 && move_data.status?)
# Check for a default animation
wanted_move = ANIMATION_DEFAULTS_FOR_TYPE_CATEGORY[move_type][default_idx]
anims = find_move_animation_for_move(wanted_move, 0, user_index)
return anims if anims
if default_idx >= 3
wanted_move = ANIMATION_DEFAULTS_FOR_TYPE_CATEGORY[move_type][default_idx - 3]
anims = find_move_animation_for_move(wanted_move, 0, user_index)
return anims if anims
return nil if ANIMATION_DEFAULTS.include?(wanted_move) # No need to check for these animations twice
end
# Use Tackle or Defense Curl's animation
if target_data.num_targets == 0 && target.data.id != :None
return find_move_animation_for_move(ANIMATION_DEFAULTS[1], 0, user_index)
end
return find_move_animation_for_move(ANIMATION_DEFAULTS[0], 0, user_index)
end
# Find an animation(s) for the given move_id.
def find_move_animation_for_move(move_id, version, user_index)
# Find new animation
anims = try_get_better_move_animation(move_id, version, user_index)
return anims if anims
if version > 0
anims = try_get_better_move_animation(move_id, 0, user_index)
return anims if anims
end
# Find old animation
anim = pbFindMoveAnimDetails(pbLoadMoveToAnim, move_id, user_index, version)
return anim
end
# Finds a new animation for the given move_id and version. Prefers opposing
# animations if the user is opposing. Can return multiple animations.
def try_get_better_move_animation(move_id, version, user_index)
ret = []
backup_ret = []
GameData::Animation.each do |anim|
next if !anim.move_animation? || anim.ignore
next if anim.move != move_id.to_s
next if anim.version != version
if !user_index
ret.push(anim)
next
end
if user_index.even? # User is on player's side
ret.push(anim) if !anim.opposing_animation?
else # User is on opposing side
(anim.opposing_animation?) ? ret.push(anim) : backup_ret.push(anim)
end
end
return ret if !ret.empty?
return backup_ret if !backup_ret.empty?
return nil
end
def try_get_better_common_animation(anim_name, user_index)
ret = []
backup_ret = []
GameData::Animation.each do |anim|
next if !anim.common_animation? || anim.ignore
next if anim.move != anim_name
if !user_index
ret.push(anim)
next
end
if user_index.even? # User is on player's side
ret.push(anim) if !anim.opposing_animation?
else # User is on opposing side
(anim.opposing_animation?) ? ret.push(anim) : backup_ret.push(anim)
end
end
return ret if !ret.empty?
return backup_ret if !backup_ret.empty?
return nil
end
#-----------------------------------------------------------------------------
def play_better_animation(anim_data, user, targets)
return if !anim_data
@briefMessage = false
# Memorize old battler coordinates, to be reset after the animation
old_battler_coords = []
if user
sprite = @sprites["pokemon_#{user.index}"]
old_battler_coords[user.index] = [sprite.x, sprite.y]
end
if targets
targets.each do |target|
sprite = @sprites["pokemon_#{target.index}"]
old_battler_coords[target.index] = [sprite.x, sprite.y]
end
end
# Create animation player
anim_player = AnimationPlayer.new(anim_data, user, targets, self)
anim_player.set_up
# Play animation
anim_player.start
loop do
pbUpdate
anim_player.update
break if anim_player.can_continue_battle?
end
anim_player.dispose
# Restore old battler coordinates
old_battler_coords.each_with_index do |values, i|
next if !values
sprite = @sprites["pokemon_#{i}"]
sprite.x = values[0]
sprite.y = values[1]
end
end
end

View File

@@ -0,0 +1,590 @@
#===============================================================================
#
#===============================================================================
class Battle::Scene
alias __newanims__pbCreateBackdropSprites pbCreateBackdropSprites unless method_defined?(:__newanims__pbCreateBackdropSprites)
def pbCreateBackdropSprites
__newanims__pbCreateBackdropSprites
["battle_bg", "battle_bg2"].each { |spr| @sprites[spr].z = -200 }
2.times do |side|
@sprites["base_#{side}"].z = -199
end
@sprites["cmdBar_bg"].z += 9999
end
alias __newanims__pbInitSprites pbInitSprites unless method_defined?(:__newanims__pbInitSprites)
def pbInitSprites
__newanims__pbInitSprites
@sprites["messageBox"].z += 9999
@sprites["messageWindow"].z += 9999
@sprites["commandWindow"].z += 9999
@sprites["fightWindow"].z += 9999
@sprites["targetWindow"].z += 9999
2.times do |side|
@sprites["partyBar_#{side}"].z += 9999
NUM_BALLS.times do |i|
@sprites["partyBall_#{side}_#{i}"].z += 9999
end
# Ability splash bars
@sprites["abilityBar_#{side}"].z += 9999 if USE_ABILITY_SPLASH
end
@battle.battlers.each_with_index do |b, i|
@sprites["dataBox_#{i}"].z += 9999 if b
end
@battle.player.each_with_index do |p, i|
@sprites["player_#{i + 1}"].z = 1500 + (i * 100)
end
if @battle.trainerBattle?
@battle.opponent.each_with_index do |p, i|
@sprites["trainer_#{i + 1}"].z = 500 - (i * 100)
end
end
end
end
#===============================================================================
# Pokémon sprite (used in battle)
#===============================================================================
class Battle::Scene::BattlerSprite < RPG::Sprite
def pbSetPosition
return if !@_iconBitmap
pbSetOrigin
if @index.even?
self.z = 1100 + (100 * @index / 2)
else
self.z = 1000 - (100 * (@index + 1) / 2)
end
# Set original position
p = Battle::Scene.pbBattlerPosition(@index, @sideSize)
@spriteX = p[0]
@spriteY = p[1]
# Apply metrics
@pkmn.species_data.apply_metrics_to_sprite(self, @index)
end
end
#===============================================================================
# Shadow sprite for Pokémon (used in battle)
#===============================================================================
class Battle::Scene::BattlerShadowSprite < RPG::Sprite
def pbSetPosition
return if !@_iconBitmap
pbSetOrigin
self.z = -198
# Set original position
p = Battle::Scene.pbBattlerPosition(@index, @sideSize)
self.x = p[0]
self.y = p[1]
# Apply metrics
@pkmn.species_data.apply_metrics_to_sprite(self, @index, true)
end
end
#===============================================================================
# Mixin module for certain hardcoded battle animations that involve Poké Balls.
#===============================================================================
module Battle::Scene::Animation::BallAnimationMixin
# The regular Poké Ball burst animation, for when a Pokémon appears from a
# Poké Ball.
def ballBurst(delay, ball, ballX, ballY, poke_ball)
num_particles = 15
num_rays = 10
glare_fade_duration = 8 # Lifetimes/durations are in 20ths of a second
particle_lifetime = 15
particle_fade_duration = 8
ray_lifetime = 13
ray_fade_duration = 5
ray_min_radius = 24 # How far out from the center a ray starts
cherish_ball_ray_tones = [Tone.new(-104, -144, -8), # Indigo
Tone.new(-64, -144, -24), # Purple
Tone.new(-8, -144, -64), # Pink
Tone.new(-8, -48, -152), # Orange
Tone.new(-8, -32, -160)] # Yellow
# Get array of things that vary for each kind of Poké Ball
variances = BALL_BURST_VARIANCES[poke_ball] || BALL_BURST_VARIANCES[:POKEBALL]
# Set up glare particles
glare1 = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_#{variances[11]}", PictureOrigin::CENTER)
glare2 = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_#{variances[8]}", PictureOrigin::CENTER)
[glare1, glare2].each_with_index do |particle, num|
particle.setZ(0, 5105 + num)
particle.setZoom(0, 0)
particle.setTone(0, variances[12 - (3 * num)])
particle.setVisible(0, false)
end
[glare1, glare2].each_with_index do |particle, num|
particle.moveTone(delay + glare_fade_duration + 3, glare_fade_duration / 2, variances[13 - (3 * num)])
end
# Animate glare particles
[glare1, glare2].each { |p| p.setVisible(delay, true) }
if poke_ball == :MASTERBALL
glare1.moveAngle(delay, 19, -135)
glare1.moveZoom(delay, glare_fade_duration, 250)
elsif poke_ball == :DUSKBALL
glare1.moveAngle(delay, 19, -270)
elsif ["whirl"].include?(variances[11])
glare1.moveZoom(delay, glare_fade_duration, 200)
else
glare1.moveZoom(delay, glare_fade_duration, (["dazzle", "ring3", "web"].include?(variances[11])) ? 100 : 250)
end
glare1.moveOpacity(delay + glare_fade_duration + 3, glare_fade_duration, 0)
if poke_ball == :MASTERBALL
glare2.moveAngle(delay, 19, -135)
glare2.moveZoom(delay, glare_fade_duration, 200)
else
glare2.moveZoom(delay, glare_fade_duration, (["dazzle", "ring3", "web"].include?(variances[8])) ? 125 : 200)
end
glare2.moveOpacity(delay + glare_fade_duration + 3, glare_fade_duration - 2, 0)
[glare1, glare2].each { |p| p.setVisible(delay + 19, false) }
# Rays
num_rays.times do |i|
# Set up ray
angle = rand(360)
radian = (angle + 90) * Math::PI / 180
start_zoom = rand(50...100)
ray = addNewSprite(ballX + (ray_min_radius * Math.cos(radian)),
ballY - (ray_min_radius * Math.sin(radian)),
"Graphics/Battle animations/ballBurst_ray", PictureOrigin::BOTTOM)
ray.setZ(0, 5100)
ray.setZoomXY(0, 200, start_zoom)
ray.setTone(0, variances[0]) if poke_ball != :CHERISHBALL
ray.setOpacity(0, 0)
ray.setVisible(0, false)
ray.setAngle(0, angle)
# Animate ray
start = delay + (i / 2)
ray.setVisible(start, true)
ray.moveZoomXY(start, ray_lifetime, 200, start_zoom * 6)
ray.moveOpacity(start, 2, 255) # Quickly fade in
ray.moveOpacity(start + ray_lifetime - ray_fade_duration, ray_fade_duration, 0) # Fade out
if poke_ball == :CHERISHBALL
ray_lifetime.times do |frame|
ray.setTone(start + frame, cherish_ball_ray_tones[frame % cherish_ball_ray_tones.length])
end
else
ray.moveTone(start + ray_lifetime - ray_fade_duration, ray_fade_duration, variances[1])
end
ray.setVisible(start + ray_lifetime, false)
end
# Particles
num_particles.times do |i|
# Set up particles
particle1 = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_#{variances[5]}", PictureOrigin::CENTER)
particle2 = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_#{variances[2]}", PictureOrigin::CENTER)
[particle1, particle2].each_with_index do |particle, num|
particle.setZ(0, 5110 + num)
particle.setZoom(0, (80 - (num * 20)) / (["ring2"].include?(variances[5 - (3 * num)]) ? 2 : 1))
particle.setTone(0, variances[6 - (3 * num)])
particle.setVisible(0, false)
end
# Animate particles
start = delay + (i / 4)
max_radius = rand(256...384)
angle = rand(360)
radian = angle * Math::PI / 180
[particle1, particle2].each_with_index do |particle, num|
particle.setVisible(start, true)
particle.moveDelta(start, particle_lifetime, max_radius * Math.cos(radian), max_radius * Math.sin(radian))
particle.moveZoom(start, particle_lifetime, 10)
particle.moveTone(start + particle_lifetime - particle_fade_duration,
particle_fade_duration / 2, variances[7 - (3 * num)])
particle.moveOpacity(start + particle_lifetime - particle_fade_duration,
particle_fade_duration,
0) # Fade out at end
particle.setVisible(start + particle_lifetime, false)
end
end
end
# The Poké Ball burst animation used when absorbing a wild Pokémon during a
# capture attempt.
def ballBurstCapture(delay, ball, ballX, ballY, poke_ball)
particle_duration = 10
ring_duration = 5
num_particles = 9
base_angle = 270
base_radius = (poke_ball == :MASTERBALL) ? 192 : 144 # How far out from the Poké Ball the particles go
# Get array of things that vary for each kind of Poké Ball
variances = BALL_BURST_CAPTURE_VARIANCES[poke_ball] || BALL_BURST_CAPTURE_VARIANCES[:POKEBALL]
# Set up glare particles
glare1 = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_#{variances[6]}", PictureOrigin::CENTER)
glare2 = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_#{variances[3]}", PictureOrigin::CENTER)
glare3 = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_#{variances[0]}", PictureOrigin::CENTER)
[glare1, glare2, glare3].each_with_index do |particle, num|
particle.setZ(0, 5100 + num)
particle.setZoom(0, 0)
particle.setTone(0, variances[7 - (3 * num)])
particle.setVisible(0, false)
end
glare2.setOpacity(0, 160)
glare3.setOpacity(0, 160) if poke_ball != :DUSKBALL
# Animate glare particles
[glare1, glare2, glare3].each { |p| p.setVisible(delay, true) }
case poke_ball
when :MASTERBALL
glare1.moveZoom(delay, particle_duration, 1200)
when :DUSKBALL
glare1.moveZoom(delay, particle_duration, 350)
else
glare1.moveZoom(delay, particle_duration, 600)
end
glare1.moveOpacity(delay + (particle_duration / 2), particle_duration / 2, 0)
[glare1, glare2, glare3].each_with_index do |particle, num|
particle.moveTone(delay, particle_duration, variances[8 - (3 * num)])
end
if poke_ball == :DUSKBALL
glare2.moveZoom(delay, particle_duration, 350)
glare3.moveZoom(delay, particle_duration, 500)
[glare2, glare3].each_with_index do |particle, num|
particle.moveOpacity(delay + (particle_duration / 2), particle_duration / 2, 0)
end
else
glare2.moveZoom(delay, particle_duration, (poke_ball == :MASTERBALL) ? 400 : 250)
glare2.moveOpacity(delay + (particle_duration / 2), particle_duration / 3, 0)
glare3.moveZoom(delay, particle_duration, (poke_ball == :MASTERBALL) ? 800 : 500)
glare3.moveOpacity(delay + (particle_duration / 2), particle_duration / 3, 0)
end
[glare1, glare2, glare3].each { |p| p.setVisible(delay + particle_duration, false) }
# Burst particles
num_particles.times do |i|
# Set up particle that keeps moving out
particle1 = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_particle", PictureOrigin::CENTER)
particle1.setZ(0, 5105)
particle1.setZoom(0, 150)
particle1.setOpacity(0, 160)
particle1.setVisible(0, false)
# Set up particles that curve back in
particle2 = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_#{variances[12]}", PictureOrigin::CENTER)
particle3 = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_#{variances[9]}", PictureOrigin::CENTER)
[particle2, particle3].each_with_index do |particle, num|
particle.setZ(0, 5110 + num)
particle.setZoom(0, (poke_ball == :NESTBALL) ? 50 : 0)
particle.setTone(0, variances[13 - (3 * num)])
particle.setVisible(0, false)
particle.setAngle(0, rand(360)) if poke_ball == :PREMIERBALL
end
particle3.setOpacity(0, 128) if poke_ball == :DIVEBALL
# Particle animations
[particle1, particle2, particle3].each { |p| p.setVisible(delay, true) }
particle2.setVisible(delay, false) if poke_ball == :NESTBALL
start_angle = base_angle + (i * 360 / num_particles)
p1_x_offset = base_radius * Math.cos(start_angle * Math::PI / 180)
p1_y_offset = base_radius * Math.sin(start_angle * Math::PI / 180)
particle_duration.times do |j|
index = j + 1
angle = start_angle + (index * (360 / num_particles) / particle_duration)
radian = angle * Math::PI / 180
radius = base_radius
prop = index.to_f / (particle_duration / 2)
prop = 2 - prop if index > particle_duration / 2
radius *= prop
particle1.moveXY(delay + j, 1,
ballX + (p1_x_offset * index * 2 / particle_duration),
ballY - (p1_y_offset * index * 2 / particle_duration))
[particle2, particle3].each do |particle|
particle.moveXY(delay + j, 1,
ballX + (radius * Math.cos(radian)),
ballY - (radius * Math.sin(radian)))
end
end
particle1.moveZoom(delay, particle_duration, 0)
particle1.moveOpacity(delay, particle_duration, 0)
[particle2, particle3].each_with_index do |particle, num|
# Zoom in
if num == 0 && poke_ball == :MASTERBALL
particle.moveZoom(delay, particle_duration / 2, 225)
elsif num == 0 && poke_ball == :DIVEBALL
particle.moveZoom(delay, particle_duration / 2, 125)
elsif ["particle"].include?(variances[12 - (3 * num)])
particle.moveZoom(delay, particle_duration / 2, (poke_ball == :PREMIERBALL) ? 50 : 80)
elsif ["ring3"].include?(variances[12 - (3 * num)])
particle.moveZoom(delay, particle_duration / 2, 50)
elsif ["dazzle", "ring4", "diamond"].include?(variances[12 - (3 * num)])
particle.moveZoom(delay, particle_duration / 2, 60)
else
particle.moveZoom(delay, particle_duration / 2, 100)
end
# Zoom out
if ["particle", "dazzle", "ring3", "ring4", "diamond"].include?(variances[12 - (3 * num)])
particle.moveZoom(delay + (particle_duration * 2 / 3), particle_duration / 3, 10)
else
particle.moveZoom(delay + (particle_duration * 2 / 3), particle_duration / 3, 25)
end
# Rotate (for Premier Ball)
particle.moveAngle(delay, particle_duration, -180) if poke_ball == :PREMIERBALL
# Change tone, fade out
particle.moveTone(delay + (particle_duration / 3), (particle_duration.to_f / 3).ceil, variances[14 - (3 * num)])
particle.moveOpacity(delay + particle_duration - 3, 3, 128) # Fade out at end
end
[particle1, particle2, particle3].each { |p| p.setVisible(delay + particle_duration, false) }
end
# Web sprite (for Net Ball)
if poke_ball == :NETBALL
web = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_web", PictureOrigin::CENTER)
web.setZ(0, 5123)
web.setZoom(0, 120)
web.setOpacity(0, 0)
web.setTone(0, Tone.new(-32, -32, -128))
web.setVisible(0, false)
start = particle_duration / 2
web.setVisible(delay + start, true)
web.moveOpacity(delay + start, 2, 160)
web_duration = particle_duration + ring_duration - (particle_duration / 2)
(web_duration / 4).times do |i|
web.moveZoom(delay + start + (i * 4), 2, 150)
web.moveZoom(delay + start + (i * 4) + 2, 2, 120)
end
now = start + ((web_duration / 4) * 4)
web.moveZoom(delay + now, particle_duration + ring_duration - now, 150)
web.moveOpacity(delay + particle_duration, ring_duration, 0)
web.setVisible(delay + particle_duration + ring_duration, false)
end
# Ring particle
ring = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_ring1", PictureOrigin::CENTER)
ring.setZ(0, 5110)
ring.setZoom(0, 0)
ring.setTone(0, variances[15])
ring.setVisible(0, false)
# Ring particle animation
ring.setVisible(delay + particle_duration, true)
ring.moveZoom(delay + particle_duration - 2, ring_duration + 2, 125) # Start slightly early
ring.moveTone(delay + particle_duration, ring_duration, variances[16])
ring.moveOpacity(delay + particle_duration, ring_duration, 0)
ring.setVisible(delay + particle_duration + ring_duration, false)
# Mark the end of the burst animation
ball.setDelta(delay + particle_duration + ring_duration, 0, 0)
end
# The animation shown over a thrown Poké Ball when it has successfully caught
# a Pokémon.
def ballCaptureSuccess(ball, delay, ballX, ballY)
ball.setSE(delay, "Battle catch click")
ball.moveTone(delay, 4, Tone.new(-128, -128, -128)) # Ball goes darker
delay = ball.totalDuration
star_duration = 12 # In 20ths of a second
y_offsets = [[0, 74, 52], [0, 62, 28], [0, 74, 48]]
3.times do |i| # Left, middle, right
# Set up particle
star = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_star", PictureOrigin::CENTER)
star.setZ(0, 5110)
star.setZoom(0, [50, 50, 33][i])
start_angle = [0, 345, 15][i]
star.setAngle(0, start_angle)
star.setOpacity(0, 0)
star.setVisible(0, false)
# Particle animation
star.setVisible(delay, true)
y_pos = y_offsets[i]
star_duration.times do |j|
index = j + 1
x = 72 * index / star_duration
proportion = index.to_f / star_duration
a = (2 * y_pos[2]) - (4 * y_pos[1])
b = y_pos[2] - a
y = ((a * proportion) + b) * proportion
star.moveXY(delay + j, 1, ballX + ([-1, 0, 1][i] * x), ballY - y)
end
star.moveAngle(delay, star_duration, start_angle + [144, 0, 45][i]) if i.even?
star.moveOpacity(delay, 4, 255) # Fade in
star.moveTone(delay + 3, 3, Tone.new(0, 0, -96)) # Light yellow
star.moveTone(delay + 6, 3, Tone.new(0, 0, 0)) # White
star.moveOpacity(delay + 8, 4, 0) # Fade out
end
end
# The Poké Ball burst animation used when recalling a Pokémon. In HGSS, this
# is the same for all types of Poké Ball except for the color that the battler
# turns - see def getBattlerColorFromPokeBall.
def ballBurstRecall(delay, ball, ballX, ballY, poke_ball)
color_duration = 10 # Change color of battler to a solid shade - see def battlerAbsorb
shrink_duration = 5 # Shrink battler into Poké Ball - see def battlerAbsorb
burst_duration = color_duration + shrink_duration
# Burst particles
num_particles = 5
base_angle = 55
base_radius = 64 # How far out from the Poké Ball the particles go
num_particles.times do |i|
# Set up particle
particle = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_particle", PictureOrigin::CENTER)
particle.setZ(0, 5110)
particle.setZoom(0, 150)
particle.setOpacity(0, 0)
particle.setVisible(0, false)
# Particle animation
particle.setVisible(delay, true)
particle.moveOpacity(delay, 2, 255) # Fade in quickly
burst_duration.times do |j|
angle = base_angle + (i * 360 / num_particles) + (135.0 * j / burst_duration)
radian = angle * Math::PI / 180
radius = base_radius
if j < burst_duration / 5
prop = j.to_f / (color_duration / 3)
radius *= 0.75 + (prop / 4)
elsif j >= burst_duration / 2
prop = (j.to_f - (burst_duration / 2)) / (burst_duration / 2)
radius *= 1 - prop
end
if j == 0
particle.setXY(delay + j, ballX + (radius * Math.cos(radian)), ballY - (radius * Math.sin(radian)))
else
particle.moveXY(delay + j, 1, ballX + (radius * Math.cos(radian)), ballY - (radius * Math.sin(radian)))
end
end
particle.moveZoom(delay, burst_duration, 0)
particle.moveTone(delay + (color_duration / 2), color_duration / 2, Tone.new(0, 0, -192)) # Yellow
particle.moveTone(delay + color_duration, shrink_duration, Tone.new(0, -128, -248)) # Dark orange
particle.moveOpacity(delay + color_duration, shrink_duration, 0) # Fade out at end
particle.setVisible(delay + burst_duration, false)
end
# Ring particles
ring1 = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_ring1", PictureOrigin::CENTER)
ring1.setZ(0, 5110)
ring1.setZoom(0, 0)
ring1.setVisible(0, false)
ring2 = addNewSprite(ballX, ballY, "Graphics/Battle animations/ballBurst_ring2", PictureOrigin::CENTER)
ring2.setZ(0, 5110)
ring2.setVisible(0, false)
# Ring particle animations
ring1.setVisible(delay + burst_duration - 2, true)
ring1.moveZoom(delay + burst_duration - 2, 4, 100)
ring1.setVisible(delay + burst_duration + 2, false)
ring2.setVisible(delay + burst_duration + 2, true)
ring2.moveZoom(delay + burst_duration + 2, 4, 200)
ring2.moveOpacity(delay + burst_duration + 2, 4, 0)
end
end
#===============================================================================
# Shows the battle scene fading in while elements slide around into place
#===============================================================================
class Battle::Scene::Animation::Intro < Battle::Scene::Animation
def createProcesses
appearTime = 20 # This is in 1/20 seconds
# Background
if @sprites["battle_bg2"]
makeSlideSprite("battle_bg", 0.5, appearTime)
makeSlideSprite("battle_bg2", 0.5, appearTime)
end
# Bases
makeSlideSprite("base_0", 1, appearTime, PictureOrigin::BOTTOM)
makeSlideSprite("base_1", -1, appearTime, PictureOrigin::CENTER)
# Player sprite, partner trainer sprite
@battle.player.each_with_index do |_p, i|
makeSlideSprite("player_#{i + 1}", 1, appearTime, PictureOrigin::BOTTOM)
end
# Opposing trainer sprite(s) or wild Pokémon sprite(s)
if @battle.trainerBattle?
@battle.opponent.each_with_index do |_p, i|
makeSlideSprite("trainer_#{i + 1}", -1, appearTime, PictureOrigin::BOTTOM)
end
else # Wild battle
@battle.pbParty(1).each_with_index do |_pkmn, i|
idxBattler = (2 * i) + 1
makeSlideSprite("pokemon_#{idxBattler}", -1, appearTime, PictureOrigin::BOTTOM)
end
end
# Shadows
@battle.battlers.length.times do |i|
makeSlideSprite("shadow_#{i}", (i.even?) ? 1 : -1, appearTime, PictureOrigin::CENTER)
end
# Fading blackness over whole screen
blackScreen = addNewSprite(0, 0, "Graphics/Battle animations/black_screen")
blackScreen.setZ(0, 99999)
blackScreen.moveOpacity(0, 8, 0)
# Fading blackness over command bar
blackBar = addNewSprite(@sprites["cmdBar_bg"].x, @sprites["cmdBar_bg"].y,
"Graphics/Battle animations/black_bar")
blackBar.setZ(0, 99998)
blackBar.moveOpacity(appearTime * 3 / 4, appearTime / 4, 0)
end
end
#===============================================================================
# Shows a Pokémon being sent out on the player's side (including by a partner).
# Includes the Poké Ball being thrown.
#===============================================================================
class Battle::Scene::Animation::PokeballPlayerSendOut < Battle::Scene::Animation
def createProcesses
batSprite = @sprites["pokemon_#{@battler.index}"]
shaSprite = @sprites["shadow_#{@battler.index}"]
traSprite = @sprites["player_#{@idxTrainer}"]
# Calculate the Poké Ball graphic to use
poke_ball = (batSprite.pkmn) ? batSprite.pkmn.poke_ball : nil
# Calculate the color to turn the battler sprite
col = getBattlerColorFromPokeBall(poke_ball)
col.alpha = 255
# Calculate start and end coordinates for battler sprite movement
ballPos = Battle::Scene.pbBattlerPosition(@battler.index, batSprite.sideSize)
battlerStartX = ballPos[0] # Is also where the Ball needs to end
battlerStartY = ballPos[1] # Is also where the Ball needs to end + 18
battlerEndX = batSprite.x
battlerEndY = batSprite.y
# Calculate start and end coordinates for Poké Ball sprite movement
ballStartX = -6
ballStartY = 202
ballMidX = 0 # Unused in trajectory calculation
ballMidY = battlerStartY - 144
# Set up Poké Ball sprite
ball = addBallSprite(ballStartX, ballStartY, poke_ball)
ball.setZ(0, 1025)
ball.setVisible(0, false)
# Poké Ball tracking the player's hand animation (if trainer is visible)
if @showingTrainer && traSprite && traSprite.x > 0
ball.setZ(0, traSprite.z - 1)
ballStartX, ballStartY = ballTracksHand(ball, traSprite)
end
delay = ball.totalDuration # 0 or 7
# Poké Ball trajectory animation
createBallTrajectory(ball, delay, 12,
ballStartX, ballStartY, ballMidX, ballMidY, battlerStartX, battlerStartY - 18)
ball.setZ(9, batSprite.z - 1)
delay = ball.totalDuration + 4
delay += 10 * @idxOrder # Stagger appearances if multiple Pokémon are sent out at once
ballOpenUp(ball, delay - 2, poke_ball)
ballBurst(delay, ball, battlerStartX, battlerStartY - 18, poke_ball)
ball.moveOpacity(delay + 2, 2, 0)
# Set up battler sprite
battler = addSprite(batSprite, PictureOrigin::BOTTOM)
battler.setXY(0, battlerStartX, battlerStartY)
battler.setZoom(0, 0)
battler.setColor(0, col)
# Battler animation
battlerAppear(battler, delay, battlerEndX, battlerEndY, batSprite, col)
if @shadowVisible
# Set up shadow sprite
shadow = addSprite(shaSprite, PictureOrigin::CENTER)
shadow.setOpacity(0, 0)
# Shadow animation
shadow.setVisible(delay, @shadowVisible)
shadow.moveOpacity(delay + 5, 10, 255)
end
end
end
#===============================================================================
# Shows the player throwing a Poké Ball and it being deflected
#===============================================================================
class Battle::Scene::Animation::PokeballThrowDeflect < Battle::Scene::Animation
def createProcesses
# Calculate start and end coordinates for battler sprite movement
batSprite = @sprites["pokemon_#{@battler.index}"]
ballPos = Battle::Scene.pbBattlerPosition(@battler.index, batSprite.sideSize)
ballStartX = -6
ballStartY = 246
ballMidX = 190 # Unused in arc calculation
ballMidY = 78
ballEndX = ballPos[0]
ballEndY = 112
# Set up Poké Ball sprite
ball = addBallSprite(ballStartX, ballStartY, @poke_ball)
ball.setZ(0, 5090)
# Poké Ball arc animation
ball.setSE(0, "Battle throw")
createBallTrajectory(ball, 0, 16,
ballStartX, ballStartY, ballMidX, ballMidY, ballEndX, ballEndY)
# Poké Ball knocked back
delay = ball.totalDuration
ball.setSE(delay, "Battle ball drop")
ball.moveXY(delay, 8, -32, Graphics.height - 96 + 32) # Back to player's corner
createBallTumbling(ball, delay, 8)
end
end

View File

@@ -0,0 +1,26 @@
#===============================================================================
#
#===============================================================================
class AnimationPlayer::FakeBattler
attr_reader :index
attr_reader :pokemon
def initialize(index, species, form = 0, gender = 0)
@index = index
@pokemon = AnimationPlayer::FakePokemon.new(species, form, gender)
end
end
#===============================================================================
#
#===============================================================================
class AnimationPlayer::FakePokemon
attr_reader :species, :form, :gender
def initialize(species, form = 0, gender = 0)
# NOTE: species will be a string, but it doesn't need to be a symbol.
@species = species
@form = form
@gender = gender
end
end