* Adding clipper to generate the terrain's negative powder-contour offset. * Fixing RDB issues (fracture, active trigger, performances) * Fixing blood emission * Fixing the freezing formula on liquids * Fixing Steve spawning due to a missed test. * Reaction table properly build
674 lines
48 KiB
C++
674 lines
48 KiB
C++
#pragma once
|
|
// Heads-up display drawn on the CPU into an ARGB overlay (1280x720, output pixels) that render.comp blends over the frame.
|
|
// Text uses an embedded 5x7 bitmap font; icons and bar colours are the game's own HUD images from data.wak.
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "core/png.h"
|
|
#include "core/wak.h"
|
|
#include "world/wang_gen.h" // Image, decode_png
|
|
|
|
namespace hud {
|
|
|
|
// 5x7 font, ASCII 32..126, one byte per column (bit 0 = top row).
|
|
inline const uint8_t kFont[95][5] = {
|
|
{0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x5F,0x00,0x00},{0x00,0x07,0x00,0x07,0x00},{0x14,0x7F,0x14,0x7F,0x14},
|
|
{0x24,0x2A,0x7F,0x2A,0x12},{0x23,0x13,0x08,0x64,0x62},{0x36,0x49,0x56,0x20,0x50},{0x00,0x08,0x07,0x03,0x00},
|
|
{0x00,0x1C,0x22,0x41,0x00},{0x00,0x41,0x22,0x1C,0x00},{0x2A,0x1C,0x7F,0x1C,0x2A},{0x08,0x08,0x3E,0x08,0x08},
|
|
{0x00,0x80,0x70,0x30,0x00},{0x08,0x08,0x08,0x08,0x08},{0x00,0x00,0x60,0x60,0x00},{0x20,0x10,0x08,0x04,0x02},
|
|
{0x3E,0x51,0x49,0x45,0x3E},{0x00,0x42,0x7F,0x40,0x00},{0x72,0x49,0x49,0x49,0x46},{0x21,0x41,0x49,0x4D,0x33},
|
|
{0x18,0x14,0x12,0x7F,0x10},{0x27,0x45,0x45,0x45,0x39},{0x3C,0x4A,0x49,0x49,0x31},{0x41,0x21,0x11,0x09,0x07},
|
|
{0x36,0x49,0x49,0x49,0x36},{0x46,0x49,0x49,0x29,0x1E},{0x00,0x00,0x14,0x00,0x00},{0x00,0x40,0x34,0x00,0x00},
|
|
{0x00,0x08,0x14,0x22,0x41},{0x14,0x14,0x14,0x14,0x14},{0x00,0x41,0x22,0x14,0x08},{0x02,0x01,0x59,0x09,0x06},
|
|
{0x3E,0x41,0x5D,0x59,0x4E},{0x7C,0x12,0x11,0x12,0x7C},{0x7F,0x49,0x49,0x49,0x36},{0x3E,0x41,0x41,0x41,0x22},
|
|
{0x7F,0x41,0x41,0x41,0x3E},{0x7F,0x49,0x49,0x49,0x41},{0x7F,0x09,0x09,0x09,0x01},{0x3E,0x41,0x41,0x51,0x73},
|
|
{0x7F,0x08,0x08,0x08,0x7F},{0x00,0x41,0x7F,0x41,0x00},{0x20,0x40,0x41,0x3F,0x01},{0x7F,0x08,0x14,0x22,0x41},
|
|
{0x7F,0x40,0x40,0x40,0x40},{0x7F,0x02,0x1C,0x02,0x7F},{0x7F,0x04,0x08,0x10,0x7F},{0x3E,0x41,0x41,0x41,0x3E},
|
|
{0x7F,0x09,0x09,0x09,0x06},{0x3E,0x41,0x51,0x21,0x5E},{0x7F,0x09,0x19,0x29,0x46},{0x26,0x49,0x49,0x49,0x32},
|
|
{0x03,0x01,0x7F,0x01,0x03},{0x3F,0x40,0x40,0x40,0x3F},{0x1F,0x20,0x40,0x20,0x1F},{0x3F,0x40,0x38,0x40,0x3F},
|
|
{0x63,0x14,0x08,0x14,0x63},{0x03,0x04,0x78,0x04,0x03},{0x61,0x59,0x49,0x4D,0x43},{0x00,0x7F,0x41,0x41,0x41},
|
|
{0x02,0x04,0x08,0x10,0x20},{0x41,0x41,0x41,0x7F,0x00},{0x04,0x02,0x01,0x02,0x04},{0x40,0x40,0x40,0x40,0x40},
|
|
{0x00,0x03,0x07,0x08,0x00},{0x20,0x54,0x54,0x78,0x40},{0x7F,0x28,0x44,0x44,0x38},{0x38,0x44,0x44,0x44,0x28},
|
|
{0x38,0x44,0x44,0x28,0x7F},{0x38,0x54,0x54,0x54,0x18},{0x00,0x08,0x7E,0x09,0x02},{0x18,0xA4,0xA4,0x9C,0x78},
|
|
{0x7F,0x08,0x04,0x04,0x78},{0x00,0x44,0x7D,0x40,0x00},{0x20,0x40,0x40,0x3D,0x00},{0x7F,0x10,0x28,0x44,0x00},
|
|
{0x00,0x41,0x7F,0x40,0x00},{0x7C,0x04,0x78,0x04,0x78},{0x7C,0x08,0x04,0x04,0x78},{0x38,0x44,0x44,0x44,0x38},
|
|
{0xFC,0x18,0x24,0x24,0x18},{0x18,0x24,0x24,0x18,0xFC},{0x7C,0x08,0x04,0x04,0x08},{0x48,0x54,0x54,0x54,0x24},
|
|
{0x04,0x04,0x3F,0x44,0x24},{0x3C,0x40,0x40,0x20,0x7C},{0x1C,0x20,0x40,0x20,0x1C},{0x3C,0x40,0x30,0x40,0x3C},
|
|
{0x44,0x28,0x10,0x28,0x44},{0x4C,0x90,0x90,0x90,0x7C},{0x44,0x64,0x54,0x4C,0x44},{0x00,0x08,0x36,0x41,0x00},
|
|
{0x00,0x00,0x77,0x00,0x00},{0x00,0x41,0x36,0x08,0x00},{0x02,0x01,0x02,0x04,0x02},
|
|
};
|
|
|
|
struct Icon {
|
|
int w = 0, h = 0;
|
|
std::vector<uint32_t> px; // ARGB
|
|
bool ok() const { return w > 0; }
|
|
};
|
|
|
|
inline Icon load_icon(const ng::Wak& wak, const std::string& path) {
|
|
Icon ic;
|
|
std::vector<uint8_t> raw;
|
|
ng::Image im;
|
|
if (!wak.read(path, raw) || !ng::decode_png(raw.data(), raw.size(), im)) return ic;
|
|
ic.w = im.width; ic.h = im.height;
|
|
ic.px.resize((size_t)ic.w * ic.h);
|
|
for (size_t i = 0; i < ic.px.size(); ++i) {
|
|
const uint8_t* p = &im.rgba[i * 4];
|
|
ic.px[i] = (uint32_t)p[3] << 24 | (uint32_t)p[0] << 16 | (uint32_t)p[1] << 8 | p[2];
|
|
}
|
|
return ic;
|
|
}
|
|
|
|
// The game's bitmap fonts (data/fonts/*.xml: FontData with a texture and a QuadChar per character): the UI and the text sprites of the world (shop prices) use font_pixel_white.
|
|
struct GameFont {
|
|
struct Glyph { int rx = 0, ry = 0, rw = 0, rh = 0, ox = 0, oy = 0, width = 0; bool ok = false; };
|
|
Icon tex;
|
|
Glyph g[256];
|
|
int line_height = 7, char_space = 0, word_space = 6;
|
|
bool ok() const { return tex.ok(); }
|
|
};
|
|
inline GameFont load_font(const ng::Wak& wak, const std::string& xml_path) {
|
|
GameFont f;
|
|
std::vector<uint8_t> raw;
|
|
if (!wak.read(xml_path, raw)) return f;
|
|
const std::string s(raw.begin(), raw.end());
|
|
auto tagText = [&](const char* tag) { const std::string o = std::string("<") + tag + ">", c = std::string("</") + tag + ">"; const size_t a = s.find(o), b = s.find(c); if (a == std::string::npos || b == std::string::npos) return std::string(); std::string t = s.substr(a + o.size(), b - a - o.size()); const size_t i0 = t.find_first_not_of(" \t\r\n"), i1 = t.find_last_not_of(" \t\r\n"); return i0 == std::string::npos ? std::string() : t.substr(i0, i1 - i0 + 1); };
|
|
const std::string texPath = tagText("Texture");
|
|
if (!tagText("LineHeight").empty()) f.line_height = std::atoi(tagText("LineHeight").c_str());
|
|
if (!tagText("CharSpace").empty()) f.char_space = std::atoi(tagText("CharSpace").c_str());
|
|
if (!tagText("WordSpace").empty()) f.word_space = std::atoi(tagText("WordSpace").c_str());
|
|
for (size_t pos = s.find("<QuadChar"); pos != std::string::npos; pos = s.find("<QuadChar", pos + 1)) {
|
|
const size_t end = s.find('>', pos);
|
|
const std::string q = s.substr(pos, end - pos);
|
|
auto at = [&](const char* k) { const std::string key = std::string(" ") + k + "=\""; const size_t a = q.find(key); return a == std::string::npos ? 0 : std::atoi(q.c_str() + a + key.size()); };
|
|
const int id = at("id");
|
|
if (id < 0 || id > 255) continue;
|
|
GameFont::Glyph& gl = f.g[id];
|
|
gl.rx = at("rect_x"); gl.ry = at("rect_y"); gl.rw = at("rect_w"); gl.rh = at("rect_h"); gl.ox = at("offset_x"); gl.oy = at("offset_y"); gl.width = at("width"); gl.ok = true;
|
|
}
|
|
if (!texPath.empty()) f.tex = load_icon(wak, texPath);
|
|
return f;
|
|
}
|
|
|
|
constexpr uint32_t rgba(int r, int g, int b, int a = 255) { return (uint32_t)a << 24 | (uint32_t)r << 16 | (uint32_t)g << 8 | (uint32_t)b; }
|
|
|
|
// Draws into a caller-owned ARGB buffer (premultiplication is not used: the shader does a plain alpha mix).
|
|
struct Canvas {
|
|
uint32_t* px = nullptr;
|
|
int w = 0, h = 0;
|
|
const GameFont* font = nullptr; // the game's font when it is loaded (else the embedded 5x7)
|
|
|
|
// The 32 x 32 tiles drawn into since the last clear: clear() zeroes only those, main.cpp uploads only those and render.comp reads the overlay only
|
|
// there (the HUD covers a few percent of the window; the whole 14 MB buffer was cleared, copied to mapped memory and read over PCIe every frame).
|
|
std::vector<uint8_t> tiles;
|
|
int tw = 0, th = 0;
|
|
void mark(int x, int y) { tiles[(size_t)(y >> 5) * tw + (x >> 5)] = 1; }
|
|
void mark_all() { std::fill(tiles.begin(), tiles.end(), (uint8_t)1); } // (a drawer that writes px directly over the whole window: the replay editor)
|
|
void clear() {
|
|
const int ntw = (w + 31) >> 5, nth = (h + 31) >> 5;
|
|
if (ntw != tw || nth != th) { tw = ntw; th = nth; tiles.assign((size_t)tw * th, 0); std::memset(px, 0, (size_t)w * h * 4); return; } // (a new size: a new row stride)
|
|
for (int ty = 0; ty < th; ++ty) for (int tx = 0; tx < tw; ++tx) {
|
|
uint8_t& m = tiles[(size_t)ty * tw + tx];
|
|
if (!m) continue;
|
|
m = 0;
|
|
const int x0 = tx * 32, n = std::min(32, w - x0);
|
|
for (int y = ty * 32; y < std::min(h, ty * 32 + 32); ++y) std::memset(px + (size_t)y * w + x0, 0, (size_t)n * 4);
|
|
}
|
|
}
|
|
|
|
void put(int x, int y, uint32_t c) {
|
|
if (x < 0 || y < 0 || x >= w || y >= h) return;
|
|
const uint32_t a = c >> 24;
|
|
if (a == 0) return;
|
|
mark(x, y);
|
|
uint32_t& d = px[(size_t)y * w + x];
|
|
if (a == 255 || (d >> 24) == 0) { d = c; return; }
|
|
// blend over an existing overlay pixel (keep the larger alpha)
|
|
const uint32_t da = d >> 24;
|
|
const uint32_t oa = a + da * (255 - a) / 255;
|
|
auto mix = [&](int sh) { return ((c >> sh & 255) * a + (d >> sh & 255) * da * (255 - a) / 255) / std::max<uint32_t>(oa, 1); };
|
|
d = oa << 24 | mix(16) << 16 | mix(8) << 8 | mix(0);
|
|
}
|
|
void rect(int x, int y, int rw, int rh, uint32_t c) {
|
|
for (int j = 0; j < rh; ++j) for (int i = 0; i < rw; ++i) put(x + i, y + j, c);
|
|
}
|
|
void frame(int x, int y, int rw, int rh, uint32_t c) {
|
|
rect(x, y, rw, 1, c); rect(x, y + rh - 1, rw, 1, c); rect(x, y, 1, rh, c); rect(x + rw - 1, y, 1, rh, c);
|
|
}
|
|
void line(int x0, int y0, int x1, int y1, uint32_t c) {
|
|
// Clip first: terrain fixtures may be far outside the camera.
|
|
double a = 0, b = 1;
|
|
const double dx = double(x1) - x0, dy = double(y1) - y0;
|
|
auto clip = [&](double p, double q) { if (p == 0) return q >= 0; const double t = q / p; if (p < 0) a = std::max(a, t); else b = std::min(b, t); return a <= b; };
|
|
if (!clip(-dx, x0) || !clip(dx, w - 1 - x0) || !clip(-dy, y0) || !clip(dy, h - 1 - y0)) return;
|
|
const int ax = (int)std::lround(x0 + a * dx), ay = (int)std::lround(y0 + a * dy);
|
|
const int bx = (int)std::lround(x0 + b * dx), by = (int)std::lround(y0 + b * dy);
|
|
// Integer stepping avoids two floating divisions/rounds per debug-line pixel.
|
|
int x = ax, y = ay;
|
|
const int ix = std::abs(bx - ax), iy = -std::abs(by - ay), sx = ax < bx ? 1 : -1, sy = ay < by ? 1 : -1;
|
|
int error = ix + iy;
|
|
for (;;) {
|
|
put(x, y, c);
|
|
if (x == bx && y == by) break;
|
|
const int twice = 2 * error;
|
|
if (twice >= iy) { error += iy; x += sx; }
|
|
if (twice <= ix) { error += ix; y += sy; }
|
|
}
|
|
}
|
|
int text_width(const std::string& s, int scale) const {
|
|
if (font && font->ok()) { int w = 0; for (unsigned char ch : s) { const GameFont::Glyph& gl = font->g[ch]; w += (gl.ok ? gl.width : font->word_space) + font->char_space; } return w * scale; }
|
|
return (int)s.size() * 6 * scale - scale;
|
|
}
|
|
void text(int x, int y, const std::string& s, uint32_t c, int scale = 2, bool shadow = true) {
|
|
if (font && font->ok()) { // the game's font: its texture holds the glyph (white) and its shadow (black): the white pixels take the colour, the black ones stay black
|
|
const Icon& t = font->tex;
|
|
const uint32_t shade = rgba(0, 0, 0, (int)std::min<uint32_t>(220u, c >> 24)); // (the shadow fades with the text; without `shadow` it is left out)
|
|
for (int pass = 1; pass < 2; ++pass) {
|
|
const int off = 0;
|
|
int cx = x + off;
|
|
for (unsigned char ch : s) {
|
|
const GameFont::Glyph& gl = font->g[ch];
|
|
if (gl.ok) for (int j = 0; j < gl.rh; ++j) for (int i = 0; i < gl.rw; ++i) {
|
|
const int sx = gl.rx + i, sy = gl.ry + j;
|
|
if (sx < 0 || sy < 0 || sx >= t.w || sy >= t.h) continue;
|
|
const uint32_t tp = t.px[(size_t)sy * t.w + sx];
|
|
if ((tp >> 24) < 128) continue;
|
|
const bool dark = (tp & 0xFFFFFFu) < 0x404040u;
|
|
if (dark && !shadow) continue;
|
|
rect(cx + (gl.ox + i) * scale, y + off + (gl.oy + j) * scale, scale, scale, dark ? shade : c);
|
|
}
|
|
cx += ((gl.ok ? gl.width : font->word_space) + font->char_space) * scale;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
for (int pass = shadow ? 0 : 1; pass < 2; ++pass) {
|
|
const uint32_t col = pass == 0 ? rgba(0, 0, 0, (int)std::min<uint32_t>(200u, c >> 24)) : c; // (the shadow fades with the text)
|
|
const int off = pass == 0 ? scale : 0;
|
|
int cx = x + off;
|
|
for (char ch : s) {
|
|
if (ch >= 32 && ch < 127) {
|
|
const uint8_t* g = kFont[ch - 32];
|
|
for (int col_i = 0; col_i < 5; ++col_i)
|
|
for (int row = 0; row < 8; ++row)
|
|
if (g[col_i] >> row & 1) rect(cx + col_i * scale, y + off + row * scale, scale, scale, col);
|
|
}
|
|
cx += 6 * scale;
|
|
}
|
|
}
|
|
}
|
|
void icon(const Icon& ic, int x, int y, int scale) {
|
|
for (int j = 0; j < ic.h; ++j)
|
|
for (int i = 0; i < ic.w; ++i) {
|
|
const uint32_t c = ic.px[(size_t)j * ic.w + i];
|
|
if (c >> 24) rect(x + i * scale, y + j * scale, scale, scale, c);
|
|
}
|
|
}
|
|
// Draws an icon stretched to w x h output pixels (nearest texel: the slots of the information boxes, 0.6 of the UI scale)
|
|
void icon_fit(const Icon& ic, int x, int y, int w, int h) {
|
|
if (ic.w <= 0 || ic.h <= 0 || w <= 0 || h <= 0) return;
|
|
for (int j = 0; j < ic.h; ++j) {
|
|
const int y0 = y + j * h / ic.h, y1 = y + (j + 1) * h / ic.h;
|
|
for (int i = 0; i < ic.w; ++i) {
|
|
const uint32_t c = ic.px[(size_t)j * ic.w + i];
|
|
if (!(c >> 24)) continue;
|
|
const int x0 = x + i * w / ic.w, x1 = x + (i + 1) * w / ic.w;
|
|
if (x1 > x0 && y1 > y0) rect(x0, y0, x1 - x0, y1 - y0, c);
|
|
}
|
|
}
|
|
}
|
|
// Draws texels (ARGB, w x h) centred on (cx, cy).
|
|
void blit_centered(const uint32_t* tex, int tw, int th, int cx, int cy, int scale) {
|
|
for (int j = 0; j < th; ++j)
|
|
for (int i = 0; i < tw; ++i) {
|
|
const uint32_t c = tex[(size_t)j * tw + i];
|
|
if (c >> 24) rect(cx + (i - tw / 2) * scale, cy + (j - th / 2) * scale, scale, scale, c);
|
|
}
|
|
}
|
|
// Draws texels rotated by `angle` (radians) around the pivot pixel (px, py) of the image, placed at (x, y); mirror flips the image vertically.
|
|
void blit_rotated(const uint32_t* tex, int tw, int th, int px, int py, double x, double y, double angle, int scale, bool mirror) {
|
|
const double ca = std::cos(angle), sa = std::sin(angle);
|
|
const int rad = (int)std::ceil(std::hypot((double)tw, (double)th)) * scale + 2;
|
|
for (int dy = -rad; dy <= rad; ++dy)
|
|
for (int dx = -rad; dx <= rad; ++dx) {
|
|
// inverse rotation of the destination offset into image space
|
|
const double ix = (dx * ca + dy * sa) / scale + px, iy = (-dx * sa + dy * ca) / scale + py;
|
|
int sx = (int)std::floor(ix), sy = (int)std::floor(iy);
|
|
if (mirror) sy = th - 1 - sy + 0;
|
|
if (sx < 0 || sy < 0 || sx >= tw || sy >= th) continue;
|
|
const uint32_t c = tex[(size_t)sy * tw + sx];
|
|
if (c >> 24) put((int)std::lround(x) + dx, (int)std::lround(y) + dy, c);
|
|
}
|
|
}
|
|
// Horizontal bar: dark frame, background, filled part.
|
|
void bar(int x, int y, int bw, int bh, float frac, uint32_t fill, uint32_t back) {
|
|
frac = std::clamp(frac, 0.0f, 1.0f);
|
|
rect(x - 2, y - 2, bw + 4, bh + 4, rgba(0, 0, 0, 220));
|
|
rect(x, y, bw, bh, back);
|
|
rect(x, y, (int)(bw * frac + 0.5f), bh, fill);
|
|
}
|
|
};
|
|
|
|
struct Enemy { int sx, sy; float hp, maxhp; };
|
|
struct BossBar { float hp, maxhp; }; // a BossHealthBarComponent bar on the screen (00a58380)
|
|
|
|
// Everything the HUD shows for one frame.
|
|
struct State {
|
|
bool world = false, has_player = false, paused = false, brush_mode = false, free_cam = false, debug = false, help = false;
|
|
float hp = 4, maxhp = 4, energy = 1; // hp in game units (x25 on screen), energy 0..1
|
|
double px = 0, py = 0;
|
|
int view_x = 0, view_y = 0;
|
|
int mouse_x = 0, mouse_y = 0; // output pixels
|
|
int brush_select = 0, brush_radius = 6;
|
|
const char* const* palette_names = nullptr;
|
|
const uint32_t* palette_colors = nullptr; // ARGB swatches of the 8 brush materials
|
|
double fps = 0;
|
|
double gpu_ms = 0, frame_ms = 0; // GPU time of the last frame (timestamp queries), wall time between frames
|
|
int sim_speed = 1; // cell simulation ticks per frame
|
|
bool show_timing = false; // always-on timing line (sandbox)
|
|
int entities = 0, stored_tiles = 0, frame = 0;
|
|
int death_timer = 0; // frames left of the "you died" message
|
|
std::vector<Enemy> enemies;
|
|
std::vector<BossBar> boss_bars; // (main.cpp: the bosses within gui_max_distance_visible)
|
|
int ui_frame = 0; // (the blink of a low boss bar)
|
|
// the mod GUI (Gui* of the Lua API) in GUI units: gui_scale window pixels each; kind 0 text, 1 image, 2 nine-piece
|
|
struct GuiDraw { int kind = 0; float x = 0, y = 0, w = 0, h = 0, scale = 1, alpha = 1; uint32_t color = 0xFFFFFFFFu; std::string text; const Icon* icon = nullptr; };
|
|
std::vector<GuiDraw> gui;
|
|
int gui_scale = 2;
|
|
struct WorldText { int x = 0, y = 0; std::string text; uint32_t color = 0xFFFFFFFFu; int scale = 2; bool dmg_font = false; }; // damage numbers (font_damage, centred on x and y, no shadow) and BLOCK (centred on x)
|
|
std::vector<WorldText> damage_texts;
|
|
int money = 0;
|
|
struct WandInfo { const Icon* icon = nullptr; std::string name; int cards = 0, capacity = 0, uses = -1; }; // uses: the uses left of its first limited spell (-1 none)
|
|
std::vector<WandInfo> wands; // inventory slots
|
|
int active_wand = -1;
|
|
struct ItemInfo { const Icon* icon = nullptr; float frac = 1.0f; std::string name; bool empty = false; }; // an item of the quick inventory (flask): icon, how full it is
|
|
std::vector<ItemInfo> items; // slots 5-8
|
|
int active_slot = -1; // highlighted slot (0-7) when it is an item
|
|
std::vector<const Icon*> spell_icons; // spells of the active wand, in deck order
|
|
std::vector<int> spell_uses; // their uses left (-1: unlimited), drawn in the corner like in the inventory
|
|
float mana = 0, mana_max = 0;
|
|
float air = 1.0f; // breath left (1 = full)
|
|
bool burning = false;
|
|
int reload_left = 0, cooldown_left = 0;
|
|
struct ProjDraw { int sx = 0, sy = 0; const uint32_t* tex = nullptr; int w = 0, h = 0; uint32_t color = 0xFFFFD040u; };
|
|
std::vector<ProjDraw> projectiles;
|
|
// the wand held in the hand: sprite, pivot in the sprite, hand position and aim angle (output pixels / radians)
|
|
const uint32_t* hand_tex = nullptr; int hand_w = 0, hand_h = 0, hand_px = 0, hand_py = 0, hand_x = 0, hand_y = 0; double hand_angle = 0; bool hand_mirror = false;
|
|
std::vector<std::string> log_lines; // recent game messages, newest last
|
|
std::vector<std::string> inventory; // names of picked up items
|
|
std::string hint; // e.g. "E Open chest" next to the player
|
|
int hint_x = -1, hint_y = -1; // output pixels
|
|
uint32_t hint_color = 0xFFFFFFFFu;
|
|
const char* seed_text = "";
|
|
// ---- the open inventory (wand editing): cells are laid out and hit-tested by the host, drawn here ----
|
|
struct InvCell { int x = 0, y = 0, w = 0, h = 0; bool draw = true; int bg = -1; const Icon* icon = nullptr; float fill = -1.0f; bool hover = false, selected = false, locked = false, ghost = false; bool frame = true; std::string label; };
|
|
struct InvPanel { int x = 0, y = 0, w = 0, h = 0; int style = 0; bool active = false; }; // style 0: the brown frame of a bar, 1: the dark box of a wand
|
|
struct InvText { int x = 0, y = 0; std::string text; uint32_t color = 0xFFFFFFFFu; int scale = 1; };
|
|
struct TipLine { std::string text; uint32_t color = 0xFFFFFFFFu; const Icon* icon = nullptr; std::string value; };
|
|
struct StatusIcon { const Icon* icon = nullptr; std::string name, desc; float alpha = 1.0f; };
|
|
std::vector<StatusIcon> statuses; // the active statuses of the player (stains, ingestion, effects with a UIIconComponent display_in_hud): icons left of the bars, the text on hover
|
|
struct HeadIcon { const Icon* icon = nullptr; int x = 0, y = 0, scale = 1; };
|
|
std::vector<HeadIcon> head_icons; // the status icons over the player's head (UIIconComponent display_above_head, the stains), top-left corner in output pixels
|
|
bool inv_open = false;
|
|
int hover_slot = -1; // quick slot (0-7) under the mouse
|
|
std::vector<InvCell> inv_cells;
|
|
std::vector<InvPanel> inv_panels;
|
|
bool tip_fixed = false; int tip_w = 0; const Icon* tip_icon = nullptr; // the information box at a fixed place (width in output pixels, a big icon on its right)
|
|
std::vector<InvText> inv_texts;
|
|
bool drag_on = false; const Icon* drag_icon = nullptr; int drag_bg = -1; int drag_x = 0, drag_y = 0; // the item on the cursor
|
|
std::vector<TipLine> tip; // hover information
|
|
int tip_x = 0, tip_y = 0;
|
|
// ---- the text sprites of the world (shop prices: SpriteComponent is_text_sprite, font_pixel_white), top-left corner in output pixels, scale = output pixels per world pixel ----
|
|
std::vector<InvText> world_texts;
|
|
// ---- overlays drawn over everything (the information box of a wand in the world, the new-wand choice): style 2 the dark information box, 3 the orange banner ----
|
|
struct Layer { bool dim = false; int dim_alpha = 120; std::vector<InvPanel> panels; std::vector<InvCell> cells; std::vector<InvText> texts; std::vector<InvPanel> shades; }; // (shades: darkened over everything of the layer - the slot under the mouse in the new-wand choice)
|
|
std::vector<Layer> layers;
|
|
};
|
|
|
|
struct Assets {
|
|
Icon heart, jetpack, box, boxHi, mana, money, itemBg[8], inactive, boxQuickHi, importantMsg;
|
|
GameFont font, dmgFont;
|
|
bool loaded = false;
|
|
void load(const ng::Wak& wak) {
|
|
font = load_font(wak, "data/fonts/font_pixel_white.xml");
|
|
dmgFont = load_font(wak, "data/fonts/font_damage.xml"); // REPORT_DAMAGE_FONT: red digits and "-" only
|
|
if (std::getenv("NG_FONTLOG")) { int n = 0; for (const auto& gl : dmgFont.g) n += gl.ok; std::printf("font_damage: texture %dx%d, %d glyphs%c", dmgFont.tex.w, dmgFont.tex.h, n, 10); }
|
|
heart = load_icon(wak, "data/ui_gfx/hud/health.png");
|
|
importantMsg = load_icon(wak, "data/ui_gfx/decorations/3piece_important_msg.png"); // (the banner of the new-wand choice: 3 pieces of 24, the middle stretched)
|
|
jetpack = load_icon(wak, "data/ui_gfx/hud/jetpack.png");
|
|
box = load_icon(wak, "data/ui_gfx/inventory/full_inventory_box.png");
|
|
money = load_icon(wak, "data/ui_gfx/hud/money.png");
|
|
boxHi = load_icon(wak, "data/ui_gfx/inventory/full_inventory_box_highlight.png");
|
|
mana = load_icon(wak, "data/ui_gfx/hud/mana.png");
|
|
static const char* const bgs[8] = {"projectile", "static_projectile", "modifier", "draw_many", "material", "other", "utility", "passive"}; // by spell type
|
|
for (int i = 0; i < 8; ++i) itemBg[i] = load_icon(wak, std::string("data/ui_gfx/inventory/item_bg_") + bgs[i] + ".png");
|
|
inactive = load_icon(wak, "data/ui_gfx/inventory/inventory_box_inactive_overlay.png");
|
|
boxQuickHi = load_icon(wak, "data/ui_gfx/inventory/quick_inventory_box_hovered.png");
|
|
loaded = true;
|
|
}
|
|
};
|
|
|
|
inline std::string fmt(const char* f, double a) { char b[64]; std::snprintf(b, sizeof b, f, a); return b; }
|
|
|
|
// Draws the whole HUD. Coordinates are output pixels (1280x720).
|
|
inline void draw(Canvas& cv, const Assets& as, const State& s) {
|
|
cv.clear();
|
|
cv.font = as.font.ok() ? &as.font : nullptr;
|
|
for (const State::InvText& t : s.world_texts) cv.text(t.x, t.y, t.text, t.color, t.scale, false);
|
|
const int uiS = std::max(1, (int)std::lround(cv.w * 2.8 / 2000.0)), tsS = std::max(1, uiS * 2 / 3);
|
|
auto drawCell = [&](const State::InvCell& c, int ui, int ts) {
|
|
if (!c.draw) return;
|
|
const Icon& frame = c.hover ? as.boxHi : as.box;
|
|
const Icon* fr = c.frame ? ((c.bg >= 0 && c.bg < 8 && as.itemBg[c.bg].ok()) ? &as.itemBg[c.bg] : &frame) : nullptr;
|
|
const bool exact = fr && fr->w * ui == c.w; // (a slot at the UI scale: the art as it is; a smaller one - the information boxes, 0.6 - stretched to the cell, its icon to 16 / 20 of it)
|
|
if (fr) { if (exact) cv.icon(*fr, c.x, c.y, ui); else cv.icon_fit(*fr, c.x, c.y, c.w, c.h); }
|
|
if (c.icon && c.icon->ok()) {
|
|
if (c.frame && !exact) { const int iw = c.w * c.icon->w / 20, ih = c.h * c.icon->h / 20; cv.icon_fit(*c.icon, c.x + (c.w - iw) / 2, c.y + (c.h - ih) / 2, iw, ih); }
|
|
else cv.blit_centered(c.icon->px.data(), c.icon->w, c.icon->h, c.x + c.w / 2, c.y + c.h / 2, c.frame ? std::max(1, std::min(ui, (c.w * 17 / 20) / std::max(c.icon->w, 1))) : std::max(1, c.w / std::max(c.icon->w, 1)));
|
|
}
|
|
if (c.hover && c.bg >= 0) cv.frame(c.x, c.y, c.w, c.h, rgba(255, 255, 255, 220));
|
|
if (c.selected) cv.frame(c.x - ui, c.y - ui, c.w + 2 * ui, c.h + 2 * ui, rgba(255, 220, 120, 230));
|
|
if (c.locked && as.inactive.ok()) cv.icon(as.inactive, c.x, c.y, ui);
|
|
if (c.ghost) cv.rect(c.x, c.y, c.w, c.h, rgba(0, 0, 0, 150));
|
|
if (!c.label.empty()) cv.text(c.x + 2 * ui, c.y + 2 * ui, c.label, rgba(255, 255, 255), std::max(1, ts));
|
|
};
|
|
const uint32_t white = rgba(255, 255, 255), grey = rgba(190, 190, 200), red = rgba(210, 40, 40), dark = rgba(40, 20, 20, 230);
|
|
if (s.inv_open && s.has_player) {
|
|
cv.rect(0, 0, cv.w, cv.h, rgba(0, 0, 0, 120)); // the inventory dims the world
|
|
for (const State::InvPanel& p : s.inv_panels) {
|
|
if (p.style == 0) { cv.rect(p.x, p.y, p.w, p.h, rgba(60, 47, 42, 255)); cv.frame(p.x, p.y, p.w, p.h, rgba(79, 50, 45, 255)); } // (the game's colours, sampled at 2560 x 1440)
|
|
else { cv.rect(p.x, p.y, p.w, p.h, rgba(4, 3, 4, 245)); cv.frame(p.x, p.y, p.w, p.h, p.active ? rgba(255, 160, 40, 255) : rgba(200, 190, 170, 255)); if (p.active) cv.frame(p.x + 1, p.y + 1, p.w - 2, p.h - 2, rgba(255, 160, 40, 255)); }
|
|
}
|
|
}
|
|
// ---- the status icons over the player's head ----
|
|
for (const State::HeadIcon& hi : s.head_icons) if (hi.icon && hi.icon->ok()) cv.icon(*hi.icon, hi.x, hi.y, hi.scale);
|
|
// ---- the damage numbers / BLOCK rising over the entities (00ab9a90 / 00b071f0) ----
|
|
for (const State::WorldText& wt : s.damage_texts) {
|
|
if (wt.dmg_font && as.dmgFont.ok()) {
|
|
const GameFont* keep = cv.font;
|
|
cv.font = &as.dmgFont;
|
|
cv.text(wt.x - cv.text_width(wt.text, wt.scale) / 2, wt.y - as.dmgFont.line_height * wt.scale / 2, wt.text, wt.color, wt.scale, false);
|
|
cv.font = keep;
|
|
} else cv.text(wt.x - cv.text_width(wt.text, wt.scale) / 2, wt.y, wt.text, wt.color, wt.scale);
|
|
}
|
|
// TODO(to-do #5): the bar widget details (damage chunk, fade, name), the final boss variant, in_world slider scaling
|
|
// ---- boss bars (BossHealthBarComponent, 00a58380 / 00b7cbf0): centred, at 0.8 of the screen height in virtual pixels and 24 higher for each further boss; the
|
|
// width 20 log10(2.5 max hp); the player's bar art (frame, back, fill), the heart and the hp; below 0.25 hp the fill blinks every 30 frames in the damage colour
|
|
{
|
|
const int ui = std::max(1, (int)std::lround(cv.w * 2.8 / 2000.0));
|
|
const int ts = std::max(1, ui * 2 / 3);
|
|
for (size_t k = 0; k < s.boss_bars.size(); ++k) {
|
|
const BossBar& b = s.boss_bars[k];
|
|
const float wv = std::clamp(20.0f * std::log10(std::max(2.5f * b.maxhp, 1.0f)), 8.0f, 200.0f);
|
|
const int bw = (int)(wv * ui), bh = 6 * ui;
|
|
const int cy = (int)((cv.h / (float)ui * 0.8f - 24.0f * (float)k) * ui), bx = cv.w / 2 - bw / 2, top = cy - bh / 2;
|
|
const bool blink = b.hp < 0.25f && (s.ui_frame / 30) % 2 == 1;
|
|
cv.rect(bx, top, bw, bh, rgba(121, 71, 56));
|
|
cv.rect(bx + ui, top + ui, bw - 2 * ui, bh - 2 * ui, b.hp < 0.25f && !blink ? rgba(110, 30, 30, 235) : rgba(58, 36, 31, 235));
|
|
cv.rect(bx + ui, top + ui, (int)((bw - 2 * ui) * std::clamp(b.hp / std::max(b.maxhp, 0.001f), 0.0f, 1.0f)), bh - 2 * ui, blink ? rgba(255, 255, 255) : rgba(135, 191, 28));
|
|
if (as.heart.ok()) cv.icon(as.heart, bx - 10 * ui, cy - 4 * ui, ui);
|
|
const std::string hp = std::to_string((int)std::lround(std::max(b.hp, 0.0f) * 25.0f));
|
|
cv.text(bx + bw + 3 * ui, cy - 3 * ts, hp, rgba(255, 255, 255), ts);
|
|
}
|
|
}
|
|
// ---- health bars (HealthBarComponent: the bosses) ----
|
|
for (const Enemy& e : s.enemies) {
|
|
const int bw = 28;
|
|
cv.rect(e.sx - bw / 2 - 1, e.sy - 1, bw + 2, 5, rgba(0, 0, 0, 200));
|
|
cv.rect(e.sx - bw / 2, e.sy, bw, 3, dark);
|
|
cv.rect(e.sx - bw / 2, e.sy, (int)(bw * std::clamp(e.hp / std::max(e.maxhp, 0.001f), 0.0f, 1.0f)), 3, red);
|
|
}
|
|
if (s.has_player) {
|
|
// The layout of the game (measured on a 2000x1088 screenshot, where the HUD is drawn at 2.8 pixels per asset pixel): the quick inventory top left,
|
|
// the health / flight / mana bars with their icons top right, the gold under them.
|
|
const int ui = std::max(1, (int)std::lround(cv.w * 2.8 / 2000.0)); // output pixels per asset pixel
|
|
const int ts = std::max(1, ui * 2 / 3); // text scale
|
|
const int slot = 20 * ui, pitch = (int)(22.5 * ui), x0 = 23 * ui, y0 = 23 * ui;
|
|
for (int i = 0; i < 8; ++i) {
|
|
const int x = x0 + i * pitch;
|
|
cv.icon(((s.active_slot >= 0 ? i == s.active_slot : i == s.active_wand) || i == s.hover_slot) ? as.boxHi : as.box, x, y0, ui);
|
|
if (i >= 4 && i - 4 < (int)s.items.size() && !s.items[i - 4].empty) {
|
|
const State::ItemInfo& it = s.items[i - 4];
|
|
if (it.icon && it.icon->ok()) cv.blit_centered(it.icon->px.data(), it.icon->w, it.icon->h, x + slot / 2, y0 + slot / 2, std::max(1, std::min(ui, (slot * 3 / 4) / std::max(it.icon->w, 1))));
|
|
cv.rect(x + 2 * ui, y0 + slot - 3 * ui, slot - 4 * ui, 2 * ui, rgba(0, 0, 0, 170)); // how full the flask is
|
|
cv.rect(x + 2 * ui, y0 + slot - 3 * ui, (int)((slot - 4 * ui) * std::clamp(it.frac, 0.0f, 1.0f)), 2 * ui, rgba(110, 170, 230));
|
|
}
|
|
if (i < 4 && i < (int)s.wands.size() && s.wands[i].icon && s.wands[i].icon->ok()) {
|
|
const Icon& ic = *s.wands[i].icon;
|
|
cv.blit_centered(ic.px.data(), ic.w, ic.h, x + slot / 2, y0 + slot / 2, std::max(1, std::min(ui, (slot * 3 / 4) / std::max(ic.w, 1))));
|
|
if (s.wands[i].uses >= 0) cv.text(x + 2 * ui, y0 + 2 * ui, std::to_string(s.wands[i].uses), rgba(255, 255, 255), std::max(1, ts)); // the first limited spell's uses left
|
|
}
|
|
}
|
|
// spells of the active wand, under the quick inventory
|
|
const int sy = y0 + slot + 3 * ui;
|
|
for (size_t i = 0; i < s.spell_icons.size() && !s.inv_open; ++i) {
|
|
const int x = x0 + (int)i * 18 * ui;
|
|
cv.rect(x, sy, 17 * ui, 17 * ui, rgba(0, 0, 0, 150));
|
|
if (s.spell_icons[i] && s.spell_icons[i]->ok()) cv.blit_centered(s.spell_icons[i]->px.data(), s.spell_icons[i]->w, s.spell_icons[i]->h, x + 17 * ui / 2, sy + 17 * ui / 2, ui);
|
|
if (i < s.spell_uses.size() && s.spell_uses[i] >= 0) cv.text(x + 2 * ui, sy + 2 * ui, std::to_string(s.spell_uses[i]), rgba(255, 255, 255), std::max(1, ts));
|
|
}
|
|
if (s.inv_open) {
|
|
} else if (s.active_slot >= 4 && s.active_slot - 4 < (int)s.items.size()) {
|
|
cv.text(x0, sy + (s.spell_icons.empty() ? 0 : 19 * ui), s.items[s.active_slot - 4].name, white, ts);
|
|
} else if (s.active_wand >= 0 && s.active_wand < (int)s.wands.size()) {
|
|
const auto& w = s.wands[s.active_wand];
|
|
cv.text(x0, sy + (s.spell_icons.empty() ? 0 : 19 * ui), w.name.empty() ? "Wand" : w.name, white, ts);
|
|
}
|
|
if (s.inv_open) { // the wand rows, the full inventory and what is on the cursor
|
|
for (const State::InvText& t : s.inv_texts) cv.text(t.x, t.y, t.text, t.color, t.scale);
|
|
for (const State::InvCell& c : s.inv_cells) drawCell(c, ui, ts);
|
|
if (!s.tip.empty()) { // hover information: a dark box with the name, the description and the numbers
|
|
const int ts = std::max(2, ui * 3 / 4); // (the information box has bigger text than the rest)
|
|
int tw = 0;
|
|
for (const State::TipLine& l : s.tip) tw = std::max(tw, cv.text_width(l.text, ts) + (l.value.empty() ? 0 : cv.text_width(l.value, ts) + 10 * ts) + (l.icon ? 9 * ts : 0));
|
|
const int lh = 9 * ts, bw = s.tip_fixed ? s.tip_w : tw + 8 * ts, bh = (int)s.tip.size() * lh + 6 * ts;
|
|
const int bx = s.tip_fixed ? s.tip_x : std::clamp(s.tip_x, 4, std::max(4, cv.w - bw - 4)), by = s.tip_fixed ? s.tip_y : std::clamp(s.tip_y, 4, std::max(4, cv.h - bh - 4));
|
|
cv.rect(bx, by, bw, bh, rgba(12, 10, 12, 235));
|
|
cv.frame(bx, by, bw, bh, rgba(150, 130, 110, 255));
|
|
if (s.tip_icon && s.tip_icon->ok()) cv.blit_centered(s.tip_icon->px.data(), s.tip_icon->w, s.tip_icon->h, bx + bw - 28 * ui, by + bh / 2, ui);
|
|
int ty = by + 3 * ts;
|
|
for (const State::TipLine& l : s.tip) {
|
|
int tx = bx + 4 * ts;
|
|
if (l.icon && l.icon->ok()) { cv.blit_centered(l.icon->px.data(), l.icon->w, l.icon->h, tx + 3 * ts, ty + 3 * ts, std::max(1, ts)); tx += 9 * ts; }
|
|
cv.text(tx, ty, l.text, l.color, ts);
|
|
if (!l.value.empty()) cv.text(bx + bw - 4 * ts - cv.text_width(l.value, ts), ty, l.value, rgba(255, 255, 255), ts);
|
|
ty += lh;
|
|
}
|
|
}
|
|
if (s.drag_on && (s.drag_icon || s.drag_bg >= 0)) {
|
|
if (s.drag_bg >= 0 && s.drag_bg < 8 && as.itemBg[s.drag_bg].ok()) cv.icon(as.itemBg[s.drag_bg], s.drag_x - 10 * ui, s.drag_y - 10 * ui, ui);
|
|
if (s.drag_icon && s.drag_icon->ok()) cv.blit_centered(s.drag_icon->px.data(), s.drag_icon->w, s.drag_icon->h, s.drag_x, s.drag_y, std::max(1, std::min(ui, (17 * ui) / std::max(s.drag_icon->w, 1))));
|
|
}
|
|
}
|
|
// bars: a brown frame, a dark back, the fill in the colour of the game's colour strips
|
|
const int right = cv.w - 45 * ui;
|
|
auto bar = [&](float cy, int h, float frac, uint32_t fill) {
|
|
const int top = (int)((cy - h * 0.5f) * ui), bx = right - 46 * ui, bw = 46 * ui;
|
|
cv.rect(bx, top, bw, h * ui, rgba(121, 71, 56));
|
|
cv.rect(bx + ui, top + ui, bw - 2 * ui, (h - 2) * ui, rgba(58, 36, 31, 235));
|
|
cv.rect(bx + ui, top + ui, (int)((bw - 2 * ui) * std::clamp(frac, 0.0f, 1.0f)), (h - 2) * ui, fill);
|
|
};
|
|
auto small_icon = [&](const Icon& ic, float cy) { cv.icon(ic, right + 3 * ui, (int)((cy - 4.0f) * ui), ui); };
|
|
const bool low = s.hp < 0.3f * s.maxhp;
|
|
bar(25.7f, 6, s.hp / std::max(s.maxhp, 0.001f), low ? rgba(214, 90, 56) : rgba(135, 191, 28));
|
|
small_icon(as.heart, 25.7f);
|
|
cv.text(right + 14 * ui, (int)(25.7f * ui) - 3 * ts, std::to_string((int)std::lround(std::max(s.hp, 0.0f) * 25.0f)), white, ts);
|
|
bar(35.7f, 5, s.energy, rgba(255, 170, 64));
|
|
small_icon(as.jetpack, 35.7f);
|
|
if (s.mana_max > 0) {
|
|
bar(44.6f, 5, s.mana / s.mana_max, rgba(66, 168, 226));
|
|
small_icon(as.mana, 44.6f);
|
|
if (s.reload_left > 0) cv.rect(right - 46 * ui, (int)(47.6f * ui), std::min(46 * ui, s.reload_left * ui / 2), ui, rgba(255, 150, 60));
|
|
else if (s.cooldown_left > 0) cv.rect(right - 46 * ui, (int)(47.6f * ui), std::min(46 * ui, s.cooldown_left * ui / 2), ui, rgba(255, 255, 255, 160));
|
|
}
|
|
small_icon(as.money, 52.5f);
|
|
{ // the statuses: a column of icons under the bars (from the gold row down, at the bars' left edge); the mouse over one shows its name and description
|
|
const int colX = right - 46 * ui;
|
|
int sy2 = (int)(s.air < 0.999f ? 67.0f * ui : 60.0f * ui), hover = -1, hy = 0;
|
|
for (size_t k = 0; k < s.statuses.size(); ++k) {
|
|
const State::StatusIcon& si = s.statuses[k];
|
|
const int w = si.icon && si.icon->ok() ? si.icon->w * ui : 8 * ui, h = si.icon && si.icon->ok() ? si.icon->h * ui : 8 * ui;
|
|
if (si.icon && si.icon->ok()) cv.icon(*si.icon, colX + (10 * ui - w) / 2, sy2, ui);
|
|
else cv.rect(colX + ui, sy2, 8 * ui, 8 * ui, rgba(200, 200, 200, 160));
|
|
if (s.mouse_x >= colX - ui && s.mouse_x < colX + 11 * ui && s.mouse_y >= sy2 && s.mouse_y < sy2 + h) { hover = (int)k; hy = sy2; }
|
|
sy2 += h + 2 * ui;
|
|
}
|
|
if (hover >= 0) {
|
|
const State::StatusIcon& si = s.statuses[(size_t)hover];
|
|
const int lh = 9 * ts, tw = std::max(cv.text_width(si.name, ts), si.desc.empty() ? 0 : cv.text_width(si.desc, ts)) + 8 * ts;
|
|
const int bh = (si.desc.empty() ? 1 : 2) * lh + 6 * ts, bx = std::max(4, colX - 3 * ui - tw);
|
|
cv.rect(bx, hy, tw, bh, rgba(22, 20, 20, 235)); cv.frame(bx, hy, tw, bh, rgba(196, 178, 140));
|
|
cv.text(bx + 4 * ts, hy + 3 * ts, si.name, white, ts);
|
|
if (!si.desc.empty()) cv.text(bx + 4 * ts, hy + 3 * ts + lh, si.desc, rgba(190, 190, 200), ts);
|
|
}
|
|
}
|
|
cv.text(right + 14 * ui, (int)(52.5f * ui) - 3 * ts, std::to_string(s.money), white, ts);
|
|
if (s.air < 0.999f) { // breath under liquid
|
|
bar(61.0f, 5, s.air, s.air < 0.25f ? rgba(255, 80, 60) : rgba(120, 220, 255));
|
|
cv.text(right + 14 * ui, (int)(61.0f * ui) - 3 * ts, "AIR", rgba(150, 230, 255), ts);
|
|
}
|
|
if (s.burning) cv.text(right - 46 * ui, (int)(66.0f * ui), "ON FIRE", rgba(255, 150, 40), ts);
|
|
}
|
|
// ---- brush palette ----
|
|
if (!s.world || s.brush_mode) {
|
|
const int x0 = 14, y0 = s.world ? 120 : 14;
|
|
cv.text(x0, y0, s.world ? "BRUSH MODE (B to leave)" : "SANDBOX", rgba(255, 220, 120), 2);
|
|
for (int i = 0; i < 8; ++i) {
|
|
const int x = x0 + i * 74, y = y0 + 24;
|
|
cv.rect(x - 2, y - 2, 70, 34, i == s.brush_select ? rgba(255, 255, 255, 230) : rgba(0, 0, 0, 160));
|
|
cv.rect(x, y, 66, 20, s.palette_colors ? s.palette_colors[i] : white);
|
|
cv.rect(x, y + 20, 66, 10, rgba(0, 0, 0, 180));
|
|
cv.text(x + 2, y + 21, std::to_string(i + 1) + " " + std::string(s.palette_names ? s.palette_names[i] : "").substr(0, 8), white, 1, false);
|
|
}
|
|
cv.text(x0, y0 + 64, "LMB paint RMB erase wheel = size " + std::to_string(s.brush_radius), grey, 1);
|
|
}
|
|
// ---- messages and the interaction hint ----
|
|
{
|
|
int y = 110;
|
|
for (size_t i = 0; i < s.log_lines.size(); ++i) {
|
|
const std::string& l = s.log_lines[i];
|
|
cv.text(cv.w / 2 - cv.text_width(l, 2) / 2, 20 + (int)i * 24, l, rgba(255, 240, 160), 2);
|
|
(void)y;
|
|
}
|
|
if (!s.hint.empty() && s.hint_x >= 0) cv.text(s.hint_x - cv.text_width(s.hint, 2) / 2, s.hint_y, s.hint, s.hint_color, 2);
|
|
}
|
|
// ---- wand in hand and projectiles ----
|
|
if (s.hand_tex) cv.blit_rotated(s.hand_tex, s.hand_w, s.hand_h, s.hand_px, s.hand_py, s.hand_x, s.hand_y, s.hand_angle, 2, s.hand_mirror);
|
|
for (const auto& p : s.projectiles) {
|
|
if (p.tex && p.w > 0) cv.blit_centered(p.tex, p.w, p.h, p.sx, p.sy, 2);
|
|
else cv.rect(p.sx - 3, p.sy - 3, 6, 6, p.color);
|
|
}
|
|
// ---- overlays ----
|
|
for (const State::Layer& L : s.layers) {
|
|
if (L.dim) cv.rect(0, 0, cv.w, cv.h, rgba(0, 0, 0, L.dim_alpha));
|
|
for (const State::InvPanel& p : L.panels) {
|
|
if (p.style == 3 && as.importantMsg.ok() && as.importantMsg.w == 72) { // the banner of the new-wand choice: 3piece_important_msg.png, its ends as they are, its middle stretched
|
|
const Icon& im = as.importantMsg;
|
|
const int pw = 24, ph = im.h;
|
|
auto piece = [&](int k) { Icon c; c.w = pw; c.h = ph; c.px.resize((size_t)pw * ph); for (int j = 0; j < ph; ++j) for (int i = 0; i < pw; ++i) c.px[(size_t)j * pw + i] = im.px[(size_t)j * im.w + k * pw + i]; return c; };
|
|
static Icon left, mid, right; static bool cut = false;
|
|
if (!cut) { left = piece(0); mid = piece(1); right = piece(2); cut = true; }
|
|
const int ew = pw * uiS, eh = p.h;
|
|
cv.icon_fit(left, p.x, p.y, ew, eh);
|
|
cv.icon_fit(mid, p.x + ew, p.y, std::max(1, p.w - 2 * ew), eh);
|
|
cv.icon_fit(right, p.x + p.w - ew, p.y, ew, eh);
|
|
} else if (p.style == 3) { // (without the art: an orange band with a golden frame and rounded ends)
|
|
for (int j = 0; j < p.h; ++j) { const float t = (float)j / std::max(1, p.h - 1); const int r = 220 - (int)(60 * std::fabs(t - 0.4f)), gg = 112 - (int)(40 * std::fabs(t - 0.4f)); cv.rect(p.x, p.y + j, p.w, 1, rgba(r, gg, 24, 245)); }
|
|
cv.frame(p.x, p.y, p.w, p.h, rgba(255, 196, 70)); cv.frame(p.x + uiS, p.y + uiS, p.w - 2 * uiS, p.h - 2 * uiS, rgba(120, 50, 10));
|
|
cv.rect(p.x - 3 * uiS, p.y + p.h / 2 - 3 * uiS, 3 * uiS, 6 * uiS, rgba(255, 196, 70)); cv.rect(p.x + p.w, p.y + p.h / 2 - 3 * uiS, 3 * uiS, 6 * uiS, rgba(255, 196, 70));
|
|
} else if (p.style == 2) { cv.rect(p.x, p.y, p.w, p.h, rgba(15, 14, 13, 225)); cv.frame(p.x, p.y, p.w, p.h, rgba(148, 128, 100)); cv.frame(p.x + 1, p.y + 1, p.w - 2, p.h - 2, rgba(148, 128, 100)); if (p.active) cv.frame(p.x - uiS, p.y - uiS, p.w + 2 * uiS, p.h + 2 * uiS, rgba(255, 160, 40)); } // (the information box: the game's fill and 2 px frame, sampled at 2560 x 1440)
|
|
else { cv.rect(p.x, p.y, p.w, p.h, rgba(30, 22, 22, 235)); cv.frame(p.x, p.y, p.w, p.h, p.active ? rgba(255, 160, 40, 255) : rgba(200, 190, 170, 255)); }
|
|
}
|
|
for (const State::InvCell& c : L.cells) drawCell(c, uiS, tsS);
|
|
for (const State::InvText& t : L.texts) cv.text(t.x, t.y, t.text, t.color, t.scale);
|
|
for (const State::InvPanel& p : L.shades) cv.rect(p.x, p.y, p.w, p.h, rgba(0, 0, 0, 110));
|
|
}
|
|
// ---- messages ----
|
|
if (s.death_timer > 0) {
|
|
const std::string m = "YOU DIED";
|
|
cv.rect(0, cv.h / 2 - 60, cv.w, 90, rgba(0, 0, 0, std::min(200, s.death_timer * 4)));
|
|
cv.text(cv.w / 2 - cv.text_width(m, 6) / 2, cv.h / 2 - 40, m, rgba(220, 40, 40), 6);
|
|
const std::string m2 = "respawning at the start";
|
|
cv.text(cv.w / 2 - cv.text_width(m2, 2) / 2, cv.h / 2 + 6, m2, grey, 2);
|
|
}
|
|
if (s.paused) {
|
|
cv.rect(0, 0, cv.w, cv.h, rgba(0, 0, 0, 90));
|
|
const std::string m = "PAUSED";
|
|
cv.text(cv.w / 2 - cv.text_width(m, 6) / 2, cv.h / 2 - 40, m, white, 6);
|
|
}
|
|
if (s.help) {
|
|
const char* lines[] = {
|
|
"A / D, arrows walk", "W, Up, Space jump (hold in the air to levitate)", "mouse aim, the camera leans toward it",
|
|
"LMB fire the spark bolt", "E pick up / open the item next to you", "F kick (melee swing, throws crates and barrels)", "B brush mode: paint materials with the mouse (keys 1-8)",
|
|
"5-8 flasks (LMB pour, RMB throw)", "Tab, I inventory: drag spells and wands (spells only in a workshop)", "RMB throw the item in the hand, Q drop it", "F2 free camera: WASD, [ ] speed, T teleport, G 10000 gold, no fog", "F8 debug mode: wand editing everywhere, K test wand, C spell panel (type to filter, click to spawn)",
|
|
"F9 save the last 20 s (clips folder), F11 clip editor: cut and save a GIF", "F10 performance report of the last 20 s (perf folder)", "P pause", "F3 debug info, entity ids", "F4 lighting on / off", "F5 sky debug view", "F1 this help", "Esc quit",
|
|
};
|
|
cv.rect(300, 120, 680, 470, rgba(0, 0, 0, 215));
|
|
cv.frame(300, 120, 680, 470, rgba(255, 255, 255, 160));
|
|
cv.text(320, 134, "CONTROLS", rgba(255, 220, 120), 3);
|
|
int y = 172;
|
|
for (const char* l : lines) { cv.text(320, y, l, white, 2); y += 24; }
|
|
} else if (s.has_player || !s.world) {
|
|
cv.text(cv.w - 14 - cv.text_width("F1 help", 1), cv.h - 18, "F1 help", grey, 1);
|
|
}
|
|
if (s.debug || s.show_timing) { // speed measurement: GPU time of a whole frame, per simulation tick (upper bound), frame pacing
|
|
const std::string t = "sim x" + std::to_string(s.sim_speed) + " GPU " + fmt("%.2f", s.gpu_ms) + " ms (" + fmt("%.3f", s.gpu_ms / std::max(1, s.sim_speed)) +
|
|
"/tick) " + fmt("%.1f", s.frame_ms) + " ms/frame " + fmt("%.0f", s.fps) + " fps";
|
|
cv.text(cv.w - 14 - cv.text_width(t, 2), s.debug ? 90 : 14, t, rgba(255, 220, 120), 2);
|
|
}
|
|
if (s.debug) {
|
|
cv.text(14, cv.h - 30, "F3+C colliders F3+L save physics dump (debug folder)", rgba(160, 255, 160), 1);
|
|
const std::string l1 = fmt("%.0f fps", s.fps) + " frame " + std::to_string(s.frame);
|
|
const std::string l2 = "player " + fmt("%.0f", s.px) + ", " + fmt("%.0f", s.py) + " view " + std::to_string(s.view_x) + ", " + std::to_string(s.view_y);
|
|
const std::string l3 = "entities " + std::to_string(s.entities) + " stored tiles " + std::to_string(s.stored_tiles) + " " + s.seed_text;
|
|
int y = 14;
|
|
for (const std::string& l : {l1, l2, l3}) {
|
|
cv.text(cv.w - 14 - cv.text_width(l, 2), y, l, rgba(160, 255, 160), 2);
|
|
y += 22;
|
|
}
|
|
}
|
|
// ---- the mod GUI ----
|
|
for (const State::GuiDraw& d : s.gui) {
|
|
const float k = (float)s.gui_scale;
|
|
const uint32_t a = (uint32_t)std::clamp(d.alpha * (float)(d.color >> 24), 0.0f, 255.0f);
|
|
if (d.kind == 0) { cv.text((int)std::lround(d.x * k), (int)std::lround(d.y * k), d.text, (d.color & 0xFFFFFFu) | a << 24, std::max(1, (int)std::lround(k * d.scale)), true); continue; }
|
|
if (!d.icon || !d.icon->ok()) continue;
|
|
const Icon& ic = *d.icon;
|
|
auto blit = [&](int sx0, int sy0, int sw, int sh, float dx, float dy, float dw, float dh) { // a part of the image stretched over the rectangle, alpha blended
|
|
const int X0 = (int)std::lround(dx * k), Y0 = (int)std::lround(dy * k), X1 = (int)std::lround((dx + dw) * k), Y1 = (int)std::lround((dy + dh) * k);
|
|
if (sw <= 0 || sh <= 0) return;
|
|
for (int Y = std::max(0, Y0); Y < std::min(cv.h, Y1); ++Y) for (int X = std::max(0, X0); X < std::min(cv.w, X1); ++X) {
|
|
const int ix = sx0 + std::min(sw - 1, (int)((X - X0) * (float)sw / std::max(1, X1 - X0))), iy = sy0 + std::min(sh - 1, (int)((Y - Y0) * (float)sh / std::max(1, Y1 - Y0)));
|
|
const uint32_t c = ic.px[(size_t)iy * ic.w + ix];
|
|
const uint32_t ca = (uint32_t)((c >> 24) * a / 255);
|
|
if (ca < 8) continue;
|
|
const uint32_t tint = d.color & 0xFFFFFFu;
|
|
const uint32_t r = ((c >> 16) & 255) * ((tint >> 16) & 255) / 255, g2 = ((c >> 8) & 255) * ((tint >> 8) & 255) / 255, b = (c & 255) * (tint & 255) / 255;
|
|
uint32_t& o = cv.px[(size_t)Y * cv.w + X];
|
|
cv.mark(X, Y);
|
|
const uint32_t oa = o >> 24;
|
|
auto mix = [&](uint32_t s0, uint32_t d0) { return (s0 * ca + d0 * (255 - ca)) / 255; };
|
|
o = std::max(oa, ca) << 24 | mix(r, (o >> 16) & 255) << 16 | mix(g2, (o >> 8) & 255) << 8 | mix(b, o & 255);
|
|
}
|
|
};
|
|
if (d.kind == 1) { blit(0, 0, ic.w, ic.h, d.x, d.y, d.w, d.h); continue; }
|
|
const int cw = std::max(1, ic.w / 3), ch = std::max(1, ic.h / 3), mw = ic.w - 2 * cw, mh = ic.h - 2 * ch; // nine pieces: the corners keep their size, the edges and the middle stretch
|
|
const float W = std::max(d.w, (float)(2 * cw)), H = std::max(d.h, (float)(2 * ch));
|
|
const float xs[3] = {d.x, d.x + cw, d.x + W - cw}, ws[3] = {(float)cw, W - 2 * cw, (float)cw}, ys[3] = {d.y, d.y + ch, d.y + H - ch}, hs3[3] = {(float)ch, H - 2 * ch, (float)ch};
|
|
const int sxs[3] = {0, cw, cw + mw}, sws[3] = {cw, mw, cw}, sys[3] = {0, ch, ch + mh}, shs[3] = {ch, mh, ch};
|
|
for (int j = 0; j < 3; ++j) for (int i = 0; i < 3; ++i) blit(sxs[i], sys[j], sws[i], shs[j], xs[i], ys[j], ws[i], hs3[j]);
|
|
}
|
|
// ---- crosshair (last: over the overlays and the menus) ----
|
|
if (s.mouse_x >= 0 && !s.paused && !s.drag_on) { // (an item on the cursor replaces the crosshair)
|
|
const uint32_t c = s.world && !s.brush_mode ? rgba(255, 255, 255, 230) : rgba(255, 220, 120, 230);
|
|
cv.rect(s.mouse_x - 8, s.mouse_y - 1, 6, 2, c); cv.rect(s.mouse_x + 3, s.mouse_y - 1, 6, 2, c);
|
|
cv.rect(s.mouse_x - 1, s.mouse_y - 8, 2, 6, c); cv.rect(s.mouse_x - 1, s.mouse_y + 3, 2, 6, c);
|
|
}
|
|
}
|
|
|
|
} // namespace hud
|