Added canvas to new animation editor (isn't interactive yet), improved example animations

This commit is contained in:
Maruno17
2024-01-25 21:07:16 +00:00
parent 94f0a9c8d0
commit 4455c093b8
245 changed files with 22085 additions and 14046 deletions

View File

@@ -9,7 +9,7 @@ class UIControls::DropdownList < UIControls::BaseControl
TEXT_BOX_WIDTH = 200
TEXT_BOX_HEIGHT = 24
TEXT_BOX_PADDING = 4 # Gap between sides of text box and text
MAX_LIST_ROWS = 8
MAX_LIST_ROWS = 10
# NOTE: options is a hash: keys are symbols, values are display names.
def initialize(width, height, viewport, options, value)
@@ -32,6 +32,11 @@ class UIControls::DropdownList < UIControls::BaseControl
invalidate
end
def values=(new_vals)
@options = new_vals
@dropdown_menu.values = @options if @dropdown_menu
end
def set_interactive_rects
@button_rect = Rect.new(TEXT_BOX_X, (height - TEXT_BOX_HEIGHT) / 2,
[@box_width, width - (TEXT_BOX_X * 2)].min, TEXT_BOX_HEIGHT)

View File

@@ -14,6 +14,25 @@ module GameData
DATA_FILENAME = "animations.dat"
OPTIONAL = true
# TODO: All mentions of focus types can be found by searching for
# :user_and_target, plus there's :foreground in PARTICLE_DEFAULT_VALUES
# below.
# TODO: Add :user_ground, :target_ground?
FOCUS_TYPES = {
"Foreground" => :foreground,
"Midground" => :midground,
"Background" => :background,
"User" => :user,
"Target" => :target,
"UserAndTarget" => :user_and_target,
"UserSideForeground" => :user_side_foreground,
"UserSideBackground" => :user_side_background,
"TargetSide" => :target_side_foreground,
"TargetSideBackground" => :target_side_background,
}
FOCUS_TYPES_WITH_TARGET = [
:target, :user_and_target, :target_side_foreground, :target_side_background
]
INTERPOLATION_TYPES = {
"None" => :none,
"Linear" => :linear,
@@ -21,6 +40,7 @@ module GameData
"EaseOut" => :ease_out,
"EaseBoth" => :ease_both
}
USER_AND_TARGET_SEPARATION = [200, -200, -200] # x, y, z (from user to target)
# Properties that apply to the animation in general, not to individual
# particles. They don't change during the animation.
@@ -49,9 +69,7 @@ module GameData
"Graphic" => [:graphic, "s"],
# TODO: If more focus types are added, add ones that involve a target to
# the Compiler's check relating to "NoTarget".
"Focus" => [:focus, "e", {"User" => :user, "Target" => :target,
"UserAndTarget" => :user_and_target,
"Screen" => :screen}],
"Focus" => [:focus, "e", FOCUS_TYPES],
# TODO: FlipIfFoe, RotateIfFoe kinds of thing.
# All properties below are "SetXYZ" or "MoveXYZ". "SetXYZ" has the
@@ -66,6 +84,8 @@ module GameData
"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"],
@@ -91,9 +111,6 @@ module GameData
"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.
@@ -112,7 +129,7 @@ module GameData
PARTICLE_DEFAULT_VALUES = {
:name => "",
:graphic => "",
:focus => :screen
:focus => :foreground
}
# NOTE: Particles are invisible until their first command, and automatically
# become visible then. "User" and "Target" are visible from the start,
@@ -123,6 +140,7 @@ module GameData
:flip => false,
:x => 0,
:y => 0,
:z => 0,
:zoom_x => 100,
:zoom_y => 100,
:angle => 0,

View File

@@ -135,7 +135,7 @@ module Compiler
case particle[:name]
when "User" then particle[:focus] = :user
when "Target" then particle[:focus] = :target
else particle[:focus] = :screen
else particle[:focus] = GameData::Animation::PARTICLE_DEFAULT_VALUES[:focus]
end
end
# Ensure user/target particles have a default graphic if not given
@@ -145,9 +145,11 @@ module Compiler
when "Target" then particle[:graphic] = "TARGET"
end
end
# TODO: Ensure that particles don't have a focus involving a user if the
# animation itself doesn't involve a user.
# 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])
if hash[:no_target] && GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus])
raise _INTL("Particle \"{1}\" can't have a \"Focus\" that involves a target if property \"NoTarget\" is set to true.",
particle[:name]) + "\n" + FileLineData.linereport
end

View File

@@ -1,3 +1,6 @@
# TODO: OppMove animations have their user and target swapped, and will need
# them swapping back.
module AnimationConverter
module_function
@@ -50,6 +53,7 @@ module AnimationConverter
}
add_frames_to_new_anim_hash(anim, new_anim)
add_bg_fg_commands_to_new_anim_hash(anim, new_anim)
add_se_commands_to_new_anim_hash(anim, new_anim)
new_anim[:particles].compact!
@@ -60,6 +64,9 @@ module AnimationConverter
end
def add_frames_to_new_anim_hash(anim, hash)
# Lookup array for particle index using cel index
index_lookup = []
max_index = -1
# Set up previous frame's values
default_frame = []
default_frame[AnimFrame::X] = -999
@@ -77,30 +84,70 @@ module AnimationConverter
default_frame[AnimFrame::TONEGREEN] = 0
default_frame[AnimFrame::TONEBLUE] = 0
default_frame[AnimFrame::TONEGRAY] = 0
default_frame[AnimFrame::PATTERN] = 0
default_frame[AnimFrame::PRIORITY] = 0 # 0=back, 1=front, 2=behind focus, 3=before focus
default_frame[AnimFrame::VISIBLE] = 1 # Boolean
default_frame[AnimFrame::MIRROR] = 0 # Boolean
default_frame[AnimFrame::FOCUS] = 4 # 1=target, 2=user, 3=user and target, 4=screen
default_frame[99] = anim.graphic
last_frame_values = []
# Go through each frame
anim.length.times do |frame_num|
frame = anim[frame_num]
had_particles = []
changed_particles = []
frame.each_with_index do |cel, i|
next if !cel
# If the particle from the previous frame for this cel had a different
# focus, start a new particle.
if i > 1 && frame_num > 0 && index_lookup[i] && index_lookup[i] >= 0 &&
last_frame_values[index_lookup[i]]
this_graphic = (cel[AnimFrame::PATTERN] == -1) ? "USER" : (cel[AnimFrame::PATTERN] == -2) ? "TARGET" : anim.graphic
if last_frame_values[index_lookup[i]][AnimFrame::FOCUS] != cel[AnimFrame::FOCUS] ||
last_frame_values[index_lookup[i]][99] != this_graphic # Graphic
index_lookup[i] = -1
end
end
# Get the particle index for this cel
if !index_lookup[i] || index_lookup[i] < 0
max_index += 1
index_lookup[i] = max_index
end
idx = index_lookup[i]
had_particles.push(idx)
# i=0 for "User", i=1 for "Target"
hash[:particles][i] ||= {
:name => "Particle #{i}"
}
particle = hash[:particles][i]
if i == 0
hash[:particles][idx] ||= { :name => "Particle #{idx}" }
particle = hash[:particles][idx]
last_frame = last_frame_values[idx] || default_frame.clone
# User and target particles have specific names
if idx == 0
particle[:name] = "User"
elsif i == 1
elsif idx == 1
particle[:name] = "Target"
else
# Set graphic
case cel[AnimFrame::PATTERN]
when -1 # User's sprite
particle[:graphic] = "USER"
last_frame[99] = "USER"
when -2 # Target's sprite
particle[:graphic] = "TARGET"
last_frame[99] = "TARGET"
else
particle[:graphic] = anim.graphic
last_frame[99] = anim.graphic
end
end
# Set focus for non-User/non-Target
if idx > 1
particle[:focus] = [:foreground, :target, :user, :user_and_target, :foreground][cel[AnimFrame::FOCUS]]
last_frame[AnimFrame::FOCUS] = cel[AnimFrame::FOCUS]
end
last_frame = last_frame_values[i] || default_frame.clone
# Copy commands across
[
[AnimFrame::X, :x],
@@ -118,25 +165,182 @@ module AnimationConverter
[AnimFrame::TONEGREEN, :tone_green],
[AnimFrame::TONEBLUE, :tone_blue],
[AnimFrame::TONEGRAY, :tone_gray],
[AnimFrame::PATTERN, :frame],
[AnimFrame::PRIORITY, :z],
[AnimFrame::VISIBLE, :visible], # Boolean
[AnimFrame::MIRROR, :flip], # Boolean
].each do |property|
next if cel[property[0]] == last_frame[property[0]]
particle[property[1]] ||= []
val = cel[property[0]].to_i
val = (val == 1) if [:visible, :flip].include?(property[1])
case property[1]
when :x
# TODO: What if the animation is an OppMove one? I think this should
# be the other way around.
if particle[:focus] == :user_and_target
fraction = (val - Battle::Scene::FOCUSUSER_X).to_f / (Battle::Scene::FOCUSTARGET_X - Battle::Scene::FOCUSUSER_X)
val = (fraction * GameData::Animation::USER_AND_TARGET_SEPARATION[0]).to_i
end
when :y
# TODO: What if the animation is an OppMove one? I think this should
# be the other way around.
if particle[:focus] == :user_and_target
fraction = (val - Battle::Scene::FOCUSUSER_Y).to_f / (Battle::Scene::FOCUSTARGET_Y - Battle::Scene::FOCUSUSER_Y)
val = (fraction * GameData::Animation::USER_AND_TARGET_SEPARATION[1]).to_i
end
when :visible, :flip
val = (val == 1) # Boolean
when :z
next if idx <= 1 # User or target
case val
when 0 then val = -50 + i # Back
when 1 then val = 25 + i # Front
when 2 then val = -25 + i # Behind focus
when 3 then val = i # Before focus
end
when :frame
next if val < 0 # -1 is user, -2 is target
end
# TODO: Come up with a better way to set a particle's graphic being
# the user or target. Probably can't, due to overlapping cel
# numbers and user/target being the :graphic property which
# doesn't change.
particle[property[1]].push([frame_num, 0, val])
last_frame[property[0]] = cel[property[0]]
end
# Set graphic
particle[:graphic] = anim.graphic
# Set focus for non-User/non-Target
if i > 1
particle[:focus] = [:screen, :target, :user, :user_and_target, :screen][cel[AnimFrame::FOCUS]]
changed_particles.push(idx) if !changed_particles.include?(idx)
end
# Remember this cel's values at this frame
last_frame_values[i] = last_frame
last_frame_values[idx] = last_frame
end
# End every particle lifetime that didn't have a corresponding cel this
# frame
hash[:particles].each_with_index do |particle, idx|
next if !particle || had_particles.include?(idx)
next if ["User", "Target"].include?(particle[:name])
if last_frame_values[idx][AnimFrame::VISIBLE] == 1
particle[:visible] ||= []
particle[:visible].push([frame_num, 0, false])
changed_particles.push(idx) if !changed_particles.include?(idx)
end
last_frame_values[idx][AnimFrame::VISIBLE] = 0
next if !index_lookup.include?(idx)
lookup_idx = index_lookup.index(idx)
index_lookup[lookup_idx] = -1
end
# Add a dummy command to the user particle in the last frame if that frame
# doesn't have any commands
if frame_num == anim.length - 1 && changed_particles.empty?
hash[:particles].each_with_index do |particle, idx|
next if !particle || idx <= 1 # User or target
# TODO: Making all non-user non-target particles invisible in the last
# frame isn't a perfect solution, but it's good enough to get
# example animation data.
next if last_frame_values[idx][AnimFrame::VISIBLE] == 0
particle[:visible] ||= []
particle[:visible].push([frame_num + 1, 0, false])
end
end
end
hash[:particles][0][:focus] = :user
hash[:particles][1][:focus] = :target
# Adjust all x/y positions based on particle[:focus]
hash[:particles].each do |particle|
next if !particle
offset_x = 0
offset_y = 0
case particle[:focus]
when :user
offset_x = -Battle::Scene::FOCUSUSER_X
offset_y = -Battle::Scene::FOCUSUSER_Y
when :target
offset_x = -Battle::Scene::FOCUSTARGET_X
offset_y = -Battle::Scene::FOCUSTARGET_Y
when :user_and_target
# NOTE: No change, done above.
end
next if offset_x == 0 && offset_y == 0
particle[:x].each { |cmd| cmd[2] += offset_x }
particle[:y].each { |cmd| cmd[2] += offset_y }
end
end
# TODO: Haven't tested this as no Essentials animations use them.
def add_bg_fg_commands_to_new_anim_hash(anim, new_anim)
bg_particle = { :name => "Background", :focus => :background }
fg_particle = { :name => "Foreground", :focus => :foreground }
first_bg_frame = 999
first_fg_frame = 999
anim.timing.each do |cmd|
case cmd.timingType
when 1, 2, 3, 4 # BG graphic (set, move/recolour), FG graphic (set, move/recolour)
is_bg = (cmd.timingType <= 2)
particle = (is_bg) ? bg_particle : fg_particle
duration = (cmd.timingType == 2) ? cmd.duration : 0
added = false
if cmd.name && cmd.name != ""
particle[:graphic] ||= []
particle[:graphic].push([cmd.frame, duration, cmd.name])
added = true
end
if cmd.colorRed
particle[:color_red] ||= []
particle[:color_red].push([cmd.frame, duration, cmd.colorRed])
added = true
end
if cmd.colorGreen
particle[:color_green] ||= []
particle[:color_green].push([cmd.frame, duration, cmd.colorGreen])
added = true
end
if cmd.colorBlue
particle[:color_blue] ||= []
particle[:color_blue].push([cmd.frame, duration, cmd.colorBlue])
added = true
end
if cmd.colorAlpha
particle[:color_alpha] ||= []
particle[:color_alpha].push([cmd.frame, duration, cmd.colorAlpha])
added = true
end
if cmd.opacity
particle[:opacity] ||= []
particle[:opacity].push([cmd.frame, duration, cmd.opacity])
added = true
end
if cmd.bgX
particle[:x] ||= []
particle[:x].push([cmd.frame, duration, cmd.bgX])
added = true
end
if cmd.bgY
particle[:y] ||= []
particle[:y].push([cmd.frame, duration, cmd.bgY])
added = true
end
if added
if is_bg
first_bg_frame = [first_bg_frame, cmd.frame].min
else
first_fg_frame = [first_fg_frame, cmd.frame].min
end
end
end
end
if bg_particle.keys.length > 2
if !bg_particle[:graphic]
particle[:graphic] ||= []
particle[:graphic].push([first_bg_frame, 0, "black_screen"])
end
new_anim[:particles].push(bg_particle)
end
if fg_particle.keys.length > 2
if !fg_particle[:graphic]
particle[:graphic] ||= []
particle[:graphic].push([first_fg_frame, 0, "black_screen"])
end
new_anim[:particles].push(fg_particle)
end
end

View File

@@ -84,6 +84,7 @@ class AnimationEditor
]
def initialize(anim_id, anim)
load_settings
@anim_id = anim_id
@anim = anim
@pbs_path = anim[:pbs_path]
@@ -109,11 +110,7 @@ class AnimationEditor
# Menu bar
@components[:menu_bar] = AnimationEditor::MenuBar.new(0, 0, MENU_BAR_WIDTH, MENU_BAR_HEIGHT, @viewport)
# Canvas
@components[:canvas] = AnimationEditor::Canvas.new(@canvas_viewport)
# Play controls
@components[:play_controls] = AnimationEditor::PlayControls.new(
PLAY_CONTROLS_X, PLAY_CONTROLS_Y, PLAY_CONTROLS_WIDTH, PLAY_CONTROLS_HEIGHT, @viewport
)
@components[:canvas] = AnimationEditor::Canvas.new(@canvas_viewport, @anim, @settings)
# Side panes
[:commands_pane, :se_pane, :particle_pane, :keyframe_pane].each do |pane|
@components[pane] = UIControls::ControlsContainer.new(SIDE_PANE_X, SIDE_PANE_Y, SIDE_PANE_WIDTH, SIDE_PANE_HEIGHT)
@@ -127,6 +124,10 @@ class AnimationEditor
PARTICLE_LIST_X, PARTICLE_LIST_Y, PARTICLE_LIST_WIDTH, PARTICLE_LIST_HEIGHT, @viewport
)
@components[:particle_list].set_interactive_rects
# Play controls
@components[:play_controls] = AnimationEditor::PlayControls.new(
PLAY_CONTROLS_X, PLAY_CONTROLS_Y, PLAY_CONTROLS_WIDTH, PLAY_CONTROLS_HEIGHT, @viewport
)
# Animation properties pop-up window
@components[:animation_properties] = UIControls::ControlsContainer.new(
ANIM_PROPERTIES_X + 4, ANIM_PROPERTIES_Y, ANIM_PROPERTIES_WIDTH - 8, ANIM_PROPERTIES_HEIGHT
@@ -200,22 +201,14 @@ class AnimationEditor
end
def set_canvas_contents
@components[:canvas].bg_name = "indoor1"
end
def set_play_controls_contents
@components[:play_controls].duration = @components[:particle_list].duration
end
def set_commands_pane_contents
commands_pane = @components[:commands_pane]
commands_pane.add_header_label(:header, _INTL("Edit particle at keyframe"))
# :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_component(:commands_pane) makes the appropriate one visible.
commands_pane.add_labelled_number_text_box(:x, _INTL("X"), -(CANVAS_WIDTH + 128), CANVAS_WIDTH + 128, 0)
commands_pane.add_labelled_number_text_box(:y, _INTL("Y"), -(CANVAS_WIDTH + 128), CANVAS_HEIGHT + 128, 0)
commands_pane.add_labelled_number_text_box(:x, _INTL("X"), -200, 200, 0)
commands_pane.add_labelled_number_text_box(:y, _INTL("Y"), -200, 200, 0)
commands_pane.add_labelled_number_slider(:z, _INTL("Priority"), -50, 50, 0)
# TODO: If the graphic is user's sprite/target's sprite, make :frame instead
# a choice of front/back/same as the main sprite/opposite of the main
# sprite. Will need two controls in the same space.
@@ -232,13 +225,6 @@ class AnimationEditor
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?
@@ -271,12 +257,7 @@ class AnimationEditor
particle_pane.get_control(:name).set_blacklist("User", "Target", "SE")
particle_pane.add_labelled_label(:graphic_name, _INTL("Graphic"), "")
particle_pane.add_labelled_button(: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)
particle_pane.add_labelled_dropdown_list(:focus, _INTL("Focus"), {}, :user)
# FlipIfFoe
# RotateIfFoe
# Delete button (if not "User"/"Target"/"SE")
@@ -302,6 +283,10 @@ class AnimationEditor
@components[:particle_list].set_particles(@anim[:particles])
end
def set_play_controls_contents
@components[:play_controls].duration = @components[:particle_list].duration
end
def set_animation_properties_contents
anim_properties = @components[:animation_properties]
anim_properties.add_header_label(:header, _INTL("Animation properties"))
@@ -386,9 +371,9 @@ class AnimationEditor
def set_components_contents
set_menu_bar_contents
set_canvas_contents
set_play_controls_contents
set_side_panes_contents
set_particle_list_contents
set_play_controls_contents # Intentionally after set_particle_list_contents
set_animation_properties_contents
set_graphic_chooser_contents
set_audio_chooser_contents
@@ -396,6 +381,21 @@ class AnimationEditor
#-----------------------------------------------------------------------------
def load_settings
# TODO: Load these from a saved file.
@settings = {
:side_sizes => [1, 1],
:user_index => 0,
:target_indices => [1],
:user_opposes => false,
# TODO: Ideally be able to independently choose base graphics, which will
# be a separate setting here.
:canvas_bg => "indoor1",
:user_sprite_name => "ARCANINE",
:target_sprite_name => "ABOMASNOW"
}
end
def save
GameData::Animation.register(@anim, @anim_id)
Compiler.write_battle_animation_file(@anim[:pbs_path])
@@ -454,6 +454,7 @@ class AnimationEditor
ctrl = @components[:animation_properties].get_control(:move)
case @anim[:type]
when :move, :opp_move
# TODO: Cache this list?
move_list = []
GameData::Move.each { |m| move_list.push([m.id.to_s, m.name]) }
move_list.push(["STRUGGLE", _INTL("Struggle")]) if move_list.none? { |val| val[0] == "STRUGGLE" }
@@ -467,6 +468,8 @@ class AnimationEditor
def refresh_component_values(component_sym)
component = @components[component_sym]
case component_sym
when :canvas
component.keyframe = keyframe
when :commands_pane
new_vals = AnimationEditor::ParticleDataHelper.get_all_keyframe_particle_values(@anim[:particles][particle_index], keyframe)
component.controls.each do |ctrl|
@@ -475,8 +478,45 @@ class AnimationEditor
# TODO: new_vals[ctrl[0]][1] is whether the value is being interpolated,
# which should be indicated somehow in ctrl[1].
end
# TODO: component.get_control(:frame).disable if the particle's graphic is
# not a spritesheet or is "USER"/"TARGET"/etc. (enable otherwise).
# Set an appropriate range for the X and Y properties depending on the
# particle's focus
case @anim[:particles][particle_index][:focus]
when :foreground, :midground, :background # Cover the whole screen
component.get_control(:x).min_value = -128
component.get_control(:x).max_value = CANVAS_WIDTH + 128
component.get_control(:y).min_value = -128
component.get_control(:y).max_value = CANVAS_HEIGHT + 128
when :user, :target, :user_side_foreground, :user_side_background,
:target_side_foreground, :target_side_background # Around the focus
component.get_control(:x).min_value = -CANVAS_WIDTH
component.get_control(:x).max_value = CANVAS_WIDTH
component.get_control(:y).min_value = -CANVAS_HEIGHT
component.get_control(:y).max_value = CANVAS_HEIGHT
when :user_and_target # Covers both foci
component.get_control(:x).min_value = -CANVAS_WIDTH
component.get_control(:x).max_value = GameData::Animation::USER_AND_TARGET_SEPARATION[0] + CANVAS_WIDTH
component.get_control(:y).min_value = GameData::Animation::USER_AND_TARGET_SEPARATION[1] - CANVAS_HEIGHT
component.get_control(:y).max_value = CANVAS_HEIGHT
end
# Set an appropriate range for the priority (z) property depending on the
# particle's focus
case @anim[:particles][particle_index][:focus]
when :user_and_target
component.get_control(:z).min_value = GameData::Animation::USER_AND_TARGET_SEPARATION[2] - 50
component.get_control(:z).max_value = 50
else
component.get_control(:z).min_value = -50
component.get_control(:z).max_value = 50
end
# Disable the "Frame" control if the particle's graphic is predefined to
# be the user's or target's sprite
# TODO: Also disable it if the particle's graphic isn't a spritesheet.
if ["USER", "USER_OPP", "USER_FRONT", "USER_BACK",
"TARGET", "TARGET_OPP", "TARGET_FRONT", "TARGET_BACK"].include?(@anim[:particles][particle_index][:graphic])
component.get_control(:frame).disable
else
component.get_control(:frame).enable
end
when :se_pane
se_particle = @anim[:particles].select { |p| p[:name] == "SE" }[0]
kyfrm = keyframe
@@ -532,8 +572,32 @@ class AnimationEditor
component.get_control(:graphic).enable
component.get_control(:focus).enable
end
# TODO: Set the possible focus options depending on whether the animation
# has a target/user.
# Set the possible foci depending on whether the animation involves a
# target
# TODO: Also filter for user/no user if implemented.
if @anim[:no_target]
component.get_control(:focus).values = {
:foreground => _INTL("Foreground"),
:midground => _INTL("Midground"),
:background => _INTL("Background"),
:user => _INTL("User"),
:user_side_foreground => _INTL("In front of user's side"),
:user_side_background => _INTL("Behind user's side")
}
else
component.get_control(:focus).values = {
:foreground => _INTL("Foreground"),
:midground => _INTL("Midground"),
:background => _INTL("Background"),
:user => _INTL("User"),
:target => _INTL("Target"),
:user_and_target => _INTL("User and target"),
:user_side_foreground => _INTL("In front of user's side"),
:user_side_background => _INTL("Behind user's side"),
:target_side_foreground => _INTL("In front of target's side"),
:target_side_background => _INTL("Behind target's side")
}
end
when :animation_properties
refresh_move_property_options
case @anim[:type]
@@ -596,6 +660,7 @@ class AnimationEditor
@components[:particle_list].change_particle_commands(particle_index)
@components[:play_controls].duration = @components[:particle_list].duration
refresh_component(:commands_pane)
refresh_component(:canvas)
end
when :se_pane
case property
@@ -651,6 +716,7 @@ class AnimationEditor
new_cmds = AnimationEditor::ParticleDataHelper.set_property(particle, property, value)
@components[:particle_list].change_particle(particle_index)
refresh_component(:particle_pane)
refresh_component(:canvas)
end
when :keyframe_pane
# TODO: Stuff here once I decide what controls to add.
@@ -658,6 +724,8 @@ class AnimationEditor
# refresh if keyframe != old_keyframe || particle_index != old_particle_index
# TODO: Lots of stuff here when buttons are added to it.
when :animation_properties
# TODO: Will changes here need to refresh any other components (e.g. side
# panes)? Probably.
case property
when :type, :opp_variant
type = @components[:animation_properties].get_control(:type).value
@@ -669,12 +737,15 @@ class AnimationEditor
@anim[:type] = (opp) ? :opp_common : :common
end
refresh_component(:animation_properties)
refresh_component(:canvas)
when :pbs_path
txt = value.gsub!(/\.txt$/, "")
@anim[property] = txt
when :has_target
@anim[:no_target] = !value
# TODO: Add/delete the "Target" particle accordingly.
# TODO: Add/delete the "Target" particle accordingly. Then refresh a lot
# of components.
refresh_component(:canvas)
when :usable
@anim[:ignore] = !value
else
@@ -704,6 +775,7 @@ class AnimationEditor
end
component.clear_changed
end
# TODO: Call repaint only if component responds to it? Canvas won't.
component.repaint if sym == :particle_list || sym == :menu_bar
if @captured
@captured = nil if !component.busy?

View File

@@ -34,7 +34,12 @@ module AnimationEditor::ParticleDataHelper
next
end
# In a "MoveXYZ" command; need to interpolate
ret[0] = lerp(ret[0], cmd[2], cmd[1], cmd[0], frame).to_i
case (cmd[3] || :linear)
when :linear
ret[0] = lerp(ret[0], cmd[2], cmd[1], cmd[0], frame).to_i
else
# TODO: Use an appropriate interpolation.
end
ret[1] = true # Interpolating
break
end

View File

@@ -1,44 +1,124 @@
#===============================================================================
# TODO
# NOTE: z values:
# -200 = backdrop.
# -199 = side bases
# -198 = battler shadows.
# 0 +/-50 = background focus, foe side background.
# 900, 800, 700... +/-50 = foe battlers.
# 1000 +/-50 = foe side foreground, player side background.
# 1100, 1200, 1300... +/-50 = player battlers.
# 2000 +/-50 = player side foreground, foreground focus.
# 9999+ = UI
# TODO: Should the canvas be able to show boxes/faded sprites of particles from
# the previous keyframe? I suppose ideally, but don't worry about it.
# TODO: Battler/particle sprites should be their own class, which combine a
# sprite and a target-dependent coloured frame. Alternatively, have the
# frame be a separate sprite but only draw it around the currently
# selected particle(s).
# TODO: Ideally refresh the canvas while editing a particle's property in the
# :commands_pane component (e.g. moving a number slider but not finalising
# it). Refresh a single particle. I don't think any other side pane needs
# to refresh the canvas in the middle of changing a value. The new value
# of a control in the middle of being changed isn't part of the particle's
# data, so it'll need to be input manually somehow.
#===============================================================================
class AnimationEditor::Canvas < Sprite
attr_reader :bg_name
def initialize(viewport, anim, settings)
super(viewport)
@anim = anim
@settings = settings
@keyframe = 0
@user_coords = []
@target_coords = []
@playing = false # TODO: What should this affect? Is it needed?
initialize_background
initialize_battlers
initialize_particle_sprites
refresh
end
def initialize(viewport)
super
@bg_val = ""
def initialize_background
self.z = -200
# NOTE: The background graphic is self.bitmap.
# TODO: Add second (flipped) background graphic, for screen shake commands.
player_base_pos = Battle::Scene.pbBattlerPosition(0)
@player_base = IconSprite.new(*player_base_pos, viewport)
@player_base.z = 1
@player_base.z = -199
foe_base_pos = Battle::Scene.pbBattlerPosition(1)
@foe_base = IconSprite.new(*foe_base_pos, viewport)
@foe_base.z = 1
@foe_base.z = -199
@message_bar_sprite = Sprite.new(viewport)
@message_bar_sprite.z = 999
@message_bar_sprite.z = 9999
end
def initialize_battlers
@battler_sprites = []
end
def initialize_particle_sprites
@particle_sprites = []
end
def dispose
@message_bar_sprite.dispose
@user_bitmap_front&.dispose
@user_bitmap_back&.dispose
@target_bitmap_front&.dispose
@target_bitmap_back&.dispose
@player_base.dispose
@foe_base.dispose
@message_bar_sprite.dispose
@battler_sprites.each { |s| s.dispose if s && !s.disposed? }
@battler_sprites.clear
@particle_sprites.each do |s|
if s.is_a?(Array)
s.each { |s2| s2.dispose if s2 && !s2.disposed? }
else
s.dispose if s && !s.disposed?
end
end
@particle_sprites.clear
super
end
def bg_name=(val)
return if @bg_name == val
@bg_name = val
# TODO: Make the choice of background graphics match the in-battle one in
# def pbCreateBackdropSprites. Ideally make that method a class method
# so the canvas can use it rather than duplicate it.
self.bitmap = RPG::Cache.load_bitmap("Graphics/Battlebacks/", @bg_name + "_bg")
@player_base.setBitmap("Graphics/Battlebacks/" + @bg_name + "_base0")
@player_base.ox = @player_base.bitmap.width / 2
@player_base.oy = @player_base.bitmap.height
@foe_base.setBitmap("Graphics/Battlebacks/" + @bg_name + "_base1")
@foe_base.ox = @foe_base.bitmap.width / 2
@foe_base.oy = @foe_base.bitmap.height / 2
@message_bar_sprite.bitmap = RPG::Cache.load_bitmap("Graphics/Battlebacks/", @bg_name + "_message")
@message_bar_sprite.y = Settings::SCREEN_HEIGHT - @message_bar_sprite.height
#-----------------------------------------------------------------------------
# Returns whether the user is on the foe's (non-player's) side.
def sides_swapped?
return @settings[:user_opposes] || [:opp_move, :opp_common].include?(@anim[:type])
end
# index is a battler index (even for player's side, odd for foe's side)
def side_size(index)
side = index % 2
side = (side + 1) % 2 if sides_swapped?
return @settings[:side_sizes][side]
end
def user_index
ret = @settings[:user_index]
ret += 1 if sides_swapped?
return ret
end
def target_indices
ret = @settings[:target_indices].clone
if sides_swapped?
ret.length.times do |i|
ret[i] += (ret[i].even?) ? 1 : -1
end
end
return ret
end
def position_empty?(index)
return user_index != index && !target_indices.include?(index)
end
def keyframe=(val)
return if @keyframe == val || val < 0
@keyframe = val
refresh
end
#-----------------------------------------------------------------------------
@@ -53,9 +133,304 @@ class AnimationEditor::Canvas < Sprite
#-----------------------------------------------------------------------------
def repaint
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.
@playing = true
end
def end_playing_animation
@playing = false
refresh
end
#-----------------------------------------------------------------------------
def refresh_bg_graphics
return if @bg_name && @bg_name == @settings[:canvas_bg]
@bg_name = @settings[:canvas_bg]
# TODO: Make the choice of background graphics match the in-battle one in
# def pbCreateBackdropSprites. Ideally make that method a class method
# so the canvas can use it rather than duplicate it.
self.bitmap = RPG::Cache.load_bitmap("Graphics/Battlebacks/", @bg_name + "_bg")
@player_base.setBitmap("Graphics/Battlebacks/" + @bg_name + "_base0")
@player_base.ox = @player_base.bitmap.width / 2
@player_base.oy = @player_base.bitmap.height
@foe_base.setBitmap("Graphics/Battlebacks/" + @bg_name + "_base1")
@foe_base.ox = @foe_base.bitmap.width / 2
@foe_base.oy = @foe_base.bitmap.height / 2
@message_bar_sprite.bitmap = RPG::Cache.load_bitmap("Graphics/Battlebacks/", @bg_name + "_message")
@message_bar_sprite.y = Settings::SCREEN_HEIGHT - @message_bar_sprite.height
end
# TODO: def refresh_box_display which checks @settings for whether boxes
# should be drawn around sprites.
# TODO: Create shadow sprites?
def ensure_battler_sprites
if !@side_size0 || @side_size0 != side_size(0)
@battler_sprites.each_with_index { |s, i| s.dispose if i.even? && s && !s.disposed? }
@side_size0 = side_size(0)
@side_size0.times do |i|
next if position_empty?(i * 2)
@battler_sprites[i * 2] = Sprite.new(self.viewport)
end
end
if !@side_size1 || @side_size1 != side_size(1)
@battler_sprites.each_with_index { |s, i| s.dispose if i.odd? && s && !s.disposed? }
@side_size1 = side_size(1)
@side_size1.times do |i|
next if position_empty?((i * 2) + 1)
@battler_sprites[(i * 2) + 1] = Sprite.new(self.viewport)
end
end
end
def refresh_battler_graphics
if !@user_sprite_name || !@user_sprite_name || @user_sprite_name != @settings[:user_sprite_name]
@user_sprite_name = @settings[:user_sprite_name]
@user_bitmap_front&.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)
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&.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)
end
end
def refresh_battler_positions
user_idx = user_index
@user_coords = recalculate_battler_position(
user_idx, side_size(user_idx), @user_sprite_name,
(user_idx.even?) ? @user_bitmap_back : @user_bitmap_front
)
target_indices.each do |target_idx|
@target_coords[target_idx] = recalculate_battler_position(
target_idx, side_size(target_idx), @target_sprite_name,
(target_idx.even?) ? @target_bitmap_back : @target_bitmap_front
)
end
end
def recalculate_battler_position(index, size, sprite_name, btmp)
spr = Sprite.new(self.viewport)
spr.x, spr.y = Battle::Scene.pbBattlerPosition(index, size)
data = GameData::Species.get_species_form(sprite_name, 0) # Form 0
data.apply_metrics_to_sprite(spr, index) if data
return [spr.x, spr.y - (btmp.height / 2)]
end
def create_particle_sprite(index, target_idx = -1)
if target_idx >= 0
if @particle_sprites[index].is_a?(Array)
return if @particle_sprites[index][target_idx] && !@particle_sprites[index][target_idx].disposed?
else
@particle_sprites[index].dispose if @particle_sprites[index] && !@particle_sprites[index].disposed?
@particle_sprites[index] = []
end
@particle_sprites[index][target_idx] = Sprite.new(self.viewport)
else
if @particle_sprites[index].is_a?(Array)
@particle_sprites[index].each { |s| s.dispose if s && !s.disposed? }
@particle_sprites[index] = nil
else
return if @particle_sprites[index] && !@particle_sprites[index].disposed?
end
@particle_sprites[index] = Sprite.new(self.viewport)
end
end
def refresh_sprite(index, target_idx = -1)
particle = @anim[:particles][index]
return if !particle
# Get sprite
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}).",
particle[:name], user_index) if !spr
when "Target"
spr = @battler_sprites[target_idx]
raise _INTL("Sprite for particle {1} not found somehow (battler index {2}).",
particle[:name], target_idx) if !spr
else
create_particle_sprite(index, target_idx)
if target_idx >= 0
spr = @particle_sprites[index][target_idx]
else
spr = @particle_sprites[index]
end
end
# Calculate all values of particle at the current keyframe
values = AnimationEditor::ParticleDataHelper.get_all_keyframe_particle_values(particle, @keyframe)
values.each_pair do |property, val|
values[property] = val[0]
end
# Set visibility
spr.visible = values[:visible]
return if !spr.visible
# 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(target_idx)
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
# 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"
if target_idx < 0
raise _INTL("Particle {1} was given a graphic of \"TARGET\" but its focus doesn't include a target.",
particle[:name])
end
spr.bitmap = (target_idx.even?) ? @target_bitmap_back : @target_bitmap_front
when "TARGET_OPP"
if target_idx < 0
raise _INTL("Particle {1} was given a graphic of \"TARGET_OPP\" but its focus doesn't include a target.",
particle[:name])
end
spr.bitmap = (target_idx.even?) ? @target_bitmap_front : @target_bitmap_back
when "TARGET_FRONT"
if target_idx < 0
raise _INTL("Particle {1} was given a graphic of \"TARGET_FRONT\" but its focus doesn't include a target.",
particle[:name])
end
spr.bitmap = @target_bitmap_front
when "TARGET_BACK"
if target_idx < 0
raise _INTL("Particle {1} was given a graphic of \"TARGET_BACK\" but its focus doesn't include a target.",
particle[:name])
end
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])
# TODO: Set the oy to spr.bitmap.height if particle[:graphic] has
# something special in it (don't know what yet).
if 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
end
# 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]
spr.z = user_pos + ((values[:z].to_f / distance) * (target_pos - user_pos)).to_i
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
# Set various other properties
spr.zoom_x = values[:zoom_x] / 100.0
spr.zoom_y = values[:zoom_y] / 100.0
spr.angle = values[:angle]
spr.mirror = values[:flip]
spr.blend_type = values[:blending]
# Set color and tone
spr.color.set(values[:color_red], values[:color_green], values[:color_blue], values[:color_alpha])
spr.tone.set(values[:tone_red], values[:tone_green], values[:tone_blue], values[:tone_gray])
end
def refresh_particle(index)
target_indices.each { |target_idx| refresh_sprite(index, target_idx) }
end
def refresh
refresh_bg_graphics
ensure_battler_sprites
refresh_battler_graphics
refresh_battler_positions
@battler_sprites.each { |s| s.visible = false if s && !s.disposed? }
@particle_sprites.each do |s|
if s.is_a?(Array)
s.each { |s2| s2.visible = false if s2 && !s2.disposed? }
else
s.visible = false if s && !s.disposed?
end
end
@anim[:particles].each_with_index do |particle, i|
if GameData::Animation::FOCUS_TYPES_WITH_TARGET.include?(particle[:focus])
refresh_particle(i)
else
refresh_sprite(i) if particle[:name] != "SE"
end
end
end
#-----------------------------------------------------------------------------
# TODO: def update_input. Includes def pbSpriteHitTest equivalent.
def update
# TODO: Update input (mouse clicks, dragging particles).
# TODO: Keyboard shortcuts?
end
end

View File

@@ -19,10 +19,16 @@ class AnimationEditor::ParticleList < UIControls::BaseControl
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
:target => Color.new(248, 96, 96), # Red
:user_and_target => Color.new(248, 248, 96), # Yellow
:screen => Color.new(128, 160, 248) # Blue
:foreground => Color.new(128, 160, 248), # Blue
:midground => Color.new(128, 160, 248), # Blue
:background => Color.new(128, 160, 248), # Blue
:user => Color.new(96, 248, 96), # Green
:target => Color.new(248, 96, 96), # Red
:user_and_target => Color.new(248, 248, 96), # Yellow
:user_side_foreground => Color.new(128, 248, 248), # Cyan
:user_side_background => Color.new(128, 248, 248), # Cyan
:target_side_foreground => Color.new(128, 248, 248), # Cyan
:target_side_background => Color.new(128, 248, 248) # Cyan
}
SE_CONTROL_BG = Color.gray