#include "Location.h" #include "utils/Utils.h" #include "render/OpenGlExtensions.h" #include #include "render/TextureManager.h" #include "TextModel.h" #include #include #include #include #include #include #include #include "GameConstants.h" #include "Character.h" #include "external/nlohmann/json.hpp" #include 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(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(); 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(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(); 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& 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{}; std::vector 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(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"; } 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(Environment::width) / static_cast(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(Environment::width) / static_cast(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(Environment::width) / static_cast(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(Environment::width) / static_cast(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(Environment::width) / static_cast(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(Environment::width) / static_cast(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); } 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(M_PI * 0.5), static_cast(-M_PI * 0.5), static_cast(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 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); } else if (bWasMoving && !aWasMoving) { nudgeCharacterAside(a, b->position); } } } } } 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 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 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(delta)); updateTriggerZones(player->position); } for (auto& npc : npcs) { npc->update(delta); } resolveCharacterCollisions(); updateDynamicReplans(delta); // 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(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(mx), Environment::projectionHeight - static_cast(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(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(mx), Environment::projectionHeight - static_cast(my)); return; } } void Location::handleMotion(int64_t fingerId, int eventX, int eventY, int mx, int my) { if (dialogueSystem.blocksGameplayInput()) { dialogueSystem.handlePointerMoved( static_cast(mx), Environment::projectionHeight - static_cast(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 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