Browse Source

Character commands loaded during battle.

The installer now generates XML files relating battle scenes and playable characters with their 3D models, and V-Gears uses them. Now, each battle can load it's own background.
Iñigo Valentin 3 years ago
parent
commit
4b4af67fb7

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

@@ -29,6 +29,21 @@
             <widget name="Character3" x="2%" y="72%" width="96%" height="30%" visible="true">
                 <prototype name="BattleCharacter" />
             </widget>
+            <widget name="Commands" x="22%" y="5%" width="60" height="95%" visible="false">
+                <prototype name="Window" />
+                <text_area name="Cmd1" x="5" y="0%+5" width="100%-5" height="25%" font="FFVIIFont" text_name="BattleUiHp" visible="true" />
+                <text_area name="Cmd2" x="5" y="22%+5" width="100%-5" height="25%" font="FFVIIFont" text_name="BattleUiHp" visible="true" />
+                <text_area name="Cmd3" x="5" y="44%+5" width="100%-5" height="25%" font="FFVIIFont" text_name="BattleUiHp" visible="true" />
+                <text_area name="Cmd4" x="5" y="66%+5" width="100%-5" height="25%" font="FFVIIFont" text_name="BattleUiHp" visible="true" />
+                <text_area name="Cmd5" x="55" y="0%+5" width="100%-5" height="25%" font="FFVIIFont" text_name="BattleUiHp" visible="true" />
+                <text_area name="Cmd6" x="55" y="22%+5" width="100%-5" height="25%" font="FFVIIFont" text_name="BattleUiHp" visible="true" />
+                <text_area name="Cmd7" x="55" y="44%+5" width="100%-5" height="25%" font="FFVIIFont" text_name="BattleUiHp" visible="true" />
+                <text_area name="Cmd8" x="55" y="66%+5" width="100%-5" height="25%" font="FFVIIFont" text_name="BattleUiHp" visible="true" />
+                <text_area name="Cmd9" x="100" y="0%+5" width="100%-5" height="25%" font="FFVIIFont" text_name="BattleUiHp" visible="true" />
+                <text_area name="Cmd10" x="100" y="22%+5" width="100%-5" height="25%" font="FFVIIFont" text_name="BattleUiHp" visible="true" />
+                <text_area name="Cmd11" x="100" y="44%+5" width="100%-5" height="25%" font="FFVIIFont" text_name="BattleUiHp" visible="true" />
+                <text_area name="Cmd12" x="100" y="66%+5" width="100%-5" height="25%" font="FFVIIFont" text_name="BattleUiHp" visible="true" />
+            </widget>
         </widget>
     </widget>
 </screen>

+ 187 - 72
data/data/scripts/battle/battle_ui.lua

@@ -3,25 +3,11 @@ if UiContainer == nil then UiContainer = {} end
 --- The main menu.
 UiContainer.BattleUi = {
 
-    --- Indicates if the battle UI is active.
-    active = false,
+    --- Party members
+    party = {},
 
-    --- 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,
+    --- Enemies
+    enemies = {},
 
     --- Run when the menu is creaded.
     --
@@ -45,6 +31,156 @@ UiContainer.BattleUi = {
         return 0
     end,
 
+    load_party_members = function(self)
+        for i = 1, Party.MAX do
+            if Party[i] ~= nil then
+                self.party[i] = {}
+                self.party[i].character = Party[i]
+                --math.randomseed(('' .. os.time()):reverse())
+                -- TODO: Consider battle layouts.
+                -- TODO: Check random seed
+                self.party[i].atb = math.random(0, 100)
+                self.party[i].command = {}
+                self.party[i].magic = {}
+                self.party[i].summon = {}
+                self.party[i].cover = 0
+                self.party[i].counter = 0
+                self.party[i].long_range = false
+                self.party[i].mega_all = false
+                self.party[i].pre_emptive = 0
+                local w_magic = false
+                local w_item = false
+                local w_summon = false
+                for e = 1, 2 do -- Loop for weapon, armor
+                    local equip = nil
+                    if e == 1 then
+                        equip = Characters[Party[i]].weapon
+                    else
+                        equip = Characters[Party[i]].armor
+                    end
+                    for m = 1, 8 do -- lop for materia in weapon/armor
+                        if equip.materia[m - 1] ~= nil then
+                            local materia = Game.Materia[equip.materia[m - 1].id]
+                            local ap = equip.materia[m - 1].ap
+                            local level = 0
+                            for l = 1, #materia.levels_ap do
+                                if materia.levels_ap[l] <= ap then
+                                    level = l
+                                end
+                            end
+                            if materia.type == 5 then -- Summon
+                                summon = {
+                                    id = materia.summon,
+                                    times = materia.times[level],
+                                    quadra = 0,
+                                    mp_turbo = 0,
+                                    mp_absorb = 0,
+                                    hp_absorb = 0,
+                                    sneak_attack = 0,
+                                    final_attack = 0,
+                                    added_cut = false,
+                                    steal_as_well = false
+                                }
+                                -- TODO: Check if already inserted.
+                                self.party[i].summon[#self.party[i].summon + 1] = summon
+                            elseif materia.type == 1 then -- Magic
+                                for lv, spell in ipairs(materia.magic) do
+                                    if level >= lv then
+                                        magic = {
+                                            id = spell,
+                                            all = 0,
+                                            quadra = 0,
+                                            mp_turbo = 0,
+                                            mp_absorb = 0,
+                                            hp_absorb = 0,
+                                            sneak_attack = 0,
+                                            final_attack = 0,
+                                            added_cut = false,
+                                            steal_as_well = false
+                                        }
+                                        -- TODO: Check if already inserted.
+                                        self.party[i].magic[#self.party[i].magic + 1] = magic
+                                    end
+                                end
+                            elseif materia.type == 3 then -- Command
+                                -- Pick only the highest level command available
+                                local command_id = nil
+                                for lv, cmd in ipairs(materia.command) do
+                                    if level >= lv then
+                                        command_id = cmd
+                                    end
+                                end
+                                if command_id ~= nil then
+                                    if
+                                      command_id ~= 1 and command_id ~= 20 and command_id ~= 24
+                                      and command_id ~= 25 and command_id ~= 26
+                                      and command_id ~= 26 and #self.party[i].command + 1 == 1
+                                    then
+                                        -- Leave command index 1 available for atgtack and substitutes
+                                        -- 1: Attack, 20: Limit, 24: Slash-all, 25: 2x-Cut, 26: Flash, 27: 4x-Cut
+                                        self.party[i].command[#self.party[i].command + 2] = command_id
+                                    elseif command_id ~= 23 and #self.party[i].command + 1 == 4 then
+                                        -- Leave command index 4 available for item
+                                        self.party[i].command[#self.party[i].command + 2] = command_id
+                                    else
+                                        self.party[i].command[#self.party[i].command + 1] = command_id
+                                    end
+                                end
+                            end
+                            -- TODO: Independent materia
+                            -- TODO: Linked materia
+                        end
+                    end
+                end
+                -- Set default "Attack" command.
+                if self.party[i].command[1] == nil then
+                    self.party[i].command[1] = 1
+                end
+                -- Command in slot 4 is reserved for "Item" or "W-Item"
+                if self.party[i].command[4] == nil then
+                    self.party[i].command[4] = 4
+                end
+                -- Enable Magic command if any magic is available and W-Magic is disabled.
+                if #self.party[i].magic > 0 then
+                    local w = false
+                    for x = 1, #self.party[i].command do
+                        if self.party[i].command[x] == 21 then
+                            w = true
+                        end
+                    end
+                    if w == false then
+                        if #self.party[i].command + 1 == 1 or #self.party[i].command + 1 == 4 then
+                            -- Leave command index 1 available for attack or substitutes, and
+                            -- index 4 available for item
+                            self.party[i].command[#self.party[i].command + 2] = 2
+                        else
+                            self.party[i].command[#self.party[i].command + 1] = 2
+                        end
+                    end
+                end
+                -- Enable Summon command if any magic is available and W-Summon is disabled.
+                if #self.party[i].summon > 0 then
+                    local w = false
+                    for x = 1, #self.party[i].command do
+                        if self.party[i].command[x] == 22 then
+                            w = true
+                        end
+                    end
+                    if w == false then
+                        if #self.party[i].command + 1 == 1 or #self.party[i].command + 1 == 4 then
+                            -- Leave command index 1 available for attack or substitutes, and
+                            -- index 4 available for item
+                            self.party[i].command[#self.party[i].command + 2] = 3
+                        else
+                            self.party[i].command[#self.party[i].command + 1] = 3
+                        end
+                    end
+                end
+                -- TODO: Sort commands/magic/summons
+            end
+        end
+    end,
+
     --- Shows the battle UI.
     --
     -- Populates and updates displayed data.
@@ -52,66 +188,45 @@ UiContainer.BattleUi = {
         self:populate()
         ui_manager:get_widget("BattleUi"):set_visible(true)
         self.active = 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()
+
+        -- Set help window transparency
+        ui_manager:get_widget("BattleUi.Container.Help.Center"):set_alpha(0.5)
+        ui_manager:get_widget("BattleUi.Container.Message.Center"):set_alpha(0.5)
+
+        -- Lopad party data
+        self:load_party_members()
+
+        -- DEBUG:
+        ui_manager:get_widget("BattleUi.Container.Bottom.Commands.Center"):set_alpha(0.5)
+        self:show_commands(1)
+
         return 0;
     end,
 
+    ---
+    -- Shows the command window for a character
+    -- @param party_id The ID of the party member whose commands to show
+    show_commands = function(self, party_id)
+        ui_manager:get_widget("BattleUi.Container.Bottom.Commands"):set_visible(true)
+        -- Resize the window
+        ui_manager:get_widget("BattleUi.Container.Bottom.Commands"):set_width(
+          40 + 40 * (1 + math.floor(#self.party[party_id].command / 4))
+        )
+        -- Show command names
+        for c = 1, 12 do
+            ui_manager:get_widget("BattleUi.Container.Bottom.Commands.Cmd" .. tostring(c)):set_visible(false)
+        end
+        for pos, cmd in ipairs(self.party[party_id].command) do
+            ui_manager:get_widget("BattleUi.Container.Bottom.Commands.Cmd" .. tostring(pos)):set_text(Game.Commands[cmd].name)
+            ui_manager:get_widget("BattleUi.Container.Bottom.Commands.Cmd" .. tostring(pos)):set_visible(true)
+        end
+
+    end,
+
     --- Hides the battle UI.
     --
-    -- It also unpauses the game.
+    -- It also sets this widget to inactive.
     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)
         self.active = false

+ 2 - 0
src/CMakeLists.txt

@@ -135,6 +135,8 @@ set(VGEARS_SOURCE_FILES
     core/Utilites.cpp
     core/Walkmesh.cpp
     core/XmlBackground2DFile.cpp
+    core/XmlBattleCharactersFile.cpp
+    core/XmlBattleScenesFile.cpp
     core/XmlEnemyFile.cpp
     core/XmlFile.cpp
     core/XmlFontFile.cpp

+ 33 - 43
src/core/BattleManager.cpp

@@ -52,6 +52,11 @@ BattleManager::BattleManager(){
     LOG_TRIVIAL("BattleManager created.");
     scene_node_ = Ogre::Root::getSingleton().getSceneManager("Scene")
       ->getRootSceneNode()->createChildSceneNode("BattleManager");
+    // Build scene model map.
+    scene_model_map_ = XmlBattleScenesFile("./data/models/battle/scenes.xml").GetScenes();
+    // Build character model map.
+    character_model_map_
+      = XmlBattleCharactersFile("./data/models/battle/characters.xml").GetCharacters();
 }
 
 BattleManager::~BattleManager(){
@@ -116,6 +121,8 @@ void BattleManager::StartBattle(const unsigned int id){
     filename = "./data/gamedata/formation/" + filename + ".xml";
     // This sets the data in the battle manager singleton.
     XmlFormationFile(filename).LoadFormation();
+
+
     // TODO: Music
     if (camera_.size() == 0)
         LOG_ERROR("Unable to start battle camera. No cameras defined in the BattleManager.");
@@ -255,11 +262,18 @@ void BattleManager::SetLocation(const int id, const Ogre::String name){
         LOG_ERROR("Tried to set a location in the BattleManager, but it's not in battle mode.");
         return;
     }
-    EntityManager::getSingleton().SetBackground3D(name, "scenes/oi_grassland.mesh");
-    /**EntityManager::getSingleton().AddBattleEntity(
-      "Background", "scenes/oi_grassland.mesh", Ogre::Vector3(0, 0, 0), Ogre::Degree(0),
-      Ogre::Vector3(SCENE_SCALE, SCENE_SCALE, SCENE_SCALE), 999, true, true
-    );*/
+    std::string model = "";
+    for (XmlBattleScenesFile::BattleScene scene : scene_model_map_){
+        if (scene.id == id){
+            model = "scenes/" + scene.model;
+            break;
+        }
+    }
+    if (model == ""){
+        LOG_ERROR("No 3D model assigned to battle location " + std::to_string(id));
+        return;
+    }
+    EntityManager::getSingleton().SetBackground3D(name, model);
 }
 
 void BattleManager::SetArenaBattle(const bool arena){
@@ -294,45 +308,21 @@ void BattleManager::LoadParty(){
     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_xiii.mesh";
-                break;
-            // TODO: Cid data not installed
-            case Character::CID:
-                name = "party_cid";
-                model = "characters/rz_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";
+
+
+        for (XmlBattleCharactersFile::BattleCharacter character : character_model_map_){
+            if (character.id == party.at(positions.at(i))){
+                model = "characters/" + character.models.at(0).model;
+                name = "party_" + character.models.at(0).name;
                 break;
-            default: continue;
+            }
+        }
+        if (model == ""){
+            LOG_ERROR(
+              "No 3D model assigned to battle character "
+              + std::to_string(party.at(positions.at(i)))
+            );
+            return;
         }
         EntityManager::getSingleton().AddEntity(
           name, model, position, Ogre::Degree(0),

+ 12 - 0
src/core/BattleManager.h

@@ -19,6 +19,8 @@
 #include "Enemy.h"
 #include "Event.h"
 #include "Manager.h"
+#include "core/XmlBattleCharactersFile.h"
+#include "core/XmlBattleScenesFile.h"
 
 /**
  * The battle manager.
@@ -425,6 +427,16 @@ class BattleManager : public Manager, public Ogre::Singleton<BattleManager>{
          */
         std::vector<Enemy> enemies_;
 
+        /**
+         * Map of playable character models.
+         */
+        std::vector<XmlBattleCharactersFile::BattleCharacter> character_model_map_;
+
+        /**
+         * Map of battle scene models.
+         */
+        std::vector<XmlBattleScenesFile::BattleScene> scene_model_map_;
+
         /**
          * List of party members in the battle.
          */

+ 10 - 3
src/core/EntityManager.cpp

@@ -37,7 +37,7 @@ template<>EntityManager *Ogre::Singleton<EntityManager>::msSingleton = nullptr;
 ConfigVar cv_debug_grid("debug_grid", "Draw debug grid", "false");
 ConfigVar cv_debug_axis("debug_axis", "Draw debug axis", "false");
 
-const float EntityManager::SCENE_SCALE = 0.0012f;
+const float EntityManager::SCENE_SCALE = 0.003f;
 
 const unsigned int EntityManager::BATTLE_BACKGROUND_ID = 1000;
 
@@ -458,9 +458,16 @@ void EntityManager::ClearBattle(){
     }
     battle_entity_.clear();
     if (background_3d_ != nullptr){
-        scene_node_->removeAndDestroyChild("bg_" + background_3d_->GetName());
-        ScriptManager::getSingleton().RemoveEntity(
+        Ogre::Root::getSingleton().getSceneManager("Scene")->destroyEntity(
+          "bg_" + background_3d_->GetName()
+        );
+        /*ScriptManager::getSingleton().RemoveEntity(
           ScriptManager::ENTITY, background_3d_->GetName()
+        );*/
+        scene_node_->getChild("bg_" + background_3d_->GetName())->removeAllChildren();
+        scene_node_->removeAndDestroyChild("bg_" + background_3d_->GetName());
+        Ogre::Root::getSingleton().getSceneManager("Scene")->destroyEntity(
+          background_3d_->GetName()
         );
         background_3d_ = nullptr;
     }

+ 57 - 0
src/core/XmlBattleCharactersFile.cpp

@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include "core/XmlBattleCharactersFile.h"
+#include "core/Logger.h"
+
+XmlBattleCharactersFile::XmlBattleCharactersFile(const Ogre::String& file): XmlFile(file){
+
+    TiXmlNode* node = file_.RootElement();
+    if (node == nullptr || node->ValueStr() != "characters"){
+        LOG_ERROR(
+          file_.ValueStr() + " is not a valid battle scenes file! No <characters> in root."
+        );
+        return;
+    }
+    node = node->FirstChild();
+    while (node != nullptr){
+        if (node->ValueStr() != "character"){
+            node = node->NextSibling();
+            continue;
+        }
+        BattleCharacter character;
+        character.id = GetInt(node, "id");
+        TiXmlNode* model_node = node->FirstChild();
+        while (model_node != nullptr){
+            if (model_node->ValueStr() != "model"){
+                model_node = model_node->NextSibling();
+                continue;
+            }
+            BattleCharacterModel model;
+            model.model = GetString(model_node, "file");
+            model.name = GetString(model_node, "name");
+            character.models.push_back(model);
+            model_node = model_node->NextSibling();
+        }
+        list_.push_back(character);
+        node = node->NextSibling();
+    }
+}
+
+XmlBattleCharactersFile::~XmlBattleCharactersFile(){}
+
+std::vector<XmlBattleCharactersFile::BattleCharacter> XmlBattleCharactersFile::GetCharacters(){
+    return list_;
+}

+ 86 - 0
src/core/XmlBattleCharactersFile.h

@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#pragma once
+
+#include "XmlFile.h"
+
+/**
+ * Handles battle character model list xml files.
+ *
+ * Those files contain a correlation between playable characters and their models.
+ */
+class XmlBattleCharactersFile : public XmlFile{
+
+    public:
+
+        /**
+         * Information about each character model.
+         */
+        struct BattleCharacterModel{
+
+            /**
+             * Model file.
+             */
+            std::string model;
+
+            /**
+             * Descriptive name of the model.
+             */
+            std::string name;
+        };
+
+        /**
+         * Relation of character and it's models
+         */
+        struct BattleCharacter{
+
+            /**
+             * Character id.
+             */
+            int id;
+
+            /**
+             * Character models.
+             */
+            std::vector<BattleCharacterModel> models;
+        };
+
+        /**
+         * Constructor.
+         *
+         * Parses the information from the file.
+         *
+         * @param[in] file Path to the scenes XML file.
+         */
+        explicit XmlBattleCharactersFile(const Ogre::String& file);
+
+        /**
+         * Destructor.
+         */
+        virtual ~XmlBattleCharactersFile();
+
+        /**
+         * Retrieves the battle character list.
+         *
+         * @return The map of battle character models.
+         */
+        std::vector<BattleCharacter> GetCharacters();
+
+    private:
+
+        std::vector<BattleCharacter> list_;
+
+};

+ 43 - 0
src/core/XmlBattleScenesFile.cpp

@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include "core/XmlBattleScenesFile.h"
+#include "core/Logger.h"
+
+XmlBattleScenesFile::XmlBattleScenesFile(const Ogre::String& file): XmlFile(file){
+
+    TiXmlNode* node = file_.RootElement();
+    if (node == nullptr || node->ValueStr() != "scenes"){
+        LOG_ERROR(file_.ValueStr() + " is not a valid battle scenes file! No <scenes> in root.");
+        return;
+    }
+    node = node->FirstChild();
+    while (node != nullptr){
+        if (node->ValueStr() != "scene"){
+            node = node->NextSibling();
+            continue;
+        }
+        BattleScene scene;
+        scene.id = GetInt(node, "id");
+        scene.model = GetString(node, "file");
+        scene.description = GetString(node, "description");
+        list_.push_back(scene);
+        node = node->NextSibling();
+    }
+}
+
+XmlBattleScenesFile::~XmlBattleScenesFile(){}
+
+std::vector<XmlBattleScenesFile::BattleScene> XmlBattleScenesFile::GetScenes(){return list_;}

+ 75 - 0
src/core/XmlBattleScenesFile.h

@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#pragma once
+
+#include "XmlFile.h"
+
+/**
+ * Handles battle scene list xml files.
+ *
+ * These files relate battle scene IDs with their models.
+ */
+class XmlBattleScenesFile : public XmlFile{
+
+    public:
+
+        /**
+         * Information about a battle scene.
+         */
+        struct BattleScene{
+
+            /**
+             * Scene id.
+             */
+            int id;
+
+            /**
+             * Scene model.
+             */
+            std::string model;
+
+            /**
+             * Scene description;
+             */
+            std::string description;
+        };
+
+        /**
+         * Constructor.
+         *
+         * Parses the information from the file.
+         *
+         * @param[in] file Path to the scenes XML file.
+         */
+        explicit XmlBattleScenesFile(const Ogre::String& file);
+
+        /**
+         * Destructor.
+         */
+        virtual ~XmlBattleScenesFile();
+
+        /**
+         * Retrieves the battle scene list.
+         *
+         * @return The map of battle scene models.
+         */
+        std::vector<BattleScene> GetScenes();
+
+    private:
+
+        std::vector<BattleScene> list_;
+
+};

+ 23 - 2
src/installer/BattleDataInstaller.cpp

@@ -245,7 +245,14 @@ unsigned int BattleDataInstaller::ProcessModel(){
                 character_data_.push_back(character_data);
             }
         }
-        else if (info.is_scene) path += "scenes/";
+        else if (info.is_scene){
+            SceneModel scene_data;
+            scene_data.id = info.numeric_id;
+            scene_data.model = info.alphanumeric_id + "_" + info.name_normal + ".mesh";
+            scene_data.description = info.name;
+            scene_data_.push_back(scene_data);
+            path += "scenes/";
+        }
         else path += "enemies/";
         path += (id + type + "_" + info.name_normal + ".png");
         tex.SavePng(path);
@@ -704,7 +711,21 @@ void BattleDataInstaller::WriteCharacterData(){
         container->LinkEndChild(xml_character.release());
     }
     xml.LinkEndChild(container.release());
-    xml.SaveFile(output_dir_ + "gamedata/characters.xml");
+    xml.SaveFile(output_dir_ + "models/battle/characters.xml");
+}
+
+void BattleDataInstaller::WriteSceneData(){
+    TiXmlDocument xml;
+    std::unique_ptr<TiXmlElement> container(new TiXmlElement("scenes"));
+    for (SceneModel data : scene_data_){
+        std::unique_ptr<TiXmlElement> xml_scene(new TiXmlElement("scene"));
+        xml_scene->SetAttribute("id", data.id);
+        xml_scene->SetAttribute("file", data.model);
+        xml_scene->SetAttribute("description", data.description);
+        container->LinkEndChild(xml_scene.release());
+    }
+    xml.LinkEndChild(container.release());
+    xml.SaveFile(output_dir_ + "models/battle/scenes.xml");
 }
 
 std::string BattleDataInstaller::BuildEnemyFileName(Enemy enemy){

+ 31 - 0
src/installer/BattleDataInstaller.h

@@ -113,6 +113,11 @@ class BattleDataInstaller{
          */
         void WriteCharacterData();
 
+        /**
+         * Writes battle scene data.
+         */
+        void WriteSceneData();
+
     private:
 
         /**
@@ -177,6 +182,27 @@ class BattleDataInstaller{
             std::string model;
         };
 
+        /**
+         * Models for battle scenes.
+         */
+        struct SceneModel{
+
+            /**
+             * Scene ID.
+             */
+            int id;
+
+            /**
+             * Model file.
+             */
+            std::string model;
+
+            /**
+             * Scene description.
+             */
+            std::string description;
+        };
+
         /**
          * Playable character model info.
          */
@@ -331,6 +357,11 @@ class BattleDataInstaller{
          */
         std::vector<CharacterData> character_data_;
 
+        /**
+         * List of models for battle scenes;
+         */
+        std::vector<SceneModel> scene_data_;
+
         /**
          * Map of enemies, ID and xml file path.
          */

+ 6 - 1
src/installer/DataInstaller.cpp

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

+ 5 - 0
src/installer/DataInstaller.h

@@ -225,6 +225,11 @@ class DataInstaller{
              */
             BATTLE_MODELS_WRITE_CHARACTERS,
 
+            /**
+             * Writtes battle scene model info.
+             */
+            BATTLE_MODELS_WRITE_SCENES,
+
             /**
              * Parses item and materia prices from ff7.exe.
              */