readme
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user