Major refactoring for game objects

This commit is contained in:
Vladislav Khorev 2026-05-12 21:19:29 +03:00
parent efb85f7328
commit 2cae9998a0
18 changed files with 547 additions and 716 deletions

View File

@ -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
}
]
}
}

View File

@ -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
}
]
}
}

View File

@ -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"
}
]
}

View File

@ -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"
}
]
}

View File

@ -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"
}
]
}

View File

@ -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

View File

@ -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

View File

@ -2,7 +2,6 @@
#include "BoneAnimatedModelNew.h"
#include "render/Renderer.h"
#include "render/TextureManager.h"
#include "items/Item.h"
#include "SparkEmitter.h"
#include <functional>
#include <memory>
@ -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.

View File

@ -1,6 +1,7 @@
#include "Game.h"
#include "AnimatedModel.h"
#include "utils/Utils.h"
#include "items/ItemRegistry.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
#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<Location>(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<Location>(renderer, inventory);

View File

@ -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<Texture>(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() << ", "

View File

@ -32,6 +32,7 @@ namespace ZL
struct LocationSetup
{
std::string gameObjectsJsonPath;
std::string interactiveObjectsJsonPath;
std::string npcsJsonPath;
std::string dialoguesJsonPath;
std::vector<std::string> navigationJsonPaths;

View File

@ -4,6 +4,7 @@
#include <stdexcept>
#include <unordered_map>
#include "Location.h"
#include "items/ItemRegistry.h"
#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>
@ -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<int>(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);
}

View File

@ -7,6 +7,7 @@
#include "utils/Utils.h"
#include <Eigen/Geometry>
#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<GameObjectData> 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<GameObjectData> 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<GameObjectData> GameObjectLoader::loadFromJson(const std::string& jsonPath, const std::string& zipPath)
{
std::vector<GameObjectData> 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<InteractiveObjectData> GameObjectLoader::loadInteractiveFromJson(const std::string& jsonPath, const std::string& zipPath)
{
std::vector<InteractiveObjectData> 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<std::string, LoadedGameObject> GameObjectLoader::loadAndCreateGameObjects(
const std::string& jsonPath,
Renderer& renderer,
const std::string& zipPath)
{
std::unordered_map<std::string, LoadedGameObject> gameObjects;
std::vector<GameObjectData> 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<InteractiveObject> interactiveObjects;
std::vector<GameObjectData> 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<NpcData> GameObjectLoader::loadNpcsFromJson(const std::string& jsonPath, const std::string& zipPath)
{
std::vector<NpcData> 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<std::string>();
}
}
}
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<float>(M_PI) / 180.0f;
data.modelCorrectionRotY = item.value("modelCorrectionRotY", 0.0f) * static_cast<float>(M_PI) / 180.0f;
data.modelCorrectionRotZ = item.value("modelCorrectionRotZ", 0.0f) * static_cast<float>(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<std::unique_ptr<Character>> npcs;
std::vector<NpcData> 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<Character>();
// 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<std::string, std::shared_ptr<Texture>> 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<Texture>(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<Texture>(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<Texture>(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
} // namespace ZL

View File

@ -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> texture;
VertexRenderStruct mesh;
std::string name;
};
class GameObjectLoader {
public:
static std::vector<GameObjectData> loadFromJson(const std::string& jsonPath, const std::string& zipPath = "");
static std::unordered_map<std::string, LoadedGameObject> loadAndCreateGameObjects(
const std::string& jsonPath,
Renderer& renderer,
@ -98,7 +81,12 @@ namespace ZL {
);
static std::vector<NpcData> 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<GameObjectData> loadFromJson(const std::string& jsonPath, const std::string& zipPath);
static std::vector<InteractiveObjectData> loadInteractiveFromJson(const std::string& jsonPath, const std::string& zipPath);
static LoadedGameObject buildLoadedObject(const GameObjectData& data, Renderer& renderer, const std::string& zipPath);
};
} // namespace ZL

View File

@ -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();
}

View File

@ -2,43 +2,28 @@
#include <string>
#include <memory>
#include <Eigen/Core>
#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> 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
} // namespace ZL

View File

@ -0,0 +1,68 @@
#include "ItemRegistry.h"
#include "utils/Utils.h"
#include "external/nlohmann/json.hpp"
#include <iostream>
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

23
src/items/ItemRegistry.h Normal file
View File

@ -0,0 +1,23 @@
#pragma once
#include <string>
#include <unordered_map>
#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<std::string, Item> items_;
};
} // namespace ZL