Working on state for location
This commit is contained in:
parent
e0968b5522
commit
b01e970554
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
34
src/Game.cpp
34
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
|
||||
|
||||
251
src/Location.cpp
251
src/Location.cpp
@ -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
|
||||
|
||||
@ -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
52
src/LocationState.cpp
Normal 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
40
src/LocationState.h
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user