diff --git a/resources/w/ui/hud_uni_ext_dark.json b/resources/w/ui/hud_uni_ext_dark.json index 09a8f09..b7f543c 100644 --- a/resources/w/ui/hud_uni_ext_dark.json +++ b/resources/w/ui/hud_uni_ext_dark.json @@ -89,15 +89,21 @@ "texture": "resources/w/ui/img/HealthBarBack001.png" }, { - "type": "StaticImage", - "name": "healthBarBorder", + "type": "Slider", + "name": "healthBarFill", "width": 151.2, "height": 22.8, "y": 49.8, "x": 100.6, "horizontal_gravity": "left", - "vertical_align": "top", - "texture": "resources/w/ui/img/HealthBarFill001.png" + "vertical_gravity": "top", + "orientation": "horizontal", + "value": 1.0, + "fillMode": true, + "interactive": false, + "textures": { + "track": "resources/w/ui/img/HealthBarFill001.png" + } }, { "type": "TextView", diff --git a/resources/w/ui/hud_uni_int_dark_full.json b/resources/w/ui/hud_uni_int_dark_full.json index b0a5213..21f6d93 100644 --- a/resources/w/ui/hud_uni_int_dark_full.json +++ b/resources/w/ui/hud_uni_int_dark_full.json @@ -89,15 +89,21 @@ "texture": "resources/w/ui/img/HealthBarBack001.png" }, { - "type": "StaticImage", - "name": "healthBarBorder", + "type": "Slider", + "name": "healthBarFill", "width": 151.2, "height": 22.8, "y": 49.8, "x": 100.6, "horizontal_gravity": "left", - "vertical_align": "top", - "texture": "resources/w/ui/img/HealthBarFill001.png" + "vertical_gravity": "top", + "orientation": "horizontal", + "value": 1.0, + "fillMode": true, + "interactive": false, + "textures": { + "track": "resources/w/ui/img/HealthBarFill001.png" + } }, { "type": "TextView", diff --git a/resources/w/ui/hud_uni_int_full.json b/resources/w/ui/hud_uni_int_full.json index c788eef..53708af 100644 --- a/resources/w/ui/hud_uni_int_full.json +++ b/resources/w/ui/hud_uni_int_full.json @@ -99,32 +99,6 @@ "horizontal_gravity": "center", "vertical_align": "top", "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" } ] } diff --git a/resources/w/ui/hud_uni_int_step12.json b/resources/w/ui/hud_uni_int_step12.json index 64abba5..81c0308 100644 --- a/resources/w/ui/hud_uni_int_step12.json +++ b/resources/w/ui/hud_uni_int_step12.json @@ -115,15 +115,21 @@ "texture": "resources/w/ui/img/HealthBarBack001.png" }, { - "type": "StaticImage", + "type": "Slider", "name": "healthBarFill", "width": 151.2, "height": 22.8, "y": 49.8, "x": 100.6, "horizontal_gravity": "left", - "vertical_align": "top", - "texture": "resources/w/ui/img/HealthBarFill001.png" + "vertical_gravity": "top", + "orientation": "horizontal", + "value": 1.0, + "fillMode": true, + "interactive": false, + "textures": { + "track": "resources/w/ui/img/HealthBarFill001.png" + } }, { "type": "TextView", diff --git a/resources/w/ui/hud_uni_int_step13.json b/resources/w/ui/hud_uni_int_step13.json index bb8a8d1..352f0cf 100644 --- a/resources/w/ui/hud_uni_int_step13.json +++ b/resources/w/ui/hud_uni_int_step13.json @@ -98,15 +98,21 @@ "texture": "resources/w/ui/img/HealthBarBack001.png" }, { - "type": "StaticImage", + "type": "Slider", "name": "healthBarFill", "width": 151.2, "height": 22.8, "y": 49.8, "x": 100.6, "horizontal_gravity": "left", - "vertical_align": "top", - "texture": "resources/w/ui/img/HealthBarFill001.png" + "vertical_gravity": "top", + "orientation": "horizontal", + "value": 1.0, + "fillMode": true, + "interactive": false, + "textures": { + "track": "resources/w/ui/img/HealthBarFill001.png" + } }, { "type": "TextView", diff --git a/src/Character.cpp b/src/Character.cpp index 8b47a60..6093d9c 100644 --- a/src/Character.cpp +++ b/src/Character.cpp @@ -922,6 +922,8 @@ void Character::applyDamage(float damageAmount, const Eigen::Vector3f& attackDir hp = hp - damageAmount; if (hp < 0) hp = 0; + if (onHpChanged) onHpChanged(hp, initialHp); + if (!hitSparkEmitter.isConfigured()) return; 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& projectionMatrix) { - if (!isPlayer && !canAttack) return; + if (isPlayer || !canAttack) return; if (!enabled) return; if (hp <= 0.f) return; if (initialHp <= 0.f) return; diff --git a/src/Character.h b/src/Character.h index e7f1323..fcff55c 100644 --- a/src/Character.h +++ b/src/Character.h @@ -160,6 +160,9 @@ public: // Called once when the death animation finishes and the character enters DEATH_IDLE. std::function onDeathAnimComplete; + // Called whenever hp changes (e.g. on damage). Arguments: current hp, max hp. + std::function onHpChanged; + private: std::map animations; diff --git a/src/Game.cpp b/src/Game.cpp index 5e1fea1..a0bf3fe 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -131,7 +131,6 @@ namespace ZL } - void Game::setupPart2() { @@ -461,6 +460,15 @@ namespace ZL 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; if (audioPlayer->init()) { diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index ad89cd1..907e744 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -110,6 +110,7 @@ namespace ZL { void MenuManager::enterGameplay() { state = GameState::Gameplay; uiManager.replaceRoot(hudRoot); + applyCurrentHealthBar(); /* uiManager.setTextButtonCallback("inventory_button", [this](const std::string&) { openInventory(); @@ -366,12 +367,14 @@ namespace ZL { if (locationName == "uni_exterior") { uiManager.replaceRoot(currentIsDarklands_ ? hudUniExtDarkRoot : hudUniExtRoot); + applyCurrentHealthBar(); setupGameplayHudCallbacks(); } else if (locationName == "uni_interior") { applyUniIntHud(); } else { // Returning to dorm: reuse step5ab, suppress already-completed hints uiManager.replaceRoot(hudStep5abRoot); + applyCurrentHealthBar(); setupStep5Callbacks(); } } @@ -402,6 +405,7 @@ namespace ZL { if (state == GameState::Gameplay && nextRoot) { uiManager.replaceRoot(nextRoot); + applyCurrentHealthBar(); uiManager.setButtonCallback("inventoryButton", [this](const std::string&) { openInventory(); @@ -450,6 +454,7 @@ namespace ZL { if (nextRoot) { uiManager.replaceRoot(nextRoot); + applyCurrentHealthBar(); // Register item-screen buttons and re-apply any already-completed hint visibility. setupStep5Callbacks(); } @@ -628,6 +633,7 @@ namespace ZL { } else if (currentLocationName_ == "uni_exterior") { if (state == GameState::Gameplay) { uiManager.replaceRoot(enabled ? hudUniExtDarkRoot : hudUniExtRoot); + applyCurrentHealthBar(); setupGameplayHudCallbacks(); } } else { @@ -654,6 +660,7 @@ namespace ZL { } } uiManager.replaceRoot(root); + applyCurrentHealthBar(); setupGameplayHudCallbacks(); } @@ -684,4 +691,20 @@ namespace ZL { 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(currentPlayerHp_)) + "/" + + std::to_string(static_cast(currentPlayerMaxHp_)); + if (hpText.size() < 7) hpText.insert(0, 7 - hpText.size(), ' '); + uiManager.setText("healthValue", hpText); + } + } // namespace ZL diff --git a/src/MenuManager.h b/src/MenuManager.h index eae37a5..10441d8 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -67,6 +67,9 @@ namespace ZL { void advanceUniIntDarklandsHud(); 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; protected: @@ -130,6 +133,10 @@ namespace ZL { std::shared_ptr texItemSelected_; std::shared_ptr texItemTransparent_; + float currentPlayerHp_ = 200.f; + float currentPlayerMaxHp_ = 200.f; + void applyCurrentHealthBar(); + int selectedQuestIndex = -1; std::vector visibleQuestIds; diff --git a/src/UiManager.cpp b/src/UiManager.cpp index 679b577..30e4ffa 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -284,9 +284,22 @@ namespace ZL { float x0 = rect.x; float y0 = rect.y; - float x1 = rect.x + rect.w; 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.TexCoordData.push_back({ 0, 0 }); @@ -294,16 +307,16 @@ namespace ZL { trackMesh.data.TexCoordData.push_back({ 0, 1 }); 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.TexCoordData.push_back({ 0, 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.TexCoordData.push_back({ 1, 0 }); + trackMesh.data.TexCoordData.push_back({ u1, 0 }); trackMesh.RefreshVBO(); } @@ -566,6 +579,8 @@ namespace ZL { std::transform(orient.begin(), orient.end(), orient.begin(), ::tolower); s->vertical = (orient != "horizontal"); } + if (j.contains("fillMode")) s->fillMode = j["fillMode"].get(); + if (j.contains("interactive")) s->interactive = j["interactive"].get(); node->slider = s; } @@ -1162,6 +1177,7 @@ namespace ZL { value = std::clamp(value, 0.0f, 1.0f); if (fabs(s->value - value) < 1e-6f) return true; s->value = value; + s->buildTrackMesh(); s->buildKnobMesh(); if (s->onValueChanged) s->onValueChanged(s->name, s->value); return true; @@ -1634,18 +1650,21 @@ namespace ZL { auto it = pressedSliders.find(fingerId); if (it != pressedSliders.end()) { auto s = it->second; - float t; - if (s->vertical) { - t = (y - s->rect.y) / s->rect.h; + if (s->interactive) { + float t; + 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) { + if (!s->interactive) continue; if (s->rect.contains((float)x, (float)y)) { pressedSliders[fingerId] = s; float t; @@ -1688,6 +1708,7 @@ namespace ZL { 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); break; diff --git a/src/UiManager.h b/src/UiManager.h index cc14b33..039eb39 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -129,6 +129,8 @@ namespace ZL { float value = 0.0f; bool vertical = true; + bool fillMode = false; // track width = value * rect.w (display-only bar) + bool interactive = true; // false = ignore touch events std::function onValueChanged;