diff --git a/Data/Scripts/801_UI controls/Control elements/002_Label.rb b/Data/Scripts/801_UI controls/Control elements/002_Label.rb index 122cd9bb2..8d6d429fc 100644 --- a/Data/Scripts/801_UI controls/Control elements/002_Label.rb +++ b/Data/Scripts/801_UI controls/Control elements/002_Label.rb @@ -20,6 +20,10 @@ class UIControls::Label < UIControls::BaseControl refresh end + def text_width + return self.bitmap.text_size(@text).width + end + def refresh super if @header diff --git a/Data/Scripts/801_UI controls/Control elements/007_Button.rb b/Data/Scripts/801_UI controls/Control elements/007_Button.rb index 4a87efbe1..70eea2802 100644 --- a/Data/Scripts/801_UI controls/Control elements/007_Button.rb +++ b/Data/Scripts/801_UI controls/Control elements/007_Button.rb @@ -8,11 +8,13 @@ class UIControls::Button < UIControls::BaseControl BUTTON_HEIGHT = 28 # Used when @fixed_size is false # TODO: This will also depend on the font size. TEXT_BASE_OFFSET_Y = 18 # Text is centred vertically in the button + HIGHLIGHT_COLOR = Color.green def initialize(width, height, viewport, text = "") super(width, height, viewport) @text = text @fixed_size = false + @highlight = false end def set_fixed_size @@ -37,6 +39,10 @@ class UIControls::Button < UIControls::BaseControl invalidate end + def disabled? + return highlighted? || super + end + def set_changed @value = true super @@ -47,12 +53,33 @@ class UIControls::Button < UIControls::BaseControl super end + def highlighted? + return @highlight + end + + def set_highlighted + return if highlighted? + @highlight = true + invalidate + end + + def set_not_highlighted + return if !highlighted? + @highlight = false + invalidate + end + #----------------------------------------------------------------------------- def refresh super - # Draw disabled colour - if disabled? + if highlighted? + # Draw highligted colour + self.bitmap.fill_rect(@button_rect.x, @button_rect.y, + @button_rect.width, @button_rect.height, + HIGHLIGHT_COLOR) + elsif disabled? + # Draw disabled colour self.bitmap.fill_rect(@button_rect.x, @button_rect.y, @button_rect.width, @button_rect.height, DISABLED_COLOR) diff --git a/Data/Scripts/902_Anim GameData/001_Animation.rb b/Data/Scripts/902_Anim GameData/001_Animation.rb index e3cba63df..f3dcd6612 100644 --- a/Data/Scripts/902_Anim GameData/001_Animation.rb +++ b/Data/Scripts/902_Anim GameData/001_Animation.rb @@ -275,6 +275,29 @@ module GameData return ret end + def inspect + ret = super.chop + ": " + case @type + when :move then ret += _INTL("[Move]") + when :opp_move then ret += _INTL("[Foe Move]") + when :common then ret += _INTL("[Common]") + when :opp_common then ret += _INTL("[Foe Common]") + else + raise _INTL("Unknown animation type.") + end + case @type + when :move, :opp_move + move_data = GameData::Move.try_get(@move) + move_name = (move_data) ? move_data.name : @move + ret += " " + move_name + when :common, :opp_common + ret += " " + @move + end + ret += " (" + @version.to_s + ")" if @version > 0 + ret += " - " + @name if @name + return ret + end + def move_animation? return [:move, :opp_move].include?(@type) end diff --git a/Data/Scripts/904_Anim Editor/001_AnimationEditor.rb b/Data/Scripts/904_Anim Editor/001_AnimationEditor.rb index fc1fd3da0..ad6d216ac 100644 --- a/Data/Scripts/904_Anim Editor/001_AnimationEditor.rb +++ b/Data/Scripts/904_Anim Editor/001_AnimationEditor.rb @@ -220,7 +220,7 @@ class AnimationEditor :canvas_bg => "indoor1", # NOTE: These sprite names are also used in Pokemon.play_cry and so should # be a species ID (being a string is fine). - :user_sprite_name => "ARCANINE", + :user_sprite_name => "DRAGONITE", :target_sprite_name => "CHARIZARD" } end @@ -285,7 +285,7 @@ class AnimationEditor ].each_with_index do |tab, i| btn = UIControls::Button.new(100, 28, pane.viewport, tab[2]) btn.set_fixed_size - btn.disable if tab[0] == component + btn.set_highlighted if tab[0] == component pane.add_control_at(tab[1], btn, next_pos_x, next_pos_y) next_pos_x += btn.width end @@ -313,6 +313,7 @@ class AnimationEditor end def set_play_controls_contents + @components[:play_controls].add_play_controls @components[:play_controls].duration = @components[:particle_list].duration end @@ -330,7 +331,6 @@ class AnimationEditor move_ctrl.max_rows = 16 anim_properties.add_labelled_number_text_box(:version, _INTL("Version"), 0, 99, 0) anim_properties.add_labelled_text_box(:name, _INTL("Name"), "") - # TODO: Have two TextBoxes, one for folder and one for filename? anim_properties.add_labelled_text_box(:pbs_path, _INTL("PBS filepath"), "") anim_properties.add_labelled_checkbox(:has_user, _INTL("Involves a user?"), true) anim_properties.add_labelled_checkbox(:has_target, _INTL("Involves a target?"), true) @@ -420,6 +420,53 @@ class AnimationEditor #----------------------------------------------------------------------------- + def play_animation + # TODO: Grey out the rest of the screen. + play_controls = @components[:play_controls] + # Set up canvas as a pseudo-battle screen + @components[:canvas].prepare_to_play_animation + play_controls.prepare_to_play_animation + # Set up fake battlers for the animation player + user_battler = nil + if !@anim[:no_user] + user_battler = AnimationPlayer::FakeBattler.new(@settings[:user_index], @settings[:user_sprite_name]) + end + target_battlers = nil + if !@anim[:no_target] + target_battlers = [] + @settings[:target_indices].each do |idx| + target_battlers.push(AnimationPlayer::FakeBattler.new(idx, @settings[:target_sprite_name])) + end + end + # Create animation player + anim_player = AnimationPlayer.new(@anim, user_battler, target_battlers, @components[:canvas]) + anim_player.looping = @components[:play_controls].looping + anim_player.slowdown = @components[:play_controls].slowdown + anim_player.set_up + # Play animation + anim_player.start + loop do + Graphics.update + Input.update + anim_player.update + # TODO: Maybe get elapsed time from anim_player and pass it to + # play_controls to be drawn? + play_controls.update + if play_controls.changed? + if play_controls.values.keys.include?(:stop) + play_controls.clear_changed + break + end + end + break if anim_player.finished? + end + anim_player.dispose + @components[:canvas].end_playing_animation + play_controls.end_playing_animation + end + + #----------------------------------------------------------------------------- + def refresh_component_visibility(component_sym) # Panes are all mutually exclusive side_pane = AnimationEditor::SidePanes.get_pane(component_sym) @@ -558,8 +605,10 @@ class AnimationEditor refresh end when :play_controls - # TODO: Will the play controls ever signal themselves as changed? I don't - # think so. + case property + when :play + @ready_to_play = true + end when :particle_list case property when :add_particle @@ -739,7 +788,10 @@ class AnimationEditor Graphics.update Input.update update - if @captured.nil? && @quit + if @ready_to_play + play_animation + @ready_to_play = false + elsif @captured.nil? && @quit case message(_INTL("Do you want to save changes to the animation?"), [:yes, _INTL("Yes")], [:no, _INTL("No")], [:cancel, _INTL("Cancel")]) when :yes diff --git a/Data/Scripts/904_Anim Editor/010_AnimationSelector.rb b/Data/Scripts/904_Anim Editor/010_AnimationSelector.rb index ff54a2f30..584fdfab7 100644 --- a/Data/Scripts/904_Anim Editor/010_AnimationSelector.rb +++ b/Data/Scripts/904_Anim Editor/010_AnimationSelector.rb @@ -298,13 +298,13 @@ class AnimationEditor::AnimationSelector # Put the correct list into the moves list case @animation_type when 0 - @components.get_control(:moves).disable - @components.get_control(:commons).enable + @components.get_control(:moves).set_highlighted + @components.get_control(:commons).set_not_highlighted @components.get_control(:moves_list).values = @move_list @components.get_control(:moves_label).text = _INTL("Moves") when 1 - @components.get_control(:moves).enable - @components.get_control(:commons).disable + @components.get_control(:moves).set_not_highlighted + @components.get_control(:commons).set_highlighted @components.get_control(:moves_list).values = @common_list @components.get_control(:moves_label).text = _INTL("Common animations") end diff --git a/Data/Scripts/904_Anim Editor/901_ParticleDataHelper.rb b/Data/Scripts/904_Anim Editor/901_ParticleDataHelper.rb index 75cdd3bc4..c2f27855e 100644 --- a/Data/Scripts/904_Anim Editor/901_ParticleDataHelper.rb +++ b/Data/Scripts/904_Anim Editor/901_ParticleDataHelper.rb @@ -4,18 +4,6 @@ module AnimationEditor::ParticleDataHelper module_function - def get_duration(particles) - ret = 0 - particles.each do |particle| - particle.each_pair do |property, value| - next if !value.is_a?(Array) || value.length == 0 - max = value.last[0] + value.last[1] # Keyframe + duration - ret = max if ret < max - end - end - return ret - end - def get_keyframe_particle_value(particle, property, frame) if !GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES.include?(property) raise _INTL("Couldn't get default value for property {1} for particle {2}.", @@ -34,28 +22,9 @@ module AnimationEditor::ParticleDataHelper next end # In a "MoveXYZ" command; need to interpolate - case (cmd[3] || :linear) - when :linear - ret[0] = lerp(ret[0], cmd[2], cmd[1], cmd[0], frame).to_i - when :ease_in # Quadratic - x = (frame - cmd[0]) / cmd[1].to_f - ret[0] += (cmd[2] - ret[0]) * x * x - ret[0] = ret[0].round - when :ease_out # Quadratic - x = (frame - cmd[0]) / cmd[1].to_f - ret[0] += (cmd[2] - ret[0]) * (1 - ((1 - x) * (1 - x))) - ret[0] = ret[0].round - when :ease_both # Quadratic - x = (frame - cmd[0]) / cmd[1].to_f - if x < 0.5 - ret[0] += (cmd[2] - ret[0]) * x * x * 2 - else - ret[0] += (cmd[2] - ret[0]) * (1 - (((-2 * x) + 2) * ((-2 * x) + 2) / 2)) - end - ret[0] = ret[0].round - else - raise _INTL("Unknown interpolation method {1}.", cmd[3]) - end + ret[0] = AnimationPlayer::Helper.interpolate( + (cmd[3] || :linear), ret[0], cmd[2], cmd[1], cmd[0], frame + ) ret[1] = true # Interpolating break end @@ -68,7 +37,7 @@ module AnimationEditor::ParticleDataHelper first_cmd = (["User", "Target"].include?(particle[:name])) ? 0 : -1 first_visible_cmd = -1 particle.each_pair do |prop, value| - next if !value.is_a?(Array) || value.length == 0 + next if !value.is_a?(Array) || value.empty? first_cmd = value[0][0] if first_cmd < 0 || first_cmd > value[0][0] first_visible_cmd = value[0][0] if prop == :visible && (first_visible_cmd < 0 || first_visible_cmd > value[0][0]) end @@ -111,7 +80,7 @@ module AnimationEditor::ParticleDataHelper if !["User", "Target", "SE"].include?(particle[:name]) earliest = duration particle.each_pair do |prop, value| - next if !value.is_a?(Array) || value.length == 0 + next if !value.is_a?(Array) || value.empty? earliest = value[0][0] if earliest > value[0][0] end ret[earliest] = true @@ -152,7 +121,7 @@ module AnimationEditor::ParticleDataHelper # [+/- duration, interpolation type] --- MoveXYZ (duration's sign is whether # it makes the value higher or lower) def get_particle_property_commands_timeline(particle, property, commands) - return nil if !commands || commands.length == 0 + return nil if !commands || commands.empty? if particle[:name] == "SE" ret = [] commands.each { |cmd| ret[cmd[0]] = 0 } @@ -192,7 +161,7 @@ module AnimationEditor::ParticleDataHelper se_particle = particles.select { |particle| particle[:name] == "SE" }[0] if se_particle se_particle.each_pair do |property, values| - next if !values.is_a?(Array) || values.length == 0 + next if !values.is_a?(Array) || values.empty? ret = values.any? { |value| value[0] == frame } break if ret end @@ -310,7 +279,7 @@ module AnimationEditor::ParticleDataHelper first_cmd = (["User", "Target", "SE"].include?(particle[:name])) ? 0 : -1 first_non_visible_cmd = -1 particle.each_pair do |prop, value| - next if !value.is_a?(Array) || value.length == 0 + next if !value.is_a?(Array) || value.empty? next if prop == property && value[0][0] == frame first_cmd = value[0][0] if first_cmd < 0 || first_cmd > value[0][0] next if prop == :visible 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 2c98b175a..d0109be0a 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 @@ -11,6 +11,7 @@ # 9999+ = UI #=============================================================================== class AnimationEditor::Canvas < Sprite + attr_reader :sprites # Only used while playing the animation attr_reader :values FRAME_SIZE = 48 @@ -25,7 +26,6 @@ class AnimationEditor::Canvas < Sprite @captured = nil @user_coords = [] @target_coords = [] - @playing = false # TODO: What should this affect? Is it needed? initialize_background initialize_battlers initialize_particle_sprites @@ -195,15 +195,40 @@ class AnimationEditor::Canvas < Sprite #----------------------------------------------------------------------------- def prepare_to_play_animation - # TODO: Hide particle sprites, set battler sprites to starting positions so - # that the animation can play properly. Also need a way to end this - # override after the animation finishes playing. This method does not - # literally play the animation; the main editor screen or playback - # control does that. + @sprites = {} + # Populate @sprites with sprites that are present during battle, and reset + # their x/y/z values so the animation player knows where they start + idx = user_index + particle_idx = @anim[:particles].index { |particle| particle[:name] == "User" } + @sprites["pokemon_#{idx}"] = @battler_sprites[idx] + @battler_sprites[idx].x = @user_coords[0] + @battler_sprites[idx].y = @user_coords[1] + offset_xy = AnimationPlayer::Helper.get_xy_offset(@anim[:particles][particle_idx], @battler_sprites[idx]) + @battler_sprites[idx].x += offset_xy[0] + @battler_sprites[idx].y += offset_xy[1] + focus_z = AnimationPlayer::Helper.get_z_focus(@anim[:particles][particle_idx], idx, idx) + AnimationPlayer::Helper.apply_z_focus_to_sprite(@battler_sprites[idx], 0, focus_z) + @battler_sprites[idx].z = 0 + particle_idx = @anim[:particles].index { |particle| particle[:name] == "Target" } + target_indices.each do |idx| + @sprites["pokemon_#{idx}"] = @battler_sprites[idx] + @battler_sprites[idx].x = @target_coords[idx][0] + @battler_sprites[idx].y = @target_coords[idx][1] + offset_xy = AnimationPlayer::Helper.get_xy_offset(@anim[:particles][particle_idx], @battler_sprites[idx]) + @battler_sprites[idx].x += offset_xy[0] + @battler_sprites[idx].y += offset_xy[1] + focus_z = AnimationPlayer::Helper.get_z_focus(@anim[:particles][particle_idx], idx, idx) + AnimationPlayer::Helper.apply_z_focus_to_sprite(@battler_sprites[idx], 0, focus_z) + end + # TODO: Also add background/bases and so on. + hide_all_sprites + @sel_frame_sprite.visible = false @playing = true end def end_playing_animation + @sprites.clear + @sprites = nil @playing = false refresh end @@ -298,17 +323,21 @@ class AnimationEditor::Canvas < Sprite def refresh_battler_graphics if !@user_sprite_name || !@user_sprite_name || @user_sprite_name != @settings[:user_sprite_name] @user_sprite_name = @settings[:user_sprite_name] + @user_bitmap_front_name = GameData::Species.front_sprite_filename(@user_sprite_name) + @user_bitmap_back_name = GameData::Species.back_sprite_filename(@user_sprite_name) @user_bitmap_front&.dispose @user_bitmap_back&.dispose - @user_bitmap_front = RPG::Cache.load_bitmap("Graphics/Pokemon/Front/", @user_sprite_name) - @user_bitmap_back = RPG::Cache.load_bitmap("Graphics/Pokemon/Back/", @user_sprite_name) + @user_bitmap_front = RPG::Cache.load_bitmap("", @user_bitmap_front_name) + @user_bitmap_back = RPG::Cache.load_bitmap("", @user_bitmap_back_name) end if !@target_bitmap_front || !@target_sprite_name || @target_sprite_name != @settings[:target_sprite_name] @target_sprite_name = @settings[:target_sprite_name] + @target_bitmap_front_name = GameData::Species.front_sprite_filename(@target_sprite_name) + @target_bitmap_back_name = GameData::Species.back_sprite_filename(@target_sprite_name) @target_bitmap_front&.dispose @target_bitmap_back&.dispose - @target_bitmap_front = RPG::Cache.load_bitmap("Graphics/Pokemon/Front/", @target_sprite_name) - @target_bitmap_back = RPG::Cache.load_bitmap("Graphics/Pokemon/Back/", @target_sprite_name) + @target_bitmap_front = RPG::Cache.load_bitmap("", @target_bitmap_front_name) + @target_bitmap_back = RPG::Cache.load_bitmap("", @target_bitmap_back_name) end end @@ -403,108 +432,24 @@ class AnimationEditor::Canvas < Sprite # Set opacity spr.opacity = values[:opacity] # Set coordinates - spr.x = values[:x] - spr.y = values[:y] - case particle[:focus] - when :foreground, :midground, :background - when :user - spr.x += @user_coords[0] - spr.y += @user_coords[1] - when :target - spr.x += @target_coords[target_idx][0] - spr.y += @target_coords[target_idx][1] - when :user_and_target - user_pos = @user_coords - target_pos = @target_coords[target_idx] - distance = GameData::Animation::USER_AND_TARGET_SEPARATION - spr.x = user_pos[0] + ((values[:x].to_f / distance[0]) * (target_pos[0] - user_pos[0])).to_i - spr.y = user_pos[1] + ((values[:y].to_f / distance[1]) * (target_pos[1] - user_pos[1])).to_i - when :user_side_foreground, :user_side_background - base_coords = Battle::Scene.pbBattlerPosition(user_index) - spr.x += base_coords[0] - spr.y += base_coords[1] - when :target_side_foreground, :target_side_background - base_coords = Battle::Scene.pbBattlerPosition(target_idx) - spr.x += base_coords[0] - spr.y += base_coords[1] - end + focus_xy = AnimationPlayer::Helper.get_xy_focus(particle, user_index, target_idx, + @user_coords, @target_coords[target_idx]) + AnimationPlayer::Helper.apply_xy_focus_to_sprite(spr, :x, values[:x], focus_xy) + AnimationPlayer::Helper.apply_xy_focus_to_sprite(spr, :y, values[:y], focus_xy) # Set graphic and ox/oy (may also alter y coordinate) - case particle[:graphic] - when "USER", "USER_OPP", "USER_FRONT", "USER_BACK", - "TARGET", "TARGET_OPP", "TARGET_FRONT", "TARGET_BACK" - case particle[:graphic] - when "USER" - spr.bitmap = (user_index.even?) ? @user_bitmap_back : @user_bitmap_front - when "USER_OPP" - spr.bitmap = (user_index.even?) ? @user_bitmap_front : @user_bitmap_back - when "USER_FRONT" - spr.bitmap = @user_bitmap_front - when "USER_BACK" - spr.bitmap = @user_bitmap_back - when "TARGET" - spr.bitmap = (target_idx.even?) ? @target_bitmap_back : @target_bitmap_front - when "TARGET_OPP" - spr.bitmap = (target_idx.even?) ? @target_bitmap_front : @target_bitmap_back - when "TARGET_FRONT" - spr.bitmap = @target_bitmap_front - when "TARGET_BACK" - spr.bitmap = @target_bitmap_back - end - spr.ox = spr.bitmap.width / 2 - spr.oy = spr.bitmap.height - spr.y += spr.bitmap.height / 2 - else - spr.bitmap = RPG::Cache.load_bitmap("Graphics/Battle animations/", particle[:graphic]) - if [:foreground, :midground, :background].include?(particle[:focus]) && - spr.bitmap.width == AnimationEditor::CANVAS_WIDTH && - spr.bitmap.height >= AnimationEditor::CANVAS_HEIGHT - @message_bar_sprite.y - spr.ox = 0 - spr.oy = 0 - elsif spr.bitmap.width > spr.bitmap.height * 2 - spr.src_rect.set(values[:frame] * spr.bitmap.height, 0, spr.bitmap.height, spr.bitmap.height) - spr.ox = spr.bitmap.height / 2 - spr.oy = spr.bitmap.height / 2 - else - spr.src_rect.set(0, 0, spr.bitmap.width, spr.bitmap.height) - spr.ox = spr.bitmap.width / 2 - spr.oy = spr.bitmap.height / 2 - end - if particle[:graphic][/\[\s*bottom\s*\]\s*$/i] # [bottom] at end of filename - spr.oy = spr.bitmap.height - end - end + AnimationPlayer::Helper.set_bitmap_and_origin(particle, spr, user_index, target_idx, + [@user_bitmap_front_name, @user_bitmap_back_name], + [@target_bitmap_front_name, @target_bitmap_back_name]) + offset_xy = AnimationPlayer::Helper.get_xy_offset(particle, spr) + spr.x += offset_xy[0] + spr.y += offset_xy[1] + # Set frame + # TODO: Should this always happens or only if the graphic is a spritesheet? + # I don't think there's harm in it always being set. + spr.src_rect.x = values[:frame].floor * spr.src_rect.width # Set z (priority) - spr.z = values[:z] - case particle[:focus] - when :foreground - spr.z += 2000 - when :midground - spr.z += 1000 - when :background - # NOTE: No change. - when :user - spr.z += 1000 + ((100 * ((user_index / 2) + 1)) * (user_index.even? ? 1 : -1)) - when :target - spr.z += 1000 + ((100 * ((target_idx / 2) + 1)) * (target_idx.even? ? 1 : -1)) - when :user_and_target - user_pos = 1000 + ((100 * ((user_index / 2) + 1)) * (user_index.even? ? 1 : -1)) - target_pos = 1000 + ((100 * ((target_idx / 2) + 1)) * (target_idx.even? ? 1 : -1)) - distance = GameData::Animation::USER_AND_TARGET_SEPARATION[2] - if values[:z] >= 0 - spr.z += user_pos - elsif values[:z] <= distance - spr.z += target_pos - else - spr.z = user_pos + ((values[:z].to_f / distance) * (target_pos - user_pos)).to_i - end - when :user_side_foreground, :target_side_foreground - this_idx = (particle[:focus] == :user_side_foreground) ? user_index : target_idx - spr.z += 1000 - spr.z += 1000 if this_idx.even? # On player's side - when :user_side_background, :target_side_background - this_idx = (particle[:focus] == :user_side_background) ? user_index : target_idx - spr.z += 1000 if this_idx.even? # On player's side - end + focus_z = AnimationPlayer::Helper.get_z_focus(particle, user_index, target_idx) + AnimationPlayer::Helper.apply_z_focus_to_sprite(spr, values[:z], focus_z) # Set various other properties spr.zoom_x = values[:zoom_x] / 100.0 spr.zoom_y = values[:zoom_y] / 100.0 @@ -544,11 +489,7 @@ class AnimationEditor::Canvas < Sprite update_selected_particle_frame end - def refresh - refresh_bg_graphics - ensure_battler_sprites - refresh_battler_graphics - refresh_battler_positions + def hide_all_sprites [@battler_sprites, @battler_frame_sprites].each do |sprites| sprites.each { |s| s.visible = false if s && !s.disposed? } end @@ -561,6 +502,14 @@ class AnimationEditor::Canvas < Sprite end end end + end + + def refresh + refresh_bg_graphics + ensure_battler_sprites + refresh_battler_graphics + refresh_battler_positions + hide_all_sprites @anim[:particles].each_with_index do |particle, i| if GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus]) refresh_particle(i) # Because there can be multiple targets diff --git a/Data/Scripts/904_Anim Editor/Anim Editor elements/002_PlayControls.rb b/Data/Scripts/904_Anim Editor/Anim Editor elements/002_PlayControls.rb index c4c39c772..ccd7358b4 100644 --- a/Data/Scripts/904_Anim Editor/Anim Editor elements/002_PlayControls.rb +++ b/Data/Scripts/904_Anim Editor/Anim Editor elements/002_PlayControls.rb @@ -1,25 +1,193 @@ #=============================================================================== -# TODO +# #=============================================================================== -class AnimationEditor::PlayControls < UIControls::BaseControl +class AnimationEditor::PlayControls < UIControls::ControlsContainer + attr_reader :slowdown, :looping + + ROW_HEIGHT = 28 + PLAY_BUTTON_X = 241 + PLAY_BUTTON_Y = 13 + PLAY_BUTTON_SIZE = 22 + LOOP_BUTTON_X = PLAY_BUTTON_X + PLAY_BUTTON_SIZE + 12 + LOOP_BUTTON_Y = 16 + LOOP_BUTTON_SIZE = 16 + # NOTE: Slowdown label is centered horizontally over the buttons. + SLOWDOWN_LABEL_Y = 0 + SLOWDOWN_BUTTON_X = 1 + SLOWDOWN_BUTTON_Y = ROW_HEIGHT - 1 + SLOWDOWN_BUTTON_WIDTH = 32 + SLOWDOWN_BUTTON_SPACING = -3 + # NOTE: Duration label and value are centered horizontally on DURATION_TEXT_X. + DURATION_TEXT_X = 464 + DURATION_LABEL_Y = SLOWDOWN_LABEL_Y + DURATION_VALUE_Y = ROW_HEIGHT + SLOWDOWN_FACTORS = [1, 2, 4, 6, 8] + ICON_COLOR = Color.black + def initialize(x, y, width, height, viewport) - super(width, height, viewport) - self.x = x - self.y = y + super(x, y, width, height) + @viewport.z = viewport.z + 10 + generate_button_bitmaps @duration = 0 + @slowdown = SLOWDOWN_FACTORS[0] + @looping = false end + #----------------------------------------------------------------------------- + + def add_play_controls + # Play button + play_button = UIControls::BitmapButton.new(PLAY_BUTTON_X, PLAY_BUTTON_Y, self.viewport, @play_button_bitmap) + play_button.set_interactive_rects + play_button.disable + @controls.push([:play, play_button]) + # Stop button + stop_button = UIControls::BitmapButton.new(PLAY_BUTTON_X, PLAY_BUTTON_Y, self.viewport, @stop_button_bitmap) + stop_button.set_interactive_rects + stop_button.visible = false + @controls.push([:stop, stop_button]) + # Loop buttons + loop_button = UIControls::BitmapButton.new(LOOP_BUTTON_X, LOOP_BUTTON_Y, self.viewport, @play_once_button_bitmap) + loop_button.set_interactive_rects + loop_button.visible = false if @looping + @controls.push([:loop, loop_button]) + unloop_button = UIControls::BitmapButton.new(LOOP_BUTTON_X, LOOP_BUTTON_Y, self.viewport, @looping_button_bitmap) + unloop_button.set_interactive_rects + unloop_button.visible = false if !@looping + @controls.push([:unloop, unloop_button]) + # Slowdown label + duration_label = UIControls::Label.new(200, ROW_HEIGHT, self.viewport, _INTL("Slowdown factor")) + duration_label.x = SLOWDOWN_BUTTON_X + (SLOWDOWN_FACTORS.length * (SLOWDOWN_BUTTON_WIDTH - SLOWDOWN_BUTTON_SPACING) / 2) + duration_label.x -= (duration_label.text_width / 2) + 5 + duration_label.y = SLOWDOWN_LABEL_Y + @controls.push([:slowdown_label, duration_label]) + # Slowdown factor buttons + SLOWDOWN_FACTORS.each_with_index do |value, i| + button = UIControls::Button.new(SLOWDOWN_BUTTON_WIDTH, ROW_HEIGHT, self.viewport, value.to_s) + button.set_fixed_size + button.x = SLOWDOWN_BUTTON_X + ((SLOWDOWN_BUTTON_WIDTH - SLOWDOWN_BUTTON_SPACING) * i) + button.y = SLOWDOWN_BUTTON_Y + button.set_interactive_rects + button.set_highlighted if value == @slowdown + @controls.push([("slowdown" + value.to_s).to_sym, button]) + end + # Duration label + duration_label = UIControls::Label.new(200, ROW_HEIGHT, self.viewport, _INTL("Duration")) + duration_label.x = DURATION_TEXT_X - (duration_label.text_width / 2) + duration_label.y = DURATION_LABEL_Y + @controls.push([:duration_label, duration_label]) + # Duration value + duration_value = UIControls::Label.new(200, ROW_HEIGHT, self.viewport, _INTL("{1}s", 0.0)) + duration_value.x = DURATION_TEXT_X - (duration_value.text_width / 2) + duration_value.y = DURATION_VALUE_Y + @controls.push([:duration_value, duration_value]) + end + + def generate_button_bitmaps + @play_button_bitmap = Bitmap.new(PLAY_BUTTON_SIZE, PLAY_BUTTON_SIZE) + (PLAY_BUTTON_SIZE - 2).times do |j| + @play_button_bitmap.fill_rect(PLAY_BUTTON_SIZE / 4, j + 1, (j >= (PLAY_BUTTON_SIZE - 2) / 2) ? PLAY_BUTTON_SIZE - j : j + 3, 1, ICON_COLOR) + end + @stop_button_bitmap = Bitmap.new(PLAY_BUTTON_SIZE, PLAY_BUTTON_SIZE) + @stop_button_bitmap.fill_rect(4, 4, PLAY_BUTTON_SIZE - 8, PLAY_BUTTON_SIZE - 8, ICON_COLOR) + # Loop button + @play_once_button_bitmap = Bitmap.new(LOOP_BUTTON_SIZE, LOOP_BUTTON_SIZE) + @play_once_button_bitmap.fill_rect(1, 7, 11, 2, ICON_COLOR) + @play_once_button_bitmap.fill_rect(8, 5, 2, 6, ICON_COLOR) + @play_once_button_bitmap.fill_rect(10, 6, 1, 4, ICON_COLOR) + @play_once_button_bitmap.fill_rect(13, 1, 2, 14, ICON_COLOR) + @looping_button_bitmap = Bitmap.new(LOOP_BUTTON_SIZE, LOOP_BUTTON_SIZE) + [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, + 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, + 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, + 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, + 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, + 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0].each_with_index do |val, i| + next if val == 0 + @looping_button_bitmap.fill_rect(1 + (i % 14), 1 + (i / 14), 1, 1, ICON_COLOR) + end + end + + def dispose + @play_button_bitmap.dispose + @stop_button_bitmap.dispose + @play_once_button_bitmap.dispose + @looping_button_bitmap.dispose + super + end + + #----------------------------------------------------------------------------- + def duration=(new_val) return if @duration == new_val @duration = new_val + if @duration == 0 + get_control(:play).disable + else + get_control(:play).enable + end + ctrl = get_control(:duration_value) + ctrl.text = _INTL("{1}s", @duration / 20.0) + ctrl.x = DURATION_TEXT_X - (ctrl.text_width / 2) refresh end #----------------------------------------------------------------------------- - def refresh + def prepare_to_play_animation + get_control(:play).visible = false + get_control(:stop).visible = true + @controls.each { |ctrl| ctrl[1].disable if ctrl[0] != :stop } + end + + def end_playing_animation + get_control(:stop).visible = false + get_control(:play).visible = true + @controls.each { |ctrl| ctrl[1].enable } + end + + #----------------------------------------------------------------------------- + + def update super - 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)) + if @values + @values.keys.each do |key| + case key + when :loop + get_control(:loop).visible = false + get_control(:unloop).visible = true + @looping = true + @values.delete(key) + when :unloop + get_control(:unloop).visible = false + get_control(:loop).visible = true + @looping = false + @values.delete(key) + else + if key.to_s[/slowdown/] + # A slowdown button was pressed; apply its effect now + @slowdown = key.to_s.sub("slowdown", "").to_i + @controls.each do |ctrl| + next if !ctrl[0].to_s[/slowdown\d+/] + if ctrl[0].to_s.sub("slowdown", "").to_i == @slowdown + ctrl[1].set_highlighted + else + ctrl[1].set_not_highlighted + end + end + @values.delete(key) + end + end + end + @values = nil if @values.empty? + end end end diff --git a/Data/Scripts/904_Anim Editor/Anim Editor elements/003_ParticleList.rb b/Data/Scripts/904_Anim Editor/Anim Editor elements/003_ParticleList.rb index a83178690..e64abd464 100644 --- a/Data/Scripts/904_Anim Editor/Anim Editor elements/003_ParticleList.rb +++ b/Data/Scripts/904_Anim Editor/Anim Editor elements/003_ParticleList.rb @@ -177,6 +177,7 @@ class AnimationEditor::ParticleList < UIControls::BaseControl @list_viewport.dispose @commands_bg_viewport.dispose @commands_viewport.dispose + super end def dispose_listed_sprites @@ -430,7 +431,7 @@ class AnimationEditor::ParticleList < UIControls::BaseControl #----------------------------------------------------------------------------- def calculate_duration - @duration = AnimationEditor::ParticleDataHelper.get_duration(@particles) + @duration = AnimationPlayer::Helper.get_duration(@particles) @duration += DURATION_BUFFER end diff --git a/Data/Scripts/905_Anim player/001_Anim player.rb b/Data/Scripts/905_Anim player/001_Anim player.rb new file mode 100644 index 000000000..ca0858675 --- /dev/null +++ b/Data/Scripts/905_Anim player/001_Anim player.rb @@ -0,0 +1,230 @@ +#=============================================================================== +# +#=============================================================================== +class AnimationPlayer + attr_accessor :looping + attr_accessor :slowdown # 1 = normal speed, 2 = half speed, 3 = one third speed, etc. + + # animation is either a GameData::Animation or a hash made from one. + # user is a Battler, or nil + # targets is an array of Battlers, or nil + def initialize(animation, user, targets, scene) + @animation = animation + @user = user + @targets = targets + @scene = scene + @viewport = @scene.viewport + @sprites = @scene.sprites + initialize_battler_sprite_names + initialize_battler_coordinates + @looping = false + @slowdown = 1 + @timer_start = nil + @anim_sprites = [] # Each is a ParticleSprite + @duration = total_duration + end + + # Doesn't actually create any sprites; just gathers them into a more useful array + def initialize_battler_sprite_names + @battler_sprites = [] + if @user + pkmn = @user.pokemon + @battler_sprites[@user.index] = [] + @battler_sprites[@user.index].push(GameData::Species.front_sprite_filename( + pkmn.species, pkmn.form, pkmn.gender)) + @battler_sprites[@user.index].push(GameData::Species.back_sprite_filename( + pkmn.species, pkmn.form, pkmn.gender)) + end + if @targets + @targets.each do |target| + pkmn = target.pokemon + @battler_sprites[target.index] = [] + @battler_sprites[target.index].push(GameData::Species.front_sprite_filename( + pkmn.species, pkmn.form, pkmn.gender)) + @battler_sprites[target.index].push(GameData::Species.back_sprite_filename( + pkmn.species, pkmn.form, pkmn.gender)) + end + end + end + + def initialize_battler_coordinates + @user_coords = nil + if @user + sprite = @sprites["pokemon_#{@user.index}"] + @user_coords = [sprite.x, sprite.y - (sprite.bitmap.height / 2)] + end + @target_coords = [] + if @targets + @targets.each do |target| + sprite = @sprites["pokemon_#{target.index}"] + @target_coords[target.index] = [sprite.x, sprite.y - (sprite.bitmap.height / 2)] + end + end + end + + def dispose + @anim_sprites.each { |particle| particle.dispose } + @anim_sprites.clear + end + + #----------------------------------------------------------------------------- + + def particles + return (@animation.is_a?(GameData::Animation)) ? @animation.particles : @animation[:particles] + end + + # Return value is in seconds. + def total_duration + ret = AnimationPlayer::Helper.get_duration(particles) / 20.0 + ret *= slowdown + return ret + end + + #----------------------------------------------------------------------------- + + def set_up_particle(particle, target_idx = -1) + particle_sprite = AnimationPlayer::ParticleSprite.new + # Get/create a sprite + sprite = nil + case particle[:name] + when "User" + sprite = @sprites["pokemon_#{@user.index}"] + particle_sprite.set_as_battler_sprite + when "Target" + sprite = @sprites["pokemon_#{target_idx}"] + particle_sprite.set_as_battler_sprite + when "SE" + # Intentionally no sprite created + else + sprite = Sprite.new(@viewport) + end + particle_sprite.sprite = sprite if sprite + # Set sprite's graphic and ox/oy + if sprite + AnimationPlayer::Helper.set_bitmap_and_origin(particle, sprite, @user.index, target_idx, + @battler_sprites[@user.index], @battler_sprites[target_idx]) + end + # Calculate x/y/z focus values and additional x/y modifier and pass them all + # to particle_sprite + focus_xy = AnimationPlayer::Helper.get_xy_focus(particle, @user.index, target_idx, + @user_coords, @target_coords[target_idx]) + offset_xy = AnimationPlayer::Helper.get_xy_offset(particle, sprite) + focus_z = AnimationPlayer::Helper.get_z_focus(particle, @user.index, target_idx) + particle_sprite.focus_xy = focus_xy + particle_sprite.offset_xy = offset_xy + particle_sprite.focus_z = focus_z + # Find earliest command and add a "make visible" command then + 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 + end + particle_sprite.add_set_process(:visible, first_cmd * slowdown, true) if first_cmd >= 0 + end + # Add all commands + particle.each_pair do |property, cmds| + next if !cmds.is_a?(Array) || cmds.empty? + cmds.each do |cmd| + if cmd[1] == 0 + if sprite + particle_sprite.add_set_process(property, cmd[0] * slowdown, cmd[2]) + else + # SE particle + filename = nil + case property + when :user_cry + filename = GameData::Species.cry_filename_from_pokemon(@user.pokemon) if @user + when :target_cry + # NOTE: If there are multiple targets, only the first one's cry + # will be played. + if @targets && !@targets.empty? + filename = GameData::Species.cry_filename_from_pokemon(@targets.first.pokemon) + end + else + filename = "Anim/" + cmd[2] + end + particle_sprite.add_set_process(property, cmd[0] * 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) + end + end + end + # Finish up + @anim_sprites.push(particle_sprite) + 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) + end + else + set_up_particle(particle) + end + end + reset_anim_sprites + end + + # Sets the initial properties of all sprites, and marks all processes as not + # yet started. + def reset_anim_sprites + @anim_sprites.each { |particle| particle.reset_processes } + end + + #----------------------------------------------------------------------------- + + def start + @timer_start = System.uptime + end + + def playing? + return !@timer_start.nil? + end + + def finish + @timer_start = nil + @finished = true + end + + def finished? + return @finished + end + + def can_continue_battle? + return finished? + end + + #----------------------------------------------------------------------------- + + def update + return if !playing? + if @need_reset + reset_anim_sprites + start + @need_reset = false + end + time_now = System.uptime + elapsed = time_now - @timer_start + # Update all particles/sprites + @anim_sprites.each { |particle| particle.update(elapsed) } + # Finish or loop the animation + if elapsed >= @duration + if looping + @need_reset = true + else + finish + end + end + end +end diff --git a/Data/Scripts/905_Anim player/002_ParticleSprite.rb b/Data/Scripts/905_Anim player/002_ParticleSprite.rb new file mode 100644 index 000000000..a1f23377d --- /dev/null +++ b/Data/Scripts/905_Anim player/002_ParticleSprite.rb @@ -0,0 +1,141 @@ +#=============================================================================== +# NOTE: This assumes that processes are added (for a given property) in the +# order they happen. +#=============================================================================== +class AnimationPlayer::ParticleSprite + attr_accessor :sprite + attr_accessor :focus_xy, :offset_xy, :focus_z + + FRAMES_PER_SECOND = 20.0 + + def initialize + @processes = [] + @sprite = nil + @battler_sprite = false + initialize_values + end + + def initialize_values + @values = GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES.clone + end + + def dispose + return if battler_sprite? || !@sprite || @sprite.disposed? + @sprite.bitmap&.dispose + @sprite.dispose + end + + #----------------------------------------------------------------------------- + + def set_as_battler_sprite + @battler_sprite = true + @values[:visible] = true + end + + def battler_sprite? + return @battler_sprite + end + + #----------------------------------------------------------------------------- + + def add_set_process(property, frame, value) + add_move_process(property, frame, 0, value, :none) + end + + def add_move_process(property, start_frame, duration, value, interpolation = :linear) + # First nil is progress (nil = not started, true = running, false = finished) + # Second nil is start value (set when the process starts running) + @processes.push([property, start_frame, duration, value, interpolation, nil, nil]) + end + + # Sets sprite's initial For looping purposes. + def reset_processes + initialize_values + set_as_battler_sprite if battler_sprite? # Start battler sprites as visible + @values.each_pair { |property, value| update_sprite_property(property, value) } + @processes.each { |process| process[5] = nil } + end + + #----------------------------------------------------------------------------- + + def start_process(process) + return if !process[5].nil? + process[6] = @values[process[0]] + process[5] = true + end + + def update_process_value(process, elapsed_time) + # SetXYZ + if process[2] == 0 + @values[process[0]] = process[3] + process[5] = false # Mark process as finished + return + end + # MoveXYZ + @values[process[0]] = AnimationPlayer::Helper.interpolate( + process[4], process[6], process[3], process[2] / FRAMES_PER_SECOND, + process[1] / FRAMES_PER_SECOND, elapsed_time + ) + if elapsed_time >= (process[1] + process[2]) / FRAMES_PER_SECOND + process[5] = false # Mark process as finished + end + end + + def update_sprite(changed_properties) + changed_properties.uniq! + changed_properties.each do |property| + update_sprite_property(property, @values[property]) + end + end + + def update_sprite_property(property, value) + if !@sprite + pbSEPlay(*value) if [:se, :user_cry, :target_cry].include?(property) && value + return + end + case property + when :frame then @sprite.src_rect.x = value.floor * @sprite.src_rect.width + when :blending then @sprite.blend_type = value + when :flip then @sprite.mirror = value + when :x + AnimationPlayer::Helper.apply_xy_focus_to_sprite(@sprite, :x, value.round, @focus_xy) + @sprite.x += @offset_xy[0] + when :y + AnimationPlayer::Helper.apply_xy_focus_to_sprite(@sprite, :y, value.round, @focus_xy) + @sprite.y += @offset_xy[1] + when :z + AnimationPlayer::Helper.apply_z_focus_to_sprite(@sprite, value, @focus_z) + when :zoom_x then @sprite.zoom_x = value / 100.0 + when :zoom_y then @sprite.zoom_y = value / 100.0 + when :angle then @sprite.angle = value + when :visible then @sprite.visible = value + when :opacity then @sprite.opacity = value + when :color_red then @sprite.color.red = value + when :color_green then @sprite.color.green = value + when :color_blue then @sprite.color.blue = value + when :color_alpha then @sprite.color.alpha = value + when :tone_red then @sprite.tone.red = value + when :tone_green then @sprite.tone.green = value + when :tone_blue then @sprite.tone.blue = value + when :tone_gray then @sprite.tone.gray = value + end + end + + def update(elapsed_time) + frame = (elapsed_time * FRAMES_PER_SECOND).floor + changed_properties = [] + @processes.each do |process| + # Skip processes that aren't due to start yet + next if process[1] > frame + # Skip processes that have already fully happened + next if process[5] == false + # Mark process as running if it isn't already + start_process(process) + # Update process's value + update_process_value(process, elapsed_time) + changed_properties.push(process[0]) # Record property as having changed + end + # Apply changed values to sprite + update_sprite(changed_properties) if !changed_properties.empty? + end +end diff --git a/Data/Scripts/905_Anim player/003_AnimPlayerHelper.rb b/Data/Scripts/905_Anim player/003_AnimPlayerHelper.rb new file mode 100644 index 000000000..1bae53f30 --- /dev/null +++ b/Data/Scripts/905_Anim player/003_AnimPlayerHelper.rb @@ -0,0 +1,198 @@ +#=============================================================================== +# Methods used by both AnimationPlayer and AnimationEditor::Canvas. +#=============================================================================== +module AnimationPlayer::Helper + BATTLE_MESSAGE_BAR_HEIGHT = 96 # NOTE: You shouldn't need to change this. + + module_function + + # Returns the duration of the animation in frames (1/20ths of a second). + def get_duration(particles) + ret = 0 + particles.each do |particle| + particle.each_pair do |property, value| + next if !value.is_a?(Array) || value.empty? + max = value.last[0] + value.last[1] # Keyframe + duration + ret = max if ret < max + end + end + return ret + end + + #----------------------------------------------------------------------------- + + def get_xy_focus(particle, user_index, target_index, user_coords, target_coords) + ret = nil + case particle[:focus] + when :foreground, :midground, :background + when :user + ret = [user_coords.clone] + when :target + ret = [target_coords.clone] + when :user_and_target + ret = [user_coords.clone, target_coords.clone] + when :user_side_foreground, :user_side_background + ret = [Battle::Scene.pbBattlerPosition(user_index)] + when :target_side_foreground, :target_side_background + ret = [Battle::Scene.pbBattlerPosition(target_idx)] + end + return ret + end + + def get_xy_offset(particle, sprite) + ret = [0, 0] + case particle[:graphic] + when "USER", "USER_OPP", "USER_FRONT", "USER_BACK", + "TARGET", "TARGET_OPP", "TARGET_FRONT", "TARGET_BACK" + ret[1] += sprite.bitmap.height / 2 if sprite + end + return ret + end + + # property is :x or :y. + def apply_xy_focus_to_sprite(sprite, property, value, focus) + result = value + coord_idx = (property == :x) ? 0 : 1 + if focus + if focus.length == 2 + distance = GameData::Animation::USER_AND_TARGET_SEPARATION + result = focus[0][coord_idx] + ((value.to_f / distance[coord_idx]) * (focus[1][coord_idx] - focus[0][coord_idx])).to_i + else + result = value + focus[0][coord_idx] + end + end + case property + when :x then sprite.x = result + when :y then sprite.y = result + end + end + + #----------------------------------------------------------------------------- + + # Returns either a number or an array of two numbers. + def get_z_focus(particle, user_index, target_index) + ret = 0 + case particle[:focus] + when :foreground + ret = 2000 + when :midground + ret = 1000 + when :background + # NOTE: No change. + when :user + ret = 1000 + ((100 * ((user_index / 2) + 1)) * (user_index.even? ? 1 : -1)) + when :target + ret = 1000 + ((100 * ((target_index / 2) + 1)) * (target_index.even? ? 1 : -1)) + when :user_and_target + user_pos = 1000 + ((100 * ((user_index / 2) + 1)) * (user_index.even? ? 1 : -1)) + target_pos = 1000 + ((100 * ((target_index / 2) + 1)) * (target_index.even? ? 1 : -1)) + ret = [user_pos, target_pos] + when :user_side_foreground, :target_side_foreground + this_idx = (particle[:focus] == :user_side_foreground) ? user_index : target_index + ret = 1000 + ret += 1000 if this_idx.even? # On player's side + when :user_side_background, :target_side_background + this_idx = (particle[:focus] == :user_side_background) ? user_index : target_index + ret = 1000 if this_idx.even? # On player's side + end + return ret + end + + def apply_z_focus_to_sprite(sprite, z, focus) + if focus.is_a?(Array) + distance = GameData::Animation::USER_AND_TARGET_SEPARATION[2] + if z >= 0 + sprite.z = z + focus[0] + elsif z <= distance + sprite.z = z + focus[1] + else + sprite.z = focus[0] + ((z.to_f / distance) * (focus[1] - focus[0])).to_i + end + elsif focus + sprite.z = z + focus + else + sprite.z = z + end + end + + #----------------------------------------------------------------------------- + + # user_sprites, target_sprites = [front sprite, back sprite] + def set_bitmap_and_origin(particle, sprite, user_index, target_index, user_sprites, target_sprites) + return if sprite&.is_a?(Battle::Scene::BattlerSprite) + case particle[:graphic] + when "USER", "USER_OPP", "USER_FRONT", "USER_BACK", + "TARGET", "TARGET_OPP", "TARGET_FRONT", "TARGET_BACK" + filename = nil + case particle[:graphic] + when "USER" + filename = (user_index.even?) ? user_sprites[1] : user_sprites[0] + when "USER_OPP" + filename = (user_index.even?) ? user_sprites[0] : user_sprites[1] + when "USER_FRONT" + filename = user_sprites[0] + when "USER_BACK" + filename = user_sprites[1] + when "TARGET" + filename = (target_index.even?) ? target_sprites[1] : target_sprites[0] + when "TARGET_OPP" + filename = (target_index.even?) ? target_sprites[0] : target_sprites[1] + when "TARGET_FRONT" + filename = target_sprites[0] + when "TARGET_BACK" + filename = target_sprites[1] + end + sprite.bitmap = RPG::Cache.load_bitmap("", filename) + sprite.ox = sprite.bitmap.width / 2 + sprite.oy = sprite.bitmap.height + else + sprite.bitmap = RPG::Cache.load_bitmap("Graphics/Battle animations/", particle[:graphic]) + sprite.src_rect.set(0, 0, sprite.bitmap.width, sprite.bitmap.height) + if [:foreground, :midground, :background].include?(particle[:focus]) && + sprite.bitmap.width == Settings::SCREEN_WIDTH && + sprite.bitmap.height >= Settings::SCREEN_HEIGHT - BATTLE_MESSAGE_BAR_HEIGHT + sprite.ox = 0 + sprite.oy = 0 + elsif sprite.bitmap.width > sprite.bitmap.height * 2 + sprite.src_rect.set(0, 0, sprite.bitmap.height, sprite.bitmap.height) + sprite.ox = sprite.bitmap.height / 2 + sprite.oy = sprite.bitmap.height / 2 + else + sprite.ox = sprite.bitmap.width / 2 + sprite.oy = sprite.bitmap.height / 2 + end + if particle[:graphic][/\[\s*bottom\s*\]\s*$/i] # [bottom] at end of filename + sprite.oy = sprite.bitmap.height + end + end + end + + #----------------------------------------------------------------------------- + + def interpolate(interpolation, start_val, end_val, duration, start_time, now) + case interpolation + when :linear + return lerp(start_val, end_val, duration, start_time, now).to_i + when :ease_in # Quadratic + ret = start_val + x = (now - start_time) / duration.to_f + ret += (end_val - start_val) * x * x + return ret.round + when :ease_out # Quadratic + ret = start_val + x = (now - start_time) / duration.to_f + ret += (end_val - start_val) * (1 - ((1 - x) * (1 - x))) + return ret.round + when :ease_both # Quadratic + ret = start_val + x = (now - start_time) / duration.to_f + if x < 0.5 + ret += (end_val - start_val) * x * x * 2 + else + ret += (end_val - start_val) * (1 - (((-2 * x) + 2) * ((-2 * x) + 2) / 2)) + end + return ret.round + end + raise _INTL("Unknown interpolation method {1}.", interpolation) + end +end diff --git a/Data/Scripts/905_Anim player/010_Battle code.rb b/Data/Scripts/905_Anim player/010_Battle code.rb new file mode 100644 index 000000000..482dba8bb --- /dev/null +++ b/Data/Scripts/905_Anim player/010_Battle code.rb @@ -0,0 +1,187 @@ +#=============================================================================== +# +#=============================================================================== +class Battle::Scene + BETTER_ANIMATION_DEFAULTS = { + :NORMAL => [:TACKLE, :SONICBOOM, :DEFENSECURL, :EXPLOSION, :SWIFT, :TAILWHIP], + :FIGHTING => [:MACHPUNCH, :AURASPHERE, :BULKUP, nil, nil, nil], + :FLYING => [:WINGATTACK, :GUST, :ROOST, nil, :AIRCUTTER, :FEATHERDANCE], + :POISON => [:POISONSTING, :SLUDGE, :ACIDARMOR, nil, :ACID, :POISONPOWDER], + :GROUND => [:SANDTOMB, :MUDSLAP, :MUDSPORT, :EARTHQUAKE, :EARTHPOWER, :SANDATTACK], + :ROCK => [:ROCKTHROW, :POWERGEM, :ROCKPOLISH, :ROCKSLIDE, nil, :SANDSTORM], + :BUG => [:TWINEEDLE, :BUGBUZZ, :QUIVERDANCE, nil, :STRUGGLEBUG, :STRINGSHOT], + :GHOST => [:ASTONISH, :SHADOWBALL, :GRUDGE, nil, nil, :CONFUSERAY], + :STEEL => [:IRONHEAD, :MIRRORSHOT, :IRONDEFENSE, nil, nil, :METALSOUND], + :FIRE => [:FIREPUNCH, :EMBER, :SUNNYDAY, nil, :INCINERATE, :WILLOWISP], + :WATER => [:CRABHAMMER, :WATERGUN, :AQUARING, nil, :SURF, :WATERSPORT], + :GRASS => [:VINEWHIP, :MAGICALLEAF, :COTTONGUARD, :RAZORLEAF, nil, :SPORE], + :ELECTRIC => [:THUNDERPUNCH, :THUNDERSHOCK, :CHARGE, nil, :DISCHARGE, :THUNDERWAVE], + :PSYCHIC => [:ZENHEADBUTT, :CONFUSION, :CALMMIND, nil, :SYNCHRONOISE, :MIRACLEEYE], + :ICE => [:ICEPUNCH, :ICEBEAM, :MIST, nil, :POWDERSNOW, :HAIL], + :DRAGON => [:DRAGONCLAW, :DRAGONRAGE, :DRAGONDANCE, nil, :TWISTER, nil], + :DARK => [:KNOCKOFF, :DARKPULSE, :HONECLAWS, nil, :SNARL, :EMBARGO], + :FAIRY => [:TACKLE, :FAIRYWIND, :MOONLIGHT, nil, :DAZZLINGGLEAM, :SWEETKISS] + } + + #----------------------------------------------------------------------------- + + def pbAnimation(move_id, user, targets, version = 0) + anims = find_move_animation(move_id, version, user&.index) + return if !anims || anims.empty? + if anims[0].is_a?(GameData::Animation) # New animation + pbSaveShadows do + # NOTE: anims.sample is a random valid animation. + play_better_animation(anims.sample, user, targets) + end + else # Old animation + anim = anims[0] + target = (targets.is_a?(Array)) ? targets[0] : targets + animations = pbLoadBattleAnimations + return if !animations + pbSaveShadows do + if anims[1] # On opposing side and using OppMove animation + pbAnimationCore(animations[anim], target, user, true) + else # On player's side, and/or using Move animation + pbAnimationCore(animations[anim], user, target) + end + end + end + end + + alias __newanims__pbCommonAnimation pbCommonAnimation unless method_defined?(:__newanims__pbCommonAnimation) + def pbCommonAnimation(anim_name, user = nil, target = nil) + return if nil_or_empty?(anim_name) + anims = try_get_better_common_animation(anim_name, user.index) + if anims + # NOTE: anims.sample is a random valid animation. + play_better_animation(anims.sample, user, target) + else + __newanims__pbCommonAnimation(anim_name, user, target) + end + end + + #----------------------------------------------------------------------------- + + # Returns an array of GameData::Animation if a new animation(s) is found. + # Return [animation index, shouldn't be flipped] if an old animation is found. + def find_move_animation(move_id, version, user_index) + # Get animation + anims = find_move_animation_for_move(move_id, version, user_index) + return anims if anims + # Get information to decide which default animation to try + move_data = GameData::Move.get(move_id) + target_data = GameData::Target.get(move_data.target) + move_type = move_data.type + default_idx = move_data.category + default_idx += 3 if target_data.num_targets > 1 || target_data.affects_foe_side + default_idx += 3 if move_data.status? && target_data.num_targets > 0 + # Check for a default animation + wanted_move = BETTER_ANIMATION_DEFAULTS[move_type][default_idx] + anims = find_move_animation_for_move(wanted_move, 0, user_index) + return anims if anims + if default_idx >= 3 + wanted_move = BETTER_ANIMATION_DEFAULTS[move_type][default_idx - 3] + anims = find_move_animation_for_move(wanted_move, 0, user_index) + return anims if anims + return nil if wanted_move == :TACKLE # No need to check for Tackle's animation twice + end + # Use Tackle's animation + return find_move_animation_for_move(:TACKLE, 0, user_index) + end + + # Find an animation(s) for the given move_id. + def find_move_animation_for_move(move_id, version, user_index) + # Find new animation + anims = try_get_better_move_animation(move_id, version, user_index) + return anims if anims + if version > 0 + anims = try_get_better_move_animation(move_id, 0, user_index) + return anims if anims + end + # Find old animation + anim = pbFindMoveAnimDetails(pbLoadMoveToAnim, move_id, user_index, version) + return anim + end + + # Finds a new animation for the given move_id and version. Prefers opposing + # animations if the user is opposing. Can return multiple animations. + def try_get_better_move_animation(move_id, version, user_index) + ret = [] + backup_ret = [] + GameData::Animation.each do |anim| + next if !anim.move_animation? || anim.ignore + next if anim.move != move_id.to_s + next if anim.version != version + if !user_index + ret.push(anim) + next + end + if user_index.even? # User is on player's side + ret.push(anim) if !anim.opposing_animation? + else # User is on opposing side + (anim.opposing_animation?) ? ret.push(anim) : backup_ret.push(anim) + end + end + return ret if !ret.empty? + return backup_ret if !backup_ret.empty? + return nil + end + + def try_get_better_common_animation(anim_name, user_index) + ret = [] + backup_ret = [] + GameData::Animation.each do |anim| + next if !anim.common_animation? || anim.ignore + next if anim.move != anim_name + if !user_index + ret.push(anim) + next + end + if user_index.even? # User is on player's side + ret.push(anim) if !anim.opposing_animation? + else # User is on opposing side + (anim.opposing_animation?) ? ret.push(anim) : backup_ret.push(anim) + end + end + return ret if !ret.empty? + return backup_ret if !backup_ret.empty? + return nil + end + + #----------------------------------------------------------------------------- + + def play_better_animation(anim_data, user, targets) + return if !anim_data + @briefMessage = false + # Memorize old battler coordinates, to be reset after the animation + old_battler_coords = [] + if user + sprite = @sprites["pokemon_#{user.index}"] + old_battler_coords[user.index] = [sprite.x, sprite.y] + end + if targets + targets.each do |target| + sprite = @sprites["pokemon_#{target.index}"] + old_battler_coords[target.index] = [sprite.x, sprite.y] + end + end + # Create animation player + anim_player = AnimationPlayer.new(anim_data, user, targets, self) + anim_player.set_up + # Play animation + anim_player.start + loop do + pbUpdate + anim_player.update + break if anim_player.can_continue_battle? + end + anim_player.dispose + # Restore old battler coordinates + old_battler_coords.each_with_index do |values, i| + next if !values + sprite = @sprites["pokemon_#{i}"] + sprite.x = values[0] + sprite.y = values[1] + end + end +end diff --git a/Data/Scripts/905_Anim player/011_Battle z modifiers.rb b/Data/Scripts/905_Anim player/011_Battle z modifiers.rb new file mode 100644 index 000000000..e10886741 --- /dev/null +++ b/Data/Scripts/905_Anim player/011_Battle z modifiers.rb @@ -0,0 +1,71 @@ +# TODO: Hardcoded animations have incorrect z values because of the change to +# other sprites' z values. + +#=============================================================================== +# +#=============================================================================== +class Battle::Scene + alias __newanims__pbInitSprites pbInitSprites unless method_defined?(:__newanims__pbInitSprites) + def pbInitSprites + __newanims__pbInitSprites + ["battle_bg", "battle_bg2"].each { |spr| @sprites[spr].z = -200 } + 2.times do |side| + @sprites["base_#{side}"].z = -199 + end + @sprites["cmdBar_bg"].z += 9999 + @sprites["messageBox"].z += 9999 + @sprites["messageWindow"].z += 9999 + @sprites["commandWindow"].z += 9999 + @sprites["fightWindow"].z += 9999 + @sprites["targetWindow"].z += 9999 + 2.times do |side| + @sprites["partyBar_#{side}"].z += 9999 + NUM_BALLS.times do |i| + @sprites["partyBall_#{side}_#{i}"].z += 9999 + end + # Ability splash bars + @sprites["abilityBar_#{side}"].z += 9999 if USE_ABILITY_SPLASH + end + @battle.battlers.each_with_index do |b, i| + @sprites["dataBox_#{i}"].z += 9999 if b + end + end +end + +#=============================================================================== +# Pokémon sprite (used in battle) +#=============================================================================== +class Battle::Scene::BattlerSprite < RPG::Sprite + def pbSetPosition + return if !@_iconBitmap + pbSetOrigin + if @index.even? + self.z = 1100 + (100 * @index / 2) + else + self.z = 1000 - (100 * (@index + 1) / 2) + end + # Set original position + p = Battle::Scene.pbBattlerPosition(@index, @sideSize) + @spriteX = p[0] + @spriteY = p[1] + # Apply metrics + @pkmn.species_data.apply_metrics_to_sprite(self, @index) + end +end + +#=============================================================================== +# Shadow sprite for Pokémon (used in battle) +#=============================================================================== +class Battle::Scene::BattlerShadowSprite < RPG::Sprite + def pbSetPosition + return if !@_iconBitmap + pbSetOrigin + self.z = -198 + # Set original position + p = Battle::Scene.pbBattlerPosition(@index, @sideSize) + self.x = p[0] + self.y = p[1] + # Apply metrics + @pkmn.species_data.apply_metrics_to_sprite(self, @index, true) + end +end diff --git a/Data/Scripts/905_Anim player/012_Fake battler.rb b/Data/Scripts/905_Anim player/012_Fake battler.rb new file mode 100644 index 000000000..97991fc35 --- /dev/null +++ b/Data/Scripts/905_Anim player/012_Fake battler.rb @@ -0,0 +1,26 @@ +#=============================================================================== +# +#=============================================================================== +class AnimationPlayer::FakeBattler + attr_reader :index + attr_reader :pokemon + + def initialize(index, species, form = 0, gender = 0) + @index = index + @pokemon = AnimationPlayer::FakePokemon.new(species, form, gender) + end +end + +#=============================================================================== +# +#=============================================================================== +class AnimationPlayer::FakePokemon + attr_reader :species, :form, :gender + + def initialize(species, form = 0, gender = 0) + # NOTE: species will be a string, but it doesn't need to be a symbol. + @species = species + @form = form + @gender = gender + end +end