From dba28332f22038a8a32527a68ef49674e3c98aa1 Mon Sep 17 00:00:00 2001 From: Maruno17 Date: Sat, 4 May 2024 19:17:23 +0100 Subject: [PATCH] Anim Editor: added basic particle spawner functionality and graphic frame randomiser --- .../Control elements/001_BaseControl.rb | 7 +- .../902_Anim GameData/001_Animation.rb | 117 ++++++++++-------- .../003_AnimationEditor_side_panes.rb | 31 +++++ .../Anim Editor elements/001_Canvas.rb | 21 ++-- .../905_Anim player/001_Anim player.rb | 88 ++++++++++--- .../905_Anim player/003_AnimPlayerHelper.rb | 44 +++++++ 6 files changed, 230 insertions(+), 78 deletions(-) diff --git a/Data/Scripts/801_UI controls/Control elements/001_BaseControl.rb b/Data/Scripts/801_UI controls/Control elements/001_BaseControl.rb index 3c5926510..aef513672 100644 --- a/Data/Scripts/801_UI controls/Control elements/001_BaseControl.rb +++ b/Data/Scripts/801_UI controls/Control elements/001_BaseControl.rb @@ -34,6 +34,11 @@ class UIControls::BaseControl < BitmapSprite return self.bitmap.height end + def visible=(value) + super + @captured_area = nil if !self.visible + end + #----------------------------------------------------------------------------- def mouse_pos @@ -83,7 +88,7 @@ class UIControls::BaseControl < BitmapSprite end def busy? - return !@captured_area.nil? + return self.visible && !@captured_area.nil? end def changed? diff --git a/Data/Scripts/902_Anim GameData/001_Animation.rb b/Data/Scripts/902_Anim GameData/001_Animation.rb index 12687a0f3..d358ea3d6 100644 --- a/Data/Scripts/902_Anim GameData/001_Animation.rb +++ b/Data/Scripts/902_Anim GameData/001_Animation.rb @@ -44,6 +44,11 @@ module GameData "EaseOut" => :ease_out } USER_AND_TARGET_SEPARATION = [200, -200, -100] # x, y, z (from user to target) + SPAWNER_TYPES = { + "None" => :none, + "RandomDirection" => :random_direction, + "RandomDirectionGravity" => :random_direction_gravity + } # Properties that apply to the animation in general, not to individual # particles. They don't change during the animation. @@ -62,62 +67,69 @@ module GameData # These properties cannot be changed partway through the animation. # NOTE: "Name" isn't a property here, because the particle's name comes # from the "Particle" property above. - "Graphic" => [:graphic, "s"], - "Focus" => [:focus, "e", FOCUS_TYPES], - "FoeInvertX" => [:foe_invert_x, "b"], - "FoeInvertY" => [:foe_invert_y, "b"], - "FoeFlip" => [:foe_flip, "b"], + "Graphic" => [:graphic, "s"], + "Focus" => [:focus, "e", FOCUS_TYPES], + "FoeInvertX" => [:foe_invert_x, "b"], + "FoeInvertY" => [:foe_invert_y, "b"], + "FoeFlip" => [:foe_flip, "b"], + "Spawner" => [:spawner, "e", SPAWNER_TYPES], + "SpawnQuantity" => [:spawn_quantity, "v"], + "RandomFrameMax" => [:random_frame_max, "u"], # All properties below are "SetXYZ" or "MoveXYZ". "SetXYZ" has the # keyframe and the value, and "MoveXYZ" has the keyframe, duration and the # value. All have "^" in their schema. "SetXYZ" is turned into "MoveXYZ" # when compiling by inserting a duration (second value) of 0. - "SetFrame" => [:frame, "^uu"], # Frame within the graphic if it's a spritesheet - "MoveFrame" => [:frame, "^uuuE", nil, nil, nil, INTERPOLATION_TYPES], - "SetBlending" => [:blending, "^uu"], # 0, 1 or 2 - "SetFlip" => [:flip, "^ub"], - "SetX" => [:x, "^ui"], - "MoveX" => [:x, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], - "SetY" => [:y, "^ui"], - "MoveY" => [:y, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], - "SetZ" => [:z, "^ui"], - "MoveZ" => [:z, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], - "SetZoomX" => [:zoom_x, "^uu"], - "MoveZoomX" => [:zoom_x, "^uuuE", nil, nil, nil, INTERPOLATION_TYPES], - "SetZoomY" => [:zoom_y, "^uu"], - "MoveZoomY" => [:zoom_y, "^uuuE", nil, nil, nil, INTERPOLATION_TYPES], - "SetAngle" => [:angle, "^ui"], - "MoveAngle" => [:angle, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], - "SetVisible" => [:visible, "^ub"], - "SetOpacity" => [:opacity, "^uu"], - "MoveOpacity" => [:opacity, "^uuuE", nil, nil, nil, INTERPOLATION_TYPES], - "SetColorRed" => [:color_red, "^ui"], - "MoveColorRed" => [:color_red, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], - "SetColorGreen" => [:color_green, "^ui"], - "MoveColorGreen" => [:color_green, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], - "SetColorBlue" => [:color_blue, "^ui"], - "MoveColorBlue" => [:color_blue, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], - "SetColorAlpha" => [:color_alpha, "^ui"], - "MoveColorAlpha" => [:color_alpha, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], - "SetToneRed" => [:tone_red, "^ui"], - "MoveToneRed" => [:tone_red, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], - "SetToneGreen" => [:tone_green, "^ui"], - "MoveToneGreen" => [:tone_green, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], - "SetToneBlue" => [:tone_blue, "^ui"], - "MoveToneBlue" => [:tone_blue, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], - "SetToneGray" => [:tone_gray, "^ui"], - "MoveToneGray" => [:tone_gray, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], + "SetFrame" => [:frame, "^uu"], # Frame within the graphic if it's a spritesheet + "MoveFrame" => [:frame, "^uuuE", nil, nil, nil, INTERPOLATION_TYPES], + "SetBlending" => [:blending, "^uu"], # 0, 1 or 2 + "SetFlip" => [:flip, "^ub"], + "SetX" => [:x, "^ui"], + "MoveX" => [:x, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], + "SetY" => [:y, "^ui"], + "MoveY" => [:y, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], + "SetZ" => [:z, "^ui"], + "MoveZ" => [:z, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], + "SetZoomX" => [:zoom_x, "^uu"], + "MoveZoomX" => [:zoom_x, "^uuuE", nil, nil, nil, INTERPOLATION_TYPES], + "SetZoomY" => [:zoom_y, "^uu"], + "MoveZoomY" => [:zoom_y, "^uuuE", nil, nil, nil, INTERPOLATION_TYPES], + "SetAngle" => [:angle, "^ui"], + "MoveAngle" => [:angle, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], + "SetVisible" => [:visible, "^ub"], + "SetOpacity" => [:opacity, "^uu"], + "MoveOpacity" => [:opacity, "^uuuE", nil, nil, nil, INTERPOLATION_TYPES], + "SetColorRed" => [:color_red, "^ui"], + "MoveColorRed" => [:color_red, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], + "SetColorGreen" => [:color_green, "^ui"], + "MoveColorGreen" => [:color_green, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], + "SetColorBlue" => [:color_blue, "^ui"], + "MoveColorBlue" => [:color_blue, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], + "SetColorAlpha" => [:color_alpha, "^ui"], + "MoveColorAlpha" => [:color_alpha, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], + "SetToneRed" => [:tone_red, "^ui"], + "MoveToneRed" => [:tone_red, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], + "SetToneGreen" => [:tone_green, "^ui"], + "MoveToneGreen" => [:tone_green, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], + "SetToneBlue" => [:tone_blue, "^ui"], + "MoveToneBlue" => [:tone_blue, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], + "SetToneGray" => [:tone_gray, "^ui"], + "MoveToneGray" => [:tone_gray, "^uuiE", nil, nil, nil, INTERPOLATION_TYPES], # These properties are specifically for the "SE" particle. - "Play" => [:se, "^usUU"], # Filename, volume, pitch - "PlayUserCry" => [:user_cry, "^uUU"], # Volume, pitch - "PlayTargetCry" => [:target_cry, "^uUU"] # Volume, pitch + "Play" => [:se, "^usUU"], # Filename, volume, pitch + "PlayUserCry" => [:user_cry, "^uUU"], # Volume, pitch + "PlayTargetCry" => [:target_cry, "^uUU"] # Volume, pitch } PARTICLE_DEFAULT_VALUES = { - :name => "", - :graphic => "", - :focus => :foreground, - :foe_invert_x => false, - :foe_invert_y => false, - :foe_flip => false + :name => "", + :graphic => "", + :focus => :foreground, + :foe_invert_x => false, + :foe_invert_y => false, + :foe_flip => false, + :spawner => :none, + :spawn_quantity => 1, + :random_frame_max => 0 + } # NOTE: Particles are invisible until their first command, and automatically # become visible then. "User" and "Target" are visible from the start, @@ -315,6 +327,13 @@ module GameData # The User and Target particles have hardcoded graphics/foci, so they # don't need writing to PBS ret = nil if ["User", "Target"].include?(@particles[index][:name]) + when "Spawner" + ret = nil if ret == :none + when "SpawnQuantity" + ret = nil if @particles[index][:spawner].nil? || @particles[index][:spawner] == :none + ret = nil if ret && ret <= 1 + when "RandomFrameMax" + ret = nil if ret == 0 when "AllCommands" # Get translations of all properties to their names as seen in PBS # animation files diff --git a/Data/Scripts/904_Anim Editor/003_AnimationEditor_side_panes.rb b/Data/Scripts/904_Anim Editor/003_AnimationEditor_side_panes.rb index b9c4cbbbc..37f3af55d 100644 --- a/Data/Scripts/904_Anim Editor/003_AnimationEditor_side_panes.rb +++ b/Data/Scripts/904_Anim Editor/003_AnimationEditor_side_panes.rb @@ -524,6 +524,37 @@ AnimationEditor::SidePanes.add_property(:particle_pane, :focus, { } }) +AnimationEditor::SidePanes.add_property(:particle_pane, :random_frame_max, { + :new => proc { |pane, editor| + pane.add_labelled_number_text_box(:random_frame_max, _INTL("Rand. frame"), 0, 99, 0) + } +}) + +AnimationEditor::SidePanes.add_property(:particle_pane, :spawner, { + :new => proc { |pane, editor| + values = { + :none => _INTL("None"), + :random_direction => _INTL("Random direction"), + :random_direction_gravity => _INTL("Random direction gravity") + } + pane.add_labelled_dropdown_list(:spawner, _INTL("Spawner"), values, :none) + } +}) + +AnimationEditor::SidePanes.add_property(:particle_pane, :spawn_quantity, { + :new => proc { |pane, editor| + pane.add_labelled_number_text_box(:spawn_quantity, _INTL("Spawn qty"), 1, 99, 1) + }, + :refresh_value => proc { |control, editor| + spawner = editor.anim[:particles][editor.particle_index][:spawner] + if !spawner || spawner == :none + control.disable + else + control.enable + end + } +}) + AnimationEditor::SidePanes.add_property(:particle_pane, :opposing_label, { :new => proc { |pane, editor| pane.add_label(:opposing_label, _INTL("If on opposing side...")) diff --git a/Data/Scripts/904_Anim Editor/Anim Editor elements/001_Canvas.rb b/Data/Scripts/904_Anim Editor/Anim Editor elements/001_Canvas.rb index 405f93eae..921043987 100644 --- a/Data/Scripts/904_Anim Editor/Anim Editor elements/001_Canvas.rb +++ b/Data/Scripts/904_Anim Editor/Anim Editor elements/001_Canvas.rb @@ -148,6 +148,14 @@ class AnimationEditor::Canvas < Sprite return true end + def show_particle_sprite?(index) + return false if index < 0 || index >= @anim[:particles].length + particle = @anim[:particles][index] + return false if !particle || particle[:name] == "SE" + return false if particle[:spawner] && particle[:spawner] != :none + return true + end + def selected_particle=(val) return if @selected_particle == val @selected_particle = val @@ -411,12 +419,11 @@ class AnimationEditor::Canvas < Sprite end def get_sprite_and_frame(index, target_idx = -1) + return if !show_particle_sprite?(index) spr = nil frame = nil particle = @anim[:particles][index] case particle[:name] - when "SE" - return when "User" spr = @battler_sprites[user_index] raise _INTL("Sprite for particle {1} not found somehow (battler index {2}).", @@ -442,7 +449,7 @@ class AnimationEditor::Canvas < Sprite def refresh_sprite(index, target_idx = -1) particle = @anim[:particles][index] - return if !particle || particle[:name] == "SE" + return if !show_particle_sprite?(index) relative_to_index = -1 if particle[:focus] != :user_and_target if GameData::Animation::FOCUS_TYPES_WITH_USER.include?(particle[:focus]) @@ -519,8 +526,7 @@ class AnimationEditor::Canvas < Sprite end def refresh_particle_frame - return if @selected_particle < 0 || @selected_particle >= @anim[:particles].length || - @anim[:particles][@selected_particle][:name] == "SE" + return if !show_particle_sprite?(@selected_particle) focus = @anim[:particles][@selected_particle][:focus] frame_color = AnimationEditor::ParticleList::CONTROL_BG_COLORS[focus] || Color.magenta @sel_frame_bitmap.outline_rect(1, 1, @sel_frame_bitmap.width - 2, @sel_frame_bitmap.height - 2, frame_color) @@ -552,7 +558,7 @@ class AnimationEditor::Canvas < Sprite if GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus]) refresh_particle(i) # Because there can be multiple targets else - refresh_sprite(i) if particle[:name] != "SE" + refresh_sprite(i) if show_particle_sprite?(i) end end refresh_particle_frame # Intentionally after refreshing particles @@ -708,8 +714,7 @@ class AnimationEditor::Canvas < Sprite end def update_selected_particle_frame - if @selected_particle < 0 || @selected_particle >= @anim[:particles].length || - @anim[:particles][@selected_particle][:name] == "SE" + if !show_particle_sprite?(@selected_particle) @sel_frame_sprite.visible = false return end diff --git a/Data/Scripts/905_Anim player/001_Anim player.rb b/Data/Scripts/905_Anim player/001_Anim player.rb index 14b651197..54d18221a 100644 --- a/Data/Scripts/905_Anim player/001_Anim player.rb +++ b/Data/Scripts/905_Anim player/001_Anim player.rb @@ -82,7 +82,7 @@ class AnimationPlayer #----------------------------------------------------------------------------- - def set_up_particle(particle, target_idx = -1) + def set_up_particle(particle, target_idx = -1, instance = 0) particle_sprite = AnimationPlayer::ParticleSprite.new # Get/create a sprite sprite = nil @@ -127,23 +127,25 @@ class AnimationPlayer particle_sprite.foe_flip = particle[:foe_flip] end # Find earliest command and add a "make visible" command then + delay = AnimationPlayer::Helper.get_particle_delay(particle, instance) if sprite && !particle_sprite.battler_sprite? - first_cmd = -1 - particle.each_pair do |property, cmds| - next if !cmds.is_a?(Array) || cmds.empty? - cmds.each do |cmd| - first_cmd = cmd[0] if first_cmd < 0 || first_cmd > cmd[0] - end + first_cmd = AnimationPlayer::Helper.get_first_command_frame(particle) + particle_sprite.add_set_process(:visible, (first_cmd + delay) * slowdown, true) if first_cmd >= 0 + # Apply random frame + if particle[:random_frame_max] && particle[:random_frame_max] > 0 + particle_sprite.add_set_process(:frame, (first_cmd + delay) * slowdown, rand(particle[:random_frame_max] + 1)) end - particle_sprite.add_set_process(:visible, first_cmd * slowdown, true) if first_cmd >= 0 end # Add all commands + spawner_type = particle[:spawner] || :none + regular_properties_skipped = AnimationPlayer::Helper::PROPERTIES_SET_BY_SPAWNER[spawner_type] || [] particle.each_pair do |property, cmds| next if !cmds.is_a?(Array) || cmds.empty? + next if regular_properties_skipped.include?(property) cmds.each do |cmd| if cmd[1] == 0 if sprite - particle_sprite.add_set_process(property, cmd[0] * slowdown, cmd[2]) + particle_sprite.add_set_process(property, (cmd[0] + delay) * slowdown, cmd[2]) else # SE particle filename = nil @@ -159,31 +161,77 @@ class AnimationPlayer else filename = "Anim/" + cmd[2] end - particle_sprite.add_set_process(property, cmd[0] * slowdown, [filename, cmd[3], cmd[4]]) if filename + particle_sprite.add_set_process(property, (cmd[0] + delay) * slowdown, [filename, cmd[3], cmd[4]]) if filename end else - particle_sprite.add_move_process(property, cmd[0] * slowdown, cmd[1] * slowdown, cmd[2], cmd[3] || :linear) + particle_sprite.add_move_process(property, (cmd[0] + delay) * slowdown, cmd[1] * slowdown, cmd[2], cmd[3] || :linear) end end end + # Add spawner commands + add_spawner_commands(particle_sprite, particle, instance, delay) # Finish up @anim_sprites.push(particle_sprite) end + def add_spawner_commands(particle_sprite, particle, instance, delay) + life_start = AnimationPlayer::Helper.get_first_command_frame(particle) + life_end = AnimationPlayer::Helper.get_last_command_frame(particle) + life_end = AnimationPlayer::Helper.get_duration(particles) if life_end < 0 + lifetime = life_end - life_start + spawner_type = particle[:spawner] || :none + case spawner_type + when :random_direction, :random_direction_gravity + angle = rand(360) + angle = rand(360) if angle >= 180 && spawner_type == :random_direction_gravity # Prefer upwards angles + speed = rand(150, 250) + start_x_speed = speed * Math.cos(angle * Math::PI / 180) + start_y_speed = -speed * Math.sin(angle * Math::PI / 180) + start_x = (start_x_speed * 0.05) + rand(-8, 8) + start_y = (start_y_speed * 0.05) + rand(-8, 8) + # Set initial positions + [:x, :y].each do |property| + offset = (property == :x) ? start_x : start_y + particle[property].each do |cmd| + next if cmd[1] > 0 + particle_sprite.add_set_process(property, (cmd[0] + delay) * slowdown, cmd[2] + offset) + break + end + end + # Set movements + particle_sprite.add_move_process(:x, + (life_start + delay) * slowdown, lifetime * slowdown, + start_x + (start_x_speed * lifetime / 20.0), :linear) + if spawner_type == :random_direction_gravity + particle_sprite.add_move_process(:y, + (life_start + delay) * slowdown, lifetime * slowdown, + [start_y_speed / slowdown, AnimationPlayer::Helper::GRAVITY_STRENGTH.to_f / (slowdown * slowdown)], :gravity) + else + particle_sprite.add_move_process(:y, + (life_start + delay) * slowdown, lifetime * slowdown, + start_y + (start_y_speed * lifetime / 20.0), :linear) + end + end + end + # Creates sprites and ParticleSprites, and sets sprite properties that won't # change during the animation. def set_up particles.each do |particle| - if GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus]) && @targets - one_per_side = [:target_side_foreground, :target_side_background].include?(particle[:focus]) - sides_covered = [] - @targets.each do |target| - next if one_per_side && sides_covered.include?(target.index % 2) - set_up_particle(particle, target.index) - sides_covered.push(target.index % 2) + qty = 1 + qty = particle[:spawn_quantity] || 1 if particle[:spawner] && particle[:spawner] != :none + qty.times do |i| + if GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus]) && @targets + one_per_side = [:target_side_foreground, :target_side_background].include?(particle[:focus]) + sides_covered = [] + @targets.each do |target| + next if one_per_side && sides_covered.include?(target.index % 2) + set_up_particle(particle, target.index, i) + sides_covered.push(target.index % 2) + end + else + set_up_particle(particle, -1, i) end - else - set_up_particle(particle) end end reset_anim_sprites diff --git a/Data/Scripts/905_Anim player/003_AnimPlayerHelper.rb b/Data/Scripts/905_Anim player/003_AnimPlayerHelper.rb index 815773f2b..eab3bd2e1 100644 --- a/Data/Scripts/905_Anim player/003_AnimPlayerHelper.rb +++ b/Data/Scripts/905_Anim player/003_AnimPlayerHelper.rb @@ -2,6 +2,11 @@ # Methods used by both AnimationPlayer and AnimationEditor::Canvas. #=============================================================================== module AnimationPlayer::Helper + PROPERTIES_SET_BY_SPAWNER = { + :random_direction => [:x, :y], + :random_direction_gravity => [:x, :y] + } + GRAVITY_STRENGTH = 300 BATTLE_MESSAGE_BAR_HEIGHT = 96 # NOTE: You shouldn't need to change this. module_function @@ -19,6 +24,39 @@ module AnimationPlayer::Helper return ret end + # Returns the frame that the particle has its earliest command. + def get_first_command_frame(particle) + ret = -1 + particle.each_pair do |property, cmds| + next if !cmds.is_a?(Array) || cmds.empty? + cmds.each do |cmd| + ret = cmd[0] if ret < 0 || ret > cmd[0] + end + end + return (ret >= 0) ? ret : 0 + end + + # Returns the frame that the particle has (the end of) its latest command. + def get_last_command_frame(particle) + ret = -1 + particle.each_pair do |property, cmds| + next if !cmds.is_a?(Array) || cmds.empty? + cmds.each do |cmd| + ret = cmd[0] + cmd[1] if ret < cmd[0] + cmd[1] + end + end + return ret + end + + # For spawner particles + def get_particle_delay(particle, instance) + case particle[:spawner] || :none + when :random_direction, :random_direction_gravity + return instance / 4 + end + return 0 + end + #----------------------------------------------------------------------------- def get_xy_focus(particle, user_index, target_index, user_coords, target_coords) @@ -200,6 +238,12 @@ module AnimationPlayer::Helper ret += (end_val - start_val) * (1 - (((-2 * x) + 2) * ((-2 * x) + 2) / 2)) end return ret.round + when :gravity # Used by particle spawner + # end_val is [initial speed, gravity] + # s = ut + 1/2 at^2 + t = now - start_time + ret = start_val + (end_val[0] * t) + (end_val[1] * t * t / 2) + return ret.round end raise _INTL("Unknown interpolation method {1}.", interpolation) end