|
| 1 | +#pragma once |
| 2 | + |
| 3 | +#include <algorithm> |
| 4 | +#include <application.hpp> |
| 5 | +#include <cmath> |
| 6 | +#include <deserialize-utils.hpp> |
| 7 | +#include <ecs/world.hpp> |
| 8 | +#include <glm/common.hpp> |
| 9 | +#include <glm/geometric.hpp> |
| 10 | +#include <glm/vec2.hpp> |
| 11 | +#include <glm/vec3.hpp> |
| 12 | +#include <glm/vec4.hpp> |
| 13 | +#include <json/json.hpp> |
| 14 | +#include <systems/ui-renderer.hpp> |
| 15 | + |
| 16 | +#include "../components/collider.hpp" |
| 17 | +#include "../components/enemy.hpp" |
| 18 | +#include "../components/health.hpp" |
| 19 | + |
| 20 | +namespace gameplay { |
| 21 | + |
| 22 | + class EnemyHealthBarSystem { |
| 23 | + struct Config { |
| 24 | + bool enabled = true; |
| 25 | + float maxDistance = 24.0f; |
| 26 | + glm::vec4 lowColor = {0.84f, 0.18f, 0.24f, 0.96f}; |
| 27 | + glm::vec4 midColor = {0.96f, 0.76f, 0.25f, 0.96f}; |
| 28 | + glm::vec4 highColor = {0.28f, 0.84f, 0.45f, 0.96f}; |
| 29 | + glm::vec4 borderColor = {0.02f, 0.03f, 0.05f, 0.92f}; |
| 30 | + glm::vec4 backgroundColor = {0.12f, 0.14f, 0.18f, 0.88f}; |
| 31 | + glm::vec4 highlightColor = {1.0f, 1.0f, 1.0f, 0.12f}; |
| 32 | + } config; |
| 33 | + |
| 34 | + static void deserializeColor(const nlohmann::json& colorConfig, const char* key, glm::vec4& color) { |
| 35 | + if (!(colorConfig.contains(key) && colorConfig[key].is_array())) return; |
| 36 | + |
| 37 | + const auto& value = colorConfig[key]; |
| 38 | + if (value.size() == 3) { |
| 39 | + color = glm::vec4(value.get<glm::vec3>(), color.a); |
| 40 | + } else if (value.size() == 4) { |
| 41 | + color = value.get<glm::vec4>(); |
| 42 | + } |
| 43 | + } |
| 44 | + |
| 45 | + static float estimateBarHeight(const our::Entity* entity, const ColliderComponent* collider) { |
| 46 | + float scaleHeight = std::max(std::abs(entity->localTransform.scale.y) * 1.2f, 1.0f); |
| 47 | + if (!collider) return scaleHeight; |
| 48 | + |
| 49 | + float colliderHeight = collider->radius * 2.0f; |
| 50 | + if (collider->shape == ColliderShape::Capsule) colliderHeight = std::max(colliderHeight, collider->height); |
| 51 | + |
| 52 | + return std::max(colliderHeight, scaleHeight); |
| 53 | + } |
| 54 | + |
| 55 | + public: |
| 56 | + void deserialize(const nlohmann::json& sceneConfig) { |
| 57 | + if (!(sceneConfig.contains("game") && sceneConfig["game"].contains("enemyHealthBars"))) return; |
| 58 | + |
| 59 | + const auto& healthBarConfig = sceneConfig["game"]["enemyHealthBars"]; |
| 60 | + if (!healthBarConfig.is_object()) return; |
| 61 | + |
| 62 | + config = {}; |
| 63 | + config.enabled = healthBarConfig.value("enabled", config.enabled); |
| 64 | + config.maxDistance = healthBarConfig.value("maxDistance", config.maxDistance); |
| 65 | + config.maxDistance = std::max(0.0f, config.maxDistance); |
| 66 | + |
| 67 | + if (healthBarConfig.contains("colors") && healthBarConfig["colors"].is_object()) { |
| 68 | + const auto& colors = healthBarConfig["colors"]; |
| 69 | + deserializeColor(colors, "low", config.lowColor); |
| 70 | + deserializeColor(colors, "mid", config.midColor); |
| 71 | + deserializeColor(colors, "high", config.highColor); |
| 72 | + deserializeColor(colors, "border", config.borderColor); |
| 73 | + deserializeColor(colors, "background", config.backgroundColor); |
| 74 | + deserializeColor(colors, "highlight", config.highlightColor); |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + void render(our::World* world, our::Application* app, const our::UIRenderer& ui, |
| 79 | + const our::CameraComponent* camera) const { |
| 80 | + if (!(config.enabled && world && app && camera)) return; |
| 81 | + |
| 82 | + glm::ivec2 framebufferSize = app->getFrameBufferSize(); |
| 83 | + if (framebufferSize.x <= 0 || framebufferSize.y <= 0) return; |
| 84 | + |
| 85 | + glm::mat4 view = camera->getViewMatrix(); |
| 86 | + glm::mat4 proj = camera->getProjectionMatrix(framebufferSize); |
| 87 | + glm::mat4 viewProj = proj * view; |
| 88 | + glm::mat4 overlayProj = ui.overlayProjection(framebufferSize); |
| 89 | + glm::vec3 cameraPos = glm::vec3(camera->getOwner()->getLocalToWorldMatrix() * glm::vec4(0, 0, 0, 1)); |
| 90 | + |
| 91 | + for (our::Entity* entity : world->getEntities()) { |
| 92 | + auto* health = entity->getComponent<HealthComponent>(); |
| 93 | + if (!(entity->getComponent<EnemyComponent>() && health) || health->isDead || health->maxHealth <= 0.0f) |
| 94 | + continue; |
| 95 | + |
| 96 | + float healthRatio = health->getHealthRatio(); |
| 97 | + glm::mat4 localToWorld = entity->getLocalToWorldMatrix(); |
| 98 | + glm::vec3 enemyPosition = glm::vec3(localToWorld * glm::vec4(0, 0, 0, 1)); |
| 99 | + float distance = glm::distance(enemyPosition, cameraPos); |
| 100 | + |
| 101 | + bool visibleWhenDamaged = health->damageRevealTimer > 0.0f; |
| 102 | + bool visibleWhenClose = distance <= config.maxDistance; |
| 103 | + |
| 104 | + if (!(visibleWhenDamaged || visibleWhenClose)) continue; |
| 105 | + |
| 106 | + ColliderComponent* collider = entity->getComponent<ColliderComponent>(); |
| 107 | + float barHeightOffset = estimateBarHeight(entity, collider) + 0.35f; |
| 108 | + |
| 109 | + glm::vec3 barWorldPos = glm::vec3(localToWorld * glm::vec4(0.0f, barHeightOffset, 0.0f, 1.0f)); |
| 110 | + our::ScreenPoint screen = our::UIRenderer::worldToScreen(barWorldPos, viewProj, framebufferSize); |
| 111 | + if (!screen.visible) continue; |
| 112 | + |
| 113 | + float dist = glm::clamp((distance - 6.0f) / glm::max(1.0f, config.maxDistance - 6.0f), 0.0f, 1.0f); |
| 114 | + float alpha = visibleWhenDamaged |
| 115 | + ? 1.0f |
| 116 | + : 1.0f - glm::smoothstep(config.maxDistance * 0.7f, config.maxDistance, distance); |
| 117 | + if (alpha <= 0.0f) continue; |
| 118 | + |
| 119 | + float barWidth = glm::mix(86.0f, 48.0f, dist); |
| 120 | + float barHeight = glm::mix(10.0f, 6.0f, dist); |
| 121 | + |
| 122 | + glm::vec2 barPosition = {screen.position.x - barWidth * 0.5f, screen.position.y - barHeight}; |
| 123 | + glm::vec2 outerSize = {barWidth + 2.0f, barHeight + 2.0f}; |
| 124 | + glm::vec2 innerPosition = barPosition + glm::vec2(2.0f, 2.0f); |
| 125 | + glm::vec2 innerSize = {barWidth - 4.0f, barHeight - 4.0f}; |
| 126 | + glm::vec2 fillSize = {innerSize.x * healthRatio, innerSize.y}; |
| 127 | + |
| 128 | + glm::vec4 borderColor = config.borderColor; |
| 129 | + borderColor.a *= alpha; |
| 130 | + ui.drawRect(overlayProj, barPosition - glm::vec2(1.0f), outerSize, borderColor); |
| 131 | + |
| 132 | + glm::vec4 backgroundColor = config.backgroundColor; |
| 133 | + backgroundColor.a *= alpha; |
| 134 | + ui.drawRect(overlayProj, barPosition, {barWidth, barHeight}, backgroundColor); |
| 135 | + |
| 136 | + float clampedHealthRatio = glm::clamp(healthRatio, 0.0f, 1.0f); |
| 137 | + glm::vec4 fillColor = |
| 138 | + clampedHealthRatio < 0.5f |
| 139 | + ? glm::mix(config.lowColor, config.midColor, clampedHealthRatio * 2.0f) |
| 140 | + : glm::mix(config.midColor, config.highColor, (clampedHealthRatio - 0.5f) * 2.0f); |
| 141 | + fillColor.a *= alpha; |
| 142 | + ui.drawRect(overlayProj, innerPosition, fillSize, fillColor); |
| 143 | + |
| 144 | + if (fillSize.x > 6.0f) { |
| 145 | + glm::vec4 highlightColor = config.highlightColor; |
| 146 | + highlightColor.a *= alpha; |
| 147 | + ui.drawRect(overlayProj, innerPosition + glm::vec2(0.0f, 1.0f), |
| 148 | + {fillSize.x, std::max(1.0f, innerSize.y * 0.28f)}, highlightColor); |
| 149 | + } |
| 150 | + } |
| 151 | + } |
| 152 | + }; |
| 153 | + |
| 154 | +} // namespace gameplay |
0 commit comments