Working on state for location
This commit is contained in:
parent
e0968b5522
commit
b01e970554
@ -103,6 +103,8 @@ set(SOURCES
|
|||||||
../src/MenuManager.cpp
|
../src/MenuManager.cpp
|
||||||
../src/Location.h
|
../src/Location.h
|
||||||
../src/Location.cpp
|
../src/Location.cpp
|
||||||
|
../src/LocationState.h
|
||||||
|
../src/LocationState.cpp
|
||||||
../src/LocationEditor.h
|
../src/LocationEditor.h
|
||||||
../src/LocationEditor.cpp
|
../src/LocationEditor.cpp
|
||||||
../src/GameConstants.h
|
../src/GameConstants.h
|
||||||
|
|||||||
@ -58,6 +58,8 @@ add_executable(witcher001
|
|||||||
../src/MenuManager.cpp
|
../src/MenuManager.cpp
|
||||||
../src/Location.h
|
../src/Location.h
|
||||||
../src/Location.cpp
|
../src/Location.cpp
|
||||||
|
../src/LocationState.h
|
||||||
|
../src/LocationState.cpp
|
||||||
../src/LocationEditor.h
|
../src/LocationEditor.h
|
||||||
../src/LocationEditor.cpp
|
../src/LocationEditor.cpp
|
||||||
../src/GameConstants.h
|
../src/GameConstants.h
|
||||||
|
|||||||
@ -29,4 +29,61 @@ void CharacterState::setHp(float newHp) {
|
|||||||
if (onHpChanged) onHpChanged(hp, initialHp);
|
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
|
} // namespace ZL
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <Eigen/Core>
|
#include <Eigen/Core>
|
||||||
#include <Eigen/Geometry>
|
#include <Eigen/Geometry>
|
||||||
|
#include "external/nlohmann/json.hpp"
|
||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
@ -124,6 +125,10 @@ public:
|
|||||||
void stopInPlace();
|
void stopInPlace();
|
||||||
float getHp() const { return hp; }
|
float getHp() const { return hp; }
|
||||||
void setHp(float newHp);
|
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
|
} // namespace ZL
|
||||||
|
|||||||
34
src/Game.cpp
34
src/Game.cpp
@ -422,13 +422,13 @@ namespace ZL
|
|||||||
menuManager.tutorialShowTaxiHint();
|
menuManager.tutorialShowTaxiHint();
|
||||||
};
|
};
|
||||||
|
|
||||||
locations["location_dorm"]->tutorialInteractiveObjectsLocked = true;
|
locations["location_dorm"]->state.tutorialInteractiveObjectsLocked = true;
|
||||||
|
|
||||||
menuManager.tutorialUnlockInteractiveObjectsFunc = [this]()
|
menuManager.tutorialUnlockInteractiveObjectsFunc = [this]()
|
||||||
{
|
{
|
||||||
if (locations["location_dorm"])
|
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.facingAngle = destRotY;
|
||||||
currentLocation->player->state.targetFacingAngle = destRotY;
|
currentLocation->player->state.targetFacingAngle = destRotY;
|
||||||
}
|
}
|
||||||
currentLocation->cameraAzimuth = destRotY;
|
currentLocation->state.cameraAzimuth = destRotY;
|
||||||
currentLocation->isDarklands = isDarklands;
|
currentLocation->state.isDarklands = isDarklands;
|
||||||
currentLocation->isNight = menuManager.isNight;
|
currentLocation->state.isNight = menuManager.isNight;
|
||||||
currentLocation->isDawn = menuManager.isDawn;
|
currentLocation->state.isDawn = menuManager.isDawn;
|
||||||
currentLocation->scriptEngine.callLocationEnterCallback();
|
currentLocation->scriptEngine.callLocationEnterCallback();
|
||||||
|
|
||||||
|
|
||||||
@ -638,9 +638,9 @@ namespace ZL
|
|||||||
if (currentLocation)
|
if (currentLocation)
|
||||||
{
|
{
|
||||||
// Sync global flags so Location's draw functions see them.
|
// Sync global flags so Location's draw functions see them.
|
||||||
currentLocation->isDarklands = isDarklands;
|
currentLocation->state.isDarklands = isDarklands;
|
||||||
currentLocation->isNight = menuManager.isNight;
|
currentLocation->state.isNight = menuManager.isNight;
|
||||||
currentLocation->isDawn = menuManager.isDawn;
|
currentLocation->state.isDawn = menuManager.isDawn;
|
||||||
|
|
||||||
if (isDarklands) {
|
if (isDarklands) {
|
||||||
currentLocation->drawGameDarklands();
|
currentLocation->drawGameDarklands();
|
||||||
@ -1071,8 +1071,8 @@ namespace ZL
|
|||||||
//x = x - 1;
|
//x = x - 1;
|
||||||
//std::cout << "current x: " << x << std::endl;
|
//std::cout << "current x: " << x << std::endl;
|
||||||
|
|
||||||
std::cout << "Azimuth: " << currentLocation->cameraAzimuth << std::endl;
|
std::cout << "Azimuth: " << currentLocation->state.cameraAzimuth << std::endl;
|
||||||
std::cout << "Inclination: " << currentLocation->cameraInclination << std::endl;
|
std::cout << "Inclination: " << currentLocation->state.cameraInclination << std::endl;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDLK_c:
|
case SDLK_c:
|
||||||
@ -1177,8 +1177,8 @@ namespace ZL
|
|||||||
currentLocation->lastMouseY = eventY;
|
currentLocation->lastMouseY = eventY;
|
||||||
|
|
||||||
// Snapshot current angles so we can measure how far the user rotates.
|
// Snapshot current angles so we can measure how far the user rotates.
|
||||||
dragStartAzimuth = currentLocation->cameraAzimuth;
|
dragStartAzimuth = currentLocation->state.cameraAzimuth;
|
||||||
dragStartInclination = currentLocation->cameraInclination;
|
dragStartInclination = currentLocation->state.cameraInclination;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1383,8 +1383,8 @@ namespace ZL
|
|||||||
static constexpr float TUTORIAL_ROTATION_THRESHOLD = 0.15f;
|
static constexpr float TUTORIAL_ROTATION_THRESHOLD = 0.15f;
|
||||||
if (cameraDragging
|
if (cameraDragging
|
||||||
&& menuManager.tutorialStep == TutorialStep::Step1) {
|
&& menuManager.tutorialStep == TutorialStep::Step1) {
|
||||||
float deltaAz = std::abs(currentLocation->cameraAzimuth - dragStartAzimuth);
|
float deltaAz = std::abs(currentLocation->state.cameraAzimuth - dragStartAzimuth);
|
||||||
float deltaInc = std::abs(currentLocation->cameraInclination - dragStartInclination);
|
float deltaInc = std::abs(currentLocation->state.cameraInclination - dragStartInclination);
|
||||||
if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) {
|
if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) {
|
||||||
menuManager.advanceTutorialStep();
|
menuManager.advanceTutorialStep();
|
||||||
}
|
}
|
||||||
@ -1438,7 +1438,7 @@ namespace ZL
|
|||||||
if (currentLocation)
|
if (currentLocation)
|
||||||
{
|
{
|
||||||
//currentLocation->dialogueSystem.startDialogue("dialog_video001");
|
//currentLocation->dialogueSystem.startDialogue("dialog_video001");
|
||||||
currentLocation->isNight = menuManager.isNight;
|
currentLocation->state.isNight = menuManager.isNight;
|
||||||
currentLocation->scriptEngine.callTriggerNightEnterCallback();
|
currentLocation->scriptEngine.callTriggerNightEnterCallback();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1448,7 +1448,7 @@ namespace ZL
|
|||||||
menuManager.setDarklandsMode(isDarklands);
|
menuManager.setDarklandsMode(isDarklands);
|
||||||
|
|
||||||
if (currentLocation) {
|
if (currentLocation) {
|
||||||
currentLocation->isDarklands = isDarklands;
|
currentLocation->state.isDarklands = isDarklands;
|
||||||
if (isDarklands)
|
if (isDarklands)
|
||||||
currentLocation->scriptEngine.callDarklandsEnterCallback();
|
currentLocation->scriptEngine.callDarklandsEnterCallback();
|
||||||
else
|
else
|
||||||
|
|||||||
251
src/Location.cpp
251
src/Location.cpp
@ -1,4 +1,4 @@
|
|||||||
#include "Location.h"
|
#include "Location.h"
|
||||||
#include "utils/Utils.h"
|
#include "utils/Utils.h"
|
||||||
#include "render/OpenGlExtensions.h"
|
#include "render/OpenGlExtensions.h"
|
||||||
#include <iostream>
|
#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)
|
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::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::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::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, "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_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);
|
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));
|
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;
|
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)
|
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;
|
if (!tz.enabled) continue;
|
||||||
const float dist = (playerPos - tz.position).norm();
|
const float dist = (playerPos - tz.position).norm();
|
||||||
if (!tz.playerInside && dist <= tz.radius) {
|
if (!tz.playerInside && dist <= tz.radius) {
|
||||||
tz.playerInside = true;
|
tz.playerInside = true;
|
||||||
|
state.triggerZonePlayerInside[i] = true;
|
||||||
scriptEngine.callTriggerEnterCallback(tz.id);
|
scriptEngine.callTriggerEnterCallback(tz.id);
|
||||||
} else if (tz.playerInside && dist > tz.radius + tz.hysteresis) {
|
} else if (tz.playerInside && dist > tz.radius + tz.hysteresis) {
|
||||||
tz.playerInside = false;
|
tz.playerInside = false;
|
||||||
|
state.triggerZonePlayerInside[i] = false;
|
||||||
scriptEngine.callTriggerExitCallback(tz.id);
|
scriptEngine.callTriggerExitCallback(tz.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -368,7 +408,7 @@ namespace ZL
|
|||||||
navigationMaps[i].build(paths[i], CONST_ZIP_FILE);
|
navigationMaps[i].build(paths[i], CONST_ZIP_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
activeNavigationIndex = 0;
|
state.activeNavigationIndex = 0;
|
||||||
navigation = navigationMaps.empty() ? nullptr : &navigationMaps[0];
|
navigation = navigationMaps.empty() ? nullptr : &navigationMaps[0];
|
||||||
|
|
||||||
if (editorMode == EditorMode::Navigation && navigation) {
|
if (editorMode == EditorMode::Navigation && navigation) {
|
||||||
@ -424,7 +464,7 @@ namespace ZL
|
|||||||
std::cerr << "[NAV] switchNavigation: index " << index << " out of range\n";
|
std::cerr << "[NAV] switchNavigation: index " << index << " out of range\n";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
activeNavigationIndex = index;
|
state.activeNavigationIndex = index;
|
||||||
navigation = &navigationMaps[index];
|
navigation = &navigationMaps[index];
|
||||||
|
|
||||||
// Force all characters to replan their paths against the new nav map.
|
// Force all characters to replan their paths against the new nav map.
|
||||||
@ -461,7 +501,7 @@ namespace ZL
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDarklands) {
|
if (state.isDarklands) {
|
||||||
if (!intObj.loadedObject.textureDarklands) continue;
|
if (!intObj.loadedObject.textureDarklands) continue;
|
||||||
} else {
|
} else {
|
||||||
if (!intObj.loadedObject.texture) continue;
|
if (!intObj.loadedObject.texture) continue;
|
||||||
@ -615,8 +655,8 @@ namespace ZL
|
|||||||
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
|
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
|
||||||
//renderer.TranslateMatrix({ 0, -6.f, 0 });
|
//renderer.TranslateMatrix({ 0, -6.f, 0 });
|
||||||
|
|
||||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(state.cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(state.cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||||
const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero();
|
const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero();
|
||||||
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
||||||
@ -785,9 +825,9 @@ namespace ZL
|
|||||||
|
|
||||||
renderer.LoadIdentity();
|
renderer.LoadIdentity();
|
||||||
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
|
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(state.cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(state.cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||||
const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero();
|
const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero();
|
||||||
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
||||||
@ -897,8 +937,8 @@ namespace ZL
|
|||||||
renderer.LoadIdentity();
|
renderer.LoadIdentity();
|
||||||
renderer.TranslateMatrix({ 0, 0, -1.0f * Environment::zoom });
|
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(state.cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(state.cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||||
const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero();
|
const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero();
|
||||||
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
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 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 kNightFogColor[3] = { 0.01f, 0.01f, 0.05f };
|
||||||
static const float kDawnFogColor[3] = { 0.50f, 0.44f, 0.47f }; // grey with slight pink
|
static const float kDawnFogColor[3] = { 0.50f, 0.44f, 0.47f }; // grey with slight pink
|
||||||
const float* ambientColor = isDawn ? kDawnAmbient : kNightAmbient;
|
const float* ambientColor = state.isDawn ? kDawnAmbient : kNightAmbient;
|
||||||
const float* fogColor = isDawn ? kDawnFogColor : kNightFogColor;
|
const float* fogColor = state.isDawn ? kDawnFogColor : kNightFogColor;
|
||||||
|
|
||||||
if (hasShadows) {
|
if (hasShadows) {
|
||||||
drawNightShadowDepthPass(*shadowLight);
|
drawNightShadowDepthPass(*shadowLight);
|
||||||
@ -1079,8 +1119,8 @@ namespace ZL
|
|||||||
renderer.LoadIdentity();
|
renderer.LoadIdentity();
|
||||||
renderer.TranslateMatrix({ 0, 0, -1.0f * Environment::zoom });
|
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(state.cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(state.cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||||
const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero();
|
const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero();
|
||||||
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
||||||
@ -1492,63 +1532,65 @@ namespace ZL
|
|||||||
for (auto& [idx, t] : npcBumpsPlayerCooldown) t -= deltaS;
|
for (auto& [idx, t] : npcBumpsPlayerCooldown) t -= deltaS;
|
||||||
|
|
||||||
// Check if player reached target interactive object
|
// Check if player reached target interactive object
|
||||||
if (!tutorialInteractiveObjectsLocked && targetInteractiveObject && player && !targetInteractiveObject->state.isAnimating) {
|
if (auto* obj = getTargetInteractiveObject();
|
||||||
const Eigen::Vector3f& approachTarget = targetInteractiveObject->state.hasInteractionPosition
|
!state.tutorialInteractiveObjectsLocked && obj && player && !obj->state.isAnimating)
|
||||||
? targetInteractiveObject->state.interactionPosition
|
{
|
||||||
: targetInteractiveObject->state.position;
|
const Eigen::Vector3f& approachTarget = obj->state.hasInteractionPosition
|
||||||
|
? obj->state.interactionPosition
|
||||||
|
: obj->state.position;
|
||||||
float distToObject = (player->state.position - approachTarget).norm();
|
float distToObject = (player->state.position - approachTarget).norm();
|
||||||
|
|
||||||
// If player is close enough to pick up the item
|
// 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] 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
|
// Call custom activate function if specified, otherwise use fallback
|
||||||
try {
|
try {
|
||||||
if (!targetInteractiveObject->state.activateFunctionName.empty()) {
|
if (!obj->state.activateFunctionName.empty()) {
|
||||||
std::cout << "[PICKUP] Using custom function: " << targetInteractiveObject->state.activateFunctionName << std::endl;
|
std::cout << "[PICKUP] Using custom function: " << obj->state.activateFunctionName << std::endl;
|
||||||
scriptEngine.callActivateFunction(targetInteractiveObject->state.activateFunctionName);
|
scriptEngine.callActivateFunction(obj->state.activateFunctionName);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
std::cout << "[PICKUP] Using fallback callback" << std::endl;
|
std::cout << "[PICKUP] Using fallback callback" << std::endl;
|
||||||
scriptEngine.callItemPickupCallback(targetInteractiveObject->loadedObject.name);
|
scriptEngine.callItemPickupCallback(obj->loadedObject.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (const std::exception& e) {
|
catch (const std::exception& e) {
|
||||||
std::cerr << "[PICKUP] Error calling function: " << e.what() << std::endl;
|
std::cerr << "[PICKUP] Error calling function: " << e.what() << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
targetInteractiveObject = nullptr;
|
state.targetInteractiveObjectIndex = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if player reached target NPC for interaction.
|
// Check if player reached target NPC for interaction.
|
||||||
if (targetInteractNpcIndex >= 0 && player) {
|
if (state.targetInteractNpcIndex >= 0 && player) {
|
||||||
float distToNpc = (player->state.position - npcs[targetInteractNpcIndex]->state.position).norm();
|
float distToNpc = (player->state.position - npcs[state.targetInteractNpcIndex]->state.position).norm();
|
||||||
if (distToNpc <= NPC_TALK_DISTANCE) {
|
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;
|
<< " (distance " << distToNpc << "); firing on_npc_interact" << std::endl;
|
||||||
// Stop the player at the talk distance and have the NPC turn to face them.
|
// Stop the player at the talk distance and have the NPC turn to face them.
|
||||||
player->setTarget(player->state.position);
|
player->setTarget(player->state.position);
|
||||||
npcs[targetInteractNpcIndex]->state.faceTargetIndex = CharacterState::kPlayerIndex;
|
npcs[state.targetInteractNpcIndex]->state.faceTargetIndex = CharacterState::kPlayerIndex;
|
||||||
try {
|
try {
|
||||||
scriptEngine.callNpcInteractCallback(targetInteractNpcIndex);
|
scriptEngine.callNpcInteractCallback(state.targetInteractNpcIndex);
|
||||||
}
|
}
|
||||||
catch (const std::exception& e) {
|
catch (const std::exception& e) {
|
||||||
std::cerr << "[NPC] callback error: " << e.what() << std::endl;
|
std::cerr << "[NPC] callback error: " << e.what() << std::endl;
|
||||||
}
|
}
|
||||||
targetInteractNpcIndex = -1;
|
state.targetInteractNpcIndex = -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& tz : teleportZones) tz.update(static_cast<float>(delta));
|
for (auto& tz : teleportZones) tz.update(static_cast<float>(delta));
|
||||||
|
|
||||||
if (targetTeleportZone && player) {
|
if (auto* tz = getTargetTeleportZone(); tz && player) {
|
||||||
float dist = (player->state.position - targetTeleportZone->position).norm();
|
float dist = (player->state.position - tz->position).norm();
|
||||||
if (dist <= targetTeleportZone->radius) {
|
if (dist <= tz->radius) {
|
||||||
std::cout << "[TELEPORT] Player reached teleport zone '" << targetTeleportZone->id << "'" << std::endl;
|
std::cout << "[TELEPORT] Player reached teleport zone '" << tz->id << "'" << std::endl;
|
||||||
if (onTeleport) onTeleport(targetTeleportZone->destinationLocation, targetTeleportZone->destinationPosition, targetTeleportZone->destinationRotationY);
|
if (onTeleport) onTeleport(tz->destinationLocation, tz->destinationPosition, tz->destinationRotationY);
|
||||||
targetTeleportZone = nullptr;
|
state.targetTeleportZoneIndex = -1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1596,8 +1638,8 @@ namespace ZL
|
|||||||
float aspect = (float)Environment::width / (float)Environment::height;
|
float aspect = (float)Environment::width / (float)Environment::height;
|
||||||
float tanHalfFov = tan(CAMERA_FOV_Y * 0.5f);
|
float tanHalfFov = tan(CAMERA_FOV_Y * 0.5f);
|
||||||
|
|
||||||
float cosAzim = cos(cameraAzimuth), sinAzim = sin(cameraAzimuth);
|
float cosAzim = cos(state.cameraAzimuth), sinAzim = sin(state.cameraAzimuth);
|
||||||
float cosIncl = cos(cameraInclination), sinIncl = sin(cameraInclination);
|
float cosIncl = cos(state.cameraInclination), sinIncl = sin(state.cameraInclination);
|
||||||
|
|
||||||
Eigen::Vector3f camRight(cosAzim, 0.f, sinAzim);
|
Eigen::Vector3f camRight(cosAzim, 0.f, sinAzim);
|
||||||
Eigen::Vector3f camForward(sinAzim * cosIncl, -sinIncl, -cosAzim * cosIncl);
|
Eigen::Vector3f camForward(sinAzim * cosIncl, -sinIncl, -cosAzim * cosIncl);
|
||||||
@ -1639,9 +1681,9 @@ namespace ZL
|
|||||||
std::cout << "[CLICK] Player position: (" << player->state.position.x() << ", "
|
std::cout << "[CLICK] Player position: (" << player->state.position.x() << ", "
|
||||||
<< player->state.position.y() << ", " << player->state.position.z() << ")" << std::endl;
|
<< player->state.position.y() << ", " << player->state.position.z() << ")" << std::endl;
|
||||||
|
|
||||||
targetInteractiveObject = clickedObject;
|
state.targetInteractiveObjectIndex = findInteractiveObjectIndex(clickedObject);
|
||||||
targetInteractNpcIndex = -1;
|
state.targetInteractNpcIndex = -1;
|
||||||
targetTeleportZone = nullptr;
|
state.targetTeleportZoneIndex = -1;
|
||||||
player->setTarget(clickedObject->state.hasInteractionPosition
|
player->setTarget(clickedObject->state.hasInteractionPosition
|
||||||
? clickedObject->state.interactionPosition
|
? clickedObject->state.interactionPosition
|
||||||
: clickedObject->state.position);
|
: clickedObject->state.position);
|
||||||
@ -1662,13 +1704,13 @@ namespace ZL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (npcIndex != -1) {
|
if (npcIndex != -1) {
|
||||||
targetInteractiveObject = nullptr;
|
state.targetInteractiveObjectIndex = -1;
|
||||||
targetTeleportZone = nullptr;
|
state.targetTeleportZoneIndex = -1;
|
||||||
|
|
||||||
if (clickedNpc->state.canAttack) {
|
if (clickedNpc->state.canAttack) {
|
||||||
// Hostile NPC: combat logic walks the player in via attackTargetIndex.
|
// Hostile NPC: combat logic walks the player in via attackTargetIndex.
|
||||||
player->state.attackTargetIndex = npcIndex;
|
player->state.attackTargetIndex = npcIndex;
|
||||||
targetInteractNpcIndex = -1;
|
state.targetInteractNpcIndex = -1;
|
||||||
|
|
||||||
if (distance <= clickedNpc->state.interactionRadius) {
|
if (distance <= clickedNpc->state.interactionRadius) {
|
||||||
std::cout << "[CLICK] Hostile NPC " << npcIndex << " in range; firing on_npc_interact" << std::endl;
|
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);
|
player->setTarget(player->state.position);
|
||||||
npcs[npcIndex]->state.faceTargetIndex = CharacterState::kPlayerIndex;
|
npcs[npcIndex]->state.faceTargetIndex = CharacterState::kPlayerIndex;
|
||||||
scriptEngine.callNpcInteractCallback(npcIndex);
|
scriptEngine.callNpcInteractCallback(npcIndex);
|
||||||
targetInteractNpcIndex = -1;
|
state.targetInteractNpcIndex = -1;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
std::cout << "[CLICK] NPC " << npcIndex << " out of talk range (distance " << distance
|
std::cout << "[CLICK] NPC " << npcIndex << " out of talk range (distance " << distance
|
||||||
<< " > " << NPC_TALK_DISTANCE << "); walking to NPC..." << std::endl;
|
<< " > " << NPC_TALK_DISTANCE << "); walking to NPC..." << std::endl;
|
||||||
player->setTarget(clickedNpc->state.position);
|
player->setTarget(clickedNpc->state.position);
|
||||||
targetInteractNpcIndex = npcIndex;
|
state.targetInteractNpcIndex = npcIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1712,16 +1754,16 @@ namespace ZL
|
|||||||
|
|
||||||
if (clickedTeleport) {
|
if (clickedTeleport) {
|
||||||
std::cout << "[CLICK] Clicked teleport zone '" << clickedTeleport->id << "'" << std::endl;
|
std::cout << "[CLICK] Clicked teleport zone '" << clickedTeleport->id << "'" << std::endl;
|
||||||
targetTeleportZone = clickedTeleport;
|
state.targetTeleportZoneIndex = findTeleportZoneIndex(clickedTeleport);
|
||||||
targetInteractiveObject = nullptr;
|
state.targetInteractiveObjectIndex = -1;
|
||||||
targetInteractNpcIndex = -1;
|
state.targetInteractNpcIndex = -1;
|
||||||
player->setTarget(clickedTeleport->position);
|
player->setTarget(clickedTeleport->position);
|
||||||
player->state.attackTargetIndex = CharacterState::kNoTarget;
|
player->state.attackTargetIndex = CharacterState::kNoTarget;
|
||||||
} else {
|
} else {
|
||||||
player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z()));
|
player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z()));
|
||||||
player->state.attackTargetIndex = CharacterState::kNoTarget;
|
player->state.attackTargetIndex = CharacterState::kNoTarget;
|
||||||
targetInteractNpcIndex = -1;
|
state.targetInteractNpcIndex = -1;
|
||||||
targetTeleportZone = nullptr;
|
state.targetTeleportZoneIndex = -1;
|
||||||
if (onPlayerFloorWalk) onPlayerFloorWalk();
|
if (onPlayerFloorWalk) onPlayerFloorWalk();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1755,12 +1797,12 @@ namespace ZL
|
|||||||
lastMouseY = eventY;
|
lastMouseY = eventY;
|
||||||
|
|
||||||
const float sensitivity = 0.005f;
|
const float sensitivity = 0.005f;
|
||||||
cameraAzimuth += dx * sensitivity;
|
state.cameraAzimuth += dx * sensitivity;
|
||||||
cameraInclination += dy * sensitivity;
|
state.cameraInclination += dy * sensitivity;
|
||||||
|
|
||||||
const float minInclination = M_PI * 30.f / 180.f;
|
const float minInclination = M_PI * 30.f / 180.f;
|
||||||
const float maxInclination = M_PI * 0.5f;
|
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);
|
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
|
} // namespace ZL
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "render/Renderer.h"
|
#include "render/Renderer.h"
|
||||||
#include "Environment.h"
|
#include "Environment.h"
|
||||||
@ -14,6 +14,7 @@
|
|||||||
#include "SparkEmitter.h"
|
#include "SparkEmitter.h"
|
||||||
#include "TeleportZone.h"
|
#include "TeleportZone.h"
|
||||||
#include "LocationEditor.h"
|
#include "LocationEditor.h"
|
||||||
|
#include "LocationState.h"
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@ -42,7 +43,7 @@ namespace ZL
|
|||||||
float radius = 1.5f;
|
float radius = 1.5f;
|
||||||
float hysteresis = 0.3f; // exit fires at radius + hysteresis to prevent flickering
|
float hysteresis = 0.3f; // exit fires at radius + hysteresis to prevent flickering
|
||||||
bool enabled = true;
|
bool enabled = true;
|
||||||
bool playerInside = false; // runtime tracking state, not serialized
|
bool playerInside = false; // runtime tracking state, not serialized (mirrored in LocationState)
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LocationSetup
|
struct LocationSetup
|
||||||
@ -64,6 +65,9 @@ namespace ZL
|
|||||||
public:
|
public:
|
||||||
Location(Renderer& iRenderer, Inventory& iInventory);
|
Location(Renderer& iRenderer, Inventory& iInventory);
|
||||||
|
|
||||||
|
// ---- All serialisable runtime state ----
|
||||||
|
LocationState state;
|
||||||
|
|
||||||
std::unordered_map<std::string, LoadedGameObject> gameObjects;
|
std::unordered_map<std::string, LoadedGameObject> gameObjects;
|
||||||
|
|
||||||
std::vector<InteractiveObject> interactiveObjects;
|
std::vector<InteractiveObject> interactiveObjects;
|
||||||
@ -71,21 +75,11 @@ namespace ZL
|
|||||||
std::unique_ptr<Character> player;
|
std::unique_ptr<Character> player;
|
||||||
std::vector<std::unique_ptr<Character>> npcs;
|
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;
|
std::vector<PathFinder> navigationMaps;
|
||||||
PathFinder* navigation = nullptr;
|
PathFinder* navigation = nullptr;
|
||||||
int activeNavigationIndex = 0;
|
|
||||||
|
|
||||||
std::unique_ptr<ShadowMap> shadowMap;
|
std::unique_ptr<ShadowMap> shadowMap;
|
||||||
Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity();
|
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;
|
std::unique_ptr<TextRenderer> npcNameText;
|
||||||
|
|
||||||
@ -93,21 +87,12 @@ namespace ZL
|
|||||||
Dialogue::DialogueSystem dialogueSystem;
|
Dialogue::DialogueSystem dialogueSystem;
|
||||||
|
|
||||||
std::vector<TeleportZone> teleportZones;
|
std::vector<TeleportZone> teleportZones;
|
||||||
TeleportZone* targetTeleportZone = nullptr;
|
|
||||||
std::function<void(const std::string&, const Eigen::Vector3f&, float)> onTeleport;
|
std::function<void(const std::string&, const Eigen::Vector3f&, float)> onTeleport;
|
||||||
|
|
||||||
std::vector<TriggerZone> triggerZones;
|
std::vector<TriggerZone> triggerZones;
|
||||||
|
|
||||||
std::vector<PointLight> pointLights;
|
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.
|
// 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.
|
// Used by the tutorial system to detect the "tap to walk" gesture.
|
||||||
std::function<void()> onPlayerFloorWalk;
|
std::function<void()> onPlayerFloorWalk;
|
||||||
@ -119,7 +104,7 @@ namespace ZL
|
|||||||
std::function<bool()> requestDarklandsTransition;
|
std::function<bool()> requestDarklandsTransition;
|
||||||
|
|
||||||
std::function<void(bool, bool)> requestNightDayTransition;
|
std::function<void(bool, bool)> requestNightDayTransition;
|
||||||
|
|
||||||
// Set by Game after setup(). Lua calls this to advance the uni_interior HUD
|
// Set by Game after setup(). Lua calls this to advance the uni_interior HUD
|
||||||
// from step12 to step13 (trigger-zone encounter hint).
|
// from step12 to step13 (trigger-zone encounter hint).
|
||||||
std::function<void()> requestAdvanceDarklandsHud;
|
std::function<void()> requestAdvanceDarklandsHud;
|
||||||
@ -146,7 +131,7 @@ namespace ZL
|
|||||||
bool switchNavigation(int index);
|
bool switchNavigation(int index);
|
||||||
InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir);
|
InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir);
|
||||||
Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f);
|
Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f);
|
||||||
|
|
||||||
void drawGame();
|
void drawGame();
|
||||||
void drawShadowDepthPass();
|
void drawShadowDepthPass();
|
||||||
void drawGameWithShadows();
|
void drawGameWithShadows();
|
||||||
@ -168,12 +153,25 @@ namespace ZL
|
|||||||
void setDialogueFlag(const std::string& flag, int value);
|
void setDialogueFlag(const std::string& flag, int value);
|
||||||
int getDialogueFlag(const std::string& flag) const;
|
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:
|
protected:
|
||||||
friend class LocationEditor;
|
friend class LocationEditor;
|
||||||
Renderer& renderer;
|
Renderer& renderer;
|
||||||
Inventory& inventory;
|
Inventory& inventory;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// ---- Pointer-to-index helpers ----
|
||||||
|
InteractiveObject* getTargetInteractiveObject();
|
||||||
|
TeleportZone* getTargetTeleportZone();
|
||||||
|
int findInteractiveObjectIndex(const InteractiveObject* obj) const;
|
||||||
|
int findTeleportZoneIndex(const TeleportZone* tz) const;
|
||||||
|
|
||||||
void resolveCharacterCollisions();
|
void resolveCharacterCollisions();
|
||||||
void updateDynamicReplans(int64_t deltaMs);
|
void updateDynamicReplans(int64_t deltaMs);
|
||||||
void loadTeleportZones(const std::string& jsonPath, const char* zipFile);
|
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",
|
api.set_function("is_night",
|
||||||
[loc]() {
|
[loc]() {
|
||||||
return loc->isNight;
|
return loc->state.isNight;
|
||||||
});
|
});
|
||||||
|
|
||||||
api.set_function("is_dawn",
|
api.set_function("is_dawn",
|
||||||
[loc]() {
|
[loc]() {
|
||||||
return loc->isDawn;
|
return loc->state.isDawn;
|
||||||
});
|
});
|
||||||
|
|
||||||
// advance_darklands_hud()
|
// advance_darklands_hud()
|
||||||
@ -311,7 +311,7 @@ namespace ZL {
|
|||||||
|
|
||||||
// is_darklands() → bool
|
// is_darklands() → bool
|
||||||
api.set_function("is_darklands",
|
api.set_function("is_darklands",
|
||||||
[loc]() { return loc->isDarklands; });
|
[loc]() { return loc->state.isDarklands; });
|
||||||
|
|
||||||
api.set_function("setFloatValue",
|
api.set_function("setFloatValue",
|
||||||
[this_impl = impl.get()](const std::string& key, float value) {
|
[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
|
} // namespace ZL
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include "quest/QuestJournal.h"
|
#include "quest/QuestJournal.h"
|
||||||
|
#include "external/nlohmann/json.hpp"
|
||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
@ -51,6 +52,14 @@ public:
|
|||||||
void callChatOpenCallback(int chatIndex);
|
void callChatOpenCallback(int chatIndex);
|
||||||
void callCallTaxiCallback();
|
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:
|
private:
|
||||||
struct Impl;
|
struct Impl;
|
||||||
std::unique_ptr<Impl> impl;
|
std::unique_ptr<Impl> impl;
|
||||||
|
|||||||
@ -230,4 +230,31 @@ namespace ZL {
|
|||||||
renderer.RenderUniform1f("uAlpha", 1.0f);
|
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
|
} // namespace ZL
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <Eigen/Core>
|
#include <Eigen/Core>
|
||||||
#include <Eigen/Geometry>
|
#include <Eigen/Geometry>
|
||||||
|
#include "external/nlohmann/json.hpp"
|
||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
@ -63,6 +64,10 @@ public:
|
|||||||
bool isActive = true;
|
bool isActive = true;
|
||||||
bool isAnimating = false;
|
bool isAnimating = false;
|
||||||
std::optional<AnimTask> animTask;
|
std::optional<AnimTask> animTask;
|
||||||
|
|
||||||
|
// --- Serialisation (runtime mutable fields only) ---
|
||||||
|
void save(nlohmann::json& out) const;
|
||||||
|
void load(const nlohmann::json& in);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZL
|
} // namespace ZL
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user