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/GameConstants.cpp
|
||||||
../src/ScriptEngine.h
|
../src/ScriptEngine.h
|
||||||
../src/ScriptEngine.cpp
|
../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
|
# Установка проекта по умолчанию для 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;
|
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);
|
scriptEngine.init(this);
|
||||||
|
|
||||||
std::cout << "Load resurces step 13" << std::endl;
|
std::cout << "Load resurces step 13" << std::endl;
|
||||||
@ -248,22 +237,12 @@ namespace ZL
|
|||||||
{
|
{
|
||||||
glClear(GL_DEPTH_BUFFER_BIT);
|
glClear(GL_DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
glDisable(GL_DEPTH_TEST);
|
|
||||||
glDepthMask(GL_FALSE);
|
|
||||||
|
|
||||||
renderer.shaderManager.PushShader(defaultShaderName);
|
renderer.shaderManager.PushShader(defaultShaderName);
|
||||||
renderer.RenderUniform1i(textureUniformName, 0);
|
renderer.RenderUniform1i(textureUniformName, 0);
|
||||||
glEnable(GL_BLEND);
|
glEnable(GL_BLEND);
|
||||||
|
|
||||||
menuManager.uiManager.draw(renderer);
|
menuManager.uiManager.draw(renderer);
|
||||||
dialogueSystem.draw(renderer);
|
|
||||||
|
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
renderer.shaderManager.PopShader();
|
renderer.shaderManager.PopShader();
|
||||||
|
|
||||||
glDepthMask(GL_TRUE);
|
|
||||||
glEnable(GL_DEPTH_TEST);
|
|
||||||
|
|
||||||
CheckGlError();
|
CheckGlError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -382,10 +361,7 @@ namespace ZL
|
|||||||
lastTickCount = newTickCount;
|
lastTickCount = newTickCount;
|
||||||
|
|
||||||
if (player) player->update(delta);
|
if (player) player->update(delta);
|
||||||
//for (auto& npc : npcs) npc->update(delta);
|
for (auto& npc : npcs) npc->update(delta);
|
||||||
if (player) {
|
|
||||||
dialogueSystem.update(static_cast<int>(delta), player->position);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -474,11 +450,6 @@ namespace ZL
|
|||||||
if (event.type == SDL_MOUSEBUTTONDOWN) {
|
if (event.type == SDL_MOUSEBUTTONDOWN) {
|
||||||
handleDown(ZL::UiManager::MOUSE_FINGER_ID, mx, my);
|
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
|
// Unproject click to ground plane (y=0) for Viola's walk target
|
||||||
float ndcX = 2.0f * event.button.x / Environment::width - 1.0f;
|
float ndcX = 2.0f * event.button.x / Environment::width - 1.0f;
|
||||||
float ndcY = 1.0f - 2.0f * event.button.y / Environment::height;
|
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.type == SDL_KEYDOWN) {
|
||||||
if (event.key.keysym.sym == SDLK_BACKSPACE) {
|
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
|
} // namespace ZL
|
||||||
|
|||||||
@ -17,7 +17,6 @@
|
|||||||
#include <render/TextRenderer.h>
|
#include <render/TextRenderer.h>
|
||||||
#include "MenuManager.h"
|
#include "MenuManager.h"
|
||||||
#include "ScriptEngine.h"
|
#include "ScriptEngine.h"
|
||||||
#include "dialogue/DialogueSystem.h"
|
|
||||||
|
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
@ -34,9 +33,6 @@ namespace ZL {
|
|||||||
void render();
|
void render();
|
||||||
|
|
||||||
bool shouldExit() const { return Environment::exitGameLoop; }
|
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;
|
Renderer renderer;
|
||||||
TaskManager taskManager;
|
TaskManager taskManager;
|
||||||
@ -98,7 +94,6 @@ namespace ZL {
|
|||||||
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
|
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
|
||||||
|
|
||||||
MenuManager menuManager;
|
MenuManager menuManager;
|
||||||
Dialogue::DialogueSystem dialogueSystem;
|
|
||||||
ScriptEngine scriptEngine;
|
ScriptEngine scriptEngine;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -47,23 +47,6 @@ void ScriptEngine::init(Game* game) {
|
|||||||
npcs[index]->setTarget(Eigen::Vector3f(x, y, z), std::move(cb));
|
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");
|
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));
|
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 padding = 1;
|
||||||
const int maxAtlasWidth = 1024; // безопасное значение для большинства устройств
|
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 totalW = 0.0f;
|
||||||
float maxH = 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);
|
auto git = glyphs.find(ch);
|
||||||
if (git == glyphs.end()) continue;
|
if (git == glyphs.end()) continue;
|
||||||
const GlyphInfo& g = git->second;
|
const GlyphInfo& g = git->second;
|
||||||
@ -342,7 +332,6 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
|
|||||||
totalW = penX;
|
totalW = penX;
|
||||||
maxH = max(maxH, h);
|
maxH = max(maxH, h);
|
||||||
}
|
}
|
||||||
totalW = max(maxLineWidth, penX);
|
|
||||||
|
|
||||||
// Сохраняем в кеш
|
// Сохраняем в кеш
|
||||||
CachedText ct;
|
CachedText ct;
|
||||||
|
|||||||
@ -47,7 +47,6 @@ private:
|
|||||||
std::shared_ptr<Texture> atlasTexture;
|
std::shared_ptr<Texture> atlasTexture;
|
||||||
size_t atlasWidth = 0;
|
size_t atlasWidth = 0;
|
||||||
size_t atlasHeight = 0;
|
size_t atlasHeight = 0;
|
||||||
float lineHeight = 32.0f;
|
|
||||||
|
|
||||||
VertexRenderStruct textMesh;
|
VertexRenderStruct textMesh;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user