Ver código fonte

Basic battle UI.

A very basic UI is displayed during the battles, but it can be used. Batttle now loads the correct models for the playable characters. The installers now creates an xml file with playable character models.
Iñigo Valentin 3 anos atrás
pai
commit
97dcb9b86f

+ 3 - 0
data/data/screens/_screens.xml

@@ -17,6 +17,9 @@
     <!-- Dialogs -->
     <prototype file_name="screens/dialog/prototypes.xml" />
     <screen file_name="screens/dialog/dialog.xml" />
+    <!-- Battle UI -->
+    <prototype file_name="screens/battle/prototypes.xml" />
+    <screen file_name="screens/battle/battle.xml" />
 
     <!-- Fade effect for map changer -->
     <screen file_name="screens/fade/fade.xml" />

+ 34 - 0
data/data/screens/battle/battle.xml

@@ -0,0 +1,34 @@
+<screen name="BattleUi" script="scripts/battle/battle_ui.lua" visible="false">
+    <widget name="Container" width="40%" height="40%" x="0" y="0" align="left" valign="top" scale="2.5 2.5" visible="true">
+        <widget name="Message" x="5%" y="2%" width="90%" height="24" visible="true">
+            <prototype name="Window" />
+            <text_area name="Name" x="12" y="6" width="100%" height="100%" font="FFVIIFont" text_name="BattleUiDebugExit" visible="true" />
+        </widget>
+        <widget name="Help" x="0%" y="75%-24" width="100%" height="24" visible="true">
+            <prototype name="Window" />
+        </widget>
+        <widget name="Bottom" x="0%" y="75%" width="100%" height="25%" visible="true">
+            <widget name="Left" x="0%" y="0%" width="40%" height="100%" visible="true">
+                <prototype name="Window" />
+                <text_area name="LabelName" scale="0.5 0.5" x="5%" y="-2" colour="0.9 0.9 0.9" font="FFVIIMenuFont" text_name="BattleUiName" visible="true" />
+                <text_area name="LabelBarrier" scale="0.5 0.5" x="68%" y="-2" colour="0.9 0.9 0.9" font="FFVIIMenuFont" text_name="BattleUiBarrier" visible="true" />
+            </widget>
+            <widget name="Right" x="40%" y="0%" width="60%" height="100%" visible="true">
+                <prototype name="Window" />
+                <text_area name="LabelHp" scale="0.5 0.5" x="3%" y="-2" font="FFVIIMenuFont" text_name="BattleUiHp" visible="true" />
+                <text_area name="LabelMp" scale="0.5 0.5" x="40%" y="-2" font="FFVIIMenuFont" text_name="BattleUiMp" visible="true" />
+                <text_area name="LabelLimit" scale="0.5 0.5" x="59%" y="-2" font="FFVIIMenuFont" text_name="BattleUiLimit" visible="true" />
+                <text_area name="LabelTime" scale="0.5 0.5" x="77%" y="-2" font="FFVIIMenuFont" text_name="BattleUiTime" visible="true" />
+            </widget>
+            <widget name="Character1" x="2%" y="12%" width="96%" height="30%" visible="true">
+                <prototype name="BattleCharacter" />
+            </widget>
+            <widget name="Character2" x="2%" y="42%" width="96%" height="30%" visible="true">
+                <prototype name="BattleCharacter" />
+            </widget>
+            <widget name="Character3" x="2%" y="72%" width="96%" height="30%" visible="true">
+                <prototype name="BattleCharacter" />
+            </widget>
+        </widget>
+    </widget>
+</screen>

+ 26 - 0
data/data/screens/battle/prototypes.xml

@@ -0,0 +1,26 @@
+<prototypes>
+    <prototype name="BattleCharacter">
+        <widget name="Left" x="0%" y="0%" width="40%" height="24" visible="true">
+            <text_area name="Name" x="0%" y="0%" width="65%" font="FFVIIFont" visible="true" />
+            <sprite name="Barriers" image="icons/battle_bar_barriers.png" x="65%" y="0" width="30%" height="14" visible="true" />
+        </widget>
+        <widget name="Right" x="40%" y="0%" width="60%" height="24" visible="true">
+            <widget name="Hp" x="3%" y="0%" width="35%" height="100%" visible="true">
+                <text_area name="HpCurrent" x="0%" y="0%" colour="0.9 0.9 0.9" font="FFVIIMenuDigits" visible="true" />
+                <sprite name="HpSlash" image="icons/slash.png" x="45%" y="0" width="4" height="8" visible="true" />
+                <text_area name="HpMax" x="55%" y="0%" font="FFVIIMenuDigits" visible="true" />
+                <sprite name="HpLine" x="0" y="10" width="100%" height="2" colour="0.3 0 0" visible="true" />
+                <sprite name="HpLineCurrent" x="0" y="10" width="100%" height="2" colours="0 0.5 1, 0.78 0.78 0.78, 0.78 0.78 0.78, 0 0.5 1" visible="true" />
+                <sprite name="HpLineShadow" x="0" y="15" width="100%" height="1" colour="0 0 0" visible="true" />
+            </widget>
+            <widget name="Mp" x="40%" y="0%" width="16%" height="100%" visible="true">
+                <text_area name="MpCurrent" x="0%" y="0%" width="100%" colour="0.9 0.9 0.9" font="FFVIIMenuDigits" visible="true" />
+                <sprite name="MpLine" x="0" y="10" width="100%" height="2" colour="0.3 0 0" visible="true" />
+                <sprite name="MpLineCurrent" x="0" y="10" width="100%" height="2" colours="0 0.5 1, 0.78 0.78 0.78, 0.78 0.78 0.78, 0 0.5 1" visible="true" />
+                <sprite name="MpLineShadow" x="0" y="11" width="100%" height="1" colour="0 0 0" visible="true" />
+            </widget>
+            <sprite name="Limit" image="icons/battle_bar.png" x="60%" y="0" width="18%" height="14" visible="true" />
+            <sprite name="Atb" image="icons/battle_bar.png" x="80%" y="0" width="18%" height="14" visible="true" />
+        </widget>
+    </prototype>
+</prototypes>

+ 17 - 0
data/data/scripts/battle.lua

@@ -96,6 +96,23 @@ Battle.logic_script = {
     end,
 }
 
+--- Starts a battle.
+--
+-- @param id Battle ID.
+Battle.start = function(id)
+    print("Starting battle " .. tostring(id))
+    battle_manager:start_battle(id)
+    script:request_end_sync(Script.UI, "BattleUi", "show", 0)
+    return 0
+end
+
+--- Ends a battle.
+Battle.finish = function()
+    print("Ending battle")
+    battle_manager:end_battle()
+    script:request_end_sync(Script.UI, "Battle", "hide", 0)
+    return 0
+end
 
 
 Battle.init = function()

+ 148 - 0
data/data/scripts/battle/battle_ui.lua

@@ -0,0 +1,148 @@
+if UiContainer == nil then UiContainer = {} end
+
+--- The main menu.
+UiContainer.BattleUi = {
+
+    --- Current cursor position in the main menu (1-10).
+    position = 1,
+
+    -- Max cursor position in the main menu.
+    position_total = 11,
+
+    --- When selecting a character, the menu in this position will be opened.
+    --
+    -- If 0, not selecting character.
+    select_character_for_menu = 0,
+
+    --- Current cursorposition in the charaster list (1-3).
+    character_position = 1,
+
+    --- Max cursor position in the characetr list.
+    character_position_total = 3,
+
+    --- Run when the menu is creaded.
+    --
+    -- It does nothing.
+    on_start = function(self)
+        print("BattleUi start")
+        return 0
+    end,
+
+    --- Handles button events.
+    --
+    -- For the current submenu, handles directional keys, enter and escape events.
+    -- @param button Pressed button string key. "Up", "Left", "Enter" and "Escape" are handled.
+    -- @param event Trigger event. Normally, "Press".
+    on_button = function(self, button, event)
+        return 0
+    end,
+
+    --- Shows the battle UI.
+    --
+    -- Populates and updates displayed data.
+    show = function(self)
+        self:populate()
+        print("POPULATED")
+        ui_manager:get_widget("BattleUi"):set_visible(true)
+        --local characters  = ui_manager:get_widget("MainMenu.Container.Characters")
+        --local menu = ui_manager:get_widget("MainMenu.Container.Menu")
+        --local menu_cursor = ui_manager:get_widget("MainMenu.Container.Menu.Cursor")
+        --local timegil = ui_manager:get_widget("MainMenu.Container.TimeGil")
+        --local location = ui_manager:get_widget("MainMenu.Container.Location")
+
+
+        --for c = 1, 3 do
+            --if Party[c] ~= nil then
+                --UiContainer.populate_character_data("MainMenu.Container.Characters.Character" .. tostring(c), Characters[Party[c]])
+                --ui_manager:get_widget("MainMenu.Container.Characters.Character" .. tostring(c) .. ".Portrait"):set_image("characters/" .. tostring(Party[c]) .. ".png")
+                --if Characters[Party[c]].back_row == 1 then
+                    --ui_manager:get_widget("MainMenu.Container.Characters.Character" .. tostring(c) .. ".Portrait"):set_default_animation("RowBack")
+                --else
+                   --ui_manager:get_widget("MainMenu.Container.Characters.Character" .. tostring(c) .. ".Portrait"):set_default_animation("RowFront")
+                --end
+                --ui_manager:get_widget("MainMenu.Container.Characters.Character" .. tostring(c) .. ".LimitLevel"):set_text(tostring(Characters[Party[c]].limit.current))
+            --else
+                --UiContainer.populate_character_data("MainMenu.Container.Characters.Character" .. tostring(c), nil)
+            --end
+        --end
+
+        -- TODO: Set portraits of the correct char
+
+        --ui_manager:get_widget("MainMenu.Container.Menu.PHSText"):set_visible(false)
+        --ui_manager:get_widget("MainMenu.Container.Menu.SaveText"):set_colour(0.4, 0.4, 0.4)
+
+        -- Set location
+        --ui_manager:get_widget("MainMenu.Container.Location.Text"):set_text(System["MapChanger"].location_name)
+
+        -- Set money
+        --local money_str = tostring(Inventory.money)
+        --while #(money_str) < 8 do
+        --    money_str = " " .. money_str
+        --end
+        --ui_manager:get_widget("MainMenu.Container.TimeGil.Gil"):set_text(money_str)
+
+        --characters:play_animation_stop("Appear")
+        --menu:play_animation_stop("Appear")
+        --menu_cursor:set_default_animation("Position" .. self.position)
+        --timegil:play_animation_stop("Appear")
+        --location:play_animation_stop("Appear")
+        --location:animation_sync()
+        return 0;
+    end,
+
+    --- Hides the battle UI.
+    --
+    -- It also unpauses the game.
+    hide = function(self)
+        --local characters  = ui_manager:get_widget("MainMenu.Container.Characters")
+        --local menu = ui_manager:get_widget("MainMenu.Container.Menu")
+        --local timegil = ui_manager:get_widget("MainMenu.Container.TimeGil")
+        --local location = ui_manager:get_widget("MainMenu.Container.Location")
+
+        --characters:play_animation_stop("Disappear")
+        --menu:play_animation_stop("Disappear")
+        --timegil:play_animation_stop("Disappear")
+        --location:play_animation_stop("Disappear")
+        --location:animation_sync()
+
+        ui_manager:get_widget("BattleUi"):set_visible(false)
+
+        --MenuSettings.pause_available = true
+        --entity_manager:set_paused(false)
+
+        return 0;
+    end,
+
+
+    populate = function(self)
+
+        space_pad = function(val, length)
+            str = tostring(val)
+            while #(str) < length do
+                str = " " .. str
+            end
+            return str
+        end
+
+        print("POPULATING")
+
+        for c = 1, 3 do
+            print("POPULATING " .. tostring(c))
+            if Party[c] ~= nil then
+                print("POPULATING " .. tostring(c) .. " NOT NULL")
+                ui_manager:get_widget("BattleUi.Container.Bottom.Character" .. tostring(c)):set_visible(true)
+                print("POPULATING " .. tostring(c) .. " NAME")
+                ui_manager:get_widget("BattleUi.Container.Bottom.Character" .. tostring(c) .. ".Left.Name"):set_text(Characters[Party[c]].name)
+                print("POPULATING " .. tostring(c) .. " HP")
+                ui_manager:get_widget("BattleUi.Container.Bottom.Character" .. tostring(c) .. ".Right.Hp.HpCurrent"):set_text(space_pad(Characters[Party[c]].stats.hp.current, 5))
+                ui_manager:get_widget("BattleUi.Container.Bottom.Character" .. tostring(c) .. ".Right.Hp.HpMax"):set_text(space_pad(Characters[Party[c]].stats.hp.base, 5))
+                print("POPULATING " .. tostring(c) .. " MP")
+                ui_manager:get_widget("BattleUi.Container.Bottom.Character" .. tostring(c) .. ".Right.Mp.MpCurrent"):set_text(space_pad(Characters[Party[c]].stats.mp.current, 5))
+            else
+                ui_manager:get_widget("BattleUi.Container.Bottom.Character" .. tostring(c)):set_visible(false)
+            end
+        end
+        print("POPULATING END")
+        return 0
+    end
+}

+ 29 - 29
data/data/scripts/system.lua

@@ -342,53 +342,53 @@ Inventory.sort = function(criteria)
         print("Custom order unimplemented")
     elseif criteria == Inventory.ORDER.FIELD then
         table.sort(Inventory, function (left, right)
-                                  if Game.Items[left.item].menu ~= Game.Items[right.item].menu then
-                                      return Game.Items[left.item].menu > Game.Items[right.item].menu
-                                  end
-                                  return left.item < right.item
-                              end
+            if Game.Items[left.item].menu ~= Game.Items[right.item].menu then
+                return Game.Items[left.item].menu > Game.Items[right.item].menu
+            end
+            return left.item < right.item
+        end
         )
     elseif criteria == Inventory.ORDER.BATTLE then
         table.sort(Inventory, function (left, right)
-                                  if Game.Items[left.item].battle ~= Game.Items[right.item].battle then
-                                      return Game.Items[left.item].battle > Game.Items[right.item].battle
-                                  end
-                                  return left.item < right.item
-                              end
+            if Game.Items[left.item].battle ~= Game.Items[right.item].battle then
+                return Game.Items[left.item].battle > Game.Items[right.item].battle
+            end
+            return left.item < right.item
+        end
         )
     elseif criteria == Inventory.ORDER.THROW then
         table.sort(Inventory, function (left, right)
-                                  if Game.Items[left.item].throw ~= Game.Items[right.item].throw then
-                                      return Game.Items[left.item].throw > Game.Items[right.item].throw
-                                  end
-                                  return left.item < right.item
-                              end
+            if Game.Items[left.item].throw ~= Game.Items[right.item].throw then
+                return Game.Items[left.item].throw > Game.Items[right.item].throw
+            end
+            return left.item < right.item
+        end
         )
     elseif criteria == Inventory.ORDER.TYPE then
         table.sort(Inventory, function (left, right)
-                                  return left.item < right.item
-                              end
+            return left.item < right.item
+        end
         )
     elseif criteria == Inventory.ORDER.NAME then
         table.sort(Inventory, function (left, right)
-                                  return Game.Items[left.item].name < Game.Items[right.item].name
-                              end
+            return Game.Items[left.item].name < Game.Items[right.item].name
+        end
         )
     elseif criteria == Inventory.ORDER.MOST then
         table.sort(Inventory, function (left, right)
-                                  if left.quantity ~= right.quantity then
-                                      return left.quantity > right.quantity
-                                  end
-                                  return left.item < right.item
-                              end
+            if left.quantity ~= right.quantity then
+                return left.quantity > right.quantity
+            end
+            return left.item < right.item
+        end
         )
     elseif criteria == Inventory.ORDER.LEAST then
         table.sort(Inventory, function (left, right)
-                                  if left.quantity ~= right.quantity then
-                                      return left.quantity < right.quantity
-                                  end
-                                  return left.item < right.item
-                              end
+            if left.quantity ~= right.quantity then
+                return left.quantity < right.quantity
+            end
+            return left.item < right.item
+        end
         )
     end
 end

+ 8 - 0
data/data/texts/english/menu.xml

@@ -128,4 +128,12 @@
     <text name="CharacterDataNextLevel">Next Lv.</text>
     <text name="CharacterDataLimitLevel">Limit Lv.</text>
     <text name="CharacterDataLimitLevelNumber">3</text>
+
+    <text name="BattleUiName">NAME</text>
+    <text name="BattleUiBarrier">BARRIER</text>
+    <text name="BattleUiHp">HP</text>
+    <text name="BattleUiMp">MP</text>
+    <text name="BattleUiLimit">LIMIT</text>
+    <text name="BattleUiTime">TIME</text>
+    <text name="BattleUiDebugExit">Debug: Press 'K' to end battle.</text>
 </texts>

+ 49 - 3
src/core/BattleManager.cpp

@@ -19,6 +19,7 @@
 #include <OgreEntity.h>
 #include <OgreRoot.h>
 #include <OgreViewport.h>
+#include "FF7Character.h"
 #include "core/AudioManager.h"
 #include "core/BattleManager.h"
 #include "core/CameraManager.h"
@@ -29,6 +30,7 @@
 #include "core/InputManager.h"
 #include "core/Logger.h"
 #include "core/ScriptManager.h"
+#include "core/TextHandler.h"
 #include "core/UiManager.h"
 #include "core/XmlFormationFile.h"
 
@@ -270,14 +272,58 @@ void BattleManager::LoadParty(){
         LOG_ERROR("Tried to load party in the BattleManager, but it's not in battle mode.");
         return;
     }
-    std::vector<int> positions {0, 1, 2};
+    std::vector<int> party = TextHandler::getSingleton().GetParty();
+    std::vector<int> positions {};
+    for (int i = 0; i < party.size(); i ++) positions.push_back(i);
     Ogre::Vector3 position = Ogre::Vector3(0, 5, 0);
     // Randomize party member positions.
     std::random_shuffle(positions.begin(), positions.end());
+
+    // TODO: Find a way to do this during the instalallation
     for (int i = 0; i < positions.size(); i ++){
+        std::string name = "";
+        std::string model = "";
+        switch (party.at(positions.at(i))){
+            case Character::CLOUD:
+                name = "party_cloud";
+                model = "characters/rt_cloud.mesh";
+                break;
+            case Character::TIFA:
+                name = "party_tifa";
+                model = "characters/ru_tifa.mesh";
+                break;
+            case Character::AERIS:
+                name = "party_aeris";
+                model = "characters/rv_aeris.mesh";
+                break;
+            case Character::BARRET:
+                name = "party_barret";
+                model = "characters/sb_barret.mesh";
+                break;
+            case Character::RED:
+                name = "party_red";
+                model = "characters/rw_red.mesh";
+                break;
+            // TODO: Cid data not installed
+            case Character::CID:
+                name = "party_cid";
+                model = "characters/TODO_cid.mesh";
+                break;
+            case Character::VINCENT:
+                name = "party_vincent";
+                model = "characters/sh_vincent.mesh";
+                break;
+            case Character::YUVI:
+                name = "party_yuvi";
+                model = "characters/rx_yuffie.mesh";
+                break;
+            case Character::KETCY:
+                name = "party_ketcy";
+                model = "characters/ry_cait_sith.mesh";
+                break;
+        }
         EntityManager::getSingleton().AddBattleEntity(
-          "party_" + std::to_string(i),
-          "enemies/at_grunt.mesh", position, Ogre::Degree(0),
+          name, model, position, Ogre::Degree(0),
           Ogre::Vector3(MODEL_SCALE, MODEL_SCALE, MODEL_SCALE), 100 + i, true
         );
 

+ 0 - 1
src/core/ScriptManager.cpp

@@ -324,7 +324,6 @@ void ScriptManager::ClearField(){}
 
 void ScriptManager::ClearBattle(){
     // Remove all battle script entities.
-    // TODO: Allso remove them as node scenes.
     for (unsigned int i = 0; i < script_entity_.size(); ++ i){
         if (script_entity_[i].type == BATTLE){
             while(script_entity_[i].queue.size() > 0)

+ 7 - 2
src/core/TextHandler.cpp

@@ -120,7 +120,12 @@ void TextHandler::SetCharacterName(int id, char* name){
 
 void TextHandler::SetParty(int char_1, int char_2, int char_3){
     current_party_[0] = char_1;
-    current_party_[2] = char_2;
-    current_party_[3] = char_3;
+    current_party_[1] = char_2;
+    current_party_[2] = char_3;
+}
+
+std::vector<int> TextHandler::GetParty(){
+    std::vector<int> party = {current_party_[0], current_party_[1], current_party_[2]};
+    return party;
 }
 

+ 7 - 0
src/core/TextHandler.h

@@ -152,6 +152,13 @@ class TextHandler : public Ogre::Singleton<TextHandler>{
          */
         void SetParty(int char_1, int char_2, int char_3);
 
+        /**
+         * Retrieves the party member IDs.
+         *
+         * @return The list of party member IDs.
+         */
+        std::vector<int> GetParty();
+
     private:
 
         /**

+ 54 - 1
src/installer/BattleDataInstaller.cpp

@@ -208,7 +208,42 @@ unsigned int BattleDataInstaller::ProcessModel(){
         );
         TexFile tex(battle_lgp_files_[next_model_to_process_]);
         std::string path = output_dir_ + "models/battle/";
-        if (info.is_player) path += "characters/";
+        if (info.is_player){
+            path += "characters/";
+            // Add or update on character_data_
+            bool found = false;
+            for (auto data: character_data_){
+                if (data.id == info.numeric_id){
+                    // Check if the model already exists for this character
+                    bool model_found = false;
+                    for (auto mod: data.models){
+                        if (mod.model == info.alphanumeric_id + "_" + info.name_normal + ".mesh"){
+                            model_found = true;
+                            break;
+                        }
+                    }
+                    if (model_found) break;
+                    CharacterModel model;
+                    model.name = info.name;
+                    model.model = info.alphanumeric_id + "_" + info.name_normal + ".mesh";
+                    std::cout << "Character model update: " << std::to_string(data.id) << " " << model.name << " : " << model.model << std::endl;
+                    data.models.push_back(model);
+                    character_data_.push_back(data);
+                    found = true;
+                    break;
+                }
+            }
+            if (!found){
+                CharacterData character_data;
+                character_data.id = info.numeric_id;
+                CharacterModel model;
+                model.name = info.name;
+                model.model = info.alphanumeric_id + "_" + info.name_normal + ".mesh";
+                std::cout << "Character model new: " << std::to_string(character_data.id) << " " << model.name << " : " << model.model << std::endl;
+                character_data.models.push_back(model);
+                character_data_.push_back(character_data);
+            }
+        }
         else if (info.is_scene) path += "scenes/";
         else path += "enemies/";
         path += (id + type + "_" + info.name_normal + ".png");
@@ -643,6 +678,24 @@ void BattleDataInstaller::WriteFormations(){
     }
 }
 
+void BattleDataInstaller::WriteCharacterData(){
+    TiXmlDocument xml;
+    std::unique_ptr<TiXmlElement> container(new TiXmlElement("characters"));
+    for (CharacterData data : character_data_){
+        std::unique_ptr<TiXmlElement> xml_character(new TiXmlElement("character"));
+        xml_character->SetAttribute("id", data.id);
+        for (CharacterModel model : data.models){
+            std::unique_ptr<TiXmlElement> xml_model(new TiXmlElement("model"));
+            xml_model->SetAttribute("name", model.name);
+            xml_model->SetAttribute("file", model.model);
+            xml_character->LinkEndChild(xml_model.release());
+        }
+        container->LinkEndChild(xml_character.release());
+    }
+    xml.LinkEndChild(container.release());
+    xml.SaveFile(output_dir_ + "gamedata/characters.xml");
+}
+
 std::string BattleDataInstaller::BuildEnemyFileName(Enemy enemy){
     std::string file_name = "gamedata/enemy/";
     std::string id = std::to_string(enemy.id);

+ 42 - 0
src/installer/BattleDataInstaller.h

@@ -108,6 +108,11 @@ class BattleDataInstaller{
          */
         void WriteFormations();
 
+        /**
+         * Writes playable character data.
+         */
+        void WriteCharacterData();
+
     private:
 
         /**
@@ -156,6 +161,38 @@ class BattleDataInstaller{
 
         };
 
+        /**
+         * Models for playable characters.
+         */
+        struct CharacterModel{
+
+            /**
+             * Descriptive name for the model.
+             */
+            std::string name;
+
+            /**
+             * Model file.
+             */
+            std::string model;
+        };
+
+        /**
+         * Playable character model info.
+         */
+        struct CharacterData{
+
+            /**
+             * Character ID.
+             */
+            unsigned int id;
+
+            /**
+             * List of models for the character.
+             */
+            std::vector<CharacterModel> models;
+        };
+
         /**
          * Extract a gzipped file.
          *
@@ -267,6 +304,11 @@ class BattleDataInstaller{
          */
         std::vector<Enemy> enemies_;
 
+        /**
+         * List of models for playable character;
+         */
+        std::vector<CharacterData> character_data_;
+
         /**
          * Map of enemies, ID and xml file path.
          */

+ 6 - 1
src/installer/DataInstaller.cpp

@@ -134,7 +134,12 @@ float DataInstaller::Progress(){
             return CalcProgress();
         case BATTLE_MODELS_CONVERT:
             cur_substep_ = battle_installer_->ConvertModel();
-            if (cur_substep_ >= substeps_) installation_state_ = KERNEL_PRICES;
+            if (cur_substep_ >= substeps_) installation_state_ = BATTLE_MODELS_WRITE_CHARACTERS;
+            return CalcProgress();
+        case BATTLE_MODELS_WRITE_CHARACTERS:
+            write_output_line_("Writing character data...", 2, true);
+            battle_installer_->WriteCharacterData();
+            installation_state_ = KERNEL_ITEMS;
             return CalcProgress();
         case KERNEL_PRICES:
             // Skip kernel data if option is set.

+ 5 - 0
src/installer/DataInstaller.h

@@ -220,6 +220,11 @@ class DataInstaller{
              */
             BATTLE_MODELS_CONVERT,
 
+            /**
+             * Writes playable character model info.
+             */
+            BATTLE_MODELS_WRITE_CHARACTERS,
+
             /**
              * Parses item and materia prices from ff7.exe.
              */

+ 6 - 0
src/installer/data/FF7Data.h

@@ -3021,6 +3021,12 @@ class FF7Data{
                 info.name_normal = "cait_sith";
                 info.is_player = true;
             }
+            else if ("ry" == alphanumeric_id){
+                info.numeric_id = CHAR_ID::CID;
+                info.name = "Cid";
+                info.name_normal = "cid";
+                info.is_player = true;
+            }
             else if (
               "sf" == alphanumeric_id
               || "sg" == alphanumeric_id

+ 1 - 1
src/installer/decompiler/field/instruction/FieldModuleInstruction.cpp

@@ -107,7 +107,7 @@ void FieldModuleInstruction::ProcessBATTLE(CodeGenerator* code_gen){
     const auto& battle_id = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[0]->GetUnsigned(), params_[1]->GetUnsigned()
     );
-    code_gen->AddOutputLine((boost::format("battle_manager:start_battle(%1%)") % battle_id).str());
+    code_gen->AddOutputLine((boost::format("Battle.start(%1%)") % battle_id).str());
 }
 
 void FieldModuleInstruction::ProcessBTLON(CodeGenerator* code_gen){