Merge branch 'main' into linux
This commit is contained in:
commit
3094fc6112
@ -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%); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button id="fs-button">Fullscreen</button>
|
||||
<div id="status">Downloading...</div>
|
||||
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
BIN
resources/black.png
(Stored with Git LFS)
Normal file
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
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
BIN
resources/button_players.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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",
|
||||
|
||||
@ -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<float>(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<int>(roundf(value * 10));
|
||||
|
||||
@ -73,6 +73,7 @@ namespace ZL {
|
||||
std::function<void()> onTakeButtonPressed;
|
||||
std::function<void(const std::string&, int)> onSingleplayerPressed;
|
||||
std::function<void(const std::string&, int)> onMultiplayerPressed;
|
||||
std::function<void()> onShowPlayersPressed;
|
||||
};
|
||||
|
||||
} // namespace ZL
|
||||
184
src/Space.cpp
184
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;
|
||||
|
||||
@ -1008,7 +1023,7 @@ namespace ZL
|
||||
|
||||
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,6 +2075,8 @@ namespace ZL
|
||||
|
||||
void Space::handleDown(int mx, int my)
|
||||
{
|
||||
if (playerListVisible) return;
|
||||
|
||||
Environment::tapDownHold = true;
|
||||
|
||||
Environment::tapDownStartPos(0) = mx;
|
||||
@ -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<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
|
||||
|
||||
11
src/Space.h
11
src/Space.h
@ -126,8 +126,12 @@ namespace ZL {
|
||||
|
||||
std::unordered_set<int> 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<UiNode> buildPlayerListRoot();
|
||||
|
||||
void updateSparkEmitters(float deltaMs);
|
||||
void prepareSparkEmittersForDraw();
|
||||
void drawShipSparkEmitters();
|
||||
|
||||
@ -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<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() {
|
||||
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<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()) {
|
||||
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<std::shared_ptr<UiButton>> clicked;
|
||||
std::vector<std::shared_ptr<UiTextButton>> 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<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
|
||||
@ -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<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 {
|
||||
std::string name;
|
||||
UiRect rect;
|
||||
@ -197,6 +233,7 @@ namespace ZL {
|
||||
|
||||
// Компоненты (только один из них обычно активен для ноды)
|
||||
std::shared_ptr<UiButton> button;
|
||||
std::shared_ptr<UiTextButton> textButton;
|
||||
std::shared_ptr<UiSlider> slider;
|
||||
std::shared_ptr<UiTextView> textView;
|
||||
std::shared_ptr<UiTextField> 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<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 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,
|
||||
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::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<UiTextView>> textViews;
|
||||
std::vector<std::shared_ptr<UiTextField>> textFields;
|
||||
@ -343,16 +394,20 @@ namespace ZL {
|
||||
|
||||
// Per-finger tracking for multi-touch support
|
||||
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::shared_ptr<UiTextField> focusedTextField;
|
||||
|
||||
struct MenuState {
|
||||
std::shared_ptr<UiNode> root;
|
||||
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<UiTextView>> textViews;
|
||||
std::vector<std::shared_ptr<UiTextField>> textFields;
|
||||
std::vector<std::shared_ptr<UiStaticImage>> staticImages;
|
||||
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::shared_ptr<UiTextField> focusedTextField;
|
||||
std::string path;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user