diff --git a/Data/Scripts/901_GameData/Animation.rb b/Data/Scripts/901_GameData/Animation.rb index 67622c8d0..79b8ebf1e 100644 --- a/Data/Scripts/901_GameData/Animation.rb +++ b/Data/Scripts/901_GameData/Animation.rb @@ -19,7 +19,7 @@ module GameData "Common" => :common, "OppCommon" => :opp_common}], "Name" => [:name, "s"], # TODO: Target (Screen, User, UserAndTarget, etc. Determines which focuses - # a particle can be given). + # a particle can be given and whether "Target" particle exists). # TODO: DamageFrame (keyframe at which the battle continues, i.e. damage # animations start playing). "Flags" => [:flags, "*s"], @@ -56,6 +56,13 @@ module GameData "MoveZoomY" => [:zoom_y, "^uuu"], "SetAngle" => [:angle, "^ui"], "MoveAngle" => [:angle, "^uui"], + # TODO: Remember that :visible defaults to false at the beginning for a + # particle, and becomes true automatically when the first command + # happens for that particle. For "User" and "Target", it defaults to + # true at the beginning instead. This affects the display of the + # particle's timeline and canvas sprite in the editor, as well as + # the animation player. + "SetVisible" => [:visible, "^ub"], "SetOpacity" => [:opacity, "^uu"], "MoveOpacity" => [:opacity, "^uuu"] # TODO: SetPriority should be an enum. There should also be a property @@ -69,6 +76,23 @@ module GameData # validate_compiled_animation like the "SE" particle does with the # "Play"-type commands. } + PARTICLE_DEFAULT_VALUES = { +# :name => "", + :focus => :screen + } + PARTICLE_KEYFRAME_DEFAULT_VALUES = { + :graphic => nil, + :frame => 0, + :blending => 0, + :flip => false, + :x => 0, + :y => 0, + :zoom_x => 100, + :zoom_y => 100, + :angle => 0, + :visible => false, + :opacity => 255 + } @@cmd_to_pbs_name = nil # USed for writing animation PBS files @@ -133,15 +157,17 @@ module GameData new_p = {} particle.each_pair do |key, val| if val.is_a?(Array) - new_p[val] = [] - val.each { |cmd| new_p[val].push(cmd.clone) } + 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 move_animation? @@ -197,7 +223,6 @@ module GameData next if !val.is_a?(Array) val.each do |cmd| new_cmd = cmd.clone - new_cmd.insert(1, 0) if @@cmd_to_pbs_name[key].length == 1 # "SetXYZ" only if new_cmd[1] > 0 ret.push([@@cmd_to_pbs_name[key][1]] + new_cmd) # ["MoveXYZ", keyframe, duration, value] else diff --git a/Data/Scripts/902_Anim compiler/anim pbs compiler.rb b/Data/Scripts/902_Anim compiler/anim pbs compiler.rb index 0cca9c250..c77aa3da6 100644 --- a/Data/Scripts/902_Anim compiler/anim pbs compiler.rb +++ b/Data/Scripts/902_Anim compiler/anim pbs compiler.rb @@ -96,34 +96,22 @@ module Compiler hash[:type] = hash[:id][0] hash[:move] = hash[:id][1] hash[:version] = hash[:id][2] || 0 + # TODO: raise if "Target" particle exists but animation's target doesn't + # involve a target battler. + # Create "User" and "SE" particles if they don't exist + if hash[:particles].none? { |particle| particle[:name] == "User" } + hash[:particles].push({:name => "User"}) + end + if hash[:particles].none? { |particle| particle[:name] == "SE" } + hash[:particles].push({:name => "SE"}) + end + # TODO: Create "Target" particle if it doesn't exist and animation's target + # involves a target battler. # Go through each particle in turn hash[:particles].each do |particle| - # Convert all "SetXYZ" particle commands to "MoveXYZ" by giving them a - # duration of 0 - [:frame, :x, :y, :zoom_x, :zoom_y, :angle, :opacity].each do |prop| - next if !particle[prop] - particle[prop].each do |cmd| - cmd.insert(1, 0) if cmd.length == 2 - 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] } - # TODO: Find any overlapping particle commands and raise an error. - end - # Ensure valid values for "SetBlending" commands - if particle[:blending] - particle[:blending].each do |blend| - next if blend[1] <= 2 - raise _INTL("Invalid blend value: {1} (must be 0, 1 or 2).\n{2}", - blend[1], FileLineData.linereport) - end - end # TODO: Ensure "Play", "PlayUserCry", "PlayTargetCry" are exclusively used # by the particle "SE", and that the "SE" particle can only use # those commands. Raise if problems found. - # Ensure all particles have a default focus if not given if !particle[:focus] if particle[:name] == "User" @@ -134,10 +122,47 @@ module Compiler particle[:focus] = :screen end end - # TODO: Depending on hash[:target], ensure all particles have an # appropriate focus (i.e. can't be :user_and_target if hash[:target] # doesn't include a target). Raise if problems found. + + # 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 + 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] } + # 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.\n{2}", + key.to_s.capitalize, FileLineData.linereport) + end + if particle[:name] != "SE" && cmd[1] == 0 && last_set_frame >= cmd[0] + raise _INTL("Animation has multiple \"Set\" commands in the same keyframe for the {1} property.\n{2}", + key.to_s.capitalize, 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 diff --git a/Data/Scripts/905_New controls/001 basic control.rb b/Data/Scripts/905_New controls/001 basic control.rb index 4ef26e5dc..dd0e3ebc2 100644 --- a/Data/Scripts/905_New controls/001 basic control.rb +++ b/Data/Scripts/905_New controls/001 basic control.rb @@ -134,7 +134,6 @@ class UIControls::BaseControl < BitmapSprite end end - # Returns whether this control has been properly decaptured. def on_mouse_release @captured_area = nil invalidate @@ -167,6 +166,7 @@ class UIControls::BaseControl < BitmapSprite # Updates the logic on the control, invalidating it if necessary. def update + return if !self.visible # TODO: Disabled control stuff. # return if self.disabled diff --git a/Data/Scripts/905_New controls/003 checkbox.rb b/Data/Scripts/905_New controls/003 checkbox.rb index 103f74b9e..06bddaf97 100644 --- a/Data/Scripts/905_New controls/003 checkbox.rb +++ b/Data/Scripts/905_New controls/003 checkbox.rb @@ -15,9 +15,9 @@ class UIControls::Checkbox < UIControls::BaseControl @value = value end - def value=(val) - return if @value == val - @value = val + def value=(new_value) + return if @value == new_value + @value = new_value invalidate end diff --git a/Data/Scripts/905_New controls/004 text box.rb b/Data/Scripts/905_New controls/004 text box.rb index 20fa86850..8b615fb43 100644 --- a/Data/Scripts/905_New controls/004 text box.rb +++ b/Data/Scripts/905_New controls/004 text box.rb @@ -267,6 +267,7 @@ class UIControls::TextBox < UIControls::BaseControl end def update + return if !self.visible super # TODO: Disabled control stuff. # return if self.disabled diff --git a/Data/Scripts/905_New controls/005 number slider.rb b/Data/Scripts/905_New controls/005 number slider.rb index 2e2d1e867..1e8e7499d 100644 --- a/Data/Scripts/905_New controls/005 number slider.rb +++ b/Data/Scripts/905_New controls/005 number slider.rb @@ -106,6 +106,7 @@ class UIControls::Slider < UIControls::BaseControl end def update + return if !self.visible super # TODO: Disabled control stuff. # return if self.disabled diff --git a/Data/Scripts/905_New controls/006 value box.rb b/Data/Scripts/905_New controls/006 value box.rb index 14fd5e9a6..1742a7e32 100644 --- a/Data/Scripts/905_New controls/006 value box.rb +++ b/Data/Scripts/905_New controls/006 value box.rb @@ -122,6 +122,7 @@ class UIControls::ValueBox < UIControls::TextBox end def update + return if !self.visible super case @captured_area when :minus diff --git a/Data/Scripts/905_New controls/007 button.rb b/Data/Scripts/905_New controls/007 button.rb index e84d2acae..c6882ac0c 100644 --- a/Data/Scripts/905_New controls/007 button.rb +++ b/Data/Scripts/905_New controls/007 button.rb @@ -29,6 +29,16 @@ class UIControls::Button < UIControls::BaseControl } end + def set_changed + @value = true + super + end + + def clear_changed + @value = false + super + end + #----------------------------------------------------------------------------- def refresh diff --git a/Data/Scripts/905_New controls/008_list.rb b/Data/Scripts/905_New controls/008_list.rb index 5f68a98e6..020295306 100644 --- a/Data/Scripts/905_New controls/008_list.rb +++ b/Data/Scripts/905_New controls/008_list.rb @@ -33,6 +33,7 @@ class UIControls::List < UIControls::BaseControl else self.top_row = 0 end + self.selected = -1 if @selected >= @values.length invalidate end @@ -202,6 +203,7 @@ class UIControls::List < UIControls::BaseControl end def update + return if !self.visible super # TODO: Disabled control stuff. # return if self.disabled diff --git a/Data/Scripts/906_New controls container/001 controls container.rb b/Data/Scripts/906_New controls container/001 controls container.rb index fe558b466..00ecabd9c 100644 --- a/Data/Scripts/906_New controls container/001 controls container.rb +++ b/Data/Scripts/906_New controls container/001 controls container.rb @@ -16,6 +16,8 @@ class UIControls::ControlsContainer attr_reader :x, :y attr_reader :controls + attr_reader :values + attr_reader :visible LINE_SPACING = 32 OFFSET_FROM_LABEL_X = 80 @@ -32,6 +34,7 @@ class UIControls::ControlsContainer @control_rects = [] @row_count = 0 @captured = nil + @visible = true end def dispose @@ -40,6 +43,20 @@ class UIControls::ControlsContainer @viewport.dispose end + def changed? + return !@values.nil? + end + + def clear_changed + @values = nil + end + + def visible=(value) + @visible = value + @controls.each { |c| c[1].visible = value } + repaint if @visible + end + #----------------------------------------------------------------------------- def add_label(id, label, has_label = false) @@ -110,6 +127,7 @@ class UIControls::ControlsContainer #----------------------------------------------------------------------------- def update + return if !@visible # Update controls if @captured # TODO: Ideally all controls will be updated here, if only to redraw @@ -128,8 +146,8 @@ class UIControls::ControlsContainer # Check for updated controls @controls.each do |ctrl| next if !ctrl[1].changed? - # TODO: Get the new value from ctrl and put it in a hash for the main - # editor class to notice and use. + @values ||= {} + @values[ctrl[0]] = ctrl[1].value ctrl[1].clear_changed end # Redraw controls if needed diff --git a/Data/Scripts/910_New anim editor/001_anim selection.rb b/Data/Scripts/910_New anim editor/001_anim selection.rb index 71691a440..cd8c2d6e8 100644 --- a/Data/Scripts/910_New anim editor/001_anim selection.rb +++ b/Data/Scripts/910_New anim editor/001_anim selection.rb @@ -29,14 +29,22 @@ class AnimationEditorLoadScreen @viewport.dispose end + # TODO: Make separate arrays for move and common animations. Group animations + # for the same move/common animation together somehow to be listed in + # the main list - individual animations are shown in the secondary list. + # The display names will need improving accordingly. Usage of + # @animations in this class will need redoing. def generate_list @animations = [] - # TODO: Look through GameData to populate @animations; below is temporary. - # There will be separate arrays for move animations, common animations - # and overworld animations. The move animations one will primarily be - # a list of moves that have any animations, with the actual GameData - # animations being in a sub-array for each move. - 67.times { |i| @animations.push([i, "Animation #{i + 1}"]) } + GameData::Animation.keys.each do |id| + anim = GameData::Animation.get(id) + if anim.version > 0 + name = "#{anim.type}: #{anim.move} (#{anim.version}) - #{anim.name}" + else + name = "#{anim.type}: #{anim.move} - #{anim.name}" + end + @animations.push([id, name]) + end end def draw_editor_background @@ -53,7 +61,8 @@ class AnimationEditorLoadScreen @screen_bitmap.bitmap.outline_rect(area[0] - 2, area[1] - 2, area[2] + 4, area[3] + 4, Color.black) @screen_bitmap.bitmap.outline_rect(area[0] - 1, area[1] - 1, area[2] + 2, area[3] + 2, Color.white) # Fill the area with white -# @screen_bitmap.bitmap.fill_rect(area[0], area[1], area[2], area[3], Color.white) + # TODO: This line was quoted out previously, and I'm not sure why. + @screen_bitmap.bitmap.fill_rect(area[0], area[1], area[2], area[3], Color.white) end end @@ -74,7 +83,7 @@ class AnimationEditorLoadScreen # change to the text box's value. Perhaps it should only do so after # 0.5 seconds of non-typing. What exactly should the filter be applied # to? Animation's name, move's name (if there is one), what else? - # TODO: Filter dropdown list to pick a type? Other filter options? + # TODO: Filter dropdown list to pick a move type? Other filter options? # "Load animation" button @load_button = UIControls::Button.new(LOAD_BUTTON_WIDTH, LOAD_BUTTON_HEIGHT, @viewport, "Load animation") @load_button.x = LOAD_BUTTON_X @@ -118,21 +127,22 @@ class AnimationEditorLoadScreen Graphics.update Input.update update + # Open editor with animation if @load_animation_id - # Open editor with animation - # TODO: Add animation to be edited as an argument. This will be - # GameData::Animation.get(@load_animation_id).to_hash. - echoln "Anim number #{@load_animation_id}: #{@animations[@load_animation_id][1]}" - screen = AnimationEditor.new + screen = AnimationEditor.new(@load_animation_id, GameData::Animation.get(@load_animation_id).clone_as_hash) screen.run @load_animation_id = nil - # TODO: Regenerate @animations in case the edited animation changed its - # name/move/version. Reapply @animations to @list and the sublist - # (this should invalidate them). + # Refresh list of animations, in case the edited one changed its type, + # move, version or name + generate_list + @list.values = @animations repaint - elsif !inputting_text - break if Input.trigger?(Input::BACK) + next end + # Typing text into a text box; don't want key presses to trigger anything + next if inputting_text + # Inputs + break if Input.trigger?(Input::BACK) end dispose end diff --git a/Data/Scripts/910_New anim editor/010 editor scene.rb b/Data/Scripts/910_New anim editor/010 editor scene.rb index ccc7b2fcf..c02981fd0 100644 --- a/Data/Scripts/910_New anim editor/010 editor scene.rb +++ b/Data/Scripts/910_New anim editor/010 editor scene.rb @@ -9,24 +9,37 @@ # TODO: Remove the particle named "Target" if the animation's focus is changed # to one that doesn't include a target, and vice versa. Do the same for # "User". +# TODO: Things that need pop-up windows (draws a semi-transparent grey over the +# whole screen behind the window): +# - graphic picker +# - SE file picker +# - animation properties (Move/OppMove/Common/OppCommon, move, version, +# extra name, target, filepath, flags, etc.) +# - editor settings (theme, canvas BG graphics, user/target graphics, +# display of canvas particle boxes, etc.) +# TODO: While playing the animation, draw a semi-transparent grey over the +# screen except for the canvas and playback controls. Can't edit anything +# while it's playing. #=============================================================================== class AnimationEditor WINDOW_WIDTH = AnimationEditorLoadScreen::WINDOW_WIDTH WINDOW_HEIGHT = AnimationEditorLoadScreen::WINDOW_HEIGHT - CANVAS_X = 4 - CANVAS_Y = 32 + 4 - CANVAS_WIDTH = Settings::SCREEN_WIDTH - CANVAS_HEIGHT = Settings::SCREEN_HEIGHT - SIDE_PANEL_X = CANVAS_X + CANVAS_WIDTH + 4 + 4 - SIDE_PANEL_Y = CANVAS_Y - SIDE_PANEL_WIDTH = WINDOW_WIDTH - SIDE_PANEL_X - 4 - SIDE_PANEL_HEIGHT = CANVAS_HEIGHT + (32 * 2) + BORDER_THICKNESS = 4 + CANVAS_X = BORDER_THICKNESS + CANVAS_Y = 32 + BORDER_THICKNESS + CANVAS_WIDTH = Settings::SCREEN_WIDTH + CANVAS_HEIGHT = Settings::SCREEN_HEIGHT + SIDE_PANE_X = CANVAS_X + CANVAS_WIDTH + (BORDER_THICKNESS * 2) + SIDE_PANE_Y = CANVAS_Y + SIDE_PANE_WIDTH = WINDOW_WIDTH - SIDE_PANE_X - BORDER_THICKNESS + SIDE_PANE_HEIGHT = CANVAS_HEIGHT + (32 * 2) - # TODO: Add a parameter which is the animation to be edited, and also a - # parameter for that animation's ID in GameData (just for the sake of - # saving changes over the same GameData slot). - def initialize + def initialize(anim_id, anim) + @anim_id = anim_id + @anim = anim + @keyframe = 0 + @particle = -1 @viewport = Viewport.new(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT) @viewport.z = 99999 @screen_bitmap = BitmapSprite.new(WINDOW_WIDTH, WINDOW_HEIGHT, @viewport) @@ -36,18 +49,75 @@ class AnimationEditor @canvas.x = CANVAS_X @canvas.y = CANVAS_Y @canvas.bitmap = RPG::Cache.load_bitmap("Graphics/Battlebacks/", "field_bg") - # Side pane - @side_pane = ControlPane.new(SIDE_PANEL_X, SIDE_PANEL_Y, SIDE_PANEL_WIDTH, SIDE_PANEL_HEIGHT) - set_side_panel_contents + # Side panes + @keyframe_particle_pane = ControlPane.new(SIDE_PANE_X, SIDE_PANE_Y, SIDE_PANE_WIDTH, SIDE_PANE_HEIGHT) + # TODO: Make more side panes for: + # - colour/tone editor (accessed from keyframe_particle_pane via a + # button; has Apply/Cancel buttons to only apply all its values at + # the end of editing them, although canvas will be updated in real + # time to show the changes) + # - particle properties (that don't change during the animation; name, + # focus...) + # - SE particle properties (depends on keyframe) + # - effects particle properties (depends on keyframe; for screen + # shake, etc.) + # - keyframe properties (shift all later particle commands forward/ + # backward). + set_side_panes_contents + refresh end def dispose @screen_bitmap.dispose @canvas.dispose - @side_pane.dispose + @keyframe_particle_pane.dispose @viewport.dispose end + #----------------------------------------------------------------------------- + + def set_keyframe_particle_pane_contents + # TODO: Move these properties to a new side pane for particle properties + # (ones that don't change during the animation). + @keyframe_particle_pane.add_labelled_text_box(:name, "Name", "Untitled") + # @keyframe_particle_pane.add_labelled_dropdown_list(:focus, "Focus", { + # :user => "User", + # :target => "Target", + # :user_and_target => "User and target", + # :screen => "Screen" + # }, :user) + + # TODO: Make sure the IDs for these controls all match up to particle + # properties that can change during the animation. + @keyframe_particle_pane.add_labelled_value_box(:x, "X", -128, CANVAS_WIDTH + 128, 64) + @keyframe_particle_pane.add_labelled_value_box(:y, "Y", -128, CANVAS_HEIGHT + 128, 96) + @keyframe_particle_pane.add_labelled_value_box(:zoom_x, "Zoom X", 0, 1000, 100) + @keyframe_particle_pane.add_labelled_value_box(:zoom_y, "Zoom Y", 0, 1000, 100) + @keyframe_particle_pane.add_labelled_checkbox(:visible, "Visible", true) + @keyframe_particle_pane.add_labelled_slider(:opacity, "Opacity", 0, 255, 255) + @keyframe_particle_pane.add_labelled_value_box(:angle, "Angle", -1080, 1080, 0) + @keyframe_particle_pane.add_labelled_checkbox(:flip, "Flip", false) + @keyframe_particle_pane.add_labelled_dropdown_list(:priority, "Priority", { # TODO: Include sub-priority. + :behind_all => "Behind all", + :behind_user => "Behind user", + :above_user => "In front of user", + :above_all => "In front of everything" + }, :above_user) + @keyframe_particle_pane.add_labelled_button(:color, "Color", "Edit") + @keyframe_particle_pane.add_labelled_button(:tone, "Tone", "Edit") + @keyframe_particle_pane.add_labelled_button(:graphic, "Graphic", "Change") + # :frame (related to graphic) + # :blending + # TODO: Add buttons that shift all commands from the current keyframe and + # later forwards/backwards in time? + end + + def set_side_panes_contents + set_keyframe_particle_pane_contents + end + + #----------------------------------------------------------------------------- + def draw_editor_background # Fill the whole screen with black @screen_bitmap.bitmap.fill_rect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, Color.black) @@ -55,50 +125,121 @@ class AnimationEditor @screen_bitmap.bitmap.outline_rect(CANVAS_X - 3, CANVAS_Y - 3, CANVAS_WIDTH + 6, CANVAS_HEIGHT + 6, Color.white) @screen_bitmap.bitmap.outline_rect(CANVAS_X - 2, CANVAS_Y - 2, CANVAS_WIDTH + 4, CANVAS_HEIGHT + 4, Color.black) @screen_bitmap.bitmap.outline_rect(CANVAS_X - 1, CANVAS_Y - 1, CANVAS_WIDTH + 2, CANVAS_HEIGHT + 2, Color.white) - # Outline around side panel - @screen_bitmap.bitmap.outline_rect(SIDE_PANEL_X - 3, SIDE_PANEL_Y - 3, SIDE_PANEL_WIDTH + 6, SIDE_PANEL_HEIGHT + 6, Color.white) - @screen_bitmap.bitmap.outline_rect(SIDE_PANEL_X - 2, SIDE_PANEL_Y - 2, SIDE_PANEL_WIDTH + 4, SIDE_PANEL_HEIGHT + 4, Color.black) - @screen_bitmap.bitmap.outline_rect(SIDE_PANEL_X - 1, SIDE_PANEL_Y - 1, SIDE_PANEL_WIDTH + 2, SIDE_PANEL_HEIGHT + 2, Color.white) - # Fill the side panel with white - @screen_bitmap.bitmap.fill_rect(SIDE_PANEL_X, SIDE_PANEL_Y, SIDE_PANEL_WIDTH, SIDE_PANEL_HEIGHT, Color.white) + # Outline around side pane + @screen_bitmap.bitmap.outline_rect(SIDE_PANE_X - 3, SIDE_PANE_Y - 3, SIDE_PANE_WIDTH + 6, SIDE_PANE_HEIGHT + 6, Color.white) + @screen_bitmap.bitmap.outline_rect(SIDE_PANE_X - 2, SIDE_PANE_Y - 2, SIDE_PANE_WIDTH + 4, SIDE_PANE_HEIGHT + 4, Color.black) + @screen_bitmap.bitmap.outline_rect(SIDE_PANE_X - 1, SIDE_PANE_Y - 1, SIDE_PANE_WIDTH + 2, SIDE_PANE_HEIGHT + 2, Color.white) + # Fill the side pane with white + @screen_bitmap.bitmap.fill_rect(SIDE_PANE_X, SIDE_PANE_Y, SIDE_PANE_WIDTH, SIDE_PANE_HEIGHT, Color.white) end - def set_side_panel_contents - @side_pane.add_labelled_text_box(:name, "Name", "Untitled") - @side_pane.add_labelled_value_box(:x, "X", -128, CANVAS_WIDTH + 128, 64) - @side_pane.add_labelled_value_box(:y, "Y", -128, CANVAS_HEIGHT + 128, 96) - @side_pane.add_labelled_value_box(:zoom_x, "Zoom X", 0, 1000, 100) - @side_pane.add_labelled_value_box(:zoom_y, "Zoom Y", 0, 1000, 100) - @side_pane.add_labelled_value_box(:angle, "Angle", -1080, 1080, 0) - @side_pane.add_labelled_checkbox(:visible, "Visible", true) - @side_pane.add_labelled_slider(:opacity, "Opacity", 0, 255, 255) - @side_pane.add_labelled_checkbox(:flip, "Flip", false) - @side_pane.add_labelled_dropdown_list(:priority, "Priority", { # TODO: Include sub-priority. - :behind_all => "Behind all", - :behind_user => "Behind user", - :above_user => "In front of user", - :above_all => "In front of everything" - }, :above_user) -# @side_pane.add_labelled_dropdown_list(:focus, "Focus", { -# :user => "User", -# :target => "Target", -# :user_and_target => "User and target", -# :screen => "Screen" -# }, :user) - @side_pane.add_labelled_button(:color, "Color/tone", "Edit") - @side_pane.add_labelled_button(:graphic, "Graphic", "Change") + def get_keyframe_particle_value(particle, frame, property) + 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] = lerp(ret[0], cmd[2], cmd[1], cmd[0], frame).to_i + 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 + if first_cmd < 0 + particle.each_pair do |prop, value| + next if !value.is_a?(Array) || value.length == 0 + 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 + 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, frame, prop) + end + return ret + end + + def refresh_keyframe_particle_pane + if @particle < 0 || !@anim[:particles][@particle] + @keyframe_particle_pane.visible = false + else + @keyframe_particle_pane.visible = true + new_vals = get_all_keyframe_particle_values(@anim[:particles][@particle], @keyframe) + # TODO: Need to do something special for :color, :tone and :graphic/:frame + # which all have button controls. + @keyframe_particle_pane.controls.each do |ctrl| + next if !new_vals.include?(ctrl[0]) + ctrl.value = new_vals[ctrl[0]][0] + # TODO: new_vals[ctrl[0]][1] is whether the value is being interpolated, + # which should be indicated somehow in ctrl. + end + end + end + + def refresh + # Set all side pane controls to values from animation + refresh_keyframe_particle_pane + end + + #----------------------------------------------------------------------------- + + def update_canvas + @canvas.update + # TODO: Detect and apply changes made in canvas, e.g. moving particle, + # double-clicking to add particle, deleting particle. + end + + def update_keyframe_particle_pane + @keyframe_particle_pane.update + if @keyframe_particle_pane.changed? + # TODO: Make undo/redo snapshot. + values = @keyframe_particle_pane.values + # TODO: Apply vals to the animation data, unless the changed control is a + # button (its value will be true), in which case run some special + # code. Maybe this special code should be passed to/run in the + # control as a proc instead, and the button control can be given a + # value like any other control? Probably not. + echoln values + if values[:color] + elsif values[:tone] + elsif values[:graphic] + end + @keyframe_particle_pane.clear_changed + end end def update - @canvas.update - @side_pane.update - # TODO: Check @side_pane for whether it's changed. Note that it includes - # buttons which won't themselves have a value but will flag themselves - # as changed when clicked; code here should determine what happens if - # a button is pressed (unless I put said code in a proc passed to the - # button control; said code will be lengthy). + update_canvas + update_keyframe_particle_pane end + #----------------------------------------------------------------------------- + def run Input.text_input = false loop do