Working on teleports

This commit is contained in:
Vladislav Khorev 2026-05-07 20:53:20 +03:00
parent 7e94009217
commit 8214342248
15 changed files with 298 additions and 131 deletions

View File

@ -83,6 +83,8 @@ set(SOURCES
../src/utils/Utils.h
../src/SparkEmitter.cpp
../src/SparkEmitter.h
../src/TeleportZone.h
../src/TeleportZone.cpp
../src/utils/TaskManager.cpp
../src/utils/TaskManager.h
../src/render/FrameBuffer.cpp

View File

@ -40,6 +40,8 @@ add_executable(witcher001
../src/utils/Utils.h
../src/SparkEmitter.cpp
../src/SparkEmitter.h
../src/TeleportZone.h
../src/TeleportZone.cpp
../src/utils/TaskManager.cpp
../src/utils/TaskManager.h
../src/render/FrameBuffer.cpp

View File

@ -0,0 +1,17 @@
{
"teleports": [
{
"id": "tp_loc1_to_loc2",
"positionX": 2.64621,
"positionY": 0.0,
"positionZ": -9.37259,
"radius": 1.5,
"active": true,
"destinationLocation": "location2",
"destinationPositionX": 8.2,
"destinationPositionY": 0.0,
"destinationPositionZ": -9.9,
"destinationRotationY": 0.0
}
]
}

View File

@ -0,0 +1,30 @@
{
"teleports": [
{
"id": "tp_loc2_to_loc1",
"positionX": 8.2,
"positionY": 0.0,
"positionZ": -9.9,
"radius": 1.5,
"active": true,
"destinationLocation": "location1",
"destinationPositionX": 2.64621,
"destinationPositionY": 0.0,
"destinationPositionZ": -9.37259,
"destinationRotationY": -1.5708
},
{
"id": "tp_loc2_to_dorm",
"positionX": -21.7327,
"positionY": 0.0,
"positionZ": -34.1036,
"radius": 2.5,
"active": true,
"destinationLocation": "location_dorm",
"destinationPositionX": -8.32343,
"destinationPositionY": 0.0,
"destinationPositionZ": -0.152264,
"destinationRotationY": 1.5708
}
]
}

View File

@ -0,0 +1,30 @@
{
"teleports": [
{
"id": "tp_dorm_to_loc2",
"positionX": -8.32343,
"positionY": 0.0,
"positionZ": -0.152264,
"radius": 1.5,
"active": true,
"destinationLocation": "location2",
"destinationPositionX": -21.7327,
"destinationPositionY": 0.0,
"destinationPositionZ": -34.1036,
"destinationRotationY": 3.14159
},
{
"id": "tp_dorm_to_2floor",
"positionX": 3.95908,
"positionY": 0.0,
"positionZ": -3.06444,
"radius": 1.5,
"active": false,
"destinationLocation": "NONE",
"destinationPositionX": 0.0,
"destinationPositionY": 0.0,
"destinationPositionZ": 0.0,
"destinationRotationY": 0.0
}
]
}

View File

@ -189,7 +189,7 @@
"id": "line_goods",
"type": "Line",
"speaker": "Беспокойный Призрак",
"portrait": "resources/avatar_ghost.png",
"portrait": "resources/w/avatar_ghost.png",
"text": "Это моя месть студентам за то что они призвали меня.",
"next": "end_1"
},
@ -197,7 +197,7 @@
"id": "line_who",
"type": "Line",
"speaker": "Беспокойный Призрак",
"portrait": "resources/avatar_ghost.png",
"portrait": "resources/w/avatar_ghost.png",
"text": "Группа студентов совершила ритуал и призвала меня сюда. Пока проклятие не спадет, я всегда буду здесь обитать.",
"next": "choice_1"
},
@ -375,7 +375,7 @@
},
{
"speaker": "Ghost",
"portrait": "resources/ghost_avatar.png",
"portrait": "resources/w/avatar_ghost.png",
"text": "Some memories never fade.",
"durationMs": 2600,
"background": "resources/loading.png"

BIN
resources/ghost_avatar.png (Stored with Git LFS)

Binary file not shown.

BIN
resources/w/star_red.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -96,6 +96,8 @@ public:
// Public: read by Game for camera tracking and ray-cast origin
Eigen::Vector3f position = Eigen::Vector3f(0.f, 0.f, 0.f);
float facingAngle = 0.0f;
float targetFacingAngle = 0.0f;
// Per-character tuning — set after construction, before first update
float walkSpeed = 3.0f;
@ -159,7 +161,6 @@ private:
std::vector<Eigen::Vector3f> pathWaypoints;
size_t currentWaypointIndex = 0;
PathPlanner pathPlanner;
float targetFacingAngle = 0.0f;
std::function<void()> onArrivedCallback;
static constexpr float WALK_THRESHOLD = 0.05f;

View File

@ -165,27 +165,25 @@ namespace ZL
params1.npcsJsonPath = "resources/config2/npcs.json";
params1.dialoguesJsonPath = "resources/dialogue/sample_dialogues.json";
params1.navigationJsonPath = "resources/config2/navigation.json";
params1.teleportsJsonPath = "resources/config2/teleports.json";
params1.scriptPath = "resources/start.lua";
params1.playerPosition = Eigen::Vector3f(0.942694, 0, -9.63104);
params1.teleportPosition = Eigen::Vector3f(2.64621, 0, -9.37259);
params1.teleportRadius = 1.5f;
location1 = std::make_shared<Location>(renderer, inventory);
location1->setup(params1);
locations["location1"] = std::make_shared<Location>(renderer, inventory);
locations["location1"]->setup(params1);
LocationSetup params2 = params1;
params2.roomMeshPath = "resources/w/exterior/Segmented_Plane002.txt";
params2.roomTexturePath = "resources/w/exterior/Segmented_Plane002.png";
params2.gameObjectsJsonPath = "resources/config2/gameobjects2.json";
params2.navigationJsonPath = "resources/config2/navigation2.json";
params2.teleportsJsonPath = "resources/config2/teleports2.json";
params2.scriptPath = "resources/start2.lua";
params2.playerPosition = Eigen::Vector3f(5, 0, -18.4);
params2.npcsJsonPath = "resources/config2/npcs2.json";
params2.teleportPosition = Eigen::Vector3f(8.2, 0, -9.9);
params2.teleportRadius = 1.5f;
location2 = std::make_shared<Location>(renderer, inventory);
location2->setup(params2);
locations["location2"] = std::make_shared<Location>(renderer, inventory);
locations["location2"]->setup(params2);
LocationSetup params_dorm;
params_dorm.roomMeshPath = "";
@ -194,48 +192,38 @@ namespace ZL
params_dorm.npcsJsonPath = "resources/config2/npcs_dorm.json";
params_dorm.dialoguesJsonPath = "resources/dialogue/sample_dialogues.json";
params_dorm.navigationJsonPath = "resources/config2/navigation_dorm.json";
params_dorm.teleportsJsonPath = "resources/config2/teleports_dorm.json";
params_dorm.scriptPath = "resources/start_dorm.lua";
params_dorm.playerPosition = Eigen::Vector3f(6.0357, 0, -16.0581);
params_dorm.teleportPosition = Eigen::Vector3f(-8.32343, 0, -0.152264);
params_dorm.teleportRadius = 1.5f;
location_dorm = std::make_shared<Location>(renderer, inventory);
location_dorm->setup(params_dorm);
locations["location_dorm"] = std::make_shared<Location>(renderer, inventory);
locations["location_dorm"]->setup(params_dorm);
// Teleport callbacks: stepping into a location's zone hands the player
// to the other location at *its* teleport position. We pre-arm the
// destination's playerInTeleportZone so the player must walk out and
// back in before they can teleport again.
location1->onTeleport = [this]() {
std::cout << "[TELEPORT] location 1 -> location 2" << std::endl;
currentLocation = location2;
if (currentLocation->player) {
currentLocation->player->position = currentLocation->teleportPosition;
currentLocation->player->setTarget(currentLocation->teleportPosition);
}
currentLocation->playerInTeleportZone = true;
};
location2->onTeleport = [this]() {
std::cout << "[TELEPORT] location 2 -> location 1" << std::endl;
currentLocation = location1;
if (currentLocation->player) {
currentLocation->player->position = currentLocation->teleportPosition;
currentLocation->player->setTarget(currentLocation->teleportPosition);
}
currentLocation->playerInTeleportZone = true;
// Teleport callbacks: destination name and position come from the teleport zone data.
auto makeTeleportCallback = [this](const std::string& sourceName) {
return [this, sourceName](const std::string& destName, const Eigen::Vector3f& destPos, float destRotY) {
std::cout << "[TELEPORT] " << sourceName << " -> " << destName << std::endl;
auto it = locations.find(destName);
if (it == locations.end()) {
std::cerr << "[TELEPORT] Unknown destination location: " << destName << std::endl;
return;
}
currentLocation = it->second;
if (currentLocation->player) {
currentLocation->player->position = destPos;
currentLocation->player->setTarget(destPos);
currentLocation->player->facingAngle = destRotY;
currentLocation->player->targetFacingAngle = destRotY;
}
currentLocation->cameraAzimuth = destRotY;
};
};
location_dorm->onTeleport = [this]() {
std::cout << "[TELEPORT] location 2 -> location 1" << std::endl;
currentLocation = location2;
if (currentLocation->player) {
currentLocation->player->position = currentLocation->teleportPosition;
currentLocation->player->setTarget(currentLocation->teleportPosition);
}
currentLocation->playerInTeleportZone = true;
};
locations["location1"]->onTeleport = makeTeleportCallback("location1");
locations["location2"]->onTeleport = makeTeleportCallback("location2");
locations["location_dorm"]->onTeleport = makeTeleportCallback("location_dorm");
currentLocation = location_dorm;
currentLocation = locations["location_dorm"];
std::cout << "Load resurces step 5" << std::endl;
@ -540,8 +528,8 @@ namespace ZL
break;
case SDLK_p:
currentLocation = (currentLocation == location1) ? location2 : location1;
std::cout << "Switched to location " << ((currentLocation == location1) ? "1" : "2") << std::endl;
currentLocation = (currentLocation == locations["location1"]) ? locations["location2"] : locations["location1"];
std::cout << "Switched to location " << ((currentLocation == locations["location1"]) ? "1" : "2") << std::endl;
break;
case SDLK_l:

View File

@ -42,10 +42,7 @@ namespace ZL {
VertexRenderStruct loadingMesh;
bool loadingCompleted = false;
std::shared_ptr<Location> location1;
std::shared_ptr<Location> location2;
std::shared_ptr<Location> location_dorm;
std::unordered_map<std::string, std::shared_ptr<Location>> locations;
std::shared_ptr<Location> currentLocation;
Inventory inventory;

View File

@ -12,6 +12,7 @@
#include <cfloat>
#include "GameConstants.h"
#include "Character.h"
#include "external/nlohmann/json.hpp"
namespace ZL
@ -133,38 +134,7 @@ namespace ZL
}
}
auto sparkTexture2 = renderer.textureManager.LoadFromPng("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;
}
}
loadTeleportZones(params.teleportsJsonPath, CONST_ZIP_FILE);
#ifndef EMSCRIPTEN
// Create shadow map (2048x2048, ortho size 40, near 0.1, far 100)
@ -198,6 +168,65 @@ namespace ZL
}
void Location::loadTeleportZones(const std::string& jsonPath, const char* zipFile)
{
if (jsonPath.empty()) return;
std::string content;
try {
if (!zipFile || zipFile[0] == '\0') {
content = readTextFile(jsonPath);
} else {
auto buf = readFileFromZIP(jsonPath, zipFile);
if (buf.empty()) {
std::cerr << "[TELEPORT] Failed to read " << jsonPath << " from zip" << std::endl;
return;
}
content.assign(buf.begin(), buf.end());
}
} catch (const std::exception& e) {
std::cerr << "[TELEPORT] Failed to open " << jsonPath << ": " << e.what() << std::endl;
return;
}
using json = nlohmann::json;
json j;
try {
j = json::parse(content);
} catch (const std::exception& e) {
std::cerr << "[TELEPORT] JSON parse error in " << jsonPath << ": " << e.what() << std::endl;
return;
}
if (!j.contains("teleports") || !j["teleports"].is_array()) return;
auto activeTex = renderer.textureManager.LoadFromPng("resources/w/star.png", zipFile ? zipFile : "");
auto inactiveTex = renderer.textureManager.LoadFromPng("resources/w/star_red.png", zipFile ? zipFile : "");
for (const auto& item : j["teleports"]) {
TeleportZone tz;
tz.id = item.value("id", "");
tz.active = item.value("active", false);
tz.position = Eigen::Vector3f(
item.value("positionX", 0.0f),
item.value("positionY", 0.0f),
item.value("positionZ", 0.0f)
);
tz.radius = item.value("radius", 0.0f);
tz.destinationLocation = item.value("destinationLocation", "");
tz.destinationPosition = Eigen::Vector3f(
item.value("destinationPositionX", 0.0f),
item.value("destinationPositionY", 0.0f),
item.value("destinationPositionZ", 0.0f)
);
tz.destinationRotationY = item.value("destinationRotationY", 0.0f);
tz.initSparks(activeTex, inactiveTex);
teleportZones.push_back(std::move(tz));
}
std::cout << "[TELEPORT] Loaded " << teleportZones.size() << " teleport(s) from " << jsonPath << std::endl;
}
void Location::setupNavigation(const std::string& navigationJsonPath)
{
// Static navigation blockers are defined in the navigation JSON as polygons.
@ -447,14 +476,12 @@ namespace ZL
const Eigen::Matrix4f currentView = renderer.GetCurrentModelViewMatrix();
if (player) player->prepareHitSparksForDraw(currentView);
for (auto& npc : npcs) npc->prepareHitSparksForDraw(currentView);
if (teleportSparks) teleportSparks->prepareForDraw(currentView);
for (auto& tz : teleportZones) tz.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);
}
for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height);
#ifdef SHOW_PATH
drawDebugNavigation();
@ -641,16 +668,14 @@ namespace ZL
if (player) player->prepareHitSparksForDraw(cameraViewMatrix);
for (auto& npc : npcs) npc->prepareHitSparksForDraw(cameraViewMatrix);
if (teleportSparks) teleportSparks->prepareForDraw(cameraViewMatrix);
for (auto& tz : teleportZones) tz.prepareForDraw(cameraViewMatrix);
if (player) player->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
CheckGlError(__FILE__, __LINE__);
for (auto& npc : npcs) npc->drawWithShadow(renderer, lightFromCamera, shadowMap->getDepthTexture(), lightDirCamera);
if (teleportSparks) {
teleportSparks->draw(renderer, Environment::zoom, Environment::width, Environment::height);
}
for (auto& tz : teleportZones) tz.draw(renderer, Environment::zoom, Environment::width, Environment::height);
#endif
CheckGlError(__FILE__, __LINE__);
@ -889,26 +914,15 @@ namespace ZL
}
}
// Drive teleport spark animation regardless of whether the zone is armed.
if (teleportSparks) {
teleportSparks->update(static_cast<float>(delta));
}
for (auto& tz : teleportZones) tz.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;
if (targetTeleportZone && player) {
float dist = (player->position - targetTeleportZone->position).norm();
if (dist <= targetTeleportZone->radius) {
std::cout << "[TELEPORT] Player reached teleport zone '" << targetTeleportZone->id << "'" << std::endl;
if (onTeleport) onTeleport(targetTeleportZone->destinationLocation, targetTeleportZone->destinationPosition, targetTeleportZone->destinationRotationY);
targetTeleportZone = nullptr;
return;
}
}
}
@ -955,6 +969,7 @@ namespace ZL
targetInteractiveObject = clickedObject;
targetInteractNpc = nullptr;
targetInteractNpcIndex = -1;
targetTeleportZone = nullptr;
player->setTarget(clickedObject->position);
player->attackTarget = nullptr;
std::cout << "[CLICK] Player moving to object..." << std::endl;
@ -974,6 +989,7 @@ namespace ZL
}
if (npcIndex != -1) {
targetInteractiveObject = nullptr;
targetTeleportZone = nullptr;
if (clickedNpc->canAttack) {
// Hostile NPC: combat logic walks the player in via attackTarget;
@ -1012,15 +1028,33 @@ namespace ZL
}
else if (rayDir.y() < -0.001f && player) {
// Otherwise, unproject click to ground plane for Viola's walk target
// Unproject click to ground plane for walk target / teleport detection
float t = -camPos.y() / rayDir.y();
Eigen::Vector3f hit = camPos + rayDir * t;
std::cout << "[CLICK] Clicked on ground at: (" << hit.x() << ", " << hit.z() << ")" << std::endl;
player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z()));
player->attackTarget = nullptr;
targetInteractNpc = nullptr;
targetInteractNpcIndex = -1;
TeleportZone* clickedTeleport = nullptr;
for (auto& tz : teleportZones) {
if (!tz.active) continue;
Eigen::Vector2f d(hit.x() - tz.position.x(), hit.z() - tz.position.z());
if (d.norm() <= tz.radius) { clickedTeleport = &tz; break; }
}
if (clickedTeleport) {
std::cout << "[CLICK] Clicked teleport zone '" << clickedTeleport->id << "'" << std::endl;
targetTeleportZone = clickedTeleport;
targetInteractiveObject = nullptr;
targetInteractNpc = nullptr;
targetInteractNpcIndex = -1;
player->setTarget(clickedTeleport->position);
player->attackTarget = nullptr;
} else {
player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z()));
player->attackTarget = nullptr;
targetInteractNpc = nullptr;
targetInteractNpcIndex = -1;
targetTeleportZone = nullptr;
}
}
else {
std::cout << "[CLICK] No valid target found" << std::endl;

View File

@ -12,6 +12,7 @@
#include "ScriptEngine.h"
#include "dialogue/DialogueSystem.h"
#include "SparkEmitter.h"
#include "TeleportZone.h"
#include <functional>
#include <cstdint>
#include <unordered_map>
@ -27,9 +28,8 @@ namespace ZL
std::string dialoguesJsonPath;
std::string navigationJsonPath;
std::string scriptPath;
std::string teleportsJsonPath;
Eigen::Vector3f playerPosition = Eigen::Vector3f::Zero();
Eigen::Vector3f teleportPosition = Eigen::Vector3f::Zero();
float teleportRadius = 0.0f;
};
class Location
@ -69,14 +69,9 @@ namespace ZL
ScriptEngine scriptEngine;
Dialogue::DialogueSystem dialogueSystem;
// Teleport zone: when the player crosses into the radius of teleportPosition,
// onTeleport() is invoked once. Re-triggering requires leaving the zone first
// (rising-edge detection via playerInTeleportZone).
Eigen::Vector3f teleportPosition = Eigen::Vector3f::Zero();
float teleportRadius = 0.0f;
bool playerInTeleportZone = false;
std::function<void()> onTeleport;
std::unique_ptr<SparkEmitter> teleportSparks;
std::vector<TeleportZone> teleportZones;
TeleportZone* targetTeleportZone = nullptr;
std::function<void(const std::string&, const Eigen::Vector3f&, float)> onTeleport;
#ifdef SHOW_PATH
std::vector<VertexRenderStruct> debugNavMeshes;
@ -118,6 +113,7 @@ namespace ZL
private:
void resolveCharacterCollisions();
void updateDynamicReplans(int64_t deltaMs);
void loadTeleportZones(const std::string& jsonPath, const char* zipFile);
std::unordered_map<Character*, Eigen::Vector3f> lastCharacterPositions;
std::unordered_map<Character*, int64_t> replanCooldownRemainingMs;

43
src/TeleportZone.cpp Normal file
View File

@ -0,0 +1,43 @@
#include "TeleportZone.h"
#include "render/Renderer.h"
#include "render/TextureManager.h"
namespace ZL {
void TeleportZone::initSparks(std::shared_ptr<Texture> activeTex, std::shared_ptr<Texture> inactiveTex)
{
sparks = std::make_unique<SparkEmitter>();
std::vector<Vector3f> emitPoints;
emitPoints.push_back(Vector3f{ position.x(), position.y(), position.z() });
sparks->setEmissionPoints(emitPoints);
sparks->setTexture(active ? activeTex : inactiveTex);
sparks->setEmissionRate(50.0f);
sparks->setMaxParticles(80);
sparks->setParticleSize(0.05f);
sparks->setBiasX(0.0f);
sparks->setEmissionDirection(Vector3f{ 0.0f, 1.0f, 0.0f });
sparks->setEmissionRadius(radius);
sparks->setSpeedRange(0.5f, 1.0f);
sparks->setZSpeedRange(0.5f, 1.5f);
sparks->setScaleRange(0.5f, 1.0f);
sparks->setLifeTimeRange(1500.0f, 2500.0f);
sparks->setUseWorldSpace(true);
sparks->markConfigured();
}
void TeleportZone::update(float deltaMs)
{
if (sparks) sparks->update(deltaMs);
}
void TeleportZone::prepareForDraw(const Eigen::Matrix4f& viewMatrix)
{
if (sparks) sparks->prepareForDraw(viewMatrix);
}
void TeleportZone::draw(Renderer& renderer, float zoom, int width, int height)
{
if (sparks) sparks->draw(renderer, zoom, width, height);
}
} // namespace ZL

27
src/TeleportZone.h Normal file
View File

@ -0,0 +1,27 @@
#pragma once
#include <string>
#include <memory>
#include <Eigen/Core>
#include "SparkEmitter.h"
namespace ZL {
class Renderer;
class Texture;
struct TeleportZone {
std::string id;
Eigen::Vector3f position = Eigen::Vector3f::Zero();
float radius = 0.0f;
bool active = false;
std::string destinationLocation;
Eigen::Vector3f destinationPosition = Eigen::Vector3f::Zero();
float destinationRotationY = 0.0f;
std::unique_ptr<SparkEmitter> sparks;
void initSparks(std::shared_ptr<Texture> activeTex, std::shared_ptr<Texture> inactiveTex);
void update(float deltaMs);
void prepareForDraw(const Eigen::Matrix4f& viewMatrix);
void draw(Renderer& renderer, float zoom, int width, int height);
};
} // namespace ZL