diff --git a/resources/config2/npcs.json b/resources/config2/npcs.json index 1866615..a63cbb5 100644 --- a/resources/config2/npcs.json +++ b/resources/config2/npcs.json @@ -14,7 +14,13 @@ "modelScale": 0.01, "modelCorrectionRotX": 0.0, "modelCorrectionRotY": 180.0, - "modelCorrectionRotZ": 0.0 + "modelCorrectionRotZ": 0.0, + "gift": { + "id": "guard_token", + "name": "Guard's Token", + "description": "A token from the Guard - sign of respect", + "icon": "resources/fire2.png" + } }, { "id": "npc_02_ghost", @@ -30,7 +36,13 @@ "modelScale": 0.01, "modelCorrectionRotX": 0.0, "modelCorrectionRotY": 180.0, - "modelCorrectionRotZ": 0.0 + "modelCorrectionRotZ": 0.0, + "gift": { + "id": "ghost_essence", + "name": "Ghost's Essence", + "description": "A mysterious essence from the Ghost realm", + "icon": "resources/fire2.png" + } } ] } \ No newline at end of file diff --git a/resources/start.lua b/resources/start.lua index 14b93d7..6135227 100644 --- a/resources/start.lua +++ b/resources/start.lua @@ -26,3 +26,13 @@ function on_health_pickup() end +-- ============================================ +-- NPC INTERACTION HANDLER +-- ============================================ + +function on_npc_interact(npc_index) + print("[Lua] NPC interaction! Index: " .. tostring(npc_index)) + game_api.receive_npc_gift(npc_index) +end + +print("Lua script loaded successfully!") \ No newline at end of file diff --git a/src/Character.h b/src/Character.h index f82a51b..6dd1f70 100644 --- a/src/Character.h +++ b/src/Character.h @@ -2,6 +2,7 @@ #include "BoneAnimatedModel.h" #include "render/Renderer.h" #include "render/TextureManager.h" +#include "items/Item.h" #include #include #include @@ -18,7 +19,7 @@ enum class AnimationState { class Character { public: void loadAnimation(AnimationState state, const std::string& filename, const std::string& zipFile = ""); - // void setTexture(std::shared_ptr texture); + // void setTexture(std::shared_ptr texture); void setTexture(std::shared_ptr texture) { this->texture = texture; } @@ -40,6 +41,12 @@ 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; + private: struct AnimationData { BoneSystem model; diff --git a/src/Game.cpp b/src/Game.cpp index 6b1ea68..3aa7486 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -305,6 +305,28 @@ namespace ZL return closestObject; } + + Character* Game::raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance) { + Character* closestNpc = nullptr; + float closestDist = maxDistance; + + for (auto& npc : npcs) { + Eigen::Vector3f toNpc = npc->position - rayOrigin; + float distAlongRay = toNpc.dot(rayDir); + if (distAlongRay < 0.1f) continue; + + Eigen::Vector3f closestPoint = rayOrigin + rayDir * distAlongRay; + float distToNpc = (closestPoint - npc->position).norm(); + + float radius = npc->modelScale * 50.0f; + + if (distToNpc <= radius && distAlongRay < closestDist) { + closestDist = distAlongRay; + closestNpc = npc.get(); + } + } + return closestNpc; + } void Game::drawUI() { @@ -585,24 +607,41 @@ namespace ZL 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] Object position: (" << clickedObject->position.x() << ", " + std::cout << "[CLICK] Object position: (" << clickedObject->position.x() << ", " << clickedObject->position.y() << ", " << clickedObject->position.z() << ")" << std::endl; - std::cout << "[CLICK] Player position: (" << player->position.x() << ", " + std::cout << "[CLICK] Player position: (" << player->position.x() << ", " << player->position.y() << ", " << player->position.z() << ")" << std::endl; - + targetInteractiveObject = clickedObject; player->setTarget(clickedObject->position); std::cout << "[CLICK] Player moving to object..." << std::endl; } - else if (rayDir.y() < -0.001f && player) { - // Otherwise, unproject click to ground plane for Viola's walk target - float t = -camPos.y() / rayDir.y(); - Eigen::Vector3f hit = camPos + rayDir * t; - std::cout << "[CLICK] Clicked on ground at: (" << hit.x() << ", " << hit.z() << ")" << std::endl; - player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z())); - } else { - std::cout << "[CLICK] No valid target found" << std::endl; + // Check if we clicked on an NPC + Character* clickedNpc = raycastNpcs(camPos, rayDir); + if (clickedNpc && player) { + int npcIndex = -1; + for (size_t i = 0; i < npcs.size(); ++i) { + if (npcs[i].get() == clickedNpc) { + npcIndex = static_cast(i); + break; + } + } + if (npcIndex != -1) { + std::cout << "[CLICK] *** SUCCESS: Clicked on NPC index: " << npcIndex << " ***" << std::endl; + scriptEngine.callNpcInteractCallback(npcIndex); + } + } + else if (rayDir.y() < -0.001f && player) { + // Otherwise, unproject click to ground plane for Viola's walk target + float t = -camPos.y() / rayDir.y(); + Eigen::Vector3f hit = camPos + rayDir * t; + std::cout << "[CLICK] Clicked on ground at: (" << hit.x() << ", " << hit.z() << ")" << std::endl; + player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z())); + } + else { + std::cout << "[CLICK] No valid target found" << std::endl; + } } std::cout << "========================================\n" << std::endl; } else { diff --git a/src/Game.h b/src/Game.h index 539bf02..75edbce 100644 --- a/src/Game.h +++ b/src/Game.h @@ -98,6 +98,7 @@ namespace ZL { void handleUp(int64_t fingerId, int mx, int my); void handleMotion(int64_t fingerId, int mx, int my); InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir); + Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f); #ifdef EMSCRIPTEN static Game* s_instance; diff --git a/src/ScriptEngine.cpp b/src/ScriptEngine.cpp index 6d2765b..d206d84 100644 --- a/src/ScriptEngine.cpp +++ b/src/ScriptEngine.cpp @@ -138,9 +138,64 @@ namespace ZL { return game->inventory.hasItem(id); }); + // receive_npc_gift(npc_index) + api.set_function("receive_npc_gift", [game](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; + } + + game->inventory.addItem(npc->giftItem); + npc->giftReceived = true; + + std::cout << "[script] Received gift from " << npc->npcName << ": " + << npc->giftItem.name << std::endl; + }); + lua.script_file("resources/start.lua"); } + void ScriptEngine::callNpcInteractCallback(int npcIndex) { + if (!impl) { + std::cerr << "[SCRIPT] Engine not initialized!" << std::endl; + return; + } + sol::state& lua = impl->lua; + sol::function fn = lua["on_npc_interact"]; + if (fn.valid()) { + auto result = fn(npcIndex); + if (!result.valid()) { + sol::error err = result; + std::cerr << "[SCRIPT] on_npc_interact error: " << err.what() << "\n"; + } + else { + std::cout << "[SCRIPT] on_npc_interact called with index " << npcIndex << std::endl; + } + } + else { + std::cerr << "[SCRIPT] Lua function 'on_npc_interact' not found!" << std::endl; + } + } + void ScriptEngine::runScript(const std::string& path) { auto result = impl->lua.safe_script_file(path, sol::script_pass_on_error); if (!result.valid()) { diff --git a/src/ScriptEngine.h b/src/ScriptEngine.h index 1709b79..d5d08bb 100644 --- a/src/ScriptEngine.h +++ b/src/ScriptEngine.h @@ -22,6 +22,7 @@ public: void callActivateFunction(const std::string& functionName); void showInventory(Game* game); + void callNpcInteractCallback(int npcIndex); private: struct Impl; diff --git a/src/items/GameObjectLoader.cpp b/src/items/GameObjectLoader.cpp index 2b3bf85..151c090 100644 --- a/src/items/GameObjectLoader.cpp +++ b/src/items/GameObjectLoader.cpp @@ -296,6 +296,15 @@ namespace ZL { 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; + // 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); } @@ -356,7 +365,16 @@ namespace ZL { npc->modelScale = npcData.modelScale; npc->position = Eigen::Vector3f(npcData.positionX, npcData.positionY, npcData.positionZ); - // Set model correction rotation + // Set NPC metadata + npc->npcId = npcData.id; + npc->npcName = npcData.name; + + // 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; diff --git a/src/items/GameObjectLoader.h b/src/items/GameObjectLoader.h index fa229d7..575dd00 100644 --- a/src/items/GameObjectLoader.h +++ b/src/items/GameObjectLoader.h @@ -34,6 +34,13 @@ namespace ZL { std::string activateFunctionName; }; + struct GiftData { + std::string id; + std::string name; + std::string description; + std::string icon; + }; + struct NpcData { std::string id; std::string name; @@ -49,6 +56,7 @@ namespace ZL { float modelCorrectionRotX = 0.0f; float modelCorrectionRotY = 0.0f; float modelCorrectionRotZ = 0.0f; + GiftData gift; }; struct LoadedGameObject {