Browse Source

World map loads and its walkable. Some vehicles implemented.

Iñigo Valentin 1 tuần trước cách đây
mục cha
commit
c20623cea9

+ 1 - 0
data/data/scripts/_scripts.xml

@@ -21,6 +21,7 @@
     <script file="gamedata/initial_savemap.lua" />
 
     <script file="scripts/field.lua" />
+    <script file="scripts/worldmap/vehicles.lua" />
     <script file="scripts/battle_ai.lua" />
     <script file="scripts/menu/menu_helpers.lua" />
 

+ 61 - 0
data/data/scripts/worldmap/vehicles.lua

@@ -0,0 +1,61 @@
+EntityContainer = EntityContainer or {}
+
+--[[
+Boards a world-map vehicle and hides the previous player entity.
+@param entity_name Name of the vehicle entity.
+@param rise_amount Height to raise the vehicle while boarding.
+]]
+local function board_vehicle(entity_name, rise_amount)
+    local player_entity = entity_manager:get_player_entity()
+    local vehicle_entity = entity_manager:get_entity(entity_name)
+    if not player_entity or not vehicle_entity then
+        return
+    end
+
+    local player_name = player_entity:get_name()
+    local boarding_from_foot = player_name == "WorldMapCloud"
+    local chocobo_to_highwind
+        = player_name == "WorldMapChocobo" and entity_name == "WorldMapHighwind"
+    if not boarding_from_foot and not chocobo_to_highwind then
+        print("Leave your current vehicle before boarding another.")
+        return
+    end
+
+    if rise_amount ~= 0 then
+        vehicle_entity:move_by(0, rise_amount, 0)
+    end
+    player_entity:set_visible(false)
+    player_entity:set_solid(false)
+    player_entity:set_talkable(false)
+    entity_manager:set_player_entity(entity_name)
+end
+
+EntityContainer["WorldMapHighwind"] = {
+    on_interact = function(self)
+        board_vehicle("WorldMapHighwind", 0.75)
+    end,
+}
+
+EntityContainer["WorldMapBuggy"] = {
+    on_interact = function(self)
+        board_vehicle("WorldMapBuggy", 0)
+    end,
+}
+
+EntityContainer["WorldMapChocobo"] = {
+    on_interact = function(self)
+        board_vehicle("WorldMapChocobo", 0)
+    end,
+}
+
+EntityContainer["WorldMapSubmarine"] = {
+    on_interact = function(self)
+        board_vehicle("WorldMapSubmarine", 0)
+    end,
+}
+
+EntityContainer["WorldMapTinyBronco"] = {
+    on_interact = function(self)
+        print("Tiny Bronco interaction is not implemented yet.")
+    end,
+}

+ 17 - 1
src/core/Entity.cpp

@@ -94,7 +94,7 @@ Entity::Entity(const Ogre::String& name, Ogre::SceneNode* node):
     //talk_collision_->setMaterial("entity/talk_collision");
     Ogre::MaterialPtr mat_talk_collision
       = Ogre::MaterialManager::getSingleton().getByName("entity/talk_collision");
-    solid_collision_->setMaterial(mat_talk_collision);
+    talk_collision_->setMaterial(mat_talk_collision);
     talk_collision_node_ = scene_node_->createChildSceneNode();
     talk_collision_node_->setScale(talk_radius_, talk_radius_, height_);
     talk_collision_node_->attachObject(talk_collision_);
@@ -213,6 +213,11 @@ void Entity::ScriptSetPosition(const float x, const float y, const float z){
     move_triangle_id_ = -1;
 }
 
+  void Entity::ScriptMoveBy(const float x, const float y, const float z){
+    SetPosition(GetPosition() + Ogre::Vector3(x, y, z));
+    move_triangle_id_ = -1;
+  }
+
 const Ogre::Vector3 Entity::GetPosition() const{return scene_node_->getPosition();}
 
 void Entity::ScriptGetPosition() const{
@@ -280,6 +285,17 @@ void Entity::setRootOrientation(const Ogre::Quaternion &root_orientation){
 
 float Entity::GetHeight() const{return height_;}
 
+void Entity::SetHeight(const float height){
+  height_ = height;
+  solid_collision_node_->setScale(solid_radius_, solid_radius_, height_);
+  talk_collision_node_->setScale(talk_radius_, talk_radius_, height_);
+}
+
+void Entity::SetCollisionDebugOrientation(const Ogre::Quaternion& orientation){
+  solid_collision_node_->setOrientation(orientation);
+  talk_collision_node_->setOrientation(orientation);
+}
+
 void Entity::SetSolidRadius(const float radius){
     solid_radius_ = radius;
     solid_collision_node_->setScale(solid_radius_, solid_radius_, height_);

+ 23 - 0
src/core/Entity.h

@@ -224,6 +224,15 @@ class Entity{
          */
         void ScriptSetPosition(const float x, const float y, const float z);
 
+        /**
+         * Moves the entity by the specified position offset.
+         *
+         * @param[in] x X coordinate offset.
+         * @param[in] y Y coordinate offset.
+         * @param[in] z Z coordinate offset.
+         */
+        void ScriptMoveBy(const float x, const float y, const float z);
+
         /**
          * Retrieves the entity position.
          *
@@ -320,6 +329,20 @@ class Entity{
          */
         float GetHeight() const;
 
+        /**
+         * Sets the entity collision height and updates its debug proxies.
+         *
+         * @param[in] height Height in map units.
+         */
+        void SetHeight(const float height);
+
+        /**
+         * Sets the orientation of the debug collision proxies.
+         *
+         * @param[in] orientation Proxy orientation relative to the entity.
+         */
+        void SetCollisionDebugOrientation(const Ogre::Quaternion& orientation);
+
         /**
          * Sets the entity's solid radius.
          *

+ 97 - 2
src/core/EntityManager.cpp

@@ -101,6 +101,7 @@ Ogre::Degree EntityManager::GetDirectionToPoint(
 }
 
 EntityManager::EntityManager():
+    background_3d_(nullptr),
   player_entity_(nullptr),
   player_move_(Ogre::Vector3::ZERO),
   player_move_rotation_(0),
@@ -305,7 +306,12 @@ void EntityManager::UpdateBattle(){
 }
 
 void EntityManager::UpdateWorld(){
-    // TODO: Implement
+    ScriptManager::getSingleton().Update(ScriptManager::ENTITY);
+    for (Entity* entity : entity_){
+        entity->Update();
+    }
+    player_move_ = Ogre::Vector3::ZERO;
+    player_run_ = false;
 }
 
 void EntityManager::AddFieldOrWorldMapEntity(
@@ -328,6 +334,31 @@ void EntityManager::AddFieldOrWorldMapEntity(
     ScriptManager::getSingleton().AddEntity(ScriptManager::ENTITY, entity->GetName(), entity);
 }
 
+EntityModel* EntityManager::AddWorldMapEntity(
+  const Ogre::String& name, const Ogre::String& file_name,
+  const Ogre::Vector3& position, const Ogre::Degree& rotation,
+  const Ogre::Vector3& scale, const Ogre::String& resource_group
+){
+    if (!IsWorldModule()){
+        LOG_ERROR("Tried to add a world-map entity outside world-map mode.");
+        return nullptr;
+    }
+    if (GetEntity(name) != nullptr){
+        LOG_ERROR("Tried to add duplicate world-map entity '" + name + "'.");
+        return nullptr;
+    }
+
+    Ogre::SceneNode* node = scene_node_->createChildSceneNode("Model_" + name);
+    EntityModel* entity = new EntityModel(name, file_name, node, resource_group);
+    entity->SetPosition(position);
+    entity->SetRotation(rotation);
+    entity->setScale(scale);
+    entity->SetVisible(true);
+    entity_.push_back(entity);
+    ScriptManager::getSingleton().AddEntity(ScriptManager::ENTITY, entity->GetName(), entity);
+    return entity;
+}
+
 void EntityManager::AddBattleEntity(
   const Ogre::String& name, const Ogre::String& file_name, const Ogre::Vector3& position,
   const Ogre::Degree& rotation, const Ogre::Vector3& scale,
@@ -474,11 +505,54 @@ void EntityManager::ClearBattle(){
 }
 
 void EntityManager::ClearWorld(){
-    // TODO implement
+    walkmesh_.Clear();
+    for (Entity* entity : entity_){
+        ScriptManager::getSingleton().RemoveEntity(ScriptManager::ENTITY, entity->GetName());
+        delete entity;
+    }
+    entity_.clear();
+    player_entity_ = nullptr;
+    player_move_ = Ogre::Vector3::ZERO;
+    player_move_rotation_ = 0;
+    player_lock_ = false;
+    player_run_ = false;
+    for (EntityTrigger* trigger : entity_triggers_){
+        ScriptManager::getSingleton().RemoveEntity(ScriptManager::ENTITY, trigger->GetName());
+        delete trigger;
+    }
+    entity_triggers_.clear();
+    for (EntityPoint* point : entity_points_) delete point;
+    entity_points_.clear();
+    for (const Ogre::String& script : entity_scripts_){
+        ScriptManager::getSingleton().RemoveEntity(ScriptManager::ENTITY, script);
+    }
+    entity_scripts_.clear();
+    scene_node_->removeAndDestroyAllChildren();
 }
 
 Walkmesh* EntityManager::GetWalkmesh(){return &walkmesh_;}
 
+bool EntityManager::IsWorldPositionBlocked(
+    const Entity* moving_entity, const Ogre::Vector3& position
+) const{
+        if (!IsWorldModule() || moving_entity == nullptr) return false;
+
+        for (const Entity* other : entity_){
+                if (other == moving_entity || !other->IsSolid()) continue;
+
+                const Ogre::Vector3 other_position = other->GetPosition();
+                const Ogre::Real dx = position.x - other_position.x;
+                const Ogre::Real dz = position.z - other_position.z;
+                const Ogre::Real radius = moving_entity->GetSolidRadius() + other->GetSolidRadius();
+                if (dx * dx + dz * dz >= radius * radius) continue;
+
+                const Ogre::Real moving_top = position.y + moving_entity->GetHeight();
+                const Ogre::Real other_top = other_position.y + other->GetHeight();
+                if (position.y < other_top && other_position.y < moving_top) return true;
+        }
+        return false;
+}
+
 Background2D* EntityManager::GetBackground2D(){return &background_2d_;}
 
 void EntityManager::AddEntity(
@@ -1219,6 +1293,7 @@ void EntityManager::CheckEntityInteract(){
     Ogre::Degree angle_pc = player_entity_->GetRotation();
     Entity* entity_to_interact = NULL;
     Ogre::Degree less_angle(90);
+    Ogre::Real nearest_worldmap_distance = -1.0f;
     for (unsigned int i = 0; i < entity_.size(); ++ i){
         if (entity_[i]->IsTalkable() == false) continue;
         if (entity_[i] == player_entity_) continue;
@@ -1226,6 +1301,26 @@ void EntityManager::CheckEntityInteract(){
         float interact_range =  entity_[i]->GetTalkRadius();
         Ogre::Vector3 pos2 = player_entity_->GetPosition();
         float solid_range = player_entity_->GetSolidRadius();
+                if (IsWorldModule()){
+                        const Ogre::Real target_bottom = pos1.y;
+                        const Ogre::Real player_bottom = pos2.y;
+                                                const Ogre::Real talk_range
+                                                    = interact_range + solid_range + 0.05f;
+                        const Ogre::Real distance
+                            = (pos1.x - pos2.x) * (pos1.x - pos2.x)
+                                + (pos1.z - pos2.z) * (pos1.z - pos2.z);
+                        const bool height_overlap
+                            = player_bottom <= target_bottom + entity_[i]->GetHeight()
+                                && player_bottom + player_entity_->GetHeight() >= target_bottom;
+                        if (
+                                height_overlap && distance <= talk_range * talk_range
+                                && (nearest_worldmap_distance < 0 || distance < nearest_worldmap_distance)
+                        ){
+                                entity_to_interact = entity_[i];
+                                nearest_worldmap_distance = distance;
+                        }
+                        continue;
+                }
         int height = (pos1.z < pos2.z) ? entity_[i]->GetHeight() : player_entity_->GetHeight();
         if (((pos1.z - pos2.z + height) < (height * 2)) && ((pos1.z - pos2.z + height) >= 0)){
             interact_range = (interact_range + solid_range) * (interact_range + solid_range);

+ 30 - 0
src/core/EntityManager.h

@@ -24,6 +24,8 @@
 #include "Manager.h"
 #include "Walkmesh.h"
 
+class EntityModel;
+
 /**
  * The entity manager.
  */
@@ -88,6 +90,17 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          */
         Walkmesh* GetWalkmesh();
 
+        /**
+         * Checks a proposed world-map position against managed solid entities.
+         *
+         * @param[in] moving_entity Entity being moved.
+         * @param[in] position Proposed base position in world-map coordinates.
+         * @return True if the entity would overlap another solid entity.
+         */
+        bool IsWorldPositionBlocked(
+          const Entity* moving_entity, const Ogre::Vector3& position
+        ) const;
+
         /**
          * Retrieves the 2D background.
          *
@@ -130,6 +143,23 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
           const Ogre::Vector3& scale, const Ogre::Quaternion& root_orientation, const int index
         );
 
+        /**
+         * Adds a managed world-map entity using an explicit resource group.
+         *
+         * @param[in] name Entity name.
+         * @param[in] file_name Mesh resource name.
+         * @param[in] position Initial position.
+         * @param[in] rotation Initial rotation.
+         * @param[in] scale Initial scale.
+         * @param[in] resource_group Ogre resource group containing the mesh.
+         * @return The managed entity, or nullptr if the manager is not in world mode.
+         */
+        EntityModel* AddWorldMapEntity(
+          const Ogre::String& name, const Ogre::String& file_name,
+          const Ogre::Vector3& position, const Ogre::Degree& rotation,
+          const Ogre::Vector3& scale, const Ogre::String& resource_group
+        );
+
         /**
          * Adds an entity to the manager.
          *

+ 14 - 11
src/core/EntityModel.cpp

@@ -22,21 +22,24 @@
 #include "core/Timer.h"
 
 EntityModel::EntityModel(
-  const Ogre::String& name, const Ogre::String file_name, Ogre::SceneNode* node
+  const Ogre::String& name, const Ogre::String file_name, Ogre::SceneNode* node,
+  const Ogre::String& resource_group
 ): Entity(name, node), animation_current_(nullptr)
 {
     Ogre::SceneManager* scene_manager;
-    Ogre::String res_group;
+  Ogre::String res_group = resource_group;
+  if (res_group.empty()){
     switch(EntityManager::getSingleton().GetModule()){
-        case Manager::BATTLE:
-            res_group = "BATTLE_MODELS";
-            break;
-        case Manager::WORLD:
-            res_group = "WORLD_MODELS";
-            break;
-        case Manager::FIELD:
-        default:
-            res_group = "FIELD_MODELS";
+      case Manager::BATTLE:
+        res_group = "BATTLE_MODELS";
+        break;
+      case Manager::WORLD:
+        res_group = "WORLD_MODELS";
+        break;
+      case Manager::FIELD:
+      default:
+        res_group = "FIELD_MODELS";
+    }
     }
     scene_manager = Ogre::Root::getSingleton().getSceneManager("Scene");
     model_ = scene_manager->createEntity(name_, file_name, res_group);

+ 1 - 1
src/core/EntityModel.h

@@ -36,7 +36,7 @@ class EntityModel : public Entity{
          */
         EntityModel(
           const Ogre::String& name, const Ogre::String file_name,
-          Ogre::SceneNode* node
+          Ogre::SceneNode* node, const Ogre::String& resource_group = ""
         );
         virtual ~EntityModel();
 

+ 2 - 0
src/core/ScriptManager.cpp

@@ -630,6 +630,8 @@ bool ScriptManager::ScriptRequest(
   ScriptEntity* script_entity, const Ogre::String& function, const int priority,
   const Ogre::String& argument1, const Ogre::String& argument2, bool start_sync, bool end_sync
 ){
+    if (script_entity == nullptr) return false;
+
     sol::object table
       = GetTableByEntityName(script_entity->type, script_entity->name, lua_state_);
     sol::object table_function;

+ 2 - 0
src/core/ScriptManagerBinds.h

@@ -110,7 +110,9 @@ void ScriptManager::InitBinds(){
     lua.new_usertype<Entity>(
       "Entity",
       "set_position", &Entity::ScriptSetPosition,
+      "move_by", &Entity::ScriptMoveBy,
       "get_position", &Entity::ScriptGetPosition,
+      "get_name", &Entity::GetName,
       "set_rotation", &Entity::ScriptSetRotation,
       "get_rotation", &Entity::ScriptGetRotation,
       "set_solid_radius", &Entity::SetSolidRadius,

+ 3 - 0
src/main.cpp

@@ -104,6 +104,9 @@ int main(int argc, char *argv[]){
         Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
           "./data/models/fields/", "FileSystem", "FIELD_MODELS"
         );
+        Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
+          "./data/models/world/", "FileSystem", "WORLD_MODELS", true
+        );
         Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
           "./data/models/battle/", "FileSystem", "BATTLE_MODELS", true
         );

+ 423 - 23
src/modules/worldmap/WorldmapModule.cpp

@@ -2,6 +2,13 @@
  * Copyright (C) 2022 The V-Gears Team
  *
  * This file is part of V-Gears
+  static bool IsJunonTexture(uint16 texture_id){
+    const size_t texture_count = sizeof(textures) / sizeof(textures[0]);
+    if (texture_id >= texture_count) return false;
+    const std::string texture_name = textures[texture_id].name;
+    return texture_name.compare(0, 3, "jun") == 0
+      || texture_name == "jutmpl01";
+  }
  *
  * 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
@@ -18,9 +25,11 @@
 #include <algorithm>
 #include <map>
 #include <memory>
+#include <limits>
 #include <set>
 #include <string>
 #include <vector>
+#include <OgreEntity.h>
 #include <OgreSceneManager.h>
 #include <OgreManualObject.h>
 #include <OgreRoot.h>
@@ -31,6 +40,9 @@
 #include <OgreMaterialManager.h>
 #include <OgreResourceGroupManager.h>
 #include "data/VGearsTexFile.h"
+#include "core/InputManager.h"
+#include "core/EntityManager.h"
+#include "core/EntityModel.h"
 #include "data/worldmap/WorldmapFile.h"
 #include "data/worldmap/WorldmapFileManager.h"
 #include "data/worldmap/WorldmapFileSerializer.h"
@@ -371,6 +383,35 @@ namespace VGears {
 
     static const char* WORLD_MAP_RESOURCE_GROUP = "WORLD_MAP";
 
+    static bool IsFootWalkable(uint8 walkability_type){
+      using WalkType = VGears::WorldmapFileSerializer::TriangleWalkMapTypes;
+      switch (walkability_type){
+        case WalkType::GRASS:
+        case WalkType::FOREST:
+        case WalkType::SWAMP:
+        case WalkType::DESERT:
+        case WalkType::WASTELAND:
+        case WalkType::SNOW:
+        case WalkType::RIVERSIDE:
+        case WalkType::CLIFF:
+        case WalkType::COREL_BRIDGE:
+        case WalkType::WUTAI_BRIDGE:
+        case WalkType::HILL_SIDE:
+        case WalkType::BEACH:
+        case WalkType::CANYON:
+        case WalkType::MOUNTAIN_PASS:
+        case WalkType::GOLD_SAUCER_DESERT:
+        case WalkType::JUNGLE:
+        case WalkType::NORTHERND_CAVE:
+        case WalkType::GOLD_SAUCER_DESERT_BORDER:
+        case WalkType::BRIDGE_HEAD:
+        case WalkType::BACK_ENTRANCE:
+          return true;
+        default:
+          return false;
+      }
+    }
+
     /**
      * Loads and parses the installed WM0 map.
      *
@@ -491,12 +532,30 @@ namespace VGears {
         return material;
     }
 
-    /**
-     * Creates a test map.
-     *
-     * @param[in] scene_manager The scene manager.
-     */
-    void CreateWorldMap(SceneManager* scene_manager){
+    WorldmapModule::WorldmapModule():
+      scene_manager_(nullptr),
+      world_map_node_(nullptr),
+      player_entity_(nullptr),
+      highwind_entity_(nullptr),
+      buggy_entity_(nullptr),
+      chocobo_entity_(nullptr),
+      submarine_entity_(nullptr),
+      current_vehicle_(VehicleType::ON_FOOT),
+      player_height_offset_(0),
+      initialized_(false)
+    {}
+
+    WorldmapModule::~WorldmapModule(){
+      if (initialized_) Root::getSingleton().removeFrameListener(this);
+      if (scene_manager_ != nullptr && world_map_node_ != nullptr){
+        scene_manager_->getRootSceneNode()->removeAndDestroyChild("WorldMapTerrain");
+      }
+    }
+
+    void WorldmapModule::CreateWorldMap(SceneManager* scene_manager){
+      scene_manager_ = scene_manager;
+      world_map_node_ = scene_manager_->getRootSceneNode()
+        ->createChildSceneNode("WorldMapTerrain");
         const std::vector<VGears::WorldmapFileSerializer::Block> blocks
           = LoadWorldMap();
         const size_t texture_count = sizeof(textures) / sizeof(textures[0]);
@@ -520,6 +579,31 @@ namespace VGears {
 
                 for (const VGears::WorldmapFileSerializer::BlockTriangle& triangle
                   : part.triangles){
+                    const VGears::WorldmapFileSerializer::Vertex& vertex_0
+                      = part.vertices.at(triangle.vertex_0_index);
+                    const VGears::WorldmapFileSerializer::Vertex& vertex_1
+                      = part.vertices.at(triangle.vertex_1_index);
+                    const VGears::WorldmapFileSerializer::Vertex& vertex_2
+                      = part.vertices.at(triangle.vertex_2_index);
+                    TerrainTriangle terrain_triangle;
+                    terrain_triangle.vertices[0] = Ogre::Vector3(
+                      From16BitFixedPoint(vertex_0.x) + pos_x,
+                      From16BitFixedPoint(vertex_0.y),
+                      From16BitFixedPoint(vertex_0.z) + pos_z
+                    );
+                    terrain_triangle.vertices[1] = Ogre::Vector3(
+                      From16BitFixedPoint(vertex_1.x) + pos_x,
+                      From16BitFixedPoint(vertex_1.y),
+                      From16BitFixedPoint(vertex_1.z) + pos_z
+                    );
+                    terrain_triangle.vertices[2] = Ogre::Vector3(
+                      From16BitFixedPoint(vertex_2.x) + pos_x,
+                      From16BitFixedPoint(vertex_2.y),
+                      From16BitFixedPoint(vertex_2.z) + pos_z
+                    );
+                    terrain_triangle.walkability_type = triangle.walkability_info_;
+                    terrain_triangles_.push_back(terrain_triangle);
+
                     if (unavailable_texture_ids.find(triangle.TextureInfo)
                       != unavailable_texture_ids.end()){
                         continue;
@@ -562,36 +646,30 @@ namespace VGears {
                     }
                     else manual = object->second;
 
-                    const VGears::WorldmapFileSerializer::Vertex& vertex_2
-                      = part.vertices.at(triangle.vertex_2_index);
                     manual->position(
-                      From16BitFixedPoint(vertex_2.x) + pos_x,
-                      From16BitFixedPoint(vertex_2.y),
-                      From16BitFixedPoint(vertex_2.z) + pos_z
+                      terrain_triangle.vertices[2].x,
+                      terrain_triangle.vertices[2].y,
+                      terrain_triangle.vertices[2].z
                     );
                     manual->textureCoord(
                       ToTextureCoordU(triangle.vertex_2_u, triangle.TextureInfo),
                       ToTextureCoordV(triangle.vertex_2_v, triangle.TextureInfo)
                     );
 
-                    const VGears::WorldmapFileSerializer::Vertex& vertex_1
-                      = part.vertices.at(triangle.vertex_1_index);
                     manual->position(
-                      From16BitFixedPoint(vertex_1.x) + pos_x,
-                      From16BitFixedPoint(vertex_1.y),
-                      From16BitFixedPoint(vertex_1.z) + pos_z
+                      terrain_triangle.vertices[1].x,
+                      terrain_triangle.vertices[1].y,
+                      terrain_triangle.vertices[1].z
                     );
                     manual->textureCoord(
                       ToTextureCoordU(triangle.vertex_1_u, triangle.TextureInfo),
                       ToTextureCoordV(triangle.vertex_1_v, triangle.TextureInfo)
                     );
 
-                    const VGears::WorldmapFileSerializer::Vertex& vertex_0
-                      = part.vertices.at(triangle.vertex_0_index);
                     manual->position(
-                      From16BitFixedPoint(vertex_0.x) + pos_x,
-                      From16BitFixedPoint(vertex_0.y),
-                      From16BitFixedPoint(vertex_0.z) + pos_z
+                      terrain_triangle.vertices[0].x,
+                      terrain_triangle.vertices[0].y,
+                      terrain_triangle.vertices[0].z
                     );
                     manual->textureCoord(
                       ToTextureCoordU(triangle.vertex_0_u, triangle.TextureInfo),
@@ -603,8 +681,7 @@ namespace VGears {
 
         for (const auto& texture_object : texture_objects){
             texture_object.second->end();
-            scene_manager->getRootSceneNode()->createChildSceneNode()
-              ->attachObject(texture_object.second);
+            world_map_node_->attachObject(texture_object.second);
         }
 
         const float map_center_x = space * 18.0f;
@@ -614,8 +691,331 @@ namespace VGears {
         camera->lookAt(Ogre::Vector3(map_center_x, 0.0f, map_center_z));
     }
 
+    void WorldmapModule::CreatePlayer(){
+        const Ogre::Vector3 requested_spawn(42.0f, 0.0f, 36.5f);
+        Ogre::Vector3 spawn_position;
+        if (!FindWalkableSurface(requested_spawn, spawn_position, true)){
+            LogManager::getSingleton().logMessage(
+              "WM0 has no GRASS surface at Cloud's requested spawn (42, 36.5)."
+            );
+            return;
+        }
+
+        LogManager::getSingleton().logMessage(
+          "Spawning Cloud at "
+            + Ogre::StringConverter::toString(spawn_position.x) + ", "
+            + Ogre::StringConverter::toString(spawn_position.z)
+        );
+
+        player_entity_ = EntityManager::getSingleton().AddWorldMapEntity(
+          "WorldMapCloud", "cloud.mesh", spawn_position, Ogre::Degree(0),
+          Ogre::Vector3::UNIT_SCALE, "FIELD_MODELS"
+        );
+        if (player_entity_ == nullptr) return;
+
+        const Ogre::AxisAlignedBox& bounds = player_entity_->GetModel()->getBoundingBox();
+        const Ogre::Vector3 model_size = bounds.getSize();
+        const Ogre::Real model_height = model_size.y;
+        if (model_height > 0){
+            const Ogre::Real scale = From16BitFixedPoint(8182) * 0.45f * 0.25f * 0.25f
+              / model_height;
+            player_entity_->setScale(Ogre::Vector3::UNIT_SCALE * scale);
+            player_entity_->SetHeight(model_height * scale);
+            player_entity_->SetSolidRadius(
+              std::max(model_size.x, model_size.z) * scale * 0.5f
+            );
+            player_entity_->SetCollisionDebugOrientation(
+              Ogre::Quaternion(Ogre::Degree(-90), Ogre::Vector3::UNIT_X)
+            );
+            player_height_offset_ = -bounds.getMinimum().y * scale;
+            spawn_position.y += player_height_offset_;
+            player_entity_->SetPosition(spawn_position);
+            Ogre::AnimationStateSet* animations
+              = player_entity_->GetModel()->getAllAnimationStates();
+            if (animations == nullptr || !animations->hasAnimationState("Walk")){
+                LogManager::getSingleton().logMessage(
+                  "Cloud model has no Walk animation state."
+                );
+            }
+            EntityManager::getSingleton().ScriptSetPlayerEntity("WorldMapCloud");
+            player_entity_->PlayAnimationContinue("Idle");
+            CreateCompanionModels();
+            UpdateCamera(0, true);
+        }
+    }
+
+    void WorldmapModule::AddWorldModel(
+      const Ogre::String& name, const Ogre::String& mesh_name,
+      const Ogre::Vector3& desired_position, Ogre::Real target_size,
+      const Ogre::Quaternion& orientation, Ogre::Real collision_radius_scale,
+      bool talkable
+    ){
+        Ogre::Vector3 surface_position;
+        if (!FindWalkableSurface(desired_position, surface_position, false, false)){
+            LogManager::getSingleton().logMessage(
+              "No terrain surface found for world-map model '" + name + "'."
+            );
+            return;
+        }
+
+        EntityModel* entity = nullptr;
+        try{
+            entity = EntityManager::getSingleton().AddWorldMapEntity(
+              "WorldMap" + name, mesh_name, surface_position,
+              Ogre::Degree(0), Ogre::Vector3::UNIT_SCALE, "WORLD_MODELS"
+            );
+        }
+        catch (const Ogre::Exception& exception){
+            LogManager::getSingleton().logMessage(
+              "Failed to load world-map model '" + mesh_name + "': "
+                + exception.getDescription()
+            );
+            return;
+        }
+        if (entity == nullptr) return;
+
+        const Ogre::Vector3 bounds_size = entity->GetModel()->getBoundingBox().getSize();
+        const Ogre::Real largest_dimension = std::max(
+          bounds_size.x, std::max(bounds_size.y, bounds_size.z)
+        );
+        if (largest_dimension <= 0){
+            LogManager::getSingleton().logMessage(
+              "World-map model '" + mesh_name + "' has empty bounds."
+            );
+            return;
+        }
+
+        const Ogre::Real model_scale = target_size / largest_dimension;
+        const Ogre::Vector3 oriented_x = orientation * Ogre::Vector3::UNIT_X;
+        const Ogre::Vector3 oriented_y = orientation * Ogre::Vector3::UNIT_Y;
+        const Ogre::Vector3 oriented_z = orientation * Ogre::Vector3::UNIT_Z;
+        const Ogre::Real world_width =
+          Ogre::Math::Abs(oriented_x.x) * bounds_size.x
+          + Ogre::Math::Abs(oriented_y.x) * bounds_size.y
+          + Ogre::Math::Abs(oriented_z.x) * bounds_size.z;
+        const Ogre::Real world_height =
+          Ogre::Math::Abs(oriented_x.y) * bounds_size.x
+          + Ogre::Math::Abs(oriented_y.y) * bounds_size.y
+          + Ogre::Math::Abs(oriented_z.y) * bounds_size.z;
+        const Ogre::Real world_depth =
+          Ogre::Math::Abs(oriented_x.z) * bounds_size.x
+          + Ogre::Math::Abs(oriented_y.z) * bounds_size.y
+          + Ogre::Math::Abs(oriented_z.z) * bounds_size.z;
+        entity->setRootOrientation(orientation);
+        entity->setScale(Ogre::Vector3::UNIT_SCALE * model_scale);
+        entity->SetSolid(true);
+        entity->SetHeight(world_height * model_scale);
+        entity->SetSolidRadius(
+          std::max(world_width, world_depth) * model_scale * 0.5f
+            * collision_radius_scale
+        );
+        entity->SetTalkable(talkable);
+        if (talkable) entity->SetTalkRadius(entity->GetSolidRadius());
+        entity->SetCollisionDebugOrientation(
+          Ogre::Quaternion(Ogre::Degree(-90), Ogre::Vector3::UNIT_X)
+        );
+        LogManager::getSingleton().logMessage(
+          "Placed world-map model '" + name + "' at "
+            + Ogre::StringConverter::toString(surface_position.x) + ", "
+            + Ogre::StringConverter::toString(surface_position.z)
+        );
+    }
+
+    void WorldmapModule::CreateCompanionModels(){
+        // These models and their spawn points are now hardcoded, they mmust be
+        // loaded according to the story point and save game data.
+        const Ogre::Real space = From16BitFixedPoint(8182);
+        AddWorldModel(
+          "Chocobo", "aja.mesh",
+          Ogre::Vector3(42.5f, 0.0f, 37.5f),
+          space * 0.14f,
+          Ogre::Quaternion(Ogre::Degree(-90), Ogre::Vector3::UNIT_X), 1.0f, true
+        );
+        AddWorldModel(
+          "Highwind", "cgd.mesh",
+          Ogre::Vector3(43.0f, 0.0f, 36.5f),
+          space * 0.25f,
+          Ogre::Quaternion(Ogre::Degree(-90), Ogre::Vector3::UNIT_X), 0.5f, true
+        );
+        AddWorldModel(
+          "Buggy", "aba.mesh",
+          Ogre::Vector3(42.0f, 0.0f, 36.0f),
+          space * 0.3f, Ogre::Quaternion::IDENTITY, 0.5f, true
+        );
+        AddWorldModel(
+          "Submarine", "ddd.mesh",
+          Ogre::Vector3(41.5f, 0.0f, 36.5f),
+          space * 0.1125f, Ogre::Quaternion::IDENTITY, 1.0f, true
+        );
+        AddWorldModel(
+          "TinyBronco", "dva.mesh",
+          Ogre::Vector3(42.0f, 0.0f, 37.37f),
+          space * 0.15f, Ogre::Quaternion::IDENTITY, 0.75f, true
+        );
+        highwind_entity_ = static_cast<EntityModel*>(
+          EntityManager::getSingleton().GetEntity("WorldMapHighwind")
+        );
+        buggy_entity_ = static_cast<EntityModel*>(
+          EntityManager::getSingleton().GetEntity("WorldMapBuggy")
+        );
+        chocobo_entity_ = static_cast<EntityModel*>(
+          EntityManager::getSingleton().GetEntity("WorldMapChocobo")
+        );
+        submarine_entity_ = static_cast<EntityModel*>(
+          EntityManager::getSingleton().GetEntity("WorldMapSubmarine")
+        );
+    }
+
+    bool WorldmapModule::FindWalkableSurface(
+      const Ogre::Vector3& position, Ogre::Vector3& surface, bool grass_only,
+      bool require_foot_walkable
+    ) const{
+        bool found = false;
+        Ogre::Real closest_height_distance = std::numeric_limits<Ogre::Real>::max();
+        for (const TerrainTriangle& triangle : terrain_triangles_){
+            if (grass_only
+              && triangle.walkability_type != VGears::WorldmapFileSerializer::GRASS){
+                continue;
+            }
+            const Ogre::Vector3& a = triangle.vertices[0];
+            const Ogre::Vector3& b = triangle.vertices[1];
+            const Ogre::Vector3& c = triangle.vertices[2];
+            const Ogre::Real denominator
+              = (b.z - c.z) * (a.x - c.x) + (c.x - b.x) * (a.z - c.z);
+            if (Ogre::Math::Abs(denominator) < 1e-6f) continue;
+
+            const Ogre::Real weight_a
+              = ((b.z - c.z) * (position.x - c.x)
+                + (c.x - b.x) * (position.z - c.z)) / denominator;
+            const Ogre::Real weight_b
+              = ((c.z - a.z) * (position.x - c.x)
+                + (a.x - c.x) * (position.z - c.z)) / denominator;
+            const Ogre::Real weight_c = 1.0f - weight_a - weight_b;
+            if (weight_a < -1e-4f || weight_b < -1e-4f || weight_c < -1e-4f){
+                continue;
+            }
+
+            const Ogre::Real height
+              = weight_a * a.y + weight_b * b.y + weight_c * c.y;
+            const Ogre::Real height_distance = Ogre::Math::Abs(height - position.y);
+            if (height_distance >= closest_height_distance) continue;
+            closest_height_distance = height_distance;
+            found = !require_foot_walkable
+              || IsFootWalkable(triangle.walkability_type);
+            surface = Ogre::Vector3(position.x, height, position.z);
+        }
+        return found;
+    }
+
+    WorldmapModule::VehicleType WorldmapModule::GetCurrentVehicle() const{
+      return current_vehicle_;
+    }
+
+    bool WorldmapModule::frameStarted(const Ogre::FrameEvent& event){
+      if (!initialized_ || player_entity_ == nullptr) return true;
+
+        ::Entity* active_entity = EntityManager::getSingleton().ScriptGetPlayerEntity();
+      if (active_entity == player_entity_){
+        current_vehicle_ = VehicleType::ON_FOOT;
+      }
+      else if (active_entity == chocobo_entity_){
+        current_vehicle_ = VehicleType::CHOCOBO;
+      }
+      else if (active_entity == buggy_entity_){
+        current_vehicle_ = VehicleType::BUGGY;
+      }
+      else if (active_entity == highwind_entity_){
+        current_vehicle_ = VehicleType::HIGHWIND;
+      }
+      else if (active_entity == submarine_entity_){
+        current_vehicle_ = VehicleType::SUBMARINE;
+      }
+      else{
+        return true;
+      }
+
+        const bool highwind_active = active_entity == highwind_entity_;
+      const bool vehicle_active = current_vehicle_ != VehicleType::ON_FOOT;
+      EntityModel* active_model = active_entity == player_entity_ ? player_entity_
+        : (current_vehicle_ == VehicleType::CHOCOBO ? chocobo_entity_
+        : (current_vehicle_ == VehicleType::BUGGY ? buggy_entity_
+          : (highwind_active ? highwind_entity_ : submarine_entity_)));
+
+        Ogre::Vector3 movement = Ogre::Vector3::ZERO;
+        if (InputManager::getSingleton().IsButtonPressed(OIS::KC_LEFT)) movement.x -= 1.0f;
+        if (InputManager::getSingleton().IsButtonPressed(OIS::KC_RIGHT)) movement.x += 1.0f;
+        if (InputManager::getSingleton().IsButtonPressed(OIS::KC_UP)) movement.z -= 1.0f;
+        if (InputManager::getSingleton().IsButtonPressed(OIS::KC_DOWN)) movement.z += 1.0f;
+        bool is_walking = false;
+        if (!movement.isZeroLength()){
+          movement.normalise();
+            const Ogre::Vector3 current_position = active_entity->GetPosition();
+          const Ogre::Real speed = From16BitFixedPoint(8182) * 0.45f
+            * (vehicle_active ? 2.0f : 1.0f);
+          Ogre::Vector3 requested_position = current_position
+            + movement * speed * event.timeSinceLastFrame;
+          Ogre::Vector3 surface;
+          if (FindWalkableSurface(requested_position, surface, false, !highwind_active)){
+                if (highwind_active){
+                    Ogre::Vector3 current_surface;
+                    if (FindWalkableSurface(current_position, current_surface, false, false)){
+                        surface.y += current_position.y - current_surface.y;
+                    }
+                }
+                else if (active_entity == player_entity_){
+                  surface.y += player_height_offset_;
+                }
+                if (!EntityManager::getSingleton().IsWorldPositionBlocked(
+                  active_entity, surface
+                )){
+                    active_entity->SetPosition(surface);
+                    const Ogre::Vector3 model_forward(0.0f, 0.0f, 1.0f);
+                    const Ogre::Vector3 move_direction(movement.x, 0.0f, movement.z);
+                    const Ogre::Quaternion heading
+                      = model_forward.getRotationTo(move_direction, Ogre::Vector3::UNIT_Y);
+                    active_model->GetModel()->getParentSceneNode()
+                      ->getParentSceneNode()->setOrientation(heading);
+                    is_walking = true;
+                }
+          }
+        }
+
+        if (!vehicle_active){
+            player_entity_->PlayAnimationContinue(is_walking ? "Walk" : "Idle");
+        }
+
+        UpdateCamera(event.timeSinceLastFrame, false);
+        return true;
+    }
+
+      void WorldmapModule::UpdateCamera(Ogre::Real delta_time, bool snap_to_player){
+        ::Entity* active_entity = EntityManager::getSingleton().ScriptGetPlayerEntity();
+        if (active_entity == nullptr) return;
+        const Ogre::Vector3 target
+          = active_entity->GetPosition() + Ogre::Vector3(0, active_entity->GetHeight() * 0.5f, 0);
+        const Ogre::Vector3 camera_offset(0.0f, 1.5f, 2.25f);
+        Ogre::Camera* camera = CameraManager::getSingleton().GetCurrentCamera();
+        const Ogre::Vector3 desired_position = target + camera_offset;
+        if (snap_to_player){
+          camera->setPosition(desired_position);
+        }
+        else{
+          const Ogre::Real follow_amount = std::min(1.0f, delta_time * 8.0f);
+          camera->setPosition(
+            camera->getPosition()
+            + (desired_position - camera->getPosition()) * follow_amount
+          );
+        }
+        camera->lookAt(target);
+      }
+
     void WorldmapModule::Init(){
+        if (initialized_) return;
+      EntityManager::getSingleton().SetWorldModule(); // Ensure the world module is set
         CreateWorldMap(Root::getSingleton().getSceneManager("Scene"));
+        CreatePlayer();
+        initialized_ = true;
+        Root::getSingleton().addFrameListener(this);
     }
 
 } // namespace VGears

+ 58 - 1
src/modules/worldmap/WorldmapModule.h

@@ -15,22 +15,79 @@
 
 #pragma once
 
+#include <OgreFrameListener.h>
 #include <OgreSingleton.h>
+#include <OgreVector3.h>
+#include <vector>
 #include "common/TypeDefine.h"
 
+class EntityModel;
+
 namespace VGears {
 
     /**
      * The world map module.
      */
-    class WorldmapModule : public Ogre::Singleton<WorldmapModule>{
+    class WorldmapModule : public Ogre::Singleton<WorldmapModule>, public Ogre::FrameListener{
 
         public:
 
+            enum class VehicleType{
+                ON_FOOT,
+                CHOCOBO,
+                BUGGY,
+                HIGHWIND,
+                SUBMARINE,
+                TINY_BRONCO
+            };
+
+            WorldmapModule();
+
+            virtual ~WorldmapModule();
+
             /**
              * Initializes the world map module
              */
             void Init();
+
+            bool frameStarted(const Ogre::FrameEvent& event) override;
+
+            VehicleType GetCurrentVehicle() const;
+
+        private:
+
+            struct TerrainTriangle{
+                Ogre::Vector3 vertices[3];
+                uint8 walkability_type;
+            };
+
+            void CreateWorldMap(Ogre::SceneManager* scene_manager);
+            void CreatePlayer();
+                        void CreateCompanionModels();
+                        void AddWorldModel(
+                            const Ogre::String& name, const Ogre::String& mesh_name,
+                            const Ogre::Vector3& desired_position, Ogre::Real target_size,
+                            const Ogre::Quaternion& orientation,
+                            Ogre::Real collision_radius_scale = 1.0f,
+                            bool talkable = false
+                        );
+            void UpdateCamera(Ogre::Real delta_time, bool snap_to_player);
+            bool FindWalkableSurface(
+                            const Ogre::Vector3& position, Ogre::Vector3& surface,
+                            bool grass_only = false, bool require_foot_walkable = true
+            ) const;
+
+            std::vector<TerrainTriangle> terrain_triangles_;
+            Ogre::SceneManager* scene_manager_;
+            Ogre::SceneNode* world_map_node_;
+            EntityModel* player_entity_;
+            EntityModel* highwind_entity_;
+            EntityModel* buggy_entity_;
+            EntityModel* chocobo_entity_;
+            EntityModel* submarine_entity_;
+            VehicleType current_vehicle_;
+            Ogre::Real player_height_offset_;
+            bool initialized_;
     };
 
 }