From 340983e76513a8435a31d99494346b3a07e44e7c Mon Sep 17 00:00:00 2001 From: Maruno17 Date: Mon, 23 Oct 2023 22:36:43 +0100 Subject: [PATCH] Fleshing out animation editor's code --- Data/Scripts/901_GameData/Animation.rb | 188 +++++---- .../902_Anim compiler/anim pbs compiler.rb | 75 ++-- .../905_New controls/001 basic control.rb | 2 +- Data/Scripts/905_New controls/002 label.rb | 2 +- Data/Scripts/905_New controls/004 text box.rb | 5 +- .../905_New controls/005 number slider.rb | 2 +- Data/Scripts/905_New controls/008_list.rb | 6 +- .../Scripts/905_New controls/101_scrollbar.rb | 23 +- .../001 controls container.rb | 13 +- .../910_New anim editor/001_anim selection.rb | 3 +- .../910_New anim editor/010 editor scene.rb | 361 ++++++++++++++---- .../910_New anim editor/011_particle list.rb | 303 +++++++++++---- .../910_New anim editor/012_play controls.rb | 27 ++ .../090 particle data helper.rb | 82 +++- 14 files changed, 810 insertions(+), 282 deletions(-) create mode 100644 Data/Scripts/910_New anim editor/012_play controls.rb diff --git a/Data/Scripts/901_GameData/Animation.rb b/Data/Scripts/901_GameData/Animation.rb index 3fffe208f..52399a334 100644 --- a/Data/Scripts/901_GameData/Animation.rb +++ b/Data/Scripts/901_GameData/Animation.rb @@ -1,97 +1,141 @@ 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 - attr_reader :version # Hit number - attr_reader :name # Shown in the sublist; cosmetic only - # TODO: Boolean for not played if target is on user's side. - attr_reader :particles + 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_target # Whether there is no "Target" particle (false by default) attr_reader :flags - attr_reader :pbs_path # Whole path minus "PBS/Animations/" at start and ".txt" at end + 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 + INTERPOLATION_TYPES = { + "None" => :none, + "Linear" => :linear, + "EaseIn" => :ease_in, + "EaseOut" => :ease_out, + "EaseBoth" => :ease_both + } + + # Properties that apply to the animation in general, not to individual + # particles. They don't change during the animation. SCHEMA = { # TODO: Add support for overworld animations. "SectionName" => [:id, "esU", {"Move" => :move, "OppMove" => :opp_move, "Common" => :common, "OppCommon" => :opp_common}], "Name" => [:name, "s"], - # TODO: Target (Screen, User, UserAndTarget, etc. Determines which focuses - # a particle can be given and whether "Target" particle exists). Or - # InvolvesTarget boolean (user and screen will always exist). + "NoTarget" => [:no_target, "b"], + # TODO: Boolean for whether the animation will be played if the target is + # on the same side as the user. # TODO: DamageFrame (keyframe at which the battle continues, i.e. damage # animations start playing). "Flags" => [:flags, "*s"], - "Particle" => [:particles, "s"] + "Particle" => [:particles, "s"] # Is a subheader line like } - # For individual particles. All actions should have "^" in them. + # For individual particles. Any property whose schema begins with "^" can + # change during the animation. # TODO: If more "SetXYZ"/"MoveXYZ" properties are added, ensure the "SetXYZ" # ones are given a duration of 0 in def validate_compiled_animation. # Also add display names to def property_display_name. SUB_SCHEMA = { # These properties cannot be changed partway through the animation. - # TODO: "Name" isn't actually used; the name comes from the subsection - # written between and uses "Particle" above. -# "Name" => [:name, "s"], - "Focus" => [:focus, "e", {"User" => :user, "Target" => :target, - "UserAndTarget" => :user_and_target, "Screen" => :screen}], - # TODO FlipIfFoe, RotateIfFoe kinds of thing. + # NOTE: "Name" isn't a property here, because the particle's name comes + # from the "Particle" property above. + # TODO: If more focus types are added, add ones that involve a target to + # the Compiler's check relating to "NoTarget". + "Graphic" => [:graphic, "s"], + "Focus" => [:focus, "e", {"User" => :user, "Target" => :target, + "UserAndTarget" => :user_and_target, + "Screen" => :screen}], + # TODO: FlipIfFoe, RotateIfFoe kinds of thing. - # All properties below are "Set" or "Move". "Set" has the keyframe and the - # value, and "Move" has the keyframe, duration and the value. All are "^". - # "Set" is turned into "Move" with a duration (second value) of 0. - # TODO: The "MoveXYZ" commands will have optional easing (an enum). - "SetGraphic" => [:graphic, "^us"], - "SetFrame" => [:frame, "^uu"], # Frame within the graphic if it's a spritesheet - "MoveFrame" => [:frame, "^uuu"], - "SetBlending" => [:blending, "^uu"], # 0, 1 or 2 - "SetFlip" => [:flip, "^ub"], - "SetX" => [:x, "^ui"], - "MoveX" => [:x, "^uui"], - "SetY" => [:y, "^ui"], - "MoveY" => [:y, "^uui"], - "SetZoomX" => [:zoom_x, "^uu"], - "MoveZoomX" => [:zoom_x, "^uuu"], - "SetZoomY" => [:zoom_y, "^uu"], - "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. - "SetVisible" => [:visible, "^ub"], - "SetOpacity" => [:opacity, "^uu"], - "MoveOpacity" => [:opacity, "^uuu"] - # TODO: SetPriority should be an enum. There should also be a property - # (set and move) for the sub-priority within that priority bracket. -# "SetPriority" - # TODO: Color. - # TODO: Tone. + # All properties below are "SetXYZ" or "MoveXYZ". "SetXYZ" has the + # keyframe and the value, and "MoveXYZ" has the keyframe, duration and the + # value. All are "^". "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], + "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], + # TODO: SetPriority should be an enum (above all, above user, etc.). There + # should also be a property (set and move) for the sub-priority + # within that priority bracket. + # TODO: Add "SetColor"/"SetTone" as shorthand for the above? They'd be + # converted in the Compiler. + # TODO: Bitmap masking. + + # 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 - # TODO: Play, PlayUserCry, PlayTargetCry. # TODO: ScreenShake? Not sure how to work this yet. Edit def # validate_compiled_animation like the "SE" particle does with the # "Play"-type commands. } PARTICLE_DEFAULT_VALUES = { -# :name => "", - :focus => :screen + :name => "", + :graphic => "", + :focus => :screen } + # 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 = { - :graphic => nil, - :frame => 0, - :blending => 0, - :flip => false, - :x => 0, - :y => 0, - :zoom_x => 100, - :zoom_y => 100, - :angle => 0, - :visible => false, - :opacity => 255 + :frame => 0, + :blending => 0, + :flip => false, + :x => 0, + :y => 0, + :zoom_x => 100, + :zoom_y => 100, + :angle => 0, + :visible => false, + :opacity => 255, + :color_red => 255, + :color_green => 255, + :color_blue => 255, + :color_alpha => 0, + :tone_red => 0, + :tone_green => 0, + :tone_blue => 0, + :tone_gray => 0, + :se => nil, + :user_cry => nil, + :target_cry => nil } @@cmd_to_pbs_name = nil # USed for writing animation PBS files @@ -135,13 +179,14 @@ module GameData 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] - @particles = hash[:particles] || [] - @flags = hash[:flags] || [] - @pbs_path = hash[:pbs_path] || "#{@type} - #{@move}" + @type = hash[:type] + @move = hash[:move] + @version = hash[:version] || 0 + @name = hash[:name] + @no_target = hash[:no_target] || 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 @@ -223,7 +268,8 @@ module GameData next if !val.is_a?(Array) val.each do |cmd| new_cmd = cmd.clone - if new_cmd[1] > 0 + 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] else ret.push([@@cmd_to_pbs_name[key][0]] + new_cmd) # ["SetXYZ", keyframe, duration, value] diff --git a/Data/Scripts/902_Anim compiler/anim pbs compiler.rb b/Data/Scripts/902_Anim compiler/anim pbs compiler.rb index 7a225ddf1..e572de783 100644 --- a/Data/Scripts/902_Anim compiler/anim pbs compiler.rb +++ b/Data/Scripts/902_Anim compiler/anim pbs compiler.rb @@ -96,59 +96,84 @@ 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 + # 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", "SE" and "Target" particles if they don't exist but should if hash[:particles].none? { |particle| particle[:name] == "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 - # 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| - # 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" - particle[:focus] = :user - elsif particle[:name] == "Target" - particle[:focus] = :target - elsif particle[:name] != "SE" - particle[:focus] = :screen + # 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 - # 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. + # 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] = :screen + end + end + # Ensure that particles don't have a focus involving a target if the + # animation itself doesn't involve a target + if hash[:no_target] && [:target, :user_and_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 # 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 + 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.\n{2}", - key.to_s.capitalize, FileLineData.linereport) + raise _INTL("Animation has overlapping commands for the {1} property.", + key.to_s.capitalize) + "\n" + 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) + 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 diff --git a/Data/Scripts/905_New controls/001 basic control.rb b/Data/Scripts/905_New controls/001 basic control.rb index dd0e3ebc2..9a4e49736 100644 --- a/Data/Scripts/905_New controls/001 basic control.rb +++ b/Data/Scripts/905_New controls/001 basic control.rb @@ -10,7 +10,7 @@ class UIControls::BaseControl < BitmapSprite # attr_accessor :disabled # TODO: Make use of this. TEXT_COLOR = Color.black - TEXT_SIZE = 18 # Default is 22 if size isn't explicitly set + TEXT_SIZE = 18 # Default is 22 if size isn't explicitly set HOVER_COLOR = Color.cyan # For clickable area when hovering over it CAPTURE_COLOR = Color.pink # For area you clicked in but aren't hovering over diff --git a/Data/Scripts/905_New controls/002 label.rb b/Data/Scripts/905_New controls/002 label.rb index dac2d75bc..d306ec532 100644 --- a/Data/Scripts/905_New controls/002 label.rb +++ b/Data/Scripts/905_New controls/002 label.rb @@ -5,7 +5,7 @@ class UIControls::Label < UIControls::BaseControl attr_reader :label LABEL_END_X = 80 - TEXT_OFFSET_Y = 7 + TEXT_OFFSET_Y = 5 def initialize(width, height, viewport, label) super(width, height, viewport) diff --git a/Data/Scripts/905_New controls/004 text box.rb b/Data/Scripts/905_New controls/004 text box.rb index 8b615fb43..da5dd9539 100644 --- a/Data/Scripts/905_New controls/004 text box.rb +++ b/Data/Scripts/905_New controls/004 text box.rb @@ -10,7 +10,7 @@ class UIControls::TextBox < UIControls::BaseControl TEXT_BOX_WIDTH = 172 TEXT_BOX_HEIGHT = 24 TEXT_BOX_PADDING = 4 # Gap between sides of text box and text - TEXT_OFFSET_Y = 7 + TEXT_OFFSET_Y = 5 def initialize(width, height, viewport, value = "") super(width, height, viewport) @@ -201,6 +201,7 @@ class UIControls::TextBox < UIControls::BaseControl @cursor_timer = System.uptime invalidate else + @value.strip! if @value.respond_to?("strip!") set_changed if @initial_value && @value != @initial_value reset_interaction end @@ -220,6 +221,7 @@ class UIControls::TextBox < UIControls::BaseControl 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!") set_changed if @initial_value && @value != @initial_value reset_interaction super # Make this control not busy again @@ -247,6 +249,7 @@ class UIControls::TextBox < UIControls::BaseControl # 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!") set_changed if @initial_value && @value != @initial_value reset_interaction @captured_area = nil diff --git a/Data/Scripts/905_New controls/005 number slider.rb b/Data/Scripts/905_New controls/005 number slider.rb index 1e8e7499d..35c8db6d8 100644 --- a/Data/Scripts/905_New controls/005 number slider.rb +++ b/Data/Scripts/905_New controls/005 number slider.rb @@ -13,7 +13,7 @@ class UIControls::Slider < UIControls::BaseControl SLIDER_LENGTH = 128 PLUS_X = SLIDER_X + SLIDER_LENGTH + SLIDER_PADDING VALUE_X = PLUS_X + PLUS_MINUS_SIZE + 5 - TEXT_OFFSET_Y = 7 + TEXT_OFFSET_Y = 5 # TODO: Is there a better knob design than a big black rectangle? I'd rather # it not be a different colour. diff --git a/Data/Scripts/905_New controls/008_list.rb b/Data/Scripts/905_New controls/008_list.rb index d04c51666..4500cbd69 100644 --- a/Data/Scripts/905_New controls/008_list.rb +++ b/Data/Scripts/905_New controls/008_list.rb @@ -7,9 +7,9 @@ # through a line? Nah. #=============================================================================== class UIControls::List < UIControls::BaseControl - LIST_X = 0 - LIST_Y = 0 - ROW_HEIGHT = 24 + LIST_X = 0 + LIST_Y = 0 + ROW_HEIGHT = 24 TEXT_PADDING_X = 4 TEXT_OFFSET_Y = 3 diff --git a/Data/Scripts/905_New controls/101_scrollbar.rb b/Data/Scripts/905_New controls/101_scrollbar.rb index b267a323f..516b4e267 100644 --- a/Data/Scripts/905_New controls/101_scrollbar.rb +++ b/Data/Scripts/905_New controls/101_scrollbar.rb @@ -4,11 +4,14 @@ # seem to be any lag at the moment with a tall scrollbar. #=============================================================================== class UIControls::Scrollbar < UIControls::BaseControl - SLIDER_WIDTH = 16 - WIDTH_PADDING = 0 - TRAY_COLOR = Color.white - SLIDER_COLOR = Color.black - GRAB_COLOR = HOVER_COLOR # Cyan + SLIDER_WIDTH = 16 + WIDTH_PADDING = 0 + SCROLL_DISTANCE = 16 + TRAY_COLOR = Color.white + SLIDER_COLOR = Color.black + GRAB_COLOR = HOVER_COLOR # Cyan + + attr_reader :slider_top def initialize(x, y, size, viewport, horizontal = false, always_visible = false) if horizontal @@ -138,6 +141,16 @@ class UIControls::Scrollbar < UIControls::BaseControl self.slider_top = @slider_top + ((@tray_size - @slider_size) / 4.0).ceil end end + else + 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 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 d731ed729..bd66060c4 100644 --- a/Data/Scripts/906_New controls container/001 controls container.rb +++ b/Data/Scripts/906_New controls container/001 controls container.rb @@ -19,8 +19,8 @@ class UIControls::ControlsContainer attr_reader :values attr_reader :visible - LINE_SPACING = 32 - OFFSET_FROM_LABEL_X = 80 + LINE_SPACING = 28 + OFFSET_FROM_LABEL_X = 90 OFFSET_FROM_LABEL_Y = 0 def initialize(x, y, width, height) @@ -61,6 +61,15 @@ class UIControls::ControlsContainer repaint if @visible 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) 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 3bfb860fc..789a19ffc 100644 --- a/Data/Scripts/910_New anim editor/001_anim selection.rb +++ b/Data/Scripts/910_New anim editor/001_anim selection.rb @@ -95,7 +95,8 @@ class AnimationEditorLoadScreen @load_button.set_fixed_size @load_button.set_interactive_rects @controls[:load] = @load_button - # TODO: "New animation" button, "Delete animation" button. + # TODO: "New animation" button, "Delete animation" button, "Duplicate + # animation" button. repaint 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 217841a4c..18cd396ae 100644 --- a/Data/Scripts/910_New anim editor/010 editor scene.rb +++ b/Data/Scripts/910_New anim editor/010 editor scene.rb @@ -25,24 +25,32 @@ class AnimationEditor WINDOW_WIDTH = AnimationEditorLoadScreen::WINDOW_WIDTH WINDOW_HEIGHT = AnimationEditorLoadScreen::WINDOW_HEIGHT + TOP_BAR_HEIGHT = 30 + BORDER_THICKNESS = 4 CANVAS_X = BORDER_THICKNESS - CANVAS_Y = 32 + BORDER_THICKNESS + CANVAS_Y = TOP_BAR_HEIGHT + BORDER_THICKNESS CANVAS_WIDTH = Settings::SCREEN_WIDTH CANVAS_HEIGHT = Settings::SCREEN_HEIGHT + + PLAY_CONTROLS_X = CANVAS_X + PLAY_CONTROLS_Y = CANVAS_Y + CANVAS_HEIGHT + (BORDER_THICKNESS * 2) + PLAY_CONTROLS_WIDTH = CANVAS_WIDTH + PLAY_CONTROLS_HEIGHT = 64 - (BORDER_THICKNESS * 2) + 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) - PARTICLE_LIST_X = 0 + SIDE_PANE_HEIGHT = CANVAS_HEIGHT + PLAY_CONTROLS_HEIGHT + (BORDER_THICKNESS * 2) + + PARTICLE_LIST_X = BORDER_THICKNESS PARTICLE_LIST_Y = SIDE_PANE_Y + SIDE_PANE_HEIGHT + (BORDER_THICKNESS * 2) - PARTICLE_LIST_WIDTH = WINDOW_WIDTH - PARTICLE_LIST_HEIGHT = WINDOW_HEIGHT - PARTICLE_LIST_Y + PARTICLE_LIST_WIDTH = WINDOW_WIDTH - (BORDER_THICKNESS * 2) + PARTICLE_LIST_HEIGHT = WINDOW_HEIGHT - PARTICLE_LIST_Y - BORDER_THICKNESS def initialize(anim_id, anim) @anim_id = anim_id @anim = anim - @particle = -1 @viewport = Viewport.new(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT) @viewport.z = 99999 @screen_bitmap = BitmapSprite.new(WINDOW_WIDTH, WINDOW_HEIGHT, @viewport) @@ -53,9 +61,12 @@ class AnimationEditor @canvas.y = CANVAS_Y @canvas.bitmap = RPG::Cache.load_bitmap("Graphics/Battlebacks/", "field_bg") # Side panes - @keyframe_particle_pane = UIControls::ControlsContainer.new(SIDE_PANE_X, SIDE_PANE_Y, SIDE_PANE_WIDTH, SIDE_PANE_HEIGHT) + @commands_pane = UIControls::ControlsContainer.new(SIDE_PANE_X, SIDE_PANE_Y, SIDE_PANE_WIDTH, SIDE_PANE_HEIGHT) + @se_pane = UIControls::ControlsContainer.new(SIDE_PANE_X, SIDE_PANE_Y, SIDE_PANE_WIDTH, SIDE_PANE_HEIGHT) + @particle_pane = UIControls::ControlsContainer.new(SIDE_PANE_X, SIDE_PANE_Y, SIDE_PANE_WIDTH, SIDE_PANE_HEIGHT) + @keyframe_pane = UIControls::ControlsContainer.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 + # - colour/tone editor (accessed from commands_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) @@ -66,6 +77,10 @@ class AnimationEditor # shake, etc.) # - keyframe properties (shift all later particle commands forward/ # backward). + # Play controls + @play_controls = UIControls::AnimationPlayControls.new( + PLAY_CONTROLS_X, PLAY_CONTROLS_Y, PLAY_CONTROLS_WIDTH, PLAY_CONTROLS_HEIGHT, @viewport + ) # Timeline/particle list @particle_list = UIControls::AnimationParticleList.new( PARTICLE_LIST_X, PARTICLE_LIST_Y, PARTICLE_LIST_WIDTH, PARTICLE_LIST_HEIGHT, @viewport @@ -74,13 +89,18 @@ class AnimationEditor @captured = nil set_side_panes_contents set_particle_list_contents + set_play_controls_contents refresh end def dispose @screen_bitmap.dispose @canvas.dispose - @keyframe_particle_pane.dispose + @commands_pane.dispose + @se_pane.dispose + @particle_pane.dispose + @keyframe_pane.dispose + @play_controls.dispose @particle_list.dispose @viewport.dispose end @@ -95,55 +115,93 @@ class AnimationEditor #----------------------------------------------------------------------------- - 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 + def set_commands_pane_contents + # :frame (related to graphic) - If the graphic is user's sprite/target's + # sprite, make this instead a choice of front/back/same as the main sprite/ + # opposite of the main sprite. Probably need two controls in the same space + # and refresh_commands_pane makes the appropriate one visible. + @commands_pane.add_labelled_value_box(:x, _INTL("X"), -128, CANVAS_WIDTH + 128, 64) + @commands_pane.add_labelled_value_box(:y, _INTL("Y"), -128, CANVAS_HEIGHT + 128, 96) + @commands_pane.add_labelled_checkbox(:visible, _INTL("Visible"), true) + @commands_pane.add_labelled_slider(:opacity, _INTL("Opacity"), 0, 255, 255) + @commands_pane.add_labelled_value_box(:zoom_x, _INTL("Zoom X"), 0, 1000, 100) + @commands_pane.add_labelled_value_box(:zoom_y, _INTL("Zoom Y"), 0, 1000, 100) + @commands_pane.add_labelled_value_box(:angle, _INTL("Angle"), -1080, 1080, 0) + @commands_pane.add_labelled_checkbox(:flip, _INTL("Flip"), false) + @commands_pane.add_labelled_dropdown_list(:blending, _INTL("Blending"), { + 0 => _INTL("None"), + 1 => _INTL("Additive"), + 2 => _INTL("Subtractive") + }, 0) + @commands_pane.add_labelled_button(:color_tone, _INTL("Color/Tone"), _INTL("Edit")) + # @commands_pane.add_labelled_dropdown_list(:priority, _INTL("Priority"), { # TODO: Include sub-priority. + # :behind_all => _INTL("Behind all"), + # :behind_user => _INTL("Behind user"), + # :above_user => _INTL("In front of user"), + # :above_all => _INTL("In front of everything") + # }, :above_user) + # :sub_priority +# @commands_pane.add_labelled_button(:masking, _INTL("Masking"), _INTL("Edit")) # TODO: Add buttons that shift all commands from the current keyframe and # later forwards/backwards in time? end + def set_se_pane_contents + # TODO: A list containing all SE files that play this keyframe. Lists SE, + # user cry and target cry. + @se_pane.add_button(:add, _INTL("Add")) + @se_pane.add_button(:edit, _INTL("Edit")) + @se_pane.add_button(:delete, _INTL("Delete")) + end + + def set_particle_pane_contents + # TODO: Name should blacklist certain names ("User", "Target", "SE") and + # should be disabled if the value is one of those. + @particle_pane.add_labelled_text_box(:name, _INTL("Name"), _INTL("Untitled")) + # TODO: Graphic should show the graphic's name alongside a "Change" button. + # New kind of control that is a label plus a button? + @particle_pane.add_labelled_button(:graphic, _INTL("Graphic"), _INTL("Change")) + @particle_pane.add_labelled_dropdown_list(:focus, _INTL("Focus"), { + :user => _INTL("User"), + :target => _INTL("Target"), + :user_and_target => _INTL("User and target"), + :screen => _INTL("Screen") + }, :user) + # FlipIfFoe + # RotateIfFoe + # Delete button (if not "User"/"Target"/"SE") + # Duplicate button + # Shift all command timings by X keyframes (text box and button) + # Move particle up/down the list? + end + + def set_keyframe_pane_contents + @keyframe_pane.add_label(:temp, _INTL("Keyframe pane")) + # TODO: Various command-shifting options. + end + def set_side_panes_contents - set_keyframe_particle_pane_contents + set_commands_pane_contents + set_se_pane_contents + set_particle_pane_contents + set_keyframe_pane_contents end def set_particle_list_contents @particle_list.set_particles(@anim[:particles]) end + def set_play_controls_contents + @play_controls.duration = @particle_list.duration + end + #----------------------------------------------------------------------------- def draw_editor_background # Fill the whole screen with black @screen_bitmap.bitmap.fill_rect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, Color.black) + # Fill the top bar with white + @screen_bitmap.bitmap.fill_rect(0, 0, WINDOW_WIDTH, TOP_BAR_HEIGHT, Color.white) # Outline around canvas @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) @@ -154,22 +212,31 @@ class AnimationEditor @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) + # Outline around play controls + @screen_bitmap.bitmap.outline_rect(PLAY_CONTROLS_X - 3, PLAY_CONTROLS_Y - 3, PLAY_CONTROLS_WIDTH + 6, PLAY_CONTROLS_HEIGHT + 6, Color.white) + @screen_bitmap.bitmap.outline_rect(PLAY_CONTROLS_X - 2, PLAY_CONTROLS_Y - 2, PLAY_CONTROLS_WIDTH + 4, PLAY_CONTROLS_HEIGHT + 4, Color.black) + @screen_bitmap.bitmap.outline_rect(PLAY_CONTROLS_X - 1, PLAY_CONTROLS_Y - 1, PLAY_CONTROLS_WIDTH + 2, PLAY_CONTROLS_HEIGHT + 2, Color.white) + # Fill the play controls with white + @screen_bitmap.bitmap.fill_rect(PLAY_CONTROLS_X, PLAY_CONTROLS_Y, PLAY_CONTROLS_WIDTH, PLAY_CONTROLS_HEIGHT, Color.white) # Outline around timeline/particle list @screen_bitmap.bitmap.outline_rect(PARTICLE_LIST_X - 3, PARTICLE_LIST_Y - 3, PARTICLE_LIST_WIDTH + 6, PARTICLE_LIST_HEIGHT + 6, Color.white) @screen_bitmap.bitmap.outline_rect(PARTICLE_LIST_X - 2, PARTICLE_LIST_Y - 2, PARTICLE_LIST_WIDTH + 4, PARTICLE_LIST_HEIGHT + 4, Color.black) @screen_bitmap.bitmap.outline_rect(PARTICLE_LIST_X - 1, PARTICLE_LIST_Y - 1, PARTICLE_LIST_WIDTH + 2, PARTICLE_LIST_HEIGHT + 2, Color.white) end - def refresh_keyframe_particle_pane - if !keyframe || keyframe < 0 || !particle_index || particle_index < 0 || - !@anim[:particles][particle_index] - @keyframe_particle_pane.visible = false + def refresh_canvas + end + + def refresh_commands_pane + if keyframe < 0 || particle_index < 0 || !@anim[:particles][particle_index] || + @anim[:particles][particle_index][:name] == "SE" + @commands_pane.visible = false else - @keyframe_particle_pane.visible = true + @commands_pane.visible = true new_vals = AnimationEditor::ParticleDataHelper.get_all_keyframe_particle_values(@anim[:particles][particle_index], 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| + # TODO: Need to do something special for :color, :tone and :frame which + # all have button controls. + @commands_pane.controls.each do |ctrl| next if !new_vals.include?(ctrl[0]) ctrl[1].value = new_vals[ctrl[0]][0] if ctrl[1].respond_to?("value=") # TODO: new_vals[ctrl[0]][1] is whether the value is being interpolated, @@ -178,15 +245,58 @@ class AnimationEditor end end + def refresh_se_pane + if keyframe < 0 || particle_index < 0 || !@anim[:particles][particle_index] || + @anim[:particles][particle_index][:name] != "SE" + @se_pane.visible = false + else + @se_pane.visible = true + # TODO: Set list of SEs, activate/deactivate buttons accordingly. + end + end + + def refresh_particle_pane + if keyframe >= 0 || particle_index < 0 + @particle_pane.visible = false + else + @particle_pane.visible = true + new_vals = AnimationEditor::ParticleDataHelper.get_all_particle_values(@anim[:particles][particle_index]) + @particle_pane.controls.each do |ctrl| + next if !new_vals.include?(ctrl[0]) + ctrl[1].value = new_vals[ctrl[0]] if ctrl[1].respond_to?("value=") + end + # TODO: Disable the name and graphic controls for "User"/"Target". + end + end + + def refresh_keyframe_pane + if keyframe < 0 || particle_index >= 0 + @keyframe_pane.visible = false + else + @keyframe_pane.visible = true + end + end + def refresh_particle_list @particle_list.refresh end + def refresh_play_controls + @play_controls.refresh + end + def refresh + # Set canvas display + refresh_canvas # Set all side pane controls to values from animation - refresh_keyframe_particle_pane + refresh_commands_pane + refresh_se_pane + refresh_particle_pane + refresh_keyframe_pane # Set particle list's contents refresh_particle_list + # Set play controls' information + refresh_play_controls end #----------------------------------------------------------------------------- @@ -197,23 +307,96 @@ class AnimationEditor # double-clicking to add particle, deleting particle. end - def update_keyframe_particle_pane - @keyframe_particle_pane.update - @captured = :keyframe_particle_pane if @keyframe_particle_pane.busy? - if @keyframe_particle_pane.changed? + def update_commands_pane + return if !@commands_pane.visible + @commands_pane.update + if @commands_pane.busy? + @captured = [@commands_pane, :update_commands_pane] + end + if @commands_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] + values = @commands_pane.values + values.each_pair do |property, value| + case property + when :color_tone # Button + # TODO: Open the colour/tone side pane. + else + particle = @anim[:particles][particle_index] + new_cmds = AnimationEditor::ParticleDataHelper.add_command(particle, property, keyframe, value) + if new_cmds + particle[property] = new_cmds + else + particle.delete(property) + end + @particle_list.change_particle_commands(particle_index) + @play_controls.duration = @particle_list.duration + refresh_commands_pane + end end - @keyframe_particle_pane.clear_changed + @commands_pane.clear_changed + end + end + + def update_se_pane + return if !@se_pane.visible + @se_pane.update + if @se_pane.busy? + @captured = [@se_pane, :update_se_pane] + end + # TODO: Enable the "Edit" and "Delete" controls only if an SE is selected. + if @se_pane.changed? + # TODO: Make undo/redo snapshot. + values = @se_pane.values + values.each_pair do |property, value| + case property + when :add # Button + when :edit # Button + when :delete # Button + else + particle = @anim[:particles][particle_index] + end + end + @se_pane.clear_changed + end + end + + def update_particle_pane + return if !@particle_pane.visible + @particle_pane.update + if @particle_pane.busy? + @captured = [@particle_pane, :update_particle_pane] + end + if @particle_pane.changed? + # TODO: Make undo/redo snapshot. + values = @particle_pane.values + values.each_pair do |property, value| + case property + when :graphic # Button + # TODO: Open the graphic chooser pop-up window. + else + particle = @anim[:particles][particle_index] + new_cmds = AnimationEditor::ParticleDataHelper.set_property(particle, property, value) + @particle_list.change_particle(particle_index) + refresh_particle_pane + end + end + @particle_pane.clear_changed + end + end + + def update_keyframe_pane + return if !@keyframe_pane.visible + @keyframe_pane.update + if @keyframe_pane.busy? + @captured = [@keyframe_pane, :update_keyframe_pane] + end + if @keyframe_pane.changed? + # TODO: Make undo/redo snapshot. + values = @keyframe_pane.values + values.each_pair do |property, value| + # TODO: Stuff here once I decide what controls to add. + end + @keyframe_pane.clear_changed end end @@ -221,34 +404,44 @@ class AnimationEditor old_keyframe = keyframe old_particle_index = particle_index @particle_list.update - @captured = :particle_list if @particle_list.busy? + if @particle_list.busy? + @captured = [@particle_list, :update_particle_list] + end if @particle_list.changed? - refresh_keyframe_particle_pane if keyframe != old_keyframe || particle_index != old_particle_index + refresh if keyframe != old_keyframe || particle_index != old_particle_index # TODO: Lots of stuff here. @particle_list.clear_changed end @particle_list.repaint end + def update_play_controls + @play_controls.update + @play_controls.repaint + if @play_controls.busy? + @captured = [@play_controls, :update_play_controls] + end + # TODO: Will the play controls ever signal themselves as changed? I don't + # think so. + if @play_controls.changed? + @play_controls.clear_changed + end + @play_controls.repaint + end + def update if @captured - # TODO: There must be a better way to do this. - case @captured - when :canvas - update_canvas - @captured = nil if !@canvas.busy? - when :keyframe_particle_pane - update_keyframe_particle_pane - @captured = nil if !@keyframe_particle_pane.busy? - when :particle_list - update_particle_list - @captured = nil if !@particle_list.busy? - end - else - update_canvas - update_keyframe_particle_pane - update_particle_list + self.send(@captured[1]) + @captured = nil if !@captured[0].busy? + return end + update_canvas + update_commands_pane + update_se_pane + update_particle_pane + update_keyframe_pane + update_particle_list + update_play_controls end #----------------------------------------------------------------------------- diff --git a/Data/Scripts/910_New anim editor/011_particle list.rb b/Data/Scripts/910_New anim editor/011_particle list.rb index 074cbbd04..d5ad0a69e 100644 --- a/Data/Scripts/910_New anim editor/011_particle list.rb +++ b/Data/Scripts/910_New anim editor/011_particle list.rb @@ -5,15 +5,21 @@ # time the horizontal scrollbar changes. #=============================================================================== class UIControls::AnimationParticleList < UIControls::BaseControl - LIST_WIDTH = 150 + VIEWPORT_SPACING = 1 + TIMELINE_HEIGHT = 24 - VIEWPORT_SPACING + LIST_X = 0 + LIST_Y = TIMELINE_HEIGHT + VIEWPORT_SPACING + LIST_WIDTH = 150 - VIEWPORT_SPACING + COMMANDS_X = LIST_WIDTH + VIEWPORT_SPACING + COMMANDS_Y = LIST_Y + ROW_HEIGHT = 24 - TIMELINE_HEIGHT = 24 DIAMOND_SIZE = 3 TIMELINE_LEFT_BUFFER = DIAMOND_SIZE + 1 # Allows diamonds at keyframe 0 to be drawn fully TIMELINE_TEXT_SIZE = 16 KEYFRAME_SPACING = 20 INTERP_LINE_HEIGHT = KEYFRAME_SPACING - ((DIAMOND_SIZE * 2) + 3) - INTERP_LINE_Y = (TIMELINE_HEIGHT / 2) - (INTERP_LINE_HEIGHT / 2) + INTERP_LINE_Y = (ROW_HEIGHT / 2) - (INTERP_LINE_HEIGHT / 2) DURATION_BUFFER = 20 # Extra keyframes shown after the animation's end CONTROL_BG_COLORS = { :user => Color.new(96, 248, 96), # Green @@ -24,7 +30,6 @@ class UIControls::AnimationParticleList < UIControls::BaseControl SE_CONTROL_BG = Color.gray attr_reader :keyframe # The selected keyframe - attr_reader :particle_index # Index in @particles def initialize(x, y, width, height, viewport) super(width, height, viewport) @@ -33,28 +38,28 @@ class UIControls::AnimationParticleList < UIControls::BaseControl draw_control_background # Create viewports @list_viewport = Viewport.new( - x, y + TIMELINE_HEIGHT, LIST_WIDTH, height - TIMELINE_HEIGHT - UIControls::Scrollbar::SLIDER_WIDTH - 1 + x + LIST_X, y + LIST_Y, LIST_WIDTH, height - LIST_Y - UIControls::Scrollbar::SLIDER_WIDTH - VIEWPORT_SPACING ) @list_viewport.z = self.viewport.z + 1 - @commands_bg_viewport = Viewport.new(@list_viewport.rect.x + LIST_WIDTH, @list_viewport.rect.y, - width - @list_viewport.rect.width - UIControls::Scrollbar::SLIDER_WIDTH, - @list_viewport.rect.height) + @commands_bg_viewport = Viewport.new( + x + COMMANDS_X, y + COMMANDS_Y, + width - COMMANDS_X - UIControls::Scrollbar::SLIDER_WIDTH - VIEWPORT_SPACING, @list_viewport.rect.height + ) @commands_bg_viewport.z = self.viewport.z + 1 - @position_viewport = Viewport.new(@list_viewport.rect.x + LIST_WIDTH, y, @commands_bg_viewport.rect.width, height) + @position_viewport = Viewport.new(@commands_bg_viewport.rect.x, y, @commands_bg_viewport.rect.width, height) @position_viewport.z = self.viewport.z + 2 - @commands_viewport = Viewport.new(@list_viewport.rect.x + LIST_WIDTH, @list_viewport.rect.y, - width - @list_viewport.rect.width - UIControls::Scrollbar::SLIDER_WIDTH, - @list_viewport.rect.height) + @commands_viewport = Viewport.new(@commands_bg_viewport.rect.x, @commands_bg_viewport.rect.y, + @commands_bg_viewport.rect.width, @commands_bg_viewport.rect.height) @commands_viewport.z = self.viewport.z + 3 # Create scrollbar @list_scrollbar = UIControls::Scrollbar.new( - @commands_viewport.rect.x + @commands_viewport.rect.width, @commands_viewport.rect.y, - @commands_viewport.rect.height + 1, self.viewport, false, true + x + width - UIControls::Scrollbar::SLIDER_WIDTH, @commands_bg_viewport.rect.y, + @commands_bg_viewport.rect.height, self.viewport, false, true ) @list_scrollbar.set_interactive_rects @time_scrollbar = UIControls::Scrollbar.new( - @commands_viewport.rect.x, @commands_viewport.rect.y + @commands_viewport.rect.height + 1, - @commands_viewport.rect.width, self.viewport, true, true + @commands_bg_viewport.rect.x, y + height - UIControls::Scrollbar::SLIDER_WIDTH, + @commands_bg_viewport.rect.width, self.viewport, true, true ) @time_scrollbar.set_interactive_rects # Timeline bitmap sprite @@ -64,7 +69,7 @@ class UIControls::AnimationParticleList < UIControls::BaseControl @timeline_sprite.bitmap.font.color = TEXT_COLOR @timeline_sprite.bitmap.font.size = TIMELINE_TEXT_SIZE # Position line sprite - @position_sprite = BitmapSprite.new(3, height - UIControls::Scrollbar::SLIDER_WIDTH - 1, @position_viewport) + @position_sprite = BitmapSprite.new(3, height - UIControls::Scrollbar::SLIDER_WIDTH - VIEWPORT_SPACING, @position_viewport) @position_sprite.ox = @position_sprite.width / 2 @position_sprite.bitmap.fill_rect(0, 0, @position_sprite.bitmap.width, @position_sprite.bitmap.height, Color.red) # List sprites and commands sprites @@ -77,9 +82,10 @@ class UIControls::AnimationParticleList < UIControls::BaseControl @duration = 0 # Selected things @keyframe = 0 - @particle_index = 0 + @row_index = 0 # Particle information to display (one row each) @particles = [] # Reference to particle data from the editor scene + @expanded_particles = [0] # Each element is index in @particles @particle_list = [] # Each element is index in @particles or [index, property] @visibilities = [] # Per particle @commands = {} @@ -90,10 +96,10 @@ class UIControls::AnimationParticleList < UIControls::BaseControl # Background self.bitmap.fill_rect(0, 0, width, height, Color.white) # Separator lines - self.bitmap.fill_rect(0, TIMELINE_HEIGHT - 1, width, 1, Color.black) - self.bitmap.fill_rect(LIST_WIDTH - 1, 0, 1, height, Color.black) - self.bitmap.fill_rect(0, height - UIControls::Scrollbar::SLIDER_WIDTH - 1, width, 1, Color.black) - self.bitmap.fill_rect(width - UIControls::Scrollbar::SLIDER_WIDTH - 1, 0, 1, height, Color.black) + self.bitmap.fill_rect(0, TIMELINE_HEIGHT, width, VIEWPORT_SPACING, Color.black) + self.bitmap.fill_rect(LIST_WIDTH, 0, VIEWPORT_SPACING, height, Color.black) + self.bitmap.fill_rect(0, height - UIControls::Scrollbar::SLIDER_WIDTH - VIEWPORT_SPACING, width, VIEWPORT_SPACING, Color.black) + self.bitmap.fill_rect(width - UIControls::Scrollbar::SLIDER_WIDTH - VIEWPORT_SPACING, 0, VIEWPORT_SPACING, height, Color.black) end def dispose_listed_sprites @@ -116,6 +122,15 @@ class UIControls::AnimationParticleList < UIControls::BaseControl @commands_viewport.dispose end + def duration + return [@duration - DURATION_BUFFER, 0].max + end + + def particle_index + ret = @particle_list[@row_index] + return (ret.is_a?(Array)) ? ret[0] : ret + end + def left_pos=(val) old_val = @left_pos total_width = (@duration * KEYFRAME_SPACING) + TIMELINE_LEFT_BUFFER + 1 @@ -133,7 +148,7 @@ class UIControls::AnimationParticleList < UIControls::BaseControl def top_pos=(val) old_val = @top_pos - total_height = @particle_list.length * ROW_HEIGHT + total_height = (@particle_list.length * ROW_HEIGHT) + 1 if total_height <= @list_viewport.rect.height @top_pos = 0 else @@ -141,6 +156,7 @@ class UIControls::AnimationParticleList < UIControls::BaseControl @top_pos = @top_pos.clamp(0, total_height - @list_viewport.rect.height) end @list_viewport.oy = @top_pos + @commands_bg_viewport.oy = @top_pos @commands_viewport.oy = @top_pos if @top_pos != old_val invalidate_rows @@ -150,12 +166,22 @@ class UIControls::AnimationParticleList < UIControls::BaseControl def set_particles(particles) @particles = particles - @particle_list.clear calculate_all_commands_and_durations + create_sprites + end + + def create_sprites + # Fill in @particle_list with indices from @particles + @particle_list.clear + @particles.length.times do |i| + @particle_list.push(i) + next if !@expanded_particles.include?(i) + @particles[i].each_pair do |property, value| + @particle_list.push([i, property]) if value.is_a?(Array) + end + end # Dispose of and clear all existing list/commands sprites dispose_listed_sprites - # Fill in @particle_list with indices from @particles - @particles.length.times { |i| @particle_list.push(i) } # Create new sprites for each particle (1x list and 2x commands) @particle_list.length.times do list_sprite = BitmapSprite.new(@list_viewport.rect.width, ROW_HEIGHT, @list_viewport) @@ -174,9 +200,12 @@ class UIControls::AnimationParticleList < UIControls::BaseControl commands_sprite.bitmap.font.size = TEXT_SIZE @commands_sprites.push(commands_sprite) end - @list_scrollbar.range = @particle_list.length * ROW_HEIGHT + # Set scrollbars to the correct lengths + @list_scrollbar.range = (@particle_list.length * ROW_HEIGHT) + 1 @time_scrollbar.range = (@duration * KEYFRAME_SPACING) + TIMELINE_LEFT_BUFFER + 1 self.left_pos = @left_pos + self.top_pos = @top_pos + # Redraw all sprites invalidate end @@ -219,6 +248,8 @@ class UIControls::AnimationParticleList < UIControls::BaseControl #----------------------------------------------------------------------------- def calculate_duration + # TODO: Refresh lots of things if the duration changed (e.g. SE command + # line). @duration = AnimationEditor::ParticleDataHelper.get_duration(@particles) @duration += DURATION_BUFFER end @@ -228,31 +259,101 @@ class UIControls::AnimationParticleList < UIControls::BaseControl # particle was changed, recalculate only that particle's commands. def calculate_all_commands_and_durations calculate_duration + calculate_all_commands + end + + def calculate_all_commands @commands = {} - @particles.each_with_index do |particle, i| - overall_commands = [] + @particles.each_with_index do |particle, index| + calculate_commands_for_particle(index) + end + end + + def calculate_commands_for_particle(index) + # TODO: Delete everything from @commands that includes index. + overall_commands = [] + @particles[index].each_pair do |property, value| + next if !value.is_a?(Array) + cmds = AnimationEditor::ParticleDataHelper.get_particle_property_commands_timeline(@particles[index], value, property) + @commands[[index, property]] = cmds + cmds.each_with_index do |cmd, i| + next if !cmd + overall_commands[i] = (cmd.is_a?(Array)) ? cmd.clone : cmd + end + end + @commands[index] = overall_commands + # Calculate visibilities for every keyframe + @visibilities[index] = AnimationEditor::ParticleDataHelper.get_timeline_particle_visibilities( + @particles[index], @duration - DURATION_BUFFER + ) + end + + # Returns whether the sprites need replacing due to the addition or + # subtraction of one. + def ensure_sprites + # TODO: Check through @particle_list to ensure only ones are shown which + # correspond to something in @particles. + # Go through all @particles to ensure there are sprites for each of them + missing = false + @particles.each_with_index do |particle, index| + if @particle_list.none? { |value| next !value.is_a?(Array) && value == index } + missing = true + break + end + next if !@expanded_particles.include?(index) particle.each_pair do |property, value| next if !value.is_a?(Array) - cmds = AnimationEditor::ParticleDataHelper.get_particle_property_commands_timeline(value, property) - @commands[[i, property]] = cmds - cmds.each_with_index do |cmd, j| - next if !cmd - overall_commands[j] = (cmd.is_a?(Array)) ? cmd.clone : cmd + if @particle_list.none? { |value| next value.is_a?(Array) && value[0] == index && value[1] == property } + missing = true + break end end - @commands[i] = overall_commands + break if missing end - # Calculate visibilities for every keyframe - @particles.each_with_index do |particle, i| - @visibilities[i] = AnimationEditor::ParticleDataHelper.get_timeline_particle_visibilities( - particle, @duration - DURATION_BUFFER - ) + return true if missing + # Go through all sprites to ensure there are none for a particle or + # particle/property that don't exist + excess = false + @particle_list.each do |value| + if value.is_a?(Array) + excess = true if !@particles[value[0]] || !@particles[value[0]][value[1]] || + @particles[value[0]][value[1]].empty? + else + excess = true if !@particles[value] + end + break if excess end + return excess + end + + # Called when a change is made to a particle's commands. + def change_particle_commands(index) + old_duration = @duration + calculate_duration + if @duration != old_duration + calculate_all_commands + else + calculate_commands_for_particle(index) + end + sprites_need_changing = ensure_sprites + if @duration != old_duration || sprites_need_changing + @keyframe = @keyframe.clamp(0, @duration - 1) + @row_index = @row_index.clamp(0, @particle_list.length - 1) + create_sprites + end + invalidate + end + + # Called when a change is made to a particle's general properties. + def change_particle(index) + invalidate_rows end # TODO: Methods that will show/hide individual property rows for a given # @particles index. + #----------------------------------------------------------------------------- + def each_visible_keyframe(early_start = false) full_width = @commands_viewport.rect.width start_keyframe = ((@left_pos - TIMELINE_LEFT_BUFFER) / KEYFRAME_SPACING) @@ -279,18 +380,17 @@ class UIControls::AnimationParticleList < UIControls::BaseControl def property_display_name(property) return { - :graphic => "Graphic", - :frame => "Graphic frame", - :blending => "Blending", - :flip => "Flip", - :x => "X", - :y => "Y", - :zoom_x => "Zoom X", - :zoom_y => "Zoom Y", - :angle => "Angle", - :visible => "Visible", - :opacity => "Opacity" - }[property] || "Unnamed property" + :frame => _INTL("Graphic frame"), + :blending => _INTL("Blending"), + :flip => _INTL("Flip"), + :x => _INTL("X"), + :y => _INTL("Y"), + :zoom_x => _INTL("Zoom X"), + :zoom_y => _INTL("Zoom Y"), + :angle => _INTL("Angle"), + :visible => _INTL("Visible"), + :opacity => _INTL("Opacity") + }[property] || property.capitalize end def repaint @@ -333,7 +433,7 @@ class UIControls::AnimationParticleList < UIControls::BaseControl end def refresh_position_line - @position_sprite.visible = (@keyframe && @keyframe >= 0) + @position_sprite.visible = (@keyframe >= 0) if @keyframe >= 0 @position_sprite.x = TIMELINE_LEFT_BUFFER + (@keyframe * KEYFRAME_SPACING) - @left_pos end @@ -344,6 +444,7 @@ class UIControls::AnimationParticleList < UIControls::BaseControl spr = @list_sprites[index] return if !spr spr.bitmap.clear + box_x = (@particle_list[index].is_a?(Array)) ? 16 : 0 # Get the background color p_index = (@particle_list[index].is_a?(Array)) ? @particle_list[index][0] : @particle_list[index] particle_data = @particles[p_index] @@ -365,14 +466,15 @@ class UIControls::AnimationParticleList < UIControls::BaseControl elsif !@captured_row && !@captured_keyframe && @hover_row && @hover_row == index && !@hover_keyframe hover_color = HOVER_COLOR end - spr.bitmap.fill_rect(0, 1, spr.width - 1, spr.height - 1, hover_color) if hover_color + spr.bitmap.fill_rect(box_x, 1, spr.width - box_x, spr.height - 1, hover_color) if hover_color # Draw outline - spr.bitmap.outline_rect(0, 1, spr.width - 1, spr.height - 1, bg_color, 2) + spr.bitmap.outline_rect(box_x, 1, spr.width - box_x, spr.height - 1, bg_color, 2) # Draw text if @particle_list[index].is_a?(Array) - draw_text(spr.bitmap, 3 + 40, 3, property_display_name(@particle_list[index][1])) + draw_text(spr.bitmap, box_x + 4, 0, "→") # ► + draw_text(spr.bitmap, box_x + 4 + 17, 3, property_display_name(@particle_list[index][1])) else - draw_text(spr.bitmap, 3, 3, @particles[p_index][:name] || "Unnamed") + draw_text(spr.bitmap, 4, 3, @particles[p_index][:name] || "Unnamed") end end @@ -394,7 +496,7 @@ class UIControls::AnimationParticleList < UIControls::BaseControl each_visible_keyframe do |i| draw_x = TIMELINE_LEFT_BUFFER + (i * KEYFRAME_SPACING) - @left_pos # Draw bg - if i < @duration - DURATION_BUFFER && (particle_data[:name] == "SE" || visible_cmds[i]) + if i < @duration - DURATION_BUFFER && visible_cmds[i] bg_spr.bitmap.fill_rect(draw_x, 1, KEYFRAME_SPACING, ROW_HEIGHT - 2, bg_color) end # Draw hover highlight @@ -413,14 +515,14 @@ class UIControls::AnimationParticleList < UIControls::BaseControl end bg_spr.bitmap.fill_rect(draw_x - (KEYFRAME_SPACING / 2), 2, KEYFRAME_SPACING, ROW_HEIGHT - 3, hover_color) if hover_color next if i >= @duration - DURATION_BUFFER - next if particle_data[:name] != "SE" && !visible_cmds[i] + next if !visible_cmds[i] # Draw outline bg_spr.bitmap.fill_rect(draw_x, 1, KEYFRAME_SPACING, 1, Color.black) # Top bg_spr.bitmap.fill_rect(draw_x, ROW_HEIGHT - 1, KEYFRAME_SPACING, 1, Color.black) # Bottom - if i <= 0 || (particle_data[:name] != "SE" && !visible_cmds[i - 1]) + if i <= 0 || !visible_cmds[i - 1] bg_spr.bitmap.fill_rect(draw_x, 1, 1, ROW_HEIGHT - 1, Color.black) # Left end - if i == @duration - DURATION_BUFFER - 1 || (particle_data[:name] != "SE" && i < @duration - 1 && !visible_cmds[i + 1]) + if i == @duration - DURATION_BUFFER - 1 || (i < @duration - 1 && !visible_cmds[i + 1]) bg_spr.bitmap.fill_rect(draw_x + KEYFRAME_SPACING, 1, 1, ROW_HEIGHT - 1, Color.black) # Right end end @@ -439,7 +541,7 @@ class UIControls::AnimationParticleList < UIControls::BaseControl next if !cmds[i] draw_x = TIMELINE_LEFT_BUFFER + (i * KEYFRAME_SPACING) - @left_pos # Draw command diamond - spr.bitmap.fill_diamond(draw_x, TIMELINE_HEIGHT / 2, DIAMOND_SIZE, TEXT_COLOR) + spr.bitmap.fill_diamond(draw_x, ROW_HEIGHT / 2, DIAMOND_SIZE, TEXT_COLOR) # Draw interpolation line if cmds[i].is_a?(Array) spr.bitmap.draw_interpolation_line( @@ -500,14 +602,17 @@ class UIControls::AnimationParticleList < UIControls::BaseControl when :list new_hover_row = (mouse_y + @top_pos - rect.y) / ROW_HEIGHT break if new_hover_row >= @particle_list.length + listed_element = @particle_list[new_hover_row] + p_index = listed_element.is_a?(Array) ? listed_element[0] : listed_element + break if @particles[p_index][:name] == "SE" ret = [area, nil, new_hover_row] when :timeline - new_hover_keyframe = (mouse_x + @left_pos - rect.x - TIMELINE_LEFT_BUFFER + (KEYFRAME_SPACING / 2)) / KEYFRAME_SPACING + new_hover_keyframe = (mouse_x + @left_pos - rect.x - TIMELINE_LEFT_BUFFER + (KEYFRAME_SPACING / 2) - 1) / KEYFRAME_SPACING break if new_hover_keyframe < 0 || new_hover_keyframe >= @duration ret = [area, new_hover_keyframe, nil] when :commands new_hover_row = (mouse_y + @top_pos - rect.y) / ROW_HEIGHT - new_hover_keyframe = (mouse_x + @left_pos - rect.x - TIMELINE_LEFT_BUFFER + (KEYFRAME_SPACING / 2)) / KEYFRAME_SPACING + new_hover_keyframe = (mouse_x + @left_pos - rect.x - TIMELINE_LEFT_BUFFER + (KEYFRAME_SPACING / 2) - 1) / KEYFRAME_SPACING break if new_hover_row >= @particle_list.length break if new_hover_keyframe < 0 || new_hover_keyframe >= @duration ret = [area, new_hover_keyframe, new_hover_row] @@ -535,9 +640,14 @@ class UIControls::AnimationParticleList < UIControls::BaseControl if @captured_area == hover_element[0] && @captured_keyframe == hover_element[1] && @captured_row == hover_element[2] - set_changed if @keyframe != @captured_keyframe || @particle_index != @captured_row + if @captured_row && @particle_list[@captured_row].is_a?(Array) + # TODO: If I want to be able to select individual property rows and/or + # diamonds, I shouldn't have this line. + @captured_row = @particle_list.index(@particle_list[@captured_row][0]) + end + set_changed if @keyframe != @captured_keyframe || @row_index != @captured_row @keyframe = @captured_keyframe || -1 - @particle_index = @captured_row || -1 + @row_index = @captured_row || -1 end end @captured_keyframe = nil @@ -545,6 +655,11 @@ class UIControls::AnimationParticleList < UIControls::BaseControl super # Make this control not busy again end + def on_right_mouse_release + # TODO: Toggle interpolation line at mouse's position. Should this also have + # a def on_right_mouse_press and @right_captured_whatever? + end + def update_hover_highlight # Remove the hover highlight if there are no interactions for this control # or if the mouse is off-screen @@ -608,32 +723,54 @@ class UIControls::AnimationParticleList < UIControls::BaseControl # Update the current keyframe line's position refresh_position_line + if Input.release?(Input::MOUSERIGHT) + on_right_mouse_release + end + # TODO: This is testing code, and should be replaced by clicking on the - # timeline or a command sprite. Maybe keep it after all? - if Input.trigger?(Input::LEFT) + # timeline or a command sprite. Maybe keep it after all? If so, + # probably change left/right to <>, and also move the scrollbar(s) to + # keep the "cursor" on-screen. + if Input.repeat?(Input::LEFT) if @keyframe > 0 @keyframe -= 1 - echoln "keyframe = #{@keyframe}" set_changed end - elsif Input.trigger?(Input::RIGHT) - if @keyframe < @duration - DURATION_BUFFER + elsif Input.repeat?(Input::RIGHT) + if @keyframe < @duration - 1 @keyframe += 1 - echoln "keyframe = #{@keyframe}" set_changed end - elsif Input.trigger?(Input::UP) - if @particle_index > 0 - @particle_index -= 1 - echoln "particle_index = #{@particle_index}" - set_changed - end - elsif Input.trigger?(Input::DOWN) - if @particle_index < @particles.length - 1 - @particle_index += 1 - echoln "particle_index = #{@particle_index}" - set_changed + # TODO: If this is to be kept, @row_index should be changed by potentially + # more than 1, so that @particle_list[@row_index] is an integer and + # not an array. + # elsif Input.repeat?(Input::UP) + # if @row_index > 0 + # @row_index -= 1 + # set_changed + # end + # elsif Input.repeat?(Input::DOWN) + # if @row_index < @particles.length - 1 + # @row_index += 1 + # set_changed + # end + end + + # Mouse scroll wheel + mouse_x, mouse_y = mouse_pos + if mouse_x && mouse_y + if @interactions[:list].contains?(mouse_x, mouse_y) || + @interactions[:commands].contains?(mouse_x, mouse_y) + wheel_v = Input.scroll_v + if wheel_v > 0 # Scroll up + @list_scrollbar.slider_top -= UIControls::Scrollbar::SCROLL_DISTANCE + self.top_pos = @list_scrollbar.position + elsif wheel_v < 0 # Scroll down + @list_scrollbar.slider_top += UIControls::Scrollbar::SCROLL_DISTANCE + self.top_pos = @list_scrollbar.position + end end end + end end diff --git a/Data/Scripts/910_New anim editor/012_play controls.rb b/Data/Scripts/910_New anim editor/012_play controls.rb new file mode 100644 index 000000000..14a73d602 --- /dev/null +++ b/Data/Scripts/910_New anim editor/012_play controls.rb @@ -0,0 +1,27 @@ +#=============================================================================== +# TODO +#=============================================================================== +class UIControls::AnimationPlayControls < UIControls::BaseControl + TEXT_OFFSET_Y = 5 + + def initialize(x, y, width, height, viewport) + super(width, height, viewport) + self.x = x + self.y = y + @duration = 0 + end + + def duration=(new_val) + return if @duration == new_val + @duration = new_val + refresh + end + + #----------------------------------------------------------------------------- + + def refresh + super + draw_text(self.bitmap, 12, TEXT_OFFSET_Y + 14, _INTL("Play controls not added yet!")) + draw_text(self.bitmap, width - 134, TEXT_OFFSET_Y, _INTL("Total length: {1}s", @duration / 20.0)) + end +end diff --git a/Data/Scripts/910_New anim editor/090 particle data helper.rb b/Data/Scripts/910_New anim editor/090 particle data helper.rb index f92e2fdb1..05e5a3e5d 100644 --- a/Data/Scripts/910_New anim editor/090 particle data helper.rb +++ b/Data/Scripts/910_New anim editor/090 particle data helper.rb @@ -6,7 +6,7 @@ module AnimationEditor::ParticleDataHelper particles.each do |p| p.each_pair do |cmd, val| next if !val.is_a?(Array) || val.length == 0 - max = val.last[0] + val.last[1] + max = val.last[0] + val.last[1] # Keyframe + duration ret = max if ret < max end end @@ -51,7 +51,6 @@ module AnimationEditor::ParticleDataHelper ret[0] = true if first_cmd >= 0 && first_cmd <= frame && (first_visible_cmd < 0 || frame < first_visible_cmd) end - echoln "here 2: #{ret}" return ret end @@ -63,6 +62,14 @@ module AnimationEditor::ParticleDataHelper 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 + # TODO: Generalise this to any property? # NOTE: Particles are assumed to be not visible at the start of the # animation, and automatically become visible when the particle has @@ -75,7 +82,7 @@ module AnimationEditor::ParticleDataHelper property, particle[:name]) end value = GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES[:visible] - value = true if ["User", "Target"].include?(particle[:name]) + value = true if ["User", "Target", "SE"].include?(particle[:name]) ret = [] if particle[:visible] particle[:visible].each { |cmd| ret[cmd[0]] = cmd[2] } @@ -112,8 +119,13 @@ module AnimationEditor::ParticleDataHelper # 0 - SetXYZ # [+/- duration, interpolation type] --- MoveXYZ (duration's sign is whether # it makes the value higher or lower) - def get_particle_property_commands_timeline(commands, property) + def get_particle_property_commands_timeline(particle, commands, property) return nil if !commands || commands.length == 0 + 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 @@ -134,4 +146,66 @@ module AnimationEditor::ParticleDataHelper return ret end + #----------------------------------------------------------------------------- + + def set_property(particle, property, value) + particle[property] = value + end + + def add_command(particle, property, frame, value) + # 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 + # Add new command to points (may replace an existing command) + interp = :none + (frame + 1).times do |i| + interp = :none if set_points[i] || end_points[i] + interp = interps[i] if interps[i] + end + interps[frame] = interp if interp != :none + set_points[frame] = value + # 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] + val = true if property == :visible && ["User", "Target", "SE"].include?(particle[:name]) + length = [set_points.length, end_points.length].max + length.times do |i| + if !set_points[i].nil? && set_points[i] != val + ret.push([i, 0, set_points[i]]) + 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 end