Working on list of players

This commit is contained in:
Vladislav Khorev 2026-03-09 23:04:48 +03:00
parent a8ded217df
commit 785b96ce82
10 changed files with 544 additions and 13 deletions

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View File

@ -11,8 +11,8 @@
"name": "gameScoreText", "name": "gameScoreText",
"x": 0, "x": 0,
"y": 30, "y": 30,
"width": 80, "width": 200,
"height": 40, "height": 60,
"horizontal_gravity": "left", "horizontal_gravity": "left",
"vertical_gravity": "top", "vertical_gravity": "top",
"text": "Score: 0", "text": "Score: 0",
@ -25,6 +25,22 @@
], ],
"centered": false "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", "type": "Button",
"name": "shootButton", "name": "shootButton",

View File

@ -185,6 +185,11 @@ namespace ZL {
if (onTakeButtonPressed) onTakeButtonPressed(); if (onTakeButtonPressed) onTakeButtonPressed();
}); });
uiManager.setButtonCallback("showPlayersButton", [this](const std::string&) {
if (onShowPlayersPressed) onShowPlayersPressed();
});
/* /*
uiManager.setSliderCallback("velocitySlider", [this](const std::string&, float value) { uiManager.setSliderCallback("velocitySlider", [this](const std::string&, float value) {
int newVel = static_cast<int>(roundf(value * 10)); int newVel = static_cast<int>(roundf(value * 10));

View File

@ -73,6 +73,7 @@ namespace ZL {
std::function<void()> onTakeButtonPressed; std::function<void()> onTakeButtonPressed;
std::function<void(const std::string&, int)> onSingleplayerPressed; std::function<void(const std::string&, int)> onSingleplayerPressed;
std::function<void(const std::string&, int)> onMultiplayerPressed; std::function<void(const std::string&, int)> onMultiplayerPressed;
std::function<void()> onShowPlayersPressed;
}; };
} // namespace ZL } // namespace ZL

View File

@ -332,6 +332,10 @@ namespace ZL
firePressed = true; firePressed = true;
}; };
menuManager.onShowPlayersPressed = [this]() {
buildAndShowPlayerList();
};
menuManager.onTakeButtonPressed = [this]() { menuManager.onTakeButtonPressed = [this]() {
if (Environment::shipState.shipType != 1) return; if (Environment::shipState.shipType != 1) return;
if (!networkClient) return; if (!networkClient) return;
@ -1001,6 +1005,16 @@ namespace ZL
int Space::pickTargetId() const 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; int bestId = -1;
float bestDistSq = 1e30f; float bestDistSq = 1e30f;
@ -1008,8 +1022,8 @@ namespace ZL
if (deadRemotePlayers.count(id)) continue; if (deadRemotePlayers.count(id)) continue;
float d2 = (Environment::shipState.position - st.position).squaredNorm(); 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) { if (d2 < bestDistSq) {
bestDistSq = d2; bestDistSq = d2;
@ -1728,6 +1742,7 @@ namespace ZL
std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n"; std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n";
clearPlayerListIfVisible();
menuManager.showGameOver(this->playerScore); menuManager.showGameOver(this->playerScore);
} }
else { else {
@ -1803,6 +1818,7 @@ namespace ZL
planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty; planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty;
} }
clearPlayerListIfVisible();
menuManager.showGameOver(this->playerScore); menuManager.showGameOver(this->playerScore);
} }
} }
@ -1885,6 +1901,7 @@ namespace ZL
gameOver = true; gameOver = true;
Environment::shipState.velocity = 0.0f; Environment::shipState.velocity = 0.0f;
std::cout << "Client: Lost connection to server\n"; std::cout << "Client: Lost connection to server\n";
clearPlayerListIfVisible();
menuManager.showConnectionLost(); menuManager.showConnectionLost();
} }
@ -1923,6 +1940,7 @@ namespace ZL
} }
} }
} }
// Обработка событий смерти, присланных сервером // Обработка событий смерти, присланных сервером
auto deaths = networkClient->getPendingDeaths(); auto deaths = networkClient->getPendingDeaths();
if (!deaths.empty()) { if (!deaths.empty()) {
@ -1947,10 +1965,12 @@ namespace ZL
shipAlive = false; shipAlive = false;
gameOver = true; gameOver = true;
Environment::shipState.velocity = 0.0f; Environment::shipState.velocity = 0.0f;
clearPlayerListIfVisible();
menuManager.showGameOver(this->playerScore); menuManager.showGameOver(this->playerScore);
} }
else { else {
deadRemotePlayers.insert(d.targetId); deadRemotePlayers.insert(d.targetId);
if (d.targetId == manualTrackedTargetId) manualTrackedTargetId = -1;
std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl; std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl;
} }
if (d.killerId == localId) { if (d.killerId == localId) {
@ -1959,6 +1979,7 @@ namespace ZL
} }
} }
rebuildPlayerListIfVisible();
} }
auto respawns = networkClient->getPendingRespawns(); auto respawns = networkClient->getPendingRespawns();
@ -1976,6 +1997,7 @@ namespace ZL
std::cout << "Client: Remote player " << respawnId << " respawned, removed from dead list" << std::endl; std::cout << "Client: Remote player " << respawnId << " respawned, removed from dead list" << std::endl;
} }
} }
rebuildPlayerListIfVisible();
auto disconnects = networkClient->getPendingDisconnects(); auto disconnects = networkClient->getPendingDisconnects();
for (int pid : disconnects) { for (int pid : disconnects) {
@ -1986,9 +2008,11 @@ namespace ZL
trackedTargetId = -1; trackedTargetId = -1;
targetAcquireAnim = 0.f; targetAcquireAnim = 0.f;
} }
if (pid == manualTrackedTargetId) manualTrackedTargetId = -1;
std::cout << "Client: Remote player " << pid << " left the game, removed from scene\n"; std::cout << "Client: Remote player " << pid << " left the game, removed from scene\n";
} }
rebuildPlayerListIfVisible();
auto boxDestructions = networkClient->getPendingBoxDestructions(); auto boxDestructions = networkClient->getPendingBoxDestructions();
if (!boxDestructions.empty()) { if (!boxDestructions.empty()) {
std::cout << "Game: Received " << boxDestructions.size() << " box destruction events" << std::endl; std::cout << "Game: Received " << boxDestructions.size() << " box destruction events" << std::endl;
@ -2051,13 +2075,15 @@ namespace ZL
void Space::handleDown(int mx, int my) void Space::handleDown(int mx, int my)
{ {
Environment::tapDownHold = true; if (playerListVisible) return;
Environment::tapDownStartPos(0) = mx; Environment::tapDownHold = true;
Environment::tapDownStartPos(1) = my;
Environment::tapDownCurrentPos(0) = mx; Environment::tapDownStartPos(0) = mx;
Environment::tapDownCurrentPos(1) = my; Environment::tapDownStartPos(1) = my;
Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = my;
} }
void Space::handleUp(int mx, int my) void Space::handleUp(int mx, int my)
@ -2068,6 +2094,8 @@ namespace ZL
void Space::handleMotion(int mx, int my) void Space::handleMotion(int mx, int my)
{ {
if (playerListVisible) return;
if (Environment::tapDownHold) { if (Environment::tapDownHold) {
Environment::tapDownCurrentPos(0) = mx; Environment::tapDownCurrentPos(0) = mx;
Environment::tapDownCurrentPos(1) = my; Environment::tapDownCurrentPos(1) = my;
@ -2105,4 +2133,151 @@ namespace ZL
}*/ }*/
std::shared_ptr<UiNode> Space::buildPlayerListRoot()
{
const float btnW = 400;
const float btnH = 50.0f;
// Collect alive remote players
std::vector<std::pair<int, std::string>> 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<UiNode>();
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<UiNode>();
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<UiNode>();
btnNode->name = "playerBtn_" + std::to_string(pid);
btnNode->layoutType = LayoutType::Frame;
btnNode->width = btnW;
btnNode->height = btnH;
auto tb = std::make_shared<UiTextButton>();
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<TextRenderer>();
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", ""));
btnNode->textButton = tb;
/*auto button = std::make_shared<UiButton>();
button->name = "Hello";
button->texNormal = std::make_unique<Texture>(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<UiNode>();
backdropNode->name = "playerListBackdrop";
backdropNode->layoutType = LayoutType::Frame;
backdropNode->width = -1.0f;
backdropNode->height = -1.0f;
auto backdropTb = std::make_shared<UiTextButton>();
backdropTb->name = "playerListBackdrop";
backdropNode->textButton = backdropTb;
/*
auto backgroundNode = std::make_shared<UiNode>();
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<UiStaticImage>();
backdropImage->name = "playerListBackgroundImage";
backdropImage->texture = std::make_unique<Texture>(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 } // namespace ZL

View File

@ -129,6 +129,9 @@ namespace ZL {
int prevPlayerScore = 0; int prevPlayerScore = 0;
bool wasConnectedToServer = false; bool wasConnectedToServer = false;
bool playerListVisible = false;
int manualTrackedTargetId = -1;
static constexpr float TARGET_MAX_DIST = 50000.0f; static constexpr float TARGET_MAX_DIST = 50000.0f;
static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST; static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST;
@ -147,6 +150,13 @@ namespace ZL {
void resetPlayerState(); void resetPlayerState();
void clearTextRendererCache(); void clearTextRendererCache();
// Player list overlay
void buildAndShowPlayerList();
void closePlayerList();
void rebuildPlayerListIfVisible();
void clearPlayerListIfVisible();
std::shared_ptr<UiNode> buildPlayerListRoot();
void updateSparkEmitters(float deltaMs); void updateSparkEmitters(float deltaMs);
void prepareSparkEmittersForDraw(); void prepareSparkEmittersForDraw();
void drawShipSparkEmitters(); void drawShipSparkEmitters();

View File

@ -72,6 +72,65 @@ namespace ZL {
renderer.PopMatrix(); 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<Texture>* 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() { void UiSlider::buildTrackMesh() {
trackMesh.data.PositionData.clear(); trackMesh.data.PositionData.clear();
trackMesh.data.TexCoordData.clear(); trackMesh.data.TexCoordData.clear();
@ -393,6 +452,49 @@ namespace ZL {
node->textField = tf; node->textField = tf;
} }
if (typeStr == "TextButton") {
auto tb = std::make_shared<UiTextButton>();
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<Texture> {
if (!t.contains(key) || !t[key].is_string()) return nullptr;
std::string path = t[key].get<std::string>();
try {
auto data = CreateTextureDataFromPng(path.c_str(), zipFile.c_str());
return std::make_shared<Texture>(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<std::string>();
if (j.contains("fontPath")) tb->fontPath = j["fontPath"].get<std::string>();
if (j.contains("fontSize")) tb->fontSize = j["fontSize"].get<int>();
if (j.contains("textCentered")) tb->textCentered = j["textCentered"].get<bool>();
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<float>();
}
tb->textRenderer = std::make_unique<TextRenderer>();
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()) { if (j.contains("animations") && j["animations"].is_object()) {
for (auto it = j["animations"].begin(); it != j["animations"].end(); ++it) { for (auto it = j["animations"].begin(); it != j["animations"].end(); ++it) {
std::string animName = it.key(); std::string animName = it.key();
@ -536,6 +638,7 @@ namespace ZL {
root->localY // finalLocalY root->localY // finalLocalY
); );
buttons.clear(); buttons.clear();
textButtons.clear();
sliders.clear(); sliders.clear();
textViews.clear(); textViews.clear();
textFields.clear(); textFields.clear();
@ -547,6 +650,9 @@ namespace ZL {
for (auto& b : buttons) { for (auto& b : buttons) {
b->buildMesh(); b->buildMesh();
} }
for (auto& tb : textButtons) {
tb->buildMesh();
}
for (auto& s : sliders) { for (auto& s : sliders) {
s->buildTrackMesh(); s->buildTrackMesh();
s->buildKnobMesh(); s->buildKnobMesh();
@ -694,11 +800,14 @@ namespace ZL {
// 1. Обновляем кнопку // 1. Обновляем кнопку
if (node->button) { if (node->button) {
node->button->rect = node->screenRect; node->button->rect = node->screenRect;
// Если у кнопки есть анимационные смещения, они учитываются внутри buildMesh
// или при рендеринге через Uniform-переменные матрицы модели.
node->button->buildMesh(); node->button->buildMesh();
} }
if (node->textButton) {
node->textButton->rect = node->screenRect;
node->textButton->buildMesh();
}
// 2. Обновляем слайдер // 2. Обновляем слайдер
if (node->slider) { if (node->slider) {
node->slider->rect = node->screenRect; node->slider->rect = node->screenRect;
@ -744,6 +853,9 @@ namespace ZL {
if (node->button) { if (node->button) {
buttons.push_back(node->button); buttons.push_back(node->button);
} }
if (node->textButton) {
textButtons.push_back(node->textButton);
}
if (node->slider) { if (node->slider) {
sliders.push_back(node->slider); sliders.push_back(node->slider);
} }
@ -862,10 +974,13 @@ namespace ZL {
MenuState prev; MenuState prev;
prev.root = root; prev.root = root;
prev.buttons = buttons; prev.buttons = buttons;
prev.textButtons = textButtons;
prev.sliders = sliders; prev.sliders = sliders;
prev.textViews = textViews;
prev.textFields = textFields; prev.textFields = textFields;
prev.staticImages = staticImages; prev.staticImages = staticImages;
prev.pressedButtons = pressedButtons; prev.pressedButtons = pressedButtons;
prev.pressedTextButtons = pressedTextButtons;
prev.pressedSliders = pressedSliders; prev.pressedSliders = pressedSliders;
prev.focusedTextField = focusedTextField; prev.focusedTextField = focusedTextField;
prev.path = ""; prev.path = "";
@ -884,6 +999,14 @@ namespace ZL {
b->animScaleY = 1.0f; 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); replaceRoot(newRoot);
menuStack.push_back(std::move(prev)); menuStack.push_back(std::move(prev));
@ -913,10 +1036,13 @@ namespace ZL {
root = s.root; root = s.root;
buttons = s.buttons; buttons = s.buttons;
textButtons = s.textButtons;
sliders = s.sliders; sliders = s.sliders;
textViews = s.textViews;
textFields = s.textFields; textFields = s.textFields;
staticImages = s.staticImages; staticImages = s.staticImages;
pressedButtons = s.pressedButtons; pressedButtons = s.pressedButtons;
pressedTextButtons = s.pressedTextButtons;
pressedSliders = s.pressedSliders; pressedSliders = s.pressedSliders;
focusedTextField = s.focusedTextField; focusedTextField = s.focusedTextField;
@ -931,6 +1057,15 @@ namespace ZL {
b->buildMesh(); 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) { for (auto& sl : sliders) {
if (sl) { if (sl) {
@ -956,6 +1091,9 @@ namespace ZL {
for (const auto& b : buttons) { for (const auto& b : buttons) {
b->draw(renderer); b->draw(renderer);
} }
for (const auto& tb : textButtons) {
tb->draw(renderer);
}
for (const auto& s : sliders) { for (const auto& s : sliders) {
s->draw(renderer); s->draw(renderer);
} }
@ -1007,6 +1145,12 @@ namespace ZL {
node->button->animScaleX = act.origScaleX; node->button->animScaleX = act.origScaleX;
node->button->animScaleY = act.origScaleY; 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.stepIndex = 0;
act.elapsedMs = 0.0f; act.elapsedMs = 0.0f;
act.stepStarted = false; act.stepStarted = false;
@ -1028,12 +1172,20 @@ namespace ZL {
node->button->animOffsetX = step.toX; node->button->animOffsetX = step.toX;
node->button->animOffsetY = step.toY; node->button->animOffsetY = step.toY;
} }
if (node->textButton) {
node->textButton->animOffsetX = step.toX;
node->textButton->animOffsetY = step.toY;
}
} }
else if (step.type == "scale") { else if (step.type == "scale") {
if (node->button) { if (node->button) {
node->button->animScaleX = step.toX; node->button->animScaleX = step.toX;
node->button->animScaleY = step.toY; node->button->animScaleY = step.toY;
} }
if (node->textButton) {
node->textButton->animScaleX = step.toX;
node->textButton->animScaleY = step.toY;
}
} }
act.stepIndex++; act.stepIndex++;
act.elapsedMs = 0.0f; act.elapsedMs = 0.0f;
@ -1048,6 +1200,12 @@ namespace ZL {
act.origScaleX = node->button->animScaleX; act.origScaleX = node->button->animScaleX;
act.origScaleY = node->button->animScaleY; 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 { else {
act.origOffsetX = act.origOffsetY = 0.0f; act.origOffsetX = act.origOffsetY = 0.0f;
act.origScaleX = act.origScaleY = 1.0f; act.origScaleX = act.origScaleY = 1.0f;
@ -1064,6 +1222,12 @@ namespace ZL {
act.startScaleX = node->button->animScaleX; act.startScaleX = node->button->animScaleX;
act.startScaleY = node->button->animScaleY; 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 { else {
act.startOffsetX = act.startOffsetY = 0.0f; act.startOffsetX = act.startOffsetY = 0.0f;
act.startScaleX = act.startScaleY = 1.0f; act.startScaleX = act.startScaleY = 1.0f;
@ -1089,6 +1253,10 @@ namespace ZL {
node->button->animOffsetX = nx; node->button->animOffsetX = nx;
node->button->animOffsetY = ny; node->button->animOffsetY = ny;
} }
if (node->textButton) {
node->textButton->animOffsetX = nx;
node->textButton->animOffsetY = ny;
}
} }
else if (step.type == "scale") { else if (step.type == "scale") {
float sx = act.startScaleX + (act.endScaleX - act.startScaleX) * te; float sx = act.startScaleX + (act.endScaleX - act.startScaleX) * te;
@ -1097,6 +1265,10 @@ namespace ZL {
node->button->animScaleX = sx; node->button->animScaleX = sx;
node->button->animScaleY = sy; node->button->animScaleY = sy;
} }
if (node->textButton) {
node->textButton->animScaleX = sx;
node->textButton->animScaleY = sy;
}
} }
else if (step.type == "wait") { else if (step.type == "wait") {
//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); 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) { for (auto& s : sliders) {
if (s->rect.contains((float)x, (float)y)) { if (s->rect.contains((float)x, (float)y)) {
pressedSliders[fingerId] = s; pressedSliders[fingerId] = s;
@ -1212,6 +1407,7 @@ namespace ZL {
void UiManager::onTouchUp(int64_t fingerId, int x, int y) { void UiManager::onTouchUp(int64_t fingerId, int x, int y) {
std::vector<std::shared_ptr<UiButton>> clicked; std::vector<std::shared_ptr<UiButton>> clicked;
std::vector<std::shared_ptr<UiTextButton>> clickedText;
auto btnIt = pressedButtons.find(fingerId); auto btnIt = pressedButtons.find(fingerId);
if (btnIt != pressedButtons.end()) { if (btnIt != pressedButtons.end()) {
@ -1229,6 +1425,21 @@ namespace ZL {
pressedButtons.erase(btnIt); 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); pressedSliders.erase(fingerId);
for (auto& b : clicked) { for (auto& b : clicked) {
@ -1236,6 +1447,11 @@ namespace ZL {
b->onClick(b->name); b->onClick(b->name);
} }
} }
for (auto& tb : clickedText) {
if (tb->onClick) {
tb->onClick(tb->name);
}
}
} }
void UiManager::onKeyPress(unsigned char key) { void UiManager::onKeyPress(unsigned char key) {
@ -1287,6 +1503,12 @@ namespace ZL {
aa.origScaleX = node->button->animScaleX; aa.origScaleX = node->button->animScaleX;
aa.origScaleY = node->button->animScaleY; 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 }); auto cbIt = animCallbacks.find({ nodeName, animName });
if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second; if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second;
nodeActiveAnims[node].push_back(std::move(aa)); nodeActiveAnims[node].push_back(std::move(aa));
@ -1337,6 +1559,12 @@ namespace ZL {
aa.origScaleX = n->button->animScaleX; aa.origScaleX = n->button->animScaleX;
aa.origScaleY = n->button->animScaleY; 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 }); auto cbIt = animCallbacks.find({ n->name, animName });
if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second; if (cbIt != animCallbacks.end()) aa.onComplete = cbIt->second;
nodeActiveAnims[n].push_back(std::move(aa)); nodeActiveAnims[n].push_back(std::move(aa));
@ -1374,4 +1602,36 @@ namespace ZL {
return true; return true;
} }
std::shared_ptr<UiTextButton> 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<void(const std::string&)> 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<void(const std::string&)> 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 } // namespace ZL

View File

@ -125,6 +125,42 @@ namespace ZL {
void draw(Renderer& renderer) const; 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<Texture> texNormal;
std::shared_ptr<Texture> texHover;
std::shared_ptr<Texture> texPressed;
std::shared_ptr<Texture> 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<float, 4> color = { 1.f, 1.f, 1.f, 1.f };
bool textCentered = true;
std::unique_ptr<TextRenderer> textRenderer;
std::function<void(const std::string&)> onClick;
std::function<void(const std::string&)> 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 { struct UiTextView {
std::string name; std::string name;
UiRect rect; UiRect rect;
@ -197,6 +233,7 @@ namespace ZL {
// Компоненты (только один из них обычно активен для ноды) // Компоненты (только один из них обычно активен для ноды)
std::shared_ptr<UiButton> button; std::shared_ptr<UiButton> button;
std::shared_ptr<UiTextButton> textButton;
std::shared_ptr<UiSlider> slider; std::shared_ptr<UiSlider> slider;
std::shared_ptr<UiTextView> textView; std::shared_ptr<UiTextView> textView;
std::shared_ptr<UiTextField> textField; std::shared_ptr<UiTextField> textField;
@ -249,12 +286,12 @@ namespace ZL {
// Returns true if any finger is currently interacting with UI // Returns true if any finger is currently interacting with UI
bool isUiInteraction() const { 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 // Returns true if this specific finger is currently interacting with UI
bool isUiInteractionForFinger(int64_t fingerId) const { 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() { void stopAllAnimations() {
@ -268,6 +305,14 @@ namespace ZL {
b->animScaleY = 1.0f; 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<UiButton> findButton(const std::string& name); std::shared_ptr<UiButton> findButton(const std::string& name);
@ -275,6 +320,11 @@ namespace ZL {
bool setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb); bool setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb);
bool setButtonPressCallback(const std::string& name, std::function<void(const std::string&)> cb); bool setButtonPressCallback(const std::string& name, std::function<void(const std::string&)> cb);
std::shared_ptr<UiTextButton> findTextButton(const std::string& name);
bool setTextButtonCallback(const std::string& name, std::function<void(const std::string&)> cb);
bool setTextButtonPressCallback(const std::string& name, std::function<void(const std::string&)> 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, 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); const std::string& trackPath, const std::string& knobPath, float initialValue = 0.0f, bool vertical = true);
@ -333,6 +383,7 @@ namespace ZL {
std::shared_ptr<UiNode> root; std::shared_ptr<UiNode> root;
std::vector<std::shared_ptr<UiButton>> buttons; std::vector<std::shared_ptr<UiButton>> buttons;
std::vector<std::shared_ptr<UiTextButton>> textButtons;
std::vector<std::shared_ptr<UiSlider>> sliders; std::vector<std::shared_ptr<UiSlider>> sliders;
std::vector<std::shared_ptr<UiTextView>> textViews; std::vector<std::shared_ptr<UiTextView>> textViews;
std::vector<std::shared_ptr<UiTextField>> textFields; std::vector<std::shared_ptr<UiTextField>> textFields;
@ -343,16 +394,20 @@ namespace ZL {
// Per-finger tracking for multi-touch support // Per-finger tracking for multi-touch support
std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons; std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons;
std::map<int64_t, std::shared_ptr<UiTextButton>> pressedTextButtons;
std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders; std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders;
std::shared_ptr<UiTextField> focusedTextField; std::shared_ptr<UiTextField> focusedTextField;
struct MenuState { struct MenuState {
std::shared_ptr<UiNode> root; std::shared_ptr<UiNode> root;
std::vector<std::shared_ptr<UiButton>> buttons; std::vector<std::shared_ptr<UiButton>> buttons;
std::vector<std::shared_ptr<UiTextButton>> textButtons;
std::vector<std::shared_ptr<UiSlider>> sliders; std::vector<std::shared_ptr<UiSlider>> sliders;
std::vector<std::shared_ptr<UiTextView>> textViews;
std::vector<std::shared_ptr<UiTextField>> textFields; std::vector<std::shared_ptr<UiTextField>> textFields;
std::vector<std::shared_ptr<UiStaticImage>> staticImages; std::vector<std::shared_ptr<UiStaticImage>> staticImages;
std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons; std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons;
std::map<int64_t, std::shared_ptr<UiTextButton>> pressedTextButtons;
std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders; std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders;
std::shared_ptr<UiTextField> focusedTextField; std::shared_ptr<UiTextField> focusedTextField;
std::string path; std::string path;