#include "Hud.h" #include #include #include #include #include #include "Camera.h" #include "Character.h" #include "Gui.h" #include "Window.h" static constexpr float SCREEN_MARGIN = 32.0f; // The moves list static constexpr int MAX_MOVES = 10; // one for each number key static constexpr float MOVE_WIDTH = 64.0f; static constexpr float MOVE_HEIGHT = 64.0f; static constexpr float MOVE_GAP = 8.0f; static constexpr float READY_FLASH_TIME = 0.3f; // Bars over other characters' heads static constexpr int MAX_OVERHEADS = 16; static constexpr float OVERHEAD_CLEARANCE = 0.15f; // meters above the top of the character's box static constexpr float OVERHEAD_FADE_START = 18.0f; // meters from the camera it starts fading out static constexpr float OVERHEAD_FADE_END = 26.0f; static constexpr float OVERHEAD_FADE_SPEED = 8.0f; static const glm::vec3 MOVE_BACKGROUND(0.05f, 0.06f, 0.09f); static const glm::vec3 MOVE_COOLDOWN(0.0f, 0.0f, 0.0f); static const glm::vec3 MOVE_KEY_COLOR(0.96f, 0.58f, 0.16f); static const glm::vec3 MOVE_NAME_COLOR(0.95f, 0.97f, 1.0f); static const glm::vec3 MOVE_NAME_COOLING(0.55f, 0.58f, 0.64f); // The pop up over a move static constexpr float TOOLTIP_WIDTH = 280.0f; static constexpr float TOOLTIP_PADDING = 12.0f; static constexpr float TOOLTIP_TITLE_SIZE = 0.42f; static constexpr float TOOLTIP_TEXT_SIZE = 0.32f; static const glm::vec3 TOOLTIP_TEXT_COLOR(0.82f, 0.85f, 0.9f); // width across, the images' own shape decides how tall static StatBar::Style HealthStyle(Texture* fill, Texture* frame, float width, float labelSize) { StatBar::Style style; style.fillImage = fill; style.frameImage = frame; if (fill) { // The images are red already, so the fill is only tinted darker as it runs low style.fillColor = glm::vec3(1.0f); style.lowColor = glm::vec3(0.75f, 0.6f, 0.6f); style.width = width; style.height = std::round(width * 140.0f / 720.0f); // the placeholder images are 720 by 140 } else { style.fillColor = glm::vec3(0.36f, 0.84f, 0.44f); style.lowColor = glm::vec3(0.92f, 0.26f, 0.20f); style.width = width; style.height = std::max(6.0f, std::round(width * 16.0f / 300.0f)); } style.lowFraction = 0.3f; style.showLoss = true; style.labelSize = labelSize; return style; } static int Rounded(float value) { return (int)std::ceil(value); } static std::string Upper(std::string text) { std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { return (char)std::toupper(c); }); return text; } static std::string Seconds(float seconds) { char buffer[32]; snprintf(buffer, sizeof(buffer), "%.1f s", seconds); return buffer; } Hud::Hud(Font* font, Texture* healthFill, Texture* healthFrame) : health(font, HealthStyle(healthFill, healthFrame, 300.0f, 0.4f)), font(font) { overheads.resize(MAX_OVERHEADS); for (Overhead& overhead : overheads) overhead.bar = std::make_unique(font, HealthStyle(healthFill, healthFrame, 120.0f, 0.3f)); moves.resize(MAX_MOVES); for (MoveSlot& slot : moves) { slot.background = new Box(); slot.background->Color = MOVE_BACKGROUND; slot.background->Opacity = 0.0f; slot.background->rounding = 0.12f; slot.background->BindToRenderSystem(); slot.cooldown = new Box(); slot.cooldown->Color = MOVE_COOLDOWN; slot.cooldown->Opacity = 0.0f; slot.cooldown->BindToRenderSystem(); slot.key = new TextBox(); slot.key->font = font; slot.key->fontsize = 0.42f; slot.key->Color = MOVE_KEY_COLOR; slot.key->Opacity = 0.0f; slot.key->textCenter = { 0.0f, 0.5f }; slot.key->BindToRenderSystem(); slot.name = new TextBox(); slot.name->font = font; slot.name->fontsize = 0.6f; slot.name->Color = MOVE_NAME_COLOR; slot.name->Opacity = 0.0f; slot.name->textCenter = { 0.5f, 0.5f }; slot.name->BindToRenderSystem(); } // Made last so it covers the rest tooltip = new Box(); tooltip->Color = MOVE_BACKGROUND; tooltip->Opacity = 0.0f; tooltip->rounding = 0.06f; tooltip->BindToRenderSystem(); tooltipTitle = new TextBox(); tooltipTitle->font = font; tooltipTitle->fontsize = TOOLTIP_TITLE_SIZE; tooltipTitle->Color = MOVE_KEY_COLOR; tooltipTitle->Opacity = 0.0f; tooltipTitle->BindToRenderSystem(); tooltipText = new TextBox(); tooltipText->font = font; tooltipText->fontsize = TOOLTIP_TEXT_SIZE; tooltipText->Color = TOOLTIP_TEXT_COLOR; tooltipText->Opacity = 0.0f; tooltipText->BindToRenderSystem(); } void Hud::Update(float dt, const Character* character, const std::vector* everyone, Camera* camera, const glm::vec2* cursor) { opacity = character ? std::min(opacity + dt / 0.15f, 1.0f) : 0.0f; // Someone else now, nothing was lost const bool reset = character != shown; if (character) shown = character; const Character* c = shown; float fraction = 0.0f; std::string text; if (c) { fraction = c->maxHealth > 0.0f ? c->GetHealth() / c->maxHealth : 0.0f; text = "HEALTH " + std::to_string(Rounded(c->GetHealth())) + " / " + std::to_string(Rounded(c->maxHealth)); if (c->IsStunned()) text += " - STUNNED"; for (const StatusEffect& status : c->GetStatuses()) if (!GetStatusEffectInfo(status.type).name.empty()) text += " - " + GetStatusEffectInfo(status.type).name; } health.Update(dt, opacity, fraction, text, glm::vec2(SCREEN_MARGIN, SCREEN_MARGIN), reset); UpdateMoves(dt, c, character ? cursor : nullptr); UpdateOverheads(dt, character, character ? everyone : nullptr, camera); } void Hud::UpdateOverheads(float dt, const Character* player, const std::vector* everyone, Camera* camera) { // Who gets one: everyone standing but the player, near enough and in front of the camera struct Shown { const Character* character; glm::vec2 point; // screen pixels, just over its head float opacity; }; std::vector shown; if (everyone && camera) { const glm::mat4 viewProjection = camera->GetProjectionMatrix(0.05f, 2000.0f) * camera->GetViewMatrix(); const glm::vec3 cameraPosition = camera->t.GetTranslation(); for (const Character* character : *everyone) { if (character == player || character->IsDead() || character->ragdoll || (int)shown.size() >= MAX_OVERHEADS) continue; t& body = character->body->t; const glm::vec3 anchor = body.GetTranslation() + glm::vec3(0.0f, body.GetScale().y * 0.5f + OVERHEAD_CLEARANCE, 0.0f); const glm::vec4 clip = viewProjection * glm::vec4(anchor, 1.0f); if (clip.w <= 0.05f) continue; const float fade = 1.0f - glm::smoothstep(OVERHEAD_FADE_START, OVERHEAD_FADE_END, glm::distance(cameraPosition, anchor)); if (fade <= 0.0f) continue; const glm::vec2 ndc = glm::vec2(clip) / clip.w; shown.push_back({ character, glm::vec2((ndc.x * 0.5f + 0.5f) * window->width, (0.5f - ndc.y * 0.5f) * window->height), fade }); } } // Each keeps the bar it had last frame if it can, so its loss trail carries on std::vector taken(overheads.size(), false); std::vector barFor(shown.size(), -1); for (size_t i = 0; i < shown.size(); i++) for (size_t b = 0; b < overheads.size(); b++) if (!taken[b] && overheads[b].shown == shown[i].character) { barFor[i] = (int)b; taken[b] = true; break; } for (size_t i = 0; i < shown.size(); i++) { if (barFor[i] >= 0) continue; for (size_t b = 0; b < overheads.size(); b++) if (!taken[b]) { barFor[i] = (int)b; taken[b] = true; break; } } const float ease = 1.0f - std::exp(-OVERHEAD_FADE_SPEED * dt); for (size_t b = 0; b < overheads.size(); b++) { Overhead& overhead = overheads[b]; int which = -1; for (size_t i = 0; i < shown.size(); i++) if (barFor[i] == (int)b) which = (int)i; if (which < 0) { // Nobody on it, fading out where it last was overhead.opacity = dt > 0.0f ? overhead.opacity * (1.0f - ease) : 0.0f; if (overhead.opacity < 0.01f) { overhead.opacity = 0.0f; overhead.shown = nullptr; } if (!overhead.shown) { overhead.bar->Update(dt, 0.0f, 1.0f, "", glm::vec2(-1000.0f), true); continue; } const Character* last = overhead.shown; const float lastFraction = last->maxHealth > 0.0f ? last->GetHealth() / last->maxHealth : 0.0f; overhead.bar->Update(dt, overhead.opacity, lastFraction, "", glm::vec2(-1000.0f)); continue; } const Shown& entry = shown[which]; const bool reset = overhead.shown != entry.character; if (reset) overhead.opacity = 0.0f; overhead.shown = entry.character; overhead.opacity += (entry.opacity - overhead.opacity) * ease; const Character& character = *entry.character; const float fraction = character.maxHealth > 0.0f ? character.GetHealth() / character.maxHealth : 0.0f; const StatBar& bar = *overhead.bar; const glm::vec2 topLeft(entry.point.x - bar.GetWidth() * 0.5f, entry.point.y - bar.GetHeight()); const std::string label = character.IsStunned() ? character.GetDisplayName() + " STUNNED" : character.GetDisplayName(); overhead.bar->Update(dt, overhead.opacity, fraction, label, topLeft, reset); } } void Hud::UpdateMoves(float dt, const Character* character, const glm::vec2* cursor) { // What it has: an ability on each number key, shown by its key struct Entry { std::string key; float cooldown = 0.0f; // of it left, 0 ready const Ability* ability = nullptr; }; std::vector entries; if (character) { const CharacterCombat& combat = character->body->combat; for (int i = 0; i < (int)combat.abilities.size() && (int)entries.size() < MAX_MOVES; i++) entries.push_back({ std::to_string(i + 1), combat.GetCooldownFraction(i), &combat.abilities[i] }); } const Ability* hovered = nullptr; float hoveredX = 0.0f; // Side by side along the bottom, in the middle const float rowWidth = entries.size() * MOVE_WIDTH + (entries.empty() ? 0.0f : (entries.size() - 1) * MOVE_GAP); const float left = std::round((window->width - rowWidth) * 0.5f); const float top = std::round(window->height - SCREEN_MARGIN - MOVE_HEIGHT); const float hidden = character && character->IsDead() ? 0.4f : 1.0f; for (int i = 0; i < MAX_MOVES; i++) { MoveSlot& slot = moves[i]; if (i >= (int)entries.size()) { slot.background->Opacity = slot.cooldown->Opacity = slot.key->Opacity = slot.name->Opacity = 0.0f; slot.wasReady = true; continue; } const Entry& entry = entries[i]; const float x = left + i * (MOVE_WIDTH + MOVE_GAP); const float shownOpacity = opacity * hidden; if (cursor && cursor->x >= x && cursor->x < x + MOVE_WIDTH && cursor->y >= top && cursor->y < top + MOVE_HEIGHT) { hovered = entry.ability; hoveredX = x; } // Comes off cooldown with a little flash const bool ready = entry.cooldown <= 0.0f; if (ready && !slot.wasReady) slot.flash = 1.0f; slot.wasReady = ready; slot.flash = std::max(slot.flash - dt / READY_FLASH_TIME, 0.0f); slot.background->t2d.position = { 0.0f, 0.0f, x, top }; slot.background->t2d.size = { 0.0f, 0.0f, MOVE_WIDTH, MOVE_HEIGHT }; slot.background->Color = glm::mix(MOVE_BACKGROUND, MOVE_KEY_COLOR, slot.flash * 0.6f); slot.background->Opacity = shownOpacity * 0.82f; // Covers it from the top, shrinking down as it cools off const float cover = std::round(MOVE_HEIGHT * entry.cooldown); slot.cooldown->t2d.position = { 0.0f, 0.0f, x, top }; slot.cooldown->t2d.size = { 0.0f, 0.0f, MOVE_WIDTH, cover }; slot.cooldown->Opacity = cover > 0.0f ? shownOpacity * 0.55f : 0.0f; slot.key->Opacity = 0.0f; // Just its number, in the middle slot.name->text = entry.key; slot.name->t2d.position = { 0.0f, 0.0f, x, top }; slot.name->t2d.size = { 0.0f, 0.0f, MOVE_WIDTH, MOVE_HEIGHT }; slot.name->Color = ready ? MOVE_NAME_COLOR : MOVE_NAME_COOLING; slot.name->Opacity = shownOpacity; } // What the move under the cursor does, just above it if (!hovered || !font) { tooltip->Opacity = tooltipTitle->Opacity = tooltipText->Opacity = 0.0f; return; } const float innerWidth = TOOLTIP_WIDTH - TOOLTIP_PADDING * 2.0f; std::string body = hovered->description.empty() ? "" : TextBox::Wrap(font, hovered->description, TOOLTIP_TEXT_SIZE, innerWidth) + "\n"; body += "Cooldown " + Seconds(hovered->cooldown); const int lines = 1 + (int)std::count(body.begin(), body.end(), '\n'); const float titleHeight = TextBox::LineSpacing(font, TOOLTIP_TITLE_SIZE); const float textHeight = TextBox::LineSpacing(font, TOOLTIP_TEXT_SIZE) * lines; const float height = TOOLTIP_PADDING * 2.0f + titleHeight + 6.0f + textHeight; const float boxX = std::round(glm::clamp(hoveredX + MOVE_WIDTH * 0.5f - TOOLTIP_WIDTH * 0.5f, SCREEN_MARGIN, std::max(SCREEN_MARGIN, window->width - SCREEN_MARGIN - TOOLTIP_WIDTH))); const float boxY = std::round(top - 10.0f - height); tooltip->t2d.position = { 0.0f, 0.0f, boxX, boxY }; tooltip->t2d.size = { 0.0f, 0.0f, TOOLTIP_WIDTH, height }; tooltip->Opacity = opacity * 0.92f; tooltipTitle->text = Upper(hovered->name); tooltipTitle->t2d.position = { 0.0f, 0.0f, boxX + TOOLTIP_PADDING, boxY + TOOLTIP_PADDING }; tooltipTitle->t2d.size = { 0.0f, 0.0f, innerWidth, titleHeight }; tooltipTitle->Opacity = opacity; tooltipText->text = body; tooltipText->t2d.position = { 0.0f, 0.0f, boxX + TOOLTIP_PADDING, boxY + TOOLTIP_PADDING + titleHeight + 6.0f }; tooltipText->t2d.size = { 0.0f, 0.0f, innerWidth, textHeight }; tooltipText->Opacity = opacity; }