Compare commits
No commits in common. "6465549c3bedc317ae25b337e15339ccfe2b6a87" and "178f69967b6b6a0bcfea3a403f163d3c36d11fb0" have entirely different histories.
6465549c3b
...
178f69967b
@ -73,15 +73,6 @@ add_executable(space-game001
|
||||
../src/GameConstants.cpp
|
||||
../src/ScriptEngine.h
|
||||
../src/ScriptEngine.cpp
|
||||
../src/dialogue/DialogueTypes.h
|
||||
../src/dialogue/DialogueDatabase.h
|
||||
../src/dialogue/DialogueDatabase.cpp
|
||||
../src/dialogue/DialogueRuntime.h
|
||||
../src/dialogue/DialogueRuntime.cpp
|
||||
../src/dialogue/DialogueOverlay.h
|
||||
../src/dialogue/DialogueOverlay.cpp
|
||||
../src/dialogue/DialogueSystem.h
|
||||
../src/dialogue/DialogueSystem.cpp
|
||||
)
|
||||
|
||||
# Установка проекта по умолчанию для Visual Studio
|
||||
|
||||
BIN
resources/dialogue/choice_main.png
(Stored with Git LFS)
BIN
resources/dialogue/choice_main.png
(Stored with Git LFS)
Binary file not shown.
BIN
resources/dialogue/choice_optional.png
(Stored with Git LFS)
BIN
resources/dialogue/choice_optional.png
(Stored with Git LFS)
Binary file not shown.
BIN
resources/dialogue/choice_selected.png
(Stored with Git LFS)
BIN
resources/dialogue/choice_selected.png
(Stored with Git LFS)
Binary file not shown.
BIN
resources/dialogue/cutscene_subtitle_bg.png
(Stored with Git LFS)
BIN
resources/dialogue/cutscene_subtitle_bg.png
(Stored with Git LFS)
Binary file not shown.
BIN
resources/dialogue/portrait_frame.png
(Stored with Git LFS)
BIN
resources/dialogue/portrait_frame.png
(Stored with Git LFS)
Binary file not shown.
@ -1,168 +0,0 @@
|
||||
{
|
||||
"dialogues": [
|
||||
{
|
||||
"id": "test_line_dialogue",
|
||||
"start": "line_1",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "line_1",
|
||||
"type": "Line",
|
||||
"speaker": "Ghost",
|
||||
"portrait": "resources/ghost_avatar.png",
|
||||
"text": "You finally came here.",
|
||||
"next": "line_2"
|
||||
},
|
||||
{
|
||||
"id": "line_2",
|
||||
"type": "Line",
|
||||
"speaker": "Hero",
|
||||
"portrait": "",
|
||||
"text": "I need answers.",
|
||||
"next": "end_1"
|
||||
},
|
||||
{
|
||||
"id": "end_1",
|
||||
"type": "End"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "test_choice_dialogue",
|
||||
"start": "line_1",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "line_1",
|
||||
"type": "Line",
|
||||
"speaker": "Merchant",
|
||||
"portrait": "resources/ghost_avatar.png",
|
||||
"text": "What do you want?",
|
||||
"next": "choice_1"
|
||||
},
|
||||
{
|
||||
"id": "choice_1",
|
||||
"type": "Choice",
|
||||
"speaker": "Hero",
|
||||
"portrait": "",
|
||||
"text": "Choose your answer.",
|
||||
"choices": [
|
||||
{
|
||||
"id": "main_1",
|
||||
"kind": "Main",
|
||||
"text": "Show me your goods.",
|
||||
"next": "line_goods"
|
||||
},
|
||||
{
|
||||
"id": "optional_1",
|
||||
"kind": "Optional",
|
||||
"text": "Who are you?",
|
||||
"next": "line_who"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "line_goods",
|
||||
"type": "Line",
|
||||
"speaker": "Merchant",
|
||||
"portrait": "resources/ghost_avatar.png",
|
||||
"text": "Take a look.",
|
||||
"next": "end_1"
|
||||
},
|
||||
{
|
||||
"id": "line_who",
|
||||
"type": "Line",
|
||||
"speaker": "Merchant",
|
||||
"portrait": "resources/ghost_avatar.png",
|
||||
"text": "Just a trader passing through.",
|
||||
"next": "end_1"
|
||||
},
|
||||
{
|
||||
"id": "end_1",
|
||||
"type": "End"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "test_condition_dialogue",
|
||||
"start": "set_flag_1",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "set_flag_1",
|
||||
"type": "SetFlag",
|
||||
"effects": [
|
||||
{ "flag": "met_ghost", "value": 1 }
|
||||
],
|
||||
"next": "condition_1"
|
||||
},
|
||||
{
|
||||
"id": "condition_1",
|
||||
"type": "Condition",
|
||||
"conditions": [
|
||||
{ "flag": "met_ghost", "op": "Equals", "value": 1 }
|
||||
],
|
||||
"trueNext": "line_true",
|
||||
"falseNext": "line_false"
|
||||
},
|
||||
{
|
||||
"id": "line_true",
|
||||
"type": "Line",
|
||||
"speaker": "Ghost",
|
||||
"portrait": "resources/ghost_avatar.png",
|
||||
"text": "Now you know who I am.",
|
||||
"next": "end_1"
|
||||
},
|
||||
{
|
||||
"id": "line_false",
|
||||
"type": "Line",
|
||||
"speaker": "Ghost",
|
||||
"portrait": "resources/ghost_avatar.png",
|
||||
"text": "You should not hear this line.",
|
||||
"next": "end_1"
|
||||
},
|
||||
{
|
||||
"id": "end_1",
|
||||
"type": "End"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "test_cutscene_dialogue",
|
||||
"start": "cutscene_start",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "cutscene_start",
|
||||
"type": "CutsceneStart",
|
||||
"cutsceneId": "test_cutscene_01",
|
||||
"next": "end_1"
|
||||
},
|
||||
{
|
||||
"id": "end_1",
|
||||
"type": "End"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"cutscenes": [
|
||||
{
|
||||
"id": "test_cutscene_01",
|
||||
"background": "resources/first_cutscene.png",
|
||||
"lines": [
|
||||
{
|
||||
"speaker": "Narrator",
|
||||
"portrait": "",
|
||||
"text": "The air in the room turned cold.",
|
||||
"durationMs": 2200
|
||||
},
|
||||
{
|
||||
"speaker": "Ghost",
|
||||
"portrait": "resources/ghost_avatar.png",
|
||||
"text": "Some memories never fade.",
|
||||
"durationMs": 2600
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
resources/dialogue/textbox_bg.png
(Stored with Git LFS)
BIN
resources/dialogue/textbox_bg.png
(Stored with Git LFS)
Binary file not shown.
BIN
resources/first_cutscene.png
(Stored with Git LFS)
BIN
resources/first_cutscene.png
(Stored with Git LFS)
Binary file not shown.
BIN
resources/ghost_avatar.png
(Stored with Git LFS)
BIN
resources/ghost_avatar.png
(Stored with Git LFS)
Binary file not shown.
BIN
resources/portraits/elder_bor_neutral.png
(Stored with Git LFS)
BIN
resources/portraits/elder_bor_neutral.png
(Stored with Git LFS)
Binary file not shown.
67
src/Game.cpp
67
src/Game.cpp
@ -227,17 +227,6 @@ namespace ZL
|
||||
|
||||
loadingCompleted = true;
|
||||
|
||||
dialogueSystem.init(renderer, CONST_ZIP_FILE);
|
||||
dialogueSystem.loadDatabase("resources/dialogue/sample_dialogues.json");
|
||||
dialogueSystem.addTriggerZone({
|
||||
"ghost_room_trigger",
|
||||
"test_line_dialogue",
|
||||
Eigen::Vector3f(0.0f, 0.0f, -8.5f),
|
||||
2.0f,
|
||||
true,
|
||||
false
|
||||
});
|
||||
|
||||
scriptEngine.init(this);
|
||||
|
||||
std::cout << "Load resurces step 13" << std::endl;
|
||||
@ -248,22 +237,12 @@ namespace ZL
|
||||
{
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDepthMask(GL_FALSE);
|
||||
|
||||
renderer.shaderManager.PushShader(defaultShaderName);
|
||||
renderer.RenderUniform1i(textureUniformName, 0);
|
||||
glEnable(GL_BLEND);
|
||||
|
||||
menuManager.uiManager.draw(renderer);
|
||||
dialogueSystem.draw(renderer);
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
renderer.shaderManager.PopShader();
|
||||
|
||||
glDepthMask(GL_TRUE);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
|
||||
CheckGlError();
|
||||
}
|
||||
|
||||
@ -382,10 +361,7 @@ namespace ZL
|
||||
lastTickCount = newTickCount;
|
||||
|
||||
if (player) player->update(delta);
|
||||
//for (auto& npc : npcs) npc->update(delta);
|
||||
if (player) {
|
||||
dialogueSystem.update(static_cast<int>(delta), player->position);
|
||||
}
|
||||
for (auto& npc : npcs) npc->update(delta);
|
||||
}
|
||||
|
||||
|
||||
@ -474,11 +450,6 @@ namespace ZL
|
||||
if (event.type == SDL_MOUSEBUTTONDOWN) {
|
||||
handleDown(ZL::UiManager::MOUSE_FINGER_ID, mx, my);
|
||||
|
||||
if (dialogueSystem.blocksGameplayInput()) {
|
||||
dialogueSystem.handlePointerReleased(static_cast<float>(mx), Environment::projectionHeight - static_cast<float>(my));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unproject click to ground plane (y=0) for Viola's walk target
|
||||
float ndcX = 2.0f * event.button.x / Environment::width - 1.0f;
|
||||
float ndcY = 1.0f - 2.0f * event.button.y / Environment::height;
|
||||
@ -550,27 +521,6 @@ namespace ZL
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type == SDL_KEYDOWN && dialogueSystem.handleKeyDown(event.key.keysym.sym)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type == SDL_KEYDOWN && event.key.repeat == 0) {
|
||||
switch (event.key.keysym.sym) {
|
||||
case SDLK_f:
|
||||
dialogueSystem.startDialogue("test_line_dialogue");
|
||||
break;
|
||||
|
||||
case SDLK_e:
|
||||
dialogueSystem.startDialogue("test_cutscene_dialogue");
|
||||
break;
|
||||
|
||||
case SDLK_RETURN:
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка ввода текста
|
||||
if (event.type == SDL_KEYDOWN) {
|
||||
if (event.key.keysym.sym == SDLK_BACKSPACE) {
|
||||
@ -651,20 +601,5 @@ namespace ZL
|
||||
|
||||
}
|
||||
|
||||
bool Game::requestDialogueStart(const std::string& dialogueId)
|
||||
{
|
||||
return dialogueSystem.startDialogue(dialogueId);
|
||||
}
|
||||
|
||||
void Game::setDialogueFlag(const std::string& flag, int value)
|
||||
{
|
||||
dialogueSystem.setFlag(flag, value);
|
||||
}
|
||||
|
||||
int Game::getDialogueFlag(const std::string& flag) const
|
||||
{
|
||||
return dialogueSystem.getFlag(flag);
|
||||
}
|
||||
|
||||
|
||||
} // namespace ZL
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
#include <render/TextRenderer.h>
|
||||
#include "MenuManager.h"
|
||||
#include "ScriptEngine.h"
|
||||
#include "dialogue/DialogueSystem.h"
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
@ -34,9 +33,6 @@ namespace ZL {
|
||||
void render();
|
||||
|
||||
bool shouldExit() const { return Environment::exitGameLoop; }
|
||||
bool requestDialogueStart(const std::string& dialogueId);
|
||||
void setDialogueFlag(const std::string& flag, int value);
|
||||
int getDialogueFlag(const std::string& flag) const;
|
||||
|
||||
Renderer renderer;
|
||||
TaskManager taskManager;
|
||||
@ -98,7 +94,6 @@ namespace ZL {
|
||||
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
|
||||
|
||||
MenuManager menuManager;
|
||||
Dialogue::DialogueSystem dialogueSystem;
|
||||
ScriptEngine scriptEngine;
|
||||
};
|
||||
|
||||
|
||||
@ -47,23 +47,6 @@ void ScriptEngine::init(Game* game) {
|
||||
npcs[index]->setTarget(Eigen::Vector3f(x, y, z), std::move(cb));
|
||||
});
|
||||
|
||||
api.set_function("start_dialogue",
|
||||
[game](const std::string& dialogueId) {
|
||||
if (!game->requestDialogueStart(dialogueId)) {
|
||||
std::cerr << "[script] start_dialogue failed for id: " << dialogueId << "\n";
|
||||
}
|
||||
});
|
||||
|
||||
api.set_function("set_dialogue_flag",
|
||||
[game](const std::string& flag, int value) {
|
||||
game->setDialogueFlag(flag, value);
|
||||
});
|
||||
|
||||
api.set_function("get_dialogue_flag",
|
||||
[game](const std::string& flag) {
|
||||
return game->getDialogueFlag(flag);
|
||||
});
|
||||
|
||||
runScript("resources/start.lua");
|
||||
}
|
||||
|
||||
|
||||
@ -1,195 +0,0 @@
|
||||
#include "dialogue/DialogueDatabase.h"
|
||||
|
||||
#include "utils/Utils.h"
|
||||
#include <iostream>
|
||||
|
||||
namespace ZL::Dialogue {
|
||||
|
||||
NodeType DialogueDatabase::parseNodeType(const std::string& value) {
|
||||
if (value == "Choice") return NodeType::Choice;
|
||||
if (value == "Condition") return NodeType::Condition;
|
||||
if (value == "SetFlag") return NodeType::SetFlag;
|
||||
if (value == "Jump") return NodeType::Jump;
|
||||
if (value == "End") return NodeType::End;
|
||||
if (value == "CutsceneStart") return NodeType::CutsceneStart;
|
||||
return NodeType::Line;
|
||||
}
|
||||
|
||||
ChoiceKind DialogueDatabase::parseChoiceKind(const std::string& value) {
|
||||
if (value == "Optional") return ChoiceKind::Optional;
|
||||
if (value == "Exit") return ChoiceKind::Exit;
|
||||
return ChoiceKind::Main;
|
||||
}
|
||||
|
||||
ComparisonOp DialogueDatabase::parseComparisonOp(const std::string& value) {
|
||||
if (value == "==" || value == "Equals") return ComparisonOp::Equals;
|
||||
if (value == "!=" || value == "NotEquals") return ComparisonOp::NotEquals;
|
||||
if (value == ">=" || value == "GreaterOrEqual") return ComparisonOp::GreaterOrEqual;
|
||||
if (value == "<=" || value == "LessOrEqual") return ComparisonOp::LessOrEqual;
|
||||
return ComparisonOp::Exists;
|
||||
}
|
||||
|
||||
Condition DialogueDatabase::parseCondition(const json& j) {
|
||||
Condition c;
|
||||
c.flag = j.value("flag", "");
|
||||
c.op = parseComparisonOp(j.value("op", "Exists"));
|
||||
c.value = j.value("value", 1);
|
||||
return c;
|
||||
}
|
||||
|
||||
Effect DialogueDatabase::parseEffect(const json& j) {
|
||||
Effect e;
|
||||
e.flag = j.value("flag", "");
|
||||
e.value = j.value("value", 1);
|
||||
e.relative = j.value("relative", false);
|
||||
return e;
|
||||
}
|
||||
|
||||
Choice DialogueDatabase::parseChoice(const json& j) {
|
||||
Choice c;
|
||||
c.id = j.value("id", "");
|
||||
c.text = j.value("text", "");
|
||||
c.next = j.value("next", "");
|
||||
c.kind = parseChoiceKind(j.value("kind", "Main"));
|
||||
c.consumeOnce = j.value("consumeOnce", false);
|
||||
|
||||
if (j.contains("conditions") && j["conditions"].is_array()) {
|
||||
for (const auto& item : j["conditions"]) {
|
||||
c.conditions.push_back(parseCondition(item));
|
||||
}
|
||||
}
|
||||
if (j.contains("effects") && j["effects"].is_array()) {
|
||||
for (const auto& item : j["effects"]) {
|
||||
c.effects.push_back(parseEffect(item));
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
Node DialogueDatabase::parseNode(const json& j) {
|
||||
Node node;
|
||||
node.id = j.value("id", "");
|
||||
node.type = parseNodeType(j.value("type", "Line"));
|
||||
node.speaker = j.value("speaker", "");
|
||||
node.text = j.value("text", "");
|
||||
node.portrait = j.value("portrait", "");
|
||||
node.next = j.value("next", "");
|
||||
node.trueNext = j.value("trueNext", "");
|
||||
node.falseNext = j.value("falseNext", "");
|
||||
node.cutsceneId = j.value("cutsceneId", "");
|
||||
|
||||
if (j.contains("conditions") && j["conditions"].is_array()) {
|
||||
for (const auto& item : j["conditions"]) {
|
||||
node.conditions.push_back(parseCondition(item));
|
||||
}
|
||||
}
|
||||
if (j.contains("effects") && j["effects"].is_array()) {
|
||||
for (const auto& item : j["effects"]) {
|
||||
node.effects.push_back(parseEffect(item));
|
||||
}
|
||||
}
|
||||
if (j.contains("choices") && j["choices"].is_array()) {
|
||||
for (const auto& item : j["choices"]) {
|
||||
node.choices.push_back(parseChoice(item));
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
DialogueDefinition DialogueDatabase::parseDialogue(const json& j) {
|
||||
DialogueDefinition result;
|
||||
result.id = j.value("id", "");
|
||||
result.displayName = j.value("displayName", result.id);
|
||||
result.startNode = j.value("start", "");
|
||||
|
||||
if (j.contains("nodes") && j["nodes"].is_array()) {
|
||||
for (const auto& item : j["nodes"]) {
|
||||
Node node = parseNode(item);
|
||||
if (!node.id.empty()) {
|
||||
result.nodes[node.id] = std::move(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
CutsceneLine DialogueDatabase::parseCutsceneLine(const json& j) {
|
||||
CutsceneLine line;
|
||||
line.speaker = j.value("speaker", "");
|
||||
line.text = j.value("text", "");
|
||||
line.portrait = j.value("portrait", "");
|
||||
line.sfx = j.value("sfx", "");
|
||||
line.durationMs = j.value("durationMs", 0);
|
||||
line.waitForConfirm = j.value("waitForConfirm", false);
|
||||
return line;
|
||||
}
|
||||
|
||||
StaticCutsceneDefinition DialogueDatabase::parseCutscene(const json& j) {
|
||||
StaticCutsceneDefinition cutscene;
|
||||
cutscene.id = j.value("id", "");
|
||||
cutscene.background = j.value("background", "");
|
||||
cutscene.music = j.value("music", "");
|
||||
cutscene.skippable = j.value("skippable", true);
|
||||
|
||||
if (j.contains("lines") && j["lines"].is_array()) {
|
||||
for (const auto& item : j["lines"]) {
|
||||
cutscene.lines.push_back(parseCutsceneLine(item));
|
||||
}
|
||||
}
|
||||
|
||||
return cutscene;
|
||||
}
|
||||
|
||||
bool DialogueDatabase::loadFromFile(const std::string& path) {
|
||||
dialogues.clear();
|
||||
cutscenes.clear();
|
||||
|
||||
const std::string raw = ZL::readTextFile(path);
|
||||
if (raw.empty()) {
|
||||
std::cerr << "[dialogue] Failed to read file: " << path << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
json root;
|
||||
try {
|
||||
root = json::parse(raw);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "[dialogue] JSON parse error in " << path << ": " << e.what() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (root.contains("dialogues") && root["dialogues"].is_array()) {
|
||||
for (const auto& item : root["dialogues"]) {
|
||||
DialogueDefinition dialogue = parseDialogue(item);
|
||||
if (!dialogue.id.empty()) {
|
||||
dialogues[dialogue.id] = std::move(dialogue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (root.contains("cutscenes") && root["cutscenes"].is_array()) {
|
||||
for (const auto& item : root["cutscenes"]) {
|
||||
StaticCutsceneDefinition cutscene = parseCutscene(item);
|
||||
if (!cutscene.id.empty()) {
|
||||
cutscenes[cutscene.id] = std::move(cutscene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !dialogues.empty();
|
||||
}
|
||||
|
||||
const DialogueDefinition* DialogueDatabase::findDialogue(const std::string& id) const {
|
||||
auto it = dialogues.find(id);
|
||||
return (it != dialogues.end()) ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
const StaticCutsceneDefinition* DialogueDatabase::findCutscene(const std::string& id) const {
|
||||
auto it = cutscenes.find(id);
|
||||
return (it != cutscenes.end()) ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
} // namespace ZL::Dialogue
|
||||
@ -1,38 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "dialogue/DialogueTypes.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace ZL::Dialogue {
|
||||
|
||||
class DialogueDatabase {
|
||||
public:
|
||||
using json = nlohmann::json;
|
||||
|
||||
bool loadFromFile(const std::string& path);
|
||||
|
||||
const DialogueDefinition* findDialogue(const std::string& id) const;
|
||||
const StaticCutsceneDefinition* findCutscene(const std::string& id) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, DialogueDefinition> dialogues;
|
||||
std::unordered_map<std::string, StaticCutsceneDefinition> cutscenes;
|
||||
|
||||
static NodeType parseNodeType(const std::string& value);
|
||||
static ChoiceKind parseChoiceKind(const std::string& value);
|
||||
static ComparisonOp parseComparisonOp(const std::string& value);
|
||||
|
||||
static Condition parseCondition(const json& j);
|
||||
static Effect parseEffect(const json& j);
|
||||
static Choice parseChoice(const json& j);
|
||||
static Node parseNode(const json& j);
|
||||
static DialogueDefinition parseDialogue(const json& j);
|
||||
static CutsceneLine parseCutsceneLine(const json& j);
|
||||
static StaticCutsceneDefinition parseCutscene(const json& j);
|
||||
|
||||
|
||||
};
|
||||
|
||||
} // namespace ZL::Dialogue
|
||||
@ -1,270 +0,0 @@
|
||||
#include "dialogue/DialogueOverlay.h"
|
||||
#include "dialogue/DialogueTypes.h"
|
||||
|
||||
#include "GameConstants.h"
|
||||
#include "Environment.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace ZL::Dialogue {
|
||||
|
||||
void DialogueOverlay::TexturedQuad::rebuild(const UiRect& newRect) {
|
||||
rect = newRect;
|
||||
mesh.data = CreateRect2D(
|
||||
{ rect.x + rect.w * 0.5f, rect.y + rect.h * 0.5f },
|
||||
{ rect.w * 0.5f, rect.h * 0.5f },
|
||||
0.0f
|
||||
);
|
||||
mesh.RefreshVBO();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
bool DialogueOverlay::init(Renderer& renderer, const std::string& zipFile) {
|
||||
rendererRef = &renderer;
|
||||
zipFilename = zipFile;
|
||||
|
||||
textboxTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/dialogue/textbox_bg.png", zipFile));
|
||||
portraitFrameTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/dialogue/portrait_frame.png", zipFile));
|
||||
choiceMainTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/dialogue/choice_main.png", zipFile));
|
||||
choiceOptionalTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/dialogue/choice_optional.png", zipFile));
|
||||
choiceSelectedTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/dialogue/choice_selected.png", zipFile));
|
||||
cutsceneSubtitleTexture = std::make_shared<Texture>(CreateTextureDataFromPng("resources/dialogue/cutscene_subtitle_bg.png", zipFile));
|
||||
|
||||
nameRenderer = std::make_unique<TextRenderer>();
|
||||
bodyRenderer = std::make_unique<TextRenderer>();
|
||||
choiceRenderer = std::make_unique<TextRenderer>();
|
||||
cutsceneRenderer = std::make_unique<TextRenderer>();
|
||||
|
||||
const bool ok =
|
||||
nameRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 28, zipFile) &&
|
||||
bodyRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 24, zipFile) &&
|
||||
choiceRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 22, zipFile) &&
|
||||
cutsceneRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 24, zipFile);
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
void DialogueOverlay::draw(Renderer& renderer, const PresentationModel& model) {
|
||||
if (model.mode == PresentationMode::Hidden) {
|
||||
lastChoiceRects.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.mode == PresentationMode::Cutscene) {
|
||||
drawCutscene(renderer, model);
|
||||
}
|
||||
else {
|
||||
drawDialogue(renderer, model);
|
||||
}
|
||||
}
|
||||
|
||||
void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel& model) {
|
||||
const float W = Environment::projectionWidth;
|
||||
const float H = Environment::projectionHeight;
|
||||
|
||||
const UiRect portraitRect{ 24.0f, 24.0f, 182.0f, 182.0f };
|
||||
const UiRect textboxRect{ 220.0f, 24.0f, max(200.0f, W - 244.0f), 182.0f };
|
||||
|
||||
if (!portraitQuad.initialized || portraitQuad.rect.w != portraitRect.w || portraitQuad.rect.h != portraitRect.h ||
|
||||
portraitQuad.rect.x != portraitRect.x || portraitQuad.rect.y != portraitRect.y) {
|
||||
portraitQuad.rebuild(portraitRect);
|
||||
}
|
||||
if (!textboxQuad.initialized || textboxQuad.rect.w != textboxRect.w || textboxQuad.rect.h != textboxRect.h ||
|
||||
textboxQuad.rect.x != textboxRect.x || textboxQuad.rect.y != textboxRect.y) {
|
||||
textboxQuad.rebuild(textboxRect);
|
||||
}
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
renderer.shaderManager.PushShader(defaultShaderName);
|
||||
renderer.RenderUniform1i(textureUniformName, 0);
|
||||
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
||||
renderer.PushMatrix();
|
||||
renderer.LoadIdentity();
|
||||
|
||||
drawQuad(renderer, textboxQuad, textboxTexture);
|
||||
drawQuad(renderer, portraitQuad, model.portraitPath.empty() ? portraitFrameTexture : loadTextureCached(model.portraitPath));
|
||||
drawQuad(renderer, portraitQuad, portraitFrameTexture);
|
||||
|
||||
renderer.PopMatrix();
|
||||
renderer.PopProjectionMatrix();
|
||||
renderer.shaderManager.PopShader();
|
||||
|
||||
const float nameX = textboxRect.x + 24.0f;
|
||||
const float nameY = textboxRect.y + textboxRect.h - 38.0f;
|
||||
const float bodyX = textboxRect.x + 24.0f;
|
||||
const float bodyY = textboxRect.y + textboxRect.h - 78.0f;
|
||||
|
||||
if (!model.speaker.empty()) {
|
||||
nameRenderer->drawText(model.speaker, nameX, nameY, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f });
|
||||
}
|
||||
|
||||
const std::string wrappedBody = wrapText(model.visibleText, 56);
|
||||
bodyRenderer->drawText(wrappedBody, bodyX, bodyY, 1.0f, false, { 1.0f, 1.0f, 1.0f, 1.0f });
|
||||
|
||||
lastChoiceRects.clear();
|
||||
if (model.mode == PresentationMode::Choice) {
|
||||
const float choiceStartY = textboxRect.y + 56.0f;
|
||||
const float choiceHeight = 30.0f;
|
||||
const float choiceSpacing = 8.0f;
|
||||
const float choiceWidth = textboxRect.w - 48.0f;
|
||||
|
||||
if (choiceQuads.size() < model.choices.size()) {
|
||||
choiceQuads.resize(model.choices.size());
|
||||
}
|
||||
|
||||
renderer.shaderManager.PushShader(defaultShaderName);
|
||||
renderer.RenderUniform1i(textureUniformName, 0);
|
||||
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
||||
renderer.PushMatrix();
|
||||
renderer.LoadIdentity();
|
||||
|
||||
for (size_t i = 0; i < model.choices.size(); ++i) {
|
||||
const float y = choiceStartY + (choiceHeight + choiceSpacing) * static_cast<float>(model.choices.size() - 1 - i);
|
||||
UiRect rect{ textboxRect.x + 20.0f, y, choiceWidth, choiceHeight };
|
||||
lastChoiceRects.push_back(rect);
|
||||
choiceQuads[i].rebuild(rect);
|
||||
|
||||
const bool isSelected = static_cast<int>(i) == model.selectedChoice;
|
||||
std::shared_ptr<Texture> choiceTexture = choiceMainTexture;
|
||||
if (model.choices[i].kind == ChoiceKind::Optional) {
|
||||
choiceTexture = choiceOptionalTexture;
|
||||
}
|
||||
if (isSelected) {
|
||||
choiceTexture = choiceSelectedTexture;
|
||||
}
|
||||
drawQuad(renderer, choiceQuads[i], choiceTexture);
|
||||
}
|
||||
|
||||
renderer.PopMatrix();
|
||||
renderer.PopProjectionMatrix();
|
||||
renderer.shaderManager.PopShader();
|
||||
|
||||
for (size_t i = 0; i < model.choices.size(); ++i) {
|
||||
const UiRect& rect = lastChoiceRects[i];
|
||||
const bool isSelected = static_cast<int>(i) == model.selectedChoice;
|
||||
const std::array<float, 4> color = (model.choices[i].kind == ChoiceKind::Optional)
|
||||
? std::array<float, 4>{0.82f, 0.82f, 0.82f, 1.0f}
|
||||
: std::array<float, 4>{ 1.0f, 0.93f, 0.65f, 1.0f };
|
||||
|
||||
choiceRenderer->drawText(
|
||||
wrapText(model.choices[i].text, 52),
|
||||
rect.x + 14.0f,
|
||||
rect.y + 9.0f,
|
||||
1.0f,
|
||||
false,
|
||||
isSelected ? std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f} : color
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel& model) {
|
||||
const float W = Environment::projectionWidth;
|
||||
const float H = Environment::projectionHeight;
|
||||
const UiRect fullscreenRect{ 0.0f, 0.0f, W, H };
|
||||
const UiRect subtitleRect{ W * 0.12f, 22.0f, W * 0.76f, 110.0f };
|
||||
|
||||
backgroundQuad.rebuild(fullscreenRect);
|
||||
subtitleQuad.rebuild(subtitleRect);
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
renderer.shaderManager.PushShader(defaultShaderName);
|
||||
renderer.RenderUniform1i(textureUniformName, 0);
|
||||
renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
|
||||
renderer.PushMatrix();
|
||||
renderer.LoadIdentity();
|
||||
|
||||
if (!model.backgroundPath.empty()) {
|
||||
drawQuad(renderer, backgroundQuad, loadTextureCached(model.backgroundPath));
|
||||
}
|
||||
drawQuad(renderer, subtitleQuad, cutsceneSubtitleTexture);
|
||||
|
||||
renderer.PopMatrix();
|
||||
renderer.PopProjectionMatrix();
|
||||
renderer.shaderManager.PopShader();
|
||||
|
||||
if (!model.speaker.empty()) {
|
||||
nameRenderer->drawText(model.speaker, subtitleRect.x + 24.0f, subtitleRect.y + subtitleRect.h - 32.0f, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f });
|
||||
}
|
||||
cutsceneRenderer->drawText(
|
||||
wrapText(model.visibleText, 62),
|
||||
subtitleRect.x + 24.0f,
|
||||
subtitleRect.y + 30.0f,
|
||||
1.0f,
|
||||
false,
|
||||
{ 1.0f, 1.0f, 1.0f, 1.0f }
|
||||
);
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
bool DialogueOverlay::handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex) const {
|
||||
outChoiceIndex = -1;
|
||||
if (model.mode != PresentationMode::Choice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < lastChoiceRects.size(); ++i) {
|
||||
if (lastChoiceRects[i].contains(x, y)) {
|
||||
outChoiceIndex = static_cast<int>(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::shared_ptr<Texture> DialogueOverlay::loadTextureCached(const std::string& path) {
|
||||
if (path.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto it = textureCache.find(path);
|
||||
if (it != textureCache.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
auto texture = std::make_shared<Texture>(CreateTextureDataFromPng(path, zipFilename));
|
||||
textureCache[path] = texture;
|
||||
return texture;
|
||||
}
|
||||
|
||||
void DialogueOverlay::drawQuad(Renderer& renderer, const TexturedQuad& quad, const std::shared_ptr<Texture>& texture) const {
|
||||
if (!texture) {
|
||||
return;
|
||||
}
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
|
||||
renderer.DrawVertexRenderStruct(quad.mesh);
|
||||
}
|
||||
|
||||
std::string DialogueOverlay::wrapText(const std::string& input, size_t maxLineLength) {
|
||||
if (input.size() <= maxLineLength) {
|
||||
return input;
|
||||
}
|
||||
|
||||
std::string output;
|
||||
size_t lineStart = 0;
|
||||
while (lineStart < input.size()) {
|
||||
size_t lineEnd = min(lineStart + maxLineLength, input.size());
|
||||
if (lineEnd < input.size()) {
|
||||
const size_t split = input.rfind(' ', lineEnd);
|
||||
if (split != std::string::npos && split > lineStart) {
|
||||
lineEnd = split;
|
||||
}
|
||||
}
|
||||
|
||||
output.append(input.substr(lineStart, lineEnd - lineStart));
|
||||
if (lineEnd < input.size()) {
|
||||
output.push_back('\n');
|
||||
lineStart = lineEnd + (input[lineEnd] == ' ' ? 1 : 0);
|
||||
}
|
||||
else {
|
||||
lineStart = lineEnd;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace ZL::Dialogue
|
||||
@ -1,66 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "dialogue/DialogueRuntime.h"
|
||||
#include "render/Renderer.h"
|
||||
#include "render/TextRenderer.h"
|
||||
#include "render/TextureManager.h"
|
||||
#include "UiManager.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace ZL::Dialogue {
|
||||
|
||||
class DialogueOverlay {
|
||||
public:
|
||||
bool init(Renderer& renderer, const std::string& zipFile = "");
|
||||
void draw(Renderer& renderer, const PresentationModel& model);
|
||||
|
||||
// Coordinates are expected in the game's UI projection space
|
||||
bool handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex) const;
|
||||
|
||||
private:
|
||||
struct TexturedQuad {
|
||||
UiRect rect;
|
||||
VertexRenderStruct mesh;
|
||||
bool initialized = false;
|
||||
|
||||
void rebuild(const UiRect& newRect);
|
||||
};
|
||||
|
||||
Renderer* rendererRef = nullptr;
|
||||
std::string zipFilename;
|
||||
|
||||
std::shared_ptr<Texture> textboxTexture;
|
||||
std::shared_ptr<Texture> portraitFrameTexture;
|
||||
std::shared_ptr<Texture> choiceMainTexture;
|
||||
std::shared_ptr<Texture> choiceOptionalTexture;
|
||||
std::shared_ptr<Texture> choiceSelectedTexture;
|
||||
std::shared_ptr<Texture> cutsceneSubtitleTexture;
|
||||
|
||||
mutable std::vector<UiRect> lastChoiceRects;
|
||||
|
||||
std::unique_ptr<TextRenderer> nameRenderer;
|
||||
std::unique_ptr<TextRenderer> bodyRenderer;
|
||||
std::unique_ptr<TextRenderer> choiceRenderer;
|
||||
std::unique_ptr<TextRenderer> cutsceneRenderer;
|
||||
|
||||
TexturedQuad portraitQuad;
|
||||
TexturedQuad textboxQuad;
|
||||
TexturedQuad subtitleQuad;
|
||||
TexturedQuad backgroundQuad;
|
||||
mutable std::vector<TexturedQuad> choiceQuads;
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<Texture>> textureCache;
|
||||
|
||||
void drawDialogue(Renderer& renderer, const PresentationModel& model);
|
||||
void drawCutscene(Renderer& renderer, const PresentationModel& model);
|
||||
|
||||
std::shared_ptr<Texture> loadTextureCached(const std::string& path);
|
||||
void drawQuad(Renderer& renderer, const TexturedQuad& quad, const std::shared_ptr<Texture>& texture) const;
|
||||
|
||||
static std::string wrapText(const std::string& input, size_t maxLineLength);
|
||||
};
|
||||
|
||||
} // namespace ZL::Dialogue
|
||||
@ -1,439 +0,0 @@
|
||||
#include "dialogue/DialogueRuntime.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
|
||||
namespace ZL::Dialogue {
|
||||
|
||||
void DialogueRuntime::setDatabase(const DialogueDatabase* value) {
|
||||
database = value;
|
||||
}
|
||||
|
||||
bool DialogueRuntime::startDialogue(const std::string& dialogueId) {
|
||||
if (!database) {
|
||||
std::cerr << "[dialogue] No database assigned to runtime\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
const DialogueDefinition* dialogue = database->findDialogue(dialogueId);
|
||||
if (!dialogue) {
|
||||
std::cerr << "[dialogue] Dialogue not found: " << dialogueId << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
activeDialogue = dialogue;
|
||||
activeCutscene = nullptr;
|
||||
currentNodeId.clear();
|
||||
pendingNodeAfterCutscene.clear();
|
||||
visibleChoices.clear();
|
||||
selectedChoice = 0;
|
||||
revealCharacters = 0.0f;
|
||||
currentCutsceneLine = -1;
|
||||
cutsceneTimerMs = 0;
|
||||
presentation = {};
|
||||
presentation.dialogueId = dialogue->id;
|
||||
|
||||
return enterNode(dialogue->startNode);
|
||||
}
|
||||
|
||||
void DialogueRuntime::stop() {
|
||||
activeDialogue = nullptr;
|
||||
activeCutscene = nullptr;
|
||||
currentNodeId.clear();
|
||||
pendingNodeAfterCutscene.clear();
|
||||
visibleChoices.clear();
|
||||
selectedChoice = 0;
|
||||
revealCharacters = 0.0f;
|
||||
currentCutsceneLine = -1;
|
||||
cutsceneTimerMs = 0;
|
||||
mode = Mode::Inactive;
|
||||
presentation = {};
|
||||
}
|
||||
|
||||
void DialogueRuntime::update(int deltaMs) {
|
||||
if (mode == Mode::PresentingLine) {
|
||||
if (!presentation.revealCompleted) {
|
||||
revealCharacters += revealSpeedCharsPerSecond * (static_cast<float>(deltaMs) / 1000.0f);
|
||||
const size_t fullLen = presentation.fullText.size();
|
||||
const size_t visibleLen = static_cast<size_t>(std::min<float>(revealCharacters, static_cast<float>(fullLen)));
|
||||
presentation.visibleText = presentation.fullText.substr(0, visibleLen);
|
||||
presentation.revealCompleted = (visibleLen >= fullLen);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == Mode::PlayingCutscene && activeCutscene) {
|
||||
if (currentCutsceneLine < 0 || currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
|
||||
advanceCutsceneLine();
|
||||
return;
|
||||
}
|
||||
|
||||
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
||||
if (line.waitForConfirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
cutsceneTimerMs += deltaMs;
|
||||
const int durationMs = (line.durationMs > 0) ? line.durationMs : computeFallbackCutsceneDurationMs(line.text);
|
||||
if (cutsceneTimerMs >= durationMs) {
|
||||
advanceCutsceneLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DialogueRuntime::confirmAdvance() {
|
||||
if (mode == Mode::PresentingLine) {
|
||||
if (!presentation.revealCompleted) {
|
||||
presentation.visibleText = presentation.fullText;
|
||||
presentation.revealCompleted = true;
|
||||
revealCharacters = static_cast<float>(presentation.fullText.size());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeDialogue) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = activeDialogue->nodes.find(currentNodeId);
|
||||
if (it == activeDialogue->nodes.end()) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!it->second.next.empty()) {
|
||||
enterNode(it->second.next);
|
||||
}
|
||||
else {
|
||||
stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == Mode::WaitingForChoice) {
|
||||
if (visibleChoices.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Choice& choice = visibleChoices[std::clamp(selectedChoice, 0, static_cast<int>(visibleChoices.size()) - 1)];
|
||||
if (choice.consumeOnce && !choice.id.empty()) {
|
||||
consumedChoices.insert(choice.id);
|
||||
}
|
||||
applyEffects(choice.effects);
|
||||
enterNode(choice.next);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == Mode::PlayingCutscene) {
|
||||
advanceCutsceneLine();
|
||||
}
|
||||
}
|
||||
|
||||
void DialogueRuntime::moveSelection(int delta) {
|
||||
if (mode != Mode::WaitingForChoice || visibleChoices.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int count = static_cast<int>(visibleChoices.size());
|
||||
selectedChoice = (selectedChoice + delta) % count;
|
||||
if (selectedChoice < 0) {
|
||||
selectedChoice += count;
|
||||
}
|
||||
presentation.selectedChoice = selectedChoice;
|
||||
}
|
||||
|
||||
void DialogueRuntime::setFlag(const std::string& name, int value) {
|
||||
flags[name] = value;
|
||||
}
|
||||
|
||||
int DialogueRuntime::getFlag(const std::string& name) const {
|
||||
auto it = flags.find(name);
|
||||
return (it != flags.end()) ? it->second : 0;
|
||||
}
|
||||
|
||||
bool DialogueRuntime::evaluateConditions(const std::vector<Condition>& conditions) const {
|
||||
for (const Condition& condition : conditions) {
|
||||
const int currentValue = getFlag(condition.flag);
|
||||
switch (condition.op) {
|
||||
case ComparisonOp::Exists:
|
||||
if (currentValue == 0) return false;
|
||||
break;
|
||||
case ComparisonOp::Equals:
|
||||
if (currentValue != condition.value) return false;
|
||||
break;
|
||||
case ComparisonOp::NotEquals:
|
||||
if (currentValue == condition.value) return false;
|
||||
break;
|
||||
case ComparisonOp::GreaterOrEqual:
|
||||
if (currentValue < condition.value) return false;
|
||||
break;
|
||||
case ComparisonOp::LessOrEqual:
|
||||
if (currentValue > condition.value) return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void DialogueRuntime::applyEffects(const std::vector<Effect>& effects) {
|
||||
for (const Effect& effect : effects) {
|
||||
if (effect.flag.empty()) {
|
||||
continue;
|
||||
}
|
||||
if (effect.relative) {
|
||||
flags[effect.flag] += effect.value;
|
||||
}
|
||||
else {
|
||||
flags[effect.flag] = effect.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DialogueRuntime::enterNode(const std::string& nodeId) {
|
||||
if (!activeDialogue) {
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto it = activeDialogue->nodes.find(nodeId);
|
||||
if (it == activeDialogue->nodes.end()) {
|
||||
std::cerr << "[dialogue] Node not found: " << nodeId << " in dialogue " << activeDialogue->id << "\n";
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
const Node& node = it->second;
|
||||
currentNodeId = node.id;
|
||||
presentation.dialogueId = activeDialogue->id;
|
||||
|
||||
switch (node.type) {
|
||||
case NodeType::Line:
|
||||
presentLine(node);
|
||||
return true;
|
||||
|
||||
case NodeType::Choice:
|
||||
presentChoices(node);
|
||||
return true;
|
||||
|
||||
case NodeType::Condition:
|
||||
if (evaluateConditions(node.conditions)) {
|
||||
return enterNode(!node.trueNext.empty() ? node.trueNext : node.next);
|
||||
}
|
||||
return enterNode(node.falseNext);
|
||||
|
||||
case NodeType::SetFlag:
|
||||
applyEffects(node.effects);
|
||||
if (!node.next.empty()) {
|
||||
return enterNode(node.next);
|
||||
}
|
||||
stop();
|
||||
return true;
|
||||
|
||||
case NodeType::Jump:
|
||||
if (!node.next.empty()) {
|
||||
return enterNode(node.next);
|
||||
}
|
||||
stop();
|
||||
return true;
|
||||
|
||||
case NodeType::End:
|
||||
stop();
|
||||
return true;
|
||||
|
||||
case NodeType::CutsceneStart:
|
||||
startCutscene(node.cutsceneId, node.next);
|
||||
return true;
|
||||
}
|
||||
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
void DialogueRuntime::presentLine(const Node& node) {
|
||||
mode = Mode::PresentingLine;
|
||||
revealCharacters = 0.0f;
|
||||
presentation.mode = PresentationMode::Dialogue;
|
||||
presentation.speaker = node.speaker;
|
||||
presentation.fullText = node.text;
|
||||
presentation.visibleText.clear();
|
||||
presentation.portraitPath = node.portrait;
|
||||
presentation.backgroundPath.clear();
|
||||
presentation.choices.clear();
|
||||
presentation.selectedChoice = 0;
|
||||
presentation.revealCompleted = node.text.empty();
|
||||
|
||||
if (presentation.revealCompleted) {
|
||||
presentation.visibleText = node.text;
|
||||
revealCharacters = static_cast<float>(node.text.size());
|
||||
}
|
||||
}
|
||||
|
||||
void DialogueRuntime::presentChoices(const Node& node) {
|
||||
visibleChoices.clear();
|
||||
presentation.choices.clear();
|
||||
|
||||
for (const Choice& choice : node.choices) {
|
||||
if (!choice.id.empty() && consumedChoices.count(choice.id) > 0) {
|
||||
continue;
|
||||
}
|
||||
if (!evaluateConditions(choice.conditions)) {
|
||||
continue;
|
||||
}
|
||||
visibleChoices.push_back(choice);
|
||||
presentation.choices.push_back({ choice.id, choice.text, choice.kind });
|
||||
}
|
||||
|
||||
if (visibleChoices.empty()) {
|
||||
if (!node.next.empty()) {
|
||||
enterNode(node.next);
|
||||
}
|
||||
else {
|
||||
stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
mode = Mode::WaitingForChoice;
|
||||
selectedChoice = 0;
|
||||
presentation.mode = PresentationMode::Choice;
|
||||
presentation.speaker = node.speaker;
|
||||
presentation.fullText = node.text;
|
||||
presentation.visibleText = node.text;
|
||||
presentation.portraitPath = node.portrait;
|
||||
presentation.backgroundPath.clear();
|
||||
presentation.selectedChoice = 0;
|
||||
presentation.revealCompleted = true;
|
||||
}
|
||||
|
||||
void DialogueRuntime::startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene) {
|
||||
if (!database) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const StaticCutsceneDefinition* cutscene = database->findCutscene(cutsceneId);
|
||||
if (!cutscene) {
|
||||
std::cerr << "[dialogue] Cutscene not found: " << cutsceneId << "\n";
|
||||
if (!nextNodeAfterCutscene.empty()) {
|
||||
enterNode(nextNodeAfterCutscene);
|
||||
}
|
||||
else {
|
||||
stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
activeCutscene = cutscene;
|
||||
pendingNodeAfterCutscene = nextNodeAfterCutscene;
|
||||
mode = Mode::PlayingCutscene;
|
||||
currentCutsceneLine = -1;
|
||||
cutsceneTimerMs = 0;
|
||||
advanceCutsceneLine();
|
||||
}
|
||||
|
||||
void DialogueRuntime::advanceCutsceneLine() {
|
||||
if (!activeCutscene) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
++currentCutsceneLine;
|
||||
cutsceneTimerMs = 0;
|
||||
|
||||
if (currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
|
||||
activeCutscene = nullptr;
|
||||
if (!pendingNodeAfterCutscene.empty()) {
|
||||
const std::string nextNode = pendingNodeAfterCutscene;
|
||||
pendingNodeAfterCutscene.clear();
|
||||
enterNode(nextNode);
|
||||
}
|
||||
else {
|
||||
stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
refreshCutscenePresentation();
|
||||
}
|
||||
|
||||
void DialogueRuntime::refreshCutscenePresentation() {
|
||||
if (!activeCutscene || currentCutsceneLine < 0 ||
|
||||
currentCutsceneLine >= static_cast<int>(activeCutscene->lines.size())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const CutsceneLine& line = activeCutscene->lines[currentCutsceneLine];
|
||||
presentation.mode = PresentationMode::Cutscene;
|
||||
presentation.speaker = line.speaker;
|
||||
presentation.fullText = line.text;
|
||||
presentation.visibleText = line.text;
|
||||
presentation.portraitPath = line.portrait;
|
||||
presentation.backgroundPath = activeCutscene->background;
|
||||
presentation.choices.clear();
|
||||
presentation.selectedChoice = 0;
|
||||
presentation.revealCompleted = true;
|
||||
}
|
||||
|
||||
int DialogueRuntime::computeFallbackCutsceneDurationMs(const std::string& text) {
|
||||
const int cps = 17;
|
||||
const int minDuration = 1500;
|
||||
const int linger = 450;
|
||||
const int calculated = static_cast<int>((1000.0 * static_cast<double>(std::max<size_t>(text.size(), 1))) / cps);
|
||||
return std::max(minDuration, calculated + linger);
|
||||
}
|
||||
|
||||
DialogueRuntime::json DialogueRuntime::buildSaveState() const {
|
||||
json result;
|
||||
result["active"] = isActive();
|
||||
result["dialogueId"] = activeDialogue ? activeDialogue->id : "";
|
||||
result["currentNodeId"] = currentNodeId;
|
||||
result["pendingNodeAfterCutscene"] = pendingNodeAfterCutscene;
|
||||
result["selectedChoice"] = selectedChoice;
|
||||
result["currentCutsceneLine"] = currentCutsceneLine;
|
||||
result["cutsceneTimerMs"] = cutsceneTimerMs;
|
||||
result["flags"] = flags;
|
||||
result["consumedChoices"] = consumedChoices;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DialogueRuntime::restoreSaveState(const json& state) {
|
||||
if (!database) {
|
||||
return false;
|
||||
}
|
||||
|
||||
flags.clear();
|
||||
consumedChoices.clear();
|
||||
|
||||
if (state.contains("flags")) {
|
||||
flags = state["flags"].get<std::unordered_map<std::string, int>>();
|
||||
}
|
||||
if (state.contains("consumedChoices")) {
|
||||
consumedChoices = state["consumedChoices"].get<std::unordered_set<std::string>>();
|
||||
}
|
||||
|
||||
const bool active = state.value("active", false);
|
||||
if (!active) {
|
||||
stop();
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string dialogueId = state.value("dialogueId", "");
|
||||
if (!startDialogue(dialogueId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string nodeId = state.value("currentNodeId", "");
|
||||
pendingNodeAfterCutscene = state.value("pendingNodeAfterCutscene", "");
|
||||
selectedChoice = state.value("selectedChoice", 0);
|
||||
currentCutsceneLine = state.value("currentCutsceneLine", -1);
|
||||
cutsceneTimerMs = state.value("cutsceneTimerMs", 0);
|
||||
|
||||
const bool ok = nodeId.empty() ? true : enterNode(nodeId);
|
||||
if (mode == Mode::WaitingForChoice && !visibleChoices.empty()) {
|
||||
selectedChoice = std::clamp(selectedChoice, 0, static_cast<int>(visibleChoices.size()) - 1);
|
||||
presentation.selectedChoice = selectedChoice;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace ZL::Dialogue
|
||||
@ -1,81 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "dialogue/DialogueDatabase.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace ZL::Dialogue {
|
||||
|
||||
class DialogueRuntime {
|
||||
public:
|
||||
using json = nlohmann::json;
|
||||
|
||||
void setDatabase(const DialogueDatabase* value);
|
||||
|
||||
bool startDialogue(const std::string& dialogueId);
|
||||
void stop();
|
||||
|
||||
void update(int deltaMs);
|
||||
|
||||
bool isActive() const { return mode != Mode::Inactive; }
|
||||
bool isInChoice() const { return mode == Mode::WaitingForChoice; }
|
||||
bool isPlayingCutscene() const { return mode == Mode::PlayingCutscene; }
|
||||
|
||||
void confirmAdvance();
|
||||
void moveSelection(int delta);
|
||||
|
||||
const PresentationModel& getPresentation() const { return presentation; }
|
||||
|
||||
void setFlag(const std::string& name, int value);
|
||||
int getFlag(const std::string& name) const;
|
||||
|
||||
json buildSaveState() const;
|
||||
bool restoreSaveState(const json& state);
|
||||
|
||||
private:
|
||||
enum class Mode {
|
||||
Inactive,
|
||||
PresentingLine,
|
||||
WaitingForChoice,
|
||||
PlayingCutscene
|
||||
};
|
||||
|
||||
const DialogueDatabase* database = nullptr;
|
||||
const DialogueDefinition* activeDialogue = nullptr;
|
||||
const StaticCutsceneDefinition* activeCutscene = nullptr;
|
||||
|
||||
std::unordered_map<std::string, int> flags;
|
||||
std::unordered_set<std::string> consumedChoices;
|
||||
|
||||
std::string currentNodeId;
|
||||
std::string pendingNodeAfterCutscene;
|
||||
|
||||
std::vector<Choice> visibleChoices;
|
||||
PresentationModel presentation;
|
||||
Mode mode = Mode::Inactive;
|
||||
|
||||
int selectedChoice = 0;
|
||||
float revealCharacters = 0.0f;
|
||||
float revealSpeedCharsPerSecond = 52.0f;
|
||||
|
||||
int currentCutsceneLine = -1;
|
||||
int cutsceneTimerMs = 0;
|
||||
|
||||
bool evaluateConditions(const std::vector<Condition>& conditions) const;
|
||||
void applyEffects(const std::vector<Effect>& effects);
|
||||
|
||||
bool enterNode(const std::string& nodeId);
|
||||
void presentLine(const Node& node);
|
||||
void presentChoices(const Node& node);
|
||||
void startCutscene(const std::string& cutsceneId, const std::string& nextNodeAfterCutscene);
|
||||
|
||||
void advanceCutsceneLine();
|
||||
void refreshCutscenePresentation();
|
||||
|
||||
static int computeFallbackCutsceneDurationMs(const std::string& text);
|
||||
};
|
||||
|
||||
} // namespace ZL::Dialogue
|
||||
@ -1,104 +0,0 @@
|
||||
#include "dialogue/DialogueSystem.h"
|
||||
|
||||
namespace ZL::Dialogue {
|
||||
|
||||
bool DialogueSystem::init(Renderer& renderer, const std::string& zipFile) {
|
||||
runtime.setDatabase(&database);
|
||||
return overlay.init(renderer, zipFile);
|
||||
}
|
||||
|
||||
bool DialogueSystem::loadDatabase(const std::string& path) {
|
||||
return database.loadFromFile(path);
|
||||
}
|
||||
|
||||
void DialogueSystem::update(int deltaMs, const Eigen::Vector3f& playerPosition) {
|
||||
if (!runtime.isActive()) {
|
||||
for (TriggerZone& zone : triggerZones) {
|
||||
if (zone.once && zone.triggered) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Eigen::Vector3f diff = playerPosition - zone.center;
|
||||
if (diff.norm() <= zone.radius) {
|
||||
if (startDialogue(zone.dialogueId)) {
|
||||
zone.triggered = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runtime.update(deltaMs);
|
||||
}
|
||||
|
||||
void DialogueSystem::draw(Renderer& renderer) {
|
||||
overlay.draw(renderer, runtime.getPresentation());
|
||||
}
|
||||
|
||||
bool DialogueSystem::handleKeyDown(SDL_Keycode key) {
|
||||
if (!runtime.isActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case SDLK_RETURN:
|
||||
case SDLK_SPACE:
|
||||
case SDLK_e:
|
||||
runtime.confirmAdvance();
|
||||
return true;
|
||||
|
||||
case SDLK_UP:
|
||||
case SDLK_w:
|
||||
runtime.moveSelection(-1);
|
||||
return true;
|
||||
|
||||
case SDLK_DOWN:
|
||||
case SDLK_s:
|
||||
runtime.moveSelection(1);
|
||||
return true;
|
||||
|
||||
case SDLK_ESCAPE:
|
||||
stopDialogue();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool DialogueSystem::handlePointerReleased(float x, float y) {
|
||||
if (!runtime.isActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int choiceIndex = -1;
|
||||
const PresentationModel& model = runtime.getPresentation();
|
||||
if (overlay.handlePointerReleased(x, y, model, choiceIndex)) {
|
||||
while (model.selectedChoice != choiceIndex) {
|
||||
runtime.moveSelection(1);
|
||||
}
|
||||
runtime.confirmAdvance();
|
||||
return true;
|
||||
}
|
||||
|
||||
runtime.confirmAdvance();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DialogueSystem::startDialogue(const std::string& dialogueId) {
|
||||
return runtime.startDialogue(dialogueId);
|
||||
}
|
||||
|
||||
void DialogueSystem::stopDialogue() {
|
||||
runtime.stop();
|
||||
}
|
||||
|
||||
void DialogueSystem::addTriggerZone(const TriggerZone& zone) {
|
||||
triggerZones.push_back(zone);
|
||||
}
|
||||
|
||||
void DialogueSystem::clearTriggerZones() {
|
||||
triggerZones.clear();
|
||||
}
|
||||
|
||||
} // namespace ZL::Dialogue
|
||||
@ -1,51 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "dialogue/DialogueOverlay.h"
|
||||
#include "dialogue/DialogueRuntime.h"
|
||||
#include <Eigen/Dense>
|
||||
#include <SDL.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ZL::Dialogue {
|
||||
|
||||
struct TriggerZone {
|
||||
std::string id;
|
||||
std::string dialogueId;
|
||||
Eigen::Vector3f center = Eigen::Vector3f::Zero();
|
||||
float radius = 1.5f;
|
||||
bool once = true;
|
||||
bool triggered = false;
|
||||
};
|
||||
|
||||
class DialogueSystem {
|
||||
public:
|
||||
bool init(Renderer& renderer, const std::string& zipFile = "");
|
||||
bool loadDatabase(const std::string& path);
|
||||
|
||||
void update(int deltaMs, const Eigen::Vector3f& playerPosition);
|
||||
void draw(Renderer& renderer);
|
||||
|
||||
bool handleKeyDown(SDL_Keycode key);
|
||||
bool handlePointerReleased(float x, float y);
|
||||
|
||||
bool startDialogue(const std::string& dialogueId);
|
||||
void stopDialogue();
|
||||
|
||||
bool isActive() const { return runtime.isActive(); }
|
||||
bool blocksGameplayInput() const { return runtime.isActive(); }
|
||||
|
||||
void setFlag(const std::string& name, int value) { runtime.setFlag(name, value); }
|
||||
int getFlag(const std::string& name) const { return runtime.getFlag(name); }
|
||||
|
||||
void addTriggerZone(const TriggerZone& zone);
|
||||
void clearTriggerZones();
|
||||
|
||||
private:
|
||||
DialogueDatabase database;
|
||||
DialogueRuntime runtime;
|
||||
DialogueOverlay overlay;
|
||||
std::vector<TriggerZone> triggerZones;
|
||||
};
|
||||
|
||||
} // namespace ZL::Dialogue
|
||||
@ -1,140 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace ZL::Dialogue {
|
||||
|
||||
enum class NodeType {
|
||||
Line,
|
||||
Choice,
|
||||
Condition,
|
||||
SetFlag,
|
||||
Jump,
|
||||
End,
|
||||
CutsceneStart
|
||||
};
|
||||
|
||||
enum class ChoiceKind {
|
||||
Main,
|
||||
Optional,
|
||||
Exit
|
||||
};
|
||||
|
||||
enum class ComparisonOp {
|
||||
Exists,
|
||||
Equals,
|
||||
NotEquals,
|
||||
GreaterOrEqual,
|
||||
LessOrEqual
|
||||
};
|
||||
|
||||
struct Condition {
|
||||
std::string flag;
|
||||
ComparisonOp op = ComparisonOp::Exists;
|
||||
int value = 1;
|
||||
};
|
||||
|
||||
struct Effect {
|
||||
std::string flag;
|
||||
int value = 1;
|
||||
bool relative = false;
|
||||
};
|
||||
|
||||
struct Choice {
|
||||
std::string id;
|
||||
std::string text;
|
||||
std::string next;
|
||||
ChoiceKind kind = ChoiceKind::Main;
|
||||
std::vector<Condition> conditions;
|
||||
std::vector<Effect> effects;
|
||||
bool consumeOnce = false;
|
||||
};
|
||||
|
||||
struct Node {
|
||||
std::string id;
|
||||
NodeType type = NodeType::Line;
|
||||
|
||||
std::string speaker;
|
||||
std::string text;
|
||||
std::string portrait;
|
||||
std::string next;
|
||||
|
||||
// For Condition nodes
|
||||
std::string trueNext;
|
||||
std::string falseNext;
|
||||
std::vector<Condition> conditions;
|
||||
|
||||
// For Choice / SetFlag
|
||||
std::vector<Choice> choices;
|
||||
std::vector<Effect> effects;
|
||||
|
||||
// For CutsceneStart
|
||||
std::string cutsceneId;
|
||||
};
|
||||
|
||||
struct DialogueDefinition {
|
||||
std::string id;
|
||||
std::string displayName;
|
||||
std::string startNode;
|
||||
std::unordered_map<std::string, Node> nodes;
|
||||
};
|
||||
|
||||
struct CutsceneLine {
|
||||
std::string speaker;
|
||||
std::string text;
|
||||
std::string portrait;
|
||||
std::string sfx;
|
||||
int durationMs = 0;
|
||||
bool waitForConfirm = false;
|
||||
};
|
||||
|
||||
struct StaticCutsceneDefinition {
|
||||
std::string id;
|
||||
std::string background;
|
||||
std::string music;
|
||||
bool skippable = true;
|
||||
std::vector<CutsceneLine> lines;
|
||||
};
|
||||
|
||||
struct PresentedChoice {
|
||||
std::string id;
|
||||
std::string text;
|
||||
ChoiceKind kind = ChoiceKind::Main;
|
||||
};
|
||||
|
||||
enum class PresentationMode {
|
||||
Hidden,
|
||||
Dialogue,
|
||||
Choice,
|
||||
Cutscene
|
||||
};
|
||||
|
||||
struct PresentationModel {
|
||||
PresentationMode mode = PresentationMode::Hidden;
|
||||
std::string dialogueId;
|
||||
std::string speaker;
|
||||
std::string fullText;
|
||||
std::string visibleText;
|
||||
std::string portraitPath;
|
||||
std::string backgroundPath;
|
||||
std::vector<PresentedChoice> choices;
|
||||
int selectedChoice = 0;
|
||||
bool revealCompleted = true;
|
||||
};
|
||||
|
||||
struct SaveState {
|
||||
std::string dialogueId;
|
||||
std::string currentNodeId;
|
||||
std::string pendingNodeAfterCutscene;
|
||||
std::unordered_set<std::string, int> flags;
|
||||
std::unordered_set<std::string> consumedChoices;
|
||||
int selectedChoice = 0;
|
||||
int currentCutsceneLine = -1;
|
||||
int cutsceneTimerMs = 0;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
} // namespace ZL::Dialogue
|
||||
@ -133,8 +133,6 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
|
||||
glyphList.emplace_back((char)c, std::move(gb));
|
||||
}
|
||||
|
||||
lineHeight = std::max<float>(static_cast<float>(pixelSize) * 1.3f, static_cast<float>(maxGlyphHeight) * 1.25f);
|
||||
|
||||
// Пакуем глифы в один атлас (упрощённый алгоритм строковой укладки)
|
||||
const int padding = 1;
|
||||
const int maxAtlasWidth = 1024; // безопасное значение для большинства устройств
|
||||
@ -298,16 +296,8 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
|
||||
|
||||
float totalW = 0.0f;
|
||||
float maxH = 0.0f;
|
||||
|
||||
float maxLineWidth = 0.0f;
|
||||
for (char ch : text) {
|
||||
if (ch == '\n') {
|
||||
maxLineWidth = max(maxLineWidth, penX);
|
||||
penX = 0.0f;
|
||||
penY -= lineHeight * scale;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (char ch : text) {
|
||||
auto git = glyphs.find(ch);
|
||||
if (git == glyphs.end()) continue;
|
||||
const GlyphInfo& g = git->second;
|
||||
@ -342,7 +332,6 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
|
||||
totalW = penX;
|
||||
maxH = max(maxH, h);
|
||||
}
|
||||
totalW = max(maxLineWidth, penX);
|
||||
|
||||
// Сохраняем в кеш
|
||||
CachedText ct;
|
||||
|
||||
@ -47,7 +47,6 @@ private:
|
||||
std::shared_ptr<Texture> atlasTexture;
|
||||
size_t atlasWidth = 0;
|
||||
size_t atlasHeight = 0;
|
||||
float lineHeight = 32.0f;
|
||||
|
||||
VertexRenderStruct textMesh;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user