shadow-over-bishkek001/src/Location.cpp
2026-06-27 22:05:19 +03:00

1986 lines
69 KiB
C++

#include "Location.h"
#include "utils/Utils.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
#include "render/TextureManager.h"
#include "TextModel.h"
#include <cmath>
#include <algorithm>
#include <functional>
#include <memory>
#include <cfloat>
#include <limits>
#include <random>
#include "GameConstants.h"
#include "Character.h"
#include "external/nlohmann/json.hpp"
#include <SDL.h>
namespace ZL
{
extern const char* CONST_ZIP_FILE;
static constexpr float CAMERA_FOV_Y = 1.0f / 1.5f;
// How close the player needs to be to a peaceful NPC before the
// on_npc_interact callback fires and the conversation begins.
static constexpr float NPC_TALK_DISTANCE = 1.35f;
static float distancePointToSegmentXZ(const Eigen::Vector3f& p,
const Eigen::Vector3f& a,
const Eigen::Vector3f& b)
{
const Eigen::Vector2f p2(p.x(), p.z());
const Eigen::Vector2f a2(a.x(), a.z());
const Eigen::Vector2f b2(b.x(), b.z());
const Eigen::Vector2f ab = b2 - a2;
const float abLenSq = ab.squaredNorm();
if (abLenSq <= 1e-8f) {
return (p2 - a2).norm();
}
const float t = std::clamp((p2 - a2).dot(ab) / abLenSq, 0.0f, 1.0f);
const Eigen::Vector2f closest = a2 + ab * t;
return (p2 - closest).norm();
}
Location::Location(Renderer& iRenderer, Inventory& iInventory)
: renderer(iRenderer)
, inventory(iInventory)
, editor(*this)
{
}
// ---- 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)
{
// Load static game objects
gameObjects = GameObjectLoader::loadAndCreateGameObjects(params.gameObjectsJsonPath, renderer, CONST_ZIP_FILE);
// Load interactive objects
interactiveObjects = GameObjectLoader::loadAndCreateInteractiveObjects(params.interactiveObjectsJsonPath, renderer, CONST_ZIP_FILE);
//auto playerTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/gg/IMG_20260413_182354_992.png", CONST_ZIP_FILE));
auto playerTexture = renderer.textureManager.LoadFromPng("resources/w/gg/UniV_Grid_2K_Base_color.png", CONST_ZIP_FILE);
auto sparkTexture = renderer.textureManager.LoadFromPng("resources/w/spark.png", CONST_ZIP_FILE);
player = std::make_unique<Character>();
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);
player->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/gg/new2/gg_action_to_stand001_small.anim", CONST_ZIP_FILE);
player->loadBinaryAnimation(AnimationState::ACTION_TO_DEATH, "resources/w/gg/new2/gg_die001_small.anim", CONST_ZIP_FILE);
player->loadBinaryAnimation(AnimationState::DEATH_IDLE, "resources/w/gg/new2/gg_die_idle001_small.anim", CONST_ZIP_FILE);
player->weaponTexture = renderer.textureManager.LoadFromPng("resources/w/gg/Props.png", CONST_ZIP_FILE);
player->weaponMesh.data = LoadFromTextFile02("resources/w/gg/knife002.txt", CONST_ZIP_FILE);
player->weaponMesh.data.Move(Vector3f(0, -0.3, 0));
player->weaponMesh.data.Scale(0.2f);
player->weaponMesh.RefreshVBO();
player->weaponAttachBoneName = "RightHand";
player->weaponInitialRotation = Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitZ()).toRotationMatrix();
player->weaponInitialPosition = Eigen::Vector3f(0, 0.09, 0.016);
player->setTexture(playerTexture);
player->state.walkSpeed = 3.0f;
player->state.rotationSpeed = 8.0f;
player->state.modelScale = 1.f;
player->state.modelCorrectionRotation = Eigen::Quaternionf(
Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())
*
Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitX())
);
player->state.canAttack = true;
player->state.isPlayer = true;
player->state.position = params.playerPosition;
player->setTarget(params.playerPosition);
player->setupHitSparks(sparkTexture);
std::cout << "Load resurces step 9" << std::endl;
// Load NPCs from JSON. Aggressive (canAttack) NPCs need a player target
// and hit sparks; peaceful NPCs don't take damage from the player so
// they don't need either.
npcs = GameObjectLoader::loadAndCreate_Npcs(params.npcsJsonPath, renderer, CONST_ZIP_FILE);
for (auto& npc : npcs) {
if (!npc) continue;
npc->state.homePosition = Eigen::Vector3f(npc->state.position.x(), 0.f, npc->state.position.z());
if (npc->state.canAttack) {
npc->setupHitSparks(sparkTexture);
npc->state.attackTargetIndex = CharacterState::kPlayerIndex;
}
}
loadTeleportZones(params.teleportsJsonPath, CONST_ZIP_FILE);
loadTriggerZones(params.triggerZonesJsonPath, CONST_ZIP_FILE);
loadPointLights(params.lightsJsonPath, CONST_ZIP_FILE);
//#ifndef EMSCRIPTEN
// Create shadow map (2048x2048, ortho size 40, near 0.1, far 100)
shadowMap = std::make_unique<ShadowMap>(2048, 40.0f, 0.1f, 100.0f);
shadowMap->setLightDirection(Eigen::Vector3f(-0.5f, -1.0f, -0.3f));
std::cout << "Shadow map initialized" << std::endl;
//#endif
setupNavigation(params.navigationJsonPaths);
dialogueSystem.init(renderer, CONST_ZIP_FILE);
dialogueSystem.loadDatabase(params.dialoguesJsonPath);
dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes008.json");
dialogueSystem.setQuestJournal(journal);
npcNameText = std::make_unique<TextRenderer>();
if (!npcNameText->init(renderer, "resources/fonts/DroidSans.ttf", 24, CONST_ZIP_FILE)) {
std::cerr << "Failed to init NPC name TextRenderer" << std::endl;
npcNameText.reset();
}
scriptEngine.init(this, &inventory, params.scriptPath);
scriptEngine.setQuestJournal(journal);
dialogueSystem.setOnCutsceneFinished([this](const std::string& cutsceneId) {
scriptEngine.callCutsceneCompleteCallback(cutsceneId);
});
dialogueSystem.setOnDialogueLineStarted([this](const std::string& fn) {
try { scriptEngine.callActivateFunction(fn); }
catch (const std::exception& e) {
std::cerr << "[dialogue] line callback error: " << e.what() << "\n";
}
});
dialogueSystem.setOnCutsceneLineStarted([this](const std::string& fn) {
try { scriptEngine.callActivateFunction(fn); }
catch (const std::exception& e) {
std::cerr << "[cutscene] line callback error: " << e.what() << "\n";
}
});
dialogueSystem.setOnCutsceneFadeInComplete([this](const std::string& fn) {
try { scriptEngine.callActivateFunction(fn); }
catch (const std::exception& e) {
std::cerr << "[cutscene] fade-in callback error: " << e.what() << "\n";
}
});
}
void Location::loadTeleportZones(const std::string& jsonPath, const char* zipFile)
{
if (jsonPath.empty()) return;
std::string content;
try {
if (!zipFile || zipFile[0] == '\0') {
content = readTextFile(jsonPath);
} else {
auto buf = readFileFromZIP(jsonPath, zipFile);
if (buf.empty()) {
std::cerr << "[TELEPORT] Failed to read " << jsonPath << " from zip" << std::endl;
return;
}
content.assign(buf.begin(), buf.end());
}
} catch (const std::exception& e) {
std::cerr << "[TELEPORT] Failed to open " << jsonPath << ": " << e.what() << std::endl;
return;
}
using json = nlohmann::json;
json j;
try {
j = json::parse(content);
} catch (const std::exception& e) {
std::cerr << "[TELEPORT] JSON parse error in " << jsonPath << ": " << e.what() << std::endl;
return;
}
if (!j.contains("teleports") || !j["teleports"].is_array()) return;
auto activeTex = renderer.textureManager.LoadFromPng("resources/w/star.png", zipFile ? zipFile : "");
auto inactiveTex = renderer.textureManager.LoadFromPng("resources/w/star_red.png", zipFile ? zipFile : "");
for (const auto& item : j["teleports"]) {
TeleportZone tz;
tz.id = item.value("id", "");
tz.active = item.value("active", false);
tz.position = Eigen::Vector3f(
item.value("positionX", 0.0f),
item.value("positionY", 0.0f),
item.value("positionZ", 0.0f)
);
tz.radius = item.value("radius", 0.0f);
tz.automatic = item.value("automatic", false);
tz.automaticRadius = item.value("automaticRadius", tz.radius);
tz.destinationLocation = item.value("destinationLocation", "");
tz.destinationPosition = Eigen::Vector3f(
item.value("destinationPositionX", 0.0f),
item.value("destinationPositionY", 0.0f),
item.value("destinationPositionZ", 0.0f)
);
tz.destinationRotationY = item.value("destinationRotationY", 0.0f);
tz.initSparks(activeTex, inactiveTex);
teleportZones.push_back(std::move(tz));
}
std::cout << "[TELEPORT] Loaded " << teleportZones.size() << " teleport(s) from " << jsonPath << std::endl;
}
void Location::loadTriggerZones(const std::string& jsonPath, const char* zipFile)
{
if (jsonPath.empty()) return;
std::string content;
try {
if (!zipFile || zipFile[0] == '\0') {
content = readTextFile(jsonPath);
} else {
auto buf = readFileFromZIP(jsonPath, zipFile);
if (buf.empty()) {
std::cerr << "[TRIGGER] Failed to read " << jsonPath << " from zip" << std::endl;
return;
}
content.assign(buf.begin(), buf.end());
}
} catch (const std::exception& e) {
std::cerr << "[TRIGGER] Failed to open " << jsonPath << ": " << e.what() << std::endl;
return;
}
using json = nlohmann::json;
json j;
try {
j = json::parse(content);
} catch (const std::exception& e) {
std::cerr << "[TRIGGER] JSON parse error in " << jsonPath << ": " << e.what() << std::endl;
return;
}
if (!j.contains("trigger_zones") || !j["trigger_zones"].is_array()) return;
for (const auto& item : j["trigger_zones"]) {
TriggerZone tz;
tz.id = item.value("id", "");
tz.enabled = item.value("enabled", true);
tz.radius = item.value("radius", 1.5f);
tz.hysteresis = item.value("hysteresis", 0.3f);
tz.position = Eigen::Vector3f(
item.value("positionX", 0.0f),
item.value("positionY", 0.0f),
item.value("positionZ", 0.0f)
);
triggerZones.push_back(std::move(tz));
}
std::cout << "[TRIGGER] Loaded " << triggerZones.size() << " trigger zone(s) from " << jsonPath << std::endl;
}
void Location::loadPointLights(const std::string& jsonPath, const char* zipFile)
{
if (jsonPath.empty()) return;
std::string content;
try {
if (!zipFile || zipFile[0] == '\0') {
content = readTextFile(jsonPath);
} else {
auto buf = readFileFromZIP(jsonPath, zipFile);
if (buf.empty()) {
std::cerr << "[LIGHTS] Failed to read " << jsonPath << " from zip" << std::endl;
return;
}
content.assign(buf.begin(), buf.end());
}
} catch (const std::exception& e) {
std::cerr << "[LIGHTS] Failed to open " << jsonPath << ": " << e.what() << std::endl;
return;
}
using json = nlohmann::json;
json j;
try {
j = json::parse(content);
} catch (const std::exception& e) {
std::cerr << "[LIGHTS] JSON parse error in " << jsonPath << ": " << e.what() << std::endl;
return;
}
if (!j.contains("lights") || !j["lights"].is_array()) return;
for (const auto& item : j["lights"]) {
PointLight pl;
pl.id = item.value("id", "");
pl.autoLight = item.value("autoLight", false);
pl.autoLightDistance = item.value("autoLightDistance", -1.0f);
pl.limitX = item.value("limitX", 99999.f);
pl.limitY = item.value("limitY", 99999.f);
pl.autoLightLimitX = item.value("autoLightLimitX", -1.0f);
pl.autoLightLimitY = item.value("autoLightLimitY", -1.0f);
pl.position = Eigen::Vector3f(
item.value("positionX", 0.0f),
item.value("positionY", 0.0f),
item.value("positionZ", 0.0f)
);
pl.direction = Eigen::Vector3f(
item.value("directionX", 0.0f),
item.value("directionY", -1.0f),
item.value("directionZ", 0.0f)
);
pl.color = Eigen::Vector3f(
item.value("colorR", 1.0f),
item.value("colorG", 1.0f),
item.value("colorB", 1.0f)
);
pointLights.push_back(std::move(pl));
}
std::cout << "[LIGHTS] Loaded " << pointLights.size() << " light(s) from " << jsonPath << std::endl;
}
void Location::updateTriggerZones(const Eigen::Vector3f& playerPos)
{
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;
scriptEngine.callTriggerEnterCallback(tz.id);
} else if (tz.playerInside && dist > tz.radius + tz.hysteresis) {
tz.playerInside = false;
scriptEngine.callTriggerExitCallback(tz.id);
}
}
}
void Location::setupNavigation(const std::vector<std::string>& paths)
{
navigationMapPaths = paths;
navigationMaps.clear();
navigationMaps.resize(paths.size());
for (size_t i = 0; i < paths.size(); ++i) {
navigationMaps[i].build(paths[i], CONST_ZIP_FILE);
}
state.activeNavigationIndex = 0;
navigation = navigationMaps.empty() ? nullptr : &navigationMaps[0];
if (editorMode == EditorMode::Navigation && navigation) {
editor.buildNavMeshes();
}
static constexpr float kDynamicObstacleInfluenceDist = 6.0f;
auto makePlanner = [this](const Character* self) {
return [this, self](const Eigen::Vector3f& start, const Eigen::Vector3f& end) {
if (!navigation) return std::vector<Eigen::Vector3f>{};
std::vector<PathFinder::DynamicObstacle> dynamicObstacles;
dynamicObstacles.reserve(npcs.size() + 1);
const auto addCharacter = [&](const Character* other) {
if (!other || other == self) return;
if (other->getHp() <= 0.f || !other->state.enabled) return;
if (other->isMoving()) return;
if (distancePointToSegmentXZ(other->state.position, start, end) > kDynamicObstacleInfluenceDist) {
return;
}
PathFinder::DynamicObstacle obs;
obs.position = Eigen::Vector3f(other->state.position.x(), navigation->getFloorY(), other->state.position.z());
obs.radius = (std::max)(0.0f, other->state.collisionRadius * 0.6f);
dynamicObstacles.push_back(obs);
};
addCharacter(player.get());
for (const auto& npc : npcs) {
addCharacter(npc.get());
}
return navigation->findPathToNearest(start, end, dynamicObstacles);
};
};
if (player) {
player->setPathPlanner(makePlanner(player.get()));
}
for (auto& npc : npcs) {
if (npc) {
npc->setPathPlanner(makePlanner(npc.get()));
}
}
}
bool Location::switchNavigation(int index)
{
if (index < 0 || index >= static_cast<int>(navigationMaps.size())) {
std::cerr << "[NAV] switchNavigation: index " << index << " out of range\n";
return false;
}
state.activeNavigationIndex = index;
navigation = &navigationMaps[index];
// Force all characters to replan their paths against the new nav map.
if (player) player->forceReplan();
for (auto& npc : npcs) {
if (npc) npc->forceReplan();
}
if (editorMode == EditorMode::Navigation) {
editor.buildNavMeshes();
}
std::cout << "[NAV] Switched to navigation map " << index << "\n";
return true;
}
InteractiveObject* Location::raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir) {
if (interactiveObjects.empty()) {
//std::cout << "[RAYCAST] No interactive objects to check" << std::endl;
return nullptr;
}
//std::cout << "[RAYCAST] Starting raycast with " << interactiveObjects.size() << " objects" << std::endl;
//std::cout << "[RAYCAST] Ray origin: (" << rayOrigin.x() << ", " << rayOrigin.y() << ", " << rayOrigin.z() << ")" << std::endl;
//std::cout << "[RAYCAST] Ray dir: (" << rayDir.x() << ", " << rayDir.y() << ", " << rayDir.z() << ")" << std::endl;
float closestDistance = FLT_MAX;
InteractiveObject* closestObject = nullptr;
for (auto& intObj : interactiveObjects) {
//std::cout << "[RAYCAST] Checking object: " << intObj.loadedObject.name << " (active: " << intObj.isActive << ")" << std::endl;
if (!intObj.state.isActive || intObj.state.isAnimating) {
//std::cout << "[RAYCAST] -> Object inactive or animating, skipping" << std::endl;
continue;
}
if (state.isDarklands) {
if (!intObj.loadedObject.textureDarklands) continue;
} else {
if (!intObj.loadedObject.texture) continue;
}
//std::cout << "[RAYCAST] Position: (" << intObj.position.x() << ", " << intObj.position.y() << ", "
// << intObj.position.z() << "), Radius: " << intObj.interactionRadius << std::endl;
const bool hasBox = !(intObj.state.boundsMin.isZero() && intObj.state.boundsMax.isZero());
if (hasBox) {
const Eigen::Vector3f worldMin = intObj.state.position + intObj.state.boundsMin;
const Eigen::Vector3f worldMax = intObj.state.position + intObj.state.boundsMax;
float tMin = 0.1f;
float tMax = FLT_MAX;
bool miss = false;
for (int axis = 0; axis < 3; ++axis) {
if (std::abs(rayDir[axis]) < 1e-8f) {
if (rayOrigin[axis] < worldMin[axis] || rayOrigin[axis] > worldMax[axis]) {
miss = true;
break;
}
} else {
const float invD = 1.0f / rayDir[axis];
float t0 = (worldMin[axis] - rayOrigin[axis]) * invD;
float t1 = (worldMax[axis] - rayOrigin[axis]) * invD;
if (invD < 0.0f) std::swap(t0, t1);
tMin = max(tMin, t0);
tMax = min(tMax, t1);
if (tMax < tMin) { miss = true; break; }
}
}
if (!miss && tMin < closestDistance) {
closestDistance = tMin;
closestObject = &intObj;
}
} else {
Eigen::Vector3f toObject = intObj.state.position - rayOrigin;
float distanceAlongRay = toObject.dot(rayDir);
if (distanceAlongRay < 0.1f) {
continue;
}
Eigen::Vector3f closestPointOnRay = rayOrigin + rayDir * distanceAlongRay;
float distToObject = (closestPointOnRay - intObj.state.position).norm();
if (distToObject <= intObj.state.interactionRadius && distanceAlongRay < closestDistance) {
closestDistance = distanceAlongRay;
closestObject = &intObj;
}
}
}
/*
if (closestObject) {
std::cout << "[RAYCAST] *** RAYCAST SUCCESS: Found object " << closestObject->loadedObject.name << " ***" << std::endl;
}
else {
std::cout << "[RAYCAST] No objects hit" << std::endl;
}
*/
return closestObject;
}
Character* Location::raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance) {
// Every NPC is treated as a vertical cylinder: radius 1.0m, height 1.85m,
// base at npc->position (the model's foot). Intersection = circle hit in the
// XZ plane, then clip the entry/exit t against the [yFoot, yFoot+height] slab.
static constexpr float NPC_CLICK_RADIUS = 1.0f;
static constexpr float NPC_CLICK_HEIGHT = 1.85f;
Character* closestNpc = nullptr;
float closestDist = maxDistance;
//std::cout << "[RAYCAST_NPC] Starting raycast with " << npcs.size() << " npcs" << std::endl;
for (auto& npc : npcs) {
if (npc->getHp() <= 0.f) {
//std::cout << "[RAYCAST_NPC] " << npc->state.npcId << " is dead, skipping" << std::endl;
continue;
}
if (!npc->state.enabled) continue;
const float dx = rayOrigin.x() - npc->state.position.x();
const float dz = rayOrigin.z() - npc->state.position.z();
const float a = rayDir.x() * rayDir.x() + rayDir.z() * rayDir.z();
if (a < 1e-6f) continue; // purely-vertical ray; not a real click case
const float b = 2.0f * (dx * rayDir.x() + dz * rayDir.z());
const float c = dx * dx + dz * dz - NPC_CLICK_RADIUS * NPC_CLICK_RADIUS;
const float disc = b * b - 4.0f * a * c;
if (disc < 0.0f) continue; // ray misses the infinite cylinder
const float sqrtDisc = std::sqrt(disc);
const float tNear = (-b - sqrtDisc) / (2.0f * a);
const float tFar = (-b + sqrtDisc) / (2.0f * a);
float entryT = max(tNear, 0.1f);
float exitT = tFar;
// Clip against the cylinder's vertical extent.
const float yMin = npc->state.position.y();
const float yMax = yMin + NPC_CLICK_HEIGHT;
if (std::abs(rayDir.y()) > 1e-6f) {
const float tYa = (yMin - rayOrigin.y()) / rayDir.y();
const float tYb = (yMax - rayOrigin.y()) / rayDir.y();
entryT = max(entryT, min(tYa, tYb));
exitT = min(exitT, max(tYa, tYb));
}
else if (rayOrigin.y() < yMin || rayOrigin.y() > yMax) {
continue; // horizontal ray passing above or below the cylinder
}
if (entryT > exitT) continue;
//std::cout << "[RAYCAST_NPC] " << npc->npcId << " hit at t=" << entryT << std::endl;
if (entryT < closestDist) {
closestDist = entryT;
closestNpc = npc.get();
}
}
if (closestNpc) {
//std::cout << "[RAYCAST_NPC] HIT: " << closestNpc->npcId << std::endl;
}
else {
//std::cout << "[RAYCAST_NPC] No NPC hit" << std::endl;
}
return closestNpc;
}
void Location::drawGame()
{
glClearColor(0.53, 0.81, 0.92, 1.0f);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
renderer.shaderManager.PushShader("fog");
renderer.RenderUniform1i(textureUniformName, 0);
const float playerEyePos[3] = { 0.0f, 0.0f, -Environment::zoom };
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
//renderer.TranslateMatrix({ 0, -6.f, 0 });
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 });
//glBindTexture(GL_TEXTURE_2D, roomTexture->getTexID());
//renderer.DrawVertexRenderStruct(roomMesh);
renderer.RenderUniform1f("uAlpha", 1.0f);
for (auto& [name, gameObj] : gameObjects) {
if (!gameObj.texture) continue;
glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID());
renderer.DrawVertexRenderStruct(gameObj.mesh);
}
for (auto& intObj : interactiveObjects) {
if (intObj.state.isActive) {
intObj.draw(renderer);
}
}
renderer.RenderUniform1f("uAlpha", 1.0f);
const Eigen::Matrix4f currentView = renderer.GetCurrentModelViewMatrix();
if (player) player->prepareHitSparksForDraw(currentView);
for (auto& npc : npcs) npc->prepareHitSparksForDraw(currentView);
for (auto& tz : teleportZones) tz.prepareForDraw(currentView);
if (player) player->draw(renderer);
for (auto& npc : npcs) npc->draw(renderer);
for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height);
if (editorMode == EditorMode::Navigation) {
editor.drawNavigation();
editor.drawPoints();
}
if (editorMode == EditorMode::InteractiveObjects) {
editor.drawInteractiveObjectBounds();
}
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
if (npcNameText) {
Eigen::Matrix4f proj = MakePerspectiveMatrix(1.0f / 1.5f,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
for (auto& npc : npcs) {
if (npc) npc->drawName(*npcNameText, currentView, proj);
}
if (player) player->drawHealthBar(renderer, currentView, proj);
for (auto& npc : npcs) {
if (npc) npc->drawHealthBar(renderer, currentView, proj);
}
}
}
void Location::drawShadowDepthPass()
{
if (!shadowMap) return;
const Eigen::Vector3f& sceneCenter = player ? player->state.position : Eigen::Vector3f::Zero();
shadowMap->updateLightSpaceMatrix(sceneCenter);
shadowMap->bind();
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
// Use front-face culling during depth pass to reduce shadow acne on lit faces
//glCullFace(GL_FRONT);
glEnable(GL_CULL_FACE);
renderer.shaderManager.PushShader("shadow_depth");
// Set up light's orthographic projection
const Eigen::Matrix4f& lightProj = shadowMap->getLightProjectionMatrix();
const Eigen::Matrix4f& lightView = shadowMap->getLightViewMatrix();
// Push the light's projection matrix via the 6-param ortho overload won't
// match our pre-computed matrix. Instead, use the raw stack approach:
// push a dummy projection then overwrite via PushSpecialMatrix-style.
// Simpler: push ortho then push the light view as modelview.
renderer.PushProjectionMatrix(
-40.0f, 40.0f,
-40.0f, 40.0f,
0.1f, 100.0f);
const Eigen::Vector3f& lightDir = shadowMap->getLightDirection();
Eigen::Vector3f lightPos = sceneCenter - lightDir * 50.0f;
Eigen::Vector3f up(0.0f, 1.0f, 0.0f);
if (std::abs(lightDir.dot(up)) > 0.99f) {
up = Eigen::Vector3f(0.0f, 0.0f, 1.0f);
}
// Build the light view matrix and push it
renderer.PushSpecialMatrix(lightView);
// Draw static geometry
//renderer.DrawVertexRenderStruct(roomMesh);
for (auto& [name, gameObj] : gameObjects) {
renderer.DrawVertexRenderStruct(gameObj.mesh);
}
for (auto& intObj : interactiveObjects) {
if (intObj.state.castShadow && intObj.loadedObject.texture) {
renderer.PushMatrix();
renderer.TranslateMatrix(intObj.state.position);
renderer.DrawVertexRenderStruct(intObj.loadedObject.mesh);
renderer.PopMatrix();
}
}
// Draw characters (they handle their own skinning shader switch internally)
if (player) player->drawShadowDepth(renderer);
for (auto& npc : npcs) npc->drawShadowDepth(renderer);
renderer.PopMatrix(); // light view
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
//glCullFace(GL_BACK);
glDisable(GL_CULL_FACE);
shadowMap->unbind();
}
void Location::drawGameWithShadows()
{
glClearColor(0.53, 0.81, 0.92, 1.0f);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
#ifdef DEBUG_LIGHT
// Debug mode: render from the light's point of view using the plain
// textured shader so we can see what the shadow map "sees".
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.PushProjectionMatrix(
-40.0f, 40.0f,
-40.0f, 40.0f,
0.1f, 1000.0f);
renderer.PushSpecialMatrix(shadowMap->getLightViewMatrix());
#else
static const std::string shadowShaderName = "fog_shadow";
renderer.shaderManager.PushShader(shadowShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.RenderUniform1i("uShadowMap", 1);
const float playerEyePos[3] = { 0.0f, 0.0f, -Environment::zoom };
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos);
// Bind shadow map texture to unit 1
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, shadowMap->getDepthTexture());
glActiveTexture(GL_TEXTURE0);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
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 });
// Capture the camera view matrix and compute uLightFromCamera
cameraViewMatrix = renderer.GetCurrentModelViewMatrix();
Eigen::Matrix4f cameraViewInverse = cameraViewMatrix.inverse();
Eigen::Matrix4f lightFromCamera = shadowMap->getLightSpaceMatrix() * cameraViewInverse;
renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data());
// Light direction in camera space for diffuse lighting
Eigen::Vector3f lightDirCamera = cameraViewMatrix.block<3, 3>(0, 0) * shadowMap->getLightDirection();
renderer.RenderUniform3fv("uLightDir", lightDirCamera.data());
#endif
CheckGlError(__FILE__, __LINE__);
//glBindTexture(GL_TEXTURE_2D, roomTexture->getTexID());
//renderer.DrawVertexRenderStruct(roomMesh);
CheckGlError(__FILE__, __LINE__);
renderer.RenderUniform1f("uAlpha", 1.0f);
for (auto& [name, gameObj] : gameObjects) {
if (!gameObj.texture) continue;
glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID());
renderer.DrawVertexRenderStruct(gameObj.mesh);
}
CheckGlError(__FILE__, __LINE__);
for (auto& intObj : interactiveObjects) {
if (intObj.state.isActive) {
intObj.draw(renderer);
}
}
CheckGlError(__FILE__, __LINE__);
#ifdef DEBUG_LIGHT
// In debug-light mode characters use the plain shaders (draw normally
// but from the light's viewpoint — projection/view already on stack).
{
const Eigen::Matrix4f currentView = renderer.GetCurrentModelViewMatrix();
if (player) player->prepareHitSparksForDraw(currentView);
for (auto& npc : npcs) npc->prepareHitSparksForDraw(currentView);
}
if (player) player->draw(renderer);
for (auto& npc : npcs) npc->draw(renderer);
#else
// Characters use their own shadow-aware shaders
CheckGlError(__FILE__, __LINE__);
if (player) player->prepareHitSparksForDraw(cameraViewMatrix);
for (auto& npc : npcs) npc->prepareHitSparksForDraw(cameraViewMatrix);
for (auto& tz : teleportZones) tz.prepareForDraw(cameraViewMatrix);
if (player) player->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
CheckGlError(__FILE__, __LINE__);
for (auto& npc : npcs) npc->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height);
#endif
if (editorMode == EditorMode::Navigation) {
editor.drawNavigation();
editor.drawPoints();
}
if (editorMode == EditorMode::InteractiveObjects) {
editor.drawInteractiveObjectBounds();
}
CheckGlError(__FILE__, __LINE__);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
if (npcNameText) {
Eigen::Matrix4f proj = MakePerspectiveMatrix(1.0f / 1.5f,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
for (auto& npc : npcs) {
if (npc) npc->drawName(*npcNameText, cameraViewMatrix, proj);
}
if (player) player->drawHealthBar(renderer, cameraViewMatrix, proj);
for (auto& npc : npcs) {
if (npc) npc->drawHealthBar(renderer, cameraViewMatrix, proj);
}
}
}
void Location::drawGameDarklands()
{
glClearColor(0.05f, 0.05f, 0.2f, 1.0f);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
renderer.shaderManager.PushShader("darklands_fog");
renderer.RenderUniform1i(textureUniformName, 0);
const float playerEyePos[3] = { 0.0f, 0.0f, -Environment::zoom };
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0, 0, -1.0f * Environment::zoom });
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 });
renderer.RenderUniform1f("uAlpha", 1.0f);
for (auto& [name, gameObj] : gameObjects) {
if (!gameObj.textureDarklands) continue;
glBindTexture(GL_TEXTURE_2D, gameObj.textureDarklands->getTexID());
renderer.DrawVertexRenderStruct(gameObj.mesh);
}
for (auto& intObj : interactiveObjects) {
if (intObj.state.isActive) {
intObj.drawDarklands(renderer);
}
}
renderer.RenderUniform1f("uAlpha", 1.0f);
const Eigen::Matrix4f currentView = renderer.GetCurrentModelViewMatrix();
if (player) player->prepareHitSparksForDraw(currentView);
for (auto& npc : npcs) npc->prepareHitSparksForDraw(currentView);
for (auto& tz : teleportZones) tz.prepareForDraw(currentView);
if (player) player->draw(renderer);
for (auto& npc : npcs) npc->draw(renderer);
for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height);
if (editorMode == EditorMode::Navigation) {
editor.drawNavigation();
editor.drawPoints();
}
if (editorMode == EditorMode::InteractiveObjects) {
editor.drawInteractiveObjectBounds();
}
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
if (npcNameText) {
Eigen::Matrix4f proj = MakePerspectiveMatrix(1.0f / 1.5f,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
for (auto& npc : npcs) {
if (npc) npc->drawName(*npcNameText, currentView, proj);
}
if (player) player->drawHealthBar(renderer, currentView, proj);
for (auto& npc : npcs) {
if (npc) npc->drawHealthBar(renderer, currentView, proj);
}
}
}
void Location::drawNightShadowDepthPass(const PointLight& light)
{
if (!shadowMap) return;
static constexpr float kSpotFovY = static_cast<float>(M_PI) * 0.5f; // 90°
shadowMap->updateSpotlightMatrix(light.position, light.direction, kSpotFovY, 0.1f, 20.0f);
shadowMap->bind();
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_CULL_FACE);
renderer.shaderManager.PushShader("shadow_depth");
renderer.PushPerspectiveProjectionMatrix(kSpotFovY, 1.0f, 0.1f, 20.0f);
renderer.PushSpecialMatrix(shadowMap->getLightViewMatrix());
for (auto& [name, gameObj] : gameObjects) {
renderer.DrawVertexRenderStruct(gameObj.mesh);
}
for (auto& intObj : interactiveObjects) {
if (intObj.state.castShadowNight && intObj.loadedObject.texture) {
renderer.PushMatrix();
renderer.TranslateMatrix(intObj.state.position);
renderer.DrawVertexRenderStruct(intObj.loadedObject.mesh);
renderer.PopMatrix();
}
}
if (player) player->drawShadowDepth(renderer);
for (auto& npc : npcs) npc->drawShadowDepth(renderer);
renderer.PopMatrix(); // light view
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
glDisable(GL_CULL_FACE);
shadowMap->unbind();
}
void Location::drawGameNight()
{
// --- Determine which lights are active this frame ---
// autoLight=true lights only activate when they are the nearest light to the player.
// autoLight=false lights are always active.
int nearestLightIdx = -1;
if (player && !pointLights.empty()) {
float minDist = FLT_MAX;
for (int i = 0; i < static_cast<int>(pointLights.size()); ++i) {
const float d = (pointLights[i].position - player->state.position).norm();
if (d < minDist) { minDist = d; nearestLightIdx = i; }
}
}
static constexpr int MAX_POINT_LIGHTS = 4;
std::vector<int> activeLightIndices;
activeLightIndices.reserve(pointLights.size());
for (int i = 0; i < static_cast<int>(pointLights.size()); ++i) {
const PointLight& pl = pointLights[i];
bool active = !pl.autoLight;
if (!active && player) {
if (pl.autoLightLimitX > 0.0f && pl.autoLightLimitY > 0.0f) {
// Rectangular activation zone — independent of nearest-light heuristic
const float dx = std::abs(player->state.position.x() - pl.position.x());
const float dz = std::abs(player->state.position.z() - pl.position.z());
active = (dx <= pl.autoLightLimitX && dz <= pl.autoLightLimitY);
} else {
// Legacy: only the nearest autoLight activates, with optional distance cap
const float dist = (pl.position - player->state.position).norm();
active = (i == nearestLightIdx) &&
(pl.autoLightDistance <= 0.0f || dist <= pl.autoLightDistance);
}
}
if (active) {
activeLightIndices.push_back(i);
if (static_cast<int>(activeLightIndices.size()) >= MAX_POINT_LIGHTS) break;
}
}
// Shadow caster = nearest active light to player
const PointLight* shadowLight = nullptr;
if (player && !activeLightIndices.empty()) {
float minDist = FLT_MAX;
for (int idx : activeLightIndices) {
const float d = (pointLights[idx].position - player->state.position).norm();
if (d < minDist) { minDist = d; shadowLight = &pointLights[idx]; }
}
}
const bool hasShadows = (shadowMap != nullptr && shadowLight != nullptr);
// Ambient and fog color: different for plain night vs dawn
static const float kNightAmbient[3] = { 0.37f, 0.37f, 0.37f };
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 = state.isDawn ? kDawnAmbient : kNightAmbient;
const float* fogColor = state.isDawn ? kDawnFogColor : kNightFogColor;
if (hasShadows) {
drawNightShadowDepthPass(*shadowLight);
}
// --- Main render pass ---
glClearColor(0.01f, 0.01f, 0.05f, 1.0f);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
const std::string mainShader = hasShadows ? "night_fog_shadow" : "night_fog";
renderer.shaderManager.PushShader(mainShader);
renderer.RenderUniform1i(textureUniformName, 0);
if (hasShadows) {
renderer.RenderUniform1i("uShadowMap", 1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, shadowMap->getDepthTexture());
glActiveTexture(GL_TEXTURE0);
}
const float playerEyePos[3] = { 0.0f, 0.0f, -Environment::zoom };
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos);
renderer.RenderUniform3fv("uAmbientColor", ambientColor);
renderer.RenderUniform3fv("uFogColor", fogColor);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0, 0, -1.0f * Environment::zoom });
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 });
cameraViewMatrix = renderer.GetCurrentModelViewMatrix();
// Transform active spot lights to eye space and build uniform arrays
float plPosEye[MAX_POINT_LIGHTS * 3] = {};
float plDirEye[MAX_POINT_LIGHTS * 3] = {};
float plColors[MAX_POINT_LIGHTS * 3] = {};
float plWorldXZ[MAX_POINT_LIGHTS * 2] = {};
float plLimitXZ[MAX_POINT_LIGHTS * 2] = {};
const Eigen::Matrix3f camRot = cameraViewMatrix.block<3, 3>(0, 0);
const int lightCount = static_cast<int>(activeLightIndices.size());
for (int j = 0; j < lightCount; ++j) {
const PointLight& pl = pointLights[activeLightIndices[j]];
const Eigen::Vector4f worldPos(pl.position.x(), pl.position.y(), pl.position.z(), 1.0f);
const Eigen::Vector4f eyePos4 = cameraViewMatrix * worldPos;
plPosEye[j * 3 + 0] = eyePos4.x();
plPosEye[j * 3 + 1] = eyePos4.y();
plPosEye[j * 3 + 2] = eyePos4.z();
const Eigen::Vector3f eyeDir = camRot * pl.direction;
plDirEye[j * 3 + 0] = eyeDir.x();
plDirEye[j * 3 + 1] = eyeDir.y();
plDirEye[j * 3 + 2] = eyeDir.z();
plColors[j * 3 + 0] = pl.color.x();
plColors[j * 3 + 1] = pl.color.y();
plColors[j * 3 + 2] = pl.color.z();
plWorldXZ[j * 2 + 0] = pl.position.x();
plWorldXZ[j * 2 + 1] = pl.position.z();
plLimitXZ[j * 2 + 0] = pl.limitX;
plLimitXZ[j * 2 + 1] = pl.limitY;
}
// Compute camera view inverse (needed for shadow lookup and skinning world-pos recovery)
const Eigen::Matrix4f cameraViewInverse = cameraViewMatrix.inverse();
// Compute light-from-camera matrix for shadow lookup
Eigen::Matrix4f lightFromCamera = Eigen::Matrix4f::Identity();
if (hasShadows) {
lightFromCamera = shadowMap->getLightSpaceMatrix() * cameraViewInverse;
renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data());
// Pass first active light's eye-space direction as uLightDir for shadow bias
renderer.RenderUniform3fv("uLightDir", plDirEye);
}
renderer.RenderUniform1i("uPointLightCount", lightCount);
if (lightCount > 0) {
renderer.RenderUniform3fvArray("uPointLightPos[0]", lightCount, plPosEye);
renderer.RenderUniform3fvArray("uPointLightDir[0]", lightCount, plDirEye);
renderer.RenderUniform3fvArray("uPointLightColor[0]", lightCount, plColors);
renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", lightCount, plWorldXZ);
renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", lightCount, plLimitXZ);
}
renderer.RenderUniform1f("uAlpha", 1.0f);
for (auto& [name, gameObj] : gameObjects) {
if (!gameObj.texture) continue;
glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID());
renderer.DrawVertexRenderStruct(gameObj.mesh);
}
for (auto& intObj : interactiveObjects) {
if (intObj.state.isActive) {
intObj.draw(renderer);
}
}
renderer.RenderUniform1f("uAlpha", 1.0f);
if (player) player->prepareHitSparksForDraw(cameraViewMatrix);
for (auto& npc : npcs) npc->prepareHitSparksForDraw(cameraViewMatrix);
for (auto& tz : teleportZones) tz.prepareForDraw(cameraViewMatrix);
if (hasShadows) {
if (player) player->drawNightWithShadow(renderer, plPosEye, plDirEye, plColors, lightCount,
plWorldXZ, plLimitXZ, cameraViewInverse,
lightFromCamera, shadowMap->getDepthTexture(), ambientColor, fogColor);
for (auto& npc : npcs) npc->drawNightWithShadow(renderer, plPosEye, plDirEye, plColors, lightCount,
plWorldXZ, plLimitXZ, cameraViewInverse,
lightFromCamera, shadowMap->getDepthTexture(), ambientColor, fogColor);
} else {
if (player) player->drawNight(renderer, plPosEye, plDirEye, plColors, lightCount,
plWorldXZ, plLimitXZ, cameraViewInverse, ambientColor, fogColor);
for (auto& npc : npcs) npc->drawNight(renderer, plPosEye, plDirEye, plColors, lightCount,
plWorldXZ, plLimitXZ, cameraViewInverse, ambientColor, fogColor);
}
for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height);
if (editorMode == EditorMode::Navigation) {
editor.drawNavigation();
editor.drawPoints();
}
if (editorMode == EditorMode::InteractiveObjects) {
editor.drawInteractiveObjectBounds();
}
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
if (npcNameText) {
Eigen::Matrix4f proj = MakePerspectiveMatrix(1.0f / 1.5f,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
for (auto& npc : npcs) {
if (npc) npc->drawName(*npcNameText, cameraViewMatrix, proj);
}
if (player) player->drawHealthBar(renderer, cameraViewMatrix, proj);
for (auto& npc : npcs) {
if (npc) npc->drawHealthBar(renderer, cameraViewMatrix, proj);
}
}
}
bool Location::setNavigationAreaAvailable(const std::string& areaName, bool available)
{
if (!navigation) return false;
return navigation->setAreaAvailable(areaName, available);
}
int Location::findNpcIndex(const Character* npc) const
{
for (int i = 0; i < static_cast<int>(npcs.size()); ++i) {
if (npcs[i].get() == npc) return i;
}
return -1;
}
void Location::fireBumpCallbacks(Character* mover, Character* standing)
{
if (mover->state.isPlayer) {
// Player walked into a standing NPC
const int idx = findNpcIndex(standing);
if (idx < 0) return;
auto& cooldown = npcBumpedByPlayerCooldown[idx];
if (cooldown <= 0.f) {
cooldown = NPC_BUMP_CALLBACK_COOLDOWN;
scriptEngine.callNpcBumpedByPlayerCallback(idx);
}
} else if (standing->state.isPlayer) {
// Moving NPC walked into the standing player
const int idx = findNpcIndex(mover);
if (idx < 0) return;
auto& cooldown = npcBumpsPlayerCooldown[idx];
if (cooldown <= 0.f) {
cooldown = NPC_BUMP_CALLBACK_COOLDOWN;
scriptEngine.callNpcBumpsPlayerCallback(idx);
}
}
}
void Location::nudgeCharacterAside(Character* standing, const Eigen::Vector3f& awayFrom)
{
if (!standing || standing->state.isPlayer) return;
if (!navigation || !navigation->isReady()) return;
static constexpr float kNudgeDist = 1.2f;
Eigen::Vector3f dir = standing->state.position - awayFrom;
dir.y() = 0.f;
if (dir.norm() < 1e-3f) dir = Eigen::Vector3f(1.f, 0.f, 0.f);
else dir.normalize();
const float angles[] = { 0.f, static_cast<float>(M_PI * 0.5), static_cast<float>(-M_PI * 0.5), static_cast<float>(M_PI) };
for (float angle : angles) {
Eigen::Vector3f rotated = Eigen::AngleAxisf(angle, Eigen::Vector3f::UnitY()) * dir;
Eigen::Vector3f candidate = standing->state.position + rotated * kNudgeDist;
candidate.y() = 0.f;
if (navigation->isWalkable(candidate)) {
standing->setTarget(candidate);
return;
}
}
}
void Location::resolveCharacterCollisions()
{
std::vector<Character*> characters;
characters.reserve(npcs.size() + 1);
if (player) {
characters.push_back(player.get());
}
for (auto& npc : npcs) {
if (npc) {
characters.push_back(npc.get());
}
}
if (characters.size() < 2) {
return;
}
static constexpr int kIterations = 3;
static constexpr float kMinSeparationEps = 1e-4f;
for (int iter = 0; iter < kIterations; ++iter) {
for (size_t i = 0; i < characters.size(); ++i) {
for (size_t j = i + 1; j < characters.size(); ++j) {
Character* a = characters[i];
Character* b = characters[j];
if (!a || !b) continue;
if (a->getHp() <= 0.f || b->getHp() <= 0.f || !a->state.enabled || !b->state.enabled) continue;
const float minDist = a->state.collisionRadius + b->state.collisionRadius;
if (minDist <= 0.f) continue;
const Eigen::Vector2f pa(a->state.position.x(), a->state.position.z());
const Eigen::Vector2f pb(b->state.position.x(), b->state.position.z());
const Eigen::Vector2f delta = pb - pa;
const float dist = delta.norm();
if (dist >= minDist) {
continue;
}
Eigen::Vector2f normal(1.f, 0.f);
if (dist > kMinSeparationEps) {
normal = delta / dist;
}
const float penetration = (minDist - dist);
const float push = penetration * 0.5f;
Eigen::Vector3f newA = a->state.position;
Eigen::Vector3f newB = b->state.position;
newA.x() -= normal.x() * push;
newA.z() -= normal.y() * push;
newA.y() = 0.f;
newB.x() += normal.x() * push;
newB.z() += normal.y() * push;
newB.y() = 0.f;
const bool aWasMoving = a->isMoving();
const bool bWasMoving = b->isMoving();
if (navigation && navigation->isReady()) {
const bool aOk = navigation->isWalkable(newA);
const bool bOk = navigation->isWalkable(newB);
if (aOk && bOk) {
a->state.position = newA;
b->state.position = newB;
}
else if (aOk && !bOk) {
a->state.position = newA;
}
else if (!aOk && bOk) {
b->state.position = newB;
}
}
else {
a->state.position = newA;
b->state.position = newB;
}
if (a->state.isPlayer && !aWasMoving) a->stopInPlace();
if (b->state.isPlayer && !bWasMoving) b->stopInPlace();
if (aWasMoving && !bWasMoving) {
nudgeCharacterAside(b, a->state.position);
fireBumpCallbacks(a, b);
} else if (bWasMoving && !aWasMoving) {
nudgeCharacterAside(a, b->state.position);
fireBumpCallbacks(b, a);
}
}
}
}
}
void Location::updateDynamicReplans(int64_t deltaMs)
{
static constexpr float kMovedEps = 0.05f;
static constexpr float kReplanTriggerDist = 1.8f;
static constexpr int64_t kReplanCooldownMs = 500;
static constexpr float kStuckMovementThreshold = 0.02f;
static constexpr int kStuckFrameThreshold = 4;
static constexpr float kStuckPauseMin = 0.5f;
static constexpr float kStuckPauseMax = 1.0f;
const float deltaS = static_cast<float>(deltaMs) / 1000.f;
// Tick down pause timers; replan NPCs when their pause expires.
for (auto it = stuckPauseRemainingS.begin(); it != stuckPauseRemainingS.end();) {
it->second -= deltaS;
if (it->second <= 0.f) {
if (!it->first->state.isPlayer) {
it->first->forceReplan();
}
it = stuckPauseRemainingS.erase(it);
} else {
++it;
}
}
for (auto it = replanCooldownRemainingMs.begin(); it != replanCooldownRemainingMs.end();) {
it->second -= deltaMs;
if (it->second <= 0) {
it = replanCooldownRemainingMs.erase(it);
}
else {
++it;
}
}
std::vector<Character*> characters;
characters.reserve(npcs.size() + 1);
if (player) characters.push_back(player.get());
for (auto& npc : npcs) if (npc) characters.push_back(npc.get());
std::vector<Character*> movers;
movers.reserve(characters.size());
for (Character* c : characters) {
if (!c) continue;
auto it = lastCharacterPositions.find(c);
if (it == lastCharacterPositions.end()) {
lastCharacterPositions[c] = c->state.position;
continue;
}
const Eigen::Vector3f prev = it->second;
const float moved = (c->state.position - prev).norm();
it->second = c->state.position;
if (moved > kMovedEps) {
movers.push_back(c);
}
if (c->isMoving()) {
if (moved < kStuckMovementThreshold) {
if (++stuckFrameCounts[c] >= kStuckFrameThreshold) {
c->stopInPlace();
stuckFrameCounts[c] = 0;
if (stuckPauseRemainingS.find(c) == stuckPauseRemainingS.end()) {
if (!c->state.isPlayer) {
static std::mt19937 rng(std::random_device{}());
std::uniform_real_distribution<float> pauseDist(kStuckPauseMin, kStuckPauseMax);
stuckPauseRemainingS[c] = pauseDist(rng);
}
// Player just stops; replan is not scheduled.
}
}
} else {
stuckFrameCounts[c] = 0;
}
} else {
stuckFrameCounts[c] = 0;
}
}
if (movers.empty()) {
return;
}
for (Character* mover : movers) {
if (!mover) continue;
for (Character* walker : characters) {
if (!walker || walker == mover) continue;
if (walker->getHp() <= 0.f || !walker->state.enabled) continue;
if (!walker->isMoving()) continue;
if (replanCooldownRemainingMs.find(walker) != replanCooldownRemainingMs.end()) {
continue;
}
const Eigen::Vector3f nextTarget = walker->getCurrentNavigationTarget();
const float distToSegment = distancePointToSegmentXZ(mover->state.position, walker->state.position, nextTarget);
if (distToSegment > kReplanTriggerDist) {
continue;
}
walker->forceReplan();
replanCooldownRemainingMs[walker] = kReplanCooldownMs;
}
}
}
void Location::update(int64_t delta)
{
for (auto& intObj : interactiveObjects) {
intObj.update(delta);
}
auto resolver = [&](int idx) -> Character* {
if (idx == CharacterState::kNoTarget) return nullptr;
if (idx == CharacterState::kPlayerIndex) return player.get();
if (idx >= 0 && idx < static_cast<int>(npcs.size())) return npcs[idx].get();
return nullptr;
};
if (player) {
player->update(delta, resolver);
dialogueSystem.update(static_cast<int>(delta));
updateTriggerZones(player->state.position);
}
bool npcAttacking = false;
for (auto& npc : npcs)
{
npc->update(delta, resolver);
if (npc->state.enabled && npc->state.canAttack && (!npc->state.pathWaypoints.empty() || npc->state.battle_state != 0) && npc->getHp() > 0.f)
{
//std::cout << "[NPC] NPC '" << npc->state.npcId << "' is attacking target index " << npc->state.attackTargetIndex << std::endl;
npcAttacking = true;
}
}
if (state.isDarklands)
{
if (npcAttacking && !playBattleMusic)
{
playBattleMusic = true;
if (requestDarklandsPlayBattleMusic) requestDarklandsPlayBattleMusic();
}
else if (!npcAttacking && playBattleMusic)
{
playBattleMusic = false;
if (requestDarklandsPlayNormalMusic) requestDarklandsPlayNormalMusic();
}
}
resolveCharacterCollisions();
updateDynamicReplans(delta);
const float deltaS = static_cast<float>(delta) / 1000.f;
for (auto& [idx, t] : npcBumpedByPlayerCooldown) t -= deltaS;
for (auto& [idx, t] : npcBumpsPlayerCooldown) t -= deltaS;
// Check if player reached target interactive object
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 <= obj->state.approachRadius) {
std::cout << "[PICKUP] Player reached object! Distance: " << distToObject << 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 (!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(obj->loadedObject.name);
}
}
catch (const std::exception& e) {
std::cerr << "[PICKUP] Error calling function: " << e.what() << std::endl;
}
state.targetInteractiveObjectIndex = -1;
}
}
// Check if player reached target NPC for interaction.
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 " << 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[state.targetInteractNpcIndex]->state.faceTargetIndex = CharacterState::kPlayerIndex;
try {
scriptEngine.callNpcInteractCallback(state.targetInteractNpcIndex);
}
catch (const std::exception& e) {
std::cerr << "[NPC] callback error: " << e.what() << std::endl;
}
state.targetInteractNpcIndex = -1;
}
}
for (auto& tz : teleportZones) tz.update(static_cast<float>(delta));
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;
}
}
if (player) {
for (auto& tz : teleportZones) {
if (!tz.active || !tz.automatic) continue;
const float autoR = tz.automaticRadius > 0.0f ? tz.automaticRadius : tz.radius;
const float dist = (player->state.position - tz.position).norm();
if (!tz.automaticInitialized) {
// If the player starts inside the zone (e.g. just arrived here via teleport),
// mark them as already inside so they must leave before it can fire.
tz.automaticPlayerInside = (dist <= autoR);
tz.automaticInitialized = true;
continue;
}
const bool isInside = (dist <= autoR);
if (isInside && !tz.automaticPlayerInside) {
tz.automaticPlayerInside = true;
std::cout << "[TELEPORT] Auto-teleport triggered by zone '" << tz.id << "'" << std::endl;
if (onTeleport) onTeleport(tz.destinationLocation, tz.destinationPosition, tz.destinationRotationY);
return;
} else if (!isInside) {
tz.automaticPlayerInside = false;
}
}
}
}
void Location::handleDown(int64_t fingerId, int eventX, int eventY, int mx, int my)
{
// Calculate ray for picking
if (dialogueSystem.blocksGameplayInput()) {
dialogueSystem.handlePointerReleased(static_cast<float>(mx), Environment::projectionHeight - static_cast<float>(my));
return;
}
player->state.attackTargetIndex = CharacterState::kNoTarget;
// Unproject click to ground plane (y=0) for Viola's walk target
float ndcX = 2.0f * eventX / Environment::width - 1.0f;
float ndcY = 1.0f - 2.0f * eventY / Environment::height;
float aspect = (float)Environment::width / (float)Environment::height;
float tanHalfFov = tan(CAMERA_FOV_Y * 0.5f);
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);
Eigen::Vector3f camUp(sinAzim * sinIncl, cosIncl, -cosAzim * sinIncl);
const Eigen::Vector3f& playerPos = player ? player->state.position : Eigen::Vector3f::Zero();
Eigen::Vector3f camPos = playerPos + Eigen::Vector3f(-sinAzim * cosIncl, sinIncl, cosAzim * cosIncl) * Environment::zoom + Eigen::Vector3f(0,1,0);
Eigen::Vector3f rayDir = (camForward + camRight * (ndcX * aspect * tanHalfFov) + camUp * (ndcY * tanHalfFov)).normalized();
if (editorMode == EditorMode::Navigation) {
if (rayDir.y() < -0.001f) {
const float t = -camPos.y() / rayDir.y();
const Eigen::Vector3f hit = camPos + rayDir * t;
const bool ctrlHeld = (SDL_GetModState() & KMOD_CTRL) != 0;
editor.handleLeftClick(hit, ctrlHeld);
}
return;
}
if (editorMode == EditorMode::InteractiveObjects) {
if (rayDir.y() < -0.001f) {
const float t = -camPos.y() / rayDir.y();
const Eigen::Vector3f hit = camPos + rayDir * t;
const bool ctrlHeld = (SDL_GetModState() & KMOD_CTRL) != 0;
editor.handleInteractiveObjectClick(hit, ctrlHeld);
}
return;
}
std::cout << "[CLICK] Camera position: (" << camPos.x() << ", " << camPos.y() << ", " << camPos.z() << ")" << std::endl;
std::cout << "[CLICK] Ray direction: (" << rayDir.x() << ", " << rayDir.y() << ", " << rayDir.z() << ")" << std::endl;
// First check if we clicked on interactive object
InteractiveObject* clickedObject = raycastInteractiveObjects(camPos, rayDir);
if (clickedObject && player && clickedObject->state.isActive) {
std::cout << "[CLICK] *** SUCCESS: Clicked on interactive object: " << clickedObject->loadedObject.name << " ***" << std::endl;
std::cout << "[CLICK] Object position: (" << clickedObject->state.position.x() << ", "
<< clickedObject->state.position.y() << ", " << clickedObject->state.position.z() << ")" << std::endl;
std::cout << "[CLICK] Player position: (" << player->state.position.x() << ", "
<< player->state.position.y() << ", " << player->state.position.z() << ")" << std::endl;
state.targetInteractiveObjectIndex = findInteractiveObjectIndex(clickedObject);
state.targetInteractNpcIndex = -1;
state.targetTeleportZoneIndex = -1;
player->setTarget(clickedObject->state.hasInteractionPosition
? clickedObject->state.interactionPosition
: clickedObject->state.position);
player->state.attackTargetIndex = CharacterState::kNoTarget;
std::cout << "[CLICK] Player moving to object..." << std::endl;
}
else {
// Check if we clicked on an NPC
Character* clickedNpc = raycastNpcs(camPos, rayDir);
if (clickedNpc && player) {
float distance = (player->state.position - clickedNpc->state.position).norm();
int npcIndex = -1;
for (size_t i = 0; i < npcs.size(); ++i) {
if (npcs[i].get() == clickedNpc) {
npcIndex = static_cast<int>(i);
break;
}
}
if (npcIndex != -1) {
state.targetInteractiveObjectIndex = -1;
state.targetTeleportZoneIndex = -1;
if (clickedNpc->state.canAttack) {
// Hostile NPC: combat logic walks the player in via attackTargetIndex.
player->state.attackTargetIndex = npcIndex;
state.targetInteractNpcIndex = -1;
if (distance <= clickedNpc->state.interactionRadius) {
std::cout << "[CLICK] Hostile NPC " << npcIndex << " in range; firing on_npc_interact" << std::endl;
scriptEngine.callNpcInteractCallback(npcIndex);
}
}
else {
// Peaceful NPC: walk to them, fire on_npc_interact when within talk distance.
player->state.attackTargetIndex = CharacterState::kNoTarget;
if (distance <= NPC_TALK_DISTANCE) {
// Already in talk range — fire immediately, stop, and face the player.
std::cout << "[CLICK] *** SUCCESS: Clicked on NPC index: " << npcIndex << " (in range) ***" << std::endl;
player->setTarget(player->state.position);
npcs[npcIndex]->state.faceTargetIndex = CharacterState::kPlayerIndex;
scriptEngine.callNpcInteractCallback(npcIndex);
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);
state.targetInteractNpcIndex = npcIndex;
}
}
}
}
else if (rayDir.y() < -0.001f && player) {
// Unproject click to ground plane for walk target / teleport detection
float t = -camPos.y() / rayDir.y();
Eigen::Vector3f hit = camPos + rayDir * t;
std::cout << "[CLICK] Clicked on ground at: (" << hit.x() << ", " << hit.z() << ")" << std::endl;
TeleportZone* clickedTeleport = nullptr;
for (auto& tz : teleportZones) {
if (!tz.active) continue;
Eigen::Vector2f d(hit.x() - tz.position.x(), hit.z() - tz.position.z());
if (d.norm() <= tz.radius) { clickedTeleport = &tz; break; }
}
if (clickedTeleport) {
std::cout << "[CLICK] Clicked teleport zone '" << clickedTeleport->id << "'" << std::endl;
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;
state.targetInteractNpcIndex = -1;
state.targetTeleportZoneIndex = -1;
if (onPlayerFloorWalk) onPlayerFloorWalk();
}
}
else {
std::cout << "[CLICK] No valid target found" << std::endl;
}
}
std::cout << "========================================\n" << std::endl;
}
void Location::handleUp(int64_t fingerId, int mx, int my)
{
if (dialogueSystem.blocksGameplayInput()) {
dialogueSystem.handlePointerReleased(static_cast<float>(mx), Environment::projectionHeight - static_cast<float>(my));
return;
}
}
void Location::handleMotion(int64_t fingerId, int eventX, int eventY, int mx, int my)
{
if (dialogueSystem.blocksGameplayInput()) {
dialogueSystem.handlePointerMoved(
static_cast<float>(mx),
Environment::projectionHeight - static_cast<float>(my)
);
}
if (cameraDragging) {
int dx = eventX - lastMouseX;
int dy = eventY - lastMouseY;
lastMouseX = eventX;
lastMouseY = eventY;
const float sensitivity = 0.005f;
state.cameraAzimuth += dx * sensitivity;
state.cameraInclination += dy * sensitivity;
const float minInclination = M_PI * 30.f / 180.f;
const float maxInclination = M_PI * 0.5f;
state.cameraInclination = max(minInclination, min(maxInclination, state.cameraInclination));
}
}
bool Location::requestDialogueStart(const std::string& dialogueId)
{
return dialogueSystem.startDialogue(dialogueId);
}
bool Location::requestCutsceneStart(const std::string& cutsceneId)
{
return dialogueSystem.startCutscene(cutsceneId);
}
void Location::setOnCutsceneFinished(std::function<void(const std::string&)> cb)
{
dialogueSystem.setOnCutsceneFinished(std::move(cb));
}
void Location::setDialogueFlag(const std::string& flag, int value)
{
dialogueSystem.setFlag(flag, value);
}
int Location::getDialogueFlag(const std::string& flag) const
{
return dialogueSystem.getFlag(flag);
}
// ---- Save / Load ----
void Location::save(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);
// Dialogue runtime state
nlohmann::json dialogueState;
dialogueSystem.saveDialogueState(dialogueState);
out["dialogueState"] = std::move(dialogueState);
// Lua global variable state
nlohmann::json scriptState;
scriptEngine.saveScriptGlobals(scriptState);
out["scriptGlobals"] = std::move(scriptState);
// Trigger zone runtime state (by id)
nlohmann::json tzState = nlohmann::json::object();
for (const auto& tz : triggerZones) {
tzState[tz.id] = { {"enabled", tz.enabled}, {"playerInside", tz.playerInside} };
}
out["triggerZoneState"] = std::move(tzState);
// Teleport zone runtime state (by id)
nlohmann::json tpState = nlohmann::json::object();
for (const auto& tp : teleportZones) {
tpState[tp.id] = tp.active;
}
out["teleportZoneState"] = std::move(tpState);
}
void Location::load(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];
}
// Restore trigger zone runtime state (by id)
if (in.contains("triggerZoneState") && in["triggerZoneState"].is_object()) {
const auto& tzs = in["triggerZoneState"];
for (auto& tz : triggerZones) {
if (tzs.contains(tz.id)) {
tz.enabled = tzs[tz.id].value("enabled", tz.enabled);
tz.playerInside = tzs[tz.id].value("playerInside", tz.playerInside);
}
}
}
// Restore teleport zone runtime state (by id)
if (in.contains("teleportZoneState") && in["teleportZoneState"].is_object()) {
const auto& tps = in["teleportZoneState"];
for (auto& tp : teleportZones) {
if (tps.contains(tp.id)) {
tp.active = tps[tp.id].get<bool>();
}
}
}
// 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]);
}
}
// Dialogue runtime state
if (in.contains("dialogueState")) {
dialogueSystem.loadDialogueState(in["dialogueState"]);
}
// Lua globals
if (in.contains("scriptGlobals")) {
scriptEngine.loadScriptGlobals(in["scriptGlobals"]);
}
}
} // namespace ZL