From 2cae9998a087f62b8f3ed65ae5dbd1e8d083d465 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Tue, 12 May 2026 21:19:29 +0300 Subject: [PATCH] Major refactoring for game objects --- resources/config2/gameobjects_dorm.json | 77 +-- .../config2/gameobjects_uni_interior.json | 158 +---- .../config2/interactive_objects_dorm.json | 32 + .../interactive_objects_uni_interior.json | 59 ++ resources/config2/items.json | 28 + resources/start_dorm.lua | 2 + resources/start_uni_interior.lua | 27 +- src/Character.h | 4 - src/Game.cpp | 6 + src/Location.cpp | 16 +- src/Location.h | 1 + src/ScriptEngine.cpp | 87 +-- src/items/GameObjectLoader.cpp | 588 ++++++------------ src/items/GameObjectLoader.h | 32 +- src/items/InteractiveObject.cpp | 20 +- src/items/InteractiveObject.h | 35 +- src/items/ItemRegistry.cpp | 68 ++ src/items/ItemRegistry.h | 23 + 18 files changed, 547 insertions(+), 716 deletions(-) create mode 100644 resources/config2/interactive_objects_dorm.json create mode 100644 resources/config2/interactive_objects_uni_interior.json create mode 100644 resources/config2/items.json create mode 100644 src/items/ItemRegistry.cpp create mode 100644 src/items/ItemRegistry.h diff --git a/resources/config2/gameobjects_dorm.json b/resources/config2/gameobjects_dorm.json index ca718e7..417ab74 100644 --- a/resources/config2/gameobjects_dorm.json +++ b/resources/config2/gameobjects_dorm.json @@ -10,10 +10,9 @@ "positionX": 0.0, "positionY": -0.1, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "Building", "texturePath": "resources/w/dorm/building_base017.png", "meshPath": "resources/w/dorm/dorm_building001.txt", @@ -23,10 +22,9 @@ "positionX": 0.0, "positionY": -0.1, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "Stairs", "texturePath": "resources/w/dorm/Staircase_Obj002sm.png", "meshPath": "resources/w/dorm/dorm_stairs001.txt", @@ -36,10 +34,9 @@ "positionX": 0.0, "positionY": -0.1, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "Door", "texturePath": "resources/w/dorm/Entrance003.png", "meshPath": "resources/w/dorm/dorm_entrance001.txt", @@ -49,10 +46,9 @@ "positionX": 0.0, "positionY": -0.1, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "Bed001", "texturePath": "resources/w/dorm/ikea_bed_bake002.png", "meshPath": "resources/w/dorm/ikea_bed001.txt", @@ -62,10 +58,9 @@ "positionX": 0.0, "positionY": -0.1, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "Chair001", "texturePath": "resources/w/dorm/ikea_chair_bake002.png", "meshPath": "resources/w/dorm/ikea_chair001.txt", @@ -75,10 +70,9 @@ "positionX": 0.0, "positionY": -0.1, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "Table001", "texturePath": "resources/w/dorm/ikea_table_bake003.png", "meshPath": "resources/w/dorm/ikea_table001.txt", @@ -88,50 +82,7 @@ "positionX": 0.0, "positionY": -0.1, "positionZ": 0.0, - "scale": 1.0, - "interactive": false - }, - { - "name": "Phone001", - "texturePath": "resources/w/dorm/phone001_tex001.png", - "meshPath": "resources/w/dorm/phone001.txt", - "rotationX": 0.0, - "rotationY": 0.0, - "rotationZ": 0.0, - "positionX": 5.8729, - "positionY": 1.04917, - "positionZ": -12.5262, - "scale": 1.0, - "interactive": true, - "item": { - "id": "phone", - "name": "Телефон", - "description": "Я не могу себе представить жизнь без своего телефона", - "icon": "resources/fire2.png", - "radius": 0.3 - }, - "activateFunction" : "on_phone_pickup" - }, - { - "name": "Journal001", - "texturePath": "resources/w/dorm/journal001_tex002.png", - "meshPath": "resources/w/dorm/journal001.txt", - "rotationX": 0.0, - "rotationY": 0.0, - "rotationZ": 0.0, - "positionX": 5.05146, - "positionY": 1.06711, - "positionZ": -12.4661, - "scale": 1.0, - "interactive": true, - "item": { - "id": "journal", - "name": "Журнал", - "description": "Это мой журнал куда я вношу свои заметки.", - "icon": "resources/fire2.png", - "radius": 0.3 - }, - "activateFunction" : "on_journal_pickup" + "scale": 1.0 } ] - } \ No newline at end of file + } diff --git a/resources/config2/gameobjects_uni_interior.json b/resources/config2/gameobjects_uni_interior.json index 03f2ecd..f5eca5f 100644 --- a/resources/config2/gameobjects_uni_interior.json +++ b/resources/config2/gameobjects_uni_interior.json @@ -10,8 +10,7 @@ "positionX": 0.0, "positionY": -5.0, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, { "name": "door", @@ -23,8 +22,7 @@ "positionX": 0.0, "positionY": -5.0, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, { "name": "inai", @@ -36,10 +34,9 @@ "positionX": 0.0, "positionY": -5.0, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "Table001", "texturePath": "resources/w/interior/ikea_table_bake003.png", "meshPath": "resources/w/interior/ikea_table001.txt", @@ -49,10 +46,9 @@ "positionX": 0.0, "positionY": -5.0, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "Chair001", "texturePath": "resources/w/interior/ikea_chair_bake002.png", "meshPath": "resources/w/interior/ikea_chair001.txt", @@ -62,45 +58,9 @@ "positionX": 0.0, "positionY": -5.0, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { - "name": "Bookshelf001", - "texturePath": "resources/w/interior/ikea_bookshelf001_tex001.png", - "meshPath": "resources/w/interior/ikea_bookshelf001.txt", - "rotationX": 0.0, - "rotationY": 0.0, - "rotationZ": 0.0, - "positionX": 1.9653, - "positionY": 1.0911, - "positionZ": 0.91977, - "scale": 1.0, - "interactive": true, - "item": { - "id": "phone", - "name": "Телефон", - "description": "Я не могу себе представить жизнь без своего телефона", - "icon": "resources/fire2.png", - "radius": 0.3 - }, - "activateFunction" : "on_bookshelf_clicked" - }, - { - "name": "Computer001", - "texturePath": "resources/w/interior/computer_texture001.png", - "meshPath": "resources/w/interior/computer001.txt", - "rotationX": 0.0, - "rotationY": 0.0, - "rotationZ": 0.0, - "positionX": 5.0916, - "positionY": 1.1534, - "positionZ": 1.0568, - "scale": 1.0, - "interactive": true, - "activateFunction" : "on_computer_clicked" - }, - { + { "name": "DiningTable001", "texturePath": "resources/w/interior/dining_table_texture.png", "meshPath": "resources/w/interior/ikea_dining_table001.txt", @@ -110,52 +70,9 @@ "positionX": 0.0, "positionY": -5.0, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { - "name": "Knife001", - "texturePath": "resources/w/white.png", - "meshPath": "resources/w/interior/Knife.txt", - "rotationX": 0.0, - "rotationY": 0.0, - "rotationZ": 0.0, - "positionX": -2.26293, - "positionY": 0.91414, - "positionZ": 1.56758, - "scale": 1.0, - "interactive": true, - "item": { - "id": "knife", - "name": "Серебряный нож", - "description": "Этот серебряный нож я одолжил у Айпери, и я должен его вернуть.", - "icon": "resources/fire2.png", - "radius": 0.3 - }, - "activateFunction" : "on_knife_pickup" - }, - { - "name": "Book001", - "texturePath": "resources/w/interior/book_tex002.png", - "meshPath": "resources/w/interior/book001.txt", - "rotationX": 0.0, - "rotationY": 0.0, - "rotationZ": 0.0, - "positionX": 1.97952, - "positionY": 0.95746, - "positionZ": 0.786023, - "scale": 1.0, - "interactive": true, - "item": { - "id": "phone", - "name": "Книга", - "description": "Это книга о манасчи Жусупе Мамае.", - "icon": "resources/fire2.png", - "radius": 0.3 - }, - "activateFunction" : "on_book_pickup" - }, - { + { "name": "stairs", "texturePath": "resources/w/exterior/Staircase001.png", "meshPath": "resources/w/exterior/int_stairs001.txt", @@ -165,10 +82,9 @@ "positionX": 0.0, "positionY": -5.0, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "tree001", "texturePath": "resources/w/exterior/tree001.png", "meshPath": "resources/w/exterior/tree003.txt", @@ -178,10 +94,9 @@ "positionX": 10.0, "positionY": -5.0, "positionZ": 12.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "tree002", "texturePath": "resources/w/exterior/tree001.png", "meshPath": "resources/w/exterior/tree003.txt", @@ -191,10 +106,9 @@ "positionX": -12, "positionY": -5.0, "positionZ": 19.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "tree003", "texturePath": "resources/w/exterior/tree001.png", "meshPath": "resources/w/exterior/tree003.txt", @@ -204,10 +118,9 @@ "positionX": -12.0, "positionY": -5.0, "positionZ": 8.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "tree004", "texturePath": "resources/w/exterior/tree001.png", "meshPath": "resources/w/exterior/tree003.txt", @@ -217,10 +130,9 @@ "positionX": -12.0, "positionY": -5.0, "positionZ": 0.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "tree005", "texturePath": "resources/w/exterior/tree001.png", "meshPath": "resources/w/exterior/tree003.txt", @@ -230,10 +142,9 @@ "positionX": -12.0, "positionY": -5.0, "positionZ": -8.0, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "tree006", "texturePath": "resources/w/exterior/tree001.png", "meshPath": "resources/w/exterior/tree003.txt", @@ -243,10 +154,9 @@ "positionX": 8.49915, "positionY": -5.0, "positionZ": -2.59884, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "tree007", "texturePath": "resources/w/exterior/tree001.png", "meshPath": "resources/w/exterior/tree003.txt", @@ -256,10 +166,9 @@ "positionX": 14.5936, "positionY": -5.0, "positionZ": 5.3401, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "tree008", "texturePath": "resources/w/exterior/tree001.png", "meshPath": "resources/w/exterior/tree003.txt", @@ -269,10 +178,9 @@ "positionX": 23.9295, "positionY": -5.0, "positionZ": 9.00583, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "tree009", "texturePath": "resources/w/exterior/tree001.png", "meshPath": "resources/w/exterior/tree003.txt", @@ -282,10 +190,9 @@ "positionX": 29.8128, "positionY": -5.0, "positionZ": -1.45278, - "scale": 1.0, - "interactive": false + "scale": 1.0 }, - { + { "name": "tree010", "texturePath": "resources/w/exterior/tree001.png", "meshPath": "resources/w/exterior/tree003.txt", @@ -295,8 +202,7 @@ "positionX": 33.1771, "positionY": -5.0, "positionZ": 14.609, - "scale": 1.0, - "interactive": false + "scale": 1.0 } ] - } \ No newline at end of file + } diff --git a/resources/config2/interactive_objects_dorm.json b/resources/config2/interactive_objects_dorm.json new file mode 100644 index 0000000..2c594dc --- /dev/null +++ b/resources/config2/interactive_objects_dorm.json @@ -0,0 +1,32 @@ +{ + "objects": [ + { + "name": "Phone001", + "texturePath": "resources/w/dorm/phone001_tex001.png", + "meshPath": "resources/w/dorm/phone001.txt", + "rotationX": 0.0, + "rotationY": 0.0, + "rotationZ": 0.0, + "positionX": 5.8729, + "positionY": 1.04917, + "positionZ": -12.5262, + "scale": 1.0, + "interactionRadius": 0.3, + "activateFunction": "on_phone_pickup" + }, + { + "name": "Journal001", + "texturePath": "resources/w/dorm/journal001_tex002.png", + "meshPath": "resources/w/dorm/journal001.txt", + "rotationX": 0.0, + "rotationY": 0.0, + "rotationZ": 0.0, + "positionX": 5.05146, + "positionY": 1.06711, + "positionZ": -12.4661, + "scale": 1.0, + "interactionRadius": 0.3, + "activateFunction": "on_journal_pickup" + } + ] +} diff --git a/resources/config2/interactive_objects_uni_interior.json b/resources/config2/interactive_objects_uni_interior.json new file mode 100644 index 0000000..37f3b0c --- /dev/null +++ b/resources/config2/interactive_objects_uni_interior.json @@ -0,0 +1,59 @@ +{ + "objects": [ + { + "name": "Bookshelf001", + "texturePath": "resources/w/interior/ikea_bookshelf001_tex001.png", + "meshPath": "resources/w/interior/ikea_bookshelf001.txt", + "rotationX": 0.0, + "rotationY": 0.0, + "rotationZ": 0.0, + "positionX": 1.9653, + "positionY": 1.0911, + "positionZ": 0.91977, + "scale": 1.0, + "interactionRadius": 1.5, + "activateFunction": "on_bookshelf_clicked" + }, + { + "name": "Computer001", + "texturePath": "resources/w/interior/computer_texture001.png", + "meshPath": "resources/w/interior/computer001.txt", + "rotationX": 0.0, + "rotationY": 0.0, + "rotationZ": 0.0, + "positionX": 5.0916, + "positionY": 1.1534, + "positionZ": 1.0568, + "scale": 1.0, + "activateFunction": "on_computer_clicked" + }, + { + "name": "Knife001", + "texturePath": "resources/w/white.png", + "meshPath": "resources/w/interior/Knife.txt", + "rotationX": 0.0, + "rotationY": 0.0, + "rotationZ": 0.0, + "positionX": -2.26293, + "positionY": 0.91414, + "positionZ": 1.56758, + "scale": 1.0, + "interactionRadius": 0.3, + "activateFunction": "on_knife_pickup" + }, + { + "name": "Book001", + "texturePath": "resources/w/interior/book_tex002.png", + "meshPath": "resources/w/interior/book001.txt", + "rotationX": 0.0, + "rotationY": 0.0, + "rotationZ": 0.0, + "positionX": 1.97952, + "positionY": 0.95746, + "positionZ": 0.786023, + "scale": 1.0, + "interactionRadius": 1.5, + "activateFunction": "on_book_pickup" + } + ] +} diff --git a/resources/config2/items.json b/resources/config2/items.json new file mode 100644 index 0000000..fc94623 --- /dev/null +++ b/resources/config2/items.json @@ -0,0 +1,28 @@ +{ + "items": [ + { + "id": "phone", + "name": "Телефон", + "description": "Я не могу себе представить жизнь без своего телефона", + "icon": "resources/fire2.png" + }, + { + "id": "journal", + "name": "Журнал", + "description": "Это мой журнал куда я вношу свои заметки.", + "icon": "resources/fire2.png" + }, + { + "id": "knife", + "name": "Серебряный нож", + "description": "Этот серебряный нож я одолжил у Айпери, и я должен его вернуть.", + "icon": "resources/fire2.png" + }, + { + "id": "book", + "name": "Книга", + "description": "Это книга о манасчи Жусупе Мамае.", + "icon": "resources/fire2.png" + } + ] +} diff --git a/resources/start_dorm.lua b/resources/start_dorm.lua index 8945fc7..8e29c9d 100644 --- a/resources/start_dorm.lua +++ b/resources/start_dorm.lua @@ -20,12 +20,14 @@ journal_picked_up = false function on_phone_pickup() game_api.pickup_item("phone") +game_api.deactivate_interactive_object("Phone001") game_api.start_dialogue("dialog_phone_pickup001") phone_picked_up = true end function on_journal_pickup() game_api.pickup_item("journal") +game_api.deactivate_interactive_object("Journal001") game_api.start_dialogue("dialog_journal_pickup001") journal_picked_up = true end diff --git a/resources/start_uni_interior.lua b/resources/start_uni_interior.lua index e69909a..8bee40c 100644 --- a/resources/start_uni_interior.lua +++ b/resources/start_uni_interior.lua @@ -2,6 +2,7 @@ lection_is_over = false +player_hold_book = false function lection_hall_zone001_enter_callback() --game_api.start_dialogue("") @@ -37,13 +38,35 @@ game_api.set_trigger_zone_callbacks("knife_dialog_zone001", function on_knife_pickup() game_api.pickup_item("knife") +game_api.deactivate_interactive_object("Knife001") end - function on_book_pickup() -game_api.pickup_item("book") + if not player_hold_book then + game_api.pickup_item("book") + game_api.deactivate_interactive_object("Book001") + player_hold_book = true + else + game_api.remove_item("book") + game_api.activate_interactive_object("Book001") + player_hold_book = false + end end +function on_bookshelf_clicked() + if not player_hold_book then + game_api.pickup_item("book") + game_api.deactivate_interactive_object("Book001") + player_hold_book = true + else + game_api.remove_item("book") + game_api.activate_interactive_object("Book001") + player_hold_book = false + end +end + + + function on_npc_interact(npc_index) print("[Lua] NPC interaction! Index: " .. tostring(npc_index)) if npc_index == 1 then diff --git a/src/Character.h b/src/Character.h index da15002..2dbb4d1 100644 --- a/src/Character.h +++ b/src/Character.h @@ -2,7 +2,6 @@ #include "BoneAnimatedModelNew.h" #include "render/Renderer.h" #include "render/TextureManager.h" -#include "items/Item.h" #include "SparkEmitter.h" #include #include @@ -106,11 +105,8 @@ public: // Applied after scale, fixes model-space orientation (e.g. Blender Z-up exports) Eigen::Quaternionf modelCorrectionRotation = Eigen::Quaternionf::Identity(); - // NPC Gift - Item giftItem; std::string npcId; std::string npcName; - bool giftReceived = false; float hp = 200.f; // Captured lazily from `hp` on the first update() tick if left at zero; // set explicitly if you need a different reference for the health bar. diff --git a/src/Game.cpp b/src/Game.cpp index f53f1b4..38b58d0 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -1,6 +1,7 @@ #include "Game.h" #include "AnimatedModel.h" #include "utils/Utils.h" +#include "items/ItemRegistry.h" #include "render/OpenGlExtensions.h" #include #include "render/TextureManager.h" @@ -158,6 +159,8 @@ namespace ZL std::cout << "Load resurces step 4" << std::endl; + ItemRegistry::instance().loadFromJson("resources/config2/items.json", CONST_ZIP_FILE); + LocationSetup uniInteriorParams; uniInteriorParams.gameObjectsJsonPath = "resources/config2/gameobjects_uni_interior.json"; uniInteriorParams.npcsJsonPath = "resources/config2/npcs_uni_interior.json"; @@ -167,6 +170,7 @@ namespace ZL uniInteriorParams.teleportsJsonPath = "resources/config2/teleports.json"; uniInteriorParams.triggerZonesJsonPath = "resources/config2/trigger_zones_uni_interior.json"; uniInteriorParams.scriptPath = "resources/start_uni_interior.lua"; + uniInteriorParams.interactiveObjectsJsonPath = "resources/config2/interactive_objects_uni_interior.json"; uniInteriorParams.playerPosition = Eigen::Vector3f(0.942694, 0, -9.63104); locations["uni_interior"] = std::make_shared(renderer, inventory); @@ -174,6 +178,7 @@ namespace ZL LocationSetup uniExteriorParams = uniInteriorParams; uniExteriorParams.gameObjectsJsonPath = "resources/config2/gameobjects2.json"; + uniExteriorParams.interactiveObjectsJsonPath = ""; //uniExteriorParams.navigationJsonPaths = {"resources/config2/navigation2.json"}; uniExteriorParams.navigationJsonPaths = { "resources/w/nav_uni_exterior.txt" }; uniExteriorParams.teleportsJsonPath = "resources/config2/teleports2.json"; @@ -194,6 +199,7 @@ namespace ZL params_dorm.teleportsJsonPath = "resources/config2/teleports_dorm.json"; params_dorm.triggerZonesJsonPath = "resources/config2/trigger_zones_dorm.json"; params_dorm.scriptPath = "resources/start_dorm.lua"; + params_dorm.interactiveObjectsJsonPath = "resources/config2/interactive_objects_dorm.json"; params_dorm.playerPosition = Eigen::Vector3f(6.76345, 0, -14.6022); locations["location_dorm"] = std::make_shared(renderer, inventory); diff --git a/src/Location.cpp b/src/Location.cpp index 5c4f9f5..552cb58 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -57,7 +57,7 @@ namespace ZL gameObjects = GameObjectLoader::loadAndCreateGameObjects(params.gameObjectsJsonPath, renderer, CONST_ZIP_FILE); // Load interactive objects - interactiveObjects = GameObjectLoader::loadAndCreateInteractiveObjects(params.gameObjectsJsonPath, renderer, CONST_ZIP_FILE); + interactiveObjects = GameObjectLoader::loadAndCreateInteractiveObjects(params.interactiveObjectsJsonPath, renderer, CONST_ZIP_FILE); //auto playerTexture = std::make_shared(CreateTextureDataFromPng("resources/w/gg/IMG_20260413_182354_992.png", CONST_ZIP_FILE)); auto playerTexture = renderer.textureManager.LoadFromPng("resources/w/gg/UniV_Grid_2K_Base_color.png", CONST_ZIP_FILE); @@ -513,7 +513,7 @@ namespace ZL InteractiveObject* closestObject = nullptr; for (auto& intObj : interactiveObjects) { - std::cout << "[RAYCAST] Checking object: " << intObj.name << " (active: " << intObj.isActive << ")" << std::endl; + std::cout << "[RAYCAST] Checking object: " << intObj.loadedObject.name << " (active: " << intObj.isActive << ")" << std::endl; if (!intObj.isActive) { std::cout << "[RAYCAST] -> Object inactive, skipping" << std::endl; @@ -548,7 +548,7 @@ namespace ZL } if (closestObject) { - std::cout << "[RAYCAST] *** RAYCAST SUCCESS: Found object " << closestObject->name << " ***" << std::endl; + std::cout << "[RAYCAST] *** RAYCAST SUCCESS: Found object " << closestObject->loadedObject.name << " ***" << std::endl; } else { std::cout << "[RAYCAST] No objects hit" << std::endl; @@ -749,10 +749,10 @@ namespace ZL } for (auto& intObj : interactiveObjects) { - if (intObj.isActive && intObj.texture) { + if (intObj.isActive && intObj.loadedObject.texture) { renderer.PushMatrix(); renderer.TranslateMatrix(intObj.position); - renderer.DrawVertexRenderStruct(intObj.mesh); + renderer.DrawVertexRenderStruct(intObj.loadedObject.mesh); renderer.PopMatrix(); } } @@ -1074,7 +1074,7 @@ namespace ZL // If player is close enough to pick up the item if (distToObject <= targetInteractiveObject->interactionRadius + 1.0f) { std::cout << "[PICKUP] Player reached object! Distance: " << distToObject << std::endl; - std::cout << "[PICKUP] Calling Lua callback for: " << targetInteractiveObject->id << std::endl; + std::cout << "[PICKUP] Calling Lua callback for: " << targetInteractiveObject->loadedObject.name << std::endl; // Call custom activate function if specified, otherwise use fallback try { @@ -1084,7 +1084,7 @@ namespace ZL } else { std::cout << "[PICKUP] Using fallback callback" << std::endl; - scriptEngine.callItemPickupCallback(targetInteractiveObject->id); + scriptEngine.callItemPickupCallback(targetInteractiveObject->loadedObject.name); } } catch (const std::exception& e) { @@ -1171,7 +1171,7 @@ namespace ZL // First check if we clicked on interactive object InteractiveObject* clickedObject = raycastInteractiveObjects(camPos, rayDir); if (clickedObject && player && clickedObject->isActive) { - std::cout << "[CLICK] *** SUCCESS: Clicked on interactive object: " << clickedObject->name << " ***" << std::endl; + std::cout << "[CLICK] *** SUCCESS: Clicked on interactive object: " << clickedObject->loadedObject.name << " ***" << std::endl; std::cout << "[CLICK] Object position: (" << clickedObject->position.x() << ", " << clickedObject->position.y() << ", " << clickedObject->position.z() << ")" << std::endl; std::cout << "[CLICK] Player position: (" << player->position.x() << ", " diff --git a/src/Location.h b/src/Location.h index 7942707..06883f6 100644 --- a/src/Location.h +++ b/src/Location.h @@ -32,6 +32,7 @@ namespace ZL struct LocationSetup { std::string gameObjectsJsonPath; + std::string interactiveObjectsJsonPath; std::string npcsJsonPath; std::string dialoguesJsonPath; std::vector navigationJsonPaths; diff --git a/src/ScriptEngine.cpp b/src/ScriptEngine.cpp index 99ab5ae..84d7655 100644 --- a/src/ScriptEngine.cpp +++ b/src/ScriptEngine.cpp @@ -4,6 +4,7 @@ #include #include #include "Location.h" +#include "items/ItemRegistry.h" #define SOL_ALL_SAFETIES_ON 1 #include @@ -53,34 +54,15 @@ namespace ZL { npcs[index]->setTarget(Eigen::Vector3f(x, y, z), std::move(cb)); }); - // pickup_item(object_name) - api.set_function("pickup_item", [game, inventory](const std::string& objectName) { - - - std::cout << "[script] pickup_item: " << objectName << std::endl; - - for (auto& intObj : game->interactiveObjects) { - if (intObj.id == objectName && intObj.isActive) { - // Add item to inventory - inventory->addItem(intObj.dropItem); - - // Deactivate object - intObj.isActive = false; - - std::cout << "[script] Item picked up successfully: " << intObj.dropItem.name << std::endl; - return; - } + // pickup_item(item_id) + api.set_function("pickup_item", [inventory](const std::string& itemId) { + const Item* item = ItemRegistry::instance().findById(itemId); + if (item) { + inventory->addItem(*item); + std::cout << "[script] pickup_item: " << item->name << std::endl; + } else { + std::cerr << "[script] pickup_item: item '" << itemId << "' not found in ItemRegistry\n"; } - std::cerr << "[script] Warning: Interactive object not found or already picked up: " << objectName << std::endl; - }); - - - // add_item(item_id, name, description, icon) - api.set_function("add_item", [game, inventory](const std::string& id, const std::string& name, - const std::string& description, const std::string& icon) { - std::cout << "[script] add_item: " << name << std::endl; - Item newItem(id, name, description, icon); - inventory->addItem(newItem); }); // remove_item(item_id) @@ -91,15 +73,26 @@ namespace ZL { // deactivate_interactive_object(object_name) api.set_function("deactivate_interactive_object", [game](const std::string& objectName) { - std::cout << "[script] deactivate_interactive_object: " << objectName << std::endl; for (auto& intObj : game->interactiveObjects) { - if (intObj.id == objectName) { + if (intObj.loadedObject.name == objectName) { intObj.isActive = false; - std::cout << "[script] Interactive object deactivated: " << objectName << std::endl; + std::cout << "[script] deactivate_interactive_object: " << objectName << std::endl; return; } } - std::cerr << "[script] Warning: Interactive object not found: " << objectName << std::endl; + std::cerr << "[script] deactivate_interactive_object: not found: " << objectName << std::endl; + }); + + // activate_interactive_object(object_name) + api.set_function("activate_interactive_object", [game](const std::string& objectName) { + for (auto& intObj : game->interactiveObjects) { + if (intObj.loadedObject.name == objectName) { + intObj.isActive = true; + std::cout << "[script] activate_interactive_object: " << objectName << std::endl; + return; + } + } + std::cerr << "[script] activate_interactive_object: not found: " << objectName << std::endl; }); // get_inventory_count() @@ -181,38 +174,6 @@ namespace ZL { npcs[index]->enabled = value; }); - // receive_npc_gift(npc_index) - api.set_function("receive_npc_gift", [game, inventory](int npcIndex) { - std::cout << "[script] receive_npc_gift: npc index " << npcIndex << std::endl; - - auto& npcs = game->npcs; - if (npcIndex < 0 || npcIndex >= static_cast(npcs.size())) { - std::cerr << "[script] receive_npc_gift: index out of range\n"; - return; - } - - auto& npc = npcs[npcIndex]; - if (!npc) { - std::cerr << "[script] receive_npc_gift: npc is null\n"; - return; - } - - if (npc->giftReceived) { - std::cout << "[script] NPC " << npc->npcName << " already gave gift\n"; - return; - } - if (npc->giftItem.id.empty()) { - std::cerr << "[script] receive_npc_gift: NPC has no gift\n"; - return; - } - - inventory->addItem(npc->giftItem); - npc->giftReceived = true; - - std::cout << "[script] Received gift from " << npc->npcName << ": " - << npc->giftItem.name << std::endl; - }); - lua.script_file(scriptPath); } diff --git a/src/items/GameObjectLoader.cpp b/src/items/GameObjectLoader.cpp index 434e10a..4846c6e 100644 --- a/src/items/GameObjectLoader.cpp +++ b/src/items/GameObjectLoader.cpp @@ -7,6 +7,7 @@ #include "utils/Utils.h" #include #include "../Character.h" + namespace ZL { void set_Texture(Character& npc, const TextureDataStruct& texture); @@ -14,169 +15,144 @@ namespace ZL { using json = nlohmann::json; - std::vector GameObjectLoader::loadFromJson(const std::string& jsonPath, const std::string& zipPath) + // ----------------------------------------------------------------------- + // Private helpers + // ----------------------------------------------------------------------- + + json GameObjectLoader::loadJson(const std::string& jsonPath, const std::string& zipPath) { - std::vector objects; - std::string content; try { if (zipPath.empty()) { content = readTextFile(jsonPath); - } - else { + } else { auto buf = readFileFromZIP(jsonPath, zipPath); - if (buf.empty()) { - std::cerr << "UiManager: failed to read " << jsonPath << " from zip " << zipPath << std::endl; - throw std::runtime_error("Failed to load UI file: " + jsonPath); - } + if (buf.empty()) + throw std::runtime_error("empty result from zip"); content.assign(buf.begin(), buf.end()); } - } - catch (const std::exception& e) { - std::cerr << "UiManager: failed to open " << jsonPath << " : " << e.what() << std::endl; - throw std::runtime_error("Failed to load UI file: " + jsonPath); - } - - - json j; - try { - j = json::parse(content); - } - catch (const std::exception& e) { - std::cerr << "UiManager: json parse error: " << e.what() << std::endl; - throw std::runtime_error("Failed to load UI file: " + jsonPath); - } - - - //json j; - - try { - /*std::ifstream file(jsonPath); - if (!file.is_open()) { - throw std::runtime_error("Could not open file: " + jsonPath); - } - - if (file.peek() == std::ifstream::traits_type::eof()) { - throw std::runtime_error("JSON file is empty: " + jsonPath); - } - - file >> j;*/ - - if (!j.contains("objects") || !j["objects"].is_array()) { - std::cerr << "Warning: 'objects' array not found in " << jsonPath << std::endl; - return objects; - } - - for (const auto& item : j["objects"]) { - GameObjectData data; - - data.name = item.value("name", "Unknown"); - data.texturePath = item.value("texturePath", ""); - data.meshPath = item.value("meshPath", ""); - - data.rotationX = item.value("rotationX", 0.0f); - data.rotationY = item.value("rotationY", 0.0f); - data.rotationZ = item.value("rotationZ", 0.0f); - - data.positionX = item.value("positionX", 0.0f); - data.positionY = item.value("positionY", 0.0f); - data.positionZ = item.value("positionZ", 0.0f); - - data.scale = item.value("scale", 1.0f); - - // Interactive object properties - data.interactive = item.value("interactive", false); - if (data.interactive && item.contains("item") && item["item"].is_object()) { - const auto& itemData = item["item"]; - data.itemId = itemData.value("id", ""); - data.itemName = itemData.value("name", "Unknown Item"); - data.itemDescription = itemData.value("description", ""); - data.itemIcon = itemData.value("icon", ""); - data.interactionRadius = itemData.value("radius", 2.0f); - } - - data.activateFunctionName = item.value("activateFunction", ""); - - if (!data.meshPath.empty()) { - objects.push_back(data); - } - } - - std::cout << "Successfully loaded " << objects.size() << " game objects from " << jsonPath << std::endl; - } catch (const std::exception& e) { - std::cerr << "Error loading JSON: " << e.what() << std::endl; + throw std::runtime_error("Failed to read " + jsonPath + ": " + e.what()); } + try { + return json::parse(content); + } catch (const std::exception& e) { + throw std::runtime_error("JSON parse error in " + jsonPath + ": " + e.what()); + } + } + + static GameObjectData parseGameObjectData(const json& item) + { + GameObjectData data; + data.name = item.value("name", "Unknown"); + data.texturePath = item.value("texturePath", ""); + data.meshPath = item.value("meshPath", ""); + data.rotationX = item.value("rotationX", 0.0f); + data.rotationY = item.value("rotationY", 0.0f); + data.rotationZ = item.value("rotationZ", 0.0f); + data.positionX = item.value("positionX", 0.0f); + data.positionY = item.value("positionY", 0.0f); + data.positionZ = item.value("positionZ", 0.0f); + data.scale = item.value("scale", 1.0f); + return data; + } + + std::vector GameObjectLoader::loadFromJson(const std::string& jsonPath, const std::string& zipPath) + { + std::vector objects; + json j = loadJson(jsonPath, zipPath); + + if (!j.contains("objects") || !j["objects"].is_array()) { + std::cerr << "Warning: 'objects' array not found in " << jsonPath << std::endl; + return objects; + } + + for (const auto& item : j["objects"]) { + GameObjectData data = parseGameObjectData(item); + if (!data.meshPath.empty()) + objects.push_back(std::move(data)); + } + + std::cout << "Loaded " << objects.size() << " static objects from " << jsonPath << std::endl; return objects; } + std::vector GameObjectLoader::loadInteractiveFromJson(const std::string& jsonPath, const std::string& zipPath) + { + std::vector objects; + json j = loadJson(jsonPath, zipPath); + + if (!j.contains("objects") || !j["objects"].is_array()) { + std::cerr << "Warning: 'objects' array not found in " << jsonPath << std::endl; + return objects; + } + + for (const auto& item : j["objects"]) { + InteractiveObjectData data; + data.base = parseGameObjectData(item); + data.interactionRadius = item.value("interactionRadius", 2.0f); + data.activateFunctionName = item.value("activateFunction", ""); + if (!data.base.meshPath.empty()) + objects.push_back(std::move(data)); + } + + std::cout << "Loaded " << objects.size() << " interactive objects from " << jsonPath << std::endl; + return objects; + } + + LoadedGameObject GameObjectLoader::buildLoadedObject(const GameObjectData& data, Renderer& renderer, const std::string& zipPath) + { + LoadedGameObject obj; + obj.name = data.name; + + obj.texture = renderer.textureManager.LoadFromPng(data.texturePath, zipPath); + + if (data.meshPath.size() > 4 && data.meshPath.compare(data.meshPath.size() - 4, 4, ".bin") == 0) + obj.mesh.data = LoadModelFromBinFile(data.meshPath, zipPath); + else + obj.mesh.data = LoadFromTextFile02(data.meshPath, zipPath); + + Eigen::Quaternionf rot = Eigen::Quaternionf::Identity(); + if (data.rotationX != 0.0f) + rot = Eigen::Quaternionf(Eigen::AngleAxisf(data.rotationX, Eigen::Vector3f::UnitX())) * rot; + if (data.rotationY != 0.0f) + rot = Eigen::Quaternionf(Eigen::AngleAxisf(data.rotationY, Eigen::Vector3f::UnitY())) * rot; + if (data.rotationZ != 0.0f) + rot = Eigen::Quaternionf(Eigen::AngleAxisf(data.rotationZ, Eigen::Vector3f::UnitZ())) * rot; + obj.mesh.data.RotateByMatrix(rot.toRotationMatrix()); + + if (data.scale != 1.0f) + obj.mesh.data.Scale(data.scale); + + return obj; + } + + // ----------------------------------------------------------------------- + // Public API + // ----------------------------------------------------------------------- + std::unordered_map GameObjectLoader::loadAndCreateGameObjects( const std::string& jsonPath, Renderer& renderer, const std::string& zipPath) { std::unordered_map gameObjects; - std::vector objectsData = loadFromJson(jsonPath, zipPath); - for (const auto& objData : objectsData) { - if (objData.interactive) continue; // Skip interactive objects, they're handled separately - - std::cout << "Loading game object: " << objData.name << std::endl; - - LoadedGameObject gameObj; - gameObj.name = objData.name; - - // Load texture + for (const auto& data : loadFromJson(jsonPath, zipPath)) { + std::cout << "Loading game object: " << data.name << std::endl; try { - gameObj.texture = renderer.textureManager.LoadFromPng(objData.texturePath, zipPath); - } - catch (const std::exception& e) { - std::cerr << "GameObjectLoader: Failed to load texture for '" << objData.name << "': " << e.what() << std::endl; - continue; - } + LoadedGameObject obj = buildLoadedObject(data, renderer, zipPath); - // Load mesh - try { - if (objData.meshPath.size() > 4 && objData.meshPath.compare(objData.meshPath.size() - 4, 4, ".bin") == 0) - gameObj.mesh.data = LoadModelFromBinFile(objData.meshPath, zipPath); - else - gameObj.mesh.data = LoadFromTextFile02(objData.meshPath, zipPath); - } - catch (const std::exception& e) { - std::cerr << "GameObjectLoader: Failed to load mesh for '" << objData.name << "': " << e.what() << std::endl; - continue; - } + if (data.positionX != 0.0f || data.positionY != 0.0f || data.positionZ != 0.0f) + obj.mesh.data.Move({ data.positionX, data.positionY, data.positionZ }); - // Apply rotation - Eigen::Quaternionf rotationQuat = Eigen::Quaternionf::Identity(); - - if (objData.rotationX != 0.0f) { - rotationQuat = Eigen::Quaternionf(Eigen::AngleAxisf(objData.rotationX, Eigen::Vector3f::UnitX())) * rotationQuat; + obj.mesh.RefreshVBO(); + gameObjects[data.name] = std::move(obj); + std::cout << "Successfully loaded: " << data.name << std::endl; + } catch (const std::exception& e) { + std::cerr << "GameObjectLoader: Failed to load '" << data.name << "': " << e.what() << std::endl; } - if (objData.rotationY != 0.0f) { - rotationQuat = Eigen::Quaternionf(Eigen::AngleAxisf(objData.rotationY, Eigen::Vector3f::UnitY())) * rotationQuat; - } - if (objData.rotationZ != 0.0f) { - rotationQuat = Eigen::Quaternionf(Eigen::AngleAxisf(objData.rotationZ, Eigen::Vector3f::UnitZ())) * rotationQuat; - } - - gameObj.mesh.data.RotateByMatrix(rotationQuat.toRotationMatrix()); - - // Apply scale - if (objData.scale != 1.0f) { - gameObj.mesh.data.Scale(objData.scale); - } - - // Apply position - if (objData.positionX != 0.0f || objData.positionY != 0.0f || objData.positionZ != 0.0f) { - gameObj.mesh.data.Move({ objData.positionX, objData.positionY, objData.positionZ }); - } - - gameObj.mesh.RefreshVBO(); - gameObjects[objData.name] = gameObj; - - std::cout << "Successfully loaded: " << objData.name << std::endl; } std::cout << "Total game objects loaded: " << gameObjects.size() << std::endl; @@ -189,103 +165,49 @@ namespace ZL { const std::string& zipPath) { std::vector interactiveObjects; - std::vector objectsData = loadFromJson(jsonPath, zipPath); - for (const auto& objData : objectsData) { - if (!objData.interactive) continue; + if (jsonPath.empty()) + return interactiveObjects; - std::cout << "Loading interactive object: " << objData.name << std::endl; - - InteractiveObject intObj; - //intObj.id = objData.name; - intObj.id = !objData.itemId.empty() ? objData.itemId : objData.name; - intObj.name = objData.name; - intObj.interactionRadius = objData.interactionRadius; - intObj.activateFunctionName = objData.activateFunctionName; - - // Load texture + for (const auto& data : loadInteractiveFromJson(jsonPath, zipPath)) { + std::cout << "Loading interactive object: " << data.base.name << std::endl; try { - intObj.texture = renderer.textureManager.LoadFromPng(objData.texturePath, zipPath); - } - catch (const std::exception& e) { - std::cerr << "GameObjectLoader: Failed to load texture for interactive '" << objData.name << "': " << e.what() << std::endl; - continue; - } + InteractiveObject intObj; + intObj.loadedObject = buildLoadedObject(data.base, renderer, zipPath); + intObj.interactionRadius = data.interactionRadius; + intObj.activateFunctionName = data.activateFunctionName; - // Load mesh - try { - if (objData.meshPath.size() > 4 && objData.meshPath.compare(objData.meshPath.size() - 4, 4, ".bin") == 0) - intObj.mesh.data = LoadModelFromBinFile(objData.meshPath, zipPath); - else - intObj.mesh.data = LoadFromTextFile02(objData.meshPath, zipPath); - } - catch (const std::exception& e) { - std::cerr << "GameObjectLoader: Failed to load mesh for interactive '" << objData.name << "': " << e.what() << std::endl; - continue; - } + intObj.loadedObject.mesh.RefreshVBO(); - // Apply rotation - Eigen::Quaternionf rotationQuat = Eigen::Quaternionf::Identity(); + // Center mesh at origin; store corrected world position separately. + if (!intObj.loadedObject.mesh.data.PositionData.empty()) { + Eigen::Vector3f meshMin = intObj.loadedObject.mesh.data.PositionData[0]; + Eigen::Vector3f meshMax = intObj.loadedObject.mesh.data.PositionData[0]; + for (const auto& v : intObj.loadedObject.mesh.data.PositionData) { + meshMin = meshMin.cwiseMin(v); + meshMax = meshMax.cwiseMax(v); + } + Eigen::Vector3f meshCenter = (meshMin + meshMax) * 0.5f; - if (objData.rotationX != 0.0f) { - rotationQuat = Eigen::Quaternionf(Eigen::AngleAxisf(objData.rotationX, Eigen::Vector3f::UnitX())) * rotationQuat; - } - if (objData.rotationY != 0.0f) { - rotationQuat = Eigen::Quaternionf(Eigen::AngleAxisf(objData.rotationY, Eigen::Vector3f::UnitY())) * rotationQuat; - } - if (objData.rotationZ != 0.0f) { - rotationQuat = Eigen::Quaternionf(Eigen::AngleAxisf(objData.rotationZ, Eigen::Vector3f::UnitZ())) * rotationQuat; - } + intObj.loadedObject.mesh.data.Move(-meshCenter); + intObj.loadedObject.mesh.RefreshVBO(); - intObj.mesh.data.RotateByMatrix(rotationQuat.toRotationMatrix()); - - // Apply scale - if (objData.scale != 1.0f) { - intObj.mesh.data.Scale(objData.scale); - } - - intObj.mesh.RefreshVBO(); - - // Calculate mesh bounds to properly offset position - if (!intObj.mesh.data.PositionData.empty()) { - Eigen::Vector3f meshMin = intObj.mesh.data.PositionData[0]; - Eigen::Vector3f meshMax = intObj.mesh.data.PositionData[0]; - - for (const auto& vert : intObj.mesh.data.PositionData) { - meshMin = meshMin.cwiseMin(vert); - meshMax = meshMax.cwiseMax(vert); + intObj.position = Eigen::Vector3f(data.base.positionX, data.base.positionY, data.base.positionZ) + meshCenter; + } else { + intObj.position = Eigen::Vector3f(data.base.positionX, data.base.positionY, data.base.positionZ); } - - Eigen::Vector3f meshCenter = (meshMin + meshMax) * 0.5f; - std::cout << "[LOADER] Mesh bounds:" << std::endl; - std::cout << " Min: (" << meshMin.x() << ", " << meshMin.y() << ", " << meshMin.z() << ")" << std::endl; - std::cout << " Max: (" << meshMax.x() << ", " << meshMax.y() << ", " << meshMax.z() << ")" << std::endl; - std::cout << " Center: (" << meshCenter.x() << ", " << meshCenter.y() << ", " << meshCenter.z() << ")" << std::endl; - - // Translate mesh so it's centered at origin - intObj.mesh.data.Move(-meshCenter); - intObj.mesh.RefreshVBO(); - - // Adjust position to account for mesh offset - intObj.position = Eigen::Vector3f(objData.positionX, objData.positionY, objData.positionZ) + meshCenter; - - std::cout << "[LOADER] Corrected position: (" << intObj.position.x() << ", " - << intObj.position.y() << ", " << intObj.position.z() << ")" << std::endl; - } else { - // Fallback if no vertices - intObj.position = Eigen::Vector3f(objData.positionX, objData.positionY, objData.positionZ); + + interactiveObjects.push_back(std::move(intObj)); + + if (!data.activateFunctionName.empty()) + std::cout << "Successfully loaded interactive: " << data.base.name + << " (function: " << data.activateFunctionName << ")" << std::endl; + else + std::cout << "Successfully loaded interactive: " << data.base.name << std::endl; + + } catch (const std::exception& e) { + std::cerr << "GameObjectLoader: Failed to load interactive '" << data.base.name << "': " << e.what() << std::endl; } - - // Create item - intObj.dropItem = Item(objData.itemId, objData.itemName, objData.itemDescription, objData.itemIcon); - - interactiveObjects.push_back(intObj); - - std::cout << "Successfully loaded interactive: " << objData.name << " (item: " << objData.itemName; - if (!objData.activateFunctionName.empty()) { - std::cout << ", function: " << objData.activateFunctionName; - } - std::cout << ")" << std::endl; } std::cout << "Total interactive objects loaded: " << interactiveObjects.size() << std::endl; @@ -295,111 +217,52 @@ namespace ZL { std::vector GameObjectLoader::loadNpcsFromJson(const std::string& jsonPath, const std::string& zipPath) { std::vector npcs; + json j = loadJson(jsonPath, zipPath); - - std::string content; - try { - if (zipPath.empty()) { - content = readTextFile(jsonPath); - } - else { - auto buf = readFileFromZIP(jsonPath, zipPath); - if (buf.empty()) { - std::cerr << "UiManager: failed to read " << jsonPath << " from zip " << zipPath << std::endl; - throw std::runtime_error("Failed to load UI file: " + jsonPath); - } - content.assign(buf.begin(), buf.end()); - } + if (!j.contains("npcs") || !j["npcs"].is_array()) { + std::cerr << "Warning: 'npcs' array not found in " << jsonPath << std::endl; + return npcs; } - catch (const std::exception& e) { - std::cerr << "UiManager: failed to open " << jsonPath << " : " << e.what() << std::endl; - throw std::runtime_error("Failed to load UI file: " + jsonPath); - } - - json j; - try { - j = json::parse(content); - } - catch (const std::exception& e) { - std::cerr << "UiManager: json parse error: " << e.what() << std::endl; - throw std::runtime_error("Failed to load UI file: " + jsonPath); - } - - //json j; try { - /*std::ifstream file(jsonPath); - if (!file.is_open()) { - throw std::runtime_error("Could not open file: " + jsonPath); - } - - if (file.peek() == std::ifstream::traits_type::eof()) { - throw std::runtime_error("JSON file is empty: " + jsonPath); - } - - file >> j;*/ - - if (!j.contains("npcs") || !j["npcs"].is_array()) { - std::cerr << "Warning: 'npcs' array not found in " << jsonPath << std::endl; - return npcs; - } - for (const auto& item : j["npcs"]) { NpcData data; - data.id = item.value("id", "npc_unknown"); + data.id = item.value("id", "npc_unknown"); data.name = item.value("name", "Unknown NPC"); data.texturePath = item.value("texturePath", ""); if (item.contains("meshTextures") && item["meshTextures"].is_object()) { for (auto it = item["meshTextures"].begin(); it != item["meshTextures"].end(); ++it) { - if (it.value().is_string()) { + if (it.value().is_string()) data.meshTextures[it.key()] = it.value().get(); - } } } - data.interactionRadius = item.value("interactionRadius", 0.0f); - data.animationIdlePath = item.value("animationIdlePath", ""); - data.animationWalkPath = item.value("animationWalkPath", ""); + data.interactionRadius = item.value("interactionRadius", 0.0f); + data.animationIdlePath = item.value("animationIdlePath", ""); + data.animationWalkPath = item.value("animationWalkPath", ""); data.animationActionIdlePath = item.value("animationActionIdlePath", ""); - data.animationActionAttackPath = item.value("animationActionAttackPath", ""); + data.animationActionAttackPath = item.value("animationActionAttackPath", ""); data.animationStandToActionPath = item.value("animationStandToActionPath", ""); data.animationActionToStandPath = item.value("animationActionToStandPath", ""); data.animationActionToDeathPath = item.value("animationActionToDeathPath", ""); - data.animationDeathIdlePath = item.value("animationDeathIdlePath", ""); - - data.positionX = item.value("positionX", 0.0f); - data.positionY = item.value("positionY", 0.0f); - data.positionZ = item.value("positionZ", 0.0f); - - data.walkSpeed = item.value("walkSpeed", 1.5f); - data.rotationSpeed = item.value("rotationSpeed", 8.0f); - data.modelScale = item.value("modelScale", 0.01f); - - // Model correction rotation (in degrees, will convert to radians) + data.animationDeathIdlePath = item.value("animationDeathIdlePath", ""); + data.positionX = item.value("positionX", 0.0f); + data.positionY = item.value("positionY", 0.0f); + data.positionZ = item.value("positionZ", 0.0f); + data.walkSpeed = item.value("walkSpeed", 1.5f); + data.rotationSpeed = item.value("rotationSpeed", 8.0f); + data.modelScale = item.value("modelScale", 0.01f); data.modelCorrectionRotX = item.value("modelCorrectionRotX", 0.0f) * static_cast(M_PI) / 180.0f; data.modelCorrectionRotY = item.value("modelCorrectionRotY", 0.0f) * static_cast(M_PI) / 180.0f; data.modelCorrectionRotZ = item.value("modelCorrectionRotZ", 0.0f) * static_cast(M_PI) / 180.0f; + data.facingAngle = item.value("facingAngle", 0.0f); + data.hp = item.value("hp", 100.0f); + data.canAttack = item.value("canAttack", false); + data.enabled = item.value("enabled", true); - data.facingAngle = item.value("facingAngle", 0.0f); - - data.hp = item.value("hp", 100.0f); - data.canAttack = item.value("canAttack", false); - data.enabled = item.value("enabled", true); - - // Load gift data if available - if (item.contains("gift") && item["gift"].is_object()) { - const auto& giftData = item["gift"]; - data.gift.id = giftData.value("id", ""); - data.gift.name = giftData.value("name", "Gift"); - data.gift.description = giftData.value("description", "A gift from an NPC"); - data.gift.icon = giftData.value("icon", ""); - } - - npcs.push_back(data); + npcs.push_back(std::move(data)); } - std::cout << "Successfully loaded " << npcs.size() << " NPCs from " << jsonPath << std::endl; - } catch (const std::exception& e) { std::cerr << "Error loading NPCs from JSON: " << e.what() << std::endl; } @@ -412,62 +275,45 @@ namespace ZL { const std::string& zipPath) { std::vector> npcs; - std::vector npcsData = loadNpcsFromJson(jsonPath, zipPath); - for (const auto& npcData : npcsData) { + for (const auto& npcData : loadNpcsFromJson(jsonPath, zipPath)) { std::cout << "Loading NPC: " << npcData.name << std::endl; auto npc = std::make_unique(); - // Load animations try { if (npcData.animationIdlePath.substr(npcData.animationIdlePath.size() - 5) == ".anim") - { npc->loadBinaryAnimation(AnimationState::STAND, npcData.animationIdlePath, zipPath); - } else - { npc->loadAnimation(AnimationState::STAND, npcData.animationIdlePath, zipPath); - } std::cout << " Loaded IDLE animation: " << npcData.animationIdlePath << std::endl; - } - catch (const std::exception& e) { + } catch (const std::exception& e) { std::cerr << "GameObjectLoader: Failed to load IDLE animation for '" << npcData.name << "': " << e.what() << std::endl; continue; } try { if (npcData.animationWalkPath.substr(npcData.animationWalkPath.size() - 5) == ".anim") - { npc->loadBinaryAnimation(AnimationState::WALK, npcData.animationWalkPath, zipPath); - } else - { npc->loadAnimation(AnimationState::WALK, npcData.animationWalkPath, zipPath); - } - std::cout << " Loaded WALK animation: " << npcData.animationWalkPath << std::endl; - } - catch (const std::exception& e) { + } catch (const std::exception& e) { std::cerr << "GameObjectLoader: Failed to load WALK animation for '" << npcData.name << "': " << e.what() << std::endl; continue; } - // Optional combat/death slots — empty path means "skip this slot". auto loadOptionalAnim = [&](AnimationState state, const std::string& path, const char* label) { if (path.empty()) return; try { - if (path.size() >= 5 && path.substr(path.size() - 5) == ".anim") { + if (path.size() >= 5 && path.substr(path.size() - 5) == ".anim") npc->loadBinaryAnimation(state, path, zipPath); - } - else { + else npc->loadAnimation(state, path, zipPath); - } std::cout << " Loaded " << label << " animation: " << path << std::endl; - } - catch (const std::exception& e) { + } catch (const std::exception& e) { std::cerr << "GameObjectLoader: Failed to load " << label - << " animation for '" << npcData.name << "': " << e.what() << std::endl; + << " animation for '" << npcData.name << "': " << e.what() << std::endl; } }; loadOptionalAnim(AnimationState::ACTION_IDLE, npcData.animationActionIdlePath, "ACTION_IDLE"); @@ -477,81 +323,51 @@ namespace ZL { loadOptionalAnim(AnimationState::ACTION_TO_DEATH, npcData.animationActionToDeathPath, "ACTION_TO_DEATH"); loadOptionalAnim(AnimationState::DEATH_IDLE, npcData.animationDeathIdlePath, "DEATH_IDLE"); - // Load textures: per-mesh map takes precedence; fall back to single texturePath. try { if (!npcData.meshTextures.empty()) { - std::unordered_map> cache; for (const auto& entry : npcData.meshTextures) { - const std::string& meshName = entry.first; - const std::string& path = entry.second; - /*auto cit = cache.find(path); - if (cit == cache.end()) { - auto tex = std::make_shared(CreateTextureDataFromPng(path, zipPath.c_str())); - cit = cache.emplace(path, std::move(tex)).first; - std::cout << " Loaded texture: " << path << std::endl; - }*/ - //npc->setTexture(meshName, std::make_shared(CreateTextureDataFromPng(path, zipPath.c_str()))); - set_Texture(*npc, meshName, CreateTextureDataFromPng(path, zipPath.c_str())); - //std::cout << xxa(*npc, CreateTextureDataFromPng(path, zipPath.c_str())) << std::endl; - std::cout << " -> mesh '" << meshName << "'" << std::endl; + set_Texture(*npc, entry.first, CreateTextureDataFromPng(entry.second, zipPath.c_str())); + std::cout << " -> mesh '" << entry.first << "'" << std::endl; } - } - else { - //auto texture = std::make_shared(CreateTextureDataFromPng(npcData.texturePath, zipPath.c_str())); + } else { set_Texture(*npc, CreateTextureDataFromPng(npcData.texturePath, zipPath.c_str())); - //std::cout << xxa(*npc, CreateTextureDataFromPng(npcData.texturePath, zipPath.c_str())) << std::endl; std::cout << " Loaded texture: " << npcData.texturePath << std::endl; } - } - catch (const std::exception& e) { + } catch (const std::exception& e) { std::cerr << "GameObjectLoader: Failed to load texture for '" << npcData.name << "': " << e.what() << std::endl; continue; } - // Set NPC properties - npc->walkSpeed = npcData.walkSpeed; - npc->rotationSpeed = npcData.rotationSpeed; - npc->modelScale = npcData.modelScale; - npc->position = Eigen::Vector3f(npcData.positionX, npcData.positionY, npcData.positionZ); - npc->facingAngle = npcData.facingAngle; - npc->hp = npcData.hp; - npc->canAttack = npcData.canAttack; - npc->enabled = npcData.enabled; + npc->walkSpeed = npcData.walkSpeed; + npc->rotationSpeed = npcData.rotationSpeed; + npc->modelScale = npcData.modelScale; + npc->position = Eigen::Vector3f(npcData.positionX, npcData.positionY, npcData.positionZ); + npc->facingAngle = npcData.facingAngle; + npc->hp = npcData.hp; + npc->canAttack = npcData.canAttack; + npc->enabled = npcData.enabled; npc->interactionRadius = npcData.interactionRadius; + npc->npcId = npcData.id; + npc->npcName = npcData.name; - // Set NPC metadata - npc->npcId = npcData.id; - npc->npcName = npcData.name; + Eigen::Quaternionf corrRot = Eigen::Quaternionf::Identity(); + if (npcData.modelCorrectionRotX != 0.0f) + corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(npcData.modelCorrectionRotX, Eigen::Vector3f::UnitX())) * corrRot; + if (npcData.modelCorrectionRotY != 0.0f) + corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(npcData.modelCorrectionRotY, Eigen::Vector3f::UnitY())) * corrRot; + if (npcData.modelCorrectionRotZ != 0.0f) + corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(npcData.modelCorrectionRotZ, Eigen::Vector3f::UnitZ())) * corrRot; + npc->modelCorrectionRotation = corrRot; - // Set gift - if (!npcData.gift.id.empty()) { - npc->giftItem = Item(npcData.gift.id, npcData.gift.name, npcData.gift.description, npcData.gift.icon); - std::cout << " Gift: " << npcData.gift.name << std::endl; - } - - Eigen::Quaternionf corrRotQuat = Eigen::Quaternionf::Identity(); - if (npcData.modelCorrectionRotX != 0.0f) { - corrRotQuat = Eigen::Quaternionf(Eigen::AngleAxisf(npcData.modelCorrectionRotX, Eigen::Vector3f::UnitX())) * corrRotQuat; - } - if (npcData.modelCorrectionRotY != 0.0f) { - corrRotQuat = Eigen::Quaternionf(Eigen::AngleAxisf(npcData.modelCorrectionRotY, Eigen::Vector3f::UnitY())) * corrRotQuat; - } - if (npcData.modelCorrectionRotZ != 0.0f) { - corrRotQuat = Eigen::Quaternionf(Eigen::AngleAxisf(npcData.modelCorrectionRotZ, Eigen::Vector3f::UnitZ())) * corrRotQuat; - } - npc->modelCorrectionRotation = corrRotQuat; - - // Set initial target to current position npc->setTarget(npc->position); - npcs.push_back(std::move(npc)); - std::cout << "Successfully loaded NPC: " << npcData.name << " at position (" - << npcData.positionX << ", " << npcData.positionY << ", " << npcData.positionZ << ")" << std::endl; + std::cout << "Successfully loaded NPC: " << npcData.name << " at (" + << npcData.positionX << ", " << npcData.positionY << ", " << npcData.positionZ << ")" << std::endl; } std::cout << "Total NPCs loaded: " << npcs.size() << std::endl; return npcs; } -} // namespace ZL \ No newline at end of file +} // namespace ZL diff --git a/src/items/GameObjectLoader.h b/src/items/GameObjectLoader.h index 5fd1c39..9c33390 100644 --- a/src/items/GameObjectLoader.h +++ b/src/items/GameObjectLoader.h @@ -23,20 +23,12 @@ namespace ZL { float positionY = 0.0f; float positionZ = 0.0f; float scale = 1.0f; - bool interactive = false; - std::string itemId; - std::string itemName; - std::string itemDescription; - std::string itemIcon; - float interactionRadius = 2.0f; - std::string activateFunctionName; }; - struct GiftData { - std::string id; - std::string name; - std::string description; - std::string icon; + struct InteractiveObjectData { + GameObjectData base; + float interactionRadius = 2.0f; + std::string activateFunctionName; }; struct NpcData { @@ -66,20 +58,11 @@ namespace ZL { float hp = 100.0f; bool canAttack = false; bool enabled = true; - GiftData gift; float interactionRadius = 0.0f; }; - struct LoadedGameObject { - std::shared_ptr texture; - VertexRenderStruct mesh; - std::string name; - }; - class GameObjectLoader { public: - static std::vector loadFromJson(const std::string& jsonPath, const std::string& zipPath = ""); - static std::unordered_map loadAndCreateGameObjects( const std::string& jsonPath, Renderer& renderer, @@ -98,7 +81,12 @@ namespace ZL { ); static std::vector loadNpcsFromJson(const std::string& jsonPath, const std::string& zipPath = ""); + + private: + static nlohmann::json loadJson(const std::string& jsonPath, const std::string& zipPath); + static std::vector loadFromJson(const std::string& jsonPath, const std::string& zipPath); + static std::vector loadInteractiveFromJson(const std::string& jsonPath, const std::string& zipPath); + static LoadedGameObject buildLoadedObject(const GameObjectData& data, Renderer& renderer, const std::string& zipPath); }; } // namespace ZL - diff --git a/src/items/InteractiveObject.cpp b/src/items/InteractiveObject.cpp index 77be231..3f0a096 100644 --- a/src/items/InteractiveObject.cpp +++ b/src/items/InteractiveObject.cpp @@ -11,26 +11,12 @@ namespace ZL { namespace ZL { void InteractiveObject::draw(Renderer& renderer) const { - if (!isActive || !texture) return; - /* - std::cout << "[DRAW] InteractiveObject::draw() called" << std::endl; - std::cout << "[DRAW] Object: " << name << std::endl; - std::cout << "[DRAW] Position: (" << position.x() << ", " << position.y() << ", " << position.z() << ")" << std::endl; - - // Check mesh bounds - if (!mesh.data.PositionData.empty()) { - std::cout << "[DRAW] First vertex: (" << mesh.data.PositionData[0].x() << ", " - << mesh.data.PositionData[0].y() << ", " << mesh.data.PositionData[0].z() << ")" << std::endl; - } - */ - // Apply position transformation + if (!isActive || !loadedObject.texture) return; renderer.PushMatrix(); renderer.TranslateMatrix(position); - renderer.RenderUniform1i(textureUniformName, 0); - glBindTexture(GL_TEXTURE_2D, texture->getTexID()); - renderer.DrawVertexRenderStruct(mesh); - + glBindTexture(GL_TEXTURE_2D, loadedObject.texture->getTexID()); + renderer.DrawVertexRenderStruct(loadedObject.mesh); renderer.PopMatrix(); } diff --git a/src/items/InteractiveObject.h b/src/items/InteractiveObject.h index f3a46e6..54aa295 100644 --- a/src/items/InteractiveObject.h +++ b/src/items/InteractiveObject.h @@ -2,43 +2,28 @@ #include #include #include -#include "Item.h" -#include "render/Renderer.h" +#include "render/Renderer.h" namespace ZL { - struct Texture; - struct VertexRenderStruct; class Renderer; - struct InteractiveObject { - std::string id; - std::string name; - Eigen::Vector3f position; - float interactionRadius; + struct LoadedGameObject { std::shared_ptr texture; VertexRenderStruct mesh; - Item dropItem; + std::string name; + }; + + struct InteractiveObject { + LoadedGameObject loadedObject; // name, texture, mesh + Eigen::Vector3f position; + float interactionRadius; bool isActive = true; - - bool isNpc = false; - std::string npcInteractCallback; - float walkSpeed = 1.5f; - float rotationSpeed = 8.0f; - float modelScale = 0.01f; - std::string animationIdlePath; - std::string animationWalkPath; - std::string texturePath; - std::string activateFunctionName; InteractiveObject() : interactionRadius(2.0f) {} - bool isInRange(const Eigen::Vector3f& playerPos) const { - return (playerPos - position).norm() <= interactionRadius; - } - void draw(Renderer& renderer) const; }; -} // namespace ZL \ No newline at end of file +} // namespace ZL diff --git a/src/items/ItemRegistry.cpp b/src/items/ItemRegistry.cpp new file mode 100644 index 0000000..ad71600 --- /dev/null +++ b/src/items/ItemRegistry.cpp @@ -0,0 +1,68 @@ +#include "ItemRegistry.h" +#include "utils/Utils.h" +#include "external/nlohmann/json.hpp" +#include + +namespace ZL { + +ItemRegistry& ItemRegistry::instance() { + static ItemRegistry reg; + return reg; +} + +void ItemRegistry::loadFromJson(const std::string& jsonPath, const std::string& zipPath) { + std::string content; + try { + if (zipPath.empty()) { + content = readTextFile(jsonPath); + } else { + auto buf = readFileFromZIP(jsonPath, zipPath); + if (buf.empty()) { + std::cerr << "[ItemRegistry] Failed to read " << jsonPath << " from zip\n"; + return; + } + content.assign(buf.begin(), buf.end()); + } + } catch (const std::exception& e) { + std::cerr << "[ItemRegistry] Failed to open " << jsonPath << ": " << e.what() << "\n"; + return; + } + + using json = nlohmann::json; + json j; + try { + j = json::parse(content); + } catch (const std::exception& e) { + std::cerr << "[ItemRegistry] JSON parse error in " << jsonPath << ": " << e.what() << "\n"; + return; + } + + if (!j.contains("items") || !j["items"].is_array()) { + std::cerr << "[ItemRegistry] No 'items' array in " << jsonPath << "\n"; + return; + } + + items_.clear(); + + for (const auto& entry : j["items"]) { + std::string id = entry.value("id", ""); + if (id.empty()) continue; + + Item item( + id, + entry.value("name", "Unknown"), + entry.value("description", ""), + entry.value("icon", "") + ); + items_[id] = std::move(item); + } + + std::cout << "[ItemRegistry] Loaded " << items_.size() << " items from " << jsonPath << "\n"; +} + +const Item* ItemRegistry::findById(const std::string& id) const { + auto it = items_.find(id); + return it != items_.end() ? &it->second : nullptr; +} + +} // namespace ZL diff --git a/src/items/ItemRegistry.h b/src/items/ItemRegistry.h new file mode 100644 index 0000000..4401743 --- /dev/null +++ b/src/items/ItemRegistry.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include "Item.h" + +namespace ZL { + +class ItemRegistry { +public: + static ItemRegistry& instance(); + + void loadFromJson(const std::string& jsonPath, const std::string& zipPath = ""); + + // Returns nullptr if not found. + const Item* findById(const std::string& id) const; + +private: + ItemRegistry() = default; + + std::unordered_map items_; +}; + +} // namespace ZL