From 25bb79fb42142dfd82c7d6c47bb356cfb7d13c9d Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Sun, 19 Apr 2026 08:22:03 +0300 Subject: [PATCH] police chase --- resources/dialogue/sample_dialogues.json | 106 ++++++++++++++++++++++- src/Location.cpp | 86 ++++++++++++++++-- src/Location.h | 5 ++ src/dialogue/DialogueRuntime.cpp | 9 +- src/dialogue/DialogueRuntime.h | 4 +- src/dialogue/DialogueSystem.cpp | 4 +- src/dialogue/DialogueSystem.h | 3 +- 7 files changed, 203 insertions(+), 14 deletions(-) diff --git a/resources/dialogue/sample_dialogues.json b/resources/dialogue/sample_dialogues.json index 4552d48..1c36bf8 100644 --- a/resources/dialogue/sample_dialogues.json +++ b/resources/dialogue/sample_dialogues.json @@ -273,7 +273,7 @@ "type": "Line", "speaker": "Hero", "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png", - "text": "Phone 1 У нас бензин кончается.", + "text": "[Телефон звонит]", "next": "line_2" }, { @@ -281,7 +281,7 @@ "type": "Line", "speaker": "Hero", "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png", - "text": "Надо заправиться.", + "text": "Да, слушаю.", "next": "line_3" }, { @@ -289,7 +289,7 @@ "type": "Line", "speaker": "Ghost", "portrait": "resources/ghost_avatar.png", - "text": "Хорошо, только давай быстро.", + "text": "Алексей, это Нурланбай на связи.", "next": "line_4" }, { @@ -297,7 +297,105 @@ "type": "Line", "speaker": "Ghost", "portrait": "resources/ghost_avatar.png", - "text": "Мне как-то не по себе.", + "text": "Мои ребята сообщили, что Алтынай видели на заправке с тобой.", + "next": "line_5" + }, + { + "id": "line_5", + "type": "Line", + "speaker": "Ghost", + "portrait": "resources/ghost_avatar.png", + "text": "Как вы узнали мой номер?", + "next": "line_6" + }, { + "id": "line_6", + "type": "Line", + "speaker": "Ghost", + "portrait": "resources/ghost_avatar.png", + "text": "О, это было нетрудно. У тебя слишком заметная машина.", + "next": "line_7" + }, { + "id": "line_7", + "type": "Line", + "speaker": "Ghost", + "portrait": "resources/ghost_avatar.png", + "text": "Мои ребята уже поехали за тобой.", + "next": "line_8" + }, { + "id": "line_8", + "type": "Line", + "speaker": "Ghost", + "portrait": "resources/ghost_avatar.png", + "text": "Предлагаю тебе не усложнять ничего.", + "next": "line_9" + }, { + "id": "line_9", + "type": "Line", + "speaker": "Ghost", + "portrait": "resources/ghost_avatar.png", + "text": "Остановись на трассе, отдай нам Алтынай.", + "next": "line_10" + }, { + "id": "line_10", + "type": "Line", + "speaker": "Ghost", + "portrait": "resources/ghost_avatar.png", + "text": "И можешь ехать дальше спокойно.", + "next": "line_11" + }, { + "id": "line_11", + "type": "Line", + "speaker": "Ghost", + "portrait": "resources/ghost_avatar.png", + "text": "Никогда!", + "next": "line_12" + },{ + "id": "line_12", + "type": "Line", + "speaker": "Ghost", + "portrait": "resources/ghost_avatar.png", + "text": "Какой ты смелый парень.", + "next": "line_13" + },{ + "id": "line_13", + "type": "Line", + "speaker": "Ghost", + "portrait": "resources/ghost_avatar.png", + "text": "Ну ничего, скоро увидимся. Давай, бывай.", + "next": "line_14" + }, + { + "id": "line_14", + "type": "Line", + "speaker": "Ghost", + "portrait": "resources/ghost_avatar.png", + "text": "[Гудки]", + "next": "end_1" + }, + { + "id": "end_1", + "type": "End" + } + ] + }, + { + "id": "dialogue_police1", + "start": "line_1", + "nodes": [ + { + "id": "line_1", + "type": "Line", + "speaker": "Ghost", + "portrait": "resources/ghost_avatar.png", + "text": "Старший лейтенант Каримов, отдел милиции Чуйской области.", + "next": "line_2" + }, + { + "id": "line_2", + "type": "Line", + "speaker": "Hero", + "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png", + "text": "Я задержан?", "next": "end_1" }, { diff --git a/src/Location.cpp b/src/Location.cpp index 7de726d..7f15a05 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -1001,14 +1001,20 @@ void Location::setup() if (player) { if (!inCar) { player->targetFacingAngle = cameraAzimuth; - if (keyForward) { + 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); - } else if (wasKeyForward) { - player->clearPath(); + wasKeyForward = true; + } else { + if (wasKeyForward) { + player->clearPath(); + } + wasKeyForward = false; } - wasKeyForward = keyForward; } player->update(delta); dialogueSystem.update(static_cast(delta), player->position); @@ -1050,7 +1056,10 @@ void Location::setup() } if (salesperson) pushOutOfNpcCarFootprint(salesperson->position); - if (police) pushOutOfNpcCarFootprint(police->position); + if (police) { + police->update(delta); + pushOutOfNpcCarFootprint(police->position); + } if (bandit) pushOutOfNpcCarFootprint(bandit->position); for (auto& npc : npcs) { @@ -1159,6 +1168,17 @@ void Location::setup() { 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(delta) / 1000.f; + if (policeDrivingDialogueTimer <= 0.f && !dialogueSystem.isActive()) { + if (dialogueSystem.startDialogue("driving_dialogue1")) { + policeDrivingDialogueTimer = 8.0f; + } + } } @@ -1237,6 +1257,61 @@ void Location::setup() } } + // 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; + 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()) { @@ -1572,6 +1647,7 @@ void Location::setup() case SDLK_l: { if (!player) break; + if (playerFrozen) break; if (!inCar) { const Eigen::Vector3f diff( carPosition.x() - player->position.x(), 0.f, diff --git a/src/Location.h b/src/Location.h index 0aff772..3e2923d 100644 --- a/src/Location.h +++ b/src/Location.h @@ -116,6 +116,11 @@ namespace ZL bool policeFollow = false; + enum class PoliceEncounterStage { Idle, Approaching, Returning, Done }; + PoliceEncounterStage policeEncounterStage = PoliceEncounterStage::Idle; + bool playerFrozen = false; + float policeDrivingDialogueTimer = 8.0f; + ScriptEngine scriptEngine; Dialogue::DialogueSystem dialogueSystem; diff --git a/src/dialogue/DialogueRuntime.cpp b/src/dialogue/DialogueRuntime.cpp index 5f0fcbd..c958b81 100644 --- a/src/dialogue/DialogueRuntime.cpp +++ b/src/dialogue/DialogueRuntime.cpp @@ -10,7 +10,7 @@ void DialogueRuntime::setDatabase(const DialogueDatabase* value) { database = value; } -bool DialogueRuntime::startDialogue(const std::string& dialogueId) { +bool DialogueRuntime::startDialogue(const std::string& dialogueId, std::function onFinished) { if (!database) { std::cerr << "[dialogue] No database assigned to runtime\n"; return false; @@ -35,6 +35,7 @@ bool DialogueRuntime::startDialogue(const std::string& dialogueId) { cutsceneTotalDurationMs = 0; presentation = {}; presentation.dialogueId = dialogue->id; + onFinishedCallback = std::move(onFinished); return enterNode(dialogue->startNode); } @@ -53,6 +54,12 @@ void DialogueRuntime::stop() { cutsceneTotalDurationMs = 0; mode = Mode::Inactive; presentation = {}; + + // Move the callback out before firing so it can safely start a new dialogue + // (which would otherwise overwrite the member mid-call). + std::function cb = std::move(onFinishedCallback); + onFinishedCallback = nullptr; + if (cb) cb(); } void DialogueRuntime::update(int deltaMs) { diff --git a/src/dialogue/DialogueRuntime.h b/src/dialogue/DialogueRuntime.h index 6021df8..42a05e2 100644 --- a/src/dialogue/DialogueRuntime.h +++ b/src/dialogue/DialogueRuntime.h @@ -2,6 +2,7 @@ #include "dialogue/DialogueDatabase.h" #include "external/nlohmann/json.hpp" +#include #include #include #include @@ -15,7 +16,7 @@ public: void setDatabase(const DialogueDatabase* value); - bool startDialogue(const std::string& dialogueId); + bool startDialogue(const std::string& dialogueId, std::function onFinished = nullptr); void stop(); void update(int deltaMs); @@ -50,6 +51,7 @@ private: std::unordered_map flags; std::unordered_set consumedChoices; + std::function onFinishedCallback; std::string currentNodeId; std::string pendingNodeAfterCutscene; diff --git a/src/dialogue/DialogueSystem.cpp b/src/dialogue/DialogueSystem.cpp index 38269b8..0ea5ccf 100644 --- a/src/dialogue/DialogueSystem.cpp +++ b/src/dialogue/DialogueSystem.cpp @@ -106,8 +106,8 @@ bool DialogueSystem::handlePointerReleased(float x, float y) { return true; } -bool DialogueSystem::startDialogue(const std::string& dialogueId) { - return runtime.startDialogue(dialogueId); +bool DialogueSystem::startDialogue(const std::string& dialogueId, std::function onFinished) { + return runtime.startDialogue(dialogueId, std::move(onFinished)); } void DialogueSystem::stopDialogue() { diff --git a/src/dialogue/DialogueSystem.h b/src/dialogue/DialogueSystem.h index 1756272..5489191 100644 --- a/src/dialogue/DialogueSystem.h +++ b/src/dialogue/DialogueSystem.h @@ -4,6 +4,7 @@ #include "dialogue/DialogueRuntime.h" #include #include +#include #include #include @@ -31,7 +32,7 @@ public: void handlePointerMoved(float x, float y); bool handlePointerReleased(float x, float y); - bool startDialogue(const std::string& dialogueId); + bool startDialogue(const std::string& dialogueId, std::function onFinished = nullptr); void stopDialogue(); bool isActive() const { return runtime.isActive(); }