From b01e9705547f04faec91d85f539420c15fa1a672 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Thu, 18 Jun 2026 21:48:27 +0300 Subject: [PATCH] Working on state for location --- proj-web/CMakeLists.txt | 2 + proj-windows/CMakeLists.txt | 2 + src/CharacterState.cpp | 57 +++++++ src/CharacterState.h | 5 + src/Game.cpp | 34 ++-- src/Location.cpp | 251 ++++++++++++++++++++++------- src/Location.h | 44 +++-- src/LocationState.cpp | 52 ++++++ src/LocationState.h | 40 +++++ src/ScriptEngine.cpp | 67 +++++++- src/ScriptEngine.h | 9 ++ src/items/InteractiveObject.cpp | 27 ++++ src/items/InteractiveObjectState.h | 5 + 13 files changed, 494 insertions(+), 101 deletions(-) create mode 100644 src/LocationState.cpp create mode 100644 src/LocationState.h diff --git a/proj-web/CMakeLists.txt b/proj-web/CMakeLists.txt index 6ed480f..fbe8cb7 100644 --- a/proj-web/CMakeLists.txt +++ b/proj-web/CMakeLists.txt @@ -103,6 +103,8 @@ set(SOURCES ../src/MenuManager.cpp ../src/Location.h ../src/Location.cpp + ../src/LocationState.h + ../src/LocationState.cpp ../src/LocationEditor.h ../src/LocationEditor.cpp ../src/GameConstants.h diff --git a/proj-windows/CMakeLists.txt b/proj-windows/CMakeLists.txt index 3fae3ab..51ee49d 100644 --- a/proj-windows/CMakeLists.txt +++ b/proj-windows/CMakeLists.txt @@ -58,6 +58,8 @@ add_executable(witcher001 ../src/MenuManager.cpp ../src/Location.h ../src/Location.cpp + ../src/LocationState.h + ../src/LocationState.cpp ../src/LocationEditor.h ../src/LocationEditor.cpp ../src/GameConstants.h diff --git a/src/CharacterState.cpp b/src/CharacterState.cpp index a4f6f6a..d3c8de3 100644 --- a/src/CharacterState.cpp +++ b/src/CharacterState.cpp @@ -29,4 +29,61 @@ void CharacterState::setHp(float newHp) { if (onHpChanged) onHpChanged(hp, initialHp); } +void CharacterState::save(nlohmann::json& out) const +{ + out["position"] = { position.x(), position.y(), position.z() }; + out["facingAngle"] = facingAngle; + out["targetFacingAngle"] = targetFacingAngle; + out["hp"] = hp; + out["enabled"] = enabled; + out["battle_state"] = battle_state; + out["currentState"] = static_cast(currentState); + out["attack_cooldown"] = attack_cooldown; + out["showWeapon"] = showWeapon; + out["attackTargetIndex"] = attackTargetIndex; + out["faceTargetIndex"] = faceTargetIndex; + out["homePosition"] = { homePosition.x(), homePosition.y(), homePosition.z() }; + out["walkTarget"] = { walkTarget.x(), walkTarget.y(), walkTarget.z() }; + out["requestedWalkTarget"] = { requestedWalkTarget.x(), requestedWalkTarget.y(), requestedWalkTarget.z() }; + nlohmann::json wpArr = nlohmann::json::array(); + for (const auto& wp : pathWaypoints) { + wpArr.push_back({ wp.x(), wp.y(), wp.z() }); + } + out["pathWaypoints"] = std::move(wpArr); + out["currentWaypointIndex"] = currentWaypointIndex; + out["homeDriftCheckTimer"] = homeDriftCheckTimer; +} + +void CharacterState::load(const nlohmann::json& in) +{ + auto readVec3 = [](const nlohmann::json& j, Eigen::Vector3f def) -> Eigen::Vector3f { + if (j.is_array() && j.size() == 3) + return { j[0].get(), j[1].get(), j[2].get() }; + return def; + }; + + if (in.contains("position")) position = readVec3(in["position"], position); + facingAngle = in.value("facingAngle", facingAngle); + targetFacingAngle = in.value("targetFacingAngle", targetFacingAngle); + hp = in.value("hp", hp); + enabled = in.value("enabled", enabled); + battle_state = in.value("battle_state", battle_state); + currentState = static_cast(in.value("currentState", static_cast(currentState))); + attack_cooldown = in.value("attack_cooldown", attack_cooldown); + showWeapon = in.value("showWeapon", showWeapon); + attackTargetIndex = in.value("attackTargetIndex", attackTargetIndex); + faceTargetIndex = in.value("faceTargetIndex", faceTargetIndex); + if (in.contains("homePosition")) homePosition = readVec3(in["homePosition"], homePosition); + if (in.contains("walkTarget")) walkTarget = readVec3(in["walkTarget"], walkTarget); + if (in.contains("requestedWalkTarget")) requestedWalkTarget = readVec3(in["requestedWalkTarget"], requestedWalkTarget); + pathWaypoints.clear(); + if (in.contains("pathWaypoints") && in["pathWaypoints"].is_array()) { + for (const auto& wp : in["pathWaypoints"]) { + pathWaypoints.push_back(readVec3(wp, Eigen::Vector3f::Zero())); + } + } + currentWaypointIndex = in.value("currentWaypointIndex", currentWaypointIndex); + homeDriftCheckTimer = in.value("homeDriftCheckTimer", homeDriftCheckTimer); +} + } // namespace ZL diff --git a/src/CharacterState.h b/src/CharacterState.h index edbf08b..33a346a 100644 --- a/src/CharacterState.h +++ b/src/CharacterState.h @@ -5,6 +5,7 @@ #include #include #include +#include "external/nlohmann/json.hpp" namespace ZL { @@ -124,6 +125,10 @@ public: void stopInPlace(); float getHp() const { return hp; } void setHp(float newHp); + + // --- Serialisation (runtime mutable fields only; creation info is reloaded from config) --- + void save(nlohmann::json& out) const; + void load(const nlohmann::json& in); }; } // namespace ZL diff --git a/src/Game.cpp b/src/Game.cpp index e8e868b..178b8b0 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -422,13 +422,13 @@ namespace ZL menuManager.tutorialShowTaxiHint(); }; - locations["location_dorm"]->tutorialInteractiveObjectsLocked = true; + locations["location_dorm"]->state.tutorialInteractiveObjectsLocked = true; menuManager.tutorialUnlockInteractiveObjectsFunc = [this]() { if (locations["location_dorm"]) { - locations["location_dorm"]->tutorialInteractiveObjectsLocked = false; + locations["location_dorm"]->state.tutorialInteractiveObjectsLocked = false; } }; @@ -466,10 +466,10 @@ namespace ZL currentLocation->player->state.facingAngle = destRotY; currentLocation->player->state.targetFacingAngle = destRotY; } - currentLocation->cameraAzimuth = destRotY; - currentLocation->isDarklands = isDarklands; - currentLocation->isNight = menuManager.isNight; - currentLocation->isDawn = menuManager.isDawn; + currentLocation->state.cameraAzimuth = destRotY; + currentLocation->state.isDarklands = isDarklands; + currentLocation->state.isNight = menuManager.isNight; + currentLocation->state.isDawn = menuManager.isDawn; currentLocation->scriptEngine.callLocationEnterCallback(); @@ -638,9 +638,9 @@ namespace ZL if (currentLocation) { // Sync global flags so Location's draw functions see them. - currentLocation->isDarklands = isDarklands; - currentLocation->isNight = menuManager.isNight; - currentLocation->isDawn = menuManager.isDawn; + currentLocation->state.isDarklands = isDarklands; + currentLocation->state.isNight = menuManager.isNight; + currentLocation->state.isDawn = menuManager.isDawn; if (isDarklands) { currentLocation->drawGameDarklands(); @@ -1071,8 +1071,8 @@ namespace ZL //x = x - 1; //std::cout << "current x: " << x << std::endl; - std::cout << "Azimuth: " << currentLocation->cameraAzimuth << std::endl; - std::cout << "Inclination: " << currentLocation->cameraInclination << std::endl; + std::cout << "Azimuth: " << currentLocation->state.cameraAzimuth << std::endl; + std::cout << "Inclination: " << currentLocation->state.cameraInclination << std::endl; break; case SDLK_c: @@ -1177,8 +1177,8 @@ namespace ZL currentLocation->lastMouseY = eventY; // Snapshot current angles so we can measure how far the user rotates. - dragStartAzimuth = currentLocation->cameraAzimuth; - dragStartInclination = currentLocation->cameraInclination; + dragStartAzimuth = currentLocation->state.cameraAzimuth; + dragStartInclination = currentLocation->state.cameraInclination; } } @@ -1383,8 +1383,8 @@ namespace ZL static constexpr float TUTORIAL_ROTATION_THRESHOLD = 0.15f; if (cameraDragging && menuManager.tutorialStep == TutorialStep::Step1) { - float deltaAz = std::abs(currentLocation->cameraAzimuth - dragStartAzimuth); - float deltaInc = std::abs(currentLocation->cameraInclination - dragStartInclination); + float deltaAz = std::abs(currentLocation->state.cameraAzimuth - dragStartAzimuth); + float deltaInc = std::abs(currentLocation->state.cameraInclination - dragStartInclination); if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) { menuManager.advanceTutorialStep(); } @@ -1438,7 +1438,7 @@ namespace ZL if (currentLocation) { //currentLocation->dialogueSystem.startDialogue("dialog_video001"); - currentLocation->isNight = menuManager.isNight; + currentLocation->state.isNight = menuManager.isNight; currentLocation->scriptEngine.callTriggerNightEnterCallback(); } } else { @@ -1448,7 +1448,7 @@ namespace ZL menuManager.setDarklandsMode(isDarklands); if (currentLocation) { - currentLocation->isDarklands = isDarklands; + currentLocation->state.isDarklands = isDarklands; if (isDarklands) currentLocation->scriptEngine.callDarklandsEnterCallback(); else diff --git a/src/Location.cpp b/src/Location.cpp index 75e8d89..e89fa36 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -1,4 +1,4 @@ -#include "Location.h" +#include "Location.h" #include "utils/Utils.h" #include "render/OpenGlExtensions.h" #include @@ -49,6 +49,40 @@ namespace ZL { } + // ---- Pointer-to-index helpers ---- + + InteractiveObject* Location::getTargetInteractiveObject() + { + const int idx = state.targetInteractiveObjectIndex; + if (idx < 0 || idx >= static_cast(interactiveObjects.size())) return nullptr; + return &interactiveObjects[idx]; + } + + TeleportZone* Location::getTargetTeleportZone() + { + const int idx = state.targetTeleportZoneIndex; + if (idx < 0 || idx >= static_cast(teleportZones.size())) return nullptr; + return &teleportZones[idx]; + } + + int Location::findInteractiveObjectIndex(const InteractiveObject* obj) const + { + for (int i = 0; i < static_cast(interactiveObjects.size()); ++i) { + if (&interactiveObjects[i] == obj) return i; + } + return -1; + } + + int Location::findTeleportZoneIndex(const TeleportZone* tz) const + { + for (int i = 0; i < static_cast(teleportZones.size()); ++i) { + if (&teleportZones[i] == tz) return i; + } + return -1; + } + + // ---- Setup ---- + void Location::setup(const LocationSetup& params, Quest::QuestJournal* journal) { @@ -68,7 +102,7 @@ namespace ZL player->loadBinaryAnimation(AnimationState::STAND, "resources/w/gg/new2/gg_stand_idle001_small.anim", CONST_ZIP_FILE); player->loadBinaryAnimation(AnimationState::WALK, "resources/w/gg/new2/gg_walk001_small.anim", CONST_ZIP_FILE); player->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/gg/new2/gg_stand_to_action001_small.anim", CONST_ZIP_FILE); - + player->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/gg/new2/gg_action_chop001_small.anim", CONST_ZIP_FILE); player->loadBinaryAnimation(AnimationState::ACTION_ATTACK_2, "resources/w/gg/new2/gg_action_stab001_small.anim", CONST_ZIP_FILE); player->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/gg/new2/gg_action_idle002_small.anim", CONST_ZIP_FILE); @@ -278,6 +312,9 @@ namespace ZL triggerZones.push_back(std::move(tz)); } + // Resize the parallel playerInside vector in state to match + state.triggerZonePlayerInside.assign(triggerZones.size(), false); + std::cout << "[TRIGGER] Loaded " << triggerZones.size() << " trigger zone(s) from " << jsonPath << std::endl; } @@ -345,14 +382,17 @@ namespace ZL void Location::updateTriggerZones(const Eigen::Vector3f& playerPos) { - for (auto& tz : triggerZones) { + for (int i = 0; i < static_cast(triggerZones.size()); ++i) { + TriggerZone& tz = triggerZones[i]; if (!tz.enabled) continue; const float dist = (playerPos - tz.position).norm(); if (!tz.playerInside && dist <= tz.radius) { tz.playerInside = true; + state.triggerZonePlayerInside[i] = true; scriptEngine.callTriggerEnterCallback(tz.id); } else if (tz.playerInside && dist > tz.radius + tz.hysteresis) { tz.playerInside = false; + state.triggerZonePlayerInside[i] = false; scriptEngine.callTriggerExitCallback(tz.id); } } @@ -368,7 +408,7 @@ namespace ZL navigationMaps[i].build(paths[i], CONST_ZIP_FILE); } - activeNavigationIndex = 0; + state.activeNavigationIndex = 0; navigation = navigationMaps.empty() ? nullptr : &navigationMaps[0]; if (editorMode == EditorMode::Navigation && navigation) { @@ -424,7 +464,7 @@ namespace ZL std::cerr << "[NAV] switchNavigation: index " << index << " out of range\n"; return false; } - activeNavigationIndex = index; + state.activeNavigationIndex = index; navigation = &navigationMaps[index]; // Force all characters to replan their paths against the new nav map. @@ -461,7 +501,7 @@ namespace ZL continue; } - if (isDarklands) { + if (state.isDarklands) { if (!intObj.loadedObject.textureDarklands) continue; } else { if (!intObj.loadedObject.texture) continue; @@ -615,8 +655,8 @@ namespace ZL renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); //renderer.TranslateMatrix({ 0, -6.f, 0 }); - renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); - renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(state.cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(state.cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero(); renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); renderer.TranslateMatrix({ 0, -1.f, 0 }); @@ -785,9 +825,9 @@ namespace ZL renderer.LoadIdentity(); renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); - - renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); - renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); + + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(state.cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(state.cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero(); renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); renderer.TranslateMatrix({ 0, -1.f, 0 }); @@ -897,8 +937,8 @@ namespace ZL renderer.LoadIdentity(); renderer.TranslateMatrix({ 0, 0, -1.0f * Environment::zoom }); - renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); - renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(state.cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(state.cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero(); renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); renderer.TranslateMatrix({ 0, -1.f, 0 }); @@ -1044,8 +1084,8 @@ namespace ZL static const float kDawnAmbient[3] = { 0.72f, 0.52f, 0.62f }; // brighter warm pink static const float kNightFogColor[3] = { 0.01f, 0.01f, 0.05f }; static const float kDawnFogColor[3] = { 0.50f, 0.44f, 0.47f }; // grey with slight pink - const float* ambientColor = isDawn ? kDawnAmbient : kNightAmbient; - const float* fogColor = isDawn ? kDawnFogColor : kNightFogColor; + const float* ambientColor = state.isDawn ? kDawnAmbient : kNightAmbient; + const float* fogColor = state.isDawn ? kDawnFogColor : kNightFogColor; if (hasShadows) { drawNightShadowDepthPass(*shadowLight); @@ -1079,8 +1119,8 @@ namespace ZL renderer.LoadIdentity(); renderer.TranslateMatrix({ 0, 0, -1.0f * Environment::zoom }); - renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); - renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(state.cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(state.cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero(); renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); renderer.TranslateMatrix({ 0, -1.f, 0 }); @@ -1492,63 +1532,65 @@ namespace ZL for (auto& [idx, t] : npcBumpsPlayerCooldown) t -= deltaS; // Check if player reached target interactive object - if (!tutorialInteractiveObjectsLocked && targetInteractiveObject && player && !targetInteractiveObject->state.isAnimating) { - const Eigen::Vector3f& approachTarget = targetInteractiveObject->state.hasInteractionPosition - ? targetInteractiveObject->state.interactionPosition - : targetInteractiveObject->state.position; + if (auto* obj = getTargetInteractiveObject(); + !state.tutorialInteractiveObjectsLocked && obj && player && !obj->state.isAnimating) + { + const Eigen::Vector3f& approachTarget = obj->state.hasInteractionPosition + ? obj->state.interactionPosition + : obj->state.position; float distToObject = (player->state.position - approachTarget).norm(); // If player is close enough to pick up the item - if (distToObject <= targetInteractiveObject->state.approachRadius) { + if (distToObject <= obj->state.approachRadius) { std::cout << "[PICKUP] Player reached object! Distance: " << distToObject << std::endl; - std::cout << "[PICKUP] Calling Lua callback for: " << targetInteractiveObject->loadedObject.name << std::endl; + std::cout << "[PICKUP] Calling Lua callback for: " << obj->loadedObject.name << std::endl; // Call custom activate function if specified, otherwise use fallback try { - if (!targetInteractiveObject->state.activateFunctionName.empty()) { - std::cout << "[PICKUP] Using custom function: " << targetInteractiveObject->state.activateFunctionName << std::endl; - scriptEngine.callActivateFunction(targetInteractiveObject->state.activateFunctionName); + if (!obj->state.activateFunctionName.empty()) { + std::cout << "[PICKUP] Using custom function: " << obj->state.activateFunctionName << std::endl; + scriptEngine.callActivateFunction(obj->state.activateFunctionName); } else { std::cout << "[PICKUP] Using fallback callback" << std::endl; - scriptEngine.callItemPickupCallback(targetInteractiveObject->loadedObject.name); + scriptEngine.callItemPickupCallback(obj->loadedObject.name); } } catch (const std::exception& e) { std::cerr << "[PICKUP] Error calling function: " << e.what() << std::endl; } - targetInteractiveObject = nullptr; + state.targetInteractiveObjectIndex = -1; } } // Check if player reached target NPC for interaction. - if (targetInteractNpcIndex >= 0 && player) { - float distToNpc = (player->state.position - npcs[targetInteractNpcIndex]->state.position).norm(); + if (state.targetInteractNpcIndex >= 0 && player) { + float distToNpc = (player->state.position - npcs[state.targetInteractNpcIndex]->state.position).norm(); if (distToNpc <= NPC_TALK_DISTANCE) { - std::cout << "[NPC] Player reached NPC index " << targetInteractNpcIndex + std::cout << "[NPC] Player reached NPC index " << state.targetInteractNpcIndex << " (distance " << distToNpc << "); firing on_npc_interact" << std::endl; // Stop the player at the talk distance and have the NPC turn to face them. player->setTarget(player->state.position); - npcs[targetInteractNpcIndex]->state.faceTargetIndex = CharacterState::kPlayerIndex; + npcs[state.targetInteractNpcIndex]->state.faceTargetIndex = CharacterState::kPlayerIndex; try { - scriptEngine.callNpcInteractCallback(targetInteractNpcIndex); + scriptEngine.callNpcInteractCallback(state.targetInteractNpcIndex); } catch (const std::exception& e) { std::cerr << "[NPC] callback error: " << e.what() << std::endl; } - targetInteractNpcIndex = -1; + state.targetInteractNpcIndex = -1; } } for (auto& tz : teleportZones) tz.update(static_cast(delta)); - if (targetTeleportZone && player) { - float dist = (player->state.position - targetTeleportZone->position).norm(); - if (dist <= targetTeleportZone->radius) { - std::cout << "[TELEPORT] Player reached teleport zone '" << targetTeleportZone->id << "'" << std::endl; - if (onTeleport) onTeleport(targetTeleportZone->destinationLocation, targetTeleportZone->destinationPosition, targetTeleportZone->destinationRotationY); - targetTeleportZone = nullptr; + if (auto* tz = getTargetTeleportZone(); tz && player) { + float dist = (player->state.position - tz->position).norm(); + if (dist <= tz->radius) { + std::cout << "[TELEPORT] Player reached teleport zone '" << tz->id << "'" << std::endl; + if (onTeleport) onTeleport(tz->destinationLocation, tz->destinationPosition, tz->destinationRotationY); + state.targetTeleportZoneIndex = -1; return; } } @@ -1596,8 +1638,8 @@ namespace ZL float aspect = (float)Environment::width / (float)Environment::height; float tanHalfFov = tan(CAMERA_FOV_Y * 0.5f); - float cosAzim = cos(cameraAzimuth), sinAzim = sin(cameraAzimuth); - float cosIncl = cos(cameraInclination), sinIncl = sin(cameraInclination); + float cosAzim = cos(state.cameraAzimuth), sinAzim = sin(state.cameraAzimuth); + float cosIncl = cos(state.cameraInclination), sinIncl = sin(state.cameraInclination); Eigen::Vector3f camRight(cosAzim, 0.f, sinAzim); Eigen::Vector3f camForward(sinAzim * cosIncl, -sinIncl, -cosAzim * cosIncl); @@ -1639,9 +1681,9 @@ namespace ZL std::cout << "[CLICK] Player position: (" << player->state.position.x() << ", " << player->state.position.y() << ", " << player->state.position.z() << ")" << std::endl; - targetInteractiveObject = clickedObject; - targetInteractNpcIndex = -1; - targetTeleportZone = nullptr; + state.targetInteractiveObjectIndex = findInteractiveObjectIndex(clickedObject); + state.targetInteractNpcIndex = -1; + state.targetTeleportZoneIndex = -1; player->setTarget(clickedObject->state.hasInteractionPosition ? clickedObject->state.interactionPosition : clickedObject->state.position); @@ -1662,13 +1704,13 @@ namespace ZL } } if (npcIndex != -1) { - targetInteractiveObject = nullptr; - targetTeleportZone = nullptr; + state.targetInteractiveObjectIndex = -1; + state.targetTeleportZoneIndex = -1; if (clickedNpc->state.canAttack) { // Hostile NPC: combat logic walks the player in via attackTargetIndex. player->state.attackTargetIndex = npcIndex; - targetInteractNpcIndex = -1; + state.targetInteractNpcIndex = -1; if (distance <= clickedNpc->state.interactionRadius) { std::cout << "[CLICK] Hostile NPC " << npcIndex << " in range; firing on_npc_interact" << std::endl; @@ -1685,13 +1727,13 @@ namespace ZL player->setTarget(player->state.position); npcs[npcIndex]->state.faceTargetIndex = CharacterState::kPlayerIndex; scriptEngine.callNpcInteractCallback(npcIndex); - targetInteractNpcIndex = -1; + state.targetInteractNpcIndex = -1; } else { std::cout << "[CLICK] NPC " << npcIndex << " out of talk range (distance " << distance << " > " << NPC_TALK_DISTANCE << "); walking to NPC..." << std::endl; player->setTarget(clickedNpc->state.position); - targetInteractNpcIndex = npcIndex; + state.targetInteractNpcIndex = npcIndex; } } } @@ -1712,16 +1754,16 @@ namespace ZL if (clickedTeleport) { std::cout << "[CLICK] Clicked teleport zone '" << clickedTeleport->id << "'" << std::endl; - targetTeleportZone = clickedTeleport; - targetInteractiveObject = nullptr; - targetInteractNpcIndex = -1; + state.targetTeleportZoneIndex = findTeleportZoneIndex(clickedTeleport); + state.targetInteractiveObjectIndex = -1; + state.targetInteractNpcIndex = -1; player->setTarget(clickedTeleport->position); player->state.attackTargetIndex = CharacterState::kNoTarget; } else { player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z())); player->state.attackTargetIndex = CharacterState::kNoTarget; - targetInteractNpcIndex = -1; - targetTeleportZone = nullptr; + state.targetInteractNpcIndex = -1; + state.targetTeleportZoneIndex = -1; if (onPlayerFloorWalk) onPlayerFloorWalk(); } } @@ -1755,12 +1797,12 @@ namespace ZL lastMouseY = eventY; const float sensitivity = 0.005f; - cameraAzimuth += dx * sensitivity; - cameraInclination += dy * sensitivity; + state.cameraAzimuth += dx * sensitivity; + state.cameraInclination += dy * sensitivity; const float minInclination = M_PI * 30.f / 180.f; const float maxInclination = M_PI * 0.5f; - cameraInclination = max(minInclination, min(maxInclination, cameraInclination)); + state.cameraInclination = max(minInclination, min(maxInclination, state.cameraInclination)); } } @@ -1789,5 +1831,98 @@ namespace ZL return dialogueSystem.getFlag(flag); } + // ---- Save / Load ---- + + void Location::saveFullState(nlohmann::json& out) const + { + // Location-level state + nlohmann::json locState; + state.save(locState); + out["locationState"] = std::move(locState); + + // Player character state + if (player) { + nlohmann::json ps; + player->state.save(ps); + out["playerState"] = std::move(ps); + } + + // NPC states + nlohmann::json npcArr = nlohmann::json::array(); + for (const auto& npc : npcs) { + if (npc) { + nlohmann::json ns; + npc->state.save(ns); + npcArr.push_back(std::move(ns)); + } else { + npcArr.push_back(nullptr); + } + } + out["npcStates"] = std::move(npcArr); + + // Interactive object states + nlohmann::json ioArr = nlohmann::json::array(); + for (const auto& intObj : interactiveObjects) { + nlohmann::json ios; + intObj.state.save(ios); + ioArr.push_back(std::move(ios)); + } + out["interactiveObjectStates"] = std::move(ioArr); + + // Lua global variable state + nlohmann::json scriptState; + scriptEngine.saveScriptGlobals(scriptState); + out["scriptGlobals"] = std::move(scriptState); + } + + void Location::loadFullState(const nlohmann::json& in) + { + // Location-level state + if (in.contains("locationState")) { + state.load(in["locationState"]); + } + + // Re-sync navigation pointer from restored index + if (state.activeNavigationIndex >= 0 && + state.activeNavigationIndex < static_cast(navigationMaps.size())) + { + navigation = &navigationMaps[state.activeNavigationIndex]; + } + + // Re-sync triggerZones[i].playerInside from state + for (int i = 0; i < static_cast(triggerZones.size()); ++i) { + if (i < static_cast(state.triggerZonePlayerInside.size())) { + triggerZones[i].playerInside = state.triggerZonePlayerInside[i]; + } + } + + // Player character state + if (player && in.contains("playerState")) { + player->state.load(in["playerState"]); + } + + // NPC states + if (in.contains("npcStates") && in["npcStates"].is_array()) { + const auto& arr = in["npcStates"]; + for (int i = 0; i < static_cast(arr.size()) && i < static_cast(npcs.size()); ++i) { + if (npcs[i] && !arr[i].is_null()) { + npcs[i]->state.load(arr[i]); + } + } + } + + // Interactive object states + if (in.contains("interactiveObjectStates") && in["interactiveObjectStates"].is_array()) { + const auto& arr = in["interactiveObjectStates"]; + for (int i = 0; i < static_cast(arr.size()) && i < static_cast(interactiveObjects.size()); ++i) { + interactiveObjects[i].state.load(arr[i]); + } + } + + // Lua globals + if (in.contains("scriptGlobals")) { + scriptEngine.loadScriptGlobals(in["scriptGlobals"]); + } + } } // namespace ZL diff --git a/src/Location.h b/src/Location.h index a417994..38c1c0b 100644 --- a/src/Location.h +++ b/src/Location.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "render/Renderer.h" #include "Environment.h" @@ -14,6 +14,7 @@ #include "SparkEmitter.h" #include "TeleportZone.h" #include "LocationEditor.h" +#include "LocationState.h" #include #include #include @@ -42,7 +43,7 @@ namespace ZL float radius = 1.5f; float hysteresis = 0.3f; // exit fires at radius + hysteresis to prevent flickering bool enabled = true; - bool playerInside = false; // runtime tracking state, not serialized + bool playerInside = false; // runtime tracking state, not serialized (mirrored in LocationState) }; struct LocationSetup @@ -64,6 +65,9 @@ namespace ZL public: Location(Renderer& iRenderer, Inventory& iInventory); + // ---- All serialisable runtime state ---- + LocationState state; + std::unordered_map gameObjects; std::vector interactiveObjects; @@ -71,21 +75,11 @@ namespace ZL std::unique_ptr player; std::vector> npcs; - float cameraAzimuth = -2.35; - float cameraInclination = 1.1036;//M_PI * 30.f / 180.f; - std::vector navigationMaps; PathFinder* navigation = nullptr; - int activeNavigationIndex = 0; std::unique_ptr shadowMap; Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity(); - InteractiveObject* targetInteractiveObject = nullptr; - - // "Walk to NPC, then fire on_npc_interact" — mirrors targetInteractiveObject - // so a click from outside interactionRadius still leads to a Lua callback - // once the player gets close enough. - int targetInteractNpcIndex = -1; std::unique_ptr npcNameText; @@ -93,21 +87,12 @@ namespace ZL Dialogue::DialogueSystem dialogueSystem; std::vector teleportZones; - TeleportZone* targetTeleportZone = nullptr; std::function onTeleport; std::vector triggerZones; std::vector pointLights; - // Set by Game and kept in sync across location transitions. - // Read by draw functions and raycast — do not write from Location code. - bool isDarklands = false; - bool isNight = false; - bool isDawn = false; - - bool tutorialInteractiveObjectsLocked = false; - // Called when the player successfully taps the ground and a floor walk target is set. // Used by the tutorial system to detect the "tap to walk" gesture. std::function onPlayerFloorWalk; @@ -119,7 +104,7 @@ namespace ZL std::function requestDarklandsTransition; std::function requestNightDayTransition; - + // Set by Game after setup(). Lua calls this to advance the uni_interior HUD // from step12 to step13 (trigger-zone encounter hint). std::function requestAdvanceDarklandsHud; @@ -146,7 +131,7 @@ namespace ZL bool switchNavigation(int index); InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir); Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f); - + void drawGame(); void drawShadowDepthPass(); void drawGameWithShadows(); @@ -168,12 +153,25 @@ namespace ZL void setDialogueFlag(const std::string& flag, int value); int getDialogueFlag(const std::string& flag) const; + // ---- Save / Load ---- + // Serialises LocationState + all Character/InteractiveObject sub-states + Lua globals. + void saveFullState(nlohmann::json& out) const; + // Restores state from JSON written by saveFullState. + // Navigation pointer and triggerZone.playerInside are re-synced automatically. + void loadFullState(const nlohmann::json& in); + protected: friend class LocationEditor; Renderer& renderer; Inventory& inventory; private: + // ---- Pointer-to-index helpers ---- + InteractiveObject* getTargetInteractiveObject(); + TeleportZone* getTargetTeleportZone(); + int findInteractiveObjectIndex(const InteractiveObject* obj) const; + int findTeleportZoneIndex(const TeleportZone* tz) const; + void resolveCharacterCollisions(); void updateDynamicReplans(int64_t deltaMs); void loadTeleportZones(const std::string& jsonPath, const char* zipFile); diff --git a/src/LocationState.cpp b/src/LocationState.cpp new file mode 100644 index 0000000..96c4d3f --- /dev/null +++ b/src/LocationState.cpp @@ -0,0 +1,52 @@ +#include "LocationState.h" + +namespace ZL { + +void LocationState::save(nlohmann::json& out) const +{ + out["cameraAzimuth"] = cameraAzimuth; + out["cameraInclination"] = cameraInclination; + + out["activeNavigationIndex"] = activeNavigationIndex; + + out["targetInteractiveObjectIndex"] = targetInteractiveObjectIndex; + out["targetInteractNpcIndex"] = targetInteractNpcIndex; + out["targetTeleportZoneIndex"] = targetTeleportZoneIndex; + + out["isDarklands"] = isDarklands; + out["isNight"] = isNight; + out["isDawn"] = isDawn; + + out["tutorialInteractiveObjectsLocked"] = tutorialInteractiveObjectsLocked; + + nlohmann::json tzArr = nlohmann::json::array(); + for (bool v : triggerZonePlayerInside) tzArr.push_back(v); + out["triggerZonePlayerInside"] = std::move(tzArr); +} + +void LocationState::load(const nlohmann::json& in) +{ + cameraAzimuth = in.value("cameraAzimuth", cameraAzimuth); + cameraInclination = in.value("cameraInclination", cameraInclination); + + activeNavigationIndex = in.value("activeNavigationIndex", activeNavigationIndex); + + targetInteractiveObjectIndex = in.value("targetInteractiveObjectIndex", -1); + targetInteractNpcIndex = in.value("targetInteractNpcIndex", -1); + targetTeleportZoneIndex = in.value("targetTeleportZoneIndex", -1); + + isDarklands = in.value("isDarklands", false); + isNight = in.value("isNight", false); + isDawn = in.value("isDawn", false); + + tutorialInteractiveObjectsLocked = in.value("tutorialInteractiveObjectsLocked", false); + + triggerZonePlayerInside.clear(); + if (in.contains("triggerZonePlayerInside") && in["triggerZonePlayerInside"].is_array()) { + for (const auto& v : in["triggerZonePlayerInside"]) { + triggerZonePlayerInside.push_back(v.get()); + } + } +} + +} // namespace ZL diff --git a/src/LocationState.h b/src/LocationState.h new file mode 100644 index 0000000..6c97b5b --- /dev/null +++ b/src/LocationState.h @@ -0,0 +1,40 @@ +#pragma once +#include +#include "external/nlohmann/json.hpp" + +namespace ZL { + +struct LocationState { + // ---- Camera ---- + float cameraAzimuth = -2.35f; + float cameraInclination = 1.1036f; + + // ---- Navigation ---- + int activeNavigationIndex = 0; + + // ---- Walk-to targets (-1 = none) ---- + // Mirrors CharacterState's kNoTarget convention; raw pointers are not serialisable. + int targetInteractiveObjectIndex = -1; + int targetInteractNpcIndex = -1; + int targetTeleportZoneIndex = -1; + + // ---- World-mode flags (synced from Game after each transition) ---- + bool isDarklands = false; + bool isNight = false; + bool isDawn = false; + + // ---- Tutorial ---- + bool tutorialInteractiveObjectsLocked = false; + + // ---- Trigger-zone runtime flags ---- + // Indexed parallel to Location::triggerZones. + // The zones themselves are static config reloaded from JSON; + // only playerInside is live state that must be saved. + std::vector triggerZonePlayerInside; + + // ---- Serialisation ---- + void save(nlohmann::json& out) const; + void load(const nlohmann::json& in); +}; + +} // namespace ZL diff --git a/src/ScriptEngine.cpp b/src/ScriptEngine.cpp index 49f1928..64facf6 100644 --- a/src/ScriptEngine.cpp +++ b/src/ScriptEngine.cpp @@ -281,12 +281,12 @@ namespace ZL { api.set_function("is_night", [loc]() { - return loc->isNight; + return loc->state.isNight; }); api.set_function("is_dawn", [loc]() { - return loc->isDawn; + return loc->state.isDawn; }); // advance_darklands_hud() @@ -311,7 +311,7 @@ namespace ZL { // is_darklands() → bool api.set_function("is_darklands", - [loc]() { return loc->isDarklands; }); + [loc]() { return loc->state.isDarklands; }); api.set_function("setFloatValue", [this_impl = impl.get()](const std::string& key, float value) { @@ -974,4 +974,65 @@ namespace ZL { } } + void ScriptEngine::saveScriptGlobals(nlohmann::json& out) const + { + if (!impl) return; + lua_State* L = impl->lua.lua_state(); + + // Iterate the global table (_G). lua_next pops the key and pushes key+value. + lua_pushglobaltable(L); // stack: [_G] + lua_pushnil(L); // stack: [_G, nil] (first key) + + while (lua_next(L, -2) != 0) { // stack: [_G, key, value] + if (lua_type(L, -2) == LUA_TSTRING) { + const char* key = lua_tostring(L, -2); + // Skip Lua built-in globals and the game API table. + const std::string k(key); + if (k == "_G" || k == "_VERSION" || k == "game_api") { + lua_pop(L, 1); // pop value, keep key for next iteration + continue; + } + const int t = lua_type(L, -1); + if (t == LUA_TNUMBER) { + if (lua_isinteger(L, -1)) { + out[k] = static_cast(lua_tointeger(L, -1)); + } else { + out[k] = lua_tonumber(L, -1); + } + } else if (t == LUA_TSTRING) { + out[k] = lua_tostring(L, -1); + } else if (t == LUA_TBOOLEAN) { + out[k] = (lua_toboolean(L, -1) != 0); + } + // Skip functions, tables, userdata — recreated by re-running the script. + } + lua_pop(L, 1); // pop value, keep key for next iteration + } + lua_pop(L, 1); // pop _G + } + + void ScriptEngine::loadScriptGlobals(const nlohmann::json& in) + { + if (!impl) return; + lua_State* L = impl->lua.lua_state(); + + for (auto it = in.begin(); it != in.end(); ++it) { + const std::string& key = it.key(); + const auto& val = it.value(); + if (val.is_number_integer()) { + lua_pushinteger(L, static_cast(val.get())); + } else if (val.is_number_float()) { + lua_pushnumber(L, static_cast(val.get())); + } else if (val.is_string()) { + const std::string s = val.get(); + lua_pushstring(L, s.c_str()); + } else if (val.is_boolean()) { + lua_pushboolean(L, val.get() ? 1 : 0); + } else { + continue; + } + lua_setglobal(L, key.c_str()); + } + } + } // namespace ZL diff --git a/src/ScriptEngine.h b/src/ScriptEngine.h index cad7889..142760d 100644 --- a/src/ScriptEngine.h +++ b/src/ScriptEngine.h @@ -3,6 +3,7 @@ #include #include #include "quest/QuestJournal.h" +#include "external/nlohmann/json.hpp" namespace ZL { @@ -51,6 +52,14 @@ public: void callChatOpenCallback(int chatIndex); void callCallTaxiCallback(); + // Serialise all Lua global scalars (numbers, strings, booleans) to JSON. + // Complex types (functions, tables, userdata) are skipped — they are recreated + // by re-running the script on load. + void saveScriptGlobals(nlohmann::json& out) const; + // Restore previously saved scalar globals. Call after init() so the script + // has already run and registered all callbacks. + void loadScriptGlobals(const nlohmann::json& in); + private: struct Impl; std::unique_ptr impl; diff --git a/src/items/InteractiveObject.cpp b/src/items/InteractiveObject.cpp index f4a9a71..560b9ef 100644 --- a/src/items/InteractiveObject.cpp +++ b/src/items/InteractiveObject.cpp @@ -230,4 +230,31 @@ namespace ZL { renderer.RenderUniform1f("uAlpha", 1.0f); } + void InteractiveObjectState::save(nlohmann::json& out) const + { + out["positionX"] = position.x(); + out["positionY"] = position.y(); + out["positionZ"] = position.z(); + out["rotationY"] = rotationY; + out["scale"] = scale; + out["alpha"] = alpha; + out["isActive"] = isActive; + // isAnimating and animTask are not saved: animations snap to their end + // state on load (the final position/scale/alpha is already in the fields above). + } + + void InteractiveObjectState::load(const nlohmann::json& in) + { + position.x() = in.value("positionX", position.x()); + position.y() = in.value("positionY", position.y()); + position.z() = in.value("positionZ", position.z()); + rotationY = in.value("rotationY", rotationY); + scale = in.value("scale", scale); + alpha = in.value("alpha", alpha); + isActive = in.value("isActive", isActive); + // Cancel any in-flight animation; state was restored to final values above. + isAnimating = false; + animTask.reset(); + } + } // namespace ZL diff --git a/src/items/InteractiveObjectState.h b/src/items/InteractiveObjectState.h index 4e4c45a..f6f04ff 100644 --- a/src/items/InteractiveObjectState.h +++ b/src/items/InteractiveObjectState.h @@ -4,6 +4,7 @@ #include #include #include +#include "external/nlohmann/json.hpp" namespace ZL { @@ -63,6 +64,10 @@ public: bool isActive = true; bool isAnimating = false; std::optional animTask; + + // --- Serialisation (runtime mutable fields only) --- + void save(nlohmann::json& out) const; + void load(const nlohmann::json& in); }; } // namespace ZL