Working on health bar

This commit is contained in:
Vladislav Khorev 2026-06-01 22:03:05 +03:00
parent 8552725dfc
commit 22b0e1992f
12 changed files with 121 additions and 57 deletions

View File

@ -89,15 +89,21 @@
"texture": "resources/w/ui/img/HealthBarBack001.png" "texture": "resources/w/ui/img/HealthBarBack001.png"
}, },
{ {
"type": "StaticImage", "type": "Slider",
"name": "healthBarBorder", "name": "healthBarFill",
"width": 151.2, "width": 151.2,
"height": 22.8, "height": 22.8,
"y": 49.8, "y": 49.8,
"x": 100.6, "x": 100.6,
"horizontal_gravity": "left", "horizontal_gravity": "left",
"vertical_align": "top", "vertical_gravity": "top",
"texture": "resources/w/ui/img/HealthBarFill001.png" "orientation": "horizontal",
"value": 1.0,
"fillMode": true,
"interactive": false,
"textures": {
"track": "resources/w/ui/img/HealthBarFill001.png"
}
}, },
{ {
"type": "TextView", "type": "TextView",

View File

@ -89,15 +89,21 @@
"texture": "resources/w/ui/img/HealthBarBack001.png" "texture": "resources/w/ui/img/HealthBarBack001.png"
}, },
{ {
"type": "StaticImage", "type": "Slider",
"name": "healthBarBorder", "name": "healthBarFill",
"width": 151.2, "width": 151.2,
"height": 22.8, "height": 22.8,
"y": 49.8, "y": 49.8,
"x": 100.6, "x": 100.6,
"horizontal_gravity": "left", "horizontal_gravity": "left",
"vertical_align": "top", "vertical_gravity": "top",
"texture": "resources/w/ui/img/HealthBarFill001.png" "orientation": "horizontal",
"value": 1.0,
"fillMode": true,
"interactive": false,
"textures": {
"track": "resources/w/ui/img/HealthBarFill001.png"
}
}, },
{ {
"type": "TextView", "type": "TextView",

View File

@ -99,32 +99,6 @@
"horizontal_gravity": "center", "horizontal_gravity": "center",
"vertical_align": "top", "vertical_align": "top",
"texture": "resources/w/ui/img/Location_uni_int.png" "texture": "resources/w/ui/img/Location_uni_int.png"
},
{
"type": "StaticImage",
"name": "hint_darklands001",
"width": 470,
"height": 156,
"x": 630,
"y": 200,
"texture": "resources/w/ui/img/Hint_darklands001.png",
"pulse": {
"minScale": 0.92,
"maxScale": 1.08,
"periodMs": 1500
},
"fadeIn": {
"durationMs": 600
}
},
{
"type": "StaticImage",
"name": "hint_darklands001_arrow",
"width": 24,
"height": 118,
"x": 860,
"y": 110,
"texture": "resources/w/ui/img/hint_arrow_up.png"
} }
] ]
} }

View File

@ -115,15 +115,21 @@
"texture": "resources/w/ui/img/HealthBarBack001.png" "texture": "resources/w/ui/img/HealthBarBack001.png"
}, },
{ {
"type": "StaticImage", "type": "Slider",
"name": "healthBarFill", "name": "healthBarFill",
"width": 151.2, "width": 151.2,
"height": 22.8, "height": 22.8,
"y": 49.8, "y": 49.8,
"x": 100.6, "x": 100.6,
"horizontal_gravity": "left", "horizontal_gravity": "left",
"vertical_align": "top", "vertical_gravity": "top",
"texture": "resources/w/ui/img/HealthBarFill001.png" "orientation": "horizontal",
"value": 1.0,
"fillMode": true,
"interactive": false,
"textures": {
"track": "resources/w/ui/img/HealthBarFill001.png"
}
}, },
{ {
"type": "TextView", "type": "TextView",

View File

@ -98,15 +98,21 @@
"texture": "resources/w/ui/img/HealthBarBack001.png" "texture": "resources/w/ui/img/HealthBarBack001.png"
}, },
{ {
"type": "StaticImage", "type": "Slider",
"name": "healthBarFill", "name": "healthBarFill",
"width": 151.2, "width": 151.2,
"height": 22.8, "height": 22.8,
"y": 49.8, "y": 49.8,
"x": 100.6, "x": 100.6,
"horizontal_gravity": "left", "horizontal_gravity": "left",
"vertical_align": "top", "vertical_gravity": "top",
"texture": "resources/w/ui/img/HealthBarFill001.png" "orientation": "horizontal",
"value": 1.0,
"fillMode": true,
"interactive": false,
"textures": {
"track": "resources/w/ui/img/HealthBarFill001.png"
}
}, },
{ {
"type": "TextView", "type": "TextView",

View File

@ -922,6 +922,8 @@ void Character::applyDamage(float damageAmount, const Eigen::Vector3f& attackDir
hp = hp - damageAmount; hp = hp - damageAmount;
if (hp < 0) hp = 0; if (hp < 0) hp = 0;
if (onHpChanged) onHpChanged(hp, initialHp);
if (!hitSparkEmitter.isConfigured()) return; if (!hitSparkEmitter.isConfigured()) return;
Eigen::Vector3f emitPos = position + Eigen::Vector3f(0.f, 1.f, 0.f); Eigen::Vector3f emitPos = position + Eigen::Vector3f(0.f, 1.f, 0.f);
@ -973,7 +975,7 @@ void Character::drawHealthBar(Renderer& renderer,
const Eigen::Matrix4f& cameraViewMatrix, const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix) const Eigen::Matrix4f& projectionMatrix)
{ {
if (!isPlayer && !canAttack) return; if (isPlayer || !canAttack) return;
if (!enabled) return; if (!enabled) return;
if (hp <= 0.f) return; if (hp <= 0.f) return;
if (initialHp <= 0.f) return; if (initialHp <= 0.f) return;

View File

@ -160,6 +160,9 @@ public:
// Called once when the death animation finishes and the character enters DEATH_IDLE. // Called once when the death animation finishes and the character enters DEATH_IDLE.
std::function<void()> onDeathAnimComplete; std::function<void()> onDeathAnimComplete;
// Called whenever hp changes (e.g. on damage). Arguments: current hp, max hp.
std::function<void(float hp, float maxHp)> onHpChanged;
private: private:
std::map<AnimationState, BoneAnimationDataNew> animations; std::map<AnimationState, BoneAnimationDataNew> animations;

View File

@ -131,7 +131,6 @@ namespace ZL
} }
void Game::setupPart2() void Game::setupPart2()
{ {
@ -461,6 +460,15 @@ namespace ZL
std::cerr << "Failed to load UI: " << e.what() << std::endl; std::cerr << "Failed to load UI: " << e.what() << std::endl;
} }
// Wire HP-change callbacks so all player instances update the health bar HUD.
for (auto& [name, loc] : locations) {
if (loc->player) {
loc->player->onHpChanged = [this](float hp, float maxHp) {
menuManager.updateHealthBar(hp, maxHp);
};
}
}
loadingCompleted = true; loadingCompleted = true;
if (audioPlayer->init()) { if (audioPlayer->init()) {

View File

@ -110,6 +110,7 @@ namespace ZL {
void MenuManager::enterGameplay() { void MenuManager::enterGameplay() {
state = GameState::Gameplay; state = GameState::Gameplay;
uiManager.replaceRoot(hudRoot); uiManager.replaceRoot(hudRoot);
applyCurrentHealthBar();
/* /*
uiManager.setTextButtonCallback("inventory_button", [this](const std::string&) { uiManager.setTextButtonCallback("inventory_button", [this](const std::string&) {
openInventory(); openInventory();
@ -366,12 +367,14 @@ namespace ZL {
if (locationName == "uni_exterior") { if (locationName == "uni_exterior") {
uiManager.replaceRoot(currentIsDarklands_ ? hudUniExtDarkRoot : hudUniExtRoot); uiManager.replaceRoot(currentIsDarklands_ ? hudUniExtDarkRoot : hudUniExtRoot);
applyCurrentHealthBar();
setupGameplayHudCallbacks(); setupGameplayHudCallbacks();
} else if (locationName == "uni_interior") { } else if (locationName == "uni_interior") {
applyUniIntHud(); applyUniIntHud();
} else { } else {
// Returning to dorm: reuse step5ab, suppress already-completed hints // Returning to dorm: reuse step5ab, suppress already-completed hints
uiManager.replaceRoot(hudStep5abRoot); uiManager.replaceRoot(hudStep5abRoot);
applyCurrentHealthBar();
setupStep5Callbacks(); setupStep5Callbacks();
} }
} }
@ -402,6 +405,7 @@ namespace ZL {
if (state == GameState::Gameplay && nextRoot) { if (state == GameState::Gameplay && nextRoot) {
uiManager.replaceRoot(nextRoot); uiManager.replaceRoot(nextRoot);
applyCurrentHealthBar();
uiManager.setButtonCallback("inventoryButton", [this](const std::string&) { uiManager.setButtonCallback("inventoryButton", [this](const std::string&) {
openInventory(); openInventory();
@ -450,6 +454,7 @@ namespace ZL {
if (nextRoot) { if (nextRoot) {
uiManager.replaceRoot(nextRoot); uiManager.replaceRoot(nextRoot);
applyCurrentHealthBar();
// Register item-screen buttons and re-apply any already-completed hint visibility. // Register item-screen buttons and re-apply any already-completed hint visibility.
setupStep5Callbacks(); setupStep5Callbacks();
} }
@ -628,6 +633,7 @@ namespace ZL {
} else if (currentLocationName_ == "uni_exterior") { } else if (currentLocationName_ == "uni_exterior") {
if (state == GameState::Gameplay) { if (state == GameState::Gameplay) {
uiManager.replaceRoot(enabled ? hudUniExtDarkRoot : hudUniExtRoot); uiManager.replaceRoot(enabled ? hudUniExtDarkRoot : hudUniExtRoot);
applyCurrentHealthBar();
setupGameplayHudCallbacks(); setupGameplayHudCallbacks();
} }
} else { } else {
@ -654,6 +660,7 @@ namespace ZL {
} }
} }
uiManager.replaceRoot(root); uiManager.replaceRoot(root);
applyCurrentHealthBar();
setupGameplayHudCallbacks(); setupGameplayHudCallbacks();
} }
@ -684,4 +691,20 @@ namespace ZL {
applyUniIntHud(); applyUniIntHud();
} }
void MenuManager::updateHealthBar(float hp, float maxHp) {
currentPlayerHp_ = hp;
currentPlayerMaxHp_ = maxHp;
applyCurrentHealthBar();
}
void MenuManager::applyCurrentHealthBar() {
if (currentPlayerMaxHp_ <= 0.f) return;
const float fraction = std::clamp(currentPlayerHp_ / currentPlayerMaxHp_, 0.f, 1.f);
uiManager.setSliderValue("healthBarFill", fraction);
std::string hpText = std::to_string(static_cast<int>(currentPlayerHp_)) + "/" +
std::to_string(static_cast<int>(currentPlayerMaxHp_));
if (hpText.size() < 7) hpText.insert(0, 7 - hpText.size(), ' ');
uiManager.setText("healthValue", hpText);
}
} // namespace ZL } // namespace ZL

View File

@ -67,6 +67,9 @@ namespace ZL {
void advanceUniIntDarklandsHud(); void advanceUniIntDarklandsHud();
void onPlayerStartedWalking(); void onPlayerStartedWalking();
// Called by Game when the player's HP changes. Stores values and updates HUD.
void updateHealthBar(float hp, float maxHp);
TutorialStep tutorialStep = TutorialStep::Step0; TutorialStep tutorialStep = TutorialStep::Step0;
protected: protected:
@ -130,6 +133,10 @@ namespace ZL {
std::shared_ptr<Texture> texItemSelected_; std::shared_ptr<Texture> texItemSelected_;
std::shared_ptr<Texture> texItemTransparent_; std::shared_ptr<Texture> texItemTransparent_;
float currentPlayerHp_ = 200.f;
float currentPlayerMaxHp_ = 200.f;
void applyCurrentHealthBar();
int selectedQuestIndex = -1; int selectedQuestIndex = -1;
std::vector<std::string> visibleQuestIds; std::vector<std::string> visibleQuestIds;

View File

@ -284,9 +284,22 @@ namespace ZL {
float x0 = rect.x; float x0 = rect.x;
float y0 = rect.y; float y0 = rect.y;
float x1 = rect.x + rect.w;
float y1 = rect.y + rect.h; float y1 = rect.y + rect.h;
float x1, u1;
if (fillMode) {
const float clamped = std::clamp(value, 0.0f, 1.0f);
if (clamped <= 0.0f) {
trackMesh.RefreshVBO();
return;
}
x1 = rect.x + rect.w * clamped;
u1 = clamped;
} else {
x1 = rect.x + rect.w;
u1 = 1.0f;
}
trackMesh.data.PositionData.push_back({ x0, y0, 0 }); trackMesh.data.PositionData.push_back({ x0, y0, 0 });
trackMesh.data.TexCoordData.push_back({ 0, 0 }); trackMesh.data.TexCoordData.push_back({ 0, 0 });
@ -294,16 +307,16 @@ namespace ZL {
trackMesh.data.TexCoordData.push_back({ 0, 1 }); trackMesh.data.TexCoordData.push_back({ 0, 1 });
trackMesh.data.PositionData.push_back({ x1, y1, 0 }); trackMesh.data.PositionData.push_back({ x1, y1, 0 });
trackMesh.data.TexCoordData.push_back({ 1, 1 }); trackMesh.data.TexCoordData.push_back({ u1, 1 });
trackMesh.data.PositionData.push_back({ x0, y0, 0 }); trackMesh.data.PositionData.push_back({ x0, y0, 0 });
trackMesh.data.TexCoordData.push_back({ 0, 0 }); trackMesh.data.TexCoordData.push_back({ 0, 0 });
trackMesh.data.PositionData.push_back({ x1, y1, 0 }); trackMesh.data.PositionData.push_back({ x1, y1, 0 });
trackMesh.data.TexCoordData.push_back({ 1, 1 }); trackMesh.data.TexCoordData.push_back({ u1, 1 });
trackMesh.data.PositionData.push_back({ x1, y0, 0 }); trackMesh.data.PositionData.push_back({ x1, y0, 0 });
trackMesh.data.TexCoordData.push_back({ 1, 0 }); trackMesh.data.TexCoordData.push_back({ u1, 0 });
trackMesh.RefreshVBO(); trackMesh.RefreshVBO();
} }
@ -566,6 +579,8 @@ namespace ZL {
std::transform(orient.begin(), orient.end(), orient.begin(), ::tolower); std::transform(orient.begin(), orient.end(), orient.begin(), ::tolower);
s->vertical = (orient != "horizontal"); s->vertical = (orient != "horizontal");
} }
if (j.contains("fillMode")) s->fillMode = j["fillMode"].get<bool>();
if (j.contains("interactive")) s->interactive = j["interactive"].get<bool>();
node->slider = s; node->slider = s;
} }
@ -1162,6 +1177,7 @@ namespace ZL {
value = std::clamp(value, 0.0f, 1.0f); value = std::clamp(value, 0.0f, 1.0f);
if (fabs(s->value - value) < 1e-6f) return true; if (fabs(s->value - value) < 1e-6f) return true;
s->value = value; s->value = value;
s->buildTrackMesh();
s->buildKnobMesh(); s->buildKnobMesh();
if (s->onValueChanged) s->onValueChanged(s->name, s->value); if (s->onValueChanged) s->onValueChanged(s->name, s->value);
return true; return true;
@ -1634,18 +1650,21 @@ namespace ZL {
auto it = pressedSliders.find(fingerId); auto it = pressedSliders.find(fingerId);
if (it != pressedSliders.end()) { if (it != pressedSliders.end()) {
auto s = it->second; auto s = it->second;
float t; if (s->interactive) {
if (s->vertical) { float t;
t = (y - s->rect.y) / s->rect.h; if (s->vertical) {
t = (y - s->rect.y) / s->rect.h;
}
else {
t = (x - s->rect.x) / s->rect.w;
}
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
s->value = t;
s->buildTrackMesh();
s->buildKnobMesh();
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
} }
else {
t = (x - s->rect.x) / s->rect.w;
}
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
s->value = t;
s->buildKnobMesh();
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
} }
} }
@ -1676,6 +1695,7 @@ namespace ZL {
} }
for (auto& s : sliders) { for (auto& s : sliders) {
if (!s->interactive) continue;
if (s->rect.contains((float)x, (float)y)) { if (s->rect.contains((float)x, (float)y)) {
pressedSliders[fingerId] = s; pressedSliders[fingerId] = s;
float t; float t;
@ -1688,6 +1708,7 @@ namespace ZL {
if (t < 0.0f) t = 0.0f; if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f; if (t > 1.0f) t = 1.0f;
s->value = t; s->value = t;
s->buildTrackMesh();
s->buildKnobMesh(); s->buildKnobMesh();
if (s->onValueChanged) s->onValueChanged(s->name, s->value); if (s->onValueChanged) s->onValueChanged(s->name, s->value);
break; break;

View File

@ -129,6 +129,8 @@ namespace ZL {
float value = 0.0f; float value = 0.0f;
bool vertical = true; bool vertical = true;
bool fillMode = false; // track width = value * rect.w (display-only bar)
bool interactive = true; // false = ignore touch events
std::function<void(const std::string&, float)> onValueChanged; std::function<void(const std::string&, float)> onValueChanged;