Compare commits

..

No commits in common. "fb7b42ddf92794a8f6c4a9f645121abf9fdd7ad6" and "d24748df1e1dfd7df1903b28af67629f44f3dc26" have entirely different histories.

24 changed files with 26690 additions and 297863 deletions

View File

@ -29,7 +29,7 @@
{ {
"name": "tree001", "name": "tree001",
"texturePath": "resources/w/exterior/tree001.png", "texturePath": "resources/w/exterior/tree001.png",
"meshPath": "resources/w/exterior/tree003.txt", "meshPath": "resources/w/exterior/tree002.txt",
"rotationX": 0.0, "rotationX": 0.0,
"rotationY": -1.5707963267948966, "rotationY": -1.5707963267948966,
"rotationZ": 0.0, "rotationZ": 0.0,
@ -42,7 +42,7 @@
{ {
"name": "tree002", "name": "tree002",
"texturePath": "resources/w/exterior/tree001.png", "texturePath": "resources/w/exterior/tree001.png",
"meshPath": "resources/w/exterior/tree003.txt", "meshPath": "resources/w/exterior/tree002.txt",
"rotationX": 0.0, "rotationX": 0.0,
"rotationY": 1.5707963267948966, "rotationY": 1.5707963267948966,
"rotationZ": 0.0, "rotationZ": 0.0,
@ -55,7 +55,7 @@
{ {
"name": "tree003", "name": "tree003",
"texturePath": "resources/w/exterior/tree001.png", "texturePath": "resources/w/exterior/tree001.png",
"meshPath": "resources/w/exterior/tree003.txt", "meshPath": "resources/w/exterior/tree002.txt",
"rotationX": 0.0, "rotationX": 0.0,
"rotationY": 0.0, "rotationY": 0.0,
"rotationZ": 0.0, "rotationZ": 0.0,
@ -68,7 +68,7 @@
{ {
"name": "tree004", "name": "tree004",
"texturePath": "resources/w/exterior/tree001.png", "texturePath": "resources/w/exterior/tree001.png",
"meshPath": "resources/w/exterior/tree003.txt", "meshPath": "resources/w/exterior/tree002.txt",
"rotationX": 0.0, "rotationX": 0.0,
"rotationY": -1.5707963267948966, "rotationY": -1.5707963267948966,
"rotationZ": 0.0, "rotationZ": 0.0,
@ -81,7 +81,7 @@
{ {
"name": "tree005", "name": "tree005",
"texturePath": "resources/w/exterior/tree001.png", "texturePath": "resources/w/exterior/tree001.png",
"meshPath": "resources/w/exterior/tree003.txt", "meshPath": "resources/w/exterior/tree002.txt",
"rotationX": 0.0, "rotationX": 0.0,
"rotationY": 1.5707963267948966, "rotationY": 1.5707963267948966,
"rotationZ": 0.0, "rotationZ": 0.0,
@ -94,7 +94,7 @@
{ {
"name": "tree006", "name": "tree006",
"texturePath": "resources/w/exterior/tree001.png", "texturePath": "resources/w/exterior/tree001.png",
"meshPath": "resources/w/exterior/tree003.txt", "meshPath": "resources/w/exterior/tree002.txt",
"rotationX": 0.0, "rotationX": 0.0,
"rotationY": 0.0, "rotationY": 0.0,
"rotationZ": 0.0, "rotationZ": 0.0,
@ -107,7 +107,7 @@
{ {
"name": "tree007", "name": "tree007",
"texturePath": "resources/w/exterior/tree001.png", "texturePath": "resources/w/exterior/tree001.png",
"meshPath": "resources/w/exterior/tree003.txt", "meshPath": "resources/w/exterior/tree002.txt",
"rotationX": 0.0, "rotationX": 0.0,
"rotationY": -1.5707963267948966, "rotationY": -1.5707963267948966,
"rotationZ": 0.0, "rotationZ": 0.0,
@ -120,7 +120,7 @@
{ {
"name": "tree008", "name": "tree008",
"texturePath": "resources/w/exterior/tree001.png", "texturePath": "resources/w/exterior/tree001.png",
"meshPath": "resources/w/exterior/tree003.txt", "meshPath": "resources/w/exterior/tree002.txt",
"rotationX": 0.0, "rotationX": 0.0,
"rotationY": 1.5707963267948966, "rotationY": 1.5707963267948966,
"rotationZ": 0.0, "rotationZ": 0.0,
@ -133,7 +133,7 @@
{ {
"name": "tree009", "name": "tree009",
"texturePath": "resources/w/exterior/tree001.png", "texturePath": "resources/w/exterior/tree001.png",
"meshPath": "resources/w/exterior/tree003.txt", "meshPath": "resources/w/exterior/tree002.txt",
"rotationX": 0.0, "rotationX": 0.0,
"rotationY": 0.0, "rotationY": 0.0,
"rotationZ": 0.0, "rotationZ": 0.0,
@ -146,7 +146,7 @@
{ {
"name": "tree010", "name": "tree010",
"texturePath": "resources/w/exterior/tree001.png", "texturePath": "resources/w/exterior/tree001.png",
"meshPath": "resources/w/exterior/tree003.txt", "meshPath": "resources/w/exterior/tree002.txt",
"rotationX": 0.0, "rotationX": 0.0,
"rotationY": -1.5707963267948966, "rotationY": -1.5707963267948966,
"rotationZ": 0.0, "rotationZ": 0.0,

View File

@ -1,46 +0,0 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "TextButton",
"name": "inventory_button",
"x": 50.0,
"y": 50.0,
"width": 150.0,
"height": 60.0,
"text": "Inventory",
"fontSize": 24,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": true,
"color": [1.0, 1.0, 1.0, 1.0],
"textures": {
"normal": "resources/w/red.png",
"hover": "resources/w/red.png",
"pressed": "resources/w/red.png"
}
},
{
"type": "TextButton",
"name": "quest_journal_button",
"x": 220.0,
"y": 50.0,
"width": 170.0,
"height": 60.0,
"text": "Quests",
"fontSize": 24,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": true,
"color": [1.0, 1.0, 1.0, 1.0],
"textures": {
"normal": "resources/w/red.png",
"hover": "resources/w/red.png",
"pressed": "resources/w/red.png"
}
}
]
}
}

View File

@ -37,10 +37,16 @@
{ {
"id": "npc_02_woman", "id": "npc_02_woman",
"name": "Студентка", "name": "Студентка",
"animationIdlePath": "resources/w/new_anims/girl_stand_idle005.txt", "animationIdlePath": "resources/w/jam/woman_idle002.anim",
"animationWalkPath": "resources/w/new_anims/girl_walk005.txt", "animationWalkPath": "resources/w/jam/woman_walk002.anim",
"meshTextures": { "meshTextures": {
"polySurface1": "resources/w/new_anims/Chat_02_diff.png" "Body": "resources/w/jam/female_packed0_diffuse.png",
"Bottoms": "resources/w/jam/female_packed3_diffuse.png",
"Eyelashes": "resources/w/jam/female_packed0_diffuse.png",
"Eyes": "resources/w/jam/female_packed0_diffuse.png",
"Hair": "resources/w/jam/female_packed2_diffuse.png",
"Shoes": "resources/w/jam/female_packed1_diffuse.png",
"Tops": "resources/w/jam/female_packed2_diffuse.png"
}, },
"positionX": 19.5, "positionX": 19.5,
"positionY": 0.0, "positionY": 0.0,
@ -48,7 +54,7 @@
"facingAngle" : 3.141592, "facingAngle" : 3.141592,
"walkSpeed": 1.5, "walkSpeed": 1.5,
"rotationSpeed": 8.0, "rotationSpeed": 8.0,
"modelScale": 0.00016, "modelScale": 0.0001,
"modelCorrectionRotX": 0.0, "modelCorrectionRotX": 0.0,
"modelCorrectionRotY": 180.0, "modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0, "modelCorrectionRotZ": 0.0,

View File

@ -5,6 +5,24 @@
"width": "match_parent", "width": "match_parent",
"height": "match_parent", "height": "match_parent",
"children": [ "children": [
{
"type": "TextButton",
"name": "inventory_button",
"x": 50.0,
"y": 50.0,
"width": 150.0,
"height": 60.0,
"text": "Inventory",
"fontSize": 24,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": true,
"color": [1.0, 1.0, 1.0, 1.0],
"textures": {
"normal": "resources/w/red.png",
"hover": "resources/w/red.png",
"pressed": "resources/w/red.png"
}
},
{ {
"type": "FrameLayout", "type": "FrameLayout",
"name": "inventory_items_panel", "name": "inventory_items_panel",
@ -22,22 +40,22 @@
"height": 420.0, "height": 420.0,
"texture": "resources/w/red.png" "texture": "resources/w/red.png"
}, },
{ {
"type": "TextView", "type": "TextView",
"name": "inventory_title_text", "name": "inventory_title_text",
"x": 20.0, "x": 20.0,
"y": 18.0, "y": 18.0,
"width": 230.0, "width": 230.0,
"height": 34.0, "height": 34.0,
"text": "Inventory", "text": "Inventory",
"fontSize": 24, "fontSize": 24,
"fontPath": "resources/fonts/DroidSans.ttf", "fontPath": "resources/fonts/DroidSans.ttf",
"centered": false, "centered": false,
"topAligned": true, "topAligned": true,
"paddingX": 0.0, "paddingX": 0.0,
"paddingY": 0.0, "paddingY": 0.0,
"color": [1.0, 1.0, 1.0, 1.0] "color": [1.0, 1.0, 1.0, 1.0]
}, },
{ {
"type": "TextView", "type": "TextView",
"name": "inventory_items_text", "name": "inventory_items_text",
@ -58,21 +76,21 @@
}, },
{ {
"type": "TextButton", "type": "TextButton",
"name": "close_inventory_button", "name": "close_inventory_button",
"x": 266.0, "x": 266.0,
"y": 16.0, "y": 16.0,
"width": 40.0, "width": 40.0,
"height": 40.0, "height": 40.0,
"text": "X", "text": "X",
"fontSize": 20, "fontSize": 20,
"fontPath": "resources/fonts/DroidSans.ttf", "fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": true, "textCentered": true,
"color": [1.0, 1.0, 1.0, 1.0], "color": [1.0, 1.0, 1.0, 1.0],
"textures": { "textures": {
"normal": "resources/w/blue.png", "normal": "resources/w/blue.png",
"hover": "resources/w/blue.png", "hover": "resources/w/blue.png",
"pressed": "resources/w/blue.png" "pressed": "resources/w/blue.png"
} }
} }
] ]
} }

View File

@ -5,6 +5,24 @@
"width": "match_parent", "width": "match_parent",
"height": "match_parent", "height": "match_parent",
"children": [ "children": [
{
"type": "TextButton",
"name": "quest_journal_button",
"x": 220.0,
"y": 50.0,
"width": 170.0,
"height": 60.0,
"text": "Quests",
"fontSize": 24,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": true,
"color": [1.0, 1.0, 1.0, 1.0],
"textures": {
"normal": "resources/w/red.png",
"hover": "resources/w/red.png",
"pressed": "resources/w/red.png"
}
},
{ {
"type": "FrameLayout", "type": "FrameLayout",
"name": "quest_journal_panel", "name": "quest_journal_panel",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
resources/w/jam/woman_idle001.anim (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/w/jam/woman_idle002.anim (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/w/jam/woman_walk001.anim (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/w/jam/woman_walk002.anim (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/w/new_anims/Chat_02_diff.png (Stored with Git LFS)

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -341,6 +341,7 @@ void Character::update(int64_t deltaMs) {
facingAngle += (angleDiff > 0.f ? rotStep : -rotStep); facingAngle += (angleDiff > 0.f ? rotStep : -rotStep);
} }
if (hitSparkEmitter.isConfigured()) { if (hitSparkEmitter.isConfigured()) {
hitSparkEmitter.update(static_cast<float>(deltaMs)); hitSparkEmitter.update(static_cast<float>(deltaMs));

View File

@ -236,9 +236,51 @@ namespace ZL
std::cout << "Load resurces step 13" << std::endl; std::cout << "Load resurces step 13" << std::endl;
// Load UI with inventory button
try { try {
menuManager.setup(inventory, CONST_ZIP_FILE); menuManager.uiManager.loadFromFile("resources/config2/ui_inventory.json", renderer, CONST_ZIP_FILE);
menuManager.uiManager.appendFromFile("resources/config2/ui_quest_journal.json", renderer, CONST_ZIP_FILE);
questJournal.loadFromFile("resources/quests/quests.json", CONST_ZIP_FILE);
setupQuestJournalUi();
std::cout << "UI loaded successfully" << std::endl; std::cout << "UI loaded successfully" << std::endl;
menuManager.uiManager.setNodeVisible("inventory_items_panel", false);
menuManager.uiManager.setNodeVisible("close_inventory_button", false);
menuManager.uiManager.setTextButtonCallback("inventory_button", [this](const std::string& name) {
std::cout << "[UI] Inventory button clicked" << std::endl;
if (this->questJournalOpen) {
this->toggleQuestJournal();
}
this->menuManager.uiManager.setNodeVisible("inventory_items_panel", true);
this->menuManager.uiManager.setNodeVisible("close_inventory_button", true);
this->inventoryOpen = true;
// Update UI with current items
const auto& items = this->inventory.getItems();
std::string itemText;
if (items.empty()) {
itemText = "Inventory (Empty)";
}
else {
itemText = "Inventory (" + std::to_string(items.size()) + " items)\n\n";
for (size_t i = 0; i < items.size(); ++i) {
itemText += std::to_string(i + 1) + ". " + items[i].name + "\n";
}
}
this->menuManager.uiManager.setText("inventory_items_text", itemText);
});
menuManager.uiManager.setTextButtonCallback("close_inventory_button", [this](const std::string& name) {
std::cout << "[UI] Close button clicked" << std::endl;
menuManager.uiManager.setNodeVisible("inventory_items_panel", false);
menuManager.uiManager.setNodeVisible("close_inventory_button", false);
inventoryOpen = false;
});
} }
catch (const std::exception& e) { catch (const std::exception& e) {
std::cerr << "Failed to load UI: " << e.what() << std::endl; std::cerr << "Failed to load UI: " << e.what() << std::endl;
@ -285,6 +327,147 @@ namespace ZL
static int questStatusPriority(Quest::QuestStatus status) {
switch (status) {
case Quest::QuestStatus::Active: return 0;
case Quest::QuestStatus::Available: return 1;
case Quest::QuestStatus::Completed: return 2;
case Quest::QuestStatus::Failed: return 3;
default: return 4;
}
}
static std::array<float, 4> questStatusColor(Quest::QuestStatus status) {
switch (status) {
case Quest::QuestStatus::Completed: return { 0.25f, 0.95f, 0.35f, 1.0f };
case Quest::QuestStatus::Failed: return { 1.0f, 0.25f, 0.25f, 1.0f };
case Quest::QuestStatus::Active: return { 1.0f, 1.0f, 1.0f, 1.0f };
case Quest::QuestStatus::Available: return { 0.86f, 0.86f, 0.86f, 1.0f };
default: return { 0.45f, 0.45f, 0.45f, 1.0f };
}
}
void Game::setupQuestJournalUi() {
questJournalOpen = false;
selectedQuestIndex = -1;
visibleQuestIds.clear();
menuManager.uiManager.setNodeVisible("quest_journal_panel", false);
menuManager.uiManager.setNodeVisible("quest_close_button", false);
menuManager.uiManager.setTextButtonCallback("quest_journal_button", [this](const std::string&) {
toggleQuestJournal();
});
menuManager.uiManager.setTextButtonCallback("quest_close_button", [this](const std::string&) {
if (questJournalOpen) {
toggleQuestJournal();
}
});
for (int i = 0; i < 9; ++i) {
const std::string slotName = "quest_slot_" + std::to_string(i);
menuManager.uiManager.setTextButtonCallback(slotName, [this, i](const std::string&) {
selectQuestByIndex(i);
});
menuManager.uiManager.setNodeVisible(slotName, false);
}
}
void Game::toggleQuestJournal() {
questJournalOpen = !questJournalOpen;
std::cout << "[quest] toggleQuestJournal: " << (questJournalOpen ? "open" : "closed") << std::endl;
if (questJournalOpen) {
if (inventoryOpen) {
menuManager.uiManager.setNodeVisible("inventory_items_panel", false);
menuManager.uiManager.setNodeVisible("close_inventory_button", false);
inventoryOpen = false;
}
}
menuManager.uiManager.setNodeVisible("quest_journal_panel", questJournalOpen);
menuManager.uiManager.setNodeVisible("quest_close_button", questJournalOpen);
if (questJournalOpen) {
refreshQuestJournalUi();
if (!visibleQuestIds.empty()) {
selectQuestByIndex(0);
}
}
}
void Game::refreshQuestJournalUi() {
visibleQuestIds.clear();
auto quests = questJournal.getVisibleQuests();
std::sort(quests.begin(), quests.end(), [](const Quest::QuestState* a, const Quest::QuestState* b) {
const int pa = questStatusPriority(a->status);
const int pb = questStatusPriority(b->status);
if (pa != pb) return pa < pb;
// Newer quests are shown above older quests inside the same status bucket.
return a->orderIndex > b->orderIndex;
});
for (int i = 0; i < 9; ++i) {
const std::string slotName = "quest_slot_" + std::to_string(i);
if (i < static_cast<int>(quests.size())) {
const auto* quest = quests[i];
visibleQuestIds.push_back(quest->definition.id);
const bool selected = (i == selectedQuestIndex);
const std::string prefix = selected ? "> " : " ";
menuManager.uiManager.setTextButtonText(slotName, prefix + quest->definition.title);
menuManager.uiManager.setTextButtonColor(slotName, questStatusColor(quest->status));
menuManager.uiManager.setNodeVisible(slotName, true);
}
else {
menuManager.uiManager.setTextButtonText(slotName, "");
menuManager.uiManager.setNodeVisible(slotName, false);
}
}
}
void Game::selectQuestByIndex(int index) {
if (index < 0 || index >= static_cast<int>(visibleQuestIds.size())) {
return;
}
selectedQuestIndex = index;
Quest::QuestState* quest = questJournal.findQuest(visibleQuestIds[index]);
if (!quest) {
return;
}
const auto& def = quest->definition;
menuManager.uiManager.setText("quest_middle_title_text", def.title);
menuManager.uiManager.setTextColor("quest_middle_title_text", questStatusColor(quest->status));
const std::string meta = std::string("Category: ") + Quest::toString(def.category)
+ " | Status: " + Quest::toString(quest->status)
+ " | Level: " + std::to_string(def.recommendedLevel);
menuManager.uiManager.setText("quest_meta_text", meta);
std::string objectivesText;
for (size_t i = 0; i < def.objectives.size(); ++i) {
const auto& obj = def.objectives[i];
const bool isActive = static_cast<int>(i) == quest->activeObjectiveIndex;
const std::string mark = obj.completed ? "[x] " : (isActive ? "> [ ] " : "[ ] ");
objectivesText += mark + obj.text;
if (i + 1 < def.objectives.size()) {
objectivesText += "\n";
}
}
menuManager.uiManager.setText("quest_objectives_text", objectivesText);
menuManager.uiManager.setText("quest_lore_title_text", "Описание задания");
menuManager.uiManager.setText("quest_description_text", def.description);
refreshQuestJournalUi();
}
void Game::drawScene() { void Game::drawScene() {
glViewport(0, 0, Environment::width, Environment::height); glViewport(0, 0, Environment::width, Environment::height);
if (!loadingCompleted) { if (!loadingCompleted) {
@ -519,7 +702,7 @@ namespace ZL
break; break;
case SDLK_j: case SDLK_j:
menuManager.toggleQuestJournal(); toggleQuestJournal();
break; break;
case SDLK_RETURN: case SDLK_RETURN:

View File

@ -20,6 +20,8 @@
#include <unordered_set> #include <unordered_set>
#include "Location.h" #include "Location.h"
#include "AudioPlayerAsync.h" #include "AudioPlayerAsync.h"
#include "quest/QuestJournal.h"
namespace ZL { namespace ZL {
class Game { class Game {
@ -49,6 +51,13 @@ namespace ZL {
Inventory inventory; Inventory inventory;
InteractiveObject* pickedUpObject = nullptr; InteractiveObject* pickedUpObject = nullptr;
bool inventoryOpen = false;
ZL::Quest::QuestJournal questJournal;
bool questJournalOpen = false;
int selectedQuestIndex = -1;
std::vector<std::string> visibleQuestIds;
MenuManager menuManager; MenuManager menuManager;
void activateSlowMoEffect(); void activateSlowMoEffect();
@ -98,6 +107,11 @@ namespace ZL {
void endPinch(); void endPinch();
int countNonUiPointers() const; int countNonUiPointers() const;
void setupQuestJournalUi();
void toggleQuestJournal();
void refreshQuestJournalUi();
void selectQuestByIndex(int index);
#ifdef EMSCRIPTEN #ifdef EMSCRIPTEN
static Game* s_instance; static Game* s_instance;
static void onResourcesZipLoaded(const char* filename); static void onResourcesZipLoaded(const char* filename);

View File

@ -157,7 +157,6 @@ namespace ZL
teleportSparks->setUseWorldSpace(true); teleportSparks->setUseWorldSpace(true);
teleportSparks->markConfigured(); teleportSparks->markConfigured();
// If the player happens to spawn already inside the zone, treat them // If the player happens to spawn already inside the zone, treat them
// as in-zone so they don't immediately teleport on the first update. // as in-zone so they don't immediately teleport on the first update.
if (player && (player->position - teleportPosition).norm() <= teleportRadius) { if (player && (player->position - teleportPosition).norm() <= teleportRadius) {
@ -958,7 +957,7 @@ namespace ZL
// Check if we clicked on an NPC // Check if we clicked on an NPC
Character* clickedNpc = raycastNpcs(camPos, rayDir); Character* clickedNpc = raycastNpcs(camPos, rayDir);
if (clickedNpc && player && clickedNpc->hp > 0) { if (clickedNpc && player) {
float distance = (player->position - clickedNpc->position).norm(); float distance = (player->position - clickedNpc->position).norm();
int npcIndex = -1; int npcIndex = -1;
for (size_t i = 0; i < npcs.size(); ++i) { for (size_t i = 0; i < npcs.size(); ++i) {
@ -967,7 +966,7 @@ namespace ZL
break; break;
} }
} }
if (npcIndex != -1) { if (npcIndex != -1 && clickedNpc->hp > 0) {
targetInteractiveObject = nullptr; targetInteractiveObject = nullptr;
if (clickedNpc->canAttack) { if (clickedNpc->canAttack) {

View File

@ -1,196 +1,392 @@
#include "MenuManager.h" #include "MenuManager.h"
#include <iostream> #include <iostream>
#include <algorithm>
#include <string> #ifdef EMSCRIPTEN
#include <emscripten.h>
#include <cstdlib>
#endif
namespace ZL { namespace ZL {
static int questStatusPriority(Quest::QuestStatus status) { extern bool inverseVertical;
switch (status) {
case Quest::QuestStatus::Active: return 0;
case Quest::QuestStatus::Available: return 1;
case Quest::QuestStatus::Completed: return 2;
case Quest::QuestStatus::Failed: return 3;
default: return 4;
}
}
static std::array<float, 4> questStatusColor(Quest::QuestStatus status) {
switch (status) {
case Quest::QuestStatus::Completed: return { 0.25f, 0.95f, 0.35f, 1.0f };
case Quest::QuestStatus::Failed: return { 1.0f, 0.25f, 0.25f, 1.0f };
case Quest::QuestStatus::Active: return { 1.0f, 1.0f, 1.0f, 1.0f };
case Quest::QuestStatus::Available: return { 0.86f, 0.86f, 0.86f, 1.0f };
default: return { 0.45f, 0.45f, 0.45f, 1.0f };
}
}
MenuManager::MenuManager(Renderer& iRenderer) : MenuManager::MenuManager(Renderer& iRenderer) :
renderer(iRenderer) renderer(iRenderer)
{ {
} }
void MenuManager::setup(Inventory& inv, const std::string& zipFile) { /*
inventory = &inv; void MenuManager::setupMenu()
{
mainMenuRoot = loadUiFromFile("resources/config/main_menu.json", renderer, CONST_ZIP_FILE);
aboutMenuRoot = loadUiFromFile("resources/config/about.json", renderer, CONST_ZIP_FILE);
shipSelectionRoot = loadUiFromFile("resources/config/ship_selection_menu.json", renderer, CONST_ZIP_FILE);
connectingRoot = loadUiFromFile("resources/config/connecting.json", renderer, CONST_ZIP_FILE);
connectionFailedRoot= loadUiFromFile("resources/config/connection_failed.json", renderer, CONST_ZIP_FILE);
gameplayRoot = loadUiFromFile("resources/config/ui.json", renderer, CONST_ZIP_FILE);
gameOverRoot = loadUiFromFile("resources/config/game_over.json", renderer, CONST_ZIP_FILE);
helpScreenRoot = loadUiFromFile("resources/config/ui_with_help.json", renderer, CONST_ZIP_FILE);
connectionLostRoot = loadUiFromFile("resources/config/connection_lost.json", renderer, CONST_ZIP_FILE);
hudRoot = loadUiFromFile("resources/config2/hud.json", renderer, zipFile); enterMainMenu();
inventoryRoot = loadUiFromFile("resources/config2/ui_inventory.json", renderer, zipFile);
questJournalRoot = loadUiFromFile("resources/config2/ui_quest_journal.json", renderer, zipFile);
questJournal.loadFromFile("resources/quests/quests.json", zipFile);
enterGameplay();
} }
void MenuManager::enterGameplay() { bool MenuManager::shouldRenderSpace() const
{
return state == GameState::Gameplay
|| state == GameState::GameOver
|| state == GameState::HelpScreen
|| state == GameState::ConnectionLost;
}
// ── State: MainMenu ──────────────────────────────────────────────────────
void MenuManager::enterMainMenu()
{
state = GameState::MainMenu;
uiManager.replaceRoot(mainMenuRoot);
if (onMainMenuEntered) onMainMenuEntered();
uiManager.setButtonCallback("singleButton", [this](const std::string&) {
enterShipSelectionSingle();
});
uiManager.setButtonCallback("multiplayerButton", [this](const std::string&) {
enterShipSelectionMulti();
});
uiManager.setButtonCallback("aboutButton", [this](const std::string&) {
enterAboutMenu();
});
}
void MenuManager::enterAboutMenu()
{
state = GameState::AboutMenu;
uiManager.replaceRoot(aboutMenuRoot);
uiManager.setButtonCallback("aboutBackButton", [this](const std::string&) {
enterMainMenu();
});
}
// ── State: ShipSelectionSingle ───────────────────────────────────────────
void MenuManager::enterShipSelectionSingle()
{
state = GameState::ShipSelectionSingle;
uiManager.replaceRoot(shipSelectionRoot);
std::string initialNick;
#ifdef EMSCRIPTEN
char* savedNickC = emscripten_run_script_string("localStorage.getItem('spacegame_nick') || ''");
if (savedNickC) {
initialNick = savedNickC;
free(savedNickC);
}
#endif
auto tf = uiManager.findTextField("nicknameInput");
if (tf) {
if (!initialNick.empty()) tf->text = initialNick;
#ifdef EMSCRIPTEN
uiManager.setTextFieldCallback("nicknameInput", [](const std::string&, const std::string& value) {
EM_ASM_({
try { localStorage.setItem('spacegame_nick', UTF8ToString($0)); } catch(e) {}
}, value.c_str());
});
#endif
}
uiManager.setButtonCallback("spaceshipButton", [this, initialNick](const std::string&) {
std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = initialNick;
if (nick.empty()) nick = "Player";
enterGameplay();
if (onSingleplayerPressed) onSingleplayerPressed(nick, 0);
});
uiManager.setButtonCallback("cargoshipButton", [this, initialNick](const std::string&) {
std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = initialNick;
if (nick.empty()) nick = "Player";
enterGameplay();
if (onSingleplayerPressed) onSingleplayerPressed(nick, 1);
});
uiManager.setButtonCallback("backButton", [this](const std::string&) {
enterMainMenu();
});
}
// ── State: ShipSelectionMulti ─────────────────────────────────────────────
void MenuManager::enterShipSelectionMulti()
{
state = GameState::ShipSelectionMulti;
uiManager.replaceRoot(shipSelectionRoot);
std::string initialNick;
#ifdef EMSCRIPTEN
char* savedNickC = emscripten_run_script_string("localStorage.getItem('spacegame_nick') || ''");
if (savedNickC) {
initialNick = savedNickC;
free(savedNickC);
}
#endif
auto tf = uiManager.findTextField("nicknameInput");
if (tf) {
if (!initialNick.empty()) tf->text = initialNick;
#ifdef EMSCRIPTEN
uiManager.setTextFieldCallback("nicknameInput", [](const std::string&, const std::string& value) {
EM_ASM_({
try { localStorage.setItem('spacegame_nick', UTF8ToString($0)); } catch(e) {}
}, value.c_str());
});
#endif
}
uiManager.setButtonCallback("spaceshipButton", [this, initialNick](const std::string&) {
std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = initialNick;
if (nick.empty()) nick = "Player";
pendingMultiNick = nick;
pendingMultiShipType = 0;
enterConnecting();
if (onMultiplayerPressed) onMultiplayerPressed(nick, 0);
});
uiManager.setButtonCallback("cargoshipButton", [this, initialNick](const std::string&) {
std::string nick = uiManager.getTextFieldValue("nicknameInput");
if (nick.empty()) nick = initialNick;
if (nick.empty()) nick = "Player";
pendingMultiNick = nick;
pendingMultiShipType = 1;
enterConnecting();
if (onMultiplayerPressed) onMultiplayerPressed(nick, 1);
});
uiManager.setButtonCallback("backButton", [this](const std::string&) {
enterMainMenu();
});
}
// ── State: Connecting ────────────────────────────────────────────────────
void MenuManager::enterConnecting()
{
state = GameState::Connecting;
uiManager.replaceRoot(connectingRoot);
// No interactive elements — just a static "Connecting..." image
}
// ── State: ConnectionFailed ───────────────────────────────────────────────
void MenuManager::enterConnectionFailed()
{
state = GameState::ConnectionFailed;
uiManager.replaceRoot(connectionFailedRoot);
uiManager.setButtonCallback("connectionFailedReconnectButton", [this](const std::string&) {
enterConnecting();
if (onMultiplayerPressed) onMultiplayerPressed(pendingMultiNick, pendingMultiShipType);
});
uiManager.setButtonCallback("connectionFailedGoBack", [this](const std::string&) {
enterMainMenu();
});
}
// ── State: Gameplay ──────────────────────────────────────────────────────
void MenuManager::enterGameplay()
{
state = GameState::Gameplay; state = GameState::Gameplay;
uiManager.replaceRoot(hudRoot); uiManager.replaceRoot(gameplayRoot);
uiManager.setTextButtonCallback("inventory_button", [this](const std::string&) { if (Environment::shipState.shipType == 1)
openInventory(); {
uiManager.findButton("shootButton")->state = ButtonState::Disabled;
uiManager.findButton("shootButton2")->state = ButtonState::Disabled;
}
else
{
if (Environment::shipState.velocity < 0.1)
{
uiManager.findButton("minusButton")->state = ButtonState::Disabled;
uiManager.findButton("plusButton")->state = ButtonState::Normal;
uiManager.findButton("shootButton")->state = ButtonState::Normal;
uiManager.findButton("shootButton2")->state = ButtonState::Normal;
}
else if (Environment::shipState.velocity >= 0.1 && Environment::shipState.velocity <= 200)
{
uiManager.findButton("minusButton")->state = ButtonState::Normal;
uiManager.findButton("plusButton")->state = ButtonState::Normal;
uiManager.findButton("shootButton")->state = ButtonState::Normal;
uiManager.findButton("shootButton2")->state = ButtonState::Normal;
}
else if (Environment::shipState.velocity > 200 && Environment::shipState.velocity < 400 - 0.1)
{
uiManager.findButton("minusButton")->state = ButtonState::Normal;
uiManager.findButton("plusButton")->state = ButtonState::Normal;
uiManager.findButton("shootButton")->state = ButtonState::Disabled;
uiManager.findButton("shootButton2")->state = ButtonState::Disabled;
}
else if (Environment::shipState.velocity >= 400 - 0.1)
{
uiManager.findButton("minusButton")->state = ButtonState::Normal;
uiManager.findButton("plusButton")->state = ButtonState::Disabled;
uiManager.findButton("shootButton")->state = ButtonState::Disabled;
uiManager.findButton("shootButton2")->state = ButtonState::Disabled;
}
}
if (forceSetupSpaceUICallback)
{
forceSetupSpaceUICallback();
}
if (auto btn = uiManager.findButton("takeButton")) btn->state = ButtonState::Disabled;
if (auto btn = uiManager.findButton("showPlayersButton"))
{
btn->state = ButtonState::Disabled;
}
uiManager.setButtonPressCallback("shootButton", [this](const std::string&) {
if (onFirePressed) onFirePressed();
});
uiManager.setButtonPressCallback("shootButton2", [this](const std::string&) {
if (onFirePressed) onFirePressed();
});
uiManager.setButtonPressCallback("plusButton", [this](const std::string&) {
int newVel = Environment::shipState.selectedVelocity + 1;
if (newVel > 4) newVel = 4;
uiManager.findButton("minusButton")->state = ButtonState::Normal;
if (newVel == 4)
{
uiManager.findButton("plusButton")->state = ButtonState::Disabled;
}
else
{
uiManager.findButton("plusButton")->state = ButtonState::Normal;
}
if (onVelocityChanged) onVelocityChanged(newVel);
});
uiManager.setButtonPressCallback("minusButton", [this](const std::string&) {
int newVel = Environment::shipState.selectedVelocity - 1;
if (newVel < 0) newVel = 0;
uiManager.findButton("plusButton")->state = ButtonState::Normal;
if (newVel == 0)
{
uiManager.findButton("minusButton")->state = ButtonState::Disabled;
}
else
{
uiManager.findButton("minusButton")->state = ButtonState::Normal;
}
if (onVelocityChanged) onVelocityChanged(newVel);
}); });
uiManager.setTextButtonCallback("quest_journal_button", [this](const std::string&) { uiManager.setButtonPressCallback("takeButton", [this](const std::string&) {
openQuestJournal(); if (onTakeButtonPressed) onTakeButtonPressed();
});
uiManager.setButtonCallback("showPlayersButton", [this](const std::string&) {
if (onShowPlayersPressed) onShowPlayersPressed();
});
uiManager.setButtonPressCallback("inverseMouseButton", [this](const std::string&) {
inverseVertical = !inverseVertical;
std::cout << "Inverse mouse: " << (inverseVertical ? "ON" : "OFF") << std::endl;
});
uiManager.setButtonCallback("infoButton", [this](const std::string&) {
//if (onShowPlayersPressed) onShowPlayersPressed();
enterHelp();
});
}
// ── State: GameOver ──────────────────────────────────────────────────────
void MenuManager::enterGameOver(int score)
{
state = GameState::GameOver;
uiManager.replaceRoot(gameOverRoot);
uiManager.setText("scoreText", "Score: " + std::to_string(score));
uiManager.setButtonCallback("restartButton", [this](const std::string&) {
if (onRestartPressed) onRestartPressed();
enterGameplay();
});
uiManager.setButtonCallback("gameOverExitButton", [this](const std::string&) {
enterMainMenu();
}); });
} }
void MenuManager::openInventory() { void MenuManager::enterHelp()
state = GameState::Inventory; {
uiManager.pushMenuFromSavedRoot(inventoryRoot); state = GameState::HelpScreen;
uiManager.replaceRoot(helpScreenRoot);
uiManager.setTextButtonCallback("close_inventory_button", [this](const std::string&) { uiManager.setButtonCallback("infoButtonUnderlying_help", [this](const std::string&) {
closeInventory(); enterGameplay();
}); });
const auto& items = inventory->getItems();
std::string itemText;
if (items.empty()) {
itemText = "Inventory (Empty)";
}
else {
itemText = "Inventory (" + std::to_string(items.size()) + " items)\n\n";
for (size_t i = 0; i < items.size(); ++i) {
itemText += std::to_string(i + 1) + ". " + items[i].name + "\n";
}
}
uiManager.setText("inventory_items_text", itemText);
} }
void MenuManager::closeInventory() { // ── State: ConnectionLost ─────────────────────────────────────────────────
state = GameState::Gameplay;
uiManager.popMenu();
}
void MenuManager::openQuestJournal() { void MenuManager::enterConnectionLost()
state = GameState::QuestJournal; {
uiManager.pushMenuFromSavedRoot(questJournalRoot); state = GameState::ConnectionLost;
uiManager.replaceRoot(connectionLostRoot);
uiManager.setTextButtonCallback("quest_close_button", [this](const std::string&) { uiManager.setButtonCallback("reconnectButton", [this](const std::string&) {
closeQuestJournal(); enterConnecting();
if (onMultiplayerPressed) onMultiplayerPressed(pendingMultiNick, pendingMultiShipType);
}); });
uiManager.setButtonCallback("exitServerButton", [this](const std::string&) {
for (int i = 0; i < 9; ++i) { enterMainMenu();
const std::string slotName = "quest_slot_" + std::to_string(i);
uiManager.setTextButtonCallback(slotName, [this, i](const std::string&) {
selectQuestByIndex(i);
});
}
refreshQuestJournalUi();
if (!visibleQuestIds.empty()) {
selectQuestByIndex(0);
}
}
void MenuManager::closeQuestJournal() {
state = GameState::Gameplay;
selectedQuestIndex = -1;
visibleQuestIds.clear();
uiManager.popMenu();
}
void MenuManager::toggleQuestJournal() {
std::cout << "[quest] toggleQuestJournal: " << (isQuestJournalOpen() ? "closing" : "opening") << std::endl;
if (state == GameState::QuestJournal) {
closeQuestJournal();
}
else {
if (state == GameState::Inventory) {
closeInventory();
}
openQuestJournal();
}
}
void MenuManager::refreshQuestJournalUi() {
visibleQuestIds.clear();
auto quests = questJournal.getVisibleQuests();
std::sort(quests.begin(), quests.end(), [](const Quest::QuestState* a, const Quest::QuestState* b) {
const int pa = questStatusPriority(a->status);
const int pb = questStatusPriority(b->status);
if (pa != pb) return pa < pb;
return a->orderIndex > b->orderIndex;
}); });
}
for (int i = 0; i < 9; ++i) { // ── Public event API ──────────────────────────────────────────────────────
const std::string slotName = "quest_slot_" + std::to_string(i);
if (i < static_cast<int>(quests.size())) { void MenuManager::notifyConnected()
const auto* quest = quests[i]; {
visibleQuestIds.push_back(quest->definition.id); if (state == GameState::Connecting) {
enterGameplay();
const bool selected = (i == selectedQuestIndex);
const std::string prefix = selected ? "> " : " ";
uiManager.setTextButtonText(slotName, prefix + quest->definition.title);
uiManager.setTextButtonColor(slotName, questStatusColor(quest->status));
uiManager.setNodeVisible(slotName, true);
}
else {
uiManager.setTextButtonText(slotName, "");
uiManager.setNodeVisible(slotName, false);
}
} }
} }
void MenuManager::selectQuestByIndex(int index) { void MenuManager::notifyConnectionFailed()
if (index < 0 || index >= static_cast<int>(visibleQuestIds.size())) { {
return; if (state == GameState::Connecting) {
enterConnectionFailed();
} }
selectedQuestIndex = index;
Quest::QuestState* quest = questJournal.findQuest(visibleQuestIds[index]);
if (!quest) {
return;
}
const auto& def = quest->definition;
uiManager.setText("quest_middle_title_text", def.title);
uiManager.setTextColor("quest_middle_title_text", questStatusColor(quest->status));
const std::string meta = std::string("Category: ") + Quest::toString(def.category)
+ " | Status: " + Quest::toString(quest->status)
+ " | Level: " + std::to_string(def.recommendedLevel);
uiManager.setText("quest_meta_text", meta);
std::string objectivesText;
for (size_t i = 0; i < def.objectives.size(); ++i) {
const auto& obj = def.objectives[i];
const bool isActive = static_cast<int>(i) == quest->activeObjectiveIndex;
const std::string mark = obj.completed ? "[x] " : (isActive ? "> [ ] " : "[ ] ");
objectivesText += mark + obj.text;
if (i + 1 < def.objectives.size()) {
objectivesText += "\n";
}
}
uiManager.setText("quest_objectives_text", objectivesText);
uiManager.setText("quest_lore_title_text", "Описание задания");
uiManager.setText("quest_description_text", def.description);
refreshQuestJournalUi();
} }
} // namespace ZL void MenuManager::showGameOver(int score)
{
if (state == GameState::Gameplay) {
enterGameOver(score);
}
}
void MenuManager::showConnectionLost()
{
if (state == GameState::Gameplay) {
enterConnectionLost();
}
}*/
} // namespace ZL

View File

@ -3,58 +3,86 @@
#include "Environment.h" #include "Environment.h"
#include "render/TextureManager.h" #include "render/TextureManager.h"
#include "UiManager.h" #include "UiManager.h"
#include "items/Item.h"
#include "quest/QuestJournal.h"
#include <vector>
#include <string>
#include <memory>
namespace ZL { namespace ZL {
extern const char* CONST_ZIP_FILE; extern const char* CONST_ZIP_FILE;
enum class GameState { enum class GameState {
MainMenu,
AboutMenu,
ShipSelectionSingle,
ShipSelectionMulti,
Connecting,
ConnectionFailed,
Gameplay, Gameplay,
Inventory, GameOver,
QuestJournal HelpScreen,
ConnectionLost
}; };
class MenuManager { class MenuManager {
protected:
Renderer& renderer;
/*
// Pre-loaded UI roots (loaded once in setupMenu)
std::shared_ptr<UiNode> mainMenuRoot;
std::shared_ptr<UiNode> aboutMenuRoot;
std::shared_ptr<UiNode> shipSelectionRoot;
std::shared_ptr<UiNode> connectingRoot;
std::shared_ptr<UiNode> connectionFailedRoot;
std::shared_ptr<UiNode> gameplayRoot;
std::shared_ptr<UiNode> gameOverRoot;
std::shared_ptr<UiNode> helpScreenRoot;
std::shared_ptr<UiNode> connectionLostRoot;
// Stored for multiplayer retry
std::string pendingMultiNick;
int pendingMultiShipType = 0;
GameState state = GameState::MainMenu;
// State transition methods
void enterMainMenu();
void enterAboutMenu();
void enterShipSelectionSingle();
void enterShipSelectionMulti();
void enterConnecting();
void enterConnectionFailed();
void enterGameplay();
void enterGameOver(int score);
void enterHelp();
void enterConnectionLost();
*/
public: public:
UiManager uiManager; UiManager uiManager;
ZL::Quest::QuestJournal questJournal;
MenuManager(Renderer& iRenderer); MenuManager(Renderer& iRenderer);
void setup(Inventory& inv, const std::string& zipFile); //void setupMenu();
void openInventory(); /*
void closeInventory(); // Returns true for states where Space should render and run (Gameplay, GameOver, ConnectionLost)
bool shouldRenderSpace() const;
GameState getState() const { return state; }
void openQuestJournal(); // Called by game events
void closeQuestJournal(); void showGameOver(int score);
void toggleQuestJournal(); void showConnectionLost();
void notifyConnected();
void notifyConnectionFailed();
bool isInventoryOpen() const { return state == GameState::Inventory; } // Callbacks set by Game/Space
bool isQuestJournalOpen() const { return state == GameState::QuestJournal; } std::function<void()> onMainMenuEntered;
std::function<void()> onRestartPressed;
std::function<void(float)> onVelocityChanged;
std::function<void()> onFirePressed;
std::function<void()> onTakeButtonPressed;
std::function<void(const std::string&, int)> onSingleplayerPressed;
std::function<void(const std::string&, int)> onMultiplayerPressed;
std::function<void()> onShowPlayersPressed;
protected: std::function<void()> forceSetupSpaceUICallback;*/
Renderer& renderer;
private:
void enterGameplay();
void refreshQuestJournalUi();
void selectQuestByIndex(int index);
GameState state = GameState::Gameplay;
Inventory* inventory = nullptr;
std::shared_ptr<UiNode> hudRoot;
std::shared_ptr<UiNode> inventoryRoot;
std::shared_ptr<UiNode> questJournalRoot;
int selectedQuestIndex = -1;
std::vector<std::string> visibleQuestIds;
}; };
} // namespace ZL } // namespace ZL