343 lines
14 KiB
C++
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;
|
|
}
|