Files
2026-11-08 07:45:01 -05:00

343 lines
14 KiB
C++

#include "Hud.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <string>
#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<StatBar>(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<Character*>* 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<Character*>* 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> 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<bool> taken(overheads.size(), false);
std::vector<int> 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<Entry> 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;
}