#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 #include #include #include "GameConstants.h" #include "Character.h" #include "game_server/IGameServer.h" #include "external/nlohmann/json.hpp" #include namespace FRG { extern const char* CONST_ZIP_FILE; extern float x; constexpr float CONST_TAXI_CAR_SCALE = 0.6f; 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 constexpr float RENDER_DISTRANCE = 50.f; 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, IGameServer& server, std::string locationId) : renderer(iRenderer) , editor(*this) , gameServer_(server) , locationId_(std::move(locationId)) { } std::shared_ptr Location::stateSnapshot() const { auto snapshot = gameServer_.snapshot(); if (!snapshot) return {}; auto location = snapshot->locations.find(locationId_); if (location == snapshot->locations.end()) return {}; return std::shared_ptr( std::move(snapshot), &location->second.state); } bool Location::isDarklands() const { auto snapshot = gameServer_.snapshot(); return snapshot && snapshot->isDarklands; } bool Location::isNight() const { auto snapshot = gameServer_.snapshot(); return snapshot && snapshot->isNight; } bool Location::isDawn() const { auto snapshot = gameServer_.snapshot(); return snapshot && snapshot->isDawn; } int Location::globalInt(const std::string& key) const { auto snapshot = gameServer_.snapshot(); if (!snapshot) return 0; auto it = snapshot->globalInts.find(key); return it != snapshot->globalInts.end() ? it->second : 0; } float Location::globalFloat(const std::string& key) const { auto snapshot = gameServer_.snapshot(); if (!snapshot) return 0.f; auto it = snapshot->globalFloats.find(key); return it != snapshot->globalFloats.end() ? it->second : 0.f; } void Location::setGlobalInt(const std::string& key, int value) { gameServer_.submit(SetGlobalIntCommand{key, value}); } void Location::setGlobalFloat(const std::string& key, float value) { gameServer_.submit(SetGlobalFloatCommand{key, value}); } Location::~Location() { // The player is shared with the next location and outlives this one: drop // everything in him that points into this location. if (player) { player->onHpChanged = nullptr; } } void Location::setCharacterTarget( Character& character, const Eigen::Vector3f& target, std::string callbackName) { gameServer_.submit(SetCharacterDestinationCommand{ locationId_, character.entityId(), target, std::move(callbackName)}); } void Location::clearCharacterAttackTarget(Character& character) { gameServer_.submit(ClearCharacterAttackTargetCommand{ locationId_, character.entityId()}); } void Location::stopCharacter(Character& character) { gameServer_.submit(StopCharacterCommand{locationId_, character.entityId()}); } void Location::faceCharacter(Character& character, std::string targetId) { gameServer_.submit(FaceCharacterCommand{ locationId_, character.entityId(), std::move(targetId)}); } void Location::replanCharacter(Character& character) { gameServer_.submit(ReplanCharacterCommand{locationId_, character.entityId()}); } void Location::resetCharacterAfterDeath(Character& character) { gameServer_.submit( ResetCharacterAfterDeathCommand{locationId_, character.entityId()}); } void Location::tryCharacterAttackCombo(Character& character) { gameServer_.submit( TryCharacterAttackComboCommand{locationId_, character.entityId()}); } void Location::setCharacterHp(Character& character, float hp) { gameServer_.submit( SetCharacterHealthCommand{locationId_, character.entityId(), hp}); auto state = character.stateSnapshot(); if (character.onHpChanged && state) character.onHpChanged( state->hp, state->initialHp, std::string()); } bool Location::hasInventoryItem(const std::string& itemId) const { auto snapshot = gameServer_.snapshot(); return snapshot && std::find( snapshot->inventoryItemIds.begin(), snapshot->inventoryItemIds.end(), itemId) != snapshot->inventoryItemIds.end(); } std::size_t Location::inventoryItemCount() const { auto snapshot = gameServer_.snapshot(); return snapshot ? snapshot->inventoryItemIds.size() : 0; } void Location::addInventoryItem(std::string itemId) { gameServer_.submit(AddInventoryItemCommand{std::move(itemId)}); } void Location::removeInventoryItem(std::string itemId) { gameServer_.submit(RemoveInventoryItemCommand{std::move(itemId)}); } namespace { const nlohmann::json* findQuestProgress( const GameSnapshot& snapshot, const std::string& questId) { if (!snapshot.questJournal.contains("quests") || !snapshot.questJournal["quests"].is_array()) return nullptr; for (const auto& quest : snapshot.questJournal["quests"]) if (quest.value("id", std::string()) == questId) return ? return nullptr; } bool hasQuestObjective( const nlohmann::json& quest, const std::string& objectiveId) { if (!quest.contains("objectives") || !quest["objectives"].is_array()) return false; for (const auto& objective : quest["objectives"]) if (objective.value("id", std::string()) == objectiveId) return true; return false; } } bool Location::unlockQuest(const std::string& questId) { auto snapshot = gameServer_.snapshot(); if (!snapshot || !findQuestProgress(*snapshot, questId)) return false; gameServer_.submit(UnlockQuestCommand{questId}); return true; } bool Location::completeQuest(const std::string& questId) { auto snapshot = gameServer_.snapshot(); if (!snapshot || !findQuestProgress(*snapshot, questId)) return false; gameServer_.submit(CompleteQuestCommand{questId}); return true; } bool Location::failQuest(const std::string& questId) { auto snapshot = gameServer_.snapshot(); if (!snapshot || !findQuestProgress(*snapshot, questId)) return false; gameServer_.submit(FailQuestCommand{questId}); return true; } bool Location::setQuestObjectiveCompleted( const std::string& questId, const std::string& objectiveId, bool completed) { auto snapshot = gameServer_.snapshot(); const auto* quest = snapshot ? findQuestProgress(*snapshot, questId) : nullptr; if (!quest || !hasQuestObjective(*quest, objectiveId)) return false; gameServer_.submit(SetQuestObjectiveCompletedCommand{ questId, objectiveId, completed}); return true; } bool Location::setQuestObjectiveVisible( const std::string& questId, const std::string& objectiveId, bool visible) { auto snapshot = gameServer_.snapshot(); const auto* quest = snapshot ? findQuestProgress(*snapshot, questId) : nullptr; if (!quest || !hasQuestObjective(*quest, objectiveId)) return false; gameServer_.submit(SetQuestObjectiveVisibleCommand{ questId, objectiveId, visible}); return true; } bool Location::setQuestActiveObjective( const std::string& questId, int objectiveIndex) { auto snapshot = gameServer_.snapshot(); const auto* quest = snapshot ? findQuestProgress(*snapshot, questId) : nullptr; if (!quest || !quest->contains("objectives") || !(*quest)["objectives"].is_array() || objectiveIndex < 0 || objectiveIndex >= static_cast((*quest)["objectives"].size())) { return false; } gameServer_.submit(SetQuestActiveObjectiveCommand{ questId, objectiveIndex}); return true; } // ---- Pending-target lookup ---- TeleportZone* Location::getTargetTeleportZone(const LocationNetworkState& state) { if (state.targetTeleportZoneId.empty()) return nullptr; for (auto& zone : teleportZones) if (zone.id == state.targetTeleportZoneId) return &zone; return nullptr; } // ---- Setup ---- void Location::setup(const LocationSetup& params, AudioPlayerAsync* audioPlayer) { if (!params.gameState) throw std::runtime_error("Location requires authoritative state binding"); if (!params.definition) throw std::runtime_error("Location requires prepared content definition"); if (!params.localState) throw std::runtime_error("Location requires local state binding"); localState_ = params.localState; const LocationDefinition& definition = *params.definition; this->audioPlayer = audioPlayer; pathVisualization.initialize(); editor.buildNavMeshes(); //setupNavigation(params.navigationJsonPaths); // Load static game objects { StaticObjectSet staticObjects = GameObjectLoader::createGameObjects( definition.staticObjects, renderer, CONST_ZIP_FILE); gameObjects = std::move(staticObjects.single); instancedBatches = std::move(staticObjects.batches); } SDL_PumpEvents(); // Load interactive objects interactiveObjects = GameObjectLoader::createInteractiveObjects( definition.interactiveObjects, renderer, params.gameState->interactiveObjects, CONST_ZIP_FILE); SDL_PumpEvents(); auto sparkTexture = renderer.textureManager.LoadFromPng("resources/w/spark.png", CONST_ZIP_FILE); if (!params.player) throw std::runtime_error("Location requires game-level player binding"); player = params.player; player->bindToLocation(locationId_); synchronizeRemotePlayers(); logger() << "Load resurces step 9" << std::endl; SDL_PumpEvents(); // 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::createNpcs( definition.npcs, renderer, params.gameState->npcs, gameServer_, locationId_, CONST_ZIP_FILE); for (auto& npc : npcs) { if (!npc) continue; auto state = npc->stateSnapshot(); if (state && state->canAttack) { npc->setupHitSparks(sparkTexture); npc->audioPlayer = audioPlayer; } } SDL_PumpEvents(); auto activeTeleportTexture = renderer.textureManager.LoadFromPng( "resources/w/star.png", CONST_ZIP_FILE); auto inactiveTeleportTexture = renderer.textureManager.LoadFromPng( "resources/w/star_red.png", CONST_ZIP_FILE); for (const TeleportDefinition& data : definition.teleports) { TeleportZone zone; zone.id = data.id; zone.position = data.position; zone.radius = data.radius; zone.destinationLocation = data.destinationLocation; zone.destinationPosition = data.destinationPosition; zone.destinationRotationY = data.destinationRotationY; zone.automatic = data.automatic; zone.automaticRadius = data.automaticRadius; auto active = params.gameState->teleportZones.find(data.id); zone.active = active != params.gameState->teleportZones.end() ? active->second : data.active; zone.initSparks( activeTeleportTexture, inactiveTeleportTexture); teleportZones.push_back(std::move(zone)); } for (const TriggerZoneDefinition& data : definition.triggerZones) { TriggerZone zone; zone.id = data.id; zone.position = data.position; zone.radius = data.radius; zone.hysteresis = data.hysteresis; auto state = params.gameState->triggerZones.find(data.id); if (state != params.gameState->triggerZones.end()) { zone.enabled = state->second.enabled; zone.playerInside = state->second.playerInside; } else { zone.enabled = data.enabled; } triggerZones.push_back(std::move(zone)); } for (const PointLightDefinition& data : definition.pointLights) { PointLight light; light.id = data.id; light.position = data.position; light.direction = data.direction; light.color = data.color; light.autoLight = data.autoLight; light.autoLightDistance = data.autoLightDistance; light.limitX = data.limitX; light.limitY = data.limitY; light.autoLightLimitX = data.autoLightLimitX; light.autoLightLimitY = data.autoLightLimitY; pointLights.push_back(std::move(light)); } SDL_PumpEvents(); dialogueSystem.init(renderer, CONST_ZIP_FILE); dialogueSystem.loadDatabase(params.dialoguesJsonPath); dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes008.json"); dialogueSystem.loadTranslations("resources/dialogue/translate_dialogues.json"); dialogueSystem.loadCutsceneTranslations("resources/dialogue/translate_cutscenes.json"); dialogueSystem.setGlobalFlagAccessors( [this](const std::string& key) { return globalInt(key); }, [this](const std::string& key, int value) { setGlobalInt(key, value); }); dialogueSystem.setQuestActionHandler( [this](const Dialogue::QuestActionRequest& request) { switch (request.type) { case Dialogue::QuestActionType::Unlock: unlockQuest(request.questId); break; case Dialogue::QuestActionType::Complete: completeQuest(request.questId); break; case Dialogue::QuestActionType::Fail: failQuest(request.questId); break; case Dialogue::QuestActionType::CompleteObjective: setQuestObjectiveCompleted( request.questId, request.objectiveId, true); break; case Dialogue::QuestActionType::ShowObjective: setQuestObjectiveVisible( request.questId, request.objectiveId, true); break; } }); SDL_PumpEvents(); 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, audioPlayer, params.scriptPath); dialogueSystem.setOnCutsceneFinished([this](const std::string& cutsceneId) { scriptEngine.callCutsceneCompleteCallback(cutsceneId); }); dialogueSystem.setOnDialogueLineStarted([this](const std::string& fn) { try { scriptEngine.callActivateFunction(fn); } catch (const std::exception& e) { std::cerr << "[dialogue] line callback error: " << e.what() << "\n"; } }); dialogueSystem.setOnCutsceneLineStarted([this](const std::string& fn) { try { scriptEngine.callActivateFunction(fn); } catch (const std::exception& e) { std::cerr << "[cutscene] line callback error: " << e.what() << "\n"; } }); dialogueSystem.setOnCutsceneFadeInComplete([this](const std::string& fn) { try { scriptEngine.callActivateFunction(fn); } catch (const std::exception& e) { std::cerr << "[cutscene] fade-in callback error: " << e.what() << "\n"; } }); carTexture = renderer.textureManager.LoadFromPng("resources/e/car4096_v002.png", CONST_ZIP_FILE); carMesh.data = LoadModelFromBinFile("resources/e/car004.txt.bin", CONST_ZIP_FILE); carMesh.data.Scale(CONST_TAXI_CAR_SCALE); carMesh.data.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI * 0.5, Eigen::Vector3f::UnitY())).toRotationMatrix()); carMesh.RefreshVBO(); carWheelTexture = renderer.textureManager.LoadFromPng("resources/w/exterior/Wheel_base003.png", CONST_ZIP_FILE); carWheelMesh.data = LoadModelFromBinFile("resources/e/car_wheel001.txt.bin", CONST_ZIP_FILE); carWheelMesh.data.Scale(CONST_TAXI_CAR_SCALE); carWheelMesh.data.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI * 0.5, Eigen::Vector3f::UnitY())).toRotationMatrix()); carWheelMesh.RefreshVBO(); } std::shared_ptr Location::createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer, IGameServer& server, const std::string& playerId) { //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 playerTexture = renderer.textureManager.LoadFromPng("resources/w/gg/UniV_Grid_2K_Base_color001.png", CONST_ZIP_FILE); auto sparkTexture = renderer.textureManager.LoadFromPng("resources/w/spark.png", CONST_ZIP_FILE); auto player = std::make_shared( server, std::string(), playerId); player->loadBinaryAnimation(AnimationState::STAND, "resources/w/gg/new2/gg_stand_idle001_small.anim", CONST_ZIP_FILE); player->loadBinaryAnimation(AnimationState::WALK, "resources/w/gg/new2/gg_walk001_small.anim", CONST_ZIP_FILE); player->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/gg/new2/gg_stand_to_action001_small.anim", CONST_ZIP_FILE); player->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/gg/new2/gg_action_chop001_small.anim", CONST_ZIP_FILE); player->loadBinaryAnimation(AnimationState::ACTION_ATTACK_2, "resources/w/gg/new2/gg_action_stab001_small.anim", CONST_ZIP_FILE); player->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/gg/new2/gg_action_idle002_small.anim", CONST_ZIP_FILE); player->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/gg/new2/gg_action_to_stand001_small.anim", CONST_ZIP_FILE); player->loadBinaryAnimation(AnimationState::ACTION_TO_DEATH, "resources/w/gg/new2/gg_die001_small.anim", CONST_ZIP_FILE); player->loadBinaryAnimation(AnimationState::DEATH_IDLE, "resources/w/gg/new2/gg_die_idle001_small.anim", CONST_ZIP_FILE); player->weaponTexture = renderer.textureManager.LoadFromPng("resources/w/gg/Props.png", CONST_ZIP_FILE); player->weaponMesh.data = LoadModelFromBinFile("resources/w/gg/knife002.txt.bin", CONST_ZIP_FILE); player->weaponMesh.data.Move(Vector3f(0, -0.3, 0)); player->weaponMesh.data.Scale(0.2f); player->weaponMesh.RefreshVBO(); player->weaponAttachBoneName = "RightHand"; player->weaponInitialRotation = Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitZ()).toRotationMatrix(); player->weaponInitialPosition = Eigen::Vector3f(0, 0.09, 0.016); player->setTexture(playerTexture); player->setupHitSparks(sparkTexture); player->audioPlayer = audioPlayer; return player; } void Location::synchronizeRemotePlayers() { const auto snapshot = gameServer_.snapshot(); if (!snapshot) return; const std::string& localId = gameServer_.localPlayerId(); for (auto it = remotePlayers_.begin(); it != remotePlayers_.end();) { if (it->first == localId || snapshot->players.find(it->first) == snapshot->players.end()) { it = remotePlayers_.erase(it); } else { ++it; } } for (const auto& [id, state] : snapshot->players) { if (id == localId || remotePlayers_.count(id) > 0) continue; auto remote = createPlayer(renderer, audioPlayer, gameServer_, id); remote->bindToLocation(locationId_); remotePlayers_.emplace(id, std::move(remote)); } } void Location::drawRemotePlayerNames( const Eigen::Matrix4f& cameraViewMatrix, const Eigen::Matrix4f& projectionMatrix) { if (!npcNameText) return; for (auto& [id, remote] : remotePlayers_) { if (!remote) continue; const auto state = remote->stateSnapshot(); const std::string& name = state && !state->npcName.empty() ? state->npcName : id; remote->drawPlayerName( *npcNameText, cameraViewMatrix, projectionMatrix, name); } } 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 : ""); std::unordered_set ids; for (const auto& item : j["teleports"]) { TeleportZone tz; tz.id = item.at("id").get(); if (tz.id.empty() || !ids.insert(tz.id).second) throw std::runtime_error( "Missing or duplicate teleport id in " + jsonPath); tz.active = item.value("active", false); tz.position = Eigen::Vector3f( item.value("positionX", 0.0f), item.value("positionY", 0.0f), item.value("positionZ", 0.0f) ); tz.radius = item.value("radius", 0.0f); tz.automatic = item.value("automatic", false); tz.automaticRadius = item.value("automaticRadius", tz.radius); tz.destinationLocation = item.value("destinationLocation", ""); tz.destinationPosition = Eigen::Vector3f( item.value("destinationPositionX", 0.0f), item.value("destinationPositionY", 0.0f), item.value("destinationPositionZ", 0.0f) ); tz.destinationRotationY = item.value("destinationRotationY", 0.0f); tz.initSparks(activeTex, inactiveTex); teleportZones.push_back(std::move(tz)); } logger() << "[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; std::unordered_set ids; for (const auto& item : j["trigger_zones"]) { TriggerZone tz; tz.id = item.at("id").get(); if (tz.id.empty() || !ids.insert(tz.id).second) throw std::runtime_error( "Missing or duplicate trigger-zone id in " + jsonPath); 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)); } logger() << "[TRIGGER] Loaded " << triggerZones.size() << " trigger zone(s) from " << jsonPath << std::endl; } void Location::loadPointLights(const std::string& jsonPath, const char* zipFile) { if (jsonPath.empty()) return; std::string content; try { if (!zipFile || zipFile[0] == '\0') { content = readTextFile(jsonPath); } else { auto buf = readFileFromZIP(jsonPath, zipFile); if (buf.empty()) { std::cerr << "[LIGHTS] Failed to read " << jsonPath << " from zip" << std::endl; return; } content.assign(buf.begin(), buf.end()); } } catch (const std::exception& e) { std::cerr << "[LIGHTS] Failed to open " << jsonPath << ": " << e.what() << std::endl; return; } using json = nlohmann::json; json j; try { j = json::parse(content); } catch (const std::exception& e) { std::cerr << "[LIGHTS] JSON parse error in " << jsonPath << ": " << e.what() << std::endl; return; } if (!j.contains("lights") || !j["lights"].is_array()) return; for (const auto& item : j["lights"]) { PointLight pl; pl.id = item.value("id", ""); pl.autoLight = item.value("autoLight", false); pl.autoLightDistance = item.value("autoLightDistance", -1.0f); pl.limitX = item.value("limitX", 99999.f); pl.limitY = item.value("limitY", 99999.f); pl.autoLightLimitX = item.value("autoLightLimitX", -1.0f); pl.autoLightLimitY = item.value("autoLightLimitY", -1.0f); pl.position = Eigen::Vector3f( item.value("positionX", 0.0f), item.value("positionY", 0.0f), item.value("positionZ", 0.0f) ); pl.direction = Eigen::Vector3f( item.value("directionX", 0.0f), item.value("directionY", -1.0f), item.value("directionZ", 0.0f) ); pl.color = Eigen::Vector3f( item.value("colorR", 1.0f), item.value("colorG", 1.0f), item.value("colorB", 1.0f) ); pointLights.push_back(std::move(pl)); } logger() << "[LIGHTS] Loaded " << pointLights.size() << " light(s) from " << jsonPath << std::endl; } void Location::updateTriggerZones(const Eigen::Vector3f& playerPos) { for (int i = 0; i < static_cast(triggerZones.size()); ++i) { TriggerZone& tz = triggerZones[i]; if (!tz.enabled) continue; const float dist = (playerPos - tz.position).norm(); if (!tz.playerInside && dist <= tz.radius) { tz.playerInside = true; scriptEngine.callTriggerEnterCallback(tz.id); } else if (tz.playerInside && dist > tz.radius + tz.hysteresis) { tz.playerInside = false; scriptEngine.callTriggerExitCallback(tz.id); } } } bool Location::switchNavigation(int index) { auto state = stateSnapshot(); if (!state || index < 0 || index >= static_cast(state->navigationMaps.size())) { std::cerr << "[NAV] switchNavigation: index " << index << " out of range\n"; return false; } gameServer_.submit(SwitchLocationNavigationCommand{locationId_, index}); if (editorMode == EditorMode::Navigation) { editor.buildNavMeshes(); } logger() << "[NAV] Switched to navigation map " << index << "\n"; return true; } InteractiveObject* Location::raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir) { if (interactiveObjects.empty()) { //logger() << "[RAYCAST] No interactive objects to check" << std::endl; return nullptr; } //logger() << "[RAYCAST] Starting raycast with " << interactiveObjects.size() << " objects" << std::endl; //logger() << "[RAYCAST] Ray origin: (" << rayOrigin.x() << ", " << rayOrigin.y() << ", " << rayOrigin.z() << ")" << std::endl; //logger() << "[RAYCAST] Ray dir: (" << rayDir.x() << ", " << rayDir.y() << ", " << rayDir.z() << ")" << std::endl; float closestDistance = FLT_MAX; InteractiveObject* closestObject = nullptr; const bool darklands = isDarklands(); for (auto& intObj : interactiveObjects) { //logger() << "[RAYCAST] Checking object: " << intObj.loadedObject.name << " (active: " << intObj.isActive << ")" << std::endl; if (!intObj.state.isActive || intObj.state.isAnimating) { //logger() << "[RAYCAST] -> Object inactive or animating, skipping" << std::endl; continue; } if (darklands) { if (!intObj.loadedObject.textureDarklands) continue; } else { if (!intObj.loadedObject.texture) continue; } //logger() << "[RAYCAST] Position: (" << intObj.position.x() << ", " << intObj.position.y() << ", " // << intObj.position.z() << "), Radius: " << intObj.interactionRadius << std::endl; const bool hasBox = !(intObj.state.boundsMin.isZero() && intObj.state.boundsMax.isZero()); if (hasBox) { const Eigen::Vector3f worldMin = intObj.state.position + intObj.state.boundsMin; const Eigen::Vector3f worldMax = intObj.state.position + intObj.state.boundsMax; float tMin = 0.1f; float tMax = FLT_MAX; bool miss = false; for (int axis = 0; axis < 3; ++axis) { if (std::abs(rayDir[axis]) < 1e-8f) { if (rayOrigin[axis] < worldMin[axis] || rayOrigin[axis] > worldMax[axis]) { miss = true; break; } } else { const float invD = 1.0f / rayDir[axis]; float t0 = (worldMin[axis] - rayOrigin[axis]) * invD; float t1 = (worldMax[axis] - rayOrigin[axis]) * invD; if (invD < 0.0f) std::swap(t0, t1); tMin = max(tMin, t0); tMax = min(tMax, t1); if (tMax < tMin) { miss = true; break; } } } if (!miss && tMin < closestDistance) { closestDistance = tMin; closestObject = &intObj; } } else { Eigen::Vector3f toObject = intObj.state.position - rayOrigin; float distanceAlongRay = toObject.dot(rayDir); if (distanceAlongRay < 0.1f) { continue; } Eigen::Vector3f closestPointOnRay = rayOrigin + rayDir * distanceAlongRay; float distToObject = (closestPointOnRay - intObj.state.position).norm(); if (distToObject <= intObj.state.interactionRadius && distanceAlongRay < closestDistance) { closestDistance = distanceAlongRay; closestObject = &intObj; } } } /* if (closestObject) { logger() << "[RAYCAST] *** RAYCAST SUCCESS: Found object " << closestObject->loadedObject.name << " ***" << std::endl; } else { logger() << "[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; //logger() << "[RAYCAST_NPC] Starting raycast with " << npcs.size() << " npcs" << std::endl; for (auto& npc : npcs) { auto npcState = npc ? npc->stateSnapshot() : nullptr; if (!npcState || npcState->hp <= 0.f) { //logger() << "[RAYCAST_NPC] " << npc->entityId() << " is dead, skipping" << std::endl; continue; } if (!npcState->enabled) continue; const float dx = rayOrigin.x() - npcState->position.x(); const float dz = rayOrigin.z() - npcState->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 = npcState->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; //logger() << "[RAYCAST_NPC] " << npc->entityId() << " hit at t=" << entryT << std::endl; if (entryT < closestDist) { closestDist = entryT; closestNpc = npc.get(); } } if (closestNpc) { //logger() << "[RAYCAST_NPC] HIT: " << closestNpc->entityId() << std::endl; } else { //logger() << "[RAYCAST_NPC] No NPC hit" << std::endl; } return closestNpc; } void Location::drawInstancedStatics(const std::string& instancedShaderName, InstancedTexture textureKind, const std::function& setUniforms) { if (instancedBatches.empty()) return; renderer.shaderManager.PushShader(instancedShaderName); if (setUniforms) setUniforms(); renderer.SetMatrix(); for (auto& batch : instancedBatches) { if (textureKind != InstancedTexture::None) { const auto& texture = (textureKind == InstancedTexture::Darklands) ? batch.textureDarklands : batch.texture; if (!texture) continue; glBindTexture(GL_TEXTURE_2D, texture->getTexID()); } renderer.DrawInstanced(batch); } renderer.shaderManager.PopShader(); } void Location::setPathVisualizationEnabled(bool enabled) { pathVisualization.setEnabled(enabled); } void Location::drawGame() { auto locationState = stateSnapshot(); if (!locationState) return; auto playerState = player ? player->stateSnapshot() : nullptr; 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(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); const Eigen::Vector3f camTarget = player ? player->renderPosition() : Eigen::Vector3f::Zero(); renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); renderer.TranslateMatrix({ 0, -1.f, 0 }); //glBindTexture(GL_TEXTURE_2D, roomTexture->getTexID()); //renderer.DrawVertexRenderStruct(roomMesh); renderer.RenderUniform1f("uAlpha", 1.0f); for (auto& [name, gameObj] : gameObjects) { if (!gameObj.texture) continue; glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID()); renderer.DrawVertexRenderStruct(gameObj.mesh); } drawInstancedStatics("fog_inst", InstancedTexture::Color, [&]() { renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos); renderer.RenderUniform1f("uAlpha", 1.0f); }); for (auto& intObj : interactiveObjects) { if (intObj.state.isActive) { intObj.draw(renderer); } } renderer.RenderUniform1f("uAlpha", 1.0f); const Eigen::Matrix4f currentView = renderer.GetCurrentModelViewMatrix(); if (player) player->prepareHitSparksForDraw(currentView); for (auto& [id, remote] : remotePlayers_) if (remote) remote->prepareHitSparksForDraw(currentView); for (auto& npc : npcs) npc->prepareHitSparksForDraw(currentView); for (auto& tz : teleportZones) tz.prepareForDraw(currentView); if (player) player->draw(renderer); for (auto& [id, remote] : remotePlayers_) if (remote) remote->draw(renderer); for (auto& npc : npcs) npc->draw(renderer); drawNpcCar(); for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); pathVisualization.draw(renderer); if (editorMode == EditorMode::Navigation) { editor.drawNavigation(); editor.drawPoints(); } if (editorMode == EditorMode::InteractiveObjects) { editor.drawInteractiveObjectBounds(); } renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); if (npcNameText) { Eigen::Matrix4f proj = MakePerspectiveMatrix(1.0f / 1.5f, static_cast(Environment::width) / static_cast(Environment::height), Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); drawRemotePlayerNames(currentView, proj); 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; auto playerState = player ? player->stateSnapshot() : nullptr; const Eigen::Vector3f sceneCenter = player ? player->renderPosition() : 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); } drawInstancedStatics("shadow_depth_inst", InstancedTexture::None); for (auto& intObj : interactiveObjects) { if (intObj.state.castShadow) intObj.drawShadowDepth(renderer); } // Draw characters (they handle their own skinning shader switch internally) if (player) player->drawShadowDepth(renderer); for (auto& [id, remote] : remotePlayers_) if (remote) remote->drawShadowDepth(renderer); for (auto& npc : npcs) npc->drawShadowDepth(renderer); drawNpcCar(); renderer.PopMatrix(); // light view renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); //glCullFace(GL_BACK); glDisable(GL_CULL_FACE); shadowMap->unbind(); } void Location::drawGameWithShadows() { auto locationState = stateSnapshot(); if (!locationState) return; 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(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); auto playerState = player ? player->stateSnapshot() : nullptr; const Eigen::Vector3f camTarget = player ? player->renderPosition() : Eigen::Vector3f::Zero(); renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); renderer.TranslateMatrix({ 0, -1.f, 0 }); // Capture the camera view matrix and compute uLightFromCamera cameraViewMatrix = renderer.GetCurrentModelViewMatrix(); Eigen::Matrix4f cameraViewInverse = cameraViewMatrix.inverse(); Eigen::Matrix4f lightFromCamera = shadowMap->getLightSpaceMatrix() * cameraViewInverse; renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); // Light direction in camera space for diffuse lighting Eigen::Vector3f lightDirCamera = cameraViewMatrix.block<3, 3>(0, 0) * shadowMap->getLightDirection(); renderer.RenderUniform3fv("uLightDir", lightDirCamera.data()); #endif CheckGlError(__FILE__, __LINE__); //glBindTexture(GL_TEXTURE_2D, roomTexture->getTexID()); //renderer.DrawVertexRenderStruct(roomMesh); CheckGlError(__FILE__, __LINE__); renderer.RenderUniform1f("uAlpha", 1.0f); for (auto& [name, gameObj] : gameObjects) { if (!gameObj.texture) continue; glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID()); renderer.DrawVertexRenderStruct(gameObj.mesh); } #ifndef DEBUG_LIGHT drawInstancedStatics("fog_shadow_inst", InstancedTexture::Color, [&]() { renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i("uShadowMap", 1); renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos); renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); renderer.RenderUniform3fv("uLightDir", lightDirCamera.data()); renderer.RenderUniform1f("uAlpha", 1.0f); }); #endif CheckGlError(__FILE__, __LINE__); for (auto& intObj : interactiveObjects) { if (intObj.state.isActive) { intObj.draw(renderer); } } CheckGlError(__FILE__, __LINE__); #ifdef DEBUG_LIGHT // In debug-light mode characters use the plain shaders (draw normally // but from the light's viewpoint — projection/view already on stack). { const Eigen::Matrix4f currentView = renderer.GetCurrentModelViewMatrix(); if (player) player->prepareHitSparksForDraw(currentView); for (auto& [id, remote] : remotePlayers_) if (remote) remote->prepareHitSparksForDraw(currentView); for (auto& npc : npcs) npc->prepareHitSparksForDraw(currentView); } if (player) player->draw(renderer); for (auto& [id, remote] : remotePlayers_) if (remote) remote->draw(renderer); for (auto& npc : npcs) npc->draw(renderer); drawNpcCar(); #else // Characters use their own shadow-aware shaders CheckGlError(__FILE__, __LINE__); if (player) player->prepareHitSparksForDraw(cameraViewMatrix); for (auto& [id, remote] : remotePlayers_) if (remote) remote->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& [id, remote] : remotePlayers_) if (remote) remote->drawWithShadow( renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera); for (auto& npc : npcs) npc->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera); drawNpcCar(); for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); #endif pathVisualization.draw(renderer); if (editorMode == EditorMode::Navigation) { editor.drawNavigation(); editor.drawPoints(); } if (editorMode == EditorMode::InteractiveObjects) { editor.drawInteractiveObjectBounds(); } CheckGlError(__FILE__, __LINE__); renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); if (npcNameText) { Eigen::Matrix4f proj = MakePerspectiveMatrix(1.0f / 1.5f, static_cast(Environment::width) / static_cast(Environment::height), Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); drawRemotePlayerNames(cameraViewMatrix, proj); 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() { auto locationState = stateSnapshot(); if (!locationState) return; auto playerState = player ? player->stateSnapshot() : nullptr; 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(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); const Eigen::Vector3f camTarget = player ? player->renderPosition() : Eigen::Vector3f::Zero(); renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); renderer.TranslateMatrix({ 0, -1.f, 0 }); renderer.RenderUniform1f("uAlpha", 1.0f); for (auto& [name, gameObj] : gameObjects) { if (!gameObj.textureDarklands) continue; glBindTexture(GL_TEXTURE_2D, gameObj.textureDarklands->getTexID()); renderer.DrawVertexRenderStruct(gameObj.mesh); } drawInstancedStatics("darklands_fog_inst", InstancedTexture::Darklands, [&]() { renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos); renderer.RenderUniform1f("uAlpha", 1.0f); }); for (auto& intObj : interactiveObjects) { if (intObj.state.isActive) { intObj.drawDarklands(renderer); } } renderer.RenderUniform1f("uAlpha", 1.0f); const Eigen::Matrix4f currentView = renderer.GetCurrentModelViewMatrix(); if (player) player->prepareHitSparksForDraw(currentView); for (auto& [id, remote] : remotePlayers_) if (remote) remote->prepareHitSparksForDraw(currentView); for (auto& npc : npcs) npc->prepareHitSparksForDraw(currentView); for (auto& tz : teleportZones) tz.prepareForDraw(currentView); if (player) player->draw(renderer); for (auto& [id, remote] : remotePlayers_) if (remote) remote->draw(renderer); for (auto& npc : npcs) npc->draw(renderer); //drawNpcCar(); for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); pathVisualization.draw(renderer); if (editorMode == EditorMode::Navigation) { editor.drawNavigation(); editor.drawPoints(); } if (editorMode == EditorMode::InteractiveObjects) { editor.drawInteractiveObjectBounds(); } renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); if (npcNameText) { Eigen::Matrix4f proj = MakePerspectiveMatrix(1.0f / 1.5f, static_cast(Environment::width) / static_cast(Environment::height), Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); drawRemotePlayerNames(currentView, proj); for (auto& npc : npcs) { if (npc) npc->drawName(*npcNameText, currentView, proj); } if (player) player->drawHealthBar(renderer, currentView, proj); for (auto& npc : npcs) { if (npc) npc->drawHealthBar(renderer, currentView, proj); } } } void Location::drawNightShadowDepthPass(const PointLight& light) { if (!shadowMap) return; static constexpr float kSpotFovY = static_cast(M_PI) * 0.5f; // 90° shadowMap->updateSpotlightMatrix(light.position, light.direction, kSpotFovY, 0.1f, 20.0f); shadowMap->bind(); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glEnable(GL_CULL_FACE); renderer.shaderManager.PushShader("shadow_depth"); renderer.PushPerspectiveProjectionMatrix(kSpotFovY, 1.0f, 0.1f, 20.0f); renderer.PushSpecialMatrix(shadowMap->getLightViewMatrix()); for (auto& [name, gameObj] : gameObjects) { renderer.DrawVertexRenderStruct(gameObj.mesh); } drawInstancedStatics("shadow_depth_inst", InstancedTexture::None); for (auto& intObj : interactiveObjects) { if (intObj.state.castShadowNight) intObj.drawShadowDepth(renderer); } if (player) player->drawShadowDepth(renderer); for (auto& [id, remote] : remotePlayers_) if (remote) remote->drawShadowDepth(renderer); for (auto& npc : npcs) npc->drawShadowDepth(renderer); drawNpcCar(); renderer.PopMatrix(); // light view renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); glDisable(GL_CULL_FACE); shadowMap->unbind(); } void Location::captureNpcCarPresentation(const NpcCar& car) { NpcCarPresentationSample sample; sample.position = car.position; sample.rotation = car.rotation; sample.wheelAngle = car.wheelAngle; sample.steeringAngle = car.steeringAngle; const auto now = std::chrono::steady_clock::now(); const bool largeGap = hasNpcCarPresentationSample_ && std::chrono::duration_cast( now - lastNpcCarPresentationUpdate_).count() > 250; if (!hasNpcCarPresentationSample_ || largeGap) { previousNpcCarPresentation_ = sample; } else { previousNpcCarPresentation_ = currentNpcCarPresentation_; } currentNpcCarPresentation_ = sample; lastNpcCarPresentationUpdate_ = now; hasNpcCarPresentationSample_ = true; } Location::NpcCarPresentationSample Location::interpolatedNpcCarPresentation(const NpcCar& car) const { NpcCarPresentationSample result; result.position = car.position; result.rotation = car.rotation; result.wheelAngle = car.wheelAngle; result.steeringAngle = car.steeringAngle; if (!hasNpcCarPresentationSample_) return result; constexpr float kSimulationTickMs = 100.0f; const float elapsedMs = static_cast( std::chrono::duration_cast( std::chrono::steady_clock::now() - lastNpcCarPresentationUpdate_).count()); const float alpha = (std::max)(0.0f, (std::min)(1.0f, elapsedMs / kSimulationTickMs)); result.position = previousNpcCarPresentation_.position + alpha * (currentNpcCarPresentation_.position - previousNpcCarPresentation_.position); result.wheelAngle = previousNpcCarPresentation_.wheelAngle + alpha * (currentNpcCarPresentation_.wheelAngle - previousNpcCarPresentation_.wheelAngle); result.steeringAngle = previousNpcCarPresentation_.steeringAngle + alpha * (currentNpcCarPresentation_.steeringAngle - previousNpcCarPresentation_.steeringAngle); constexpr float kPi = 3.14159265358979323846f; constexpr float kTwoPi = 2.0f * kPi; float rotationDelta = currentNpcCarPresentation_.rotation - previousNpcCarPresentation_.rotation; while (rotationDelta > kPi) rotationDelta -= kTwoPi; while (rotationDelta < -kPi) rotationDelta += kTwoPi; result.rotation = previousNpcCarPresentation_.rotation + alpha * rotationDelta; return result; } void Location::drawNpcCar() { auto locationState = stateSnapshot(); if (!locationState) return; const NpcCarPresentationSample car = interpolatedNpcCarPresentation(locationState->taxiCar); renderer.PushMatrix(); renderer.TranslateMatrix(car.position); renderer.TranslateMatrix({ 0, 0.7f - 0.25f +x, 0 }); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(car.rotation, Eigen::Vector3f::UnitY())).toRotationMatrix()); glBindTexture(GL_TEXTURE_2D, carTexture->getTexID()); renderer.DrawVertexRenderStruct(carMesh); if (carWheelTexture) { constexpr float track_width = 1.28f * CONST_TAXI_CAR_SCALE; constexpr float wheel_base = 2.25f * CONST_TAXI_CAR_SCALE; constexpr float shift = 0.6f * CONST_TAXI_CAR_SCALE; const Eigen::Vector3f wheelPositions[4] = { Eigen::Vector3f(track_width, 0.f - 0.21f, -(wheel_base + shift) + 1.25f * CONST_TAXI_CAR_SCALE), Eigen::Vector3f(-track_width, 0.f - 0.21f, -(wheel_base + shift) + 1.25f * CONST_TAXI_CAR_SCALE), Eigen::Vector3f(track_width, 0.f - 0.21f, (wheel_base - shift) + 1.1f * CONST_TAXI_CAR_SCALE), Eigen::Vector3f(-track_width, 0.f - 0.21f, (wheel_base - shift) + 1.1f * CONST_TAXI_CAR_SCALE) }; const bool isFront[4] = { true, true, false, false }; glBindTexture(GL_TEXTURE_2D, carWheelTexture->getTexID()); for (int i = 0; i < 4; ++i) { renderer.PushMatrix(); renderer.TranslateMatrix(wheelPositions[i]); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(car.wheelAngle, -Eigen::Vector3f::UnitX())).toRotationMatrix()); if (isFront[i]) { renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(car.steeringAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); } renderer.DrawVertexRenderStruct(carWheelMesh); renderer.PopMatrix(); } } renderer.PopMatrix(); } void Location::drawGameNight() { auto locationState = stateSnapshot(); if (!locationState) return; auto gameSnapshot = gameServer_.snapshot(); if (!gameSnapshot) return; auto playerState = player ? player->stateSnapshot() : nullptr; // --- Determine which lights are active this frame --- // autoLight=true lights only activate when they are the nearest light to the player. // autoLight=false lights are always active. int nearestLightIdx = -1; if (playerState && !pointLights.empty()) { float minDist = FLT_MAX; for (int i = 0; i < static_cast(pointLights.size()); ++i) { const float d = (pointLights[i].position - playerState->position).norm(); if (d < minDist) { minDist = d; nearestLightIdx = i; } } } static constexpr int MAX_POINT_LIGHTS = 4; std::vector activeLightIndices; activeLightIndices.reserve(pointLights.size()); for (int i = 0; i < static_cast(pointLights.size()); ++i) { const PointLight& pl = pointLights[i]; bool active = !pl.autoLight; if (!active && playerState) { if (pl.autoLightLimitX > 0.0f && pl.autoLightLimitY > 0.0f) { // Rectangular activation zone — independent of nearest-light heuristic const float dx = std::abs(playerState->position.x() - pl.position.x()); const float dz = std::abs(playerState->position.z() - pl.position.z()); active = (dx <= pl.autoLightLimitX && dz <= pl.autoLightLimitY); } else { // only the nearest autoLight activates, with optional distance cap const float dist = (pl.position - playerState->position).norm(); active = (i == nearestLightIdx) && (pl.autoLightDistance <= 0.0f || dist <= pl.autoLightDistance); } } if (active) { activeLightIndices.push_back(i); if (static_cast(activeLightIndices.size()) >= MAX_POINT_LIGHTS) break; } } // Shadow caster = nearest active light to player const PointLight* shadowLight = nullptr; if (playerState && !activeLightIndices.empty()) { float minDist = FLT_MAX; for (int idx : activeLightIndices) { const float d = (pointLights[idx].position - playerState->position).norm(); if (d < minDist) { minDist = d; shadowLight = &pointLights[idx]; } } } const bool hasShadows = (shadowMap != nullptr && shadowLight != nullptr); // Ambient and fog color: different for plain night vs dawn static const float kNightAmbient[3] = { 0.37f, 0.37f, 0.37f }; static const float kDawnAmbient[3] = { 0.72f, 0.52f, 0.62f }; // brighter warm pink static const float kNightFogColor[3] = { 0.01f, 0.01f, 0.05f }; static const float kDawnFogColor[3] = { 0.50f, 0.44f, 0.47f }; // grey with slight pink const float* ambientColor = gameSnapshot->isDawn ? kDawnAmbient : kNightAmbient; const float* fogColor = gameSnapshot->isDawn ? kDawnFogColor : kNightFogColor; if (hasShadows) { drawNightShadowDepthPass(*shadowLight); } // --- Main render pass --- glClearColor(0.01f, 0.01f, 0.05f, 1.0f); glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); const std::string mainShader = hasShadows ? "night_fog_shadow" : "night_fog"; renderer.shaderManager.PushShader(mainShader); renderer.RenderUniform1i(textureUniformName, 0); if (hasShadows) { renderer.RenderUniform1i("uShadowMap", 1); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, shadowMap->getDepthTexture()); glActiveTexture(GL_TEXTURE0); } const float playerEyePos[3] = { 0.0f, 0.0f, -Environment::zoom }; renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos); renderer.RenderUniform3fv("uAmbientColor", ambientColor); renderer.RenderUniform3fv("uFogColor", fogColor); renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, static_cast(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(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); const Eigen::Vector3f camTarget = player ? player->renderPosition() : Eigen::Vector3f::Zero(); renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); renderer.TranslateMatrix({ 0, -1.f, 0 }); cameraViewMatrix = renderer.GetCurrentModelViewMatrix(); // Transform active spot lights to eye space and build uniform arrays float plPosEye[MAX_POINT_LIGHTS * 3] = {}; float plDirEye[MAX_POINT_LIGHTS * 3] = {}; float plColors[MAX_POINT_LIGHTS * 3] = {}; float plWorldXZ[MAX_POINT_LIGHTS * 2] = {}; float plLimitXZ[MAX_POINT_LIGHTS * 2] = {}; const Eigen::Matrix3f camRot = cameraViewMatrix.block<3, 3>(0, 0); const int lightCount = static_cast(activeLightIndices.size()); for (int j = 0; j < lightCount; ++j) { const PointLight& pl = pointLights[activeLightIndices[j]]; const Eigen::Vector4f worldPos(pl.position.x(), pl.position.y(), pl.position.z(), 1.0f); const Eigen::Vector4f eyePos4 = cameraViewMatrix * worldPos; plPosEye[j * 3 + 0] = eyePos4.x(); plPosEye[j * 3 + 1] = eyePos4.y(); plPosEye[j * 3 + 2] = eyePos4.z(); const Eigen::Vector3f eyeDir = camRot * pl.direction; plDirEye[j * 3 + 0] = eyeDir.x(); plDirEye[j * 3 + 1] = eyeDir.y(); plDirEye[j * 3 + 2] = eyeDir.z(); plColors[j * 3 + 0] = pl.color.x(); plColors[j * 3 + 1] = pl.color.y(); plColors[j * 3 + 2] = pl.color.z(); plWorldXZ[j * 2 + 0] = pl.position.x(); plWorldXZ[j * 2 + 1] = pl.position.z(); plLimitXZ[j * 2 + 0] = pl.limitX; plLimitXZ[j * 2 + 1] = pl.limitY; } // Compute camera view inverse (needed for shadow lookup and skinning world-pos recovery) const Eigen::Matrix4f cameraViewInverse = cameraViewMatrix.inverse(); // Compute light-from-camera matrix for shadow lookup Eigen::Matrix4f lightFromCamera = Eigen::Matrix4f::Identity(); if (hasShadows) { lightFromCamera = shadowMap->getLightSpaceMatrix() * cameraViewInverse; renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); // Pass first active light's eye-space direction as uLightDir for shadow bias renderer.RenderUniform3fv("uLightDir", plDirEye); } renderer.RenderUniform1i("uPointLightCount", lightCount); if (lightCount > 0) { renderer.RenderUniform3fvArray("uPointLightPos[0]", lightCount, plPosEye); renderer.RenderUniform3fvArray("uPointLightDir[0]", lightCount, plDirEye); renderer.RenderUniform3fvArray("uPointLightColor[0]", lightCount, plColors); renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", lightCount, plWorldXZ); renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", lightCount, plLimitXZ); } renderer.RenderUniform1f("uAlpha", 1.0f); for (auto& [name, gameObj] : gameObjects) { if (!gameObj.texture) continue; glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID()); renderer.DrawVertexRenderStruct(gameObj.mesh); } drawInstancedStatics(hasShadows ? "night_fog_shadow_inst" : "night_fog_inst", InstancedTexture::Color, [&]() { renderer.RenderUniform1i(textureUniformName, 0); if (hasShadows) { renderer.RenderUniform1i("uShadowMap", 1); renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data()); renderer.RenderUniform3fv("uLightDir", plDirEye); } renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos); renderer.RenderUniform3fv("uAmbientColor", ambientColor); renderer.RenderUniform3fv("uFogColor", fogColor); renderer.RenderUniform1i("uPointLightCount", lightCount); if (lightCount > 0) { renderer.RenderUniform3fvArray("uPointLightPos[0]", lightCount, plPosEye); renderer.RenderUniform3fvArray("uPointLightDir[0]", lightCount, plDirEye); renderer.RenderUniform3fvArray("uPointLightColor[0]", lightCount, plColors); renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", lightCount, plWorldXZ); renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", lightCount, plLimitXZ); } renderer.RenderUniform1f("uAlpha", 1.0f); }); for (auto& intObj : interactiveObjects) { if (intObj.state.isActive) { intObj.draw(renderer); } } renderer.RenderUniform1f("uAlpha", 1.0f); if (player) player->prepareHitSparksForDraw(cameraViewMatrix); for (auto& [id, remote] : remotePlayers_) if (remote) remote->prepareHitSparksForDraw(cameraViewMatrix); for (auto& npc : npcs) npc->prepareHitSparksForDraw(cameraViewMatrix); for (auto& tz : teleportZones) tz.prepareForDraw(cameraViewMatrix); if (hasShadows) { if (player) player->drawNightWithShadow(renderer, plPosEye, plDirEye, plColors, lightCount, plWorldXZ, plLimitXZ, cameraViewInverse, lightFromCamera, shadowMap->getDepthTexture(), ambientColor, fogColor); for (auto& [id, remote] : remotePlayers_) if (remote) remote->drawNightWithShadow( renderer, plPosEye, plDirEye, plColors, lightCount, plWorldXZ, plLimitXZ, cameraViewInverse, lightFromCamera, shadowMap->getDepthTexture(), ambientColor, fogColor); for (auto& npc : npcs) npc->drawNightWithShadow(renderer, plPosEye, plDirEye, plColors, lightCount, plWorldXZ, plLimitXZ, cameraViewInverse, lightFromCamera, shadowMap->getDepthTexture(), ambientColor, fogColor); } else { if (player) player->drawNight(renderer, plPosEye, plDirEye, plColors, lightCount, plWorldXZ, plLimitXZ, cameraViewInverse, ambientColor, fogColor); for (auto& [id, remote] : remotePlayers_) if (remote) remote->drawNight( renderer, plPosEye, plDirEye, plColors, lightCount, plWorldXZ, plLimitXZ, cameraViewInverse, ambientColor, fogColor); for (auto& npc : npcs) npc->drawNight(renderer, plPosEye, plDirEye, plColors, lightCount, plWorldXZ, plLimitXZ, cameraViewInverse, ambientColor, fogColor); } drawNpcCar(); for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height); pathVisualization.draw(renderer); if (editorMode == EditorMode::Navigation) { editor.drawNavigation(); editor.drawPoints(); } if (editorMode == EditorMode::InteractiveObjects) { editor.drawInteractiveObjectBounds(); } renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); if (npcNameText) { Eigen::Matrix4f proj = MakePerspectiveMatrix(1.0f / 1.5f, static_cast(Environment::width) / static_cast(Environment::height), Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); drawRemotePlayerNames(cameraViewMatrix, proj); for (auto& npc : npcs) { if (npc) npc->drawName(*npcNameText, cameraViewMatrix, proj); } if (player) player->drawHealthBar(renderer, cameraViewMatrix, proj); for (auto& npc : npcs) { if (npc) npc->drawHealthBar(renderer, cameraViewMatrix, proj); } } } bool Location::setNavigationAreaAvailable(const std::string& areaName, bool available) { auto state = stateSnapshot(); if (!state || state->activeNavigationIndex < 0 || state->activeNavigationIndex >= static_cast(state->navigationMaps.size())) { return false; } const auto& areas = state->navigationMaps[state->activeNavigationIndex].getAreas(); const bool found = std::any_of( areas.begin(), areas.end(), [&](const PathFinder::NavigationArea& area) { return area.name == areaName; }); if (!found) return false; gameServer_.submit(SetNavigationAreaAvailableCommand{ locationId_, areaName, available}); return true; } void Location::nudgeCharacterAside(Character* standing, const Eigen::Vector3f& awayFrom) { auto standingState = standing ? standing->stateSnapshot() : nullptr; if (!standingState || standingState->isPlayer) return; auto locationState = stateSnapshot(); if (!locationState || locationState->activeNavigationIndex < 0 || locationState->activeNavigationIndex >= static_cast(locationState->navigationMaps.size())) return; const PathFinder& navigation = locationState->navigationMaps[locationState->activeNavigationIndex]; if (navigation.isReady()) return; static constexpr float kNudgeDist = 1.2f; Eigen::Vector3f dir = standingState->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 = standingState->position + rotated * kNudgeDist; candidate.y() = 0.f; if (navigation.isWalkable(candidate)) { setCharacterTarget(*standing, candidate); return; } } } void Location::update(int64_t delta, const LocationSimulationEvents& events) { synchronizeRemotePlayers(); auto locationState = stateSnapshot(); if (!locationState) return; auto gameSnapshot = gameServer_.snapshot(); if (!gameSnapshot) return; captureNpcCarPresentation(locationState->taxiCar); if (events.taxiArrived) scriptEngine.callCallTaxiCallback(); for (const InteractiveObjectSimulationEvent& event : events.interactiveObjects) { try { if (!event.animationCompleteFunctionName.empty()) scriptEngine.callFunction(event.animationCompleteFunctionName); if (event.activated) { if (!event.activateFunctionName.empty()) scriptEngine.callActivateFunction(event.activateFunctionName); else scriptEngine.callItemPickupCallback(event.objectId); } } catch (const std::exception& e) { std::cerr << "[interactive object] callback error: " << e.what() << "\n"; } } if (!events.npcInteractionId.empty()) { try { scriptEngine.callNpcInteractCallback(events.npcInteractionId); } catch (const std::exception& e) { std::cerr << "[NPC] callback error: " << e.what() << std::endl; } } auto findRuntimeCharacter = [&](const std::string& id) -> Character* { if (id == gameServer_.localPlayerId()) return player.get(); auto remote = remotePlayers_.find(id); if (remote != remotePlayers_.end()) return remote->second.get(); for (auto& npc : npcs) if (npc && npc->entityId() == id) return npc.get(); return nullptr; }; for (const CharacterSimulationEvent& event : events.characters) { Character* character = findRuntimeCharacter(event.characterId); if (!event.events.arrivedCallbackName.empty()) { try { scriptEngine.callFunction(event.events.arrivedCallbackName); } catch (const std::exception& e) { std::cerr << "[script] arrival callback error: " << e.what() << "\n"; } } } for (const CharacterDamageEvent& event : events.damage) { Character* target = findRuntimeCharacter(event.targetId); if (target) target->presentDamage( event.hp, event.maxHp, event.direction, event.attackerId); } auto eventFor = [&](const std::string& id) -> const CharacterUpdateEvents* { for (const CharacterSimulationEvent& event : events.characters) if (event.characterId == id) return &event.events; return nullptr; }; if (player) { player->update(delta, eventFor(gameServer_.localPlayerId())); auto playerState = player->stateSnapshot(); if (playerState) updateTriggerZones(playerState->position); } for (auto& [id, remote] : remotePlayers_) if (remote) remote->update(delta, eventFor(id)); bool npcAttacking = false; for (auto& npc : npcs) { npc->update(delta, eventFor(npc->entityId())); auto npcState = npc->stateSnapshot(); if (npcState && npcState->enabled && npcState->canAttack && (!npcState->pathWaypoints.empty() || npcState->battle_state != 0) && npcState->hp > 0.f) { npcAttacking = true; } } if (gameSnapshot->isDarklands) { if (npcAttacking && !playBattleMusic) { playBattleMusic = true; if (requestDarklandsPlayBattleMusic) requestDarklandsPlayBattleMusic(); } else if (!npcAttacking && playBattleMusic) { playBattleMusic = false; if (requestDarklandsPlayNormalMusic) requestDarklandsPlayNormalMusic(); } } auto playerState = player ? player->stateSnapshot() : nullptr; if (auto* tz = getTargetTeleportZone(*locationState); tz && playerState) { float dist = (playerState->position - tz->position).norm(); if (dist <= tz->radius) { logger() << "[TELEPORT] Player reached teleport zone '" << tz->id << "'" << std::endl; gameServer_.submit(SetLocationTargetsCommand{ locationId_, locationState->targetInteractiveObjectId, locationState->targetInteractNpcId, std::string()}); if (onTeleport) onTeleport(tz->destinationLocation, tz->destinationPosition, tz->destinationRotationY); return; } } if (playerState) { for (auto& tz : teleportZones) { if (!tz.active || !tz.automatic) continue; const float autoR = tz.automaticRadius > 0.0f ? tz.automaticRadius : tz.radius; const float dist = (playerState->position - tz.position).norm(); if (!tz.automaticInitialized) { // If the player starts inside the zone (e.g. just arrived here via teleport), // mark them as already inside so they must leave before it can fire. tz.automaticPlayerInside = (dist <= autoR); tz.automaticInitialized = true; continue; } const bool isInside = (dist <= autoR); if (isInside && !tz.automaticPlayerInside) { tz.automaticPlayerInside = true; logger() << "[TELEPORT] Auto-teleport triggered by zone '" << tz.id << "'" << std::endl; if (onTeleport) onTeleport(tz.destinationLocation, tz.destinationPosition, tz.destinationRotationY); return; } else if (!isInside) { tz.automaticPlayerInside = false; } } } for (auto& intObj : interactiveObjects) intObj.capturePresentationState(); } 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; } auto playerState = player ? player->stateSnapshot() : nullptr; if (!playerState) return; auto locationState = stateSnapshot(); if (!locationState) return; const std::string lastPlayerAttackTarget = playerState->attackTargetId; clearCharacterAttackTarget(*player); // Unproject click to ground plane for 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(localState_->cameraAzimuth), sinAzim = sin(localState_->cameraAzimuth); float cosIncl = cos(localState_->cameraInclination), sinIncl = sin(localState_->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->renderPosition(); Eigen::Vector3f camPos = playerPos + Eigen::Vector3f(-sinAzim * cosIncl, sinIncl, cosAzim * cosIncl) * Environment::zoom + Eigen::Vector3f(0,1,0); Eigen::Vector3f rayDir = (camForward + camRight * (ndcX * aspect * tanHalfFov) + camUp * (ndcY * tanHalfFov)).normalized(); if (editorMode == EditorMode::Navigation) { if (rayDir.y() < -0.001f) { const float t = -camPos.y() / rayDir.y(); const Eigen::Vector3f hit = camPos + rayDir * t; const bool ctrlHeld = (SDL_GetModState() & KMOD_CTRL) != 0; editor.handleLeftClick(hit, ctrlHeld); } return; } if (editorMode == EditorMode::InteractiveObjects) { if (rayDir.y() < -0.001f) { const float t = -camPos.y() / rayDir.y(); const Eigen::Vector3f hit = camPos + rayDir * t; const bool ctrlHeld = (SDL_GetModState() & KMOD_CTRL) != 0; editor.handleInteractiveObjectClick(hit, ctrlHeld); } return; } // Any new gameplay click invalidates the previous click feedback. Ground // clicks and interactive-object clicks that require walking start a replacement. pathVisualization.cancel(); //logger() << "[CLICK] Camera position: (" << camPos.x() << ", " << camPos.y() << ", " << camPos.z() << ")" << std::endl; //logger() << "[CLICK] Ray direction: (" << rayDir.x() << ", " << rayDir.y() << ", " << rayDir.z() << ")" << std::endl; // First check if we clicked on interactive object InteractiveObject* clickedObject = raycastInteractiveObjects(camPos, rayDir); if (clickedObject && player && clickedObject->state.isActive) { //logger() << "[CLICK] *** SUCCESS: Clicked on interactive object: " << clickedObject->loadedObject.name << " ***" << std::endl; gameServer_.submit(SetLocationTargetsCommand{ locationId_, clickedObject->entityId(), std::string(), std::string()}); const Eigen::Vector3f interactionTarget = clickedObject->state.hasInteractionPosition ? clickedObject->state.interactionPosition : clickedObject->state.position; setCharacterTarget(*player, interactionTarget); pathVisualization.start(*player, interactionTarget); clearCharacterAttackTarget(*player); //logger() << "[CLICK] Player moving to object..." << std::endl; } else { //logger() << "Raycast npc step 1" << std::endl; // Check if we clicked on an NPC Character* clickedNpc = raycastNpcs(camPos, rayDir); if (clickedNpc && player) { auto clickedNpcState = clickedNpc->stateSnapshot(); if (!clickedNpcState) return; float distance = (playerState->position - clickedNpcState->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) { //logger() << "Raycast npc step 2" << std::endl; if (clickedNpcState->canAttack) { //logger() << "Raycast npc step 3" << std::endl; //logger() << "npcIndex=" << npcIndex << std::endl; // Hostile NPC: combat logic walks the player toward the stable target ID. if (lastPlayerAttackTarget == clickedNpc->entityId()) { //logger() << "Raycast npc step 4" << std::endl; tryCharacterAttackCombo(*player); } const std::string targetId = clickedNpc->entityId(); gameServer_.submit(SetCharacterTargetCommand{ locationId_, player->entityId(), targetId}); gameServer_.submit(SetLocationTargetsCommand{ locationId_, std::string(), std::string(), std::string()}); if (distance >= Character::kAttackRange) { setCharacterTarget(*player, clickedNpcState->position); pathVisualization.start( *player, clickedNpcState->position, PathVisualization::Style::Hostile); } if (distance <= clickedNpcState->interactionRadius) { //logger() << "[CLICK] Hostile NPC " << npcIndex << " in range; firing on_npc_interact" << std::endl; scriptEngine.callNpcInteractCallback(clickedNpc->entityId()); } } else { // Peaceful NPC: walk to them, fire on_npc_interact when within talk distance. clearCharacterAttackTarget(*player); if (distance <= NPC_TALK_DISTANCE) { // Already in talk range — fire immediately, stop, and face the player. //logger() << "[CLICK] *** SUCCESS: Clicked on NPC index: " << npcIndex << " (in range) ***" << std::endl; setCharacterTarget(*player, playerState->position); gameServer_.submit(SetCharacterFaceTargetCommand{ locationId_, npcs[npcIndex]->entityId(), gameServer_.localPlayerId()}); scriptEngine.callNpcInteractCallback(clickedNpc->entityId()); gameServer_.submit(SetLocationTargetsCommand{ locationId_, std::string(), std::string(), std::string()}); } else { //logger() << "[CLICK] NPC " << npcIndex << " out of talk range (distance " << distance // << " > " << NPC_TALK_DISTANCE << "); walking to NPC..." << std::endl; setCharacterTarget(*player, clickedNpcState->position); pathVisualization.start(*player, clickedNpcState->position); gameServer_.submit(SetLocationTargetsCommand{ locationId_, std::string(), clickedNpc->entityId(), std::string()}); } } } } 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; //logger() << "[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) { //logger() << "[CLICK] Clicked teleport zone '" << clickedTeleport->id << "'" << std::endl; gameServer_.submit(SetLocationTargetsCommand{ locationId_, std::string(), std::string(), clickedTeleport->id}); setCharacterTarget(*player, clickedTeleport->position); pathVisualization.start( *player, clickedTeleport->position, PathVisualization::Style::Teleport); clearCharacterAttackTarget(*player); } else { const Eigen::Vector3f clickedDestination(hit.x(), 0.f, hit.z()); setCharacterTarget(*player, clickedDestination); pathVisualization.start(*player, clickedDestination); clearCharacterAttackTarget(*player); gameServer_.submit(SetLocationTargetsCommand{ locationId_, std::string(), std::string(), std::string()}); if (onPlayerFloorWalk) onPlayerFloorWalk(); } } else { //logger() << "[CLICK] No valid target found" << std::endl; } } //logger() << "========================================\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; localState_->cameraAzimuth += dx * sensitivity; float inclination = localState_->cameraInclination + dy * sensitivity; const float minInclination = M_PI * 30.f / 180.f; const float maxInclination = M_PI * 0.5f; localState_->cameraInclination = max(minInclination, min(maxInclination, inclination)); } } void Location::updatePresentation(int64_t delta) { dialogueSystem.update(static_cast(delta)); pathVisualization.update(delta); for (auto& tz : teleportZones) tz.update(static_cast(delta)); } 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); } void Location::captureAuxiliaryState(LocationGameState& out) const { out.hasPersistentState = true; dialogueSystem.saveDialogueState(out.dialogue); scriptEngine.saveScriptGlobals(out.scriptGlobals); out.triggerZones.clear(); for (const auto& zone : triggerZones) out.triggerZones[zone.id] = {zone.enabled, zone.playerInside}; out.teleportZones.clear(); for (const auto& zone : teleportZones) out.teleportZones[zone.id] = zone.active; } void Location::applyAuxiliaryState(const LocationGameState& in) { dialogueSystem.loadDialogueState(in.dialogue); scriptEngine.loadScriptGlobals(in.scriptGlobals); for (auto& zone : triggerZones) { auto stateIt = in.triggerZones.find(zone.id); if (stateIt != in.triggerZones.end()) { zone.enabled = stateIt->second.enabled; zone.playerInside = stateIt->second.playerInside; } } for (auto& zone : teleportZones) { auto stateIt = in.teleportZones.find(zone.id); if (stateIt != in.teleportZones.end()) zone.active = stateIt->second; } } } // namespace FRG