1075 lines
38 KiB
C++
1075 lines
38 KiB
C++
#include "Location.h"
|
|
#include "utils/Utils.h"
|
|
#include "render/OpenGlExtensions.h"
|
|
#include <iostream>
|
|
#include "render/TextureManager.h"
|
|
#include "TextModel.h"
|
|
#include <random>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <cfloat>
|
|
#include "GameConstants.h"
|
|
#include "Character.h"
|
|
|
|
|
|
namespace ZL
|
|
{
|
|
extern const char* CONST_ZIP_FILE;
|
|
|
|
static constexpr float CAMERA_FOV_Y = 1.0f / 1.5f;
|
|
// How close the player needs to be to a peaceful NPC before the
|
|
// on_npc_interact callback fires and the conversation begins.
|
|
static constexpr float NPC_TALK_DISTANCE = 1.25f;
|
|
|
|
static float distancePointToSegmentXZ(const Eigen::Vector3f& p,
|
|
const Eigen::Vector3f& a,
|
|
const Eigen::Vector3f& b)
|
|
{
|
|
const Eigen::Vector2f p2(p.x(), p.z());
|
|
const Eigen::Vector2f a2(a.x(), a.z());
|
|
const Eigen::Vector2f b2(b.x(), b.z());
|
|
const Eigen::Vector2f ab = b2 - a2;
|
|
const float abLenSq = ab.squaredNorm();
|
|
if (abLenSq <= 1e-8f) {
|
|
return (p2 - a2).norm();
|
|
}
|
|
const float t = std::clamp((p2 - a2).dot(ab) / abLenSq, 0.0f, 1.0f);
|
|
const Eigen::Vector2f closest = a2 + ab * t;
|
|
return (p2 - closest).norm();
|
|
}
|
|
|
|
Location::Location(Renderer& iRenderer, Inventory& iInventory)
|
|
: renderer(iRenderer)
|
|
, inventory(iInventory)
|
|
{
|
|
|
|
}
|
|
|
|
void Location::setup(const LocationSetup& params)
|
|
{
|
|
roomTexture = std::make_unique<Texture>(CreateTextureDataFromPng(params.roomTexturePath, CONST_ZIP_FILE));
|
|
roomMesh.data = LoadFromTextFile02(params.roomMeshPath, CONST_ZIP_FILE);
|
|
roomMesh.data.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
roomMesh.RefreshVBO();
|
|
|
|
// Load static game objects
|
|
gameObjects = GameObjectLoader::loadAndCreateGameObjects(params.gameObjectsJsonPath, renderer, CONST_ZIP_FILE);
|
|
|
|
// Load interactive objects
|
|
interactiveObjects = GameObjectLoader::loadAndCreateInteractiveObjects(params.gameObjectsJsonPath, renderer, CONST_ZIP_FILE);
|
|
|
|
|
|
//auto playerTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/gg/IMG_20260413_182354_992.png", CONST_ZIP_FILE));
|
|
auto playerTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/gg/UniV_Grid_2K_Base_color.png", CONST_ZIP_FILE));
|
|
|
|
auto sparkTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/spark.png", CONST_ZIP_FILE));
|
|
|
|
player = std::make_unique<Character>();
|
|
/*
|
|
player->loadBinaryAnimation(AnimationState::STAND, "resources/w/gg/gg_stand_idle001.anim", CONST_ZIP_FILE);
|
|
player->loadBinaryAnimation(AnimationState::WALK, "resources/w/gg/gg_walking001.anim", CONST_ZIP_FILE);
|
|
player->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/gg/gg_stand_to_action002.anim", CONST_ZIP_FILE);
|
|
player->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/gg/gg_action_attack001.anim", CONST_ZIP_FILE);
|
|
player->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/gg/gg_action_idle001.anim", CONST_ZIP_FILE);
|
|
player->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/gg/gg_action_to_stand001.anim", CONST_ZIP_FILE);
|
|
*/
|
|
|
|
|
|
player->loadBinaryAnimation(AnimationState::STAND, "resources/w/gg/new/gg_stand_idle001.anim", CONST_ZIP_FILE);
|
|
player->loadBinaryAnimation(AnimationState::WALK, "resources/w/gg/new/gg_walk001.anim", CONST_ZIP_FILE);
|
|
player->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/gg/new/gg_stand_to_action001.anim", CONST_ZIP_FILE);
|
|
|
|
player->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/gg/gg_action_chop001.anim", CONST_ZIP_FILE);
|
|
player->loadBinaryAnimation(AnimationState::ACTION_ATTACK_2, "resources/w/gg/gg_action_stab001.anim", CONST_ZIP_FILE);
|
|
player->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/gg/gg_action_idle002.anim", CONST_ZIP_FILE);
|
|
player->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/gg/new/gg_action_to_stand001.anim", CONST_ZIP_FILE);
|
|
player->loadBinaryAnimation(AnimationState::ACTION_TO_DEATH, "resources/w/gg/new/gg_die001.anim", CONST_ZIP_FILE);
|
|
player->loadBinaryAnimation(AnimationState::DEATH_IDLE, "resources/w/gg/new/gg_die_idle001.anim", CONST_ZIP_FILE);
|
|
|
|
|
|
|
|
player->weaponTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/white.png", CONST_ZIP_FILE));
|
|
//player->weaponMesh.AssignFrom(LoadFromTextFile02("resources/w/gg/knife001.txt", CONST_ZIP_FILE));
|
|
|
|
player->weaponMesh.data = LoadFromTextFile02("resources/w/gg/knife002.txt", CONST_ZIP_FILE);
|
|
player->weaponMesh.data.Scale(0.1f);
|
|
//player->weaponMesh.data.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
player->weaponMesh.RefreshVBO();
|
|
player->weaponAttachBoneName = "RightHand";
|
|
player->weaponInitialRotation = Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitZ()).toRotationMatrix();
|
|
player->weaponInitialPosition = Eigen::Vector3f(0, 0.09, 0.016);
|
|
|
|
player->setTexture(playerTexture);
|
|
player->walkSpeed = 3.0f;
|
|
player->rotationSpeed = 8.0f;
|
|
|
|
player->modelScale = 1.f;
|
|
player->modelCorrectionRotation = Eigen::Quaternionf(
|
|
Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())
|
|
*
|
|
Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitX())
|
|
|
|
);
|
|
|
|
player->canAttack = true;
|
|
player->isPlayer = true;
|
|
player->position = params.playerPosition;
|
|
player->setTarget(params.playerPosition);
|
|
player->setupHitSparks(sparkTexture);
|
|
std::cout << "Load resurces step 9" << std::endl;
|
|
|
|
// Load NPCs from JSON. Aggressive (canAttack) NPCs need a player target
|
|
// and hit sparks; peaceful NPCs don't take damage from the player so
|
|
// they don't need either.
|
|
npcs = GameObjectLoader::loadAndCreate_Npcs(params.npcsJsonPath, CONST_ZIP_FILE);
|
|
for (auto& npc : npcs) {
|
|
if (!npc) continue;
|
|
if (npc->canAttack) {
|
|
npc->setupHitSparks(sparkTexture);
|
|
npc->attackTarget = player.get();
|
|
}
|
|
}
|
|
|
|
auto sparkTexture2 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/star.png", CONST_ZIP_FILE));
|
|
|
|
// Teleport zone visuals: an upward fountain of sparks parked at the
|
|
// teleport position, so the player can spot the zone from a distance.
|
|
teleportPosition = params.teleportPosition;
|
|
teleportRadius = params.teleportRadius;
|
|
if (teleportRadius > 0.0f) {
|
|
teleportSparks = std::make_unique<SparkEmitter>();
|
|
std::vector<Vector3f> teleportEmitPoints;
|
|
teleportEmitPoints.push_back(Vector3f{ teleportPosition.x(), teleportPosition.y(), teleportPosition.z() });
|
|
teleportSparks->setEmissionPoints(teleportEmitPoints);
|
|
teleportSparks->setTexture(sparkTexture2);
|
|
teleportSparks->setEmissionRate(50.0f);
|
|
teleportSparks->setMaxParticles(80);
|
|
teleportSparks->setParticleSize(0.05f);
|
|
teleportSparks->setBiasX(0.0f);
|
|
teleportSparks->setEmissionDirection(Vector3f{ 0.0f, 1.0f, 0.0f });
|
|
teleportSparks->setEmissionRadius(teleportRadius);
|
|
teleportSparks->setSpeedRange(0.5f, 1.0f);
|
|
teleportSparks->setZSpeedRange(0.5f, 1.5f);
|
|
teleportSparks->setScaleRange(0.5f, 1.0f);
|
|
teleportSparks->setLifeTimeRange(1500.0f, 2500.0f);
|
|
teleportSparks->setUseWorldSpace(true);
|
|
teleportSparks->markConfigured();
|
|
|
|
// If the player happens to spawn already inside the zone, treat them
|
|
// as in-zone so they don't immediately teleport on the first update.
|
|
if (player && (player->position - teleportPosition).norm() <= teleportRadius) {
|
|
playerInTeleportZone = true;
|
|
}
|
|
}
|
|
|
|
#ifndef EMSCRIPTEN
|
|
// Create shadow map (2048x2048, ortho size 40, near 0.1, far 100)
|
|
shadowMap = std::make_unique<ShadowMap>(2048, 40.0f, 0.1f, 100.0f);
|
|
shadowMap->setLightDirection(Eigen::Vector3f(-0.5f, -1.0f, -0.3f));
|
|
std::cout << "Shadow map initialized" << std::endl;
|
|
#endif
|
|
|
|
setupNavigation(params.navigationJsonPath);
|
|
|
|
scriptEngine.init(this, &inventory, params.scriptPath);
|
|
|
|
dialogueSystem.init(renderer, CONST_ZIP_FILE);
|
|
dialogueSystem.loadDatabase(params.dialoguesJsonPath);
|
|
|
|
npcNameText = std::make_unique<TextRenderer>();
|
|
if (!npcNameText->init(renderer, "resources/fonts/DroidSans.ttf", 24, CONST_ZIP_FILE)) {
|
|
std::cerr << "Failed to init NPC name TextRenderer" << std::endl;
|
|
npcNameText.reset();
|
|
}
|
|
dialogueSystem.loadDatabase("resources/dialogue/sample_dialogues.json");
|
|
/*dialogueSystem.addTriggerZone({
|
|
"ghost_room_trigger",
|
|
"test_line_dialogue",
|
|
Eigen::Vector3f(0.0f, 0.0f, -8.5f),
|
|
2.0f,
|
|
true,
|
|
false
|
|
});*/
|
|
|
|
|
|
}
|
|
|
|
void Location::setupNavigation(const std::string& navigationJsonPath)
|
|
{
|
|
// Static navigation blockers are defined in the navigation JSON as polygons.
|
|
// NPC + player are handled as dynamic obstacles, so they are intentionally NOT in JSON.
|
|
navigation.build({}, navigationJsonPath, CONST_ZIP_FILE);
|
|
|
|
#ifdef SHOW_PATH
|
|
buildDebugNavMeshes();
|
|
#endif
|
|
|
|
static constexpr float kDynamicObstacleInfluenceDist = 6.0f;
|
|
|
|
auto makePlanner = [this](const Character* self) {
|
|
return [this, self](const Eigen::Vector3f& start, const Eigen::Vector3f& end) {
|
|
std::vector<PathFinder::DynamicObstacle> dynamicObstacles;
|
|
dynamicObstacles.reserve(npcs.size() + 1);
|
|
|
|
const auto addCharacter = [&](const Character* other) {
|
|
if (!other || other == self) return;
|
|
if (other->hp <= 0.f) return;
|
|
|
|
if (distancePointToSegmentXZ(other->position, start, end) > kDynamicObstacleInfluenceDist) {
|
|
return;
|
|
}
|
|
|
|
PathFinder::DynamicObstacle obs;
|
|
obs.position = Eigen::Vector3f(other->position.x(), navigation.getFloorY(), other->position.z());
|
|
obs.radius = (std::max)(0.0f, other->collisionRadius);
|
|
dynamicObstacles.push_back(obs);
|
|
};
|
|
|
|
addCharacter(player.get());
|
|
for (const auto& npc : npcs) {
|
|
addCharacter(npc.get());
|
|
}
|
|
|
|
return navigation.findPath(start, end, dynamicObstacles);
|
|
};
|
|
};
|
|
|
|
if (player) {
|
|
player->setPathPlanner(makePlanner(player.get()));
|
|
}
|
|
|
|
for (auto& npc : npcs) {
|
|
if (npc) {
|
|
npc->setPathPlanner(makePlanner(npc.get()));
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef SHOW_PATH
|
|
void Location::buildDebugNavMeshes()
|
|
{
|
|
debugNavMeshes.clear();
|
|
const auto& areas = navigation.getAreas();
|
|
float y = navigation.getFloorY() + 0.02f;
|
|
Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
|
|
|
|
for (const auto& area : areas) {
|
|
if (area.polygon.size() < 3) continue;
|
|
VertexRenderStruct mesh;
|
|
mesh.data = CreatePolygonFloor(area.polygon, y, red);
|
|
mesh.RefreshVBO();
|
|
debugNavMeshes.push_back(std::move(mesh));
|
|
}
|
|
}
|
|
|
|
void Location::drawDebugNavigation()
|
|
{
|
|
renderer.shaderManager.PushShader("defaultColor");
|
|
renderer.SetMatrix();
|
|
for (const auto& mesh : debugNavMeshes) {
|
|
renderer.DrawVertexRenderStruct(mesh);
|
|
}
|
|
renderer.shaderManager.PopShader();
|
|
renderer.SetMatrix();
|
|
}
|
|
#endif
|
|
|
|
|
|
InteractiveObject* Location::raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir) {
|
|
if (interactiveObjects.empty()) {
|
|
std::cout << "[RAYCAST] No interactive objects to check" << std::endl;
|
|
return nullptr;
|
|
}
|
|
|
|
std::cout << "[RAYCAST] Starting raycast with " << interactiveObjects.size() << " objects" << std::endl;
|
|
std::cout << "[RAYCAST] Ray origin: (" << rayOrigin.x() << ", " << rayOrigin.y() << ", " << rayOrigin.z() << ")" << std::endl;
|
|
std::cout << "[RAYCAST] Ray dir: (" << rayDir.x() << ", " << rayDir.y() << ", " << rayDir.z() << ")" << std::endl;
|
|
|
|
float closestDistance = FLT_MAX;
|
|
InteractiveObject* closestObject = nullptr;
|
|
|
|
for (auto& intObj : interactiveObjects) {
|
|
std::cout << "[RAYCAST] Checking object: " << intObj.name << " (active: " << intObj.isActive << ")" << std::endl;
|
|
|
|
if (!intObj.isActive) {
|
|
std::cout << "[RAYCAST] -> Object inactive, skipping" << std::endl;
|
|
continue;
|
|
}
|
|
|
|
std::cout << "[RAYCAST] Position: (" << intObj.position.x() << ", " << intObj.position.y() << ", "
|
|
<< intObj.position.z() << "), Radius: " << intObj.interactionRadius << std::endl;
|
|
|
|
Eigen::Vector3f toObject = intObj.position - rayOrigin;
|
|
std::cout << "[RAYCAST] Vector to object: (" << toObject.x() << ", " << toObject.y() << ", " << toObject.z() << ")" << std::endl;
|
|
|
|
float distanceAlongRay = toObject.dot(rayDir);
|
|
std::cout << "[RAYCAST] Distance along ray: " << distanceAlongRay << std::endl;
|
|
|
|
if (distanceAlongRay < 0.1f) {
|
|
std::cout << "[RAYCAST] -> Object behind camera, skipping" << std::endl;
|
|
continue;
|
|
}
|
|
|
|
Eigen::Vector3f closestPointOnRay = rayOrigin + rayDir * distanceAlongRay;
|
|
float distToObject = (closestPointOnRay - intObj.position).norm();
|
|
|
|
std::cout << "[RAYCAST] Distance to object: " << distToObject
|
|
<< " (interaction radius: " << intObj.interactionRadius << ")" << std::endl;
|
|
|
|
if (distToObject <= intObj.interactionRadius && distanceAlongRay < closestDistance) {
|
|
std::cout << "[RAYCAST] *** HIT DETECTED! ***" << std::endl;
|
|
closestDistance = distanceAlongRay;
|
|
closestObject = &intObj;
|
|
}
|
|
}
|
|
|
|
if (closestObject) {
|
|
std::cout << "[RAYCAST] *** RAYCAST SUCCESS: Found object " << closestObject->name << " ***" << std::endl;
|
|
}
|
|
else {
|
|
std::cout << "[RAYCAST] No objects hit" << std::endl;
|
|
}
|
|
|
|
return closestObject;
|
|
}
|
|
|
|
Character* Location::raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance) {
|
|
// Every NPC is treated as a vertical cylinder: radius 1.0m, height 1.85m,
|
|
// base at npc->position (the model's foot). Intersection = circle hit in the
|
|
// XZ plane, then clip the entry/exit t against the [yFoot, yFoot+height] slab.
|
|
static constexpr float NPC_CLICK_RADIUS = 1.0f;
|
|
static constexpr float NPC_CLICK_HEIGHT = 1.85f;
|
|
|
|
Character* closestNpc = nullptr;
|
|
float closestDist = maxDistance;
|
|
|
|
std::cout << "[RAYCAST_NPC] Starting raycast with " << npcs.size() << " npcs" << std::endl;
|
|
|
|
for (auto& npc : npcs) {
|
|
const float dx = rayOrigin.x() - npc->position.x();
|
|
const float dz = rayOrigin.z() - npc->position.z();
|
|
const float a = rayDir.x() * rayDir.x() + rayDir.z() * rayDir.z();
|
|
if (a < 1e-6f) continue; // purely-vertical ray; not a real click case
|
|
|
|
const float b = 2.0f * (dx * rayDir.x() + dz * rayDir.z());
|
|
const float c = dx * dx + dz * dz - NPC_CLICK_RADIUS * NPC_CLICK_RADIUS;
|
|
const float disc = b * b - 4.0f * a * c;
|
|
if (disc < 0.0f) continue; // ray misses the infinite cylinder
|
|
|
|
const float sqrtDisc = std::sqrt(disc);
|
|
const float tNear = (-b - sqrtDisc) / (2.0f * a);
|
|
const float tFar = (-b + sqrtDisc) / (2.0f * a);
|
|
|
|
float entryT = max(tNear, 0.1f);
|
|
float exitT = tFar;
|
|
|
|
// Clip against the cylinder's vertical extent.
|
|
const float yMin = npc->position.y();
|
|
const float yMax = yMin + NPC_CLICK_HEIGHT;
|
|
if (std::abs(rayDir.y()) > 1e-6f) {
|
|
const float tYa = (yMin - rayOrigin.y()) / rayDir.y();
|
|
const float tYb = (yMax - rayOrigin.y()) / rayDir.y();
|
|
entryT = max(entryT, min(tYa, tYb));
|
|
exitT = min(exitT, max(tYa, tYb));
|
|
}
|
|
else if (rayOrigin.y() < yMin || rayOrigin.y() > yMax) {
|
|
continue; // horizontal ray passing above or below the cylinder
|
|
}
|
|
|
|
if (entryT > exitT) continue;
|
|
|
|
std::cout << "[RAYCAST_NPC] " << npc->npcId << " hit at t=" << entryT << std::endl;
|
|
|
|
if (entryT < closestDist) {
|
|
closestDist = entryT;
|
|
closestNpc = npc.get();
|
|
}
|
|
}
|
|
|
|
if (closestNpc) {
|
|
std::cout << "[RAYCAST_NPC] HIT: " << closestNpc->npcId << std::endl;
|
|
}
|
|
else {
|
|
std::cout << "[RAYCAST_NPC] No NPC hit" << std::endl;
|
|
}
|
|
|
|
return closestNpc;
|
|
}
|
|
|
|
|
|
void Location::drawGame()
|
|
{
|
|
|
|
glClearColor(0.53, 0.81, 0.92, 1.0f);
|
|
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
|
|
|
|
renderer.shaderManager.PushShader("fog");
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
const float playerEyePos[3] = { 0.0f, 0.0f, -Environment::zoom };
|
|
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos);
|
|
|
|
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
|
|
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
|
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
|
renderer.PushMatrix();
|
|
|
|
renderer.LoadIdentity();
|
|
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
|
|
//renderer.TranslateMatrix({ 0, -6.f, 0 });
|
|
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero();
|
|
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
|
|
|
glBindTexture(GL_TEXTURE_2D, roomTexture->getTexID());
|
|
renderer.DrawVertexRenderStruct(roomMesh);
|
|
|
|
for (auto& [name, gameObj] : gameObjects) {
|
|
glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID());
|
|
renderer.DrawVertexRenderStruct(gameObj.mesh);
|
|
}
|
|
|
|
for (auto& intObj : interactiveObjects) {
|
|
if (intObj.isActive) {
|
|
intObj.draw(renderer);
|
|
}
|
|
}
|
|
|
|
const Eigen::Matrix4f currentView = renderer.GetCurrentModelViewMatrix();
|
|
if (player) player->prepareHitSparksForDraw(currentView);
|
|
for (auto& npc : npcs) npc->prepareHitSparksForDraw(currentView);
|
|
if (teleportSparks) teleportSparks->prepareForDraw(currentView);
|
|
|
|
if (player) player->draw(renderer);
|
|
for (auto& npc : npcs) npc->draw(renderer);
|
|
|
|
if (teleportSparks) {
|
|
teleportSparks->draw(renderer, Environment::zoom, Environment::width, Environment::height);
|
|
}
|
|
|
|
#ifdef SHOW_PATH
|
|
drawDebugNavigation();
|
|
#endif
|
|
|
|
renderer.PopMatrix();
|
|
|
|
renderer.PopProjectionMatrix();
|
|
|
|
renderer.shaderManager.PopShader();
|
|
|
|
if (npcNameText) {
|
|
Eigen::Matrix4f proj = MakePerspectiveMatrix(1.0f / 1.5f,
|
|
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
|
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
|
for (auto& npc : npcs) {
|
|
if (npc) npc->drawName(*npcNameText, currentView, proj);
|
|
}
|
|
if (player) player->drawHealthBar(renderer, currentView, proj);
|
|
for (auto& npc : npcs) {
|
|
if (npc) npc->drawHealthBar(renderer, currentView, proj);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Location::drawShadowDepthPass()
|
|
{
|
|
if (!shadowMap) return;
|
|
|
|
const Eigen::Vector3f& sceneCenter = player ? player->position : Eigen::Vector3f::Zero();
|
|
shadowMap->updateLightSpaceMatrix(sceneCenter);
|
|
|
|
shadowMap->bind();
|
|
|
|
glEnable(GL_DEPTH_TEST);
|
|
glDepthFunc(GL_LESS);
|
|
|
|
// Use front-face culling during depth pass to reduce shadow acne on lit faces
|
|
//glCullFace(GL_FRONT);
|
|
glEnable(GL_CULL_FACE);
|
|
|
|
renderer.shaderManager.PushShader("shadow_depth");
|
|
|
|
// Set up light's orthographic projection
|
|
const Eigen::Matrix4f& lightProj = shadowMap->getLightProjectionMatrix();
|
|
const Eigen::Matrix4f& lightView = shadowMap->getLightViewMatrix();
|
|
|
|
// Push the light's projection matrix via the 6-param ortho overload won't
|
|
// match our pre-computed matrix. Instead, use the raw stack approach:
|
|
// push a dummy projection then overwrite via PushSpecialMatrix-style.
|
|
// Simpler: push ortho then push the light view as modelview.
|
|
renderer.PushProjectionMatrix(
|
|
-40.0f, 40.0f,
|
|
-40.0f, 40.0f,
|
|
0.1f, 100.0f);
|
|
|
|
const Eigen::Vector3f& lightDir = shadowMap->getLightDirection();
|
|
Eigen::Vector3f lightPos = sceneCenter - lightDir * 50.0f;
|
|
Eigen::Vector3f up(0.0f, 1.0f, 0.0f);
|
|
if (std::abs(lightDir.dot(up)) > 0.99f) {
|
|
up = Eigen::Vector3f(0.0f, 0.0f, 1.0f);
|
|
}
|
|
|
|
// Build the light view matrix and push it
|
|
renderer.PushSpecialMatrix(lightView);
|
|
|
|
// Draw static geometry
|
|
renderer.DrawVertexRenderStruct(roomMesh);
|
|
|
|
for (auto& [name, gameObj] : gameObjects) {
|
|
renderer.DrawVertexRenderStruct(gameObj.mesh);
|
|
}
|
|
|
|
for (auto& intObj : interactiveObjects) {
|
|
if (intObj.isActive && intObj.texture) {
|
|
renderer.PushMatrix();
|
|
renderer.TranslateMatrix(intObj.position);
|
|
renderer.DrawVertexRenderStruct(intObj.mesh);
|
|
renderer.PopMatrix();
|
|
}
|
|
}
|
|
|
|
// Draw characters (they handle their own skinning shader switch internally)
|
|
if (player) player->drawShadowDepth(renderer);
|
|
for (auto& npc : npcs) npc->drawShadowDepth(renderer);
|
|
|
|
renderer.PopMatrix(); // light view
|
|
renderer.PopProjectionMatrix();
|
|
renderer.shaderManager.PopShader();
|
|
|
|
//glCullFace(GL_BACK);
|
|
glDisable(GL_CULL_FACE);
|
|
|
|
shadowMap->unbind();
|
|
}
|
|
|
|
void Location::drawGameWithShadows()
|
|
{
|
|
glClearColor(0.53, 0.81, 0.92, 1.0f);
|
|
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
|
|
|
|
#ifdef DEBUG_LIGHT
|
|
// Debug mode: render from the light's point of view using the plain
|
|
// textured shader so we can see what the shadow map "sees".
|
|
renderer.shaderManager.PushShader(defaultShaderName);
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
|
|
renderer.PushProjectionMatrix(
|
|
-40.0f, 40.0f,
|
|
-40.0f, 40.0f,
|
|
0.1f, 1000.0f);
|
|
|
|
renderer.PushSpecialMatrix(shadowMap->getLightViewMatrix());
|
|
|
|
#else
|
|
static const std::string shadowShaderName = "fog_shadow";
|
|
|
|
renderer.shaderManager.PushShader(shadowShaderName);
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
renderer.RenderUniform1i("uShadowMap", 1);
|
|
const float playerEyePos[3] = { 0.0f, 0.0f, -Environment::zoom };
|
|
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos);
|
|
|
|
// Bind shadow map texture to unit 1
|
|
glActiveTexture(GL_TEXTURE1);
|
|
glBindTexture(GL_TEXTURE_2D, shadowMap->getDepthTexture());
|
|
glActiveTexture(GL_TEXTURE0);
|
|
|
|
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
|
|
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
|
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
|
renderer.PushMatrix();
|
|
|
|
renderer.LoadIdentity();
|
|
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
|
|
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero();
|
|
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
|
|
|
// Capture the camera view matrix and compute uLightFromCamera
|
|
cameraViewMatrix = renderer.GetCurrentModelViewMatrix();
|
|
Eigen::Matrix4f cameraViewInverse = cameraViewMatrix.inverse();
|
|
Eigen::Matrix4f lightFromCamera = shadowMap->getLightSpaceMatrix() * cameraViewInverse;
|
|
renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data());
|
|
|
|
// Light direction in camera space for diffuse lighting
|
|
Eigen::Vector3f lightDirCamera = cameraViewMatrix.block<3, 3>(0, 0) * shadowMap->getLightDirection();
|
|
renderer.RenderUniform3fv("uLightDir", lightDirCamera.data());
|
|
#endif
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
glBindTexture(GL_TEXTURE_2D, roomTexture->getTexID());
|
|
renderer.DrawVertexRenderStruct(roomMesh);
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
for (auto& [name, gameObj] : gameObjects) {
|
|
glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID());
|
|
renderer.DrawVertexRenderStruct(gameObj.mesh);
|
|
}
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
for (auto& intObj : interactiveObjects) {
|
|
if (intObj.isActive) {
|
|
intObj.draw(renderer);
|
|
}
|
|
}
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
#ifdef DEBUG_LIGHT
|
|
// In debug-light mode characters use the plain shaders (draw normally
|
|
// but from the light's viewpoint — projection/view already on stack).
|
|
{
|
|
const Eigen::Matrix4f currentView = renderer.GetCurrentModelViewMatrix();
|
|
if (player) player->prepareHitSparksForDraw(currentView);
|
|
for (auto& npc : npcs) npc->prepareHitSparksForDraw(currentView);
|
|
}
|
|
if (player) player->draw(renderer);
|
|
for (auto& npc : npcs) npc->draw(renderer);
|
|
#else
|
|
// Characters use their own shadow-aware shaders
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
if (player) player->prepareHitSparksForDraw(cameraViewMatrix);
|
|
for (auto& npc : npcs) npc->prepareHitSparksForDraw(cameraViewMatrix);
|
|
if (teleportSparks) teleportSparks->prepareForDraw(cameraViewMatrix);
|
|
|
|
if (player) player->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
for (auto& npc : npcs) npc->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
|
|
|
|
if (teleportSparks) {
|
|
teleportSparks->draw(renderer, Environment::zoom, Environment::width, Environment::height);
|
|
}
|
|
#endif
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
renderer.PopMatrix();
|
|
renderer.PopProjectionMatrix();
|
|
renderer.shaderManager.PopShader();
|
|
|
|
if (npcNameText) {
|
|
Eigen::Matrix4f proj = MakePerspectiveMatrix(1.0f / 1.5f,
|
|
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
|
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
|
for (auto& npc : npcs) {
|
|
if (npc) npc->drawName(*npcNameText, cameraViewMatrix, proj);
|
|
}
|
|
if (player) player->drawHealthBar(renderer, cameraViewMatrix, proj);
|
|
for (auto& npc : npcs) {
|
|
if (npc) npc->drawHealthBar(renderer, cameraViewMatrix, proj);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Location::setNavigationAreaAvailable(const std::string& areaName, bool available)
|
|
{
|
|
return navigation.setAreaAvailable(areaName, available);
|
|
}
|
|
|
|
void Location::resolveCharacterCollisions()
|
|
{
|
|
std::vector<Character*> characters;
|
|
characters.reserve(npcs.size() + 1);
|
|
|
|
if (player) {
|
|
characters.push_back(player.get());
|
|
}
|
|
for (auto& npc : npcs) {
|
|
if (npc) {
|
|
characters.push_back(npc.get());
|
|
}
|
|
}
|
|
|
|
if (characters.size() < 2) {
|
|
return;
|
|
}
|
|
|
|
static constexpr int kIterations = 3;
|
|
static constexpr float kMinSeparationEps = 1e-4f;
|
|
|
|
for (int iter = 0; iter < kIterations; ++iter) {
|
|
for (size_t i = 0; i < characters.size(); ++i) {
|
|
for (size_t j = i + 1; j < characters.size(); ++j) {
|
|
Character* a = characters[i];
|
|
Character* b = characters[j];
|
|
if (!a || !b) continue;
|
|
if (a->hp <= 0.f || b->hp <= 0.f) continue;
|
|
|
|
const float minDist = a->collisionRadius + b->collisionRadius;
|
|
if (minDist <= 0.f) continue;
|
|
|
|
const Eigen::Vector2f pa(a->position.x(), a->position.z());
|
|
const Eigen::Vector2f pb(b->position.x(), b->position.z());
|
|
const Eigen::Vector2f delta = pb - pa;
|
|
const float dist = delta.norm();
|
|
|
|
if (dist >= minDist) {
|
|
continue;
|
|
}
|
|
|
|
Eigen::Vector2f normal(1.f, 0.f);
|
|
if (dist > kMinSeparationEps) {
|
|
normal = delta / dist;
|
|
}
|
|
|
|
const float penetration = (minDist - dist);
|
|
const float push = penetration * 0.5f;
|
|
|
|
Eigen::Vector3f newA = a->position;
|
|
Eigen::Vector3f newB = b->position;
|
|
newA.x() -= normal.x() * push;
|
|
newA.z() -= normal.y() * push;
|
|
newA.y() = 0.f;
|
|
newB.x() += normal.x() * push;
|
|
newB.z() += normal.y() * push;
|
|
newB.y() = 0.f;
|
|
|
|
if (navigation.isReady()) {
|
|
const bool aOk = navigation.isWalkable(newA);
|
|
const bool bOk = navigation.isWalkable(newB);
|
|
|
|
if (aOk && bOk) {
|
|
a->position = newA;
|
|
b->position = newB;
|
|
}
|
|
else if (aOk && !bOk) {
|
|
a->position = newA;
|
|
}
|
|
else if (!aOk && bOk) {
|
|
b->position = newB;
|
|
}
|
|
}
|
|
else {
|
|
a->position = newA;
|
|
b->position = newB;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Location::updateDynamicReplans(int64_t deltaMs)
|
|
{
|
|
static constexpr float kMovedEps = 0.05f;
|
|
static constexpr float kReplanTriggerDist = 1.1f;
|
|
static constexpr int64_t kReplanCooldownMs = 300;
|
|
|
|
for (auto it = replanCooldownRemainingMs.begin(); it != replanCooldownRemainingMs.end();) {
|
|
it->second -= deltaMs;
|
|
if (it->second <= 0) {
|
|
it = replanCooldownRemainingMs.erase(it);
|
|
}
|
|
else {
|
|
++it;
|
|
}
|
|
}
|
|
|
|
std::vector<Character*> characters;
|
|
characters.reserve(npcs.size() + 1);
|
|
if (player) characters.push_back(player.get());
|
|
for (auto& npc : npcs) if (npc) characters.push_back(npc.get());
|
|
|
|
std::vector<Character*> movers;
|
|
movers.reserve(characters.size());
|
|
for (Character* c : characters) {
|
|
if (!c) continue;
|
|
auto it = lastCharacterPositions.find(c);
|
|
if (it == lastCharacterPositions.end()) {
|
|
lastCharacterPositions[c] = c->position;
|
|
continue;
|
|
}
|
|
|
|
const Eigen::Vector3f prev = it->second;
|
|
const float moved = (c->position - prev).norm();
|
|
it->second = c->position;
|
|
|
|
if (moved > kMovedEps) {
|
|
movers.push_back(c);
|
|
}
|
|
}
|
|
|
|
if (movers.empty()) {
|
|
return;
|
|
}
|
|
|
|
for (Character* mover : movers) {
|
|
if (!mover) continue;
|
|
|
|
for (Character* walker : characters) {
|
|
if (!walker || walker == mover) continue;
|
|
if (walker->hp <= 0.f) continue;
|
|
if (!walker->isMoving()) continue;
|
|
|
|
if (replanCooldownRemainingMs.find(walker) != replanCooldownRemainingMs.end()) {
|
|
continue;
|
|
}
|
|
|
|
const Eigen::Vector3f nextTarget = walker->getCurrentNavigationTarget();
|
|
const float distToSegment = distancePointToSegmentXZ(mover->position, walker->position, nextTarget);
|
|
if (distToSegment > kReplanTriggerDist) {
|
|
continue;
|
|
}
|
|
|
|
walker->forceReplan();
|
|
replanCooldownRemainingMs[walker] = kReplanCooldownMs;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Location::update(int64_t delta)
|
|
{
|
|
if (player) {
|
|
player->update(delta);
|
|
dialogueSystem.update(static_cast<int>(delta), player->position);
|
|
}
|
|
for (auto& npc : npcs)
|
|
{
|
|
npc->update(delta);
|
|
}
|
|
|
|
resolveCharacterCollisions();
|
|
updateDynamicReplans(delta);
|
|
|
|
// Check if player reached target interactive object
|
|
if (targetInteractiveObject && player) {
|
|
float distToObject = (player->position - targetInteractiveObject->position).norm();
|
|
|
|
// If player is close enough to pick up the item
|
|
if (distToObject <= targetInteractiveObject->interactionRadius + 1.0f) {
|
|
std::cout << "[PICKUP] Player reached object! Distance: " << distToObject << std::endl;
|
|
std::cout << "[PICKUP] Calling Lua callback for: " << targetInteractiveObject->id << std::endl;
|
|
|
|
// Call custom activate function if specified, otherwise use fallback
|
|
try {
|
|
if (!targetInteractiveObject->activateFunctionName.empty()) {
|
|
std::cout << "[PICKUP] Using custom function: " << targetInteractiveObject->activateFunctionName << std::endl;
|
|
scriptEngine.callActivateFunction(targetInteractiveObject->activateFunctionName);
|
|
}
|
|
else {
|
|
std::cout << "[PICKUP] Using fallback callback" << std::endl;
|
|
scriptEngine.callItemPickupCallback(targetInteractiveObject->id);
|
|
}
|
|
}
|
|
catch (const std::exception& e) {
|
|
std::cerr << "[PICKUP] Error calling function: " << e.what() << std::endl;
|
|
}
|
|
|
|
targetInteractiveObject = nullptr;
|
|
}
|
|
}
|
|
|
|
// Check if player reached target NPC for interaction.
|
|
if (targetInteractNpc && targetInteractNpcIndex >= 0 && player) {
|
|
float distToNpc = (player->position - targetInteractNpc->position).norm();
|
|
if (distToNpc <= NPC_TALK_DISTANCE) {
|
|
std::cout << "[NPC] Player reached NPC index " << targetInteractNpcIndex
|
|
<< " (distance " << distToNpc << "); firing on_npc_interact" << std::endl;
|
|
// Stop the player at the talk distance and have the NPC turn to face them.
|
|
player->setTarget(player->position);
|
|
targetInteractNpc->faceTarget = player.get();
|
|
try {
|
|
scriptEngine.callNpcInteractCallback(targetInteractNpcIndex);
|
|
}
|
|
catch (const std::exception& e) {
|
|
std::cerr << "[NPC] callback error: " << e.what() << std::endl;
|
|
}
|
|
targetInteractNpc = nullptr;
|
|
targetInteractNpcIndex = -1;
|
|
}
|
|
}
|
|
|
|
// Drive teleport spark animation regardless of whether the zone is armed.
|
|
if (teleportSparks) {
|
|
teleportSparks->update(static_cast<float>(delta));
|
|
}
|
|
|
|
// Teleport zone: rising-edge trigger only. The destination location's
|
|
// callback is responsible for placing the player back inside its own
|
|
// zone and setting playerInTeleportZone = true so we don't bounce.
|
|
if (player && onTeleport && teleportRadius > 0.0f) {
|
|
float distToZone = (player->position - teleportPosition).norm();
|
|
if (distToZone <= teleportRadius) {
|
|
if (!playerInTeleportZone) {
|
|
playerInTeleportZone = true;
|
|
std::cout << "[TELEPORT] Player entered teleport zone" << std::endl;
|
|
onTeleport();
|
|
return; // active location may have changed
|
|
}
|
|
}
|
|
else {
|
|
playerInTeleportZone = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Location::handleDown(int64_t fingerId, int eventX, int eventY, int mx, int my)
|
|
{
|
|
// Calculate ray for picking
|
|
if (dialogueSystem.blocksGameplayInput()) {
|
|
dialogueSystem.handlePointerReleased(static_cast<float>(mx), Environment::projectionHeight - static_cast<float>(my));
|
|
return;
|
|
}
|
|
|
|
player->attackTarget = nullptr;
|
|
|
|
// Unproject click to ground plane (y=0) for Viola's walk target
|
|
float ndcX = 2.0f * eventX / Environment::width - 1.0f;
|
|
float ndcY = 1.0f - 2.0f * eventY / Environment::height;
|
|
float aspect = (float)Environment::width / (float)Environment::height;
|
|
float tanHalfFov = tan(CAMERA_FOV_Y * 0.5f);
|
|
|
|
float cosAzim = cos(cameraAzimuth), sinAzim = sin(cameraAzimuth);
|
|
float cosIncl = cos(cameraInclination), sinIncl = sin(cameraInclination);
|
|
|
|
Eigen::Vector3f camRight(cosAzim, 0.f, sinAzim);
|
|
Eigen::Vector3f camForward(sinAzim * cosIncl, -sinIncl, -cosAzim * cosIncl);
|
|
Eigen::Vector3f camUp(sinAzim * sinIncl, cosIncl, -cosAzim * sinIncl);
|
|
const Eigen::Vector3f& playerPos = player ? player->position : Eigen::Vector3f::Zero();
|
|
Eigen::Vector3f camPos = playerPos + Eigen::Vector3f(-sinAzim * cosIncl, sinIncl, cosAzim * cosIncl) * Environment::zoom;
|
|
|
|
Eigen::Vector3f rayDir = (camForward + camRight * (ndcX * aspect * tanHalfFov) + camUp * (ndcY * tanHalfFov)).normalized();
|
|
|
|
std::cout << "[CLICK] Camera position: (" << camPos.x() << ", " << camPos.y() << ", " << camPos.z() << ")" << std::endl;
|
|
std::cout << "[CLICK] Ray direction: (" << rayDir.x() << ", " << rayDir.y() << ", " << rayDir.z() << ")" << std::endl;
|
|
|
|
// First check if we clicked on interactive object
|
|
InteractiveObject* clickedObject = raycastInteractiveObjects(camPos, rayDir);
|
|
if (clickedObject && player && clickedObject->isActive) {
|
|
std::cout << "[CLICK] *** SUCCESS: Clicked on interactive object: " << clickedObject->name << " ***" << std::endl;
|
|
std::cout << "[CLICK] Object position: (" << clickedObject->position.x() << ", "
|
|
<< clickedObject->position.y() << ", " << clickedObject->position.z() << ")" << std::endl;
|
|
std::cout << "[CLICK] Player position: (" << player->position.x() << ", "
|
|
<< player->position.y() << ", " << player->position.z() << ")" << std::endl;
|
|
|
|
targetInteractiveObject = clickedObject;
|
|
targetInteractNpc = nullptr;
|
|
targetInteractNpcIndex = -1;
|
|
player->setTarget(clickedObject->position);
|
|
player->attackTarget = nullptr;
|
|
std::cout << "[CLICK] Player moving to object..." << std::endl;
|
|
}
|
|
else {
|
|
|
|
// Check if we clicked on an NPC
|
|
Character* clickedNpc = raycastNpcs(camPos, rayDir);
|
|
if (clickedNpc && player) {
|
|
float distance = (player->position - clickedNpc->position).norm();
|
|
int npcIndex = -1;
|
|
for (size_t i = 0; i < npcs.size(); ++i) {
|
|
if (npcs[i].get() == clickedNpc) {
|
|
npcIndex = static_cast<int>(i);
|
|
break;
|
|
}
|
|
}
|
|
if (npcIndex != -1 && clickedNpc->hp > 0) {
|
|
targetInteractiveObject = nullptr;
|
|
|
|
if (clickedNpc->canAttack) {
|
|
// Hostile NPC: combat logic walks the player in via attackTarget;
|
|
// don't queue a Lua interaction during a fight.
|
|
player->attackTarget = clickedNpc;
|
|
targetInteractNpc = nullptr;
|
|
targetInteractNpcIndex = -1;
|
|
|
|
if (distance <= clickedNpc->interactionRadius) {
|
|
std::cout << "[CLICK] Hostile NPC " << npcIndex << " in range; firing on_npc_interact" << std::endl;
|
|
scriptEngine.callNpcInteractCallback(npcIndex);
|
|
}
|
|
}
|
|
else {
|
|
// Peaceful NPC: walk to them, fire on_npc_interact when within talk distance.
|
|
player->attackTarget = nullptr;
|
|
|
|
if (distance <= NPC_TALK_DISTANCE) {
|
|
// Already in talk range — fire immediately, stop, and face the player.
|
|
std::cout << "[CLICK] *** SUCCESS: Clicked on NPC index: " << npcIndex << " (in range) ***" << std::endl;
|
|
player->setTarget(player->position);
|
|
clickedNpc->faceTarget = player.get();
|
|
scriptEngine.callNpcInteractCallback(npcIndex);
|
|
targetInteractNpc = nullptr;
|
|
targetInteractNpcIndex = -1;
|
|
}
|
|
else {
|
|
std::cout << "[CLICK] NPC " << npcIndex << " out of talk range (distance " << distance
|
|
<< " > " << NPC_TALK_DISTANCE << "); walking to NPC..." << std::endl;
|
|
player->setTarget(clickedNpc->position);
|
|
targetInteractNpc = clickedNpc;
|
|
targetInteractNpcIndex = npcIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
else if (rayDir.y() < -0.001f && player) {
|
|
// Otherwise, unproject click to ground plane for Viola's walk target
|
|
float t = -camPos.y() / rayDir.y();
|
|
Eigen::Vector3f hit = camPos + rayDir * t;
|
|
std::cout << "[CLICK] Clicked on ground at: (" << hit.x() << ", " << hit.z() << ")" << std::endl;
|
|
|
|
player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z()));
|
|
player->attackTarget = nullptr;
|
|
targetInteractNpc = nullptr;
|
|
targetInteractNpcIndex = -1;
|
|
}
|
|
else {
|
|
std::cout << "[CLICK] No valid target found" << std::endl;
|
|
}
|
|
}
|
|
std::cout << "========================================\n" << std::endl;
|
|
}
|
|
void Location::handleUp(int64_t fingerId, int mx, int my)
|
|
{
|
|
if (dialogueSystem.blocksGameplayInput()) {
|
|
dialogueSystem.handlePointerReleased(static_cast<float>(mx), Environment::projectionHeight - static_cast<float>(my));
|
|
return;
|
|
}
|
|
|
|
}
|
|
void Location::handleMotion(int64_t fingerId, int eventX, int eventY, int mx, int my)
|
|
{
|
|
if (dialogueSystem.blocksGameplayInput()) {
|
|
dialogueSystem.handlePointerMoved(
|
|
static_cast<float>(mx),
|
|
Environment::projectionHeight - static_cast<float>(my)
|
|
);
|
|
}
|
|
|
|
if (cameraDragging) {
|
|
int dx = eventX - lastMouseX;
|
|
int dy = eventY - lastMouseY;
|
|
lastMouseX = eventX;
|
|
lastMouseY = eventY;
|
|
|
|
const float sensitivity = 0.005f;
|
|
cameraAzimuth += dx * sensitivity;
|
|
cameraInclination += dy * sensitivity;
|
|
|
|
const float minInclination = M_PI * 30.f / 180.f;
|
|
const float maxInclination = M_PI * 0.5f;
|
|
cameraInclination = max(minInclination, min(maxInclination, cameraInclination));
|
|
}
|
|
}
|
|
|
|
bool Location::requestDialogueStart(const std::string& dialogueId)
|
|
{
|
|
return dialogueSystem.startDialogue(dialogueId);
|
|
}
|
|
|
|
void Location::setDialogueFlag(const std::string& flag, int value)
|
|
{
|
|
dialogueSystem.setFlag(flag, value);
|
|
}
|
|
|
|
int Location::getDialogueFlag(const std::string& flag) const
|
|
{
|
|
return dialogueSystem.getFlag(flag);
|
|
}
|
|
|
|
|
|
} // namespace ZL
|