Working on state for location

This commit is contained in:
Vladislav Khorev 2026-06-18 21:48:27 +03:00
parent e0968b5522
commit b01e970554
13 changed files with 494 additions and 101 deletions

View File

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

View File

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

View File

@ -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<int>(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<float>(), j[1].get<float>(), j[2].get<float>() };
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<AnimationState>(in.value("currentState", static_cast<int>(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

View File

@ -5,6 +5,7 @@
#include <functional>
#include <Eigen/Core>
#include <Eigen/Geometry>
#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

View File

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

View File

@ -1,4 +1,4 @@
#include "Location.h"
#include "Location.h"
#include "utils/Utils.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
@ -49,6 +49,40 @@ namespace ZL
{
}
// ---- Pointer-to-index helpers ----
InteractiveObject* Location::getTargetInteractiveObject()
{
const int idx = state.targetInteractiveObjectIndex;
if (idx < 0 || idx >= static_cast<int>(interactiveObjects.size())) return nullptr;
return &interactiveObjects[idx];
}
TeleportZone* Location::getTargetTeleportZone()
{
const int idx = state.targetTeleportZoneIndex;
if (idx < 0 || idx >= static_cast<int>(teleportZones.size())) return nullptr;
return &teleportZones[idx];
}
int Location::findInteractiveObjectIndex(const InteractiveObject* obj) const
{
for (int i = 0; i < static_cast<int>(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<int>(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<int>(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<float>(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<int>(navigationMaps.size()))
{
navigation = &navigationMaps[state.activeNavigationIndex];
}
// Re-sync triggerZones[i].playerInside from state
for (int i = 0; i < static_cast<int>(triggerZones.size()); ++i) {
if (i < static_cast<int>(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<int>(arr.size()) && i < static_cast<int>(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<int>(arr.size()) && i < static_cast<int>(interactiveObjects.size()); ++i) {
interactiveObjects[i].state.load(arr[i]);
}
}
// Lua globals
if (in.contains("scriptGlobals")) {
scriptEngine.loadScriptGlobals(in["scriptGlobals"]);
}
}
} // namespace ZL

View File

@ -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 <functional>
#include <cstdint>
#include <unordered_map>
@ -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<std::string, LoadedGameObject> gameObjects;
std::vector<InteractiveObject> interactiveObjects;
@ -71,21 +75,11 @@ namespace ZL
std::unique_ptr<Character> player;
std::vector<std::unique_ptr<Character>> npcs;
float cameraAzimuth = -2.35;
float cameraInclination = 1.1036;//M_PI * 30.f / 180.f;
std::vector<PathFinder> navigationMaps;
PathFinder* navigation = nullptr;
int activeNavigationIndex = 0;
std::unique_ptr<ShadowMap> 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<TextRenderer> npcNameText;
@ -93,21 +87,12 @@ namespace ZL
Dialogue::DialogueSystem dialogueSystem;
std::vector<TeleportZone> teleportZones;
TeleportZone* targetTeleportZone = nullptr;
std::function<void(const std::string&, const Eigen::Vector3f&, float)> onTeleport;
std::vector<TriggerZone> triggerZones;
std::vector<PointLight> 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<void()> onPlayerFloorWalk;
@ -119,7 +104,7 @@ namespace ZL
std::function<bool()> requestDarklandsTransition;
std::function<void(bool, bool)> 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<void()> 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);

52
src/LocationState.cpp Normal file
View File

@ -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<bool>());
}
}
}
} // namespace ZL

40
src/LocationState.h Normal file
View File

@ -0,0 +1,40 @@
#pragma once
#include <vector>
#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<bool> triggerZonePlayerInside;
// ---- Serialisation ----
void save(nlohmann::json& out) const;
void load(const nlohmann::json& in);
};
} // namespace ZL

View File

@ -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<int64_t>(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<lua_Integer>(val.get<int64_t>()));
} else if (val.is_number_float()) {
lua_pushnumber(L, static_cast<lua_Number>(val.get<double>()));
} else if (val.is_string()) {
const std::string s = val.get<std::string>();
lua_pushstring(L, s.c_str());
} else if (val.is_boolean()) {
lua_pushboolean(L, val.get<bool>() ? 1 : 0);
} else {
continue;
}
lua_setglobal(L, key.c_str());
}
}
} // namespace ZL

View File

@ -3,6 +3,7 @@
#include <memory>
#include <unordered_map>
#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> impl;

View File

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

View File

@ -4,6 +4,7 @@
#include <functional>
#include <Eigen/Core>
#include <Eigen/Geometry>
#include "external/nlohmann/json.hpp"
namespace ZL {
@ -63,6 +64,10 @@ public:
bool isActive = true;
bool isAnimating = false;
std::optional<AnimTask> animTask;
// --- Serialisation (runtime mutable fields only) ---
void save(nlohmann::json& out) const;
void load(const nlohmann::json& in);
};
} // namespace ZL