diff --git a/Data/Scripts/900_New utilities/001 utilities.rb b/Data/Scripts/900_New utilities/001 utilities.rb index 38aa3c21f..d1f55e05e 100644 --- a/Data/Scripts/900_New utilities/001 utilities.rb +++ b/Data/Scripts/900_New utilities/001 utilities.rb @@ -6,3 +6,91 @@ class Bitmap fill_rect(x + width - thickness, y, thickness, height, color) end end + +#=============================================================================== +# Fixed Compiler.pbWriteCsvRecord to make it detect enums first, allowing enum +# values to be turned into symbols/booleans/whatever instead of just numbers. +#=============================================================================== +module Compiler + module_function + + def pbWriteCsvRecord(record, file, schema) + rec = (record.is_a?(Array)) ? record.flatten : [record] + start = (["*", "^"].include?(schema[1][0, 1])) ? 1 : 0 + index = -1 + loop do + (start...schema[1].length).each do |i| + index += 1 + value = rec[index] + if schema[1][i, 1][/[A-Z]/] # Optional + # Check the rest of the values for non-nil things + later_value_found = false + (index...rec.length).each do |j| + later_value_found = true if !rec[j].nil? + break if later_value_found + end + if !later_value_found + start = -1 + break + end + end + file.write(",") if index > 0 + next if value.nil? + case schema[1][i, 1] + when "e", "E" # Enumerable + enumer = schema[2 + i] + case enumer + when Array + file.write(enumer[value]) + when Symbol, String + mod = Object.const_get(enumer.to_sym) + file.write(getConstantName(mod, value)) + when Module + file.write(getConstantName(enumer, value)) + when Hash + enumer.each_key do |key| + next if enumer[key] != value + file.write(key) + break + end + end + when "y", "Y" # Enumerable or integer + enumer = schema[2 + i] + case enumer + when Array + file.write((enumer[value].nil?) ? value : enumer[value]) + when Symbol, String + mod = Object.const_get(enumer.to_sym) + file.write(getConstantNameOrValue(mod, value)) + when Module + file.write(getConstantNameOrValue(enumer, value)) + when Hash + hasenum = false + enumer.each_key do |key| + next if enumer[key] != value + file.write(key) + hasenum = true + break + end + file.write(value) unless hasenum + end + else + if value.is_a?(String) + file.write((schema[1][i, 1].downcase == "q") ? value : csvQuote(value)) + elsif value.is_a?(Symbol) + file.write(csvQuote(value.to_s)) + elsif value == true + file.write("true") + elsif value == false + file.write("false") + else + file.write(value.inspect) + end + end + end + break if start > 0 && index >= rec.length - 1 + break if start <= 0 + end + return record + end +end diff --git a/Data/Scripts/900_New utilities/anim debug.rb b/Data/Scripts/900_New utilities/anim debug.rb new file mode 100644 index 000000000..9d5f507fa --- /dev/null +++ b/Data/Scripts/900_New utilities/anim debug.rb @@ -0,0 +1,8 @@ +MenuHandlers.add(:debug_menu, :create_animation_pbs_files, { + "name" => _INTL("Write all animation PBS files"), + "parent" => :files_menu, + "description" => _INTL("Write all animation PBS files."), + "effect" => proc { + Compiler.write_all_battle_animations + } +}) diff --git a/Data/Scripts/901_GameData/Animation.rb b/Data/Scripts/901_GameData/Animation.rb index 7530922b3..67622c8d0 100644 --- a/Data/Scripts/901_GameData/Animation.rb +++ b/Data/Scripts/901_GameData/Animation.rb @@ -1,26 +1,91 @@ module GameData class Animation - attr_reader :name - attr_reader :move, :type # Type is move's type; useful for filtering; move==nil means common animation - attr_reader :version # Hit number - # TODO: Boolean for whether user is on player's side or foe's side. + attr_reader :type # :move, :opp_move, :common, :opp_common + attr_reader :move # Either the move's ID or the common animation's name + attr_reader :version # Hit number + attr_reader :name # Shown in the sublist; cosmetic only # TODO: Boolean for not played if target is on user's side. attr_reader :particles attr_reader :flags - # TODO: PBS filename. -# attr_reader :pbs_filename + attr_reader :pbs_path # Whole path minus "PBS/Animations/" at start and ".txt" at end DATA = {} - # TODO: Make sure the existence of animations.dat is optional. Currently - # it's required. -# DATA_FILENAME = "animations.dat" -# PBS_BASE_FILENAME = "animations" + DATA_FILENAME = "animations.dat" + OPTIONAL = true + + SCHEMA = { + # TODO: Add support for overworld animations. + "SectionName" => [:id, "esU", {"Move" => :move, "OppMove" => :opp_move, + "Common" => :common, "OppCommon" => :opp_common}], + "Name" => [:name, "s"], + # TODO: Target (Screen, User, UserAndTarget, etc. Determines which focuses + # a particle can be given). + # TODO: DamageFrame (keyframe at which the battle continues, i.e. damage + # animations start playing). + "Flags" => [:flags, "*s"], + "Particle" => [:particles, "s"] + } + # For individual particles. All actions should have "^" in them. + # TODO: If more "SetXYZ"/"MoveXYZ" properties are added, ensure the "SetXYZ" + # ones are given a duration of 0 in def validate_compiled_animation. + SUB_SCHEMA = { + # These properties cannot be changed partway through the animation. + # TODO: "Name" isn't actually used; the name comes from the subsection + # written between and uses "Particle" above. +# "Name" => [:name, "s"], + "Focus" => [:focus, "e", {"User" => :user, "Target" => :target, + "UserAndTarget" => :user_and_target, "Screen" => :screen}], + # TODO FlipIfFoe, RotateIfFoe kinds of thing. + + # All properties below are "Set" or "Move". "Set" has the keyframe and the + # value, and "Move" has the keyframe, duration and the value. All are "^". + # "Set" is turned into "Move" with a duration (second value) of 0. + # TODO: The "MoveXYZ" commands will have optional easing (an enum). + "SetGraphic" => [:graphic, "^us"], + "SetFrame" => [:frame, "^uu"], # Frame within the graphic if it's a spritesheet + "MoveFrame" => [:frame, "^uuu"], + "SetBlending" => [:blending, "^uu"], # 0, 1 or 2 + "SetFlip" => [:flip, "^ub"], + "SetX" => [:x, "^ui"], + "MoveX" => [:x, "^uui"], + "SetY" => [:y, "^ui"], + "MoveY" => [:y, "^uui"], + "SetZoomX" => [:zoom_x, "^uu"], + "MoveZoomX" => [:zoom_x, "^uuu"], + "SetZoomY" => [:zoom_y, "^uu"], + "MoveZoomY" => [:zoom_y, "^uuu"], + "SetAngle" => [:angle, "^ui"], + "MoveAngle" => [:angle, "^uui"], + "SetOpacity" => [:opacity, "^uu"], + "MoveOpacity" => [:opacity, "^uuu"] + # TODO: SetPriority should be an enum. There should also be a property + # (set and move) for the sub-priority within that priority bracket. +# "SetPriority" + # TODO: Color. + # TODO: Tone. + + # TODO: Play, PlayUserCry, PlayTargetCry. + # TODO: ScreenShake? Not sure how to work this yet. Edit def + # validate_compiled_animation like the "SE" particle does with the + # "Play"-type commands. + } + + @@cmd_to_pbs_name = nil # USed for writing animation PBS files extend ClassMethodsIDNumbers include InstanceMethods - def register(hash, id = -1) - DATA[(id >= 0) ? id : DATA.keys.length] = self.new(hash) + singleton_class.alias_method(:__new_anim__load, :load) unless singleton_class.method_defined?(:__new_anim__load) + def self.load + __new_anim__load if FileTest.exist?("Data/#{self::DATA_FILENAME}") + end + + def self.sub_schema + return SUB_SCHEMA + end + + def self.register(hash, id_num = -1) + DATA[(id_num >= 0) ? id_num : DATA.keys.length] = self.new(hash) end # TODO: Rewrite this to query animations from other criteria. Remember that @@ -45,23 +110,116 @@ module GameData # end def initialize(hash) - @name = hash[:name] - @move = hash[:move] - @type = hash[:type] - @version = hash[:version] || 0 - @particles = [] - # TODO: Copy particles info from hash somehow. - @flags = hash[:flags] || [] - # TODO: Come up with a decent default PBS filename; likely the move's name - # (for move anims) or @name (for common anims). + # NOTE: hash has an :id entry, but it's unused here. + @type = hash[:type] + @move = hash[:move] + @version = hash[:version] || 0 + @name = hash[:name] + @particles = hash[:particles] || [] + @flags = hash[:flags] || [] + @pbs_path = hash[:pbs_path] || "#{@type} - #{@move}" + end + + # Returns a clone of the animation in a hash format, the same as created by + # the Compiler. This hash can be passed into self.register. + def clone_as_hash + ret = {} + ret[:type] = @type + ret[:move] = @move + ret[:version] = @version + ret[:name] = @name + ret[:particles] = [] # Clone the @particles array, which is nested hashes and arrays + @particles.each do |particle| + new_p = {} + particle.each_pair do |key, val| + if val.is_a?(Array) + new_p[val] = [] + val.each { |cmd| new_p[val].push(cmd.clone) } + else + new_p[key] = val + end + end + end + ret[:flags] = @flags.clone + ret[:pbs_path] = @pbs_path end def move_animation? - return !@move.nil? + return [:move, :opp_move].include?(@type) end - # TODO: Create a def to_hash or something, which returns a hash copy version - # of this Animation object which can be edited. This hash should be - # able to be passed into def register (with an ID number). + def common_animation? + return [:common, :opp_common].include?(@type) + end + + alias __new_anim__get_property_for_PBS get_property_for_PBS unless method_defined?(:__new_anim__get_property_for_PBS) + def get_property_for_PBS(key) + ret = __new_anim__get_property_for_PBS(key) + case key + when "SectionName" + ret = [@type, @move] + ret.push(@version) if @version > 0 + end + return ret + end + + def get_particle_property_for_PBS(key, index = 0) + ret = nil + ret = @particles[index][SUB_SCHEMA[key][0]] if SUB_SCHEMA[key] + ret = nil if ret == false || (ret.is_a?(Array) && ret.length == 0) || ret == "" + case key + when "Focus" + # The User and Target particles are hardcoded to only have their + # corresponding foci, so they don't need writing to PBS + if ["User", "Target"].include?(@particles[index][:name]) + ret = nil + elsif ret + ret = SUB_SCHEMA[key][2].key(ret) + end + when "AllCommands" + # Get translations of all properties to their names as seen in PBS + # animation files + if !@@cmd_to_pbs_name + @@cmd_to_pbs_name = {} + SUB_SCHEMA.each_pair do |key, val| + @@cmd_to_pbs_name[val[0]] ||= [] + @@cmd_to_pbs_name[val[0]].push([key, val[1].length]) + end + # For each property translation, put "SetXYZ" before "MoveXYZ" + @@cmd_to_pbs_name.each_value do |val| + val.sort! { |a, b| a[1] <=> b[1] } + val.map! { |a| a[0] } + end + end + # Gather all commands into a single array + ret = [] + @particles[index].each_pair do |key, val| + next if !val.is_a?(Array) + val.each do |cmd| + new_cmd = cmd.clone + new_cmd.insert(1, 0) if @@cmd_to_pbs_name[key].length == 1 # "SetXYZ" only + if new_cmd[1] > 0 + ret.push([@@cmd_to_pbs_name[key][1]] + new_cmd) # ["MoveXYZ", keyframe, duration, value] + else + ret.push([@@cmd_to_pbs_name[key][0]] + new_cmd) # ["SetXYZ", keyframe, duration, value] + end + end + end + # Sort the array of commands by keyframe order, then by duration, then + # by the order they're defined in SUB_SCHEMA + ret.sort! do |a, b| + if a[1] == b[1] + if a[2] == b[2] + next SUB_SCHEMA.keys.index(a[0]) <=> SUB_SCHEMA.keys.index(b[0]) + else + next a[2] <=> b[2] # Sort by duration + end + else + next a[1] <=> b[1] # Sort by keyframe + end + end + end + return ret + end end end diff --git a/Data/Scripts/902_Anim compiler/anim pbs compiler.rb b/Data/Scripts/902_Anim compiler/anim pbs compiler.rb new file mode 100644 index 000000000..0cca9c250 --- /dev/null +++ b/Data/Scripts/902_Anim compiler/anim pbs compiler.rb @@ -0,0 +1,182 @@ +module Compiler + module_function + + def compile_battle_animations(*paths) + GameData::Animation::DATA.clear + schema = GameData::Animation.schema + sub_schema = GameData::Animation.sub_schema + idx = 0 + # Read from PBS file(s) + paths.each do |path| + compile_pbs_file_message_start(path) + file_name = path.gsub(/^PBS\/Animations\//, "").gsub(/.txt$/, "") + data_hash = nil + current_particle = nil + section_name = nil + section_line = nil + # Read each line of the animation PBS file at a time and compile it as an + # animation property + pbCompilerEachPreppedLine(path) do |line, line_no| + echo "." if idx % 100 == 0 + idx += 1 + Graphics.update if idx % 500 == 0 + FileLineData.setSection(section_name, nil, section_line) + if line[/^\s*\[\s*(.+)\s*\]\s*$/] + # New section [anim_type, name] + section_name = $~[1] + section_line = line + if data_hash + validate_compiled_animation(data_hash) + GameData::Animation.register(data_hash) + end + FileLineData.setSection(section_name, nil, section_line) + # Construct data hash + data_hash = { + :pbs_path => file_name + } + data_hash[schema["SectionName"][0]] = get_csv_record(section_name.clone, schema["SectionName"]) + data_hash[schema["Particle"][0]] = [] + current_particle = nil + elsif line[/^\s*<\s*(.+)\s*>\s*$/] + # New subsection [particle_name] + value = get_csv_record($~[1], schema["Particle"]) + current_particle = { + # TODO: If "Particle" is changed to be more than just a single + # string, add more properties accordingly. + :name => value + } + data_hash[schema["Particle"][0]].push(current_particle) + elsif line[/^\s*(\w+)\s*=\s*(.*)$/] + # XXX=YYY lines + if !data_hash + raise _INTL("Expected a section at the beginning of the file.\n{1}", FileLineData.linereport) + end + key = $~[1] + if schema[key] # Property of the animation + value = get_csv_record($~[2], schema[key]) + if schema[key][1][0] == "^" + value = nil if value.is_a?(Array) && value.empty? + data_hash[schema[key][0]] ||= [] + data_hash[schema[key][0]].push(value) if value + else + value = nil if value.is_a?(Array) && value.empty? + data_hash[schema[key][0]] = value + end + elsif sub_schema[key] # Property of a particle + if !current_particle + raise _INTL("Particle hasn't been defined yet!\n{1}", FileLineData.linereport) + end + value = get_csv_record($~[2], sub_schema[key]) + if sub_schema[key][1][0] == "^" + value = nil if value.is_a?(Array) && value.empty? + current_particle[sub_schema[key][0]] ||= [] + current_particle[sub_schema[key][0]].push(value) if value + else + value = nil if value.is_a?(Array) && value.empty? + current_particle[sub_schema[key][0]] = value + end + end + end + end + # Add last animation's data to records + if data_hash + FileLineData.setSection(section_name, nil, section_line) + validate_compiled_animation(data_hash) + GameData::Animation.register(data_hash) + end + process_pbs_file_message_end + end + validate_all_compiled_animations + # Save all data + GameData::Animation.save + end + + def validate_compiled_animation(hash) + # Split anim_type, move/common_name, version into their own values + hash[:type] = hash[:id][0] + hash[:move] = hash[:id][1] + hash[:version] = hash[:id][2] || 0 + # Go through each particle in turn + hash[:particles].each do |particle| + # Convert all "SetXYZ" particle commands to "MoveXYZ" by giving them a + # duration of 0 + [:frame, :x, :y, :zoom_x, :zoom_y, :angle, :opacity].each do |prop| + next if !particle[prop] + particle[prop].each do |cmd| + cmd.insert(1, 0) if cmd.length == 2 + end + end + # Sort each particle's commands by their keyframe and duration + particle.keys.each do |key| + next if !particle[key].is_a?(Array) + particle[key].sort! { |a, b| a[0] == b[0] ? a[1] == b[1] ? 0 : a[1] <=> b[1] : a[0] <=> b[0] } + # TODO: Find any overlapping particle commands and raise an error. + end + # Ensure valid values for "SetBlending" commands + if particle[:blending] + particle[:blending].each do |blend| + next if blend[1] <= 2 + raise _INTL("Invalid blend value: {1} (must be 0, 1 or 2).\n{2}", + blend[1], FileLineData.linereport) + end + end + # TODO: Ensure "Play", "PlayUserCry", "PlayTargetCry" are exclusively used + # by the particle "SE", and that the "SE" particle can only use + # those commands. Raise if problems found. + + # Ensure all particles have a default focus if not given + if !particle[:focus] + if particle[:name] == "User" + particle[:focus] = :user + elsif particle[:name] == "Target" + particle[:focus] = :target + elsif particle[:name] != "SE" + particle[:focus] = :screen + end + end + + # TODO: Depending on hash[:target], ensure all particles have an + # appropriate focus (i.e. can't be :user_and_target if hash[:target] + # doesn't include a target). Raise if problems found. + end + end + + def validate_all_compiled_animations; end +end + +#=============================================================================== +# Hook into the regular Compiler to also compile animation PBS files. +#=============================================================================== +module Compiler + module_function + + def get_animation_pbs_files_to_compile + ret = [] + if FileTest.directory?("PBS/Animations") + Dir.all("PBS/Animations", "**/**.txt").each { |file| ret.push(file) } + end + return ret + end + + class << self + if !method_defined?(:__new_anims__get_all_pbs_files_to_compile) + alias_method :__new_anims__get_all_pbs_files_to_compile, :get_all_pbs_files_to_compile + end + if !method_defined?(:__new_anims__compile_pbs_files) + alias_method :__new_anims__compile_pbs_files, :compile_pbs_files + end + end + + def get_all_pbs_files_to_compile + ret = __new_anims__get_all_pbs_files_to_compile + extra = get_animation_pbs_files_to_compile + ret[:Animation] = [nil, extra] + return ret + end + + def compile_pbs_files + __new_anims__compile_pbs_files + text_files = get_animation_pbs_files_to_compile + compile_battle_animations(*text_files) + end +end diff --git a/Data/Scripts/902_Anim compiler/anim pbs writer.rb b/Data/Scripts/902_Anim compiler/anim pbs writer.rb new file mode 100644 index 000000000..56d10521a --- /dev/null +++ b/Data/Scripts/902_Anim compiler/anim pbs writer.rb @@ -0,0 +1,122 @@ +module Compiler + module_function + + def write_all_battle_animations + # Delete all existing .txt files in the PBS/Animations/ folder + files_to_delete = get_animation_pbs_files_to_compile + files_to_delete.each { |path| File.delete(path) } + # Get all files that need writing + paths = [] + GameData::Animation.each { |anim| paths.push(anim.pbs_path) if !paths.include?(anim.pbs_path) } + idx = 0 + # Write each file in turn + paths.each do |path| + Graphics.update if idx % 500 == 0 + idx += 1 + write_battle_animation_file(path) + end + end + + def write_battle_animation_file(path) + schema = GameData::Animation.schema + sub_schema = GameData::Animation.sub_schema + write_pbs_file_message_start(path) + # Create all subfolders needed + dirs = ("PBS/Animations/" + path).split("/") + dirs.pop # Remove the filename + dirs.length.times do |i| + dir_string = dirs[0..i].join("/") + if !FileTest.directory?(dir_string) + Dir.mkdir(dir_string) rescue nil + end + end + # Write file + File.open("PBS/Animations/" + path + ".txt", "wb") do |f| + add_PBS_header_to_file(f) + # Write each element in turn + GameData::Animation.each do |element| + next if element.pbs_path != path + f.write("\#-------------------------------\r\n") + if schema["SectionName"] + f.write("[") + pbWriteCsvRecord(element.get_property_for_PBS("SectionName"), f, schema["SectionName"]) + f.write("]\r\n") + else + f.write("[#{element.id}]\r\n") + end + # Write each animation property + schema.each_key do |key| + next if ["SectionName", "Particle"].include?(key) + val = element.get_property_for_PBS(key) + next if val.nil? + f.write(sprintf("%s = ", key)) + pbWriteCsvRecord(val, f, schema[key]) + f.write("\r\n") + end + # Write each particle in turn + element.particles.sort! do |a, b| + a_val = 0 + a_val = -2 if a[:name] == "User" + a_val = -1 if a[:name] == "Target" + a_val = 1 if a[:name] == "SE" + b_val = 0 + b_val = -2 if b[:name] == "User" + b_val = -1 if b[:name] == "Target" + b_val = 1 if b[:name] == "SE" + next a_val <=> b_val + end + element.particles.each_with_index do |particle, i| + # Write header + f.write("<" + particle[:name] + ">") + f.write("\r\n") + # Write one-off particle properties + sub_schema.each_pair do |key, val| + next if val[1][0] == "^" + val = element.get_particle_property_for_PBS(key, i) + next if val.nil? + f.write(sprintf(" %s = ", key)) + pbWriteCsvRecord(val, f, sub_schema[key]) + f.write("\r\n") + end + # Write particle commands (in keyframe order) + cmds = element.get_particle_property_for_PBS("AllCommands", i) + cmds.each do |cmd| + if cmd[2] == 0 # Duration of 0 + f.write(sprintf(" %s = ", cmd[0])) + new_cmd = cmd[1..-1] + new_cmd.delete_at(1) + pbWriteCsvRecord(new_cmd, f, sub_schema[cmd[0]]) + f.write("\r\n") + else # Has a duration + f.write(sprintf(" %s = ", cmd[0])) + pbWriteCsvRecord(cmd[1..-1], f, sub_schema[cmd[0]]) + f.write("\r\n") + end + end + end + end + end + process_pbs_file_message_end + end +end + +#=============================================================================== +# Hook into the regular Compiler to also write all animation PBS files. +#=============================================================================== +module Compiler + module_function + + class << self + if !method_defined?(:__new_anims__write_all) + alias_method :__new_anims__write_all, :write_all + end + end + + def write_all + __new_anims__write_all + Console.echo_h1(_INTL("Writing all animation PBS files")) + write_all_battle_animations + echoln "" + Console.echo_h2(_INTL("Successfully rewrote all animation PBS files"), text: :green) + end +end diff --git a/Data/Scripts/910_New anim editor/010 editor scene.rb b/Data/Scripts/910_New anim editor/010 editor scene.rb index df3ad2ee3..ccc7b2fcf 100644 --- a/Data/Scripts/910_New anim editor/010 editor scene.rb +++ b/Data/Scripts/910_New anim editor/010 editor scene.rb @@ -4,6 +4,11 @@ # TODO: Need a way to recognise when text is being input into something # (Input.text_input) and disable all keyboard shortcuts if so. If only # this class has keyboard shortcuts in it, then it should be okay already. +# TODO: When creating a new particle, blacklist the names "User", "Target" and +# "SE". Make particles with those names undeletable. +# TODO: Remove the particle named "Target" if the animation's focus is changed +# to one that doesn't include a target, and vice versa. Do the same for +# "User". #=============================================================================== class AnimationEditor WINDOW_WIDTH = AnimationEditorLoadScreen::WINDOW_WIDTH