mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 00:27:06 +02:00
270 lines
14 KiB
C++
270 lines
14 KiB
C++
#include "overlay_internal.h"
|
|
#include "Extension/UI/skate_theme.h"
|
|
#include <cmath>
|
|
#include <map>
|
|
|
|
// ReSkate's S.K.A.T.E. throwdown HUD, in place of the game's debug text. The game side reads
|
|
// what that debug HUD lays out each frame (its messages and every player's letters) and
|
|
// hands it over here, where it is drawn in skate.'s menu style: the messages on a plate at
|
|
// the top of the screen, and a scoreboard with each player's S K A T E tiles on the left,
|
|
// the tricks already set this game under it.
|
|
// Background draw list, under ReSkate's own menus and chat; it takes no input.
|
|
|
|
namespace dingosdk::overlay {
|
|
namespace {
|
|
std::atomic<SkateHudFeed> skate_hud_feed{};
|
|
}
|
|
void set_skate_hud_feed(SkateHudFeed feed) noexcept { skate_hud_feed.store(feed); }
|
|
} // namespace dingosdk::overlay
|
|
|
|
using namespace dingosdk::overlay::detail;
|
|
namespace dingosdk::overlay::detail {
|
|
namespace {
|
|
namespace theme = dingosdk::skate_theme;
|
|
using Clock = std::chrono::steady_clock;
|
|
constexpr std::string_view skate_word = "SKATE";
|
|
|
|
struct HudState {
|
|
SkateHud hud;
|
|
// When the top message last changed (it slides in), and each player's letters with
|
|
// when the last one was earned (that tile flashes), by name.
|
|
std::string headline;
|
|
Clock::time_point headline_at{};
|
|
std::map<std::string, std::pair<int, Clock::time_point>, std::less<>> letters;
|
|
};
|
|
HudState &hud_state() {
|
|
static HudState value;
|
|
return value;
|
|
}
|
|
|
|
ImU32 with_alpha(ImU32 colour, float alpha) {
|
|
const auto a = static_cast<unsigned>(((colour >> IM_COL32_A_SHIFT) & 0xff) * std::clamp(alpha, 0.0f, 1.0f));
|
|
return (colour & ~IM_COL32_A_MASK) | (a << IM_COL32_A_SHIFT);
|
|
}
|
|
float seconds_since(Clock::time_point at) { return std::chrono::duration<float>(Clock::now() - at).count(); }
|
|
float ease_out(float t) {
|
|
t = std::clamp(t, 0.0f, 1.0f);
|
|
return 1.0f - (1.0f - t) * (1.0f - t) * (1.0f - t);
|
|
}
|
|
ImU32 message_colour(SkateHudMessage::Kind kind) {
|
|
switch (kind) {
|
|
case SkateHudMessage::Kind::trick: return theme::white;
|
|
case SkateHudMessage::Kind::success: return theme::good;
|
|
case SkateHudMessage::Kind::failure: return theme::danger;
|
|
default: return theme::bar;
|
|
}
|
|
}
|
|
std::string upper(std::string text) {
|
|
for (auto &c : text)
|
|
if (c >= 'a' && c <= 'z') c = static_cast<char>(c - 'a' + 'A');
|
|
return text;
|
|
}
|
|
// `text` cut with "..." to fit `width`, on a UTF-8 character boundary.
|
|
std::string fit(ImFont *font, float size, std::string text, float width) {
|
|
if (font->CalcTextSizeA(size, FLT_MAX, 0.0f, text.c_str()).x <= width) return text;
|
|
while (!text.empty() && font->CalcTextSizeA(size, FLT_MAX, 0.0f, (text + "...").c_str()).x > width) {
|
|
while (!text.empty() && (static_cast<unsigned char>(text.back()) & 0xC0) == 0x80) text.pop_back();
|
|
if (!text.empty()) text.pop_back();
|
|
}
|
|
return text + "...";
|
|
}
|
|
void shadowed(ImDrawList *draw, ImFont *font, float size, ImVec2 at, ImU32 colour, const std::string &text) {
|
|
const float offset = std::max(1.0f, size / 16.0f);
|
|
const auto alpha = static_cast<float>((colour >> IM_COL32_A_SHIFT) & 0xff) / 255.0f;
|
|
draw->AddText(font, size, ImVec2(at.x + offset, at.y + offset), with_alpha(IM_COL32(0, 0, 0, 255), alpha * 0.7f),
|
|
text.c_str());
|
|
draw->AddText(font, size, at, colour, text.c_str());
|
|
}
|
|
|
|
// The messages, centred on a plate at the top: the first one large (it slides in when it
|
|
// changes), a trick to copy in white, the rest smaller. Sizes stay near the fonts' baked
|
|
// sizes (title 44, heading 22, bold 17) so the text stays sharp.
|
|
void draw_messages(ImDrawList *draw, const SkateHud &hud, float scale, float alpha_in, float slide) {
|
|
auto &s = state();
|
|
if (hud.messages.empty()) return;
|
|
auto *title = s.menu.title ? s.menu.title : ImGui::GetFont();
|
|
auto *heading = s.menu.heading ? s.menu.heading : ImGui::GetFont();
|
|
auto *bold = s.menu.bold ? s.menu.bold : ImGui::GetFont();
|
|
struct Line {
|
|
ImFont *font;
|
|
float size;
|
|
ImU32 colour;
|
|
std::string text;
|
|
ImVec2 extent;
|
|
};
|
|
std::vector<Line> lines;
|
|
const float max_width = ImGui::GetIO().DisplaySize.x * 0.7f;
|
|
for (std::size_t i = 0; i < hud.messages.size() && i < 6; ++i) {
|
|
const auto &message = hud.messages[i];
|
|
Line line{};
|
|
if (i == 0 && message.kind != SkateHudMessage::Kind::trick) {
|
|
line = {title, 40.0f * scale, message_colour(message.kind), upper(message.text), {}};
|
|
} else if (message.kind == SkateHudMessage::Kind::trick) {
|
|
line = {heading, 26.0f * scale, theme::white, message.text, {}};
|
|
} else {
|
|
line = {bold, 20.0f * scale, message_colour(message.kind), message.text, {}};
|
|
}
|
|
line.text = fit(line.font, line.size, line.text, max_width);
|
|
line.extent = line.font->CalcTextSizeA(line.size, FLT_MAX, 0.0f, line.text.c_str());
|
|
lines.push_back(std::move(line));
|
|
}
|
|
const float pad_x = 26.0f * scale, pad_y = 12.0f * scale, gap = 4.0f * scale;
|
|
float width{}, height{};
|
|
for (const auto &line : lines) {
|
|
width = std::max(width, line.extent.x);
|
|
height += line.extent.y;
|
|
}
|
|
height += gap * static_cast<float>(lines.size() - 1);
|
|
const float centre = ImGui::GetIO().DisplaySize.x * 0.5f;
|
|
const ImVec2 min(centre - width * 0.5f - pad_x, 34.0f * scale + slide);
|
|
const ImVec2 max(centre + width * 0.5f + pad_x, min.y + height + pad_y * 2.0f);
|
|
theme::rough_rect(draw, min, max, with_alpha(theme::tile, 0.9f * alpha_in), 71u, scale);
|
|
// The plate's stripe takes the first message's colour: orange to act, green or red after.
|
|
const auto stripe = message_colour(hud.messages.front().kind == SkateHudMessage::Kind::trick
|
|
? SkateHudMessage::Kind::prompt : hud.messages.front().kind);
|
|
draw->AddRectFilled(ImVec2(min.x, max.y - 4.0f * scale), ImVec2(max.x, max.y), with_alpha(stripe, alpha_in));
|
|
float y = min.y + pad_y;
|
|
for (const auto &line : lines) {
|
|
shadowed(draw, line.font, line.size, ImVec2(centre - line.extent.x * 0.5f, y), with_alpha(line.colour, alpha_in),
|
|
line.text);
|
|
y += line.extent.y + gap;
|
|
}
|
|
}
|
|
|
|
// Every player's row on the left: whose turn it is marked in blue, eliminated players
|
|
// dimmed, and five S K A T E tiles with the earned ones in red. Returns the panel's bottom
|
|
// and width.
|
|
std::pair<float, float> draw_scoreboard(ImDrawList *draw, HudState &h, float scale) {
|
|
auto &s = state();
|
|
const auto &players = h.hud.players;
|
|
if (players.empty()) return {120.0f * scale, 0.0f};
|
|
auto *heading = s.menu.heading ? s.menu.heading : ImGui::GetFont();
|
|
auto *bold = s.menu.bold ? s.menu.bold : ImGui::GetFont();
|
|
const float name_size = 19.0f * scale, tile = 28.0f * scale, tile_gap = 4.0f * scale;
|
|
const float row = 42.0f * scale, row_gap = 6.0f * scale, pad = 12.0f * scale;
|
|
float name_width = 90.0f * scale;
|
|
for (const auto &player : players)
|
|
name_width = std::max(name_width, bold->CalcTextSizeA(name_size, FLT_MAX, 0.0f, player.name.c_str()).x);
|
|
name_width = std::min(name_width, 240.0f * scale);
|
|
const float tiles_width = tile * 5.0f + tile_gap * 4.0f;
|
|
const float width = pad * 2.0f + 14.0f * scale + name_width + 14.0f * scale + tiles_width;
|
|
const float left = 32.0f * scale;
|
|
float top = 120.0f * scale;
|
|
shadowed(draw, heading, 18.0f * scale, ImVec2(left + 2.0f * scale, top), theme::grey_text, "S.K.A.T.E.");
|
|
top += 26.0f * scale;
|
|
for (std::size_t i = 0; i < players.size(); ++i) {
|
|
const auto &player = players[i];
|
|
const float alpha = player.out ? 0.5f : 1.0f;
|
|
const ImVec2 min(left, top + static_cast<float>(i) * (row + row_gap)), max(left + width, min.y + row);
|
|
theme::rough_rect(draw, min, max, with_alpha(player.up ? theme::tile_light : theme::tile, 0.9f * alpha),
|
|
static_cast<unsigned>(i * 13 + 5), scale);
|
|
if (player.up) {
|
|
draw->AddRectFilled(min, ImVec2(min.x + 5.0f * scale, max.y), theme::blue);
|
|
const float mid = (min.y + max.y) * 0.5f, x = min.x + pad;
|
|
draw->AddTriangleFilled(ImVec2(x, mid - 6.0f * scale), ImVec2(x + 9.0f * scale, mid),
|
|
ImVec2(x, mid + 6.0f * scale), theme::blue);
|
|
}
|
|
const auto name = fit(bold, name_size, player.name.empty() ? "Player " + std::to_string(i + 1) : player.name,
|
|
name_width);
|
|
const float name_x = min.x + pad + 14.0f * scale;
|
|
const float text_y = (min.y + max.y) * 0.5f - bold->CalcTextSizeA(name_size, FLT_MAX, 0.0f, "Ag").y * 0.5f;
|
|
shadowed(draw, bold, name_size, ImVec2(name_x, text_y),
|
|
with_alpha(player.out ? theme::grey_text : theme::white, alpha), name);
|
|
if (player.out) {
|
|
const float strike = (min.y + max.y) * 0.5f;
|
|
const float end = name_x + bold->CalcTextSizeA(name_size, FLT_MAX, 0.0f, name.c_str()).x;
|
|
draw->AddLine(ImVec2(name_x - 2.0f * scale, strike), ImVec2(end + 2.0f * scale, strike),
|
|
with_alpha(theme::danger, 0.9f), std::max(1.0f, 2.0f * scale));
|
|
}
|
|
// A letter earned since the last frame flashes and pops in.
|
|
const auto key = player.name.empty() ? "#" + std::to_string(i) : player.name;
|
|
auto &seen = h.letters[key];
|
|
if (player.letters > seen.first) seen.second = Clock::now();
|
|
seen.first = player.letters;
|
|
const float flash = 1.0f - ease_out(seconds_since(seen.second) / 0.7f);
|
|
float x = name_x + name_width + 14.0f * scale;
|
|
const float tile_top = (min.y + max.y) * 0.5f - tile * 0.5f;
|
|
for (int letter = 0; letter < 5; ++letter, x += tile + tile_gap) {
|
|
const bool earned = letter < player.letters;
|
|
const bool newest = earned && letter == player.letters - 1 && flash > 0.0f;
|
|
const float grow = newest ? tile * 0.18f * flash : 0.0f;
|
|
const ImVec2 a(x - grow, tile_top - grow), b(x + tile + grow, tile_top + tile + grow);
|
|
draw->AddRectFilled(a, b, with_alpha(earned ? theme::danger : theme::tile_grey, alpha), 3.0f * scale);
|
|
if (newest)
|
|
draw->AddRectFilled(a, b, with_alpha(theme::white, 0.6f * flash), 3.0f * scale);
|
|
const char text[2]{skate_word[static_cast<std::size_t>(letter)], 0};
|
|
const float letter_size = 19.0f * scale;
|
|
const auto extent = heading->CalcTextSizeA(letter_size, FLT_MAX, 0.0f, text);
|
|
draw->AddText(heading, letter_size,
|
|
ImVec2((a.x + b.x - extent.x) * 0.5f, (a.y + b.y - extent.y) * 0.5f),
|
|
with_alpha(earned ? theme::white : theme::grey_text, earned ? alpha : 0.45f * alpha), text);
|
|
}
|
|
}
|
|
return {top + static_cast<float>(players.size()) * (row + row_gap), width};
|
|
}
|
|
|
|
// The tricks already set this game, under the scoreboard: none of them can be set again.
|
|
void draw_done_tricks(ImDrawList *draw, const SkateHud &hud, float scale, float top, float min_width) {
|
|
auto &s = state();
|
|
if (hud.done_tricks.empty()) return;
|
|
auto *heading = s.menu.heading ? s.menu.heading : ImGui::GetFont();
|
|
auto *body = s.menu.body ? s.menu.body : ImGui::GetFont();
|
|
constexpr std::size_t shown = 8;
|
|
const float left = 32.0f * scale, pad = 10.0f * scale, size = 16.0f * scale, line = size + 5.0f * scale;
|
|
top += 10.0f * scale;
|
|
shadowed(draw, heading, 16.0f * scale, ImVec2(left + 2.0f * scale, top), theme::grey_text,
|
|
"TRICKS DONE (" + std::to_string(hud.done_tricks.size()) + ")");
|
|
top += 22.0f * scale;
|
|
// The newest at the top; older ones beyond the first few are counted.
|
|
const auto count = std::min(hud.done_tricks.size(), shown);
|
|
const bool more = hud.done_tricks.size() > shown;
|
|
const float width = std::max(min_width, 260.0f * scale);
|
|
const float height = pad * 2.0f + line * static_cast<float>(count + (more ? 1 : 0)) - 5.0f * scale;
|
|
theme::rough_rect(draw, ImVec2(left, top), ImVec2(left + width, top + height), with_alpha(theme::tile, 0.85f), 29u,
|
|
scale);
|
|
float y = top + pad;
|
|
for (std::size_t i = 0; i < count; ++i, y += line) {
|
|
const auto &trick = hud.done_tricks[hud.done_tricks.size() - 1 - i];
|
|
shadowed(draw, body, size, ImVec2(left + pad, y), i == 0 ? theme::white : with_alpha(theme::white, 0.75f),
|
|
fit(body, size, trick, width - pad * 2.0f));
|
|
}
|
|
if (more)
|
|
shadowed(draw, body, size, ImVec2(left + pad, y), theme::grey_text,
|
|
"+" + std::to_string(hud.done_tricks.size() - shown) + " earlier");
|
|
}
|
|
} // namespace
|
|
|
|
bool skate_hud_pending() {
|
|
auto &h = hud_state();
|
|
h.hud = {};
|
|
if (const auto feed = skate_hud_feed.load()) {
|
|
try { h.hud = feed(); } catch (...) { h.hud = {}; }
|
|
}
|
|
if (h.hud.messages.empty() && h.hud.players.empty()) {
|
|
// The throwdown is over: the next one starts fresh.
|
|
h.headline.clear();
|
|
h.letters.clear();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void draw_skate_hud() {
|
|
auto &h = hud_state();
|
|
if (h.hud.messages.empty() && h.hud.players.empty()) return;
|
|
const auto display = ImGui::GetIO().DisplaySize;
|
|
if (display.x <= 0 || display.y <= 0) return;
|
|
const float scale = std::clamp(display.y / 1080.0f, 0.8f, 2.0f);
|
|
auto *draw = ImGui::GetBackgroundDrawList();
|
|
const auto headline = h.hud.messages.empty() ? std::string{} : h.hud.messages.front().text;
|
|
if (headline != h.headline) {
|
|
h.headline = headline;
|
|
h.headline_at = Clock::now();
|
|
}
|
|
const float shown = ease_out(seconds_since(h.headline_at) / 0.2f);
|
|
draw_messages(draw, h.hud, scale, shown, (1.0f - shown) * -12.0f * scale);
|
|
const auto [bottom, width] = draw_scoreboard(draw, h, scale);
|
|
draw_done_tricks(draw, h.hud, scale, bottom, width);
|
|
}
|
|
} // namespace dingosdk::overlay::detail
|