#include "Location.h"
#include "utils/Utils.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
#include "render/TextureManager.h"
#include "TextModel.h"
#include <random>
#include <cmath>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <functional>
#include <memory>
#include <cfloat>
#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)
	{

	}

	void Location::setup(const LocationSetup& params)
	{

		// 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/new/gg_stand_idle001.anim", CONST_ZIP_FILE);
		player->loadBinaryAnimation(AnimationState::WALK, "resources/w/gg/new/gg_walk001.anim", CONST_ZIP_FILE);
		player->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/gg/new/gg_stand_to_action001.anim", CONST_ZIP_FILE);
		
		player->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/gg/gg_action_chop001.anim", CONST_ZIP_FILE);
		player->loadBinaryAnimation(AnimationState::ACTION_ATTACK_2, "resources/w/gg/gg_action_stab001.anim", CONST_ZIP_FILE);
		player->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/gg/gg_action_idle002.anim", CONST_ZIP_FILE);
		player->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/gg/new/gg_action_to_stand001.anim", CONST_ZIP_FILE);
		player->loadBinaryAnimation(AnimationState::ACTION_TO_DEATH, "resources/w/gg/new/gg_die001.anim", CONST_ZIP_FILE);
		player->loadBinaryAnimation(AnimationState::DEATH_IDLE, "resources/w/gg/new/gg_die_idle001.anim", CONST_ZIP_FILE);


		player->weaponTexture = renderer.textureManager.LoadFromPng("resources/w/white.png", CONST_ZIP_FILE);
		player->weaponMesh.data = LoadFromTextFile02("resources/w/gg/knife002.txt", CONST_ZIP_FILE);
		player->weaponMesh.data.Scale(0.1f);
		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->walkSpeed = 3.0f;
		player->rotationSpeed = 8.0f;

		player->modelScale = 1.f;
		player->modelCorrectionRotation = Eigen::Quaternionf(
			Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())
			*
			Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitX())
		
		);

		player->canAttack = true;
		player->isPlayer = true;
		player->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, CONST_ZIP_FILE);
		for (auto& npc : npcs) {
			if (!npc) continue;
			npc->homePosition = Eigen::Vector3f(npc->position.x(), 0.f, npc->position.z());
			if (npc->canAttack) {
				npc->setupHitSparks(sparkTexture);
				npc->attackTarget = player.get();
			}
		}

		loadTeleportZones(params.teleportsJsonPath, CONST_ZIP_FILE);
		loadTriggerZones(params.triggerZonesJsonPath, 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);

		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);

		dialogueSystem.setOnCutsceneFinished([this](const std::string& cutsceneId) {
			scriptEngine.callCutsceneCompleteCallback(cutsceneId);
		});

	}

	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.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::updateTriggerZones(const Eigen::Vector3f& playerPos)
	{
		for (auto& tz : triggerZones) {
			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);
		}

		activeNavigationIndex = 0;
		navigation = navigationMaps.empty() ? nullptr : &navigationMaps[0];

		if (navigationEditorMode && navigation) {
			navigationEditorBuildNavMeshes();
		}

		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->hp <= 0.f || !other->enabled) return;
					if (other->isMoving()) return;

					if (distancePointToSegmentXZ(other->position, start, end) > kDynamicObstacleInfluenceDist) {
						return;
					}

					PathFinder::DynamicObstacle obs;
					obs.position = Eigen::Vector3f(other->position.x(), navigation->getFloorY(), other->position.z());
					obs.radius = (std::max)(0.0f, other->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;
		}
		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 (navigationEditorMode) {
			navigationEditorBuildNavMeshes();
		}
		std::cout << "[NAV] Switched to navigation map " << index << "\n";
		return true;
	}

	void Location::navigationEditorBuildNavMeshes()
	{
		navigationEditorNavMeshes.clear();
		if (!navigation) return;
		const float y = navigation->getFloorY() + 0.02f;
		const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
		for (const auto& obs : navigation->getObstaclePolygons()) {
			if (obs.polygon.size() < 3) continue;
			VertexRenderStruct mesh;
			mesh.data = CreatePolygonFloor(obs.polygon, y, red);
			mesh.RefreshVBO();
			navigationEditorNavMeshes.push_back(std::move(mesh));
		}
	}

	void Location::navigationEditorDrawNavigation()
	{
		renderer.shaderManager.PushShader("defaultColor");
		renderer.SetMatrix();
		for (const auto& mesh : navigationEditorNavMeshes) {
			renderer.DrawVertexRenderStruct(mesh);
		}
		renderer.shaderManager.PopShader();
		renderer.SetMatrix();
	}

	void Location::navigationEditorRebuildPointsMesh()
	{
		VertexDataStruct data;
		const Eigen::Vector3f yellow(1.0f, 1.0f, 0.0f);
		const float y = navigation ? navigation->getFloorY() + 0.05f : 0.05f;
		const float s = 0.2f;

		for (const auto& pt : navigationEditorPoints) {
			// Small upward-pointing equilateral triangle in the XZ plane
			Eigen::Vector3f v0(pt.x(),          y, pt.z() - s * 1.15f);
			Eigen::Vector3f v1(pt.x() - s,      y, pt.z() + s * 0.58f);
			Eigen::Vector3f v2(pt.x() + s,      y, pt.z() + s * 0.58f);

			data.PositionData.push_back(v0);
			data.PositionData.push_back(v1);
			data.PositionData.push_back(v2);
			data.ColorData.push_back(yellow);
			data.ColorData.push_back(yellow);
			data.ColorData.push_back(yellow);
		}

		navigationEditorPointsMesh.data = std::move(data);
		navigationEditorPointsMesh.RefreshVBO();
	}

	void Location::navigationEditorDrawPoints()
	{
		if (navigationEditorPoints.empty()) return;
		renderer.shaderManager.PushShader("defaultColor");
		renderer.SetMatrix();
		renderer.DrawVertexRenderStruct(navigationEditorPointsMesh);
		renderer.shaderManager.PopShader();
		renderer.SetMatrix();
	}

	void Location::navigationEditorHandleLeftClick(const Eigen::Vector3f& hit, bool ctrlHeld)
	{
		if (ctrlHeld) {
			const float removeRadius = 1.0f;
			for (auto it = navigationEditorPoints.begin(); it != navigationEditorPoints.end(); ++it) {
				const float dx = it->x() - hit.x();
				const float dz = it->z() - hit.z();
				if (dx * dx + dz * dz <= removeRadius * removeRadius) {
					navigationEditorPoints.erase(it);
					navigationEditorRebuildPointsMesh();
					std::cout << "[NAV_EDITOR] Removed point, " << navigationEditorPoints.size() << " remaining\n";
					return;
				}
			}
			std::cout << "[NAV_EDITOR] No point found within " << removeRadius << " units of click\n";
		} else {
			navigationEditorPoints.push_back(hit);
			navigationEditorRebuildPointsMesh();
			std::cout << "[NAV_EDITOR] Added point (" << hit.x() << ", " << hit.z()
			          << "), total: " << navigationEditorPoints.size() << "\n";
		}
	}

	void Location::navigationEditorHandleRightClick()
	{
		if (navigationEditorPoints.size() < 3) {
			std::cout << "[NAV_EDITOR] Need at least 3 points to form a polygon (have "
			          << navigationEditorPoints.size() << "); clearing\n";
			navigationEditorPoints.clear();
			navigationEditorRebuildPointsMesh();
			return;
		}

		PathFinder::ObstaclePolygon poly;
		poly.name = "editor_obstacle_" + std::to_string(++navigationEditorObstacleCounter);
		for (const auto& pt : navigationEditorPoints) {
			poly.polygon.emplace_back(pt.x(), pt.z());
		}

		if (navigation) navigation->addObstaclePolygon(poly);
		std::cout << "[NAV_EDITOR] Added obstacle '" << poly.name << "' with "
		          << poly.polygon.size() << " vertices\n";

		// Add a red mesh for the new obstacle polygon so it's visible immediately.
		{
			const float y = navigation ? navigation->getFloorY() + 0.02f : 0.02f;
			const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
			VertexRenderStruct mesh;
			mesh.data = CreatePolygonFloor(poly.polygon, y, red);
			mesh.RefreshVBO();
			navigationEditorNavMeshes.push_back(std::move(mesh));
		}

		navigationEditorPoints.clear();
		navigationEditorRebuildPointsMesh();
	}

	void Location::navigationEditorSave()
	{
		std::string baseName;
		for (int i = 1; ; ++i) {
			char buf[32];
			snprintf(buf, sizeof(buf), "saved_mesh%03d", i);
			baseName = buf;
			if (!std::filesystem::exists(baseName + ".json")) break;
		}

		if (!navigation) return;

		if (navigation->saveConfig(baseName + ".json"))
			std::cout << "[NAV_EDITOR] Saved config to: " << baseName << ".json\n";
		else
			std::cerr << "[NAV_EDITOR] Failed to save config to: " << baseName << ".json\n";

		if (navigation->saveGrid(baseName + ".txt"))
			std::cout << "[NAV_EDITOR] Saved grid to: " << baseName << ".txt\n";
		else
			std::cerr << "[NAV_EDITOR] Failed to save grid to: " << baseName << ".txt\n";
	}

	void Location::navigationEditorReload()
	{
		setupNavigation(navigationMapPaths);
		std::cout << "[NAV_EDITOR] Reloaded navigation maps (" << navigationMapPaths.size() << " maps)\n";
	}

	void Location::gameObjectEditorPlaceTree()
	{
		if (!player) return;

		static std::mt19937 rng(std::random_device{}());
		std::uniform_real_distribution<float> scaleDist(0.8f, 1.2f);
		std::uniform_real_distribution<float> rotDist(0.0f, 360.0f);

		GameObjectData data;
		data.name        = "editor_tree_" + std::to_string(++editorPlacedObjectCounter);
		data.texturePath = "resources/w/exterior/tree001.png";
		data.meshPath    = "resources/w/exterior/tree003.txt";
		data.rotationX   = 0.0f;
		data.rotationY   = rotDist(rng);
		data.rotationZ   = 0.0f;
		data.positionX   = player->position.x();
		data.positionY   = player->position.y();
		data.positionZ   = player->position.z();
		data.scale       = scaleDist(rng);

		LoadedGameObject obj = GameObjectLoader::buildLoadedObject(data, renderer, CONST_ZIP_FILE);
		obj.mesh.data.Move({ data.positionX, data.positionY, data.positionZ });
		obj.mesh.RefreshVBO();

		gameObjects[data.name] = std::move(obj);
		editorPlacedObjects.push_back(data);

		std::cout << "[GAME_EDITOR] Placed '" << data.name << "' at ("
		          << data.positionX << ", " << data.positionZ
		          << ") scale=" << data.scale << " rotY=" << data.rotationY << "\n";
	}

	void Location::gameObjectEditorSave()
	{
		if (editorPlacedObjects.empty()) {
			std::cout << "[GAME_EDITOR] No editor-placed objects to save\n";
			return;
		}

		std::string baseName;
		for (int i = 1; ; ++i) {
			char buf[32];
			snprintf(buf, sizeof(buf), "saved_objects%03d", i);
			baseName = buf;
			if (!std::filesystem::exists(baseName + ".json")) break;
		}

		using json = nlohmann::json;
		json j;
		j["objects"] = json::array();
		for (const auto& d : editorPlacedObjects) {
			json obj;
			obj["name"]        = d.name;
			obj["texturePath"] = d.texturePath;
			obj["meshPath"]    = d.meshPath;
			obj["rotationX"]   = d.rotationX;
			obj["rotationY"]   = d.rotationY;
			obj["rotationZ"]   = d.rotationZ;
			obj["positionX"]   = d.positionX;
			obj["positionY"]   = d.positionY;
			obj["positionZ"]   = d.positionZ;
			obj["scale"]       = d.scale;
			j["objects"].push_back(obj);
		}

		const std::string filename = baseName + ".json";
		std::ofstream out(filename);
		if (out.is_open()) {
			out << j.dump(4);
			std::cout << "[GAME_EDITOR] Saved " << editorPlacedObjects.size()
			          << " object(s) to " << filename << "\n";
		} else {
			std::cerr << "[GAME_EDITOR] Failed to open " << filename << " for writing\n";
		}
	}


	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.isActive || intObj.isAnimating) {
				//std::cout << "[RAYCAST]   -> Object inactive or animating, skipping" << std::endl;
				continue;
			}

			if (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;

			Eigen::Vector3f toObject = intObj.position - rayOrigin;
			//std::cout << "[RAYCAST]   Vector to object: (" << toObject.x() << ", " << toObject.y() << ", " << toObject.z() << ")" << std::endl;

			float distanceAlongRay = toObject.dot(rayDir);
			//std::cout << "[RAYCAST]   Distance along ray: " << distanceAlongRay << std::endl;

			if (distanceAlongRay < 0.1f) {
				//std::cout << "[RAYCAST]   -> Object behind camera, skipping" << std::endl;
				continue;
			}

			Eigen::Vector3f closestPointOnRay = rayOrigin + rayDir * distanceAlongRay;
			float distToObject = (closestPointOnRay - intObj.position).norm();

			//std::cout << "[RAYCAST]   Distance to object: " << distToObject
			//	<< " (interaction radius: " << intObj.interactionRadius << ")" << std::endl;

			if (distToObject <= intObj.interactionRadius && distanceAlongRay < closestDistance) {
				//std::cout << "[RAYCAST]   *** HIT DETECTED! ***" << std::endl;
				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->hp <= 0.f) {
				//std::cout << "[RAYCAST_NPC]   " << npc->npcId << " is dead, skipping" << std::endl;
				continue;
			}
			if (!npc->enabled) continue;
			const float dx = rayOrigin.x() - npc->position.x();
			const float dz = rayOrigin.z() - npc->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->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(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
		renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
		const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero();
		renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });

		//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.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 (navigationEditorMode) {
			navigationEditorDrawNavigation();
			navigationEditorDrawPoints();
		}

		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->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.isActive && intObj.loadedObject.texture) {
				renderer.PushMatrix();
				renderer.TranslateMatrix(intObj.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(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
		renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
		const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero();
		renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });

		// 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.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 (navigationEditorMode) {
			navigationEditorDrawNavigation();
			navigationEditorDrawPoints();
		}

		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(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
		renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
		const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero();
		renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });

		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.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 (navigationEditorMode) {
			navigationEditorDrawNavigation();
			navigationEditorDrawPoints();
		}

		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);
			}
		}
	}

	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->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->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->isPlayer) return;
		if (!navigation || !navigation->isReady()) return;

		static constexpr float kNudgeDist = 1.2f;

		Eigen::Vector3f dir = standing->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->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->hp <= 0.f || b->hp <= 0.f || !a->enabled || !b->enabled) continue;

					const float minDist = a->collisionRadius + b->collisionRadius;
					if (minDist <= 0.f) continue;

					const Eigen::Vector2f pa(a->position.x(), a->position.z());
					const Eigen::Vector2f pb(b->position.x(), b->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->position;
					Eigen::Vector3f newB = b->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->position = newA;
							b->position = newB;
						}
						else if (aOk && !bOk) {
							a->position = newA;
						}
						else if (!aOk && bOk) {
							b->position = newB;
						}
					}
					else {
						a->position = newA;
						b->position = newB;
					}

					if (a->isPlayer && !aWasMoving) a->stopInPlace();
					if (b->isPlayer && !bWasMoving) b->stopInPlace();

					if (aWasMoving && !bWasMoving) {
						nudgeCharacterAside(b, a->position);
						fireBumpCallbacks(a, b);
					} else if (bWasMoving && !aWasMoving) {
						nudgeCharacterAside(a, b->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;

		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->position;
				continue;
			}

			const Eigen::Vector3f prev = it->second;
			const float moved = (c->position - prev).norm();
			it->second = c->position;

			if (moved > kMovedEps) {
				movers.push_back(c);
			}
		}

		if (movers.empty()) {
			return;
		}

		for (Character* mover : movers) {
			if (!mover) continue;

			for (Character* walker : characters) {
				if (!walker || walker == mover) continue;
				if (walker->hp <= 0.f || !walker->enabled) continue;
				if (!walker->isMoving()) continue;

				if (replanCooldownRemainingMs.find(walker) != replanCooldownRemainingMs.end()) {
					continue;
				}

				const Eigen::Vector3f nextTarget = walker->getCurrentNavigationTarget();
				const float distToSegment = distancePointToSegmentXZ(mover->position, walker->position, nextTarget);
				if (distToSegment > kReplanTriggerDist) {
					continue;
				}

				walker->forceReplan();
				replanCooldownRemainingMs[walker] = kReplanCooldownMs;
			}
		}
	}

	void Location::update(int64_t delta)
	{
		for (auto& intObj : interactiveObjects) {
			intObj.update(delta);
		}

		if (player) {
			player->update(delta);
		    dialogueSystem.update(static_cast<int>(delta));
			updateTriggerZones(player->position);
		}
		for (auto& npc : npcs)
		{
			npc->update(delta);
		}

		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 (targetInteractiveObject && player && !targetInteractiveObject->isAnimating) {
			float distToObject = (player->position - targetInteractiveObject->position).norm();

			// If player is close enough to pick up the item
			if (distToObject <= targetInteractiveObject->interactionRadius + 1.0f) {
				std::cout << "[PICKUP] Player reached object! Distance: " << distToObject << std::endl;
				std::cout << "[PICKUP] Calling Lua callback for: " << targetInteractiveObject->loadedObject.name << std::endl;

				// Call custom activate function if specified, otherwise use fallback
				try {
					if (!targetInteractiveObject->activateFunctionName.empty()) {
						std::cout << "[PICKUP] Using custom function: " << targetInteractiveObject->activateFunctionName << std::endl;
						scriptEngine.callActivateFunction(targetInteractiveObject->activateFunctionName);
					}
					else {
						std::cout << "[PICKUP] Using fallback callback" << std::endl;
						scriptEngine.callItemPickupCallback(targetInteractiveObject->loadedObject.name);
					}
				}
				catch (const std::exception& e) {
					std::cerr << "[PICKUP] Error calling function: " << e.what() << std::endl;
				}

				targetInteractiveObject = nullptr;
			}
		}

		// Check if player reached target NPC for interaction.
		if (targetInteractNpc && targetInteractNpcIndex >= 0 && player) {
			float distToNpc = (player->position - targetInteractNpc->position).norm();
			if (distToNpc <= NPC_TALK_DISTANCE) {
				std::cout << "[NPC] Player reached NPC index " << 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->position);
				targetInteractNpc->faceTarget = player.get();
				try {
					scriptEngine.callNpcInteractCallback(targetInteractNpcIndex);
				}
				catch (const std::exception& e) {
					std::cerr << "[NPC] callback error: " << e.what() << std::endl;
				}
				targetInteractNpc = nullptr;
				targetInteractNpcIndex = -1;
			}
		}

		for (auto& tz : teleportZones) tz.update(static_cast<float>(delta));

		if (targetTeleportZone && player) {
			float dist = (player->position - targetTeleportZone->position).norm();
			if (dist <= targetTeleportZone->radius) {
				std::cout << "[TELEPORT] Player reached teleport zone '" << targetTeleportZone->id << "'" << std::endl;
				if (onTeleport) onTeleport(targetTeleportZone->destinationLocation, targetTeleportZone->destinationPosition, targetTeleportZone->destinationRotationY);
				targetTeleportZone = nullptr;
				return;
			}
		}
	}

	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->attackTarget = nullptr;

		// 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(cameraAzimuth), sinAzim = sin(cameraAzimuth);
		float cosIncl = cos(cameraInclination), sinIncl = sin(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->position : Eigen::Vector3f::Zero();
		Eigen::Vector3f camPos = playerPos + Eigen::Vector3f(-sinAzim * cosIncl, sinIncl, cosAzim * cosIncl) * Environment::zoom;

		Eigen::Vector3f rayDir = (camForward + camRight * (ndcX * aspect * tanHalfFov) + camUp * (ndcY * tanHalfFov)).normalized();

		if (navigationEditorMode) {
			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;
				navigationEditorHandleLeftClick(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->isActive) {
			std::cout << "[CLICK] *** SUCCESS: Clicked on interactive object: " << clickedObject->loadedObject.name << " ***" << std::endl;
			std::cout << "[CLICK] Object position: (" << clickedObject->position.x() << ", "
				<< clickedObject->position.y() << ", " << clickedObject->position.z() << ")" << std::endl;
			std::cout << "[CLICK] Player position: (" << player->position.x() << ", "
				<< player->position.y() << ", " << player->position.z() << ")" << std::endl;

			targetInteractiveObject = clickedObject;
			targetInteractNpc = nullptr;
			targetInteractNpcIndex = -1;
			targetTeleportZone = nullptr;
			player->setTarget(clickedObject->position);
			player->attackTarget = nullptr;
			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->position - clickedNpc->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) {
					targetInteractiveObject = nullptr;
					targetTeleportZone = nullptr;

					if (clickedNpc->canAttack) {
						// Hostile NPC: combat logic walks the player in via attackTarget;
						// don't queue a Lua interaction during a fight.
						player->attackTarget = clickedNpc;
						targetInteractNpc = nullptr;
						targetInteractNpcIndex = -1;

						if (distance <= clickedNpc->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->attackTarget = nullptr;

						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->position);
							clickedNpc->faceTarget = player.get();
							scriptEngine.callNpcInteractCallback(npcIndex);
							targetInteractNpc = nullptr;
							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->position);
							targetInteractNpc = clickedNpc;
							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;
					targetTeleportZone = clickedTeleport;
					targetInteractiveObject = nullptr;
					targetInteractNpc = nullptr;
					targetInteractNpcIndex = -1;
					player->setTarget(clickedTeleport->position);
					player->attackTarget = nullptr;
				} else {
					player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z()));
					player->attackTarget = nullptr;
					targetInteractNpc = nullptr;
					targetInteractNpcIndex = -1;
					targetTeleportZone = nullptr;
				}
			}
			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;
			cameraAzimuth += dx * sensitivity;
			cameraInclination += dy * sensitivity;

			const float minInclination = M_PI * 30.f / 180.f;
			const float maxInclination = M_PI * 0.5f;
			cameraInclination = max(minInclination, min(maxInclination, cameraInclination));
		}
	}

	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);
	}


} // namespace ZL
