1832 lines
65 KiB
C++
1832 lines
65 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"
|
|
|
|
|
|
|
|
namespace ZL
|
|
{
|
|
extern float x;
|
|
extern const char* CONST_ZIP_FILE;
|
|
|
|
static constexpr float CAMERA_FOV_Y = 1.0f / 1.5f;
|
|
|
|
Location::Location(Renderer& iRenderer, Inventory& iInventory, const std::string& locId)
|
|
: renderer(iRenderer)
|
|
, inventory(iInventory)
|
|
, locationId(locId)
|
|
, roofObjectKey("")
|
|
, azsRoofObjectKey("")
|
|
{
|
|
|
|
}
|
|
|
|
void Location::setup()
|
|
{
|
|
|
|
|
|
|
|
tileMesh.data = LoadFromTextFile02("resources/e/land/land003.txt", CONST_ZIP_FILE);
|
|
tileMesh.RefreshVBO();
|
|
roadTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/land/Gemini_Generated_Image_eq858beq858beq85.png", CONST_ZIP_FILE));
|
|
grassTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/land/Gemini_Generated_Image_321v9q321v9q321v.png", CONST_ZIP_FILE));
|
|
|
|
auto playerTexture0 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/male_packed0_diffuse.png", CONST_ZIP_FILE));
|
|
auto playerTexture1 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/male_packed1_diffuse.png", CONST_ZIP_FILE));
|
|
auto playerTexture2 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/male_packed2_diffuse.png", CONST_ZIP_FILE));
|
|
|
|
player = std::make_unique<Character>();
|
|
player->loadBinaryAnimation(AnimationState::STAND, "resources/e/gg_stand_idle003.anim2", CONST_ZIP_FILE);
|
|
player->loadBinaryAnimation(AnimationState::WALK, "resources/e/gg_run003.anim2", CONST_ZIP_FILE);
|
|
player->setTexture("Body", playerTexture0);
|
|
player->setTexture("Bottoms", playerTexture1);
|
|
player->setTexture("Eyelashes", playerTexture0);
|
|
player->setTexture("Eyes", playerTexture0);
|
|
player->setTexture("Eyewear", playerTexture0);
|
|
player->setTexture("Gloves", playerTexture1);
|
|
player->setTexture("Hair", playerTexture0);
|
|
player->setTexture("Shoes", playerTexture1);
|
|
player->setTexture("Tops", playerTexture1);
|
|
player->walkSpeed = 3.0f;
|
|
player->rotationSpeed = 8.0f;
|
|
player->modelScale = 0.01f;
|
|
player->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY()));
|
|
player->canAttack = true;
|
|
player->isPlayer = true;
|
|
//player->position = { 9.43527, 0, 0.952688 };
|
|
//player->setTarget(player->position);
|
|
|
|
|
|
|
|
auto girlfriendTexture0 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/female_packed0_diffuse.png", CONST_ZIP_FILE));
|
|
auto girlfriendTexture1 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/female_packed1_diffuse.png", CONST_ZIP_FILE));
|
|
auto girlfriendTexture2 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/female_packed2_diffuse.png", CONST_ZIP_FILE));
|
|
auto girlfriendTexture3 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/female_packed3_diffuse.png", CONST_ZIP_FILE));
|
|
|
|
girlfriend = std::make_unique<Character>();
|
|
girlfriend->loadBinaryAnimation(AnimationState::STAND, "resources/e/woman_stand_idle003.anim2", CONST_ZIP_FILE);
|
|
girlfriend->loadBinaryAnimation(AnimationState::WALK, "resources/e/woman_run003.anim2", CONST_ZIP_FILE);
|
|
girlfriend->setTexture("Body", girlfriendTexture0);
|
|
girlfriend->setTexture("Bottoms", girlfriendTexture3);
|
|
girlfriend->setTexture("Eyelashes", girlfriendTexture0);
|
|
girlfriend->setTexture("Eyes", girlfriendTexture0);
|
|
girlfriend->setTexture("Hair", girlfriendTexture2);
|
|
girlfriend->setTexture("Shoes", girlfriendTexture1);
|
|
girlfriend->setTexture("Tops", girlfriendTexture2);
|
|
girlfriend->walkSpeed = 3.0f;
|
|
girlfriend->rotationSpeed = 8.0f;
|
|
girlfriend->modelScale = 0.01f;
|
|
girlfriend->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY()));
|
|
//girlfriend->position = Vector3f{ -10, -0, -10 };
|
|
//girlfriend->setTarget(girlfriend->position);
|
|
|
|
auto salesPersonTexture0 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/Salesperson_packed0_diffuse.png", CONST_ZIP_FILE));
|
|
auto salesPersonTexture1 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/Salesperson_packed1_diffuse.png", CONST_ZIP_FILE));
|
|
auto salesPersonTexture2 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/Salesperson_packed2_diffuse.png", CONST_ZIP_FILE));
|
|
|
|
salesperson = std::make_unique<Character>();
|
|
salesperson->loadBinaryAnimation(AnimationState::STAND, "resources/e/salesperson_stand_idle003.anim2", CONST_ZIP_FILE);
|
|
salesperson->loadBinaryAnimation(AnimationState::WALK, "resources/e/salesperson_stand_idle003.anim2", CONST_ZIP_FILE);
|
|
salesperson->setTexture("Body", salesPersonTexture0);
|
|
salesperson->setTexture("Bottoms", salesPersonTexture1);
|
|
salesperson->setTexture("Eyelashes", salesPersonTexture0);
|
|
salesperson->setTexture("Eyes", salesPersonTexture0);
|
|
salesperson->setTexture("Hair", salesPersonTexture1);
|
|
salesperson->setTexture("Shoes", salesPersonTexture1);
|
|
salesperson->setTexture("Tops", salesPersonTexture1);
|
|
salesperson->walkSpeed = 3.0f;
|
|
salesperson->rotationSpeed = 8.0f;
|
|
salesperson->modelScale = 0.01f;
|
|
salesperson->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(0, Eigen::Vector3f::UnitY()));
|
|
salesperson->position = Vector3f{ -8.31099, -0, -3.56868 };
|
|
salesperson->setTarget(salesperson->position);
|
|
|
|
auto policeTexture0 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/police_packed0_diffuse.png", CONST_ZIP_FILE));
|
|
auto policeTexture1 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/police_packed1_diffuse.png", CONST_ZIP_FILE));
|
|
|
|
police = std::make_unique<Character>();
|
|
police->loadBinaryAnimation(AnimationState::STAND, "resources/e/police_stand_idle003.anim2", CONST_ZIP_FILE);
|
|
police->loadBinaryAnimation(AnimationState::WALK, "resources/e/police_stand_idle003.anim2", CONST_ZIP_FILE);
|
|
police->setTexture("Body", policeTexture0);
|
|
police->setTexture("Bottoms", policeTexture1);
|
|
police->setTexture("Eyelashes", policeTexture0);
|
|
police->setTexture("Eyes", policeTexture0);
|
|
police->setTexture("Hair", policeTexture1);
|
|
police->setTexture("Shoes", policeTexture1);
|
|
police->setTexture("Tops", policeTexture1);
|
|
police->walkSpeed = 3.0f;
|
|
police->rotationSpeed = 8.0f;
|
|
police->modelScale = 0.01f;
|
|
police->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY()));
|
|
police->position = Vector3f{ 1000, -0, 10 };
|
|
police->setTarget(police->position);
|
|
|
|
auto banditTexture0 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/bandit_packed0_diffuse.png", CONST_ZIP_FILE));
|
|
auto banditTexture1 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/bandit_packed1_diffuse.png", CONST_ZIP_FILE));
|
|
auto banditTexture2 = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/bandit_packed2_diffuse.png", CONST_ZIP_FILE));
|
|
|
|
bandit = std::make_unique<Character>();
|
|
bandit->loadBinaryAnimation(AnimationState::STAND, "resources/e/bandit_stand_idle003.anim2", CONST_ZIP_FILE);
|
|
bandit->loadBinaryAnimation(AnimationState::WALK, "resources/e/bandit_stand_idle003.anim2", CONST_ZIP_FILE);
|
|
bandit->setTexture("Body", banditTexture0);
|
|
bandit->setTexture("Bottoms", banditTexture1);
|
|
bandit->setTexture("Eyelashes", banditTexture0);
|
|
bandit->setTexture("Eyes", policeTexture0);
|
|
bandit->setTexture("Hair", banditTexture1);
|
|
bandit->setTexture("Shoes", banditTexture2);
|
|
bandit->setTexture("Tops", banditTexture2);
|
|
bandit->walkSpeed = 3.0f;
|
|
bandit->rotationSpeed = 8.0f;
|
|
bandit->modelScale = 0.01f;
|
|
bandit->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY()));
|
|
bandit->position = Vector3f{ 6, 0, 6 };
|
|
bandit->setTarget(bandit->position);
|
|
|
|
//girlfriend->canAttack = true;
|
|
//player->isPlayer = true;
|
|
struct ModelAsset {
|
|
const char* meshPath;
|
|
const char* texPath;
|
|
Eigen::Vector3f position;
|
|
float scale;
|
|
};
|
|
|
|
if (locationId == "forest")
|
|
{
|
|
std::cout << "[LOCATION] Setting up FOREST location (custom models only)" << std::endl;
|
|
|
|
carPosition ={ 7, 0, -7 + 200 };
|
|
npcCar.position = Vector3f(9, 0, -335) + Vector3f(1000,0,0);
|
|
|
|
girlfriend->position = Vector3f{ 5, 0, 0.9 + 200 };
|
|
girlfriend->setTarget(girlfriend->position);
|
|
|
|
player->position = { 9.5, 0, 0.95 + 200 };
|
|
player->setTarget(player->position);
|
|
|
|
|
|
|
|
/* gameObjects.clear();
|
|
interactiveObjects.clear();
|
|
npcs.clear();
|
|
debugNavMeshes.clear();
|
|
|
|
roomMesh.data = VertexDataStruct();
|
|
//roomMesh.data = LoadFromTextFile02("resources/w/road001.txt", CONST_ZIP_FILE);
|
|
//roomMesh.RefreshVBO();
|
|
roomTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/w/room005.png", CONST_ZIP_FILE));
|
|
*/
|
|
std::vector<ModelAsset> models = {
|
|
{"resources/out/AzsBase001.txt", "resources/ghost_avatar.png", {0.0f, 2.0f, 0.0f}, 2.0f},
|
|
{"resources/out/AzsRoof001.txt", "resources/ghost_avatar.png", {0.0f, 2.0f, 0.0f}, 2.0f},
|
|
{"resources/out/floor001.txt", "resources/ghost_avatar.png", {0.0f, -1.97f, 0.0f}, 2.0f},
|
|
{"resources/out/roof001.txt", "resources/ghost_avatar.png", {0.0f, 2.0f, 0.0f}, 2.0f},
|
|
{"resources/out/Walls001.txt", "resources/ghost_avatar.png", {0.0f, 2.0f, 0.0f}, 2.0f},
|
|
{"resources/out/Price001.txt", "resources/w/blue.png", {0.0f, 1.0f, 5.0f}, 2.0f}
|
|
};
|
|
|
|
for (size_t i = 0; i < models.size(); ++i) {
|
|
const auto& m = models[i];
|
|
LoadedGameObject obj;
|
|
obj.mesh.data = LoadFromTextFile02(m.meshPath, CONST_ZIP_FILE);
|
|
obj.mesh.data.Scale(m.scale);
|
|
obj.mesh.data.Move(m.position);
|
|
obj.mesh.RefreshVBO();
|
|
obj.texture = std::make_shared<Texture>(CreateTextureDataFromPng(m.texPath, CONST_ZIP_FILE));
|
|
|
|
if (std::string(m.meshPath) == "resources/out/AzsRoof001.txt") {
|
|
gameObjects["azsRoof"] = std::move(obj);
|
|
azsRoofObjectKey = "azsRoof";
|
|
azsRoofHideZones.clear();
|
|
azsRoofHideZones.emplace_back(Eigen::Vector2f(-7.0f, -14.0f), Eigen::Vector2f(0.1f, -8.5f));
|
|
}
|
|
else if (std::string(m.meshPath) == "resources/out/roof001.txt") {
|
|
gameObjects["roof"] = std::move(obj);
|
|
roofObjectKey = "roof";
|
|
roofHideZones.clear();
|
|
roofHideZones.emplace_back(Eigen::Vector2f(-9.3f, -5.0f), Eigen::Vector2f(2.5f, 1.7f));
|
|
}
|
|
else {
|
|
gameObjects["forest_model_" + std::to_string(i)] = std::move(obj);
|
|
}
|
|
}
|
|
|
|
|
|
shadowMap = std::make_unique<ShadowMap>(2048, 40.0f, 0.1f, 100.0f);
|
|
shadowMap->setLightDirection(Eigen::Vector3f(-0.5f, -1.0f, -0.3f));
|
|
|
|
setupNavigation();
|
|
|
|
scriptEngine.init(this, &inventory);
|
|
dialogueSystem.init(renderer, CONST_ZIP_FILE);
|
|
dialogueSystem.loadDatabase("resources/dialogue/sample_dialogues.json");
|
|
|
|
std::cout << "[FOREST] Setup complete, loaded " << gameObjects.size() << " custom models" << std::endl;
|
|
}
|
|
else // default
|
|
{
|
|
carPosition = { 5.4005, 0, -3.811283 };
|
|
npcCar.position = Vector3f(9, 0, -100);
|
|
|
|
player->position = { 9.43527, 0, 0.952688 };
|
|
player->setTarget(player->position);
|
|
|
|
girlfriend->position = Vector3f{ -10, -0, -10 };
|
|
girlfriend->setTarget(girlfriend->position);
|
|
|
|
|
|
|
|
std::vector<ModelAsset> models = {
|
|
{"resources/out/main/birch-1.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/birch-11.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/birch-2.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/birch-22.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/birch-3.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/birch-33.txt", "resources/w/blue.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/birch-4.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/birch-44.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/birch-5.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/birch-55.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/birch-6.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/birch-66.txt", "resources/w/blue.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube1.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube2.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube3.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube4.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube5.txt", "resources/w/blue.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube6.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube7.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube8.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube9.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube10.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube11.txt", "resources/w/blue.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube12.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube13.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube14.txt", "resources/ghost_avatar.png", {0.0f, 0.0f, 0.0f}, 2.0f},
|
|
{"resources/out/main/cube15.txt", "resources/w/blue.png", {0.0f, 0.0f, 0.0f}, 2.0f}
|
|
};
|
|
|
|
for (size_t i = 0; i < models.size(); ++i) {
|
|
const auto& m = models[i];
|
|
LoadedGameObject obj;
|
|
obj.mesh.data = LoadFromTextFile02(m.meshPath, CONST_ZIP_FILE);
|
|
obj.mesh.data.Scale(m.scale);
|
|
obj.mesh.data.Move(m.position);
|
|
obj.mesh.RefreshVBO();
|
|
obj.texture = std::make_shared<Texture>(CreateTextureDataFromPng(m.texPath, CONST_ZIP_FILE));
|
|
gameObjects["default_model_" + std::to_string(i)] = std::move(obj);
|
|
}
|
|
|
|
|
|
/*std::cout << "[LOCATION] Setting up DEFAULT location" << std::endl;
|
|
std::string roomTexPath = "resources/w/room005.png";
|
|
std::string roomMeshPath = "resources/w/room001.txt";
|
|
std::string gameObjectsPath = "resources/config2/gameobjects.json";
|
|
std::string interactiveObjectsPath = "resources/config2/gameobjects.json";
|
|
std::string npcsPath = "resources/config2/npcs.json";
|
|
|
|
roomTexture = std::make_unique<Texture>(CreateTextureDataFromPng(roomTexPath.c_str(), CONST_ZIP_FILE));
|
|
roomMesh.data = LoadFromTextFile02(roomMeshPath.c_str(), CONST_ZIP_FILE);
|
|
roomMesh.data.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
roomMesh.RefreshVBO();
|
|
|
|
gameObjects = GameObjectLoader::loadAndCreateGameObjects(gameObjectsPath, renderer, CONST_ZIP_FILE);
|
|
interactiveObjects = GameObjectLoader::loadAndCreateInteractiveObjects(interactiveObjectsPath, renderer, CONST_ZIP_FILE);
|
|
npcs = GameObjectLoader::loadAndCreateNpcs(npcsPath, CONST_ZIP_FILE);
|
|
|
|
auto playerTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/gg/IMG_20260413_182354_992.png", CONST_ZIP_FILE));
|
|
player = std::make_unique<Character>();
|
|
player->loadAnimation(AnimationState::STAND, "resources/e/woman_stand_idle003.txt", CONST_ZIP_FILE);
|
|
player->loadAnimation(AnimationState::WALK, "resources/e/woman_stand_idle003.txt", CONST_ZIP_FILE);
|
|
player->setTexture("Body", playerTexture1);
|
|
player->setTexture("Bottoms", playerTexture4);
|
|
player->setTexture("Eyelashes", playerTexture1);
|
|
player->setTexture("Eyes", playerTexture1);
|
|
player->setTexture("Hair", playerTexture3);
|
|
player->setTexture("Shoes", playerTexture1);
|
|
player->setTexture("Tops", playerTexture3);
|
|
player->walkSpeed = 3.0f;
|
|
player->rotationSpeed = 8.0f;
|
|
player->modelScale = 0.01f;
|
|
player->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY()));
|
|
player->canAttack = true;
|
|
player->isPlayer = true;
|
|
*/
|
|
/*
|
|
auto ghostTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/w/ghost_skin001.png", CONST_ZIP_FILE));
|
|
auto npc02 = std::make_unique<Character>();
|
|
npc02->loadBinaryAnimation(AnimationState::STAND, "resources/w/default_float001.anim", CONST_ZIP_FILE);
|
|
npc02->loadBinaryAnimation(AnimationState::WALK, "resources/w/default_float001.anim", CONST_ZIP_FILE);
|
|
npc02->loadBinaryAnimation(AnimationState::ACTION_IDLE, "resources/w/float_attack003_cut.anim", CONST_ZIP_FILE);
|
|
npc02->loadBinaryAnimation(AnimationState::ACTION_ATTACK, "resources/w/float_attack003.anim", CONST_ZIP_FILE);
|
|
npc02->loadBinaryAnimation(AnimationState::STAND_TO_ACTION, "resources/w/default_float001_cut.anim", CONST_ZIP_FILE);
|
|
npc02->loadBinaryAnimation(AnimationState::ACTION_TO_STAND, "resources/w/default_float001_cut.anim", CONST_ZIP_FILE);
|
|
npc02->setTexture(ghostTexture);
|
|
npc02->walkSpeed = 1.5f;
|
|
npc02->rotationSpeed = 8.0f;
|
|
npc02->modelScale = 0.01f;
|
|
npc02->modelCorrectionRotation = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY()));
|
|
npc02->position = Eigen::Vector3f(0.f, 0.f, -20.f);
|
|
npc02->setTarget(npc02->position);
|
|
npc02->canAttack = true;
|
|
npc02->attackTarget = player.get();
|
|
npcs.push_back(std::move(npc02));
|
|
*/
|
|
shadowMap = std::make_unique<ShadowMap>(2048, 40.0f, 0.1f, 100.0f);
|
|
shadowMap->setLightDirection(Eigen::Vector3f(-0.5f, -1.0f, -0.3f));
|
|
|
|
//setupNavigation();
|
|
scriptEngine.init(this, &inventory);
|
|
dialogueSystem.init(renderer, CONST_ZIP_FILE);
|
|
dialogueSystem.loadDatabase("resources/dialogue/sample_dialogues.json");
|
|
}
|
|
|
|
carTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/e/car002.png", CONST_ZIP_FILE));
|
|
carMesh.data = LoadFromTextFile02("resources/e/car001.txt", CONST_ZIP_FILE);
|
|
carMesh.data.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI * 0.5, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
carMesh.RefreshVBO();
|
|
|
|
carWheelTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/e/Wheel_base002.png", CONST_ZIP_FILE));
|
|
carWheelMesh.data = LoadFromTextFile02("resources/e/car_wheel001.txt", CONST_ZIP_FILE);
|
|
carWheelMesh.data.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI * 0.5, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
carWheelMesh.RefreshVBO();
|
|
|
|
npcCar.texture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/e/car_black001.png", CONST_ZIP_FILE));
|
|
//npcCar.position = carPosition + Eigen::Vector3f(0, 0.f, 14.f);//Eigen::Vector3f(-12.f, 0.f, 8.f);
|
|
npcCar.rotation = 0.f;
|
|
npcCar.mode = NpcCar::Mode::NONE;
|
|
/*npcCar.mode = NpcCar::Mode::FOLLOW_WAYPOINTS;
|
|
npcCar.waypoints = {
|
|
Eigen::Vector3f(-12.f, 0.f, 8.f),
|
|
Eigen::Vector3f( 12.f, 0.f, 8.f),
|
|
Eigen::Vector3f( 12.f, 0.f, -8.f),
|
|
Eigen::Vector3f(-12.f, 0.f, -8.f),
|
|
};*/
|
|
//npcCar.currentWaypoint = 0;
|
|
}
|
|
|
|
void Location::setupNavigation()
|
|
{
|
|
|
|
std::vector<PathFinder::ObstacleMesh> obstacles;
|
|
|
|
if (locationId == "forest") {
|
|
navigation.build(obstacles, "resources/config2/navigation2.json", CONST_ZIP_FILE);
|
|
}
|
|
else {
|
|
navigation.build(obstacles, "resources/config2/navigation.json", CONST_ZIP_FILE);
|
|
}
|
|
//#ifdef SHOW_PATH
|
|
buildDebugNavMeshes();
|
|
buildDebugForbiddenMeshes();
|
|
//#endif
|
|
|
|
|
|
auto planner = [this](const Eigen::Vector3f& start, const Eigen::Vector3f& end) {
|
|
return navigation.findPath(start, end);
|
|
};
|
|
|
|
if (player) {
|
|
player->setPathPlanner(planner);
|
|
}
|
|
|
|
for (auto& npc : npcs) {
|
|
if (npc) {
|
|
npc->setPathPlanner(planner);
|
|
}
|
|
}
|
|
}
|
|
|
|
//#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();
|
|
}
|
|
|
|
void Location::buildDebugForbiddenMeshes()
|
|
{
|
|
debugForbiddenMeshes.clear();
|
|
const auto& areas = navigation.getAreas();
|
|
float y = navigation.getFloorY() + 0.2f;
|
|
Eigen::Vector3f blue(0.0f, 0.0f, 1.0f);
|
|
|
|
for (const auto& area : areas) {
|
|
if (area.forbidden && area.polygon.size() >= 3) {
|
|
VertexRenderStruct mesh;
|
|
mesh.data = CreatePolygonFloor(area.polygon, y, blue);
|
|
mesh.RefreshVBO();
|
|
debugForbiddenMeshes.push_back(std::move(mesh));
|
|
}
|
|
}
|
|
}
|
|
|
|
void Location::drawDebugForbidden()
|
|
{
|
|
if (debugForbiddenMeshes.empty()) return;
|
|
renderer.shaderManager.PushShader("defaultColor");
|
|
renderer.SetMatrix();
|
|
for (const auto& mesh : debugForbiddenMeshes) {
|
|
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) {
|
|
Character* closestNpc = nullptr;
|
|
float closestDist = maxDistance;
|
|
|
|
for (auto& npc : npcs) {
|
|
Eigen::Vector3f toNpc = npc->position - rayOrigin;
|
|
float distAlongRay = toNpc.dot(rayDir);
|
|
if (distAlongRay < 0.1f) continue;
|
|
|
|
Eigen::Vector3f closestPoint = rayOrigin + rayDir * distAlongRay;
|
|
float distToNpc = (closestPoint - npc->position).norm();
|
|
|
|
float radius = npc->modelScale * 50.0f;
|
|
|
|
if (distToNpc <= radius && distAlongRay < closestDist) {
|
|
closestDist = distAlongRay;
|
|
closestNpc = npc.get();
|
|
}
|
|
}
|
|
return closestNpc;
|
|
}
|
|
|
|
|
|
void Location::drawGame()
|
|
{
|
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
|
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
|
|
|
|
|
|
renderer.shaderManager.PushShader(defaultShaderName);
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
|
|
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());
|
|
Eigen::Vector3f camTarget = inCar ? carPosition : (player ? player->position : Eigen::Vector3f::Zero());
|
|
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
|
renderer.TranslateMatrix({ 0, -1.3f, 0 });
|
|
|
|
for (int i = -7; i <= 7; i++)
|
|
{
|
|
for (int j = -7; j <= 7; j++)
|
|
{
|
|
renderer.PushMatrix();
|
|
renderer.TranslateMatrix({ i * 100.0f, 0, j * 100.0f });
|
|
if (i == 0)
|
|
{
|
|
glBindTexture(GL_TEXTURE_2D, roadTexture->getTexID());
|
|
}
|
|
else
|
|
{
|
|
glBindTexture(GL_TEXTURE_2D, grassTexture->getTexID());
|
|
}
|
|
renderer.DrawVertexRenderStruct(tileMesh);
|
|
renderer.PopMatrix();
|
|
}
|
|
}
|
|
|
|
if (roomMesh.data.PositionData.size() > 0)
|
|
{
|
|
glBindTexture(GL_TEXTURE_2D, roomTexture->getTexID());
|
|
renderer.DrawVertexRenderStruct(roomMesh);
|
|
}
|
|
|
|
for (auto& [name, gameObj] : gameObjects) {
|
|
if (name == roofObjectKey && !roofVisible) {
|
|
continue;
|
|
}
|
|
if (name == azsRoofObjectKey && !azsRoofVisible) {
|
|
continue;
|
|
}
|
|
glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID());
|
|
renderer.DrawVertexRenderStruct(gameObj.mesh);
|
|
}
|
|
|
|
for (auto& intObj : interactiveObjects) {
|
|
if (intObj.isActive) {
|
|
intObj.draw(renderer);
|
|
}
|
|
}
|
|
|
|
renderer.PushMatrix();
|
|
//renderer.LoadIdentity();
|
|
renderer.TranslateMatrix(carPosition);
|
|
renderer.TranslateMatrix({ 0, 0.7, 0 });
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(carRotation, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
glBindTexture(GL_TEXTURE_2D, carTexture->getTexID());
|
|
renderer.DrawVertexRenderStruct(carMesh);
|
|
|
|
if (carWheelTexture) {
|
|
constexpr float track_width = 1.28f;
|
|
constexpr float wheel_base = 2.25f;
|
|
constexpr float shift = 0.6f;
|
|
const Eigen::Vector3f wheelPositions[4] = {
|
|
Eigen::Vector3f( track_width, 0.f - 0.21, -(wheel_base + shift)+1.25), // front right
|
|
Eigen::Vector3f(-track_width, 0.f - 0.21, -(wheel_base + shift)+1.25), // front left
|
|
Eigen::Vector3f( track_width, 0.f - 0.21, (wheel_base - shift)+1.1), // rear right
|
|
Eigen::Vector3f(-track_width, 0.f - 0.21, (wheel_base - shift)+1.1) // rear left
|
|
};
|
|
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]);
|
|
if (isFront[i]) {
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(carSteeringAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
}
|
|
renderer.DrawVertexRenderStruct(carWheelMesh);
|
|
renderer.PopMatrix();
|
|
}
|
|
}
|
|
|
|
renderer.PopMatrix();
|
|
|
|
drawNpcCar();
|
|
|
|
if (player && !inCar) player->draw(renderer);
|
|
|
|
if (girlfriend && !girlfriendInCar)
|
|
{
|
|
girlfriend->draw(renderer);
|
|
}
|
|
|
|
if (salesperson)
|
|
{
|
|
salesperson->draw(renderer);
|
|
}
|
|
|
|
if (police)
|
|
{
|
|
police->draw(renderer);
|
|
}
|
|
|
|
if (bandit)
|
|
{
|
|
bandit->draw(renderer);
|
|
}
|
|
|
|
for (auto& npc : npcs) npc->draw(renderer);
|
|
|
|
//#ifdef SHOW_PATH
|
|
drawDebugNavigation();
|
|
drawDebugForbidden();
|
|
//#endif
|
|
|
|
renderer.PopMatrix();
|
|
|
|
renderer.PopProjectionMatrix();
|
|
|
|
renderer.shaderManager.PopShader();
|
|
|
|
}
|
|
|
|
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) {
|
|
if (name == roofObjectKey && !roofVisible) {
|
|
continue;
|
|
}
|
|
if (name == azsRoofObjectKey && !azsRoofVisible) {
|
|
continue;
|
|
}
|
|
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);
|
|
|
|
if (girlfriend && !girlfriendInCar)
|
|
{
|
|
girlfriend->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.0f, 0.0f, 0.0f, 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 = "default_shadow";
|
|
|
|
renderer.shaderManager.PushShader(shadowShaderName);
|
|
renderer.RenderUniform1i(textureUniformName, 0);
|
|
renderer.RenderUniform1i("uShadowMap", 1);
|
|
|
|
// 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) {
|
|
if (name == roofObjectKey && !roofVisible) {
|
|
continue;
|
|
}
|
|
if (name == azsRoofObjectKey && !azsRoofVisible) {
|
|
continue;
|
|
}
|
|
glBindTexture(GL_TEXTURE_2D, gameObj.texture->getTexID());
|
|
renderer.DrawVertexRenderStruct(gameObj.mesh);
|
|
}
|
|
|
|
CheckGlError(__FILE__, __LINE__);
|
|
for (auto& intObj : interactiveObjects) {
|
|
if (intObj.isActive) {
|
|
intObj.draw(renderer);
|
|
}
|
|
}
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
#ifdef DEBUG_LIGHT
|
|
// In debug-light mode characters use the plain shaders (draw normally
|
|
// but from the light's viewpoint — projection/view already on stack).
|
|
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->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
for (auto& npc : npcs) npc->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
|
|
#endif
|
|
CheckGlError(__FILE__, __LINE__);
|
|
|
|
drawDebugNavigation();
|
|
drawDebugForbidden();
|
|
|
|
renderer.PopMatrix();
|
|
renderer.PopProjectionMatrix();
|
|
renderer.shaderManager.PopShader();
|
|
}
|
|
|
|
bool Location::setNavigationAreaAvailable(const std::string& areaName, bool available)
|
|
{
|
|
return navigation.setAreaAvailable(areaName, available);
|
|
}
|
|
|
|
bool Location::isPointInCarFootprint(const Eigen::Vector3f& point, const Eigen::Vector3f& center, float rotation) const
|
|
{
|
|
constexpr float carLength = 7.3f;
|
|
constexpr float carWidth = 2.8f;
|
|
const Eigen::Vector3f forward(-std::sin(rotation), 0.f, -std::cos(rotation));
|
|
const Eigen::Vector3f right(std::cos(rotation), 0.f, -std::sin(rotation));
|
|
Eigen::Vector3f delta = point - center;
|
|
delta.y() = 0.f;
|
|
const float alongForward = delta.dot(forward);
|
|
const float alongRight = delta.dot(right);
|
|
return std::abs(alongForward) <= carLength * 0.5f && std::abs(alongRight) <= carWidth * 0.5f;
|
|
}
|
|
|
|
void Location::pushOutOfNpcCarFootprint(Eigen::Vector3f& position) const
|
|
{
|
|
if (!npcCar.texture) return;
|
|
|
|
constexpr float carLength = 7.3f;
|
|
constexpr float carWidth = 2.8f;
|
|
constexpr float halfLength = carLength * 0.5f;
|
|
constexpr float halfWidth = carWidth * 0.5f;
|
|
constexpr float skin = 0.05f;
|
|
|
|
const Eigen::Vector3f forward(-std::sin(npcCar.rotation), 0.f, -std::cos(npcCar.rotation));
|
|
const Eigen::Vector3f right(std::cos(npcCar.rotation), 0.f, -std::sin(npcCar.rotation));
|
|
|
|
Eigen::Vector3f delta = position - npcCar.position;
|
|
delta.y() = 0.f;
|
|
float alongForward = delta.dot(forward);
|
|
float alongRight = delta.dot(right);
|
|
|
|
if (std::abs(alongForward) >= halfLength || std::abs(alongRight) >= halfWidth) {
|
|
return;
|
|
}
|
|
|
|
const float penForward = halfLength - std::abs(alongForward);
|
|
const float penRight = halfWidth - std::abs(alongRight);
|
|
|
|
if (penRight <= penForward) {
|
|
const float sign = alongRight >= 0.f ? 1.f : -1.f;
|
|
alongRight = sign * (halfWidth + skin);
|
|
} else {
|
|
const float sign = alongForward >= 0.f ? 1.f : -1.f;
|
|
alongForward = sign * (halfLength + skin);
|
|
}
|
|
|
|
const float originalY = position.y();
|
|
Eigen::Vector3f resolved = npcCar.position + forward * alongForward + right * alongRight;
|
|
resolved.y() = originalY;
|
|
position = resolved;
|
|
}
|
|
|
|
bool Location::doesPlayerCarCollideWithNpcCar(const Eigen::Vector3f& center, float rotation) const
|
|
{
|
|
if (!npcCar.texture) return false;
|
|
|
|
constexpr float carLength = 7.3f;
|
|
constexpr float carWidth = 2.8f;
|
|
constexpr float sampleSpacing = 0.5f;
|
|
|
|
const Eigen::Vector3f delta = center - npcCar.position;
|
|
const float approxRadius = std::sqrt(carLength * carLength + carWidth * carWidth);
|
|
if (delta.squaredNorm() > approxRadius * approxRadius) {
|
|
return false;
|
|
}
|
|
|
|
const float halfLength = carLength * 0.5f;
|
|
const float halfWidth = carWidth * 0.5f;
|
|
const int longSteps = max(1, static_cast<int>(std::ceil(carLength / sampleSpacing)));
|
|
const int widthSteps = max(1, static_cast<int>(std::ceil(carWidth / sampleSpacing)));
|
|
|
|
const Eigen::Vector3f forward(-std::sin(rotation), 0.f, -std::cos(rotation));
|
|
const Eigen::Vector3f right(std::cos(rotation), 0.f, -std::sin(rotation));
|
|
|
|
for (int i = 0; i <= longSteps; ++i) {
|
|
const float fl = (static_cast<float>(i) / static_cast<float>(longSteps)) * 2.0f - 1.0f;
|
|
const Eigen::Vector3f lineOffset = forward * (fl * halfLength);
|
|
for (int j = 0; j <= widthSteps; ++j) {
|
|
const float fw = (static_cast<float>(j) / static_cast<float>(widthSteps)) * 2.0f - 1.0f;
|
|
const Eigen::Vector3f sample = center + lineOffset + right * (fw * halfWidth);
|
|
if (isPointInCarFootprint(sample, npcCar.position, npcCar.rotation)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Location::isCarFootprintWalkable(const Eigen::Vector3f& center, float rotation) const
|
|
{
|
|
if (!navigation.isReady()) return true;
|
|
|
|
constexpr float carLength = 7.3f;
|
|
constexpr float carWidth = 2.8f;
|
|
constexpr float sampleSpacing = 0.35f;
|
|
|
|
const float halfLength = carLength * 0.5f;
|
|
const float halfWidth = carWidth * 0.5f;
|
|
const int longSteps = max(1, static_cast<int>(std::ceil(carLength / sampleSpacing)));
|
|
const int widthSteps = max(1, static_cast<int>(std::ceil(carWidth / sampleSpacing)));
|
|
|
|
const Eigen::Vector3f forward(-std::sin(rotation), 0.f, -std::cos(rotation));
|
|
const Eigen::Vector3f right(std::cos(rotation), 0.f, -std::sin(rotation));
|
|
|
|
for (int i = 0; i <= longSteps; ++i) {
|
|
const float fl = (static_cast<float>(i) / static_cast<float>(longSteps)) * 2.0f - 1.0f;
|
|
const Eigen::Vector3f lineOffset = forward * (fl * halfLength);
|
|
for (int j = 0; j <= widthSteps; ++j) {
|
|
const float fw = (static_cast<float>(j) / static_cast<float>(widthSteps)) * 2.0f - 1.0f;
|
|
const Eigen::Vector3f sample = center + lineOffset + right * (fw * halfWidth);
|
|
if (!navigation.isWalkable(sample)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Location::update(int64_t delta)
|
|
{
|
|
const Eigen::Vector3f playerPosBefore = player ? player->position : Eigen::Vector3f::Zero();
|
|
|
|
if (player) {
|
|
if (!inCar) {
|
|
player->targetFacingAngle = cameraAzimuth;
|
|
if (playerFrozen) {
|
|
player->clearPath();
|
|
wasKeyForward = false;
|
|
} else if (keyForward) {
|
|
player->attackTarget = nullptr;
|
|
Eigen::Vector3f forward(std::sin(cameraAzimuth), 0.f, -std::cos(cameraAzimuth));
|
|
player->setDirectWalkTarget(player->position + forward * 5.0f);
|
|
wasKeyForward = true;
|
|
} else {
|
|
if (wasKeyForward) {
|
|
player->clearPath();
|
|
}
|
|
wasKeyForward = false;
|
|
}
|
|
}
|
|
player->update(delta);
|
|
dialogueSystem.update(static_cast<int>(delta), player->position);
|
|
|
|
if (!roofObjectKey.empty()) {
|
|
bool insideAnyZone = false;
|
|
const Eigen::Vector2f playerPos2d(player->position.x(), player->position.z());
|
|
for (const auto& zone : roofHideZones) {
|
|
if (zone.contains(playerPos2d)) {
|
|
insideAnyZone = true;
|
|
break;
|
|
}
|
|
}
|
|
const float maxHeightForHide = 3.0f;
|
|
roofVisible = !(insideAnyZone && player->position.y() < maxHeightForHide);
|
|
}
|
|
|
|
if (!azsRoofObjectKey.empty()) {
|
|
bool insideAnyZone = false;
|
|
const Eigen::Vector2f playerPos2d(player->position.x(), player->position.z());
|
|
for (const auto& zone : azsRoofHideZones) {
|
|
if (zone.contains(playerPos2d)) {
|
|
insideAnyZone = true;
|
|
break;
|
|
}
|
|
}
|
|
const float maxHeightForHide = 3.0f;
|
|
azsRoofVisible = !(insideAnyZone && player->position.y() < maxHeightForHide);
|
|
}
|
|
}
|
|
|
|
if (girlfriend)
|
|
{
|
|
updateGirlfriendFollow();
|
|
girlfriend->update(delta);
|
|
if (!girlfriendInCar) {
|
|
pushOutOfNpcCarFootprint(girlfriend->position);
|
|
}
|
|
}
|
|
|
|
if (salesperson) pushOutOfNpcCarFootprint(salesperson->position);
|
|
if (police) {
|
|
police->update(delta);
|
|
pushOutOfNpcCarFootprint(police->position);
|
|
}
|
|
if (bandit) pushOutOfNpcCarFootprint(bandit->position);
|
|
|
|
for (auto& npc : npcs) {
|
|
npc->update(delta);
|
|
pushOutOfNpcCarFootprint(npc->position);
|
|
}
|
|
|
|
if (inCar) {
|
|
float dt = static_cast<float>(delta) / 1000.0f;
|
|
|
|
constexpr float roadHalfWidth = 10.0f;
|
|
constexpr float offRoadFrictionMultiplier = 4.0f;
|
|
const bool offRoad = std::abs(carPosition.x()) > roadHalfWidth;
|
|
|
|
if (!offRoad && !carOutOfGas) {
|
|
if (keyForward) {
|
|
carVelocity += carAcceleration * dt;
|
|
}
|
|
if (keyBackward) {
|
|
carVelocity -= carAcceleration * dt;
|
|
}
|
|
}
|
|
|
|
const bool applyThrottleFriction = offRoad || (!keyForward && !keyBackward);
|
|
if (applyThrottleFriction) {
|
|
const float frictionRate = offRoad ? (carFriction * offRoadFrictionMultiplier) : carFriction;
|
|
const float resistance = frictionRate * dt;
|
|
if (carVelocity > 0.f) {
|
|
carVelocity = max(0.f, carVelocity - resistance);
|
|
} else if (carVelocity < 0.f) {
|
|
carVelocity = min(0.f, carVelocity + resistance);
|
|
}
|
|
}
|
|
|
|
carVelocity = max(-carMaxReverseSpeed, min(carMaxSpeed, carVelocity));
|
|
|
|
const float oldRotation = carRotation;
|
|
if (std::abs(carVelocity) > 0.01f) {
|
|
float speedFactor = carVelocity / carMaxSpeed;
|
|
if (keyLeft) carRotation += carTurnRate * dt * speedFactor;
|
|
if (keyRight) carRotation -= carTurnRate * dt * speedFactor;
|
|
}
|
|
if (navigation.isReady() && !isCarFootprintWalkable(carPosition, carRotation)) {
|
|
carRotation = oldRotation;
|
|
}
|
|
if (doesPlayerCarCollideWithNpcCar(carPosition, carRotation)) {
|
|
carRotation = oldRotation;
|
|
}
|
|
|
|
float targetSteer = (keyLeft ? 1.f : 0.f) - (keyRight ? 1.f : 0.f);
|
|
targetSteer *= carMaxSteerAngle;
|
|
float steerLerp = min(1.f, dt * 8.f);
|
|
carSteeringAngle += (targetSteer - carSteeringAngle) * steerLerp;
|
|
|
|
const Eigen::Vector3f forward(-std::sin(carRotation), 0.f, -std::cos(carRotation));
|
|
const Eigen::Vector3f totalDelta = forward * carVelocity * dt;
|
|
const float totalDist = totalDelta.norm();
|
|
const float maxSubstep = 0.2f;
|
|
const int substeps = max(1, static_cast<int>(std::ceil(totalDist / maxSubstep)));
|
|
const Eigen::Vector3f stepDelta = totalDelta / static_cast<float>(substeps);
|
|
for (int s = 0; s < substeps; ++s) {
|
|
Eigen::Vector3f candidate = carPosition + stepDelta;
|
|
if (navigation.isReady() && !isCarFootprintWalkable(candidate, carRotation)) {
|
|
carVelocity = 0.f;
|
|
break;
|
|
}
|
|
if (doesPlayerCarCollideWithNpcCar(candidate, carRotation)) {
|
|
carVelocity = 0.f;
|
|
if (!dialoguePlayedCrash && !dialogueSystem.isActive()) {
|
|
if (dialogueSystem.startDialogue("driving_dialogue_crash")) {
|
|
dialoguePlayedCrash = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
carPosition = candidate;
|
|
}
|
|
|
|
if (offRoad && std::abs(carVelocity) < 0.1f && (keyForward || keyBackward)) {
|
|
if (!dialoguePlayedOffroad && !dialogueSystem.isActive()) {
|
|
if (dialogueSystem.startDialogue("driving_dialogue_offroad")) {
|
|
dialoguePlayedOffroad = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dialoguePlayedCrash && carVelocity > 3.0)
|
|
{
|
|
dialoguePlayedCrash = false;
|
|
}
|
|
|
|
if (player) {
|
|
player->position = carPosition;
|
|
}
|
|
|
|
if (girlfriendInCar)
|
|
{
|
|
if (!dialoguePlayedDrivingGas1 && !dialogueSystem.isActive()) {
|
|
if (dialogueSystem.startDialogue("driving_dialogue_gas1")) {
|
|
dialoguePlayedDrivingGas1 = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ran out of gas: far south without having heard the girlfriend's gas
|
|
// reminder. Latches the stall, then plays the out-of-gas line once.
|
|
if (!carOutOfGas && !dialoguePlayedGas1 && player->position.z() < -300.f) {
|
|
carOutOfGas = true;
|
|
}
|
|
if (carOutOfGas && !dialoguePlayedDrivingGasOut && !dialogueSystem.isActive()) {
|
|
if (dialogueSystem.startDialogue("driving_dialogue_gas_out")) {
|
|
dialoguePlayedDrivingGasOut = true;
|
|
}
|
|
}
|
|
|
|
if (!policeFollow &&
|
|
player->position.z() - npcCar.position.z() > -10.f
|
|
&& player->position.z() < npcCar.position.z()
|
|
&& npcCar.position.x()<900
|
|
&& policeEncounterStage != PoliceEncounterStage::Done)
|
|
{
|
|
policeFollow = true;
|
|
npcCar.mode = NpcCar::Mode::FOLLOW_PLAYER;
|
|
policeDrivingDialogueTimer = 8.0f;
|
|
}
|
|
|
|
// While police follows and player is driving, the cop yells every 8s.
|
|
if (policeFollow && policeEncounterStage == PoliceEncounterStage::Idle) {
|
|
policeDrivingDialogueTimer -= static_cast<float>(delta) / 1000.f;
|
|
if (policeDrivingDialogueTimer <= 0.f && !dialogueSystem.isActive()) {
|
|
if (dialogueSystem.startDialogue("driving_dialogue1")) {
|
|
policeDrivingDialogueTimer = 8.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
if (player && !inCar) {
|
|
auto blocked = [&](const Eigen::Vector3f& p) {
|
|
if (navigation.isReady() && !navigation.isWalkable(p)) return true;
|
|
if (isPointInCarFootprint(p, carPosition, carRotation)) return true;
|
|
if (isPointInCarFootprint(p, npcCar.position, npcCar.rotation)) return true;
|
|
return false;
|
|
};
|
|
if (blocked(player->position)) {
|
|
const Eigen::Vector3f slideX(player->position.x(), playerPosBefore.y(), playerPosBefore.z());
|
|
const Eigen::Vector3f slideZ(playerPosBefore.x(), playerPosBefore.y(), player->position.z());
|
|
Eigen::Vector3f resolved;
|
|
if (!blocked(slideX)) {
|
|
resolved = slideX;
|
|
} else if (!blocked(slideZ)) {
|
|
resolved = slideZ;
|
|
} else {
|
|
resolved = playerPosBefore;
|
|
}
|
|
player->position = resolved;
|
|
}
|
|
}
|
|
|
|
updateNpcCar(delta);
|
|
|
|
// Track remaining driving distance toward -Z. Computed from this frame's
|
|
// real movement (captured before the tile-shift below), so the teleport
|
|
// never contributes to the count.
|
|
if (player) {
|
|
distanceRemaining += player->position.z() - playerPosBefore.z();
|
|
}
|
|
if (!dialoguePlayedDistance7000 && distanceRemaining < 7000.f/5.f && !dialogueSystem.isActive()) {
|
|
if (dialogueSystem.startDialogue("driving_dialogue_distance7000")) {
|
|
dialoguePlayedDistance7000 = true;
|
|
}
|
|
}
|
|
if (!dialoguePlayedDistance5000 && distanceRemaining < 5000.f/5.f && !dialogueSystem.isActive()) {
|
|
if (dialogueSystem.startDialogue("driving_dialogue_distance5000")) {
|
|
dialoguePlayedDistance5000 = true;
|
|
}
|
|
}
|
|
if (!dialoguePlayedDistance2000 && distanceRemaining < 2000.f/5.f && !dialogueSystem.isActive()) {
|
|
if (dialogueSystem.startDialogue("driving_dialogue_distance2000")) {
|
|
dialoguePlayedDistance2000 = true;
|
|
}
|
|
}
|
|
if (!dialoguePlayedDistance0 && distanceRemaining < 0.f && !dialogueSystem.isActive()) {
|
|
if (dialogueSystem.startDialogue("driving_dialogue_distance0")) {
|
|
dialoguePlayedDistance0 = true;
|
|
}
|
|
}
|
|
|
|
{
|
|
constexpr float tileSize = 100.0f;
|
|
constexpr float teleportThreshold = tileSize * 3.5f;
|
|
|
|
const Eigen::Vector3f anchor = inCar
|
|
? carPosition
|
|
: (player ? player->position : Eigen::Vector3f::Zero());
|
|
|
|
Eigen::Vector3f shift = Eigen::Vector3f::Zero();
|
|
if (anchor.x() > teleportThreshold) shift.x() = -tileSize;
|
|
else if (anchor.x() < -teleportThreshold) shift.x() = tileSize;
|
|
if (anchor.z() > teleportThreshold) shift.z() = -tileSize;
|
|
else if (anchor.z() < -teleportThreshold) shift.z() = tileSize;
|
|
|
|
if (shift.squaredNorm() > 0.f) {
|
|
if (inCar) {
|
|
carPosition += shift;
|
|
if (player) player->position = carPosition;
|
|
}
|
|
else if (player) {
|
|
player->position += shift;
|
|
player->setTarget(player->position);
|
|
player->clearPath();
|
|
}
|
|
if (npcCar.mode == NpcCar::Mode::FOLLOW_PLAYER || npcCar.mode == NpcCar::Mode::NONE_STAY) {
|
|
npcCar.position += shift;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
// Check if player reached target interactive object
|
|
if (targetInteractiveObject && player) {
|
|
|
|
}*/
|
|
|
|
// After the gas-station sale, once the player has walked away from the
|
|
// salesperson, the girlfriend chimes in — fires once.
|
|
if (!dialoguePlayedGirlfriend1 && dialoguePlayedGas1 && salesperson && player && !dialogueSystem.isActive()) {
|
|
constexpr float minDistance = 8.0f;
|
|
const float dx = player->position.x() - salesperson->position.x();
|
|
const float dz = player->position.z() - salesperson->position.z();
|
|
if (std::hypot(dx, dz) > minDistance) {
|
|
if (dialogueSystem.startDialogue("dialogue_girlfriend1")) {
|
|
dialoguePlayedGirlfriend1 = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Police encounter: once the player gets out of the car while being
|
|
// followed, the officer walks over from the NPC car, delivers his line,
|
|
// then walks back and despawns. Happens exactly once.
|
|
if (player && policeFollow && !inCar && policeEncounterStage == PoliceEncounterStage::Idle && !dialogueSystem.isActive() && police) {
|
|
Eigen::Vector3f toCar = npcCar.position - player->position;
|
|
toCar.y() = 0.f;
|
|
const float toCarDist = toCar.norm();
|
|
|
|
// Spawn right next to the NPC car, on its left side.
|
|
const Eigen::Vector3f carLeft(-std::cos(npcCar.rotation), 0.f, std::sin(npcCar.rotation));
|
|
constexpr float carHalfWidth = 2.8f * 0.5f;
|
|
police->position = npcCar.position + carLeft * (carHalfWidth + 1.5f);
|
|
police->position.y() = 0.f;
|
|
police->clearPath();
|
|
|
|
// Walk to a spot 1.5 m short of the player, approaching from the car's side.
|
|
Eigen::Vector3f approachTarget = player->position;
|
|
if (toCarDist > 1e-4f) {
|
|
approachTarget = player->position + (toCar / toCarDist) * 1.5f;
|
|
}
|
|
approachTarget.y() = 0.f;
|
|
police->setTarget(approachTarget);
|
|
|
|
if (dialogueSystem.startDialogue("dialogue_police1")) {
|
|
playerFrozen = true;
|
|
policeEncounterStage = PoliceEncounterStage::Approaching;
|
|
}
|
|
}
|
|
|
|
if (policeEncounterStage == PoliceEncounterStage::Approaching) {
|
|
if (!dialogueSystem.isActive()) {
|
|
// Dialogue finished — unfreeze player and send officer back to the car.
|
|
playerFrozen = false;
|
|
if (police) {
|
|
police->setTarget(npcCar.position);
|
|
}
|
|
policeEncounterStage = PoliceEncounterStage::Returning;
|
|
}
|
|
}
|
|
|
|
if (policeEncounterStage == PoliceEncounterStage::Returning) {
|
|
if (police) {
|
|
const float dx = police->position.x() - npcCar.position.x();
|
|
const float dz = police->position.z() - npcCar.position.z();
|
|
if (std::hypot(dx, dz) <= 8.0f) {
|
|
police.reset();
|
|
}
|
|
}
|
|
if (!police) {
|
|
policeFollow = false;
|
|
npcCar.mode = NpcCar::Mode::NONE_STAY;
|
|
policeEncounterStage = PoliceEncounterStage::Done;
|
|
}
|
|
}
|
|
|
|
// Phone rings once the player drives far from the gas station — fires once,
|
|
// and only after the gas-station sale.
|
|
if (inCar && dialoguePlayedGas1 && !dialoguePlayedPhone1 && !dialogueSystem.isActive()) {
|
|
constexpr float minDistance = 50.0f;
|
|
const Eigen::Vector3f gasStationPos(-3.f, 0.f, -11.f);
|
|
const float dx = carPosition.x() - gasStationPos.x();
|
|
const float dz = carPosition.z() - gasStationPos.z();
|
|
if (std::hypot(dx, dz) > minDistance) {
|
|
if (dialogueSystem.startDialogue("dialogue_phone1", [this]() {
|
|
dialoguePhone1Finished = true;
|
|
})) {
|
|
dialoguePlayedPhone1 = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// After the phone dialogue, once the player's car enters the ambush zone,
|
|
// spawn the NPC (police) car at its staged position. Fires once.
|
|
if (dialoguePhone1Finished && !npcCarSpawnedAfterPhone) {
|
|
if (carPosition.z() > -350.f && carPosition.z() < -340.f) {
|
|
npcCar.position = Eigen::Vector3f(9.f, 0.f, -325.f);
|
|
npcCarSpawnedAfterPhone = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Location::updateNpcCar(int64_t deltaMs)
|
|
{
|
|
const float dt = static_cast<float>(deltaMs) / 1000.0f;
|
|
|
|
Eigen::Vector3f target = npcCar.position;
|
|
float throttle = 0.f;
|
|
bool hasTarget = false;
|
|
|
|
if (npcCar.mode == NpcCar::Mode::FOLLOW_WAYPOINTS) {
|
|
if (!npcCar.waypoints.empty()) {
|
|
if (npcCar.currentWaypoint >= npcCar.waypoints.size()) {
|
|
npcCar.currentWaypoint = 0;
|
|
}
|
|
Eigen::Vector3f toTarget = npcCar.waypoints[npcCar.currentWaypoint] - npcCar.position;
|
|
toTarget.y() = 0.f;
|
|
if (toTarget.norm() < npcCar.waypointReachRadius) {
|
|
npcCar.currentWaypoint = (npcCar.currentWaypoint + 1) % npcCar.waypoints.size();
|
|
}
|
|
target = npcCar.waypoints[npcCar.currentWaypoint];
|
|
throttle = 1.0f;
|
|
hasTarget = true;
|
|
}
|
|
}
|
|
else if (npcCar.mode == NpcCar::Mode::FOLLOW_PLAYER) {
|
|
|
|
//if (x > 0.5)
|
|
{
|
|
target = carPosition;
|
|
Eigen::Vector3f toTarget = target - npcCar.position;
|
|
toTarget.y() = 0.f;
|
|
const float dist = toTarget.norm();
|
|
|
|
const float targetDist = npcCar.followMinDistance;
|
|
const float coastBuffer = 1.5f;
|
|
|
|
if (dist > npcCar.followMaxDistance) {
|
|
throttle = 1.0f;
|
|
}
|
|
else if (dist > targetDist + coastBuffer) {
|
|
throttle = 0.3f;
|
|
}
|
|
else if (dist > targetDist - coastBuffer) {
|
|
throttle = 0.f;
|
|
}
|
|
else {
|
|
const float underBy = (targetDist - coastBuffer) - dist;
|
|
const float brakeRamp = min(1.0f, underBy / 3.0f);
|
|
throttle = -(1.0f + 1.5f * brakeRamp);
|
|
}
|
|
hasTarget = dist > 0.5f;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throttle = 0.f;
|
|
}
|
|
|
|
float targetHeading = npcCar.rotation;
|
|
if (hasTarget) {
|
|
Eigen::Vector3f toTarget = target - npcCar.position;
|
|
toTarget.y() = 0.f;
|
|
if (toTarget.squaredNorm() > 1e-4f) {
|
|
targetHeading = std::atan2(-toTarget.x(), -toTarget.z());
|
|
}
|
|
}
|
|
|
|
float angleDiff = targetHeading - npcCar.rotation;
|
|
while (angleDiff > M_PI) angleDiff -= 2.0f * static_cast<float>(M_PI);
|
|
while (angleDiff < -M_PI) angleDiff += 2.0f * static_cast<float>(M_PI);
|
|
|
|
const float turnIntent = max(-1.0f, min(1.0f, angleDiff * 2.0f));
|
|
const float targetSteer = turnIntent * npcCar.maxSteerAngle;
|
|
const float steerLerp = min(1.f, dt * 8.f);
|
|
npcCar.steeringAngle += (targetSteer - npcCar.steeringAngle) * steerLerp;
|
|
|
|
if (throttle > 0.f) {
|
|
npcCar.velocity += npcCar.acceleration * dt * throttle;
|
|
}
|
|
else if (throttle < 0.f) {
|
|
const float brakeForce = npcCar.acceleration * dt * (-throttle);
|
|
if (npcCar.velocity > 0.f) {
|
|
npcCar.velocity = max(0.f, npcCar.velocity - brakeForce);
|
|
}
|
|
else {
|
|
npcCar.velocity = min(0.f, npcCar.velocity + brakeForce);
|
|
}
|
|
}
|
|
else {
|
|
const float resistance = npcCar.friction * dt;
|
|
if (npcCar.velocity > 0.f) {
|
|
npcCar.velocity = max(0.f, npcCar.velocity - resistance);
|
|
}
|
|
else if (npcCar.velocity < 0.f) {
|
|
npcCar.velocity = min(0.f, npcCar.velocity + resistance);
|
|
}
|
|
}
|
|
npcCar.velocity = max(-npcCar.maxReverseSpeed, min(npcCar.maxSpeed, npcCar.velocity));
|
|
|
|
const float oldRotation = npcCar.rotation;
|
|
if (std::abs(npcCar.velocity) > 0.01f) {
|
|
const float speedFactor = npcCar.velocity / npcCar.maxSpeed;
|
|
npcCar.rotation += turnIntent * npcCar.turnRate * dt * speedFactor;
|
|
}
|
|
if (navigation.isReady() && !isCarFootprintWalkable(npcCar.position, npcCar.rotation)) {
|
|
npcCar.rotation = oldRotation;
|
|
}
|
|
|
|
const Eigen::Vector3f forward(-std::sin(npcCar.rotation), 0.f, -std::cos(npcCar.rotation));
|
|
const Eigen::Vector3f totalDelta = forward * npcCar.velocity * dt;
|
|
const float totalDist = totalDelta.norm();
|
|
const float maxSubstep = 0.2f;
|
|
const int substeps = max(1, static_cast<int>(std::ceil(totalDist / maxSubstep)));
|
|
const Eigen::Vector3f stepDelta = totalDelta / static_cast<float>(substeps);
|
|
for (int s = 0; s < substeps; ++s) {
|
|
Eigen::Vector3f candidate = npcCar.position + stepDelta;
|
|
if (navigation.isReady() && !isCarFootprintWalkable(candidate, npcCar.rotation)) {
|
|
npcCar.velocity = 0.f;
|
|
break;
|
|
}
|
|
npcCar.position = candidate;
|
|
}
|
|
}
|
|
|
|
void Location::updateGirlfriendFollow()
|
|
{
|
|
if (!girlfriend || !player) return;
|
|
|
|
constexpr float retargetThreshold = 0.1f;
|
|
constexpr float stopDistance = 1.0f;
|
|
constexpr float carEnterRadius = 3.0f;
|
|
|
|
// Player just exited the car while girlfriend was inside — she steps out
|
|
// right next to him, as if coming out of the rear door (same lateral side,
|
|
// offset back along the car's length).
|
|
if (girlfriendInCar && !inCar) {
|
|
const Eigen::Vector3f forward(-std::sin(carRotation), 0.f, -std::cos(carRotation));
|
|
Eigen::Vector3f exitPos = player->position - forward * 1.5f;
|
|
exitPos.y() = 0.f;
|
|
girlfriend->position = exitPos;
|
|
girlfriend->clearPath();
|
|
girlfriendInCar = false;
|
|
girlfriendLastFollowTargetValid = false;
|
|
}
|
|
|
|
// While she is riding, her position is glued to the car and no pathing needed.
|
|
if (girlfriendInCar) {
|
|
girlfriend->position = carPosition;
|
|
girlfriend->position.y() = 0.f;
|
|
girlfriend->clearPath();
|
|
return;
|
|
}
|
|
|
|
// Player is in car and girlfriend has caught up — she jumps in.
|
|
if (inCar) {
|
|
Eigen::Vector3f diff = girlfriend->position - carPosition;
|
|
diff.y() = 0.f;
|
|
if (diff.norm() <= carEnterRadius) {
|
|
girlfriendInCar = true;
|
|
girlfriend->position = carPosition;
|
|
girlfriend->position.y() = 0.f;
|
|
girlfriend->clearPath();
|
|
girlfriendLastFollowTargetValid = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Follow anchor: the car when player is driving, otherwise a point stopDistance
|
|
// away from the player along the line to the girlfriend (so she stops near him).
|
|
Eigen::Vector3f anchor;
|
|
if (inCar) {
|
|
anchor = carPosition;
|
|
} else {
|
|
Eigen::Vector3f toPlayer = player->position - girlfriend->position;
|
|
toPlayer.y() = 0.f;
|
|
const float distToPlayer = toPlayer.norm();
|
|
if (distToPlayer <= stopDistance) {
|
|
girlfriend->clearPath();
|
|
girlfriendLastFollowTargetValid = false;
|
|
return;
|
|
}
|
|
const Eigen::Vector3f fromPlayer = -toPlayer / distToPlayer;
|
|
anchor = player->position + fromPlayer * stopDistance;
|
|
}
|
|
anchor.y() = 0.f;
|
|
|
|
const bool shouldRetarget = !girlfriendLastFollowTargetValid ||
|
|
(anchor - girlfriendLastFollowTarget).norm() > retargetThreshold;
|
|
|
|
if (shouldRetarget) {
|
|
girlfriend->setTarget(anchor);
|
|
girlfriendLastFollowTarget = anchor;
|
|
girlfriendLastFollowTargetValid = true;
|
|
}
|
|
}
|
|
|
|
void Location::drawNpcCar()
|
|
{
|
|
if (!npcCar.texture) return;
|
|
|
|
renderer.PushMatrix();
|
|
renderer.TranslateMatrix(npcCar.position);
|
|
renderer.TranslateMatrix({ 0, 0.7f, 0 });
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(npcCar.rotation, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
glBindTexture(GL_TEXTURE_2D, npcCar.texture->getTexID());
|
|
renderer.DrawVertexRenderStruct(carMesh);
|
|
|
|
if (carWheelTexture) {
|
|
constexpr float track_width = 1.28f;
|
|
constexpr float wheel_base = 2.25f;
|
|
constexpr float shift = 0.6f;
|
|
const Eigen::Vector3f wheelPositions[4] = {
|
|
Eigen::Vector3f( track_width, 0.f - 0.21f, -(wheel_base + shift) + 1.25f),
|
|
Eigen::Vector3f(-track_width, 0.f - 0.21f, -(wheel_base + shift) + 1.25f),
|
|
Eigen::Vector3f( track_width, 0.f - 0.21f, (wheel_base - shift) + 1.1f),
|
|
Eigen::Vector3f(-track_width, 0.f - 0.21f, (wheel_base - shift) + 1.1f)
|
|
};
|
|
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]);
|
|
if (isFront[i]) {
|
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(npcCar.steeringAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
|
}
|
|
renderer.DrawVertexRenderStruct(carWheelMesh);
|
|
renderer.PopMatrix();
|
|
}
|
|
}
|
|
|
|
renderer.PopMatrix();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (inCar) {
|
|
return;
|
|
}
|
|
|
|
if (salesperson && player && !dialogueSystem.isActive()) {
|
|
constexpr float maxTalkDistance = 4.0f;
|
|
// cos(45°) — salesperson must lie within the ±45° half-cone in front
|
|
// of the player (90° total field of view).
|
|
constexpr float coneCosThreshold = 0.7071067f;
|
|
|
|
|
|
Eigen::Vector3f toSales = salesperson->position - player->position;
|
|
toSales.y() = 0.f;
|
|
const float dist = toSales.norm();
|
|
|
|
if (dist > 1e-4f && dist <= maxTalkDistance) {
|
|
const Eigen::Vector3f playerForward(std::sin(cameraAzimuth), 0.f, -std::cos(cameraAzimuth));
|
|
const Eigen::Vector3f toSalesDir = toSales / dist;
|
|
|
|
if (playerForward.dot(toSalesDir) >= coneCosThreshold) {
|
|
const Eigen::Vector3f gasStationPos(-3.f, 0.f, -11.f);
|
|
const float carDistToStation = std::hypot(
|
|
carPosition.x() - gasStationPos.x(),
|
|
carPosition.z() - gasStationPos.z());
|
|
|
|
std::string dialogueId;
|
|
if (dialoguePlayedGas1) {
|
|
dialogueId = "dialogue_gas3";
|
|
} else if (carDistToStation <= 10.f) {
|
|
dialogueId = "dialogue_gas1";
|
|
} else {
|
|
dialogueId = "dialogue_gas2";
|
|
}
|
|
|
|
if (dialogueSystem.startDialogue(dialogueId)) {
|
|
// Face each other. cameraAzimuth also drives the player's
|
|
// targetFacingAngle, so updating it rotates the player toward
|
|
// the salesperson.
|
|
cameraAzimuth = std::atan2(toSales.x(), -toSales.z());
|
|
salesperson->targetFacingAngle = std::atan2(-toSales.x(), toSales.z());
|
|
if (dialogueId == "dialogue_gas1") {
|
|
dialoguePlayedGas1 = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
void Location::handleUp(int64_t fingerId, int mx, int my)
|
|
{
|
|
|
|
}
|
|
void Location::handleMotion(int64_t fingerId, int dx, int dy, int mx, int my)
|
|
{
|
|
|
|
/*if (dialogueSystem.blocksGameplayInput()) {
|
|
dialogueSystem.handlePointerMoved(
|
|
static_cast<float>(mx),
|
|
Environment::projectionHeight - static_cast<float>(my)
|
|
);
|
|
return;
|
|
}*/
|
|
|
|
const float sensitivity = 0.005f;
|
|
const float inclinationSign = invertCameraY ? -1.0f : 1.0f;
|
|
cameraAzimuth += dx * sensitivity;
|
|
cameraInclination += inclinationSign * dy * sensitivity;
|
|
|
|
const float minInclination = M_PI * 10.f / 180.f;
|
|
const float maxInclination = M_PI * 0.5f;
|
|
cameraInclination = max(minInclination, min(maxInclination, cameraInclination));
|
|
}
|
|
|
|
void Location::handleKeyDown(int sdlKey)
|
|
{
|
|
switch (sdlKey) {
|
|
case SDLK_l:
|
|
{
|
|
if (!player) break;
|
|
if (playerFrozen) break;
|
|
if (!inCar) {
|
|
const Eigen::Vector3f diff(
|
|
carPosition.x() - player->position.x(), 0.f,
|
|
carPosition.z() - player->position.z());
|
|
const float distToCar = diff.norm();
|
|
const float enterMaxDistance = 4.0f;
|
|
if (distToCar <= enterMaxDistance) {
|
|
inCar = true;
|
|
carVelocity = 0.f;
|
|
} else {
|
|
std::cout << "[CAR] Too far to enter: " << distToCar << std::endl;
|
|
}
|
|
} else {
|
|
const Eigen::Vector3f left(-std::cos(carRotation), 0.f, std::sin(carRotation));
|
|
const float halfWidth = 2.8f * 0.5f;
|
|
const float exitBuffers[] = { 1.0f, 1.5f, 2.0f, 3.0f };
|
|
Eigen::Vector3f exitPos = carPosition;
|
|
bool foundExit = false;
|
|
for (float buffer : exitBuffers) {
|
|
Eigen::Vector3f candidate = carPosition + left * (halfWidth + buffer);
|
|
candidate.y() = 0.f;
|
|
if (!navigation.isReady() || navigation.isWalkable(candidate)) {
|
|
exitPos = candidate;
|
|
foundExit = true;
|
|
break;
|
|
}
|
|
}
|
|
if (foundExit) {
|
|
player->position = exitPos;
|
|
player->clearPath();
|
|
inCar = false;
|
|
carVelocity = 0.f;
|
|
keyForward = false;
|
|
} else {
|
|
std::cout << "[CAR] No walkable exit spot" << std::endl;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case SDLK_w: keyForward = true; break;
|
|
case SDLK_s: keyBackward = true; break;
|
|
case SDLK_a: keyLeft = true; break;
|
|
case SDLK_d: keyRight = true; break;
|
|
case SDLK_i: invertCameraY = !invertCameraY; break;
|
|
case SDLK_u:
|
|
std::cout << player->position << std::endl;
|
|
//girlfriend->setTarget(player->position);
|
|
break;
|
|
case SDLK_m:
|
|
npcCar.mode = (npcCar.mode == NpcCar::Mode::FOLLOW_WAYPOINTS)
|
|
? NpcCar::Mode::FOLLOW_PLAYER
|
|
: NpcCar::Mode::FOLLOW_WAYPOINTS;
|
|
std::cout << "[NPC_CAR] Mode: "
|
|
<< (npcCar.mode == NpcCar::Mode::FOLLOW_WAYPOINTS ? "FOLLOW_WAYPOINTS" : "FOLLOW_PLAYER")
|
|
<< std::endl;
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
void Location::handleKeyUp(int sdlKey)
|
|
{
|
|
switch (sdlKey) {
|
|
case SDLK_w: keyForward = false; break;
|
|
case SDLK_s: keyBackward = false; break;
|
|
case SDLK_a: keyLeft = false; break;
|
|
case SDLK_d: keyRight = false; break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
void Location::cleanup()
|
|
{
|
|
std::cout << "[LOCATION] Starting cleanup..." << std::endl;
|
|
|
|
npcs.clear();
|
|
|
|
interactiveObjects.clear();
|
|
|
|
gameObjects.clear();
|
|
|
|
player.reset();
|
|
|
|
roomTexture.reset();
|
|
|
|
npcCar.texture.reset();
|
|
|
|
//dialogueSystem.dialogueDatabase.clear();
|
|
|
|
navigation = PathFinder();
|
|
|
|
cameraAzimuth = 0.0f;
|
|
cameraInclination = M_PI * 30.f / 180.f;
|
|
|
|
targetInteractiveObject = nullptr;
|
|
rightMouseDown = false;
|
|
lastMouseX = 0;
|
|
lastMouseY = 0;
|
|
|
|
dialoguePlayedOffroad = false;
|
|
dialoguePlayedCrash = false;
|
|
|
|
std::cout << "[LOCATION] Cleanup complete" << std::endl;
|
|
}
|
|
|
|
void Location::unload()
|
|
{
|
|
std::cout << "[LOCATION] Starting unload..." << std::endl;
|
|
cleanup();
|
|
std::cout << "[LOCATION] Unload complete" << std::endl;
|
|
}
|
|
|
|
|
|
} // namespace ZL
|