police chase

This commit is contained in:
Vladislav Khorev 2026-04-19 08:22:03 +03:00
parent 8e0936836e
commit 25bb79fb42
7 changed files with 203 additions and 14 deletions

View File

@ -273,7 +273,7 @@
"type": "Line", "type": "Line",
"speaker": "Hero", "speaker": "Hero",
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png", "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
"text": "Phone 1 У нас бензин кончается.", "text": "[Телефон звонит]",
"next": "line_2" "next": "line_2"
}, },
{ {
@ -281,7 +281,7 @@
"type": "Line", "type": "Line",
"speaker": "Hero", "speaker": "Hero",
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png", "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
"text": "Надо заправиться.", "text": "Да, слушаю.",
"next": "line_3" "next": "line_3"
}, },
{ {
@ -289,7 +289,7 @@
"type": "Line", "type": "Line",
"speaker": "Ghost", "speaker": "Ghost",
"portrait": "resources/ghost_avatar.png", "portrait": "resources/ghost_avatar.png",
"text": "Хорошо, только давай быстро.", "text": "Алексей, это Нурланбай на связи.",
"next": "line_4" "next": "line_4"
}, },
{ {
@ -297,7 +297,105 @@
"type": "Line", "type": "Line",
"speaker": "Ghost", "speaker": "Ghost",
"portrait": "resources/ghost_avatar.png", "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" "next": "end_1"
}, },
{ {

View File

@ -1001,14 +1001,20 @@ void Location::setup()
if (player) { if (player) {
if (!inCar) { if (!inCar) {
player->targetFacingAngle = cameraAzimuth; player->targetFacingAngle = cameraAzimuth;
if (keyForward) { if (playerFrozen) {
player->clearPath();
wasKeyForward = false;
} else if (keyForward) {
player->attackTarget = nullptr; player->attackTarget = nullptr;
Eigen::Vector3f forward(std::sin(cameraAzimuth), 0.f, -std::cos(cameraAzimuth)); Eigen::Vector3f forward(std::sin(cameraAzimuth), 0.f, -std::cos(cameraAzimuth));
player->setDirectWalkTarget(player->position + forward * 5.0f); player->setDirectWalkTarget(player->position + forward * 5.0f);
} else if (wasKeyForward) { wasKeyForward = true;
} else {
if (wasKeyForward) {
player->clearPath(); player->clearPath();
} }
wasKeyForward = keyForward; wasKeyForward = false;
}
} }
player->update(delta); player->update(delta);
dialogueSystem.update(static_cast<int>(delta), player->position); dialogueSystem.update(static_cast<int>(delta), player->position);
@ -1050,7 +1056,10 @@ void Location::setup()
} }
if (salesperson) pushOutOfNpcCarFootprint(salesperson->position); if (salesperson) pushOutOfNpcCarFootprint(salesperson->position);
if (police) pushOutOfNpcCarFootprint(police->position); if (police) {
police->update(delta);
pushOutOfNpcCarFootprint(police->position);
}
if (bandit) pushOutOfNpcCarFootprint(bandit->position); if (bandit) pushOutOfNpcCarFootprint(bandit->position);
for (auto& npc : npcs) { for (auto& npc : npcs) {
@ -1159,6 +1168,17 @@ void Location::setup()
{ {
policeFollow = true; policeFollow = true;
npcCar.mode = NpcCar::Mode::FOLLOW_PLAYER; 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;
}
}
} }
@ -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, // Phone rings once the player drives far from the gas station — fires once,
// and only after the gas-station sale. // and only after the gas-station sale.
if (inCar && dialoguePlayedGas1 && !dialoguePlayedPhone1 && !dialogueSystem.isActive()) { if (inCar && dialoguePlayedGas1 && !dialoguePlayedPhone1 && !dialogueSystem.isActive()) {
@ -1572,6 +1647,7 @@ void Location::setup()
case SDLK_l: case SDLK_l:
{ {
if (!player) break; if (!player) break;
if (playerFrozen) break;
if (!inCar) { if (!inCar) {
const Eigen::Vector3f diff( const Eigen::Vector3f diff(
carPosition.x() - player->position.x(), 0.f, carPosition.x() - player->position.x(), 0.f,

View File

@ -116,6 +116,11 @@ namespace ZL
bool policeFollow = false; bool policeFollow = false;
enum class PoliceEncounterStage { Idle, Approaching, Returning, Done };
PoliceEncounterStage policeEncounterStage = PoliceEncounterStage::Idle;
bool playerFrozen = false;
float policeDrivingDialogueTimer = 8.0f;
ScriptEngine scriptEngine; ScriptEngine scriptEngine;
Dialogue::DialogueSystem dialogueSystem; Dialogue::DialogueSystem dialogueSystem;

View File

@ -10,7 +10,7 @@ void DialogueRuntime::setDatabase(const DialogueDatabase* value) {
database = value; database = value;
} }
bool DialogueRuntime::startDialogue(const std::string& dialogueId) { bool DialogueRuntime::startDialogue(const std::string& dialogueId, std::function<void()> onFinished) {
if (!database) { if (!database) {
std::cerr << "[dialogue] No database assigned to runtime\n"; std::cerr << "[dialogue] No database assigned to runtime\n";
return false; return false;
@ -35,6 +35,7 @@ bool DialogueRuntime::startDialogue(const std::string& dialogueId) {
cutsceneTotalDurationMs = 0; cutsceneTotalDurationMs = 0;
presentation = {}; presentation = {};
presentation.dialogueId = dialogue->id; presentation.dialogueId = dialogue->id;
onFinishedCallback = std::move(onFinished);
return enterNode(dialogue->startNode); return enterNode(dialogue->startNode);
} }
@ -53,6 +54,12 @@ void DialogueRuntime::stop() {
cutsceneTotalDurationMs = 0; cutsceneTotalDurationMs = 0;
mode = Mode::Inactive; mode = Mode::Inactive;
presentation = {}; 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<void()> cb = std::move(onFinishedCallback);
onFinishedCallback = nullptr;
if (cb) cb();
} }
void DialogueRuntime::update(int deltaMs) { void DialogueRuntime::update(int deltaMs) {

View File

@ -2,6 +2,7 @@
#include "dialogue/DialogueDatabase.h" #include "dialogue/DialogueDatabase.h"
#include "external/nlohmann/json.hpp" #include "external/nlohmann/json.hpp"
#include <functional>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
@ -15,7 +16,7 @@ public:
void setDatabase(const DialogueDatabase* value); void setDatabase(const DialogueDatabase* value);
bool startDialogue(const std::string& dialogueId); bool startDialogue(const std::string& dialogueId, std::function<void()> onFinished = nullptr);
void stop(); void stop();
void update(int deltaMs); void update(int deltaMs);
@ -50,6 +51,7 @@ private:
std::unordered_map<std::string, int> flags; std::unordered_map<std::string, int> flags;
std::unordered_set<std::string> consumedChoices; std::unordered_set<std::string> consumedChoices;
std::function<void()> onFinishedCallback;
std::string currentNodeId; std::string currentNodeId;
std::string pendingNodeAfterCutscene; std::string pendingNodeAfterCutscene;

View File

@ -106,8 +106,8 @@ bool DialogueSystem::handlePointerReleased(float x, float y) {
return true; return true;
} }
bool DialogueSystem::startDialogue(const std::string& dialogueId) { bool DialogueSystem::startDialogue(const std::string& dialogueId, std::function<void()> onFinished) {
return runtime.startDialogue(dialogueId); return runtime.startDialogue(dialogueId, std::move(onFinished));
} }
void DialogueSystem::stopDialogue() { void DialogueSystem::stopDialogue() {

View File

@ -4,6 +4,7 @@
#include "dialogue/DialogueRuntime.h" #include "dialogue/DialogueRuntime.h"
#include <Eigen/Dense> #include <Eigen/Dense>
#include <SDL.h> #include <SDL.h>
#include <functional>
#include <string> #include <string>
#include <vector> #include <vector>
@ -31,7 +32,7 @@ public:
void handlePointerMoved(float x, float y); void handlePointerMoved(float x, float y);
bool handlePointerReleased(float x, float y); bool handlePointerReleased(float x, float y);
bool startDialogue(const std::string& dialogueId); bool startDialogue(const std::string& dialogueId, std::function<void()> onFinished = nullptr);
void stopDialogue(); void stopDialogue();
bool isActive() const { return runtime.isActive(); } bool isActive() const { return runtime.isActive(); }