diff --git a/Data/Scripts/801_UI controls/002_ControlsContainer.rb b/Data/Scripts/801_UI controls/002_ControlsContainer.rb index 3a0e23b6b..237922040 100644 --- a/Data/Scripts/801_UI controls/002_ControlsContainer.rb +++ b/Data/Scripts/801_UI controls/002_ControlsContainer.rb @@ -17,6 +17,7 @@ class UIControls::ControlsContainer attr_reader :controls attr_reader :values attr_reader :visible + attr_reader :viewport LINE_SPACING = 28 OFFSET_FROM_LABEL_X = 90 @@ -30,8 +31,8 @@ class UIControls::ControlsContainer @width = width @height = height @controls = [] - @control_rects = [] @row_count = 0 + @pixel_offset = 0 @captured = nil @visible = true end @@ -72,10 +73,15 @@ class UIControls::ControlsContainer #----------------------------------------------------------------------------- def add_label(id, label, has_label = false) - id = (id.to_s + "_label").to_sym + id = (id.to_s + "_label").to_sym if !has_label add_control(id, UIControls::Label.new(*control_size(has_label), @viewport, label), has_label) end + def add_labelled_label(id, label, text) + add_label(id, label) + add_label(id, text, true) + end + def add_header_label(id, label) ctrl = UIControls::Label.new(*control_size, @viewport, label) ctrl.header = true @@ -127,6 +133,18 @@ class UIControls::ControlsContainer add_button(id, button_text, true) end + def add_list(id, rows, options, has_label = false) + size = control_size(has_label) + size[0] -= 8 + size[1] = rows * UIControls::List::ROW_HEIGHT + add_control(id, UIControls::List.new(*size, @viewport, options), has_label, rows) + end + + def add_labelled_list(id, label, rows, options) + add_label(id, label) + add_list(id, rows, options, true) + end + def add_dropdown_list(id, options, value, has_label = false) add_control(id, UIControls::DropdownList.new(*control_size(has_label), @viewport, options, value), has_label) end @@ -176,8 +194,6 @@ class UIControls::ControlsContainer #----------------------------------------------------------------------------- - private - def control_size(has_label = false) if has_label return @width - OFFSET_FROM_LABEL_X, LINE_SPACING - OFFSET_FROM_LABEL_Y @@ -185,16 +201,23 @@ class UIControls::ControlsContainer return @width, LINE_SPACING end - def add_control(id, control, add_offset = false) - i = @controls.length - control_y = (add_offset ? @row_count - 1 : @row_count) * LINE_SPACING - # TODO: I don't think I need @control_rects. - @control_rects[i] = Rect.new(0, control_y, control.width, control.height) - control.x = @control_rects[i].x + (add_offset ? OFFSET_FROM_LABEL_X : 0) - control.y = @control_rects[i].y + (add_offset ? OFFSET_FROM_LABEL_Y : 0) + def add_control_at(id, control, x, y) + control.x = x + control.y = y control.set_interactive_rects - @controls[i] = [id, control] - @row_count += 1 if !add_offset + @controls.push([id, control]) repaint end + + def add_control(id, control, add_offset = false, rows = 1) + i = @controls.length + row_x = 0 + row_y = (add_offset ? @row_count - 1 : @row_count) * LINE_SPACING + ctrl_x = row_x + (add_offset ? OFFSET_FROM_LABEL_X : 0) + ctrl_x += 4 if control.is_a?(UIControls::List) + ctrl_y = row_y + (add_offset ? OFFSET_FROM_LABEL_Y : 0) + @pixel_offset + add_control_at(id, control, ctrl_x, ctrl_y) + @row_count += rows if !add_offset + @pixel_offset -= (LINE_SPACING - UIControls::List::ROW_HEIGHT) * (rows - 1) if control.is_a?(UIControls::List) + end end diff --git a/Data/Scripts/801_UI controls/Control elements/005_NumberSlider.rb b/Data/Scripts/801_UI controls/Control elements/005_NumberSlider.rb index 69899d87f..edaf9c524 100644 --- a/Data/Scripts/801_UI controls/Control elements/005_NumberSlider.rb +++ b/Data/Scripts/801_UI controls/Control elements/005_NumberSlider.rb @@ -81,7 +81,7 @@ class UIControls::NumberSlider < UIControls::BaseControl self.bitmap.fill_rect(SLIDER_X - 1 + (i * SLIDER_LENGTH / 4), (self.height / 2) - 2, 2, 4, self.bitmap.font.color) end # Draw slider knob - fraction = (self.value - self.min_value) / self.max_value.to_f + fraction = (self.value - self.min_value) / (self.max_value.to_f - self.min_value) knob_x = (SLIDER_LENGTH * fraction).to_i self.bitmap.fill_rect(SLIDER_X + knob_x - 4, (self.height / 2) - 6, 8, 12, SLIDER_KNOB_COLOR) # Draw plus button diff --git a/Data/Scripts/801_UI controls/Control elements/008_List.rb b/Data/Scripts/801_UI controls/Control elements/008_List.rb index beca0fa3d..f6ffd521c 100644 --- a/Data/Scripts/801_UI controls/Control elements/008_List.rb +++ b/Data/Scripts/801_UI controls/Control elements/008_List.rb @@ -48,11 +48,16 @@ class UIControls::List < UIControls::BaseControl @scrollbar.z = new_val + 1 end + def visible=(new_val) + super + @scrollbar.visible = new_val + end + # Each value in @values is an array: [id, text]. def values=(new_vals) @values = new_vals set_interactive_rects - @scrollbar.range = @values.length * ROW_HEIGHT + @scrollbar.range = [@values.length, 1].max * ROW_HEIGHT if @scrollbar.visible self.top_row = (@scrollbar.position.to_f / ROW_HEIGHT).round else @@ -143,10 +148,11 @@ class UIControls::List < UIControls::BaseControl SELECTED_ROW_COLOR ) end + txt = (val.is_a?(Array)) ? val[1] : val draw_text(self.bitmap, @interactions[i].x + TEXT_PADDING_X, @interactions[i].y + TEXT_OFFSET_Y - (@top_row * ROW_HEIGHT), - val[1]) + txt) end end diff --git a/Data/Scripts/902_Anim GameData/001_Animation.rb b/Data/Scripts/902_Anim GameData/001_Animation.rb index 51c7ee917..8c8c49700 100644 --- a/Data/Scripts/902_Anim GameData/001_Animation.rb +++ b/Data/Scripts/902_Anim GameData/001_Animation.rb @@ -98,6 +98,7 @@ module GameData # TODO: Add "SetColor"/"SetTone" as shorthand for the above? They'd be # converted in the Compiler. # TODO: Bitmap masking. + # TODO: Hue? I don't think so; color/tone do the same job. # These properties are specifically for the "SE" particle. "Play" => [:se, "^usUU"], # Filename, volume, pitch @@ -249,6 +250,14 @@ module GameData elsif ret ret = SUB_SCHEMA[key][2].key(ret) end + when "graphic" + # The User and Target particles have hardcoded graphics, so they don't + # need writing to PBS + ret = nil if ["User", "Target"].include?(@particles[index][:name]) + when "Play" + # TODO: Turn volume/pitch of 100 into nil. + when "PlayUserCry", "PlayTargetCry" + # TODO: Turn volume/pitch of 100 into nil. when "AllCommands" # Get translations of all properties to their names as seen in PBS # animation files diff --git a/Data/Scripts/903_Anim Compiler/001_Anim compiler.rb b/Data/Scripts/903_Anim Compiler/001_Anim compiler.rb index 7f3e8ea8e..a80afbc7c 100644 --- a/Data/Scripts/903_Anim Compiler/001_Anim compiler.rb +++ b/Data/Scripts/903_Anim Compiler/001_Anim compiler.rb @@ -138,13 +138,21 @@ module Compiler else particle[:focus] = :screen end end + # Ensure user/target particles have a default graphic if not given + if !particle[:graphic] && particle[:name] != "SE" + case particle[:name] + when "User" then particle[:graphic] = "USER" + when "Target" then particle[:graphic] = "TARGET" + end + end # Ensure that particles don't have a focus involving a target if the # animation itself doesn't involve a target if hash[:no_target] && [:target, :user_and_target].include?(particle[:focus]) raise _INTL("Particle \"{1}\" can't have a \"Focus\" that involves a target if property \"NoTarget\" is set to true.", particle[:name]) + "\n" + FileLineData.linereport end - + # TODO: For SE particle, ensure that it doesn't play two instances of the + # same file in the same frame. # Convert all "SetXYZ" particle commands to "MoveXYZ" by giving them a # duration of 0 (even ones that can't have a "MoveXYZ" command) GameData::Animation::PARTICLE_KEYFRAME_DEFAULT_VALUES.keys.each do |prop| diff --git a/Data/Scripts/904_Anim Editor/001_AnimationEditor.rb b/Data/Scripts/904_Anim Editor/001_AnimationEditor.rb index cce53ec3d..9b15e9130 100644 --- a/Data/Scripts/904_Anim Editor/001_AnimationEditor.rb +++ b/Data/Scripts/904_Anim Editor/001_AnimationEditor.rb @@ -47,14 +47,6 @@ class AnimationEditor PARTICLE_LIST_WIDTH = WINDOW_WIDTH - (BORDER_THICKNESS * 2) PARTICLE_LIST_HEIGHT = WINDOW_HEIGHT - PARTICLE_LIST_Y - BORDER_THICKNESS - MESSAGE_BOX_WIDTH = WINDOW_WIDTH * 3 / 4 - MESSAGE_BOX_HEIGHT = 160 - MESSAGE_BOX_X = (WINDOW_WIDTH - MESSAGE_BOX_WIDTH) / 2 - MESSAGE_BOX_Y = (WINDOW_HEIGHT - MESSAGE_BOX_HEIGHT) / 2 - MESSAGE_BOX_BUTTON_WIDTH = 150 - MESSAGE_BOX_BUTTON_HEIGHT = 32 - MESSAGE_BOX_SPACING = 16 - def initialize(anim_id, anim) @anim_id = anim_id @anim = anim @@ -65,9 +57,17 @@ class AnimationEditor @viewport.z = 99999 @canvas_viewport = Viewport.new(CANVAS_X, CANVAS_Y, CANVAS_WIDTH, CANVAS_HEIGHT) @canvas_viewport.z = @viewport.z + @pop_up_viewport = Viewport.new(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT) + @pop_up_viewport.z = @viewport.z + 50 # Background sprite @screen_bitmap = BitmapSprite.new(WINDOW_WIDTH, WINDOW_HEIGHT, @viewport) - @screen_bitmap.z = -1 + @screen_bitmap.z = -100 + @se_list_box_bitmap = BitmapSprite.new(WINDOW_WIDTH, WINDOW_HEIGHT, @viewport) + @se_list_box_bitmap.z = -90 + @se_list_box_bitmap.visible = false + @pop_up_bg_bitmap = BitmapSprite.new(WINDOW_WIDTH, WINDOW_HEIGHT, @pop_up_viewport) + @pop_up_bg_bitmap.z = -100 + @pop_up_bg_bitmap.visible = false draw_editor_background @components = {} # Menu bar @@ -105,6 +105,8 @@ class AnimationEditor def dispose @screen_bitmap.dispose + @se_list_box_bitmap.dispose + @pop_up_bg_bitmap.dispose @components.each_value { |c| c.dispose } @components.clear @viewport.dispose @@ -191,11 +193,21 @@ class AnimationEditor def set_se_pane_contents se_pane = @components[:se_pane] se_pane.add_header_label(:header, _INTL("Edit sound effects at keyframe")) - # TODO: A list containing all SE files that play this keyframe. Lists SE, - # user cry and target cry. - se_pane.add_button(:add, _INTL("Add")) - se_pane.add_button(:edit, _INTL("Edit")) - se_pane.add_button(:delete, _INTL("Delete")) + size = se_pane.control_size + size[0] -= 10 + size[1] = UIControls::List::ROW_HEIGHT * 5 # 5 rows + list = UIControls::List.new(*size, se_pane.viewport, []) + se_pane.add_control_at(:list, list, 5, 30) + button_height = UIControls::ControlsContainer::LINE_SPACING + add = UIControls::Button.new(101, button_height, se_pane.viewport, _INTL("Add")) + add.set_fixed_size + se_pane.add_control_at(:add, add, 1, 154) + edit = UIControls::Button.new(100, button_height, se_pane.viewport, _INTL("Edit")) + edit.set_fixed_size + se_pane.add_control_at(:edit, edit, 102, 154) + delete = UIControls::Button.new(101, button_height, se_pane.viewport, _INTL("Delete")) + delete.set_fixed_size + se_pane.add_control_at(:delete, delete, 202, 154) end def set_particle_pane_contents @@ -206,7 +218,8 @@ class AnimationEditor particle_pane.add_labelled_text_box(:name, _INTL("Name"), _INTL("Untitled")) # TODO: Graphic should show the graphic's name alongside a "Change" button. # New kind of control that is a label plus a button? - particle_pane.add_labelled_button(:graphic, _INTL("Graphic"), _INTL("Change")) + particle_pane.add_labelled_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"), @@ -244,74 +257,6 @@ class AnimationEditor #----------------------------------------------------------------------------- - def message(text, *options) - msg_viewport = Viewport.new(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT) - msg_viewport.z = @viewport.z + 50 - msg_bitmap = BitmapSprite.new(WINDOW_WIDTH, WINDOW_HEIGHT, msg_viewport) - msg_bitmap.bitmap.font.color = Color.black - msg_bitmap.bitmap.font.size = 18 - # Draw gray background - msg_bitmap.bitmap.fill_rect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, Color.new(0, 0, 0, 128)) - # Draw message box border - BORDER_THICKNESS.times do |i| - col = (i.even?) ? Color.white : Color.black - msg_bitmap.bitmap.outline_rect(MESSAGE_BOX_X - i - 1, MESSAGE_BOX_Y - i - 1, - MESSAGE_BOX_WIDTH + (i * 2) + 2, MESSAGE_BOX_HEIGHT + (i * 2) + 2, col) - end - # Fill message box with white - msg_bitmap.bitmap.fill_rect(MESSAGE_BOX_X, MESSAGE_BOX_Y, MESSAGE_BOX_WIDTH, MESSAGE_BOX_HEIGHT, Color.white) - # Draw text - text_size = msg_bitmap.bitmap.text_size(text) - msg_bitmap.bitmap.draw_text(MESSAGE_BOX_X, (WINDOW_HEIGHT / 2) - MESSAGE_BOX_BUTTON_HEIGHT, - MESSAGE_BOX_WIDTH, text_size.height, text, 1) - # Create buttons - buttons = [] - options.each_with_index do |option, i| - btn = UIControls::Button.new(MESSAGE_BOX_BUTTON_WIDTH, MESSAGE_BOX_BUTTON_HEIGHT, msg_viewport, option[1]) - btn.x = (WINDOW_WIDTH - (options.length * MESSAGE_BOX_BUTTON_WIDTH)) / 2 + (i * MESSAGE_BOX_BUTTON_WIDTH) - btn.y = MESSAGE_BOX_Y + MESSAGE_BOX_HEIGHT - MESSAGE_BOX_BUTTON_HEIGHT - MESSAGE_BOX_SPACING - btn.set_fixed_size - btn.set_interactive_rects - buttons.push([option[0], btn]) - end - # Interaction loop - ret = nil - captured = nil - loop do - Graphics.update - Input.update - if captured - captured.update - captured = nil if !captured.busy? - else - buttons.each do |btn| - btn[1].update - captured = btn[1] if btn[1].busy? - end - end - buttons.each do |btn| - next if !btn[1].changed? - ret = btn[0] - break - end - ret = :cancel if Input.trigger?(Input::BACK) - break if ret - buttons.each { |btn| btn[1].repaint } - end - # Dispose and return - buttons.each { |btn| btn[1].dispose } - buttons.clear - msg_bitmap.dispose - msg_viewport.dispose - return ret - end - - def confirm_message(text) - return message(text, [:yes, _INTL("Yes")], [:no, _INTL("No")]) == :yes - end - - #----------------------------------------------------------------------------- - def save GameData::Animation.register(@anim, @anim_id) Compiler.write_battle_animation_file(@anim[:pbs_path]) @@ -341,6 +286,11 @@ class AnimationEditor draw_big_outline.call(@screen_bitmap.bitmap, PLAY_CONTROLS_X, PLAY_CONTROLS_Y, PLAY_CONTROLS_WIDTH, PLAY_CONTROLS_HEIGHT) draw_big_outline.call(@screen_bitmap.bitmap, SIDE_PANE_X, SIDE_PANE_Y, SIDE_PANE_WIDTH, SIDE_PANE_HEIGHT) draw_big_outline.call(@screen_bitmap.bitmap, PARTICLE_LIST_X, PARTICLE_LIST_Y, PARTICLE_LIST_WIDTH, PARTICLE_LIST_HEIGHT) + # Draw box around SE list box in side pane + @se_list_box_bitmap.bitmap.outline_rect(SIDE_PANE_X + 3, SIDE_PANE_Y + 24 + 4, + SIDE_PANE_WIDTH - 6, (5 * UIControls::List::ROW_HEIGHT) + 4, Color.black) + # Make the pop-up background semi-transparent + @pop_up_bg_bitmap.bitmap.fill_rect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, Color.new(0, 0, 0, 128)) end def refresh_component_visibility(component_sym) @@ -355,6 +305,7 @@ class AnimationEditor component.visible = (keyframe >= 0 && particle_index >= 0 && @anim[:particles][particle_index] && @anim[:particles][particle_index][:name] == "SE") + @se_list_box_bitmap.visible = component.visible when :particle_pane component.visible = (keyframe < 0 && particle_index >= 0) when :keyframe_pane @@ -376,13 +327,44 @@ class AnimationEditor # which should be indicated somehow in ctrl[1]. end when :se_pane - # TODO: Set list of SEs, activate/deactivate buttons accordingly. + # TODO: Activate/deactivate Edit/Delete buttons accordingly. + se_particle = @anim[:particles].select { |p| p[:name] == "SE" }[0] + kyfrm = keyframe + # Populate list of files + list = [] + se_particle.each_pair do |property, values| + next if !values.is_a?(Array) + values.each do |val| + next if val[0] != kyfrm + text = AnimationEditor::ParticleDataHelper.get_se_display_text(property, val) + case property + when :user_cry then list.push(["USER", text]) + when :target_cry then list.push(["TARGET", text]) + when :se then list.push([val[2], text]) + end + end + end + list.sort! { |a, b| a[1].downcase <=> b[1].downcase } + component.get_control(:list).values = list when :particle_pane new_vals = AnimationEditor::ParticleDataHelper.get_all_particle_values(@anim[:particles][particle_index]) component.controls.each do |ctrl| next if !new_vals.include?(ctrl[0]) ctrl[1].value = new_vals[ctrl[0]] if ctrl[1].respond_to?("value=") end + graphic_name = @anim[:particles][particle_index][:graphic] + graphic_override_names = { + "USER" => _INTL("[[User's sprite]]"), + "USER_OPP" => _INTL("[[User's other side sprite]]"), + "USER_FRONT" => _INTL("[[User's front sprite]]"), + "USER_BACK" => _INTL("[[User's back sprite]]"), + "TARGET" => _INTL("[[Target's sprite]]"), + "TARGET_OPP" => _INTL("[[Target's other side sprite]]"), + "TARGET_FRONT" => _INTL("[[Target's front sprite]]"), + "TARGET_BACK" => _INTL("[[Target's back sprite]]"), + } + graphic_name = graphic_override_names[graphic_name] if graphic_override_names[graphic_name] + component.get_control(:graphic_name).label = graphic_name # TODO: Disable the name, graphic and focus controls for "User"/"Target". end end @@ -434,18 +416,54 @@ class AnimationEditor refresh_component(:commands_pane) end when :se_pane - # TODO: Enable the "Edit" and "Delete" controls only if an SE is selected. case property when :add # Button + new_file, new_volume, new_pitch = choose_audio_file("", 100, 100) + if new_file != "" + particle = @anim[:particles][particle_index] + AnimationEditor::ParticleDataHelper.add_se_command(particle, keyframe, new_file, new_volume, new_pitch) + @components[:particle_list].change_particle_commands(particle_index) + @components[:play_controls].duration = @components[:particle_list].duration + refresh_component(:se_pane) + end when :edit # Button + particle = @anim[:particles][particle_index] + list = @components[:se_pane].get_control(:list) + old_file = list.value + old_volume, old_pitch = AnimationEditor::ParticleDataHelper.get_se_values_from_filename_and_frame(particle, keyframe, old_file) + if old_file + new_file, new_volume, new_pitch = choose_audio_file(old_file, old_volume, old_pitch) + if new_file != old_file || new_volume != old_volume || new_pitch != old_pitch + AnimationEditor::ParticleDataHelper.delete_se_command(particle, keyframe, old_file) + AnimationEditor::ParticleDataHelper.add_se_command(particle, keyframe, new_file, new_volume, new_pitch) + @components[:particle_list].change_particle_commands(particle_index) + @components[:play_controls].duration = @components[:particle_list].duration + refresh_component(:se_pane) + end + end when :delete # Button + particle = @anim[:particles][particle_index] + list = @components[:se_pane].get_control(:list) + old_file = list.value + if old_file + AnimationEditor::ParticleDataHelper.delete_se_command(particle, keyframe, old_file) + @components[:particle_list].change_particle_commands(particle_index) + @components[:play_controls].duration = @components[:particle_list].duration + refresh_component(:se_pane) + end else # particle = @anim[:particles][particle_index] end when :particle_pane case property when :graphic # Button - # TODO: Open the graphic chooser pop-up window. + p_index = particle_index + new_file = choose_graphic_file(@anim[:particles][p_index][:graphic]) + if @anim[:particles][p_index][:graphic] != new_file + @anim[:particles][p_index][:graphic] = new_file + refresh_component(:particle_pane) + # TODO: refresh_component(:canvas) + end else particle = @anim[:particles][particle_index] new_cmds = AnimationEditor::ParticleDataHelper.set_property(particle, property, value) @@ -503,17 +521,15 @@ class AnimationEditor Graphics.update Input.update update - if !inputting_text && @captured.nil? - if @quit || Input.trigger?(Input::BACK) - case message(_INTL("Do you want to save changes to the animation?"), - [:yes, _INTL("Yes")], [:no, _INTL("No")], [:cancel, _INTL("Cancel")]) - when :yes - save - when :cancel - @quit = false - end - break if @quit + if !inputting_text && @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 + save + when :cancel + @quit = false end + break if @quit end end dispose diff --git a/Data/Scripts/904_Anim Editor/002_AnimationEditor_popups.rb b/Data/Scripts/904_Anim Editor/002_AnimationEditor_popups.rb new file mode 100644 index 000000000..b26795569 --- /dev/null +++ b/Data/Scripts/904_Anim Editor/002_AnimationEditor_popups.rb @@ -0,0 +1,347 @@ +#=============================================================================== +# +#=============================================================================== +class AnimationEditor + MESSAGE_BOX_WIDTH = WINDOW_WIDTH * 3 / 4 + MESSAGE_BOX_HEIGHT = 160 + MESSAGE_BOX_BUTTON_WIDTH = 150 + MESSAGE_BOX_BUTTON_HEIGHT = 32 + MESSAGE_BOX_SPACING = 16 + + GRAPHIC_CHOOSER_BUTTON_WIDTH = 150 + GRAPHIC_CHOOSER_BUTTON_HEIGHT = MESSAGE_BOX_BUTTON_HEIGHT + GRAPHIC_CHOOSER_FILE_LIST_WIDTH = GRAPHIC_CHOOSER_BUTTON_WIDTH * 2 + GRAPHIC_CHOOSER_FILE_LIST_HEIGHT = 15 * UIControls::List::ROW_HEIGHT + GRAPHIC_CHOOSER_PREVIEW_SIZE = 320 + GRAPHIC_CHOOSER_WINDOW_WIDTH = GRAPHIC_CHOOSER_FILE_LIST_WIDTH + GRAPHIC_CHOOSER_PREVIEW_SIZE + (MESSAGE_BOX_SPACING * 2) + 8 + GRAPHIC_CHOOSER_WINDOW_HEIGHT = GRAPHIC_CHOOSER_FILE_LIST_HEIGHT + GRAPHIC_CHOOSER_BUTTON_HEIGHT + 24 + (MESSAGE_BOX_SPACING * 2) + 2 + + def create_pop_up_window(width, height) + ret = BitmapSprite.new(width, height, @pop_up_viewport) + ret.x = (WINDOW_WIDTH - width) / 2 + ret.y = (WINDOW_HEIGHT - height) / 2 + ret.z = -1 + ret.bitmap.font.color = Color.black + ret.bitmap.font.size = 18 + # Draw message box border + BORDER_THICKNESS.times do |i| + col = (i.even?) ? Color.black : Color.white + ret.bitmap.outline_rect(i, i, ret.width - (i * 2), ret.height - (i * 2), col) + end + # Fill message box with white + ret.bitmap.fill_rect(BORDER_THICKNESS, BORDER_THICKNESS, + ret.width - (BORDER_THICKNESS * 2), + ret.height - (BORDER_THICKNESS * 2), + Color.white) + return ret + end + + #----------------------------------------------------------------------------- + + def message(text, *options) + @pop_up_bg_bitmap.visible = true + msg_bitmap = create_pop_up_window(MESSAGE_BOX_WIDTH, MESSAGE_BOX_HEIGHT) + # Draw text + text_size = msg_bitmap.bitmap.text_size(text) + msg_bitmap.bitmap.draw_text(0, (msg_bitmap.height / 2) - MESSAGE_BOX_BUTTON_HEIGHT, + msg_bitmap.width, text_size.height, text, 1) + # Create buttons + buttons = [] + options.each_with_index do |option, i| + btn = UIControls::Button.new(MESSAGE_BOX_BUTTON_WIDTH, MESSAGE_BOX_BUTTON_HEIGHT, @pop_up_viewport, option[1]) + btn.x = msg_bitmap.x + (msg_bitmap.width - (MESSAGE_BOX_BUTTON_WIDTH * options.length)) / 2 + btn.x += MESSAGE_BOX_BUTTON_WIDTH * i + btn.y = msg_bitmap.y + msg_bitmap.height - MESSAGE_BOX_BUTTON_HEIGHT - MESSAGE_BOX_SPACING + btn.set_fixed_size + btn.set_interactive_rects + buttons.push([option[0], btn]) + end + # Interaction loop + ret = nil + captured = nil + loop do + Graphics.update + Input.update + if captured + captured.update + captured = nil if !captured.busy? + else + buttons.each do |btn| + btn[1].update + captured = btn[1] if btn[1].busy? + end + end + buttons.each do |btn| + next if !btn[1].changed? + ret = btn[0] + break + end + ret = :cancel if Input.trigger?(Input::BACK) + break if ret + buttons.each { |btn| btn[1].repaint } + end + # Dispose and return + buttons.each { |btn| btn[1].dispose } + buttons.clear + msg_bitmap.dispose + @pop_up_bg_bitmap.visible = false + return ret + end + + def confirm_message(text) + return message(text, [:yes, _INTL("Yes")], [:no, _INTL("No")]) == :yes + end + + #----------------------------------------------------------------------------- + + def choose_graphic_file(selected) + selected ||= "" + sprite_folder = "Graphics/Battle animations/" + # Get a list of files + files = [] + Dir.chdir(sprite_folder) do + Dir.glob("*.png") { |f| files.push([File.basename(f, ".*"), f]) } + Dir.glob("*.jpg") { |f| files.push([File.basename(f, ".*"), f]) } + Dir.glob("*.jpeg") { |f| files.push([File.basename(f, ".*"), f]) } + end + files.delete_if { |f| ["USER", "USER_OPP", "USER_FRONT", "USER_BACK", + "TARGET", "TARGET_OPP", "TARGET_FRONT", + "TARGET_BACK"].include?(f[0].upcase) } + files.sort! { |a, b| a[0].downcase <=> b[0].downcase } + files.prepend(["USER", _INTL("[[User's sprite]]")], + ["USER_OPP", _INTL("[[User's other side sprite]]")], + ["USER_FRONT", _INTL("[[User's front sprite]]")], + ["USER_BACK", _INTL("[[User's back sprite]]")], + ["TARGET", _INTL("[[Target's sprite]]")], + ["TARGET_OPP", _INTL("[[Target's other side sprite]]")], + ["TARGET_FRONT", _INTL("[[Target's front sprite]]")], + ["TARGET_BACK", _INTL("[[Target's back sprite]]")]) + idx = 0 + files.each_with_index do |f, i| + next if f[0] != selected + idx = i + break + end + # Show pop-up window + @pop_up_bg_bitmap.visible = true + bg_bitmap = create_pop_up_window(GRAPHIC_CHOOSER_WINDOW_WIDTH, GRAPHIC_CHOOSER_WINDOW_HEIGHT) + text = _INTL("Choose a file...") + text_size = bg_bitmap.bitmap.text_size(text) + bg_bitmap.bitmap.draw_text(MESSAGE_BOX_SPACING, 11, bg_bitmap.width, text_size.height, text, 0) + # Create list of files + list = UIControls::List.new(GRAPHIC_CHOOSER_FILE_LIST_WIDTH, GRAPHIC_CHOOSER_FILE_LIST_HEIGHT, @pop_up_viewport, files) + list.x = bg_bitmap.x + MESSAGE_BOX_SPACING + list.y = bg_bitmap.y + MESSAGE_BOX_SPACING + 24 + list.selected = idx + list.set_interactive_rects + list.repaint + bg_bitmap.bitmap.outline_rect(MESSAGE_BOX_SPACING - 2, MESSAGE_BOX_SPACING + 24 - 2, + GRAPHIC_CHOOSER_FILE_LIST_WIDTH + 4, GRAPHIC_CHOOSER_FILE_LIST_HEIGHT + 4, Color.black) + # Create buttons + buttons = [] + [[:ok, _INTL("OK")], [:cancel, _INTL("Cancel")]].each_with_index do |option, i| + btn = UIControls::Button.new(GRAPHIC_CHOOSER_BUTTON_WIDTH, MESSAGE_BOX_BUTTON_HEIGHT, @pop_up_viewport, option[1]) + btn.x = list.x + (GRAPHIC_CHOOSER_BUTTON_WIDTH * i) + btn.y = list.y + list.height + 2 + btn.set_fixed_size + btn.set_interactive_rects + buttons.push([option[0], btn]) + end + # Create sprite preview + bg_bitmap.bitmap.outline_rect(MESSAGE_BOX_SPACING + list.width + 6, MESSAGE_BOX_SPACING + 24 - 2, + GRAPHIC_CHOOSER_PREVIEW_SIZE + 4, GRAPHIC_CHOOSER_PREVIEW_SIZE + 4, + Color.black) + preview_sprite = Sprite.new(@pop_up_viewport) + preview_sprite.x = list.x + list.width + 8 + (GRAPHIC_CHOOSER_PREVIEW_SIZE / 2) + preview_sprite.y = list.y + (GRAPHIC_CHOOSER_PREVIEW_SIZE / 2) + preview_bitmap = nil + set_preview_graphic = lambda do |sprite, filename| + preview_bitmap&.dispose + # TODO: When the canvas works, use the proper user's/target's sprite here. + case filename + when "USER", "USER_BACK", "TARGET_BACK", "TARGET_OPP" + preview_bitmap = AnimatedBitmap.new("Graphics/Pokemon/Back/" + "000") + when "TARGET", "TARGET_FRONT", "USER_FRONT", "USER_OPP" + preview_bitmap = AnimatedBitmap.new("Graphics/Pokemon/Front/" + "000") + else + preview_bitmap = AnimatedBitmap.new(sprite_folder + filename) + end + bg_bitmap.bitmap.fill_rect(MESSAGE_BOX_SPACING + list.width + 8, MESSAGE_BOX_SPACING + 24, + GRAPHIC_CHOOSER_PREVIEW_SIZE, GRAPHIC_CHOOSER_PREVIEW_SIZE, + Color.white) + next if !preview_bitmap + sprite.bitmap = preview_bitmap.bitmap + zoom = [[GRAPHIC_CHOOSER_PREVIEW_SIZE.to_f / preview_bitmap.width, + GRAPHIC_CHOOSER_PREVIEW_SIZE.to_f / preview_bitmap.height].min, 1.0].min + sprite.zoom_x = sprite.zoom_y = zoom + sprite.ox = sprite.width / 2 + sprite.oy = sprite.height / 2 + bg_bitmap.bitmap.fill_rect(MESSAGE_BOX_SPACING + list.width + 8 + (GRAPHIC_CHOOSER_PREVIEW_SIZE / 2) - (sprite.width * sprite.zoom_x / 2), + MESSAGE_BOX_SPACING + 24 + (GRAPHIC_CHOOSER_PREVIEW_SIZE / 2) - (sprite.height * sprite.zoom_y / 2), + sprite.width * sprite.zoom_x, sprite.height * sprite.zoom_y, + Color.magenta) + end + set_preview_graphic.call(preview_sprite, list.value) + # Interaction loop + ret = nil + captured = nil + loop do + Graphics.update + Input.update + if captured + captured.update + captured = nil if !captured.busy? + else + list.update + captured = list if list.busy? + buttons.each do |btn| + btn[1].update + captured = btn[1] if btn[1].busy? + end + end + if list.changed? + set_preview_graphic.call(preview_sprite, list.value) + list.clear_changed + end + buttons.each do |btn| + next if !btn[1].changed? + ret = list.value if btn[0] == :ok + ret = selected if btn[0] == :cancel + break + end + ret = selected if Input.trigger?(Input::BACK) + break if ret + list.repaint + buttons.each { |btn| btn[1].repaint } + end + # Dispose and return + list.dispose + buttons.each { |btn| btn[1].dispose } + buttons.clear + bg_bitmap.dispose + preview_sprite.dispose + preview_bitmap&.dispose + @pop_up_bg_bitmap.visible = false + return ret + end + + #----------------------------------------------------------------------------- + + def choose_audio_file(selected, volume = 100, pitch = 100) + selected ||= "" + sprite_folder = "Audio/SE/Anim/" + # Get a list of files + files = [] + Dir.chdir(sprite_folder) do + Dir.glob("*.wav") { |f| files.push([File.basename(f, ".*"), f]) } + Dir.glob("*.ogg") { |f| files.push([File.basename(f, ".*"), f]) } + Dir.glob("*.mp3") { |f| files.push([File.basename(f, ".*"), f]) } + Dir.glob("*.wma") { |f| files.push([File.basename(f, ".*"), f]) } + end + files.delete_if { |f| ["USER", "TARGET"].include?(f[0].upcase) } + files.sort! { |a, b| a[0].downcase <=> b[0].downcase } + files.prepend(["USER", _INTL("[[User's cry]]")], + ["TARGET", _INTL("[[Target's cry]]")]) + idx = 0 + files.each_with_index do |f, i| + next if f[0] != selected + idx = i + break + end + # Show pop-up window + @pop_up_bg_bitmap.visible = true + bg_bitmap = create_pop_up_window(GRAPHIC_CHOOSER_WINDOW_WIDTH - 24, GRAPHIC_CHOOSER_WINDOW_HEIGHT) + text = _INTL("Choose a file...") + text_size = bg_bitmap.bitmap.text_size(text) + bg_bitmap.bitmap.draw_text(MESSAGE_BOX_SPACING, 11, bg_bitmap.width, text_size.height, text, 0) + # Create list of files + list = UIControls::List.new(GRAPHIC_CHOOSER_FILE_LIST_WIDTH, GRAPHIC_CHOOSER_FILE_LIST_HEIGHT, @pop_up_viewport, files) + list.x = bg_bitmap.x + MESSAGE_BOX_SPACING + list.y = bg_bitmap.y + MESSAGE_BOX_SPACING + 24 + list.selected = idx + list.set_interactive_rects + list.repaint + bg_bitmap.bitmap.outline_rect(MESSAGE_BOX_SPACING - 2, MESSAGE_BOX_SPACING + 24 - 2, + GRAPHIC_CHOOSER_FILE_LIST_WIDTH + 4, GRAPHIC_CHOOSER_FILE_LIST_HEIGHT + 4, Color.black) + # Create buttons + buttons = [] + [[:ok, _INTL("OK")], [:cancel, _INTL("Cancel")]].each_with_index do |option, i| + btn = UIControls::Button.new(GRAPHIC_CHOOSER_BUTTON_WIDTH, MESSAGE_BOX_BUTTON_HEIGHT, @pop_up_viewport, option[1]) + btn.x = list.x + (GRAPHIC_CHOOSER_BUTTON_WIDTH * i) + btn.y = list.y + list.height + 2 + btn.set_fixed_size + btn.set_interactive_rects + buttons.push([option[0], btn]) + end + # Create audio player controls + [[:volume, _INTL("Volume"), 0, 100], [:pitch, _INTL("Pitch"), 0, 200]].each_with_index do |option, i| + label = UIControls::Label.new(90, 28, @pop_up_viewport, option[1]) + label.x = list.x + list.width + 8 + label.y = list.y + (28 * i) + label.set_interactive_rects + buttons.push([(option[0].to_s + "_label").to_sym, label]) + slider = UIControls::NumberSlider.new(250, 28, @pop_up_viewport, option[2], option[3], (i == 0 ? volume : pitch)) + slider.x = list.x + list.width + 8 + label.width + slider.y = list.y + (28 * i) + slider.set_interactive_rects + buttons.push([option[0], slider]) + end + [[:play, _INTL("Play")], [:stop, _INTL("Stop")]].each_with_index do |option, i| + btn = UIControls::Button.new(GRAPHIC_CHOOSER_BUTTON_WIDTH, MESSAGE_BOX_BUTTON_HEIGHT, @pop_up_viewport, option[1]) + btn.x = list.x + list.width + 8 + (GRAPHIC_CHOOSER_BUTTON_WIDTH * i) + btn.y = list.y + (28 * 2) + btn.set_fixed_size + btn.set_interactive_rects + buttons.push([option[0], btn]) + end + # Interaction loop + ret = nil + captured = nil + loop do + Graphics.update + Input.update + if captured + captured.update + captured = nil if !captured.busy? + else + list.update + captured = list if list.busy? + buttons.each do |btn| + btn[1].update + captured = btn[1] if btn[1].busy? + end + end + buttons.each do |btn| + next if !btn[1].changed? + case btn[0] + when :ok + ret = list.value + when :cancel + ret = selected + when :play + vol = buttons.select { |b| b[0] == :volume }[0][1].value + ptch = buttons.select { |b| b[0] == :pitch }[0][1].value + # TODO: Play appropriate things if a cry is selected. + pbSEPlay(RPG::AudioFile.new("Anim/" + list.value, vol, ptch)) + when :stop + pbSEStop + end + btn[1].clear_changed + break + end + ret = selected if Input.trigger?(Input::BACK) + break if ret + list.repaint + buttons.each { |btn| btn[1].repaint } + end + vol = buttons.select { |b| b[0] == :volume }[0][1].value + ptch = buttons.select { |b| b[0] == :pitch }[0][1].value + # Dispose and return + list.dispose + buttons.each { |btn| btn[1].dispose } + buttons.clear + bg_bitmap.dispose + @pop_up_bg_bitmap.visible = false + return [ret, vol, ptch] + end +end diff --git a/Data/Scripts/904_Anim Editor/901_ParticleDataHelper.rb b/Data/Scripts/904_Anim Editor/901_ParticleDataHelper.rb index 4dfbccc69..3d9393d38 100644 --- a/Data/Scripts/904_Anim Editor/901_ParticleDataHelper.rb +++ b/Data/Scripts/904_Anim Editor/901_ParticleDataHelper.rb @@ -211,4 +211,77 @@ module AnimationEditor::ParticleDataHelper end return (ret.empty?) ? nil : ret end + + #----------------------------------------------------------------------------- + + def get_se_display_text(property, value) + ret = "" + case property + when :user_cry + ret += _INTL("[[User's cry]]") + when :target_cry + ret += _INTL("[[Target's cry]]") + when :se + ret += value[2] + else + raise _INTL("Unhandled property {1} for SE particle found.", property) + end + volume = (property == :se) ? value[3] : value[2] + ret += " " + _INTL("(volume: {1})", volume) if volume && volume != 100 + pitch = (property == :se) ? value[4] : value[3] + ret += " " + _INTL("(pitch: {1})", pitch) if pitch && pitch != 100 + return ret + end + + # Returns the volume and pitch of the SE to be played at the given frame + # of the given filename. + def get_se_values_from_filename_and_frame(particle, frame, filename) + return nil if !filename + case filename + when "USER", "TARGET" + property = (filename == "USER") ? :user_cry : :target_cry + slot = particle[property].select { |s| s[0] == frame }[0] + return nil if !slot + return slot[2] || 100, slot[3] || 100 + else + slot = particle[:se].select { |s| s[0] == frame && s[2] == filename }[0] + return nil if !slot + return slot[3] || 100, slot[4] || 100 + end + return nil + end + + # Deletes an existing command that plays the same filename at the same frame, + # and adds the new one. + def add_se_command(particle, frame, filename, volume, pitch) + delete_se_command(particle, frame, filename) + case filename + when "USER", "TARGET" + property = (filename == "USER") ? :user_cry : :target_cry + particle[property] ||= [] + particle[property].push([frame, 0, (volume == 100) ? nil : volume, (pitch == 100) ? nil : pitch]) + particle[property].sort! { |a, b| a[0] <=> b[0] } + else + particle[:se] ||= [] + particle[:se].push([frame, 0, filename, (volume == 100) ? nil : volume, (pitch == 100) ? nil : pitch]) + particle[:se].sort! { |a, b| a[0] <=> b[0] } + particle[:se].sort! { |a, b| (a[0] == b[0]) ? a[2].downcase <=> b[2].downcase : a[0] <=> b[0] } + end + end + + # Deletes an existing SE-playing command at the given frame of the given + # filename. + def delete_se_command(particle, frame, filename) + case filename + when "USER", "TARGET" + property = (filename == "USER") ? :user_cry : :target_cry + return if !particle[property] || particle[property].empty? + particle[property].delete_if { |s| s[0] == frame } + particle.delete(property) if particle[property].empty? + else + return if !particle[:se] || particle[:se].empty? + particle[:se].delete_if { |s| s[0] == frame && s[2] == filename } + particle.delete(:se) if particle[:se].empty? + end + end end