2272 lines
82 KiB
C++
2272 lines
82 KiB
C++
#include "Location.h"
|
|
#include "utils/Utils.h"
|
|
#include "render/OpenGlExtensions.h"
|
|
#include <iostream>
|
|
#include "render/TextureManager.h"
|
|
#include "TextModel.h"
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <utility>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <cfloat>
|
|
#include <limits>
|
|
#include <random>
|
|
#include <stdexcept>
|
|
#include <unordered_set>
|
|
#include "GameConstants.h"
|
|
#include "Character.h"
|
|
#include "game_server/IGameServer.h"
|
|
#include "external/nlohmann/json.hpp"
|
|
#include <SDL.h>
|
|
|
|
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<const LocationNetworkState> 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<const LocationNetworkState>(
|
|
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<int>((*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<TextRenderer>();
|
|
if (!npcNameText->init(renderer, "resources/fonts/DroidSans.ttf", 24, CONST_ZIP_FILE)) {
|
|
std::cerr << "Failed to init NPC name TextRenderer" << std::endl;
|
|
npcNameText.reset();
|
|
}
|
|
|
|
scriptEngine.init(this, 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<Character> Location::createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer,
|
|
IGameServer& server, const std::string& playerId)
|
|
{
|
|
//auto playerTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/gg/IMG_20260413_182354_992.png", CONST_ZIP_FILE));
|
|
//auto playerTexture = renderer.textureManager.LoadFromPng("resources/w/gg/UniV_Grid_2K_Base_color.png", CONST_ZIP_FILE);
|
|
auto 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<Character>(
|
|
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<std::string> ids;
|
|
for (const auto& item : j["teleports"]) {
|
|
TeleportZone tz;
|
|
tz.id = item.at("id").get<std::string>();
|
|
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<std::string> ids;
|
|
for (const auto& item : j["trigger_zones"]) {
|
|
TriggerZone tz;
|
|
tz.id = item.at("id").get<std::string>();
|
|
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<int>(triggerZones.size()); ++i) {
|
|
TriggerZone& tz = triggerZones[i];
|
|
if (!tz.enabled) continue;
|
|
const float dist = (playerPos - tz.position).norm();
|
|
if (!tz.playerInside && dist <= tz.radius) {
|
|
tz.playerInside = true;
|
|
scriptEngine.callTriggerEnterCallback(tz.id);
|
|
} else if (tz.playerInside && dist > tz.radius + tz.hysteresis) {
|
|
tz.playerInside = false;
|
|
scriptEngine.callTriggerExitCallback(tz.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Location::switchNavigation(int index)
|
|
{
|
|
auto state = stateSnapshot();
|
|
if (!state || index < 0 ||
|
|
index >= static_cast<int>(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<void()>& 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<float>(Environment::width) / static_cast<float>(Environment::height),
|
|
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
|
renderer.PushMatrix();
|
|
|
|
renderer.LoadIdentity();
|
|
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
|
|
//renderer.TranslateMatrix({ 0, -6.f, 0 });
|
|
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(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<float>(Environment::width) / static_cast<float>(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<float>(Environment::width) / static_cast<float>(Environment::height),
|
|
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
|
renderer.PushMatrix();
|
|
|
|
renderer.LoadIdentity();
|
|
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
|
|
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(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<float>(Environment::width) / static_cast<float>(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<float>(Environment::width) / static_cast<float>(Environment::height),
|
|
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
|
renderer.PushMatrix();
|
|
|
|
renderer.LoadIdentity();
|
|
renderer.TranslateMatrix({ 0, 0, -1.0f * Environment::zoom });
|
|
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(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<float>(Environment::width) / static_cast<float>(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<float>(M_PI) * 0.5f; // 90°
|
|
shadowMap->updateSpotlightMatrix(light.position, light.direction, kSpotFovY, 0.1f, 20.0f);
|
|
shadowMap->bind();
|
|
|
|
glEnable(GL_DEPTH_TEST);
|
|
glDepthFunc(GL_LESS);
|
|
glEnable(GL_CULL_FACE);
|
|
|
|
renderer.shaderManager.PushShader("shadow_depth");
|
|
renderer.PushPerspectiveProjectionMatrix(kSpotFovY, 1.0f, 0.1f, 20.0f);
|
|
renderer.PushSpecialMatrix(shadowMap->getLightViewMatrix());
|
|
|
|
for (auto& [name, gameObj] : gameObjects) {
|
|
renderer.DrawVertexRenderStruct(gameObj.mesh);
|
|
}
|
|
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<std::chrono::milliseconds>(
|
|
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<float>(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
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<int>(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<int> activeLightIndices;
|
|
activeLightIndices.reserve(pointLights.size());
|
|
for (int i = 0; i < static_cast<int>(pointLights.size()); ++i) {
|
|
const PointLight& pl = pointLights[i];
|
|
bool active = !pl.autoLight;
|
|
if (!active && 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<int>(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<float>(Environment::width) / static_cast<float>(Environment::height),
|
|
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
|
renderer.PushMatrix();
|
|
|
|
renderer.LoadIdentity();
|
|
renderer.TranslateMatrix({ 0, 0, -1.0f * Environment::zoom });
|
|
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(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<int>(activeLightIndices.size());
|
|
for (int j = 0; j < lightCount; ++j) {
|
|
const PointLight& pl = pointLights[activeLightIndices[j]];
|
|
const Eigen::Vector4f worldPos(pl.position.x(), pl.position.y(), pl.position.z(), 1.0f);
|
|
const Eigen::Vector4f eyePos4 = cameraViewMatrix * worldPos;
|
|
plPosEye[j * 3 + 0] = eyePos4.x();
|
|
plPosEye[j * 3 + 1] = eyePos4.y();
|
|
plPosEye[j * 3 + 2] = eyePos4.z();
|
|
const Eigen::Vector3f eyeDir = camRot * pl.direction;
|
|
plDirEye[j * 3 + 0] = eyeDir.x();
|
|
plDirEye[j * 3 + 1] = eyeDir.y();
|
|
plDirEye[j * 3 + 2] = eyeDir.z();
|
|
plColors[j * 3 + 0] = pl.color.x();
|
|
plColors[j * 3 + 1] = pl.color.y();
|
|
plColors[j * 3 + 2] = pl.color.z();
|
|
plWorldXZ[j * 2 + 0] = pl.position.x();
|
|
plWorldXZ[j * 2 + 1] = pl.position.z();
|
|
plLimitXZ[j * 2 + 0] = pl.limitX;
|
|
plLimitXZ[j * 2 + 1] = pl.limitY;
|
|
}
|
|
|
|
// Compute camera view inverse (needed for shadow lookup and skinning world-pos recovery)
|
|
const Eigen::Matrix4f cameraViewInverse = cameraViewMatrix.inverse();
|
|
|
|
// Compute light-from-camera matrix for shadow lookup
|
|
Eigen::Matrix4f lightFromCamera = Eigen::Matrix4f::Identity();
|
|
if (hasShadows) {
|
|
lightFromCamera = shadowMap->getLightSpaceMatrix() * cameraViewInverse;
|
|
renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data());
|
|
// Pass first active light's eye-space direction as uLightDir for shadow bias
|
|
renderer.RenderUniform3fv("uLightDir", plDirEye);
|
|
}
|
|
|
|
renderer.RenderUniform1i("uPointLightCount", lightCount);
|
|
if (lightCount > 0) {
|
|
renderer.RenderUniform3fvArray("uPointLightPos[0]", lightCount, plPosEye);
|
|
renderer.RenderUniform3fvArray("uPointLightDir[0]", lightCount, plDirEye);
|
|
renderer.RenderUniform3fvArray("uPointLightColor[0]", lightCount, plColors);
|
|
renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", lightCount, plWorldXZ);
|
|
renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", lightCount, plLimitXZ);
|
|
}
|
|
|
|
renderer.RenderUniform1f("uAlpha", 1.0f);
|
|
for (auto& [name, gameObj] : gameObjects) {
|
|
if (!gameObj.texture) continue;
|
|
glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID());
|
|
renderer.DrawVertexRenderStruct(gameObj.mesh);
|
|
}
|
|
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<float>(Environment::width) / static_cast<float>(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<int>(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<int>(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<float>(M_PI * 0.5), static_cast<float>(-M_PI * 0.5), static_cast<float>(M_PI) };
|
|
for (float angle : angles) {
|
|
Eigen::Vector3f rotated = Eigen::AngleAxisf(angle, Eigen::Vector3f::UnitY()) * dir;
|
|
Eigen::Vector3f candidate = 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<float>(mx), Environment::projectionHeight - static_cast<float>(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<int>(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<float>(mx), Environment::projectionHeight - static_cast<float>(my));
|
|
return;
|
|
}
|
|
|
|
}
|
|
void Location::handleMotion(int64_t fingerId, int eventX, int eventY, int mx, int my)
|
|
{
|
|
if (dialogueSystem.blocksGameplayInput()) {
|
|
dialogueSystem.handlePointerMoved(
|
|
static_cast<float>(mx),
|
|
Environment::projectionHeight - static_cast<float>(my)
|
|
);
|
|
}
|
|
|
|
if (cameraDragging) {
|
|
int dx = eventX - lastMouseX;
|
|
int dy = eventY - lastMouseY;
|
|
lastMouseX = eventX;
|
|
lastMouseY = eventY;
|
|
|
|
const float sensitivity = 0.005f;
|
|
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<int>(delta));
|
|
pathVisualization.update(delta);
|
|
for (auto& tz : teleportZones)
|
|
tz.update(static_cast<float>(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<void(const std::string&)> cb)
|
|
{
|
|
dialogueSystem.setOnCutsceneFinished(std::move(cb));
|
|
}
|
|
|
|
void Location::setDialogueFlag(const std::string& flag, int value)
|
|
{
|
|
dialogueSystem.setFlag(flag, value);
|
|
}
|
|
|
|
int Location::getDialogueFlag(const std::string& flag) const
|
|
{
|
|
return dialogueSystem.getFlag(flag);
|
|
}
|
|
|
|
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
|