Fixing bugs, added girl

This commit is contained in:
Vladislav Khorev 2026-05-02 20:34:43 +03:00
parent d24748df1e
commit 6149182f02
21 changed files with 1255815 additions and 654 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/tree002.txt", "meshPath": "resources/w/exterior/tree003.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/tree002.txt", "meshPath": "resources/w/exterior/tree003.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/tree002.txt", "meshPath": "resources/w/exterior/tree003.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/tree002.txt", "meshPath": "resources/w/exterior/tree003.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/tree002.txt", "meshPath": "resources/w/exterior/tree003.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/tree002.txt", "meshPath": "resources/w/exterior/tree003.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/tree002.txt", "meshPath": "resources/w/exterior/tree003.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/tree002.txt", "meshPath": "resources/w/exterior/tree003.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/tree002.txt", "meshPath": "resources/w/exterior/tree003.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/tree002.txt", "meshPath": "resources/w/exterior/tree003.txt",
"rotationX": 0.0, "rotationX": 0.0,
"rotationY": -1.5707963267948966, "rotationY": -1.5707963267948966,
"rotationZ": 0.0, "rotationZ": 0.0,

View File

@ -37,16 +37,10 @@
{ {
"id": "npc_02_woman", "id": "npc_02_woman",
"name": "Студентка", "name": "Студентка",
"animationIdlePath": "resources/w/jam/woman_idle002.anim", "animationIdlePath": "resources/w/new_anims/girl_stand_idle005.txt",
"animationWalkPath": "resources/w/jam/woman_walk002.anim", "animationWalkPath": "resources/w/new_anims/girl_walk005.txt",
"meshTextures": { "meshTextures": {
"Body": "resources/w/jam/female_packed0_diffuse.png", "polySurface1": "resources/w/new_anims/Chat_02_diff.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,
@ -54,7 +48,7 @@
"facingAngle" : 3.141592, "facingAngle" : 3.141592,
"walkSpeed": 1.5, "walkSpeed": 1.5,
"rotationSpeed": 8.0, "rotationSpeed": 8.0,
"modelScale": 0.0001, "modelScale": 0.00016,
"modelCorrectionRotX": 0.0, "modelCorrectionRotX": 0.0,
"modelCorrectionRotY": 180.0, "modelCorrectionRotY": 180.0,
"modelCorrectionRotZ": 0.0, "modelCorrectionRotZ": 0.0,

View File

@ -22,6 +22,24 @@
"hover": "resources/w/red.png", "hover": "resources/w/red.png",
"pressed": "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"
}
}, },
{ {
"type": "FrameLayout", "type": "FrameLayout",

View File

@ -5,24 +5,6 @@
"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

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

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -342,7 +342,6 @@ void Character::update(int64_t deltaMs) {
} }
if (hitSparkEmitter.isConfigured()) { if (hitSparkEmitter.isConfigured()) {
hitSparkEmitter.update(static_cast<float>(deltaMs)); hitSparkEmitter.update(static_cast<float>(deltaMs));
} }

View File

@ -236,51 +236,9 @@ 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.uiManager.loadFromFile("resources/config2/ui_inventory.json", renderer, CONST_ZIP_FILE); menuManager.setup(inventory, 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;
@ -327,147 +285,6 @@ 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) {
@ -702,7 +519,7 @@ namespace ZL
break; break;
case SDLK_j: case SDLK_j:
toggleQuestJournal(); menuManager.toggleQuestJournal();
break; break;
case SDLK_RETURN: case SDLK_RETURN:

View File

@ -20,8 +20,6 @@
#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 {
@ -51,13 +49,6 @@ 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();
@ -107,11 +98,6 @@ 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,6 +157,7 @@ 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) {
@ -957,7 +958,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) { if (clickedNpc && player && clickedNpc->hp > 0) {
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) {
@ -966,7 +967,7 @@ namespace ZL
break; break;
} }
} }
if (npcIndex != -1 && clickedNpc->hp > 0) { if (npcIndex != -1) {
targetInteractiveObject = nullptr; targetInteractiveObject = nullptr;
if (clickedNpc->canAttack) { if (clickedNpc->canAttack) {

View File

@ -1,392 +1,198 @@
#include "MenuManager.h" #include "MenuManager.h"
#include <iostream> #include <iostream>
#include <algorithm>
#ifdef EMSCRIPTEN #include <string>
#include <emscripten.h>
#include <cstdlib>
#endif
namespace ZL { namespace ZL {
extern bool inverseVertical; 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 };
}
}
MenuManager::MenuManager(Renderer& iRenderer) : MenuManager::MenuManager(Renderer& iRenderer) :
renderer(iRenderer) renderer(iRenderer)
{ {
} }
/* void MenuManager::setup(Inventory& inv, const std::string& zipFile) {
void MenuManager::setupMenu() inventory = &inv;
{
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);
enterMainMenu(); ingameRoot = loadUiFromFile("resources/config2/ui_inventory.json", renderer, zipFile);
} questJournalRoot = loadUiFromFile("resources/config2/ui_quest_journal.json", renderer, zipFile);
bool MenuManager::shouldRenderSpace() const questJournal.loadFromFile("resources/quests/quests.json", zipFile);
{
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(); 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::enterGameplay() {
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(gameplayRoot); uiManager.replaceRoot(ingameRoot);
if (Environment::shipState.shipType == 1) uiManager.setNodeVisible("inventory_items_panel", false);
{ uiManager.setNodeVisible("close_inventory_button", false);
uiManager.findButton("shootButton")->state = ButtonState::Disabled; inventoryOpen = false;
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) uiManager.setTextButtonCallback("inventory_button", [this](const std::string&) {
{ std::cout << "[UI] Inventory button clicked" << std::endl;
forceSetupSpaceUICallback(); uiManager.setNodeVisible("inventory_items_panel", true);
} uiManager.setNodeVisible("close_inventory_button", true);
inventoryOpen = true;
if (auto btn = uiManager.findButton("takeButton")) btn->state = ButtonState::Disabled; const auto& items = inventory->getItems();
if (auto btn = uiManager.findButton("showPlayersButton")) std::string itemText;
{ if (items.empty()) {
btn->state = ButtonState::Disabled; 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.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.setText("inventory_items_text", itemText);
});
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.setButtonPressCallback("takeButton", [this](const std::string&) { uiManager.setTextButtonCallback("close_inventory_button", [this](const std::string&) {
if (onTakeButtonPressed) onTakeButtonPressed(); std::cout << "[UI] Close button clicked" << std::endl;
uiManager.setNodeVisible("inventory_items_panel", false);
uiManager.setNodeVisible("close_inventory_button", false);
inventoryOpen = false;
}); });
uiManager.setButtonCallback("showPlayersButton", [this](const std::string&) { uiManager.setTextButtonCallback("quest_journal_button", [this](const std::string&) {
if (onShowPlayersPressed) onShowPlayersPressed(); openQuestJournal();
});
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::enterHelp() void MenuManager::openQuestJournal() {
{ if (inventoryOpen) {
state = GameState::HelpScreen; uiManager.setNodeVisible("inventory_items_panel", false);
uiManager.replaceRoot(helpScreenRoot); uiManager.setNodeVisible("close_inventory_button", false);
inventoryOpen = false;
}
uiManager.setButtonCallback("infoButtonUnderlying_help", [this](const std::string&) { state = GameState::QuestJournal;
enterGameplay(); uiManager.pushMenuFromSavedRoot(questJournalRoot);
uiManager.setTextButtonCallback("quest_close_button", [this](const std::string&) {
closeQuestJournal();
});
for (int i = 0; i < 9; ++i) {
const std::string slotName = "quest_slot_" + std::to_string(i);
uiManager.setTextButtonCallback(slotName, [this, i](const std::string&) {
selectQuestByIndex(i);
}); });
} }
// ── State: ConnectionLost ───────────────────────────────────────────────── refreshQuestJournalUi();
if (!visibleQuestIds.empty()) {
selectQuestByIndex(0);
}
}
void MenuManager::enterConnectionLost() void MenuManager::closeQuestJournal() {
{ state = GameState::Gameplay;
state = GameState::ConnectionLost; selectedQuestIndex = -1;
uiManager.replaceRoot(connectionLostRoot); visibleQuestIds.clear();
uiManager.popMenu();
}
uiManager.setButtonCallback("reconnectButton", [this](const std::string&) { void MenuManager::toggleQuestJournal() {
enterConnecting(); std::cout << "[quest] toggleQuestJournal: " << (isQuestJournalOpen() ? "closing" : "opening") << std::endl;
if (onMultiplayerPressed) onMultiplayerPressed(pendingMultiNick, pendingMultiShipType); if (isQuestJournalOpen()) {
}); closeQuestJournal();
uiManager.setButtonCallback("exitServerButton", [this](const std::string&) { }
enterMainMenu(); else {
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) {
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 ? "> " : " ";
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);
} }
// ── Public event API ──────────────────────────────────────────────────────
void MenuManager::notifyConnected()
{
if (state == GameState::Connecting) {
enterGameplay();
} }
} }
void MenuManager::notifyConnectionFailed() void MenuManager::selectQuestByIndex(int index) {
{ if (index < 0 || index >= static_cast<int>(visibleQuestIds.size())) {
if (state == GameState::Connecting) { return;
enterConnectionFailed();
}
} }
void MenuManager::showGameOver(int score) selectedQuestIndex = index;
{ Quest::QuestState* quest = questJournal.findQuest(visibleQuestIds[index]);
if (state == GameState::Gameplay) { if (!quest) {
enterGameOver(score); return;
}
} }
void MenuManager::showConnectionLost() const auto& def = quest->definition;
{
if (state == GameState::Gameplay) { uiManager.setText("quest_middle_title_text", def.title);
enterConnectionLost(); 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 } // namespace ZL

View File

@ -3,86 +3,51 @@
#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,
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 setupMenu(); void setup(Inventory& inv, const std::string& zipFile);
void openQuestJournal();
void closeQuestJournal();
void toggleQuestJournal();
bool isQuestJournalOpen() const { return state == GameState::QuestJournal; }
/* protected:
// Returns true for states where Space should render and run (Gameplay, GameOver, ConnectionLost) Renderer& renderer;
bool shouldRenderSpace() const;
GameState getState() const { return state; }
// Called by game events private:
void showGameOver(int score); void enterGameplay();
void showConnectionLost(); void refreshQuestJournalUi();
void notifyConnected(); void selectQuestByIndex(int index);
void notifyConnectionFailed();
// Callbacks set by Game/Space GameState state = GameState::Gameplay;
std::function<void()> onMainMenuEntered; Inventory* inventory = nullptr;
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;
std::function<void()> forceSetupSpaceUICallback;*/ std::shared_ptr<UiNode> ingameRoot;
std::shared_ptr<UiNode> questJournalRoot;
bool inventoryOpen = false;
int selectedQuestIndex = -1;
std::vector<std::string> visibleQuestIds;
}; };
} // namespace ZL } // namespace ZL