Merge branch 'main' into linux

This commit is contained in:
Vladislav Khorev 2026-03-10 18:18:03 +00:00
commit d1f3d67062
46 changed files with 2199 additions and 84 deletions

View File

@ -30,13 +30,52 @@
border-radius: 5px;
}
#status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
/* Nick modal */
#nickOverlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.85);
z-index: 9999;
}
#nickBox {
background: #111;
border: 1px solid #444;
padding: 24px;
width: 320px;
box-shadow: 0 8px 24px rgba(0,0,0,0.6);
text-align: center;
}
#nickBox h2 { margin: 0 0 12px 0; font-size: 18px; color: #eee; }
#nickBox input[type="text"] {
width: 100%;
padding: 10px;
font-size: 16px;
box-sizing: border-box;
margin-bottom: 12px;
border: 1px solid #333;
background: #000;
color: #fff;
}
#nickBox button {
padding: 10px 16px;
font-size: 16px;
background: #2a9fd6;
border: none;
color: #fff;
cursor: pointer;
}
</style>
</head>
<body>
<button id="fs-button">Fullscreen</button>
<div id="status">Downloading...</div>
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
<!--
<script>
var statusElement = document.getElementById("status");
var canvas = document.getElementById("canvas");
@ -66,6 +105,128 @@
});
</script>
<script async src="space-game001.js"></script>
<script async src="space-game001.js"></script>-->
<div id="nickOverlay" style="display:none;">
<div id="nickBox">
<h2>Enter your nickname</h2>
<input id="nickInput" type="text" maxlength="32" placeholder="Player" />
<div>
<button id="nickSubmit">Start</button>
</div>
</div>
</div>
<script>
// Utility: подготовить глобальный Module до загрузки Emscripten-скрипта
function prepareModuleEnvironment() {
window.Module = window.Module || {};
var canvasEl = document.getElementById('canvas');
// Устанавливаем canvas для Emscripten, чтобы createContext не падал
window.Module.canvas = canvasEl;
// Подготовим заглушку setStatus, если ещё нет
window.Module.setStatus = window.Module.setStatus || function (text) {
var statusElement = document.getElementById("status");
statusElement.innerHTML = text;
statusElement.style.display = text ? 'block' : 'none';
};
}
// Show overlay only if no nickname saved.
function loadGameScript() {
var s = document.createElement('script');
s.src = 'space-game001.js';
s.async = true;
document.body.appendChild(s);
}
function showNickOverlay() {
var overlay = document.getElementById('nickOverlay');
overlay.style.display = 'flex';
var input = document.getElementById('nickInput');
input.focus();
}
function hideNickOverlay() {
var overlay = document.getElementById('nickOverlay');
overlay.style.display = 'none';
}
function saveNickAndStart(nick) {
try {
if (!nick || nick.trim() === '') nick = 'Player';
localStorage.setItem('spacegame_nick', nick);
} catch (e) {
console.warn('localStorage not available', e);
}
hideNickOverlay();
// перед загрузкой скрипта гарантируем, что Module.canvas задан
prepareModuleEnvironment();
loadGameScript();
}
document.getElementById('fs-button').addEventListener('click', function() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(function(e) {
console.error('Fullscreen error: ' + e.message);
});
} else {
document.exitFullscreen();
}
});
document.addEventListener('DOMContentLoaded', function() {
// Готовим Module сразу — даже если откроется модалка, поле canvas будет доступно для скрипта (если он загружается позже)
prepareModuleEnvironment();
var stored = null;
try {
stored = localStorage.getItem('spacegame_nick');
} catch (e) {
console.warn('localStorage not available', e);
}
if (stored && stored.trim() !== '') {
// Nick is present — start immediately
loadGameScript();
} else {
// Show modal to request nickname before loading WASM
showNickOverlay();
var submit = document.getElementById('nickSubmit');
var input = document.getElementById('nickInput');
submit.addEventListener('click', function() {
saveNickAndStart(input.value);
});
input.addEventListener('input', function() {
// Strip any character that is not a-z, A-Z, 0-9 or space
var pos = this.selectionStart;
var cleaned = this.value.replace(/[^a-zA-Z0-9 ]/g, '');
if (cleaned !== this.value) {
this.value = cleaned;
this.setSelectionRange(pos - 1, pos - 1);
}
});
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
saveNickAndStart(input.value);
}
});
}
});
window.addEventListener("orientationchange", function() {
// Chrome на Android обновляет innerWidth/Height не мгновенно.
// Ждем завершения анимации поворота.
setTimeout(() => {
// В Emscripten это вызовет ваш onWindowResized в C++
window.dispatchEvent(new Event('resize'));
}, 200);
});
</script>
</body>
</html>

BIN
resources/button_info.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/button_info_pressed.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/button_invmouse.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/button_invmouse_pressed.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/button_players_disabled.png (Stored with Git LFS) Normal file

Binary file not shown.

218
resources/config/about.json Normal file
View File

@ -0,0 +1,218 @@
{
"root": {
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 10,
"x": 0,
"y": 0,
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "StaticImage",
"name": "titleBtn",
"width": 434,
"height": 35,
"texture": "resources/main_menu/title.png"
},
{
"type": "StaticImage",
"name": "aboutSpace1",
"width": 434,
"height": 10
},
{
"type": "FrameLayout",
"spacing": 10,
"width": 434,
"height": 460,
"children": [
{
"type": "TextView",
"name": "aboutText1",
"width": 434,
"height": 40,
"x": -300,
"y": 0,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"text": "Producer: Vladislav Khorev",
"fontSize": 32,
"color": [
255,
255,
255,
1
],
"centered": false
},
{
"type": "TextView",
"name": "aboutText2",
"width": 434,
"height": 40,
"x": -300,
"y": 60,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"text": "Lead Developer: Vladislav Khorev",
"fontSize": 32,
"color": [
255,
255,
255,
1
],
"centered": false
},
{
"type": "TextView",
"name": "aboutText2",
"width": 434,
"height": 40,
"x": -300,
"y": 120,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"text": "Game Designer: Leila Bobrova",
"fontSize": 32,
"color": [
255,
255,
255,
1
],
"centered": false
},
{
"type": "TextView",
"name": "aboutText2",
"width": 434,
"height": 40,
"x": -300,
"y": 180,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"text": "Developers: ",
"fontSize": 32,
"color": [
255,
255,
255,
1
],
"centered": false
},
{
"type": "TextView",
"name": "aboutText2",
"width": 434,
"height": 40,
"x": -300,
"y": 220,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"text": "Vladislav Kan",
"fontSize": 32,
"color": [
255,
255,
255,
1
],
"centered": false
},
{
"type": "TextView",
"name": "aboutText2",
"width": 434,
"height": 40,
"x": -300,
"y": 260,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"text": "Beksultan Almazbekov",
"fontSize": 32,
"color": [
255,
255,
255,
1
],
"centered": false
},
{
"type": "TextView",
"name": "aboutText2",
"width": 434,
"height": 40,
"x": -300,
"y": 300,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"text": "Sergei Zotov",
"fontSize": 32,
"color": [
255,
255,
255,
1
],
"centered": false
},
{
"type": "TextView",
"name": "aboutText2",
"width": 434,
"height": 40,
"x": -300,
"y": 360,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"text": "3D Artist: David \"nokken\" Im",
"fontSize": 32,
"color": [
255,
255,
255,
1
],
"centered": false
},
{
"type": "TextView",
"name": "aboutText2",
"width": 434,
"height": 40,
"x": -300,
"y": 420,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"text": "UI/UX Design: Kenje Kazmatova",
"fontSize": 32,
"color": [
255,
255,
255,
1
],
"centered": false
}
]
},
{
"type": "Button",
"name": "aboutBackButton",
"width": 382,
"height": 56,
"textures": {
"normal": "resources/game_over/Secondarybutton.png",
"hover": "resources/game_over/Secondarybutton.png",
"pressed": "resources/game_over/Secondarybutton.png"
}
}
]
}
}

View File

@ -27,9 +27,9 @@
"horizontal_gravity": "center",
"vertical_gravity": "center",
"textures": {
"normal": "resources/game_over/Filledbuttons.png",
"hover": "resources/game_over/Variant5.png",
"pressed": "resources/game_over/Variant6.png"
"normal": "resources/game_over/reconnect1.png",
"hover": "resources/game_over/reconnect2.png",
"pressed": "resources/game_over/reconnect3.png"
}
},
{

View File

@ -27,9 +27,9 @@
"horizontal_gravity": "center",
"vertical_gravity": "center",
"textures": {
"normal": "resources/game_over/Filledbuttons.png",
"hover": "resources/game_over/Variant5.png",
"pressed": "resources/game_over/Variant6.png"
"normal": "resources/game_over/reconnect1.png",
"hover": "resources/game_over/reconnect2.png",
"pressed": "resources/game_over/reconnect3.png"
}
},
{

View File

@ -52,6 +52,17 @@
"hover": "resources/main_menu/Variant7.png",
"pressed": "resources/main_menu/Variant8.png"
}
},
{
"type": "Button",
"name": "aboutButton",
"width": 382,
"height": 56,
"textures": {
"normal": "resources/main_menu/about.png",
"hover": "resources/main_menu/about_hover.png",
"pressed": "resources/main_menu/about_pressed.png"
}
},
{
"type": "StaticImage",

View File

@ -38,7 +38,41 @@
"normal": "resources/button_players.png",
"hover": "resources/button_players.png",
"pressed": "resources/button_players.png",
"disabled": "resources/button_players.png"
"disabled": "resources/button_players_disabled.png"
}
},
{
"type": "Button",
"name": "inverseMouseButton",
"x": 0,
"y": 100,
"width": 150,
"height": 150,
"border" : 20,
"horizontal_gravity": "right",
"vertical_gravity": "top",
"textures": {
"normal": "resources/button_invmouse.png",
"hover": "resources/button_invmouse.png",
"pressed": "resources/button_invmouse_pressed.png",
"disabled": "resources/button_invmouse.png"
}
},
{
"type": "Button",
"name": "infoButton",
"x": 0,
"y": 250,
"width": 150,
"height": 150,
"border" : 20,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"textures": {
"normal": "resources/button_info.png",
"hover": "resources/button_info.png",
"pressed": "resources/button_info_pressed.png",
"disabled": "resources/button_info.png"
}
},
{

View File

@ -0,0 +1,163 @@
{
"root": {
"type": "FrameLayout",
"x": 0,
"y": 0,
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "StaticImage",
"name": "showPlayersButton_help",
"x": 0,
"y": 100,
"width": 150,
"height": 150,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"texture": "resources/button_players.png"
},
{
"type": "StaticImage",
"name": "infoButton_help",
"x": 0,
"y": 250,
"width": 150,
"height": 150,
"border" : 20,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"texture":"resources/button_info.png"
},
{
"type": "StaticImage",
"name": "infoButtonTopLeft_help",
"x": 0,
"y": 0,
"width": 300,
"height": 400,
"border" : 0,
"horizontal_gravity": "left",
"vertical_gravity": "top",
"texture": "resources/help_top_left.png"
},
{
"type": "StaticImage",
"name": "inverseMouseButton_help",
"x": 0,
"y": 100,
"width": 150,
"height": 150,
"border" : 20,
"horizontal_gravity": "right",
"vertical_gravity": "top",
"texture": "resources/button_invmouse.png"
},
{
"type": "StaticImage",
"name": "infoButtonTopRight_help",
"x": 0,
"y": 0,
"width": 300,
"height": 230,
"border" : 0,
"horizontal_gravity": "right",
"vertical_gravity": "top",
"texture": "resources/help_top_right.png"
},
{
"type": "StaticImage",
"name": "infoButtonBottomLeft_help",
"x": 0,
"y": 0,
"width": 300,
"height": 200,
"border" : 0,
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"texture": "resources/help_bottom_left.png"
},
{
"type": "StaticImage",
"name": "shootButton_help",
"x": 0,
"y": 0,
"width": 150,
"height": 150,
"horizontal_gravity": "right",
"vertical_gravity": "bottom",
"texture": "resources/fire.png"
},
{
"type": "StaticImage",
"name": "shootButton2_help",
"x": 0,
"y": 0,
"width": 150,
"height": 150,
"horizontal_gravity": "left",
"vertical_gravity": "bottom",
"texture": "resources/fire.png"
},
{
"type": "StaticImage",
"name": "minusButton_help",
"x": -20,
"y": 110,
"width": 150,
"height": 150,
"border" : 20,
"horizontal_gravity": "right",
"vertical_gravity": "bottom",
"texture": "resources/button_minus.png"
},
{
"type": "StaticImage",
"name": "plusButton_help",
"x": -20,
"y": 220,
"width": 150,
"height": 150,
"border" : 20,
"horizontal_gravity": "right",
"vertical_gravity": "bottom",
"texture": "resources/button_plus.png"
},
{
"type": "StaticImage",
"name": "takeButton_help",
"x": -20,
"y": 320,
"width": 150,
"height": 150,
"border" : 20,
"horizontal_gravity": "right",
"vertical_gravity": "bottom",
"texture": "resources/button_take.png"
},
{
"type": "StaticImage",
"name": "infoButtonBottomRight_help",
"x": 0,
"y": 0,
"width": 300,
"height": 450,
"border" : 0,
"horizontal_gravity": "right",
"vertical_gravity": "bottom",
"texture": "resources/help_bottom_right.png"
},
{
"type": "Button",
"name": "infoButtonUnderlying_help",
"x": 0,
"y": 0,
"width": "match_parent",
"height": "match_parent",
"border" : 0,
"textures": {
}
}
]
}
}

BIN
resources/game_over/Variant6.png (Stored with Git LFS)

Binary file not shown.

BIN
resources/game_over/reconnect1.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/game_over/reconnect2.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/game_over/reconnect3.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/gameover.png (Stored with Git LFS)

Binary file not shown.

File diff suppressed because one or more lines are too long

BIN
resources/help_bottom_left.png (Stored with Git LFS) Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

BIN
resources/help_bottom_right.png (Stored with Git LFS) Normal file

Binary file not shown.

209
resources/help_top_left.pdn Normal file

File diff suppressed because one or more lines are too long

BIN
resources/help_top_left.png (Stored with Git LFS) Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

BIN
resources/help_top_right.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/main_menu/about.png (Stored with Git LFS)

Binary file not shown.

BIN
resources/main_menu/about_hover.png (Stored with Git LFS)

Binary file not shown.

BIN
resources/main_menu/about_pressed.png (Stored with Git LFS)

Binary file not shown.

BIN
resources/main_menu/lang.png (Stored with Git LFS)

Binary file not shown.

BIN
resources/platform_base.png (Stored with Git LFS)

Binary file not shown.

BIN
resources/player_under.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/players_list_title.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
resources/shoot_hover.png (Stored with Git LFS)

Binary file not shown.

BIN
resources/shoot_normal.png (Stored with Git LFS)

Binary file not shown.

BIN
resources/shoot_pressed.png (Stored with Git LFS)

Binary file not shown.

BIN
resources/sky/space_red.png (Stored with Git LFS)

Binary file not shown.

BIN
resources/transparent.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -125,8 +125,6 @@ void Session::sendBoxesToClient() {
void Session::init()
{
sendBoxesToClient();
auto timer = std::make_shared<net::steady_timer>(ws_.get_executor());
timer->expires_after(std::chrono::milliseconds(100));
timer->async_wait([self = shared_from_this(), timer](const boost::system::error_code& ec) {
@ -135,6 +133,8 @@ void Session::init()
uint64_t now_ms = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(now_tp.time_since_epoch()).count());
self->sendBoxesToClient();
self->send_message("ID:" + std::to_string(self->id_) + ":" + std::to_string(now_ms));
self->do_read();
}

View File

@ -1,8 +1,15 @@
#include "MenuManager.h"
#include <iostream>
#ifdef EMSCRIPTEN
#include <emscripten.h>
#include <cstdlib>
#endif
namespace ZL {
extern bool inverseVertical;
MenuManager::MenuManager(Renderer& iRenderer) :
renderer(iRenderer)
{
@ -11,11 +18,13 @@ namespace ZL {
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);
enterMainMenu();
@ -25,6 +34,7 @@ namespace ZL {
{
return state == GameState::Gameplay
|| state == GameState::GameOver
|| state == GameState::HelpScreen
|| state == GameState::ConnectionLost;
}
@ -44,6 +54,20 @@ namespace ZL {
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 ───────────────────────────────────────────
@ -53,15 +77,39 @@ namespace ZL {
state = GameState::ShipSelectionSingle;
uiManager.replaceRoot(shipSelectionRoot);
uiManager.setButtonCallback("spaceshipButton", [this](const std::string&) {
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](const std::string&) {
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);
@ -79,8 +127,31 @@ namespace ZL {
state = GameState::ShipSelectionMulti;
uiManager.replaceRoot(shipSelectionRoot);
uiManager.setButtonCallback("spaceshipButton", [this](const std::string&) {
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;
@ -88,8 +159,9 @@ namespace ZL {
if (onMultiplayerPressed) onMultiplayerPressed(nick, 0);
});
uiManager.setButtonCallback("cargoshipButton", [this](const std::string&) {
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;
@ -135,8 +207,53 @@ namespace ZL {
state = GameState::Gameplay;
uiManager.replaceRoot(gameplayRoot);
if (Environment::shipState.shipType == 1)
{
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;
}
/*
@ -189,6 +306,16 @@ namespace ZL {
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();
});
/*
uiManager.setSliderCallback("velocitySlider", [this](const std::string&, float value) {
@ -218,6 +345,16 @@ namespace ZL {
});
}
void MenuManager::enterHelp()
{
state = GameState::HelpScreen;
uiManager.replaceRoot(helpScreenRoot);
uiManager.setButtonCallback("infoButtonUnderlying_help", [this](const std::string&) {
enterGameplay();
});
}
// ── State: ConnectionLost ─────────────────────────────────────────────────
void MenuManager::enterConnectionLost()
@ -226,7 +363,8 @@ namespace ZL {
uiManager.replaceRoot(connectionLostRoot);
uiManager.setButtonCallback("reconnectButton", [this](const std::string&) {
// TODO: reconnect logic
enterConnecting();
if (onMultiplayerPressed) onMultiplayerPressed(pendingMultiNick, pendingMultiShipType);
});
uiManager.setButtonCallback("exitServerButton", [this](const std::string&) {
enterMainMenu();

View File

@ -10,12 +10,14 @@ namespace ZL {
enum class GameState {
MainMenu,
AboutMenu,
ShipSelectionSingle,
ShipSelectionMulti,
Connecting,
ConnectionFailed,
Gameplay,
GameOver,
HelpScreen,
ConnectionLost
};
@ -25,11 +27,13 @@ namespace ZL {
// 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
@ -40,12 +44,14 @@ namespace ZL {
// 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:
@ -74,6 +80,8 @@ namespace ZL {
std::function<void(const std::string&, int)> onSingleplayerPressed;
std::function<void(const std::string&, int)> onMultiplayerPressed;
std::function<void()> onShowPlayersPressed;
std::function<void()> forceSetupSpaceUICallback;
};
} // namespace ZL

View File

@ -36,6 +36,8 @@ namespace ZL
extern float y;
extern float z;
bool inverseVertical = true;
Eigen::Quaternionf generateRandomQuaternion(std::mt19937& gen)
{
@ -271,6 +273,8 @@ namespace ZL
Environment::zoom = DEFAULT_ZOOM;
Environment::tapDownHold = false;
playerScore = 0;
mainThreadHandler.EnqueueMainThreadTask([this]() {
if (menuManager.uiManager.findButton("minusButton"))
{
menuManager.uiManager.findButton("minusButton")->state = ButtonState::Disabled;
@ -290,11 +294,29 @@ namespace ZL
menuManager.uiManager.findButton("shootButton2")->state = ButtonState::Normal;
}
}
});
}
void Space::setup() {
void Space::updateShowPlayersButtonState()
{
bool hasAlivePlayers = false;
for (auto const& [id, _] : remotePlayerStates) {
if (!deadRemotePlayers.count(id)) {
hasAlivePlayers = true;
break;
}
}
if (hasAlivePlayers == showPlayersButtonEnabled) return;
showPlayersButtonEnabled = hasAlivePlayers;
auto btn = menuManager.uiManager.findButton("showPlayersButton");
if (!btn) return;
btn->state = hasAlivePlayers ? ButtonState::Normal : ButtonState::Disabled;
}
void Space::setup() {
menuManager.onRestartPressed = [this]() {
resetPlayerState();
@ -357,6 +379,12 @@ namespace ZL
}
};
menuManager.forceSetupSpaceUICallback = [this]() {
this->nearPickupBox = false;
this->showPlayersButtonEnabled = false;
};
bool cfgLoaded = sparkEmitter.loadFromJsonFile("resources/config/spark_config.json", renderer, CONST_ZIP_FILE);
bool cfgLoaded2 = sparkEmitterCargo.loadFromJsonFile("resources/config/spark_config_cargo.json", renderer, CONST_ZIP_FILE);
sparkEmitter.setIsActive(false);
@ -1693,6 +1721,7 @@ namespace ZL
remotePlayerStates[id] = playerState;
}
updateShowPlayersButtonState();
updateSparkEmitters(static_cast<float>(delta));
for (auto& p : projectiles) {
@ -1794,7 +1823,9 @@ namespace ZL
shipAlive = false;
gameOver = true;
Environment::shipState.selectedVelocity = 0;
Environment::shipState.velocity = 0.0f;
newShipVelocity = 0;
showExplosion = true;
explosionEmitter.setUseWorldSpace(true);
@ -2013,6 +2044,7 @@ namespace ZL
}
rebuildPlayerListIfVisible();
updateShowPlayersButtonState();
auto boxDestructions = networkClient->getPendingBoxDestructions();
if (!boxDestructions.empty()) {
std::cout << "Game: Received " << boxDestructions.size() << " box destruction events" << std::endl;
@ -2079,12 +2111,23 @@ namespace ZL
Environment::tapDownHold = true;
if (inverseVertical)
{
Environment::tapDownStartPos(0) = mx;
Environment::tapDownStartPos(1) = my;
Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = my;
}
else
{
Environment::tapDownStartPos(0) = mx;
Environment::tapDownStartPos(1) = -my;
Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = -my;
}
}
void Space::handleUp(int mx, int my)
{
@ -2096,11 +2139,22 @@ namespace ZL
{
if (playerListVisible) return;
if (inverseVertical)
{
if (Environment::tapDownHold) {
Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = my;
}
}
else
{
if (Environment::tapDownHold) {
Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = -my;
}
}
}
void Space::clearTextRendererCache()
{
@ -2136,8 +2190,8 @@ namespace ZL
std::shared_ptr<UiNode> Space::buildPlayerListRoot()
{
const float btnW = 400;
const float btnH = 50.0f;
const float btnW = 250;
const float btnH = 40.0f;
// Collect alive remote players
std::vector<std::pair<int, std::string>> players;
@ -2154,7 +2208,7 @@ namespace ZL
root->height = -1.0f;
// List container: LinearLayout vertical, centered
float listH = btnH * (float)players.size();
float listH = btnH * (float)(players.size()+1);
auto listNode = std::make_shared<UiNode>();
listNode->name = "playerList";
listNode->layoutType = LayoutType::Linear;
@ -2164,6 +2218,20 @@ namespace ZL
listNode->layoutSettings.hGravity = HorizontalGravity::Center;
listNode->layoutSettings.vGravity = VerticalGravity::Center;
auto titleNode = std::make_shared<UiNode>();
titleNode->name = "player_list_title";
titleNode->layoutType = LayoutType::Frame;
titleNode->width = btnW;
titleNode->height = btnH;
auto titleImage = std::make_shared<UiButton>();
titleImage->name = "player_list_title";
titleImage->texNormal = std::make_unique<Texture>(CreateTextureDataFromPng("resources/players_list_title.png", ""));
titleImage->texPressed = titleImage->texNormal;
titleImage->texHover = titleImage->texNormal;
titleNode->button = titleImage;
listNode->children.push_back(titleNode);
for (auto& [pid, nick] : players) {
auto btnNode = std::make_shared<UiNode>();
btnNode->name = "playerBtn_" + std::to_string(pid);
@ -2182,7 +2250,7 @@ namespace ZL
if (!tb->textRenderer->init(renderer, tb->fontPath, tb->fontSize, CONST_ZIP_FILE)) {
std::cerr << "Failed to init TextRenderer for TextField: " << tb->name << std::endl;
}
//tb->texNormal = std::make_unique<Texture>(CreateTextureDataFromPng("resources/black.png", ""));
tb->texNormal = std::make_unique<Texture>(CreateTextureDataFromPng("resources/player_under.png", CONST_ZIP_FILE));
btnNode->textButton = tb;
/*auto button = std::make_shared<UiButton>();
@ -2242,6 +2310,9 @@ namespace ZL
menuManager.uiManager.setTextButtonCallback("playerListBackdrop", [this](const std::string&) {
closePlayerList();
});
menuManager.uiManager.setTextButtonCallback("player_list_title", [this](const std::string&) {
closePlayerList();
});
}
void Space::closePlayerList()

View File

@ -149,6 +149,8 @@ namespace ZL {
void resetPlayerState();
void clearTextRendererCache();
void updateShowPlayersButtonState();
bool showPlayersButtonEnabled = false;
// Player list overlay
void buildAndShowPlayerList();

View File

@ -103,9 +103,6 @@ namespace ZL {
}
void UiTextButton::draw(Renderer& renderer) const {
renderer.PushMatrix();
renderer.TranslateMatrix({ animOffsetX, animOffsetY, 0.0f });
renderer.ScaleMatrix({ animScaleX, animScaleY, 1.0f });
// Draw background texture (optional)
const std::shared_ptr<Texture>* tex = nullptr;
@ -115,13 +112,22 @@ namespace ZL {
case ButtonState::Pressed: tex = texPressed ? &texPressed : (texNormal ? &texNormal : nullptr); break;
case ButtonState::Disabled: tex = texDisabled ? &texDisabled : (texNormal ? &texNormal : nullptr); break;
}
glDisable(GL_DEPTH_TEST);
if (tex && *tex) {
renderer.shaderManager.PushShader(defaultShaderName);
renderer.PushMatrix();
renderer.TranslateMatrix({ animOffsetX, animOffsetY, 0.0f });
renderer.ScaleMatrix({ animScaleX, animScaleY, 1.0f });
renderer.RenderUniform1i(textureUniformName, 0);
glBindTexture(GL_TEXTURE_2D, (*tex)->getTexID());
renderer.DrawVertexRenderStruct(mesh);
renderer.PopMatrix();
renderer.shaderManager.PopShader();
}
renderer.PopMatrix();
// Draw text on top (uses absolute coords, add anim offset manually)
if (textRenderer && !text.empty()) {
@ -129,6 +135,7 @@ namespace ZL {
float cy = rect.y + rect.h / 2.0f + animOffsetY;
textRenderer->drawText(text, cx, cy, 1.0f, textCentered, color);
}
glEnable(GL_DEPTH_TEST);
}
void UiSlider::buildTrackMesh() {

View File

@ -9,7 +9,7 @@ namespace ZL {
// Формируем URL. Обратите внимание, что в Web часто лучше использовать ws://localhost
//std::string url = "ws://" + host + ":" + std::to_string(port);
std::string url = "wss://api.spacegame.fishrungames.com";
//std::string url = "ws://localhost:8081";
EmscriptenWebSocketCreateAttributes attr = {
url.c_str(),
nullptr,

View File

@ -98,12 +98,12 @@ namespace ZL {
stoneToBake = planetStones.inflateOne(0, 0.75);
/*
campPlatform.data = LoadFromTextFile02("resources/platform1.txt", CONST_ZIP_FILE);
campPlatform.RefreshVBO();
campPlatformTexture = std::make_unique<Texture>(CreateTextureDataFromPng("resources/platform_base.png", CONST_ZIP_FILE));
*/
}

View File

@ -361,6 +361,8 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
// 4. Рендеринг
r->shaderManager.PushShader(shaderName);
//r->PushMatrix();
//r->LoadIdentity();
// Матрица проекции — используем виртуальные проекционные размеры,
// чтобы координаты текста были независимы от физического разрешения экрана.
@ -373,6 +375,8 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
proj(0, 3) = -1.0f + 2.0f * (tx) / W;
proj(1, 3) = -1.0f + 2.0f * (ty) / H;
//cached.mesh.RefreshVBO();
r->RenderUniformMatrix4fv("uProjection", false, proj.data());
r->RenderUniform1i("uText", 0);
r->RenderUniform4fv("uColor", color.data());
@ -397,6 +401,7 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
// glDrawArrays(GL_TRIANGLES, 0, 6);
//}
r->DrawVertexRenderStruct(cached.mesh);
//r->PopMatrix();
r->shaderManager.PopShader();
// Сброс бинда текстуры не обязателен, но можно для чистоты