diff --git a/proj-web/space-game001plain.html b/proj-web/space-game001plain.html
index d3c1953..aae27f4 100644
--- a/proj-web/space-game001plain.html
+++ b/proj-web/space-game001plain.html
@@ -8,7 +8,7 @@
body, html {
margin: 0; padding: 0; width: 100%; height: 100%;
overflow: hidden; background-color: #000;
- position: fixed; /* Предотвращает pull-to-refresh на Android */
+ position: fixed;
}
#canvas {
display: block;
@@ -17,10 +17,23 @@
width: 100vw; height: 100vh;
border: none;
}
+
+ #fs-button {
+ position: absolute;
+ top: 10px; right: 10px;
+ padding: 10px;
+ z-index: 10;
+ background: rgba(255,255,255,0.3);
+ color: white; border: 1px solid white;
+ cursor: pointer;
+ font-family: sans-serif;
+ border-radius: 5px;
+ }
#status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
+
Downloading...
@@ -36,12 +49,18 @@
}
};
- // Обработка ориентации
+ document.getElementById('fs-button').addEventListener('click', function() {
+ if (!document.fullscreenElement) {
+ document.documentElement.requestFullscreen().catch(e => {
+ console.error(`Error attempting to enable full-screen mode: ${e.message}`);
+ });
+ } else {
+ document.exitFullscreen();
+ }
+ });
+
window.addEventListener("orientationchange", function() {
- // Chrome на Android обновляет innerWidth/Height не мгновенно.
- // Ждем завершения анимации поворота.
setTimeout(() => {
- // В Emscripten это вызовет ваш onWindowResized в C++
window.dispatchEvent(new Event('resize'));
}, 200);
});
diff --git a/resources/black.png b/resources/black.png
new file mode 100644
index 0000000..603e239
--- /dev/null
+++ b/resources/black.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b5a9276602359b1ed4341487185ed8b5f4c4dfa86d9efb06cc09f748a50c8560
+size 353
diff --git a/resources/blue_transparent.png b/resources/blue_transparent.png
new file mode 100644
index 0000000..c4d0889
--- /dev/null
+++ b/resources/blue_transparent.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9f459ece1aea9b5514f5c170720968f0817e7596f01c483a02f274180f6e4143
+size 364
diff --git a/resources/button_players.png b/resources/button_players.png
new file mode 100644
index 0000000..31bdb5e
--- /dev/null
+++ b/resources/button_players.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:43058b1c47b3a29d364c77642cfa0c83ccc719e260418fa89bf0dd501b45fbf7
+size 22899
diff --git a/resources/config/ui.json b/resources/config/ui.json
index dcd4df6..1dc861d 100644
--- a/resources/config/ui.json
+++ b/resources/config/ui.json
@@ -8,18 +8,39 @@
"children": [
{
"type": "TextView",
- "name": "velocityText",
- "x": 10,
- "y": 10,
+ "name": "gameScoreText",
+ "x": 0,
+ "y": 30,
"width": 200,
- "height": 40,
+ "height": 60,
"horizontal_gravity": "left",
"vertical_gravity": "top",
- "text": "Velocity: 0",
- "fontSize": 24,
- "color": [1.0, 1.0, 1.0, 1.0],
+ "text": "Score: 0",
+ "fontSize": 36,
+ "color": [
+ 0,
+ 217,
+ 255,
+ 1
+ ],
"centered": false
},
+ {
+ "type": "Button",
+ "name": "showPlayersButton",
+ "x": 0,
+ "y": 100,
+ "width": 150,
+ "height": 150,
+ "horizontal_gravity": "left",
+ "vertical_gravity": "top",
+ "textures": {
+ "normal": "resources/button_players.png",
+ "hover": "resources/button_players.png",
+ "pressed": "resources/button_players.png",
+ "disabled": "resources/button_players.png"
+ }
+ },
{
"type": "Button",
"name": "shootButton",
diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp
index bf928ad..955324e 100644
--- a/src/MenuManager.cpp
+++ b/src/MenuManager.cpp
@@ -139,11 +139,12 @@ namespace ZL {
if (auto btn = uiManager.findButton("takeButton")) btn->state = ButtonState::Disabled;
+ /*
auto velocityTv = uiManager.findTextView("velocityText");
if (velocityTv) {
velocityTv->rect.x = 10.0f;
velocityTv->rect.y = static_cast(Environment::height) - velocityTv->rect.h - 10.0f;
- }
+ }*/
uiManager.setButtonPressCallback("shootButton", [this](const std::string&) {
if (onFirePressed) onFirePressed();
@@ -184,6 +185,11 @@ namespace ZL {
if (onTakeButtonPressed) onTakeButtonPressed();
});
+ uiManager.setButtonCallback("showPlayersButton", [this](const std::string&) {
+ if (onShowPlayersPressed) onShowPlayersPressed();
+ });
+
+
/*
uiManager.setSliderCallback("velocitySlider", [this](const std::string&, float value) {
int newVel = static_cast(roundf(value * 10));
diff --git a/src/MenuManager.h b/src/MenuManager.h
index 1a52b62..ec7844c 100644
--- a/src/MenuManager.h
+++ b/src/MenuManager.h
@@ -73,6 +73,7 @@ namespace ZL {
std::function onTakeButtonPressed;
std::function onSingleplayerPressed;
std::function onMultiplayerPressed;
+ std::function onShowPlayersPressed;
};
} // namespace ZL
\ No newline at end of file
diff --git a/src/Space.cpp b/src/Space.cpp
index cde5e60..5b865fc 100644
--- a/src/Space.cpp
+++ b/src/Space.cpp
@@ -332,6 +332,10 @@ namespace ZL
firePressed = true;
};
+ menuManager.onShowPlayersPressed = [this]() {
+ buildAndShowPlayerList();
+ };
+
menuManager.onTakeButtonPressed = [this]() {
if (Environment::shipState.shipType != 1) return;
if (!networkClient) return;
@@ -349,6 +353,7 @@ namespace ZL
}
if (bestIdx >= 0) {
networkClient->Send("BOX_PICKUP:" + std::to_string(bestIdx));
+ this->playerScore += 1;
}
};
@@ -1000,6 +1005,16 @@ namespace ZL
int Space::pickTargetId() const
{
+ // Use manually selected target if it's still alive and in range
+ if (manualTrackedTargetId >= 0) {
+ auto it = remotePlayerStates.find(manualTrackedTargetId);
+ if (it != remotePlayerStates.end() && !deadRemotePlayers.count(manualTrackedTargetId)) {
+ float d2 = (Environment::shipState.position - it->second.position).squaredNorm();
+ if (d2 <= TARGET_MAX_DIST_SQ) return manualTrackedTargetId;
+ }
+ // Target no longer valid — fall through to auto-pick
+ }
+
int bestId = -1;
float bestDistSq = 1e30f;
@@ -1007,8 +1022,8 @@ namespace ZL
if (deadRemotePlayers.count(id)) continue;
float d2 = (Environment::shipState.position - st.position).squaredNorm();
-
- if (d2 > TARGET_MAX_DIST_SQ) continue; // слишком далеко
+
+ if (d2 > TARGET_MAX_DIST_SQ) continue;
if (d2 < bestDistSq) {
bestDistSq = d2;
@@ -1727,6 +1742,7 @@ namespace ZL
std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n";
+ clearPlayerListIfVisible();
menuManager.showGameOver(this->playerScore);
}
else {
@@ -1802,6 +1818,7 @@ namespace ZL
planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty;
}
+ clearPlayerListIfVisible();
menuManager.showGameOver(this->playerScore);
}
}
@@ -1837,6 +1854,11 @@ namespace ZL
}
}
+ if (playerScore != prevPlayerScore)
+ {
+ prevPlayerScore = playerScore;
+ menuManager.uiManager.setText("gameScoreText", "Score: " + std::to_string(playerScore));
+ }
}
void Space::fireProjectiles() {
@@ -1845,6 +1867,7 @@ namespace ZL
Vector3f{ 1.5f, 0.9f - 6.f, 5.0f }
};
+
const float projectileSpeed = PROJECTILE_VELOCITY;
const float lifeMs = PROJECTILE_LIFE;
const float size = 0.5f;
@@ -1878,6 +1901,7 @@ namespace ZL
gameOver = true;
Environment::shipState.velocity = 0.0f;
std::cout << "Client: Lost connection to server\n";
+ clearPlayerListIfVisible();
menuManager.showConnectionLost();
}
@@ -1916,6 +1940,7 @@ namespace ZL
}
}
}
+
// Обработка событий смерти, присланных сервером
auto deaths = networkClient->getPendingDeaths();
if (!deaths.empty()) {
@@ -1940,10 +1965,12 @@ namespace ZL
shipAlive = false;
gameOver = true;
Environment::shipState.velocity = 0.0f;
+ clearPlayerListIfVisible();
menuManager.showGameOver(this->playerScore);
}
else {
deadRemotePlayers.insert(d.targetId);
+ if (d.targetId == manualTrackedTargetId) manualTrackedTargetId = -1;
std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl;
}
if (d.killerId == localId) {
@@ -1952,6 +1979,7 @@ namespace ZL
}
}
+ rebuildPlayerListIfVisible();
}
auto respawns = networkClient->getPendingRespawns();
@@ -1969,6 +1997,7 @@ namespace ZL
std::cout << "Client: Remote player " << respawnId << " respawned, removed from dead list" << std::endl;
}
}
+ rebuildPlayerListIfVisible();
auto disconnects = networkClient->getPendingDisconnects();
for (int pid : disconnects) {
@@ -1979,9 +2008,11 @@ namespace ZL
trackedTargetId = -1;
targetAcquireAnim = 0.f;
}
+ if (pid == manualTrackedTargetId) manualTrackedTargetId = -1;
std::cout << "Client: Remote player " << pid << " left the game, removed from scene\n";
}
+ rebuildPlayerListIfVisible();
auto boxDestructions = networkClient->getPendingBoxDestructions();
if (!boxDestructions.empty()) {
std::cout << "Game: Received " << boxDestructions.size() << " box destruction events" << std::endl;
@@ -2044,13 +2075,15 @@ namespace ZL
void Space::handleDown(int mx, int my)
{
- Environment::tapDownHold = true;
+ if (playerListVisible) return;
- Environment::tapDownStartPos(0) = mx;
- Environment::tapDownStartPos(1) = my;
+ Environment::tapDownHold = true;
- Environment::tapDownCurrentPos(0) = mx;
- Environment::tapDownCurrentPos(1) = my;
+ Environment::tapDownStartPos(0) = mx;
+ Environment::tapDownStartPos(1) = my;
+
+ Environment::tapDownCurrentPos(0) = mx;
+ Environment::tapDownCurrentPos(1) = my;
}
void Space::handleUp(int mx, int my)
@@ -2061,6 +2094,8 @@ namespace ZL
void Space::handleMotion(int mx, int my)
{
+ if (playerListVisible) return;
+
if (Environment::tapDownHold) {
Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = my;
@@ -2098,4 +2133,151 @@ namespace ZL
}*/
+
+ std::shared_ptr Space::buildPlayerListRoot()
+ {
+ const float btnW = 400;
+ const float btnH = 50.0f;
+
+ // Collect alive remote players
+ std::vector> players;
+ for (auto& kv : remotePlayerStates) {
+ if (!deadRemotePlayers.count(kv.first))
+ players.push_back({ kv.first, kv.second.nickname });
+ }
+
+ // Root: FrameLayout match_parent x match_parent
+ auto root = std::make_shared();
+ root->name = "playerListRoot";
+ root->layoutType = LayoutType::Frame;
+ root->width = -1.0f; // match_parent
+ root->height = -1.0f;
+
+ // List container: LinearLayout vertical, centered
+ float listH = btnH * (float)players.size();
+ auto listNode = std::make_shared();
+ listNode->name = "playerList";
+ listNode->layoutType = LayoutType::Linear;
+ listNode->orientation = Orientation::Vertical;
+ listNode->width = btnW;
+ listNode->height = listH;
+ listNode->layoutSettings.hGravity = HorizontalGravity::Center;
+ listNode->layoutSettings.vGravity = VerticalGravity::Center;
+
+ for (auto& [pid, nick] : players) {
+ auto btnNode = std::make_shared();
+ btnNode->name = "playerBtn_" + std::to_string(pid);
+ btnNode->layoutType = LayoutType::Frame;
+ btnNode->width = btnW;
+ btnNode->height = btnH;
+
+
+ auto tb = std::make_shared();
+ tb->name = btnNode->name;
+ tb->text = nick;
+ tb->fontSize = 20;
+ tb->color = { 1.f, 1.f, 1.f, 1.f };
+ tb->textCentered = true;
+ tb->textRenderer = std::make_unique();
+ 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(CreateTextureDataFromPng("resources/black.png", ""));
+
+ btnNode->textButton = tb;
+ /*auto button = std::make_shared();
+ button->name = "Hello";
+ button->texNormal = std::make_unique(CreateTextureDataFromPng("resources/loading.png", ""));
+ btnNode->button = button;*/
+ listNode->children.push_back(btnNode);
+ }
+
+ // Backdrop: invisible full-screen TextButton — placed LAST so player buttons get priority
+ auto backdropNode = std::make_shared();
+ backdropNode->name = "playerListBackdrop";
+ backdropNode->layoutType = LayoutType::Frame;
+ backdropNode->width = -1.0f;
+ backdropNode->height = -1.0f;
+ auto backdropTb = std::make_shared();
+ backdropTb->name = "playerListBackdrop";
+ backdropNode->textButton = backdropTb;
+
+
+ /*
+ auto backgroundNode = std::make_shared();
+ backgroundNode->name = "playerListBackground";
+ backgroundNode->layoutType = LayoutType::Frame;
+ backgroundNode->width = btnW;
+ backgroundNode->height = listH;
+ backgroundNode->layoutSettings.hGravity = HorizontalGravity::Center;
+ backgroundNode->layoutSettings.vGravity = VerticalGravity::Center;
+ auto backdropImage = std::make_shared();
+ backdropImage->name = "playerListBackgroundImage";
+ backdropImage->texture = std::make_unique(CreateTextureDataFromPng("resources/blue_transparent.png", ""));
+ backgroundNode->staticImage = backdropImage;
+ */
+ root->children.push_back(listNode);
+ root->children.push_back(backdropNode);
+ //root->children.push_back(backgroundNode);
+ return root;
+ }
+
+ void Space::buildAndShowPlayerList()
+ {
+ auto listRoot = buildPlayerListRoot();
+ menuManager.uiManager.pushMenuFromSavedRoot(listRoot);
+ menuManager.uiManager.updateAllLayouts();
+ playerListVisible = true;
+
+ for (auto& kv : remotePlayerStates) {
+ if (deadRemotePlayers.count(kv.first)) continue;
+ int pid = kv.first;
+ std::string btnName = "playerBtn_" + std::to_string(pid);
+ menuManager.uiManager.setTextButtonCallback(btnName, [this, pid](const std::string&) {
+ manualTrackedTargetId = pid;
+ closePlayerList();
+ });
+ }
+
+ menuManager.uiManager.setTextButtonCallback("playerListBackdrop", [this](const std::string&) {
+ closePlayerList();
+ });
+ }
+
+ void Space::closePlayerList()
+ {
+ menuManager.uiManager.popMenu();
+ menuManager.uiManager.updateAllLayouts();
+ playerListVisible = false;
+ }
+
+ void Space::rebuildPlayerListIfVisible()
+ {
+ if (!playerListVisible) return;
+
+ auto listRoot = buildPlayerListRoot();
+ menuManager.uiManager.replaceRoot(listRoot);
+
+ for (auto& kv : remotePlayerStates) {
+ if (deadRemotePlayers.count(kv.first)) continue;
+ int pid = kv.first;
+ std::string btnName = "playerBtn_" + std::to_string(pid);
+ menuManager.uiManager.setTextButtonCallback(btnName, [this, pid](const std::string&) {
+ manualTrackedTargetId = pid;
+ closePlayerList();
+ });
+ }
+
+ menuManager.uiManager.setTextButtonCallback("playerListBackdrop", [this](const std::string&) {
+ closePlayerList();
+ });
+ }
+
+ void Space::clearPlayerListIfVisible()
+ {
+ if (!playerListVisible) return;
+ menuManager.uiManager.clearMenuStack();
+ playerListVisible = false;
+ }
+
} // namespace ZL
diff --git a/src/Space.h b/src/Space.h
index f94d839..ca45251 100644
--- a/src/Space.h
+++ b/src/Space.h
@@ -126,8 +126,12 @@ namespace ZL {
std::unordered_set deadRemotePlayers;
int playerScore = 0;
+ int prevPlayerScore = 0;
bool wasConnectedToServer = false;
+ bool playerListVisible = false;
+ int manualTrackedTargetId = -1;
+
static constexpr float TARGET_MAX_DIST = 50000.0f;
static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST;
@@ -146,6 +150,13 @@ namespace ZL {
void resetPlayerState();
void clearTextRendererCache();
+ // Player list overlay
+ void buildAndShowPlayerList();
+ void closePlayerList();
+ void rebuildPlayerListIfVisible();
+ void clearPlayerListIfVisible();
+ std::shared_ptr buildPlayerListRoot();
+
void updateSparkEmitters(float deltaMs);
void prepareSparkEmittersForDraw();
void drawShipSparkEmitters();
diff --git a/src/UiManager.cpp b/src/UiManager.cpp
index b04a861..734ddd6 100644
--- a/src/UiManager.cpp
+++ b/src/UiManager.cpp
@@ -72,6 +72,65 @@ namespace ZL {
renderer.PopMatrix();
}
+ void UiTextButton::buildMesh() {
+ mesh.data.PositionData.clear();
+ mesh.data.TexCoordData.clear();
+
+ float x0 = rect.x;
+ float y0 = rect.y;
+ float x1 = rect.x + rect.w;
+ float y1 = rect.y + rect.h;
+
+ mesh.data.PositionData.push_back({ x0, y0, 0 });
+ mesh.data.TexCoordData.push_back({ 0, 0 });
+
+ mesh.data.PositionData.push_back({ x0, y1, 0 });
+ mesh.data.TexCoordData.push_back({ 0, 1 });
+
+ mesh.data.PositionData.push_back({ x1, y1, 0 });
+ mesh.data.TexCoordData.push_back({ 1, 1 });
+
+ mesh.data.PositionData.push_back({ x0, y0, 0 });
+ mesh.data.TexCoordData.push_back({ 0, 0 });
+
+ mesh.data.PositionData.push_back({ x1, y1, 0 });
+ mesh.data.TexCoordData.push_back({ 1, 1 });
+
+ mesh.data.PositionData.push_back({ x1, y0, 0 });
+ mesh.data.TexCoordData.push_back({ 1, 0 });
+
+ mesh.RefreshVBO();
+ }
+
+ 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* tex = nullptr;
+ switch (state) {
+ case ButtonState::Normal: if (texNormal) tex = &texNormal; break;
+ case ButtonState::Hover: tex = texHover ? &texHover : (texNormal ? &texNormal : nullptr); break;
+ case ButtonState::Pressed: tex = texPressed ? &texPressed : (texNormal ? &texNormal : nullptr); break;
+ case ButtonState::Disabled: tex = texDisabled ? &texDisabled : (texNormal ? &texNormal : nullptr); break;
+ }
+ if (tex && *tex) {
+ renderer.RenderUniform1i(textureUniformName, 0);
+ glBindTexture(GL_TEXTURE_2D, (*tex)->getTexID());
+ renderer.DrawVertexRenderStruct(mesh);
+ }
+
+ renderer.PopMatrix();
+
+ // Draw text on top (uses absolute coords, add anim offset manually)
+ if (textRenderer && !text.empty()) {
+ float cx = rect.x + rect.w / 2.0f + animOffsetX;
+ float cy = rect.y + rect.h / 2.0f + animOffsetY;
+ textRenderer->drawText(text, cx, cy, 1.0f, textCentered, color);
+ }
+ }
+
void UiSlider::buildTrackMesh() {
trackMesh.data.PositionData.clear();
trackMesh.data.TexCoordData.clear();
@@ -393,6 +452,49 @@ namespace ZL {
node->textField = tf;
}
+ if (typeStr == "TextButton") {
+ auto tb = std::make_shared();
+ tb->name = node->name;
+ tb->rect = initialRect;
+ tb->border = j.value("border", 0.0f);
+
+ // Textures are optional
+ if (j.contains("textures") && j["textures"].is_object()) {
+ auto t = j["textures"];
+ auto loadTex = [&](const std::string& key) -> std::shared_ptr {
+ if (!t.contains(key) || !t[key].is_string()) return nullptr;
+ std::string path = t[key].get();
+ try {
+ auto data = CreateTextureDataFromPng(path.c_str(), zipFile.c_str());
+ return std::make_shared(data);
+ }
+ catch (const std::exception& e) {
+ std::cerr << "UiManager: TextButton '" << tb->name << "' failed to load texture " << path << ": " << e.what() << std::endl;
+ return nullptr;
+ }
+ };
+ tb->texNormal = loadTex("normal");
+ tb->texHover = loadTex("hover");
+ tb->texPressed = loadTex("pressed");
+ tb->texDisabled = loadTex("disabled");
+ }
+
+ if (j.contains("text")) tb->text = j["text"].get();
+ if (j.contains("fontPath")) tb->fontPath = j["fontPath"].get();
+ if (j.contains("fontSize")) tb->fontSize = j["fontSize"].get();
+ if (j.contains("textCentered")) tb->textCentered = j["textCentered"].get();
+ if (j.contains("color") && j["color"].is_array() && j["color"].size() == 4) {
+ for (int i = 0; i < 4; ++i) tb->color[i] = j["color"][i].get();
+ }
+
+ tb->textRenderer = std::make_unique();
+ if (!tb->textRenderer->init(renderer, tb->fontPath, tb->fontSize, zipFile)) {
+ std::cerr << "UiManager: Failed to init TextRenderer for TextButton: " << tb->name << std::endl;
+ }
+
+ node->textButton = tb;
+ }
+
if (j.contains("animations") && j["animations"].is_object()) {
for (auto it = j["animations"].begin(); it != j["animations"].end(); ++it) {
std::string animName = it.key();
@@ -536,6 +638,7 @@ namespace ZL {
root->localY // finalLocalY
);
buttons.clear();
+ textButtons.clear();
sliders.clear();
textViews.clear();
textFields.clear();
@@ -547,6 +650,9 @@ namespace ZL {
for (auto& b : buttons) {
b->buildMesh();
}
+ for (auto& tb : textButtons) {
+ tb->buildMesh();
+ }
for (auto& s : sliders) {
s->buildTrackMesh();
s->buildKnobMesh();
@@ -694,11 +800,14 @@ namespace ZL {
// 1. Обновляем кнопку
if (node->button) {
node->button->rect = node->screenRect;
- // Если у кнопки есть анимационные смещения, они учитываются внутри buildMesh
- // или при рендеринге через Uniform-переменные матрицы модели.
node->button->buildMesh();
}
+ if (node->textButton) {
+ node->textButton->rect = node->screenRect;
+ node->textButton->buildMesh();
+ }
+
// 2. Обновляем слайдер
if (node->slider) {
node->slider->rect = node->screenRect;
@@ -744,6 +853,9 @@ namespace ZL {
if (node->button) {
buttons.push_back(node->button);
}
+ if (node->textButton) {
+ textButtons.push_back(node->textButton);
+ }
if (node->slider) {
sliders.push_back(node->slider);
}
@@ -862,10 +974,13 @@ namespace ZL {
MenuState prev;
prev.root = root;
prev.buttons = buttons;
+ prev.textButtons = textButtons;
prev.sliders = sliders;
+ prev.textViews = textViews;
prev.textFields = textFields;
prev.staticImages = staticImages;
prev.pressedButtons = pressedButtons;
+ prev.pressedTextButtons = pressedTextButtons;
prev.pressedSliders = pressedSliders;
prev.focusedTextField = focusedTextField;
prev.path = "";
@@ -884,6 +999,14 @@ namespace ZL {
b->animScaleY = 1.0f;
}
}
+ for (auto& tb : textButtons) {
+ if (tb) {
+ tb->animOffsetX = 0.0f;
+ tb->animOffsetY = 0.0f;
+ tb->animScaleX = 1.0f;
+ tb->animScaleY = 1.0f;
+ }
+ }
replaceRoot(newRoot);
menuStack.push_back(std::move(prev));
@@ -913,10 +1036,13 @@ namespace ZL {
root = s.root;
buttons = s.buttons;
+ textButtons = s.textButtons;
sliders = s.sliders;
+ textViews = s.textViews;
textFields = s.textFields;
staticImages = s.staticImages;
pressedButtons = s.pressedButtons;
+ pressedTextButtons = s.pressedTextButtons;
pressedSliders = s.pressedSliders;
focusedTextField = s.focusedTextField;
@@ -931,6 +1057,15 @@ namespace ZL {
b->buildMesh();
}
}
+ for (auto& tb : textButtons) {
+ if (tb) {
+ tb->animOffsetX = 0.0f;
+ tb->animOffsetY = 0.0f;
+ tb->animScaleX = 1.0f;
+ tb->animScaleY = 1.0f;
+ tb->buildMesh();
+ }
+ }
for (auto& sl : sliders) {
if (sl) {
@@ -956,6 +1091,9 @@ namespace ZL {
for (const auto& b : buttons) {
b->draw(renderer);
}
+ for (const auto& tb : textButtons) {
+ tb->draw(renderer);
+ }
for (const auto& s : sliders) {
s->draw(renderer);
}
@@ -1007,6 +1145,12 @@ namespace ZL {
node->button->animScaleX = act.origScaleX;
node->button->animScaleY = act.origScaleY;
}
+ if (node->textButton) {
+ node->textButton->animOffsetX = act.origOffsetX;
+ node->textButton->animOffsetY = act.origOffsetY;
+ node->textButton->animScaleX = act.origScaleX;
+ node->textButton->animScaleY = act.origScaleY;
+ }
act.stepIndex = 0;
act.elapsedMs = 0.0f;
act.stepStarted = false;
@@ -1028,12 +1172,20 @@ namespace ZL {
node->button->animOffsetX = step.toX;
node->button->animOffsetY = step.toY;
}
+ if (node->textButton) {
+ node->textButton->animOffsetX = step.toX;
+ node->textButton->animOffsetY = step.toY;
+ }
}
else if (step.type == "scale") {
if (node->button) {
node->button->animScaleX = step.toX;
node->button->animScaleY = step.toY;
}
+ if (node->textButton) {
+ node->textButton->animScaleX = step.toX;
+ node->textButton->animScaleY = step.toY;
+ }
}
act.stepIndex++;
act.elapsedMs = 0.0f;
@@ -1048,6 +1200,12 @@ namespace ZL {
act.origScaleX = node->button->animScaleX;
act.origScaleY = node->button->animScaleY;
}
+ else if (node->textButton) {
+ act.origOffsetX = node->textButton->animOffsetX;
+ act.origOffsetY = node->textButton->animOffsetY;
+ act.origScaleX = node->textButton->animScaleX;
+ act.origScaleY = node->textButton->animScaleY;
+ }
else {
act.origOffsetX = act.origOffsetY = 0.0f;
act.origScaleX = act.origScaleY = 1.0f;
@@ -1064,6 +1222,12 @@ namespace ZL {
act.startScaleX = node->button->animScaleX;
act.startScaleY = node->button->animScaleY;
}
+ else if (node->textButton) {
+ act.startOffsetX = node->textButton->animOffsetX;
+ act.startOffsetY = node->textButton->animOffsetY;
+ act.startScaleX = node->textButton->animScaleX;
+ act.startScaleY = node->textButton->animScaleY;
+ }
else {
act.startOffsetX = act.startOffsetY = 0.0f;
act.startScaleX = act.startScaleY = 1.0f;
@@ -1089,6 +1253,10 @@ namespace ZL {
node->button->animOffsetX = nx;
node->button->animOffsetY = ny;
}
+ if (node->textButton) {
+ node->textButton->animOffsetX = nx;
+ node->textButton->animOffsetY = ny;
+ }
}
else if (step.type == "scale") {
float sx = act.startScaleX + (act.endScaleX - act.startScaleX) * te;
@@ -1097,6 +1265,10 @@ namespace ZL {
node->button->animScaleX = sx;
node->button->animScaleY = sy;
}
+ if (node->textButton) {
+ node->textButton->animScaleX = sx;
+ node->textButton->animScaleY = sy;
+ }
}
else if (step.type == "wait") {
//wait
@@ -1146,6 +1318,17 @@ namespace ZL {
}
}
}
+ for (auto& tb : textButtons) {
+ if (tb->state != ButtonState::Disabled)
+ {
+ if (tb->rect.containsConsideringBorder((float)x, (float)y, tb->border)) {
+ if (tb->state != ButtonState::Pressed) tb->state = ButtonState::Hover;
+ }
+ else {
+ if (tb->state != ButtonState::Pressed) tb->state = ButtonState::Normal;
+ }
+ }
+ }
}
auto it = pressedSliders.find(fingerId);
@@ -1180,6 +1363,18 @@ namespace ZL {
}
}
+ for (auto& tb : textButtons) {
+ if (tb->state != ButtonState::Disabled)
+ {
+ if (tb->rect.containsConsideringBorder((float)x, (float)y, tb->border)) {
+ tb->state = ButtonState::Pressed;
+ pressedTextButtons[fingerId] = tb;
+ if (tb->onPress) tb->onPress(tb->name);
+ break;
+ }
+ }
+ }
+
for (auto& s : sliders) {
if (s->rect.contains((float)x, (float)y)) {
pressedSliders[fingerId] = s;
@@ -1212,6 +1407,7 @@ namespace ZL {
void UiManager::onTouchUp(int64_t fingerId, int x, int y) {
std::vector> clicked;
+ std::vector> clickedText;
auto btnIt = pressedButtons.find(fingerId);
if (btnIt != pressedButtons.end()) {
@@ -1229,6 +1425,21 @@ namespace ZL {
pressedButtons.erase(btnIt);
}
+ auto tbIt = pressedTextButtons.find(fingerId);
+ if (tbIt != pressedTextButtons.end()) {
+ auto tb = tbIt->second;
+ if (tb) {
+ bool contains = tb->rect.contains((float)x, (float)y);
+ if (tb->state == ButtonState::Pressed) {
+ if (contains) {
+ clickedText.push_back(tb);
+ }
+ tb->state = (contains && fingerId == MOUSE_FINGER_ID) ? ButtonState::Hover : ButtonState::Normal;
+ }
+ }
+ pressedTextButtons.erase(tbIt);
+ }
+
pressedSliders.erase(fingerId);
for (auto& b : clicked) {
@@ -1236,6 +1447,11 @@ namespace ZL {
b->onClick(b->name);
}
}
+ for (auto& tb : clickedText) {
+ if (tb->onClick) {
+ tb->onClick(tb->name);
+ }
+ }
}
void UiManager::onKeyPress(unsigned char key) {
@@ -1287,6 +1503,12 @@ namespace ZL {
aa.origScaleX = node->button->animScaleX;
aa.origScaleY = node->button->animScaleY;
}
+ else if (node->textButton) {
+ aa.origOffsetX = node->textButton->animOffsetX;
+ aa.origOffsetY = node->textButton->animOffsetY;
+ aa.origScaleX = node->textButton->animScaleX;
+ aa.origScaleY = node->textButton->animScaleY;
+ }
auto cbIt = animCallbacks.find({ nodeName, animName });
if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second;
nodeActiveAnims[node].push_back(std::move(aa));
@@ -1337,6 +1559,12 @@ namespace ZL {
aa.origScaleX = n->button->animScaleX;
aa.origScaleY = n->button->animScaleY;
}
+ else if (n->textButton) {
+ aa.origOffsetX = n->textButton->animOffsetX;
+ aa.origOffsetY = n->textButton->animOffsetY;
+ aa.origScaleX = n->textButton->animScaleX;
+ aa.origScaleY = n->textButton->animScaleY;
+ }
auto cbIt = animCallbacks.find({ n->name, animName });
if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second;
nodeActiveAnims[n].push_back(std::move(aa));
@@ -1374,4 +1602,36 @@ namespace ZL {
return true;
}
+ std::shared_ptr UiManager::findTextButton(const std::string& name) {
+ for (auto& tb : textButtons) if (tb->name == name) return tb;
+ return nullptr;
+ }
+
+ bool UiManager::setTextButtonCallback(const std::string& name, std::function cb) {
+ auto tb = findTextButton(name);
+ if (!tb) {
+ std::cerr << "UiManager: setTextButtonCallback failed, textButton not found: " << name << std::endl;
+ return false;
+ }
+ tb->onClick = std::move(cb);
+ return true;
+ }
+
+ bool UiManager::setTextButtonPressCallback(const std::string& name, std::function cb) {
+ auto tb = findTextButton(name);
+ if (!tb) {
+ std::cerr << "UiManager: setTextButtonPressCallback failed, textButton not found: " << name << std::endl;
+ return false;
+ }
+ tb->onPress = std::move(cb);
+ return true;
+ }
+
+ bool UiManager::setTextButtonText(const std::string& name, const std::string& newText) {
+ auto tb = findTextButton(name);
+ if (!tb) return false;
+ tb->text = newText;
+ return true;
+ }
+
} // namespace ZL
\ No newline at end of file
diff --git a/src/UiManager.h b/src/UiManager.h
index e8f8c69..a237771 100644
--- a/src/UiManager.h
+++ b/src/UiManager.h
@@ -125,6 +125,42 @@ namespace ZL {
void draw(Renderer& renderer) const;
};
+ struct UiTextButton {
+ std::string name;
+ UiRect rect;
+ float border = 0;
+
+ // Textures are optional — button can be text-only
+ std::shared_ptr texNormal;
+ std::shared_ptr texHover;
+ std::shared_ptr texPressed;
+ std::shared_ptr texDisabled;
+
+ ButtonState state = ButtonState::Normal;
+ VertexRenderStruct mesh;
+
+ // Text drawn on top of the button
+ std::string text;
+ std::string fontPath = "resources/fonts/DroidSans.ttf";
+ int fontSize = 32;
+ std::array color = { 1.f, 1.f, 1.f, 1.f };
+ bool textCentered = true;
+
+ std::unique_ptr textRenderer;
+
+ std::function onClick;
+ std::function onPress;
+
+ // Animation runtime
+ float animOffsetX = 0.0f;
+ float animOffsetY = 0.0f;
+ float animScaleX = 1.0f;
+ float animScaleY = 1.0f;
+
+ void buildMesh();
+ void draw(Renderer& renderer) const;
+ };
+
struct UiTextView {
std::string name;
UiRect rect;
@@ -197,6 +233,7 @@ namespace ZL {
// Компоненты (только один из них обычно активен для ноды)
std::shared_ptr button;
+ std::shared_ptr textButton;
std::shared_ptr slider;
std::shared_ptr textView;
std::shared_ptr textField;
@@ -249,12 +286,12 @@ namespace ZL {
// Returns true if any finger is currently interacting with UI
bool isUiInteraction() const {
- return !pressedButtons.empty() || !pressedSliders.empty() || focusedTextField != nullptr;
+ return !pressedButtons.empty() || !pressedTextButtons.empty() || !pressedSliders.empty() || focusedTextField != nullptr;
}
// Returns true if this specific finger is currently interacting with UI
bool isUiInteractionForFinger(int64_t fingerId) const {
- return pressedButtons.count(fingerId) > 0 || pressedSliders.count(fingerId) > 0 || focusedTextField != nullptr;
+ return pressedButtons.count(fingerId) > 0 || pressedTextButtons.count(fingerId) > 0 || pressedSliders.count(fingerId) > 0 || focusedTextField != nullptr;
}
void stopAllAnimations() {
@@ -268,6 +305,14 @@ namespace ZL {
b->animScaleY = 1.0f;
}
}
+ for (auto& tb : textButtons) {
+ if (tb) {
+ tb->animOffsetX = 0.0f;
+ tb->animOffsetY = 0.0f;
+ tb->animScaleX = 1.0f;
+ tb->animScaleY = 1.0f;
+ }
+ }
}
std::shared_ptr findButton(const std::string& name);
@@ -275,6 +320,11 @@ namespace ZL {
bool setButtonCallback(const std::string& name, std::function cb);
bool setButtonPressCallback(const std::string& name, std::function cb);
+ std::shared_ptr findTextButton(const std::string& name);
+ bool setTextButtonCallback(const std::string& name, std::function cb);
+ bool setTextButtonPressCallback(const std::string& name, std::function cb);
+ bool setTextButtonText(const std::string& name, const std::string& newText);
+
bool addSlider(const std::string& name, const UiRect& rect, Renderer& renderer, const std::string& zipFile,
const std::string& trackPath, const std::string& knobPath, float initialValue = 0.0f, bool vertical = true);
@@ -333,6 +383,7 @@ namespace ZL {
std::shared_ptr root;
std::vector> buttons;
+ std::vector> textButtons;
std::vector> sliders;
std::vector> textViews;
std::vector> textFields;
@@ -343,16 +394,20 @@ namespace ZL {
// Per-finger tracking for multi-touch support
std::map> pressedButtons;
+ std::map> pressedTextButtons;
std::map> pressedSliders;
std::shared_ptr focusedTextField;
struct MenuState {
std::shared_ptr root;
std::vector> buttons;
+ std::vector> textButtons;
std::vector> sliders;
+ std::vector> textViews;
std::vector> textFields;
std::vector> staticImages;
std::map> pressedButtons;
+ std::map> pressedTextButtons;
std::map> pressedSliders;
std::shared_ptr focusedTextField;
std::string path;