OpenKKnD/engine-c/src/snapshot.c
2026-09-19 14:20:56 +02:00

965 lines
45 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Software snapshot renderer - draws the game world into an RGBA buffer using
* only primitive shapes (no SDL gpu readback, which is broken on this box due
* to an SDL2/SDL3 conflict). Output is saved as PNG via png_save().
* Type-aware silhouettes mirror sprites.c so shot_*.png shows KKND2-like
* unit/building variety (not generic discs/rects).
*/
#include "kknd.h"
#define SW VIEW_W
#define SH VIEW_H
/* Internal render size (pre-upscale). Zoom 2 → ~640×360 world crop, then NN→1280×720.
* Wider crop keeps HQ + clash readable; bright fill + black outline does the rest. */
#define SHOT_ZOOM 2
/* Extra silhouette scale inside the shot crop (1.0 = sprites.c size). */
#define SHOT_UNIT_SCALE 1.45f
static uint8_t *g_buf;
static int g_sw = SW, g_sh = SH;
static void px(int x, int y, uint8_t r, uint8_t g, uint8_t b) {
if (x < 0 || y < 0 || x >= g_sw || y >= g_sh) return;
int i = (y * g_sw + x) * 4;
g_buf[i] = r; g_buf[i+1] = g; g_buf[i+2] = b; g_buf[i+3] = 255;
}
static void darken_px(int x, int y, float keep) {
if (x < 0 || y < 0 || x >= g_sw || y >= g_sh) return;
int i = (y * g_sw + x) * 4;
g_buf[i] = (uint8_t)(g_buf[i] * keep);
g_buf[i+1] = (uint8_t)(g_buf[i+1] * keep);
g_buf[i+2] = (uint8_t)(g_buf[i+2] * keep);
}
static void rectf(int x0, int y0, int x1, int y1, uint8_t r, uint8_t g, uint8_t b) {
if (x0 > x1) { int t = x0; x0 = x1; x1 = t; }
if (y0 > y1) { int t = y0; y0 = y1; y1 = t; }
for (int y = y0; y <= y1; y++)
for (int x = x0; x <= x1; x++)
px(x, y, r, g, b);
}
static void fill_rect(int x, int y, int w, int h, uint8_t r, uint8_t g, uint8_t b) {
if (w <= 0 || h <= 0) return;
rectf(x, y, x + w - 1, y + h - 1, r, g, b);
}
static void disc(int cx, int cy, int rad, uint8_t r, uint8_t g, uint8_t b) {
for (int y = -rad; y <= rad; y++)
for (int x = -rad; x <= rad; x++)
if (x*x + y*y <= rad*rad) px(cx+x, cy+y, r, g, b);
}
static void ring(int cx, int cy, int rad, int t, uint8_t r, uint8_t g, uint8_t b) {
int inner = rad - t;
if (inner < 0) inner = 0;
for (int y = -rad; y <= rad; y++)
for (int x = -rad; x <= rad; x++) {
int d = x*x + y*y;
if (d <= rad*rad && d >= inner*inner) px(cx+x, cy+y, r, g, b);
}
}
static void line(int x0, int y0, int x1, int y1, uint8_t r, uint8_t g, uint8_t b) {
int dx = abs(x1 - x0), dy = -abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
int err = dx + dy;
for (;;) {
px(x0, y0, r, g, b);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}
/* Saturated faction paints — lime Survivors / hot magenta Evolved on bright sand. */
static uint8_t faction_r(Faction f){ return f==FACTION_SURVIVOR?55 : f==FACTION_EVOLVED?255 : 240; }
static uint8_t faction_g(Faction f){ return f==FACTION_SURVIVOR?235: f==FACTION_EVOLVED?40 : 55; }
static uint8_t faction_b(Faction f){ return f==FACTION_SURVIVOR?55 : f==FACTION_EVOLVED?255 : 240; }
static void darken3(uint8_t r, uint8_t g, uint8_t b, float f,
uint8_t *or, uint8_t *og, uint8_t *ob) {
*or = (uint8_t)(r * f); *og = (uint8_t)(g * f); *ob = (uint8_t)(b * f);
}
static void lighten3(uint8_t r, uint8_t g, uint8_t b, float f,
uint8_t *or, uint8_t *og, uint8_t *ob) {
*or = (uint8_t)(r + (255 - r) * f);
*og = (uint8_t)(g + (255 - g) * f);
*ob = (uint8_t)(b + (255 - b) * f);
}
/* Classic KKND2 wasteland: yellow-tan sand (R>>B, G slightly under R). */
static void terrain_color(TerrainType t, uint8_t *r, uint8_t *g, uint8_t *b) {
switch (t) {
case T_GRASS: *r=242; *g=198; *b=92; break; /* sunlit desert */
case T_ROAD: *r=175; *g=148; *b=98; break;
case T_RUBBLE:*r=168; *g=138; *b=88; break;
case T_ROCK: *r=155; *g=142; *b=128; break;
case T_WATER: *r=72; *g=128; *b=178; break;
case T_SCRAP: *r=240; *g=190; *b=78; break;
default: *r=228; *g=188; *b=98; break;
}
}
/* Spoke (wheeled) or hash (tracked) marks on a tyre disc; phase = Unit.anim rad. */
static void tyre_mark(int cx, int cy, int rad, float phase, int wheeled) {
uint8_t sr = 205, sg = 200, sb = 180;
int n = wheeled ? 4 : 3;
float step = (float)(2.0 * M_PI / (double)n);
float r0 = wheeled ? rad * 0.30f : rad * 0.55f;
float r1 = rad * 0.90f;
for (int i = 0; i < n; i++) {
float a = phase + i * step;
int x1 = cx + (int)(cosf(a) * r0);
int y1 = cy + (int)(sinf(a) * r0);
int x2 = cx + (int)(cosf(a) * r1);
int y2 = cy + (int)(sinf(a) * r1);
line(x1, y1, x2, y2, sr, sg, sb);
}
}
/* Classic RTS silhouette: thin black outline, then bright faction fill.
* Dark outer discs previously read as olive-mud blobs under NN zoom. */
static void draw_unit(Unit *u, int sx, int sy) {
uint8_t br = faction_r(u->faction), bg = faction_g(u->faction), bb = faction_b(u->faction);
uint8_t lr, lg, lb;
lighten3(br, bg, bb, 0.45f, &lr, &lg, &lb);
uint8_t ok = 18, og = 16, ob = 14; /* near-black outline */
uint8_t mr = 70, mg = 65, mb = 58; /* tyre */
float c = cosf(u->heading), s = sinf(u->heading);
const float sc = SHOT_UNIT_SCALE;
float ph = u->anim; /* continuous roll/walk phase for wheeled/tracked/footed */
#define SI(n) ((int)((n) * sc + 0.5f))
#define LX(lx, ly) (sx + (int)((((lx)-15)*sc)*c - (((ly)-15)*sc)*s))
#define LY(lx, ly) (sy + (int)((((lx)-15)*sc)*s + (((ly)-15)*sc)*c))
/* Ground shadow — depth cue so bodies don't melt into sand. */
disc(sx + SI(2), sy + SI(3), SI(9), 55, 48, 28);
/* Foot infantry/mutant: vertical body bob; shadow stays grounded. */
if (u->type == UT_INFANTRY || u->type == UT_MUTANT) {
float bob = sinf(ph) * 2.5f * sc;
sy += (int)(bob + (bob >= 0.0f ? 0.5f : -0.5f));
}
switch (u->type) {
case UT_INFANTRY:
/* legs → torso → helmet → rifle (KKND2 foot soldier read) */
disc(LX(11,23), LY(11,23), SI(3), ok, og, ob);
disc(LX(19,23), LY(19,23), SI(3), ok, og, ob);
disc(LX(11,23), LY(11,23), SI(2), br, bg, bb);
disc(LX(19,23), LY(19,23), SI(2), br, bg, bb);
disc(sx, sy, SI(8), ok, og, ob);
disc(sx, sy, SI(6), br, bg, bb);
disc(LX(15,9), LY(15,9), SI(4), ok, og, ob);
disc(LX(15,9), LY(15,9), SI(3), lr, lg, lb);
line(LX(16,14), LY(16,14), LX(16,1), LY(16,1), 40, 36, 30);
line(LX(17,14), LY(17,14), LX(17,2), LY(17,2), 55, 50, 40);
break;
case UT_BUGGY:
/* cabin + windshield + chassis + wheels */
fill_rect(sx-SI(12), sy-SI(8), SI(24), SI(16), ok, og, ob);
fill_rect(sx-SI(10), sy-SI(6), SI(20), SI(12), br, bg, bb);
fill_rect(LX(7,10)-SI(1), LY(7,10)-SI(1), SI(10), SI(6), lr, lg, lb); /* cabin */
fill_rect(LX(8,9)-SI(1), LY(8,9)-SI(1), SI(8), SI(3), 35, 55, 75); /* windshield */
disc(LX(8,7), LY(8,7), SI(4), mr, mg, mb);
disc(LX(22,7), LY(22,7), SI(4), mr, mg, mb);
disc(LX(8,23), LY(8,23), SI(4), mr, mg, mb);
disc(LX(22,23), LY(22,23), SI(4), mr, mg, mb);
tyre_mark(LX(8,7), LY(8,7), SI(4), ph, 1);
tyre_mark(LX(22,7), LY(22,7), SI(4), ph, 1);
tyre_mark(LX(8,23), LY(8,23), SI(4), ph, 1);
tyre_mark(LX(22,23), LY(22,23), SI(4), ph, 1);
line(sx, sy, LX(29,15), LY(29,15), 40, 36, 30);
break;
case UT_TANK:
/* side tracks + hull + cupola + thick barrel */
fill_rect(LX(3,10)-SI(1), LY(3,10)-SI(2), SI(5), SI(12), ok, og, ob);
fill_rect(LX(24,10)-SI(1), LY(24,10)-SI(2), SI(5), SI(12), ok, og, ob);
fill_rect(LX(4,11)-SI(1), LY(4,11)-SI(1), SI(3), SI(10), mr, mg, mb);
fill_rect(LX(25,11)-SI(1), LY(25,11)-SI(1), SI(3), SI(10), mr, mg, mb);
fill_rect(sx-SI(11), sy-SI(5), SI(22), SI(10), ok, og, ob);
fill_rect(sx-SI(9), sy-SI(4), SI(18), SI(8), br, bg, bb);
disc(sx, sy, SI(6), ok, og, ob);
disc(sx, sy, SI(5), lr, lg, lb); /* cupola */
disc(LX(15,12), LY(15,12), SI(2), 40, 38, 32); /* hatch */
line(LX(15,14), LY(15,14), LX(29,14), LY(29,14), 50, 45, 35);
line(LX(15,15), LY(15,15), LX(29,15), LY(29,15), 50, 45, 35);
line(LX(15,16), LY(15,16), LX(28,16), LY(28,16), 50, 45, 35);
disc(LX(8,23), LY(8,23), SI(4), mr, mg, mb);
disc(LX(22,23), LY(22,23), SI(4), mr, mg, mb);
tyre_mark(LX(8,23), LY(8,23), SI(4), ph, 0);
tyre_mark(LX(22,23), LY(22,23), SI(4), ph, 0);
break;
case UT_ARTILLERY:
/* chassis + long barrel tube */
fill_rect(sx-SI(13), sy-SI(5), SI(26), SI(12), ok, og, ob);
fill_rect(sx-SI(11), sy-SI(3), SI(22), SI(8), br, bg, bb);
disc(sx, sy, SI(5), ok, og, ob);
disc(sx, sy, SI(4), lr, lg, lb);
line(LX(15,14), LY(15,14), LX(4,3), LY(4,3), 50, 45, 35);
line(LX(15,15), LY(15,15), LX(3,2), LY(3,2), 55, 50, 40);
line(LX(15,16), LY(15,16), LX(4,4), LY(4,4), 50, 45, 35);
disc(LX(4,3), LY(4,3), SI(2), 60, 55, 45); /* muzzle */
disc(LX(8,23), LY(8,23), SI(3), mr, mg, mb);
disc(LX(22,23), LY(22,23), SI(3), mr, mg, mb);
tyre_mark(LX(8,23), LY(8,23), SI(3), ph, 0);
tyre_mark(LX(22,23), LY(22,23), SI(3), ph, 0);
break;
case UT_HARVESTER: {
/* cabin nose + cargo bed (empty dark → gold fill by u->cargo) + wheels */
fill_rect(sx-SI(14), sy-SI(9), SI(28), SI(18), ok, og, ob);
fill_rect(sx-SI(12), sy-SI(7), SI(24), SI(14), br, bg, bb);
fill_rect(LX(4,10)-SI(1), LY(4,10)-SI(2), SI(8), SI(8), lr, lg, lb); /* cabin */
fill_rect(LX(5,9)-SI(1), LY(5,9)-SI(1), SI(6), SI(3), 35, 55, 75); /* cabin glass */
int bed_w = SI(14), bed_h = SI(10);
int bed_x = sx - SI(4), bed_y = sy - SI(5);
fill_rect(bed_x, bed_y, bed_w, bed_h, 55, 48, 32); /* empty bed */
float cap = (float)unit_def(u->type, u->faction)->cargo;
float fill = (cap > 0.0f) ? (u->cargo / cap) : 0.0f;
if (fill < 0.0f) fill = 0.0f;
if (fill > 1.0f) fill = 1.0f;
int fw = (int)(bed_w * fill + 0.5f);
if (fw > 0)
fill_rect(bed_x, bed_y, fw, bed_h, 200, 170, 90); /* cargo gold */
if (fill > 0.15f) {
line(LX(14,10), LY(14,10), LX(26,10), LY(26,10), 160, 130, 70);
line(LX(14,13), LY(14,13), LX(26,13), LY(26,13), 160, 130, 70);
line(LX(14,16), LY(14,16), LX(26,16), LY(26,16), 160, 130, 70);
}
disc(LX(8,23), LY(8,23), SI(4), mr, mg, mb);
disc(LX(22,23), LY(22,23), SI(4), mr, mg, mb);
tyre_mark(LX(8,23), LY(8,23), SI(4), ph, 1);
tyre_mark(LX(22,23), LY(22,23), SI(4), ph, 1);
line(LX(5,7), LY(5,7), LX(5,2), LY(5,2), lr, lg, lb);
break;
}
case UT_JEEP:
/* open cabin + roll bar + MG */
fill_rect(sx-SI(11), sy-SI(6), SI(22), SI(12), ok, og, ob);
fill_rect(sx-SI(9), sy-SI(4), SI(18), SI(8), br, bg, bb);
fill_rect(LX(8,11)-SI(1), LY(8,11)-SI(1), SI(8), SI(5), lr, lg, lb);
disc(LX(9,22), LY(9,22), SI(4), mr, mg, mb);
disc(LX(21,22), LY(21,22), SI(4), mr, mg, mb);
tyre_mark(LX(9,22), LY(9,22), SI(4), ph, 1);
tyre_mark(LX(21,22), LY(21,22), SI(4), ph, 1);
line(LX(11,11), LY(11,11), LX(11,3), LY(11,3), lr, lg, lb);
line(LX(19,11), LY(19,11), LX(19,3), LY(19,3), lr, lg, lb);
line(LX(11,3), LY(11,3), LX(19,3), LY(19,3), lr, lg, lb);
line(sx, sy, LX(28,15), LY(28,15), 50, 45, 35);
break;
case UT_MUTANT:
/* hunched body + spikes + claws */
disc(sx, sy+SI(2), SI(10), ok, og, ob);
disc(sx, sy, SI(8), br, bg, bb);
disc(LX(15,9), LY(15,9), SI(4), ok, og, ob);
disc(LX(15,9), LY(15,9), SI(3), lr, lg, lb);
line(LX(12,6), LY(12,6), LX(10,1), LY(10,1), 40, 36, 30); /* back spike */
line(LX(15,5), LY(15,5), LX(15,0), LY(15,0), 40, 36, 30);
line(LX(18,6), LY(18,6), LX(20,1), LY(20,1), 40, 36, 30);
line(sx, sy+SI(1), LX(26,8), LY(26,8), 40, 36, 30); /* claw arm */
line(LX(24,9), LY(24,9), LX(28,6), LY(28,6), 55, 50, 40);
line(LX(24,9), LY(24,9), LX(28,11), LY(28,11), 55, 50, 40);
disc(LX(10,22), LY(10,22), SI(3), ok, og, ob);
disc(LX(20,22), LY(20,22), SI(3), ok, og, ob);
disc(LX(10,22), LY(10,22), SI(2), br, bg, bb);
disc(LX(20,22), LY(20,22), SI(2), br, bg, bb);
break;
case UT_BEAST:
/* chunky body + snout + multi-wheel */
fill_rect(sx-SI(10), sy-SI(6), SI(20), SI(12), ok, og, ob);
fill_rect(sx-SI(8), sy-SI(4), SI(16), SI(8), br, bg, bb);
disc(LX(9,21), LY(9,21), SI(3), mr, mg, mb);
disc(LX(14,22), LY(14,22), SI(3), mr, mg, mb);
disc(LX(19,21), LY(19,21), SI(3), mr, mg, mb);
disc(LX(23,22), LY(23,22), SI(3), mr, mg, mb);
tyre_mark(LX(9,21), LY(9,21), SI(3), ph, 1);
tyre_mark(LX(14,22), LY(14,22), SI(3), ph, 1);
tyre_mark(LX(19,21), LY(19,21), SI(3), ph, 1);
tyre_mark(LX(23,22), LY(23,22), SI(3), ph, 1);
disc(LX(24,12), LY(24,12), SI(5), ok, og, ob);
disc(LX(24,12), LY(24,12), SI(4), lr, lg, lb); /* snout */
line(LX(26,12), LY(26,12), LX(29,12), LY(29,12), 40, 36, 30);
break;
case UT_RAVAGER:
/* heavy hull + side skirts + big barrel */
fill_rect(LX(1,9)-SI(1), LY(1,9)-SI(2), SI(5), SI(14), ok, og, ob);
fill_rect(LX(26,9)-SI(1), LY(26,9)-SI(2), SI(5), SI(14), ok, og, ob);
fill_rect(sx-SI(13), sy-SI(6), SI(26), SI(12), ok, og, ob);
fill_rect(sx-SI(11), sy-SI(5), SI(22), SI(10), br, bg, bb);
disc(sx, sy, SI(7), ok, og, ob);
disc(sx, sy, SI(6), lr, lg, lb);
line(LX(15,14), LY(15,14), LX(29,14), LY(29,14), 50, 45, 35);
line(LX(15,15), LY(15,15), LX(29,15), LY(29,15), 50, 45, 35);
line(LX(15,16), LY(15,16), LX(28,16), LY(28,16), 50, 45, 35);
disc(LX(7,23), LY(7,23), SI(4), mr, mg, mb);
disc(LX(23,23), LY(23,23), SI(4), mr, mg, mb);
tyre_mark(LX(7,23), LY(7,23), SI(4), ph, 0);
tyre_mark(LX(23,23), LY(23,23), SI(4), ph, 0);
break;
case UT_FLAME:
/* backpack tank + nozzle + body */
fill_rect(sx-SI(8), sy-SI(3), SI(16), SI(12), ok, og, ob);
fill_rect(sx-SI(6), sy-SI(1), SI(12), SI(9), br, bg, bb);
disc(LX(15,9), LY(15,9), SI(4), ok, og, ob);
disc(LX(15,9), LY(15,9), SI(3), lr, lg, lb);
disc(LX(8,14), LY(8,14), SI(5), ok, og, ob); /* fuel tank */
disc(LX(8,14), LY(8,14), SI(4), 90, 70, 40);
disc(LX(8,14), LY(8,14), SI(2), 200, 120, 40);
line(LX(18,15), LY(18,15), LX(28,20), LY(28,20), 255, 150, 50);
line(LX(18,16), LY(18,16), LX(27,21), LY(27,21), 255, 180, 60);
disc(LX(28,20), LY(28,20), SI(2), 255, 200, 80);
break;
case UT_MISSILE:
/* launcher rails + tubes on wheeled chassis */
fill_rect(sx-SI(11), sy-SI(4), SI(22), SI(14), ok, og, ob);
fill_rect(sx-SI(9), sy-SI(2), SI(18), SI(10), br, bg, bb);
fill_rect(LX(12,8)-SI(1), LY(12,8)-SI(2), SI(10), SI(4), ok, og, ob);
fill_rect(LX(13,7)-SI(1), LY(13,7)-SI(1), SI(8), SI(2), 255, 80, 60);
fill_rect(LX(12,13)-SI(1), LY(12,13)-SI(2), SI(10), SI(4), ok, og, ob);
fill_rect(LX(13,12)-SI(1), LY(13,12)-SI(1), SI(8), SI(2), 255, 100, 70);
disc(LX(9,23), LY(9,23), SI(4), mr, mg, mb);
disc(LX(21,23), LY(21,23), SI(4), mr, mg, mb);
tyre_mark(LX(9,23), LY(9,23), SI(4), ph, 1);
tyre_mark(LX(21,23), LY(21,23), SI(4), ph, 1);
line(sx, sy, LX(28,6), LY(28,6), 255, 80, 60);
break;
case UT_MEDIC:
fill_rect(sx-SI(10), sy-SI(5), SI(20), SI(15), ok, og, ob);
fill_rect(sx-SI(8), sy-SI(3), SI(16), SI(11), 245, 245, 245);
fill_rect(LX(7,10)-SI(1), LY(7,10)-SI(1), SI(6), SI(4), 200, 220, 240); /* cab glass */
disc(LX(10,24), LY(10,24), SI(4), mr, mg, mb);
disc(LX(20,24), LY(20,24), SI(4), mr, mg, mb);
tyre_mark(LX(10,24), LY(10,24), SI(4), ph, 1);
tyre_mark(LX(20,24), LY(20,24), SI(4), ph, 1);
disc(LX(15,9), LY(15,9), SI(5), 240, 50, 50);
fill_rect(LX(14,6)-1, LY(14,6)-SI(4), SI(3), SI(8), 255, 255, 255);
fill_rect(LX(11,8)-SI(4), LY(11,8)-1, SI(8), SI(3), 255, 255, 255);
break;
default:
disc(sx, sy, SI(10), ok, og, ob);
disc(sx, sy, SI(8), br, bg, bb);
break;
}
/* heading tick */
{
int hx = sx + (int)(c * 14 * sc), hy = sy + (int)(s * 14 * sc);
line(sx, sy, hx, hy, 250, 245, 210);
}
/* HP bar above unit (classic RTS strip) */
if (u->max_hp > 0) {
int bw = SI(28), by = sy - SI(18);
float f = u->hp / u->max_hp;
if (f < 0) f = 0; if (f > 1) f = 1;
fill_rect(sx - bw/2 - 1, by - 1, bw + 2, 6, 20, 18, 14);
fill_rect(sx - bw/2, by, bw, 4, 55, 48, 36);
uint8_t hr = (uint8_t)(255 * (1.0f - f)), hg = (uint8_t)(230 * f);
fill_rect(sx - bw/2, by, (int)(bw * f + 0.5f), 4, hr, hg, 40);
}
if (u->selected) {
int rad = SI((u->type == UT_TANK || u->type == UT_BEAST || u->type == UT_RAVAGER) ? 16 : 13);
ring(sx, sy, rad, 2, 255, 245, 60);
}
#undef LX
#undef LY
#undef SI
}
/* Building silhouettes: bright pad + faction fill, thin black outline. */
static void draw_bld(Building *b, int sx, int sy) {
int W = b->w * TILE, H = b->h * TILE;
uint8_t br = faction_r(b->faction), bg = faction_g(b->faction), bb = faction_b(b->faction);
uint8_t lr, lg, lb;
lighten3(br, bg, bb, 0.35f, &lr, &lg, &lb);
uint8_t ok = 22, og = 20, ob = 16;
uint8_t wall = 205, wall2 = 175;
/* Bright concrete pad on sand */
fill_rect(sx - 2, sy - 2, W + 4, H + 4, ok, og, ob);
fill_rect(sx, sy, W, H, 210, 185, 130);
fill_rect(sx + 2, sy + 2, W - 4, H - 4, wall2, wall2 - 18, wall2 - 45);
fill_rect(sx + 3, sy + 3, W - 6, H - 6, wall, wall - 22, wall - 55);
fill_rect(sx + 3, sy + 3, W - 6, 6, wall + 30, wall + 10, wall - 20);
switch (b->type) {
case BT_BASE:
/* multi-wing HQ: side wings + centre block + dome + mast */
fill_rect(sx + 4, sy + 18, 18, 28, ok, og, ob);
fill_rect(sx + 6, sy + 20, 14, 24, br, bg, bb);
fill_rect(sx + W - 22, sy + 18, 18, 28, ok, og, ob);
fill_rect(sx + W - 20, sy + 20, 14, 24, br, bg, bb);
fill_rect(sx + 16, sy + 10, W - 32, H - 20, ok, og, ob);
fill_rect(sx + 18, sy + 12, W - 36, H - 24, br, bg, bb);
fill_rect(sx + 20, sy + 14, W - 40, 8, lr, lg, lb); /* roof band */
disc(sx + W/2, sy + H/2 - 2, 12, ok, og, ob);
disc(sx + W/2, sy + H/2 - 2, 10, lr, lg, lb);
disc(sx + W/2, sy + H/2 - 2, 5, 255, 250, 180); /* command dome */
fill_rect(sx + W/2 - 2, sy + 4, 4, H/2 - 8, 200, 180, 60); /* mast */
fill_rect(sx + W/2 - 5, sy + 4, 10, 3, 220, 210, 80); /* antenna tip */
fill_rect(sx + W/2 - 6, sy + H - 14, 12, 6, 40, 38, 34); /* entry */
break;
case BT_WARFACTORY:
/* hangar shell + ridge vents + dark bay door */
fill_rect(sx + 4, sy + 6, W - 8, H - 12, ok, og, ob);
fill_rect(sx + 6, sy + 8, W - 12, H - 16, br, bg, bb);
fill_rect(sx + 8, sy + 10, W - 16, 10, lr, lg, lb); /* roof ridge */
fill_rect(sx + 10, sy + 12, 6, 6, 55, 50, 42); /* vent */
fill_rect(sx + W/2 - 3, sy + 12, 6, 6, 55, 50, 42);
fill_rect(sx + W - 16, sy + 12, 6, 6, 55, 50, 42);
fill_rect(sx + 10, sy + H - 28, W - 20, 18, ok, og, ob);
fill_rect(sx + 12, sy + H - 26, W - 24, 14, 35, 35, 40); /* bay */
line(sx + W/2, sy + H - 26, sx + W/2, sy + H - 12, 70, 68, 60);
fill_rect(sx + 8, sy + H - 10, W - 16, 4, 50, 48, 42); /* ramp lip */
break;
case BT_REFINERY:
/* twin silos + pipe/conveyor slab + hopper */
disc(sx + 18, sy + H/2 - 2, 14, ok, og, ob);
disc(sx + 18, sy + H/2 - 2, 12, 200, 165, 85);
ring(sx + 18, sy + H/2 - 2, 9, 2, 90, 70, 45);
disc(sx + W - 18, sy + H/2 - 2, 14, ok, og, ob);
disc(sx + W - 18, sy + H/2 - 2, 12, 190, 155, 75);
ring(sx + W - 18, sy + H/2 - 2, 9, 2, 90, 70, 45);
fill_rect(sx + 14, sy + H - 16, W - 28, 10, ok, og, ob);
fill_rect(sx + 16, sy + H - 14, W - 32, 6, 140, 110, 55); /* conveyor */
line(sx + 18, sy + H/2 + 8, sx + W - 18, sy + H - 12, 110, 90, 50);
line(sx + 18, sy + H/2 + 10, sx + W - 18, sy + H - 10, 110, 90, 50);
fill_rect(sx + W/2 - 6, sy + 8, 12, 14, ok, og, ob);
fill_rect(sx + W/2 - 4, sy + 10, 8, 10, br, bg, bb); /* hopper */
break;
case BT_POWER:
/* cooling towers + reactor core + steam cross */
disc(sx + W/2 - 6, sy + H/2 + 2, 8, ok, og, ob);
disc(sx + W/2 - 6, sy + H/2 + 2, 6, 100, 160, 210);
disc(sx + W/2 + 6, sy + H/2 + 2, 8, ok, og, ob);
disc(sx + W/2 + 6, sy + H/2 + 2, 6, 90, 150, 200);
fill_rect(sx + 6, sy + 6, W - 12, 10, ok, og, ob);
fill_rect(sx + 8, sy + 7, W - 16, 7, 70, 120, 170); /* reactor slab */
disc(sx + W/2, sy + 10, 4, 250, 252, 255);
line(sx + W/2, sy + 4, sx + W/2, sy + H - 4, 230, 245, 255);
line(sx + 4, sy + H/2, sx + W - 4, sy + H/2, 200, 230, 250);
fill_rect(sx + W/2 - 2, sy + H - 8, 4, 5, 55, 50, 42); /* base stub */
break;
case BT_TURRET:
/* bunker base + cupola + thick barrel */
fill_rect(sx + 3, sy + H - 12, W - 6, 10, ok, og, ob);
fill_rect(sx + 5, sy + H - 10, W - 10, 7, 55, 50, 42);
disc(sx + W/2, sy + H/2 + 1, W/3 + 2, ok, og, ob);
disc(sx + W/2, sy + H/2 + 1, W/3, br, bg, bb);
disc(sx + W/2, sy + H/2 + 1, W/5, lr, lg, lb);
disc(sx + W/2, sy + H/2 - 1, 3, 40, 38, 32); /* hatch */
line(sx + W/2, sy + H/2, sx + W/2 + 13, sy + H/2 - 12, 30, 28, 24);
line(sx + W/2 + 1, sy + H/2 + 1, sx + W/2 + 13, sy + H/2 - 11, 50, 45, 35);
disc(sx + W/2 + 12, sy + H/2 - 11, 2, 255, 200, 80);
break;
case BT_WALL:
/* battlement blocks + ridge + shadow seam */
fill_rect(sx + 1, sy + 1, W - 2, H - 2, ok, og, ob);
fill_rect(sx + 2, sy + 6, W - 4, H - 8, 175, 155, 120);
fill_rect(sx + 2, sy + 2, 7, 8, 195, 175, 140); /* merlon */
fill_rect(sx + 12, sy + 2, 7, 8, 195, 175, 140);
fill_rect(sx + 22, sy + 2, 7, 8, 195, 175, 140);
fill_rect(sx + 3, sy + 10, W - 6, 4, 210, 190, 150); /* ridge */
line(sx + 2, sy + H/2, sx + W - 2, sy + H/2, 60, 52, 42);
line(sx + W/2, sy + 6, sx + W/2, sy + H - 2, 70, 60, 48);
break;
case BT_REPAIR:
/* garage bay + crane arm + green service mark */
fill_rect(sx + 5, sy + 8, W - 10, H - 14, ok, og, ob);
fill_rect(sx + 7, sy + 10, W - 14, H - 18, br, bg, bb);
fill_rect(sx + 10, sy + H - 26, W - 20, 14, 40, 38, 34); /* bay floor */
fill_rect(sx + 12, sy + H - 24, W - 24, 10, 55, 52, 48);
fill_rect(sx + W - 18, sy + 8, 10, 6, ok, og, ob); /* crane mast */
fill_rect(sx + W - 16, sy + 10, 6, 22, 90, 85, 70);
line(sx + W - 13, sy + 12, sx + 14, sy + 18, 120, 110, 90); /* jib */
line(sx + W - 13, sy + 13, sx + 14, sy + 19, 100, 90, 70);
ring(sx + W/2 - 4, sy + H/2 + 2, 8, 2, 120, 220, 120);
line(sx + W/2 - 10, sy + H/2 + 2, sx + W/2 + 2, sy + H/2 + 2, 120, 220, 120);
line(sx + W/2 - 4, sy + H/2 - 4, sx + W/2 - 4, sy + H/2 + 8, 120, 220, 120);
break;
case BT_RESEARCH:
/* lab block + dish + instrument stubs */
fill_rect(sx + 6, sy + 14, W - 12, H - 20, ok, og, ob);
fill_rect(sx + 8, sy + 16, W - 16, H - 24, br, bg, bb);
fill_rect(sx + 10, sy + 18, W - 20, 8, lr, lg, lb);
disc(sx + W/2 + 6, sy + 14, 12, ok, og, ob);
disc(sx + W/2 + 6, sy + 14, 10, 210, 180, 80); /* dish */
ring(sx + W/2 + 6, sy + 14, 7, 2, 180, 150, 60);
disc(sx + W/2 + 6, sy + 14, 3, 255, 240, 160);
fill_rect(sx + 12, sy + H - 18, 6, 8, 230, 230, 160); /* stub */
fill_rect(sx + 22, sy + H - 20, 5, 10, 230, 230, 160);
line(sx + W/2 + 6, sy + 4, sx + W/2 + 6, sy + 14, 200, 190, 120);
break;
default:
break;
}
/* construction scaffold: yard posts/rails/braces (not flat white wash) */
if (b->build < 1.0f) {
int mid = sy + (int)(H * b->build);
if (mid > sy + 2)
fill_rect(sx + 2, sy + 2, W - 4, mid - sy - 2, 235, 232, 220);
line(sx, sy, sx, sy + H, 175, 145, 85);
line(sx + W - 1, sy, sx + W - 1, sy + H, 175, 145, 85);
line(sx, sy, sx + W, sy, 205, 175, 105);
line(sx, mid, sx + W, mid, 215, 185, 115);
line(sx, sy + H - 1, sx + W, sy + H - 1, 155, 125, 70);
line(sx + 2, sy + 2, sx + W - 2, sy + H - 2, 150, 120, 65);
line(sx + W - 2, sy + 2, sx + 2, sy + H - 2, 150, 120, 65);
if (W >= 48)
line(sx + W / 2, sy, sx + W / 2, sy + H, 165, 135, 75);
/* progress tick above pad */
fill_rect(sx, sy - 7, W, 4, 55, 50, 40);
fill_rect(sx, sy - 7, (int)(W * b->build + 0.5f), 4, 120, 220, 120);
}
/* battle damage: scorch patches + crack lines + rubble chips */
if (b->max_hp > 0 && b->hp < b->max_hp * 0.95f) {
float dmg = 1.0f - (b->hp / b->max_hp);
int pw = W / 4; if (pw < 3) pw = 3;
int ph = H / 5; if (ph < 2) ph = 2;
fill_rect(sx + W / 4, sy + H / 3, pw, ph, 45, 35, 25);
if (dmg > 0.3f)
fill_rect(sx + W / 2, sy + H / 2, pw, ph, 55, 40, 28);
line(sx + 3, sy + H / 4, sx + W / 2, sy + (H * 2) / 3, 28, 24, 18);
line(sx + W / 2, sy + (H * 2) / 3, sx + W - 3, sy + H / 3, 28, 24, 18);
if (dmg > 0.5f) {
line(sx + W / 3, sy + 2, sx + W / 3 + 3, sy + H - 2, 22, 18, 14);
fill_rect(sx + 2, sy + H - 4, 5, 3, 95, 78, 55);
fill_rect(sx + W - 9, sy + H - 5, 7, 4, 85, 70, 50);
}
/* thin HP bar so damage reads in KKND_SHOT */
{
int by = sy - 6;
float f = b->hp / b->max_hp;
fill_rect(sx - 1, by - 1, W + 2, 5, 20, 18, 14);
fill_rect(sx, by, W, 3, 55, 48, 36);
fill_rect(sx, by, (int)(W * f + 0.5f), 3, br, bg, 40);
}
}
}
void snapshot_capture(Game *g, const char *path) {
const int zoom = SHOT_ZOOM;
g_sw = SW / zoom;
g_sh = SH / zoom;
if (g_sw < 1) g_sw = 1;
if (g_sh < 1) g_sh = 1;
uint8_t *small = (uint8_t *)malloc((size_t)g_sw * g_sh * 4);
if (!small) return;
g_buf = small;
for (int i = 0; i < g_sw * g_sh; i++) {
small[i*4]=0; small[i*4+1]=0; small[i*4+2]=0; small[i*4+3]=255;
}
/* Lock camera on the expected raid locus (hx+88, hy-48). Mild bias toward
* living enemies still inside the clash radius — never mid-map garrison. */
float hx = 0, hy = 0;
{
int sx = g->map.spawn_x[g->human], sy = g->map.spawn_y[g->human];
map_tile_to_world(sx, sy, &hx, &hy);
}
float clash_x = hx + 88.0f, clash_y = hy - 48.0f;
const float clash_r2 = 220.0f * 220.0f;
float ex = 0, ey = 0;
int ne = 0;
for (int i = 0; i < g->unit_count; i++) {
Unit *u = &g->units[i];
if (u->dead || u->faction == g->human) continue;
float dx = u->x - clash_x, dy = u->y - clash_y;
if (dx * dx + dy * dy > clash_r2) continue;
ex += u->x; ey += u->y; ne++;
}
float ecx = ne ? ex / ne : clash_x;
float ecy = ne ? ey / ne : clash_y;
/* Clash + forward fight, with enough HQ so base buildings stay in frame. */
float cx = clash_x * 0.55f + ecx * 0.15f + hx * 0.30f;
float cy = clash_y * 0.55f + ecy * 0.15f + hy * 0.30f;
float camx = cx - g_sw / 2.0f, camy = cy - g_sh / 2.0f;
if (camx < 0) camx = 0; if (camy < 0) camy = 0;
if (camx > MAP_PIX_W - g_sw) camx = (float)(MAP_PIX_W - g_sw);
if (camy > MAP_PIX_H - g_sh) camy = (float)(MAP_PIX_H - g_sh);
if (camx < 0) camx = 0; if (camy < 0) camy = 0;
int in_u = 0, in_b = 0, alive_e = 0, alive_h = 0;
for (int i = 0; i < g->unit_count; i++) {
Unit *u = &g->units[i];
if (u->dead) continue;
if (u->faction == g->human) alive_h++; else alive_e++;
int sx = (int)(u->x - camx), sy = (int)(u->y - camy);
if (sx >= -16 && sy >= -16 && sx < g_sw + 16 && sy < g_sh + 16) in_u++;
}
for (int i = 0; i < g->bld_count; i++) {
Building *b = &g->buildings[i];
if (b->dead) continue;
int sx = (int)(b->tx * TILE - camx), sy = (int)(b->ty * TILE - camy);
int w = b->w * TILE, h = b->h * TILE;
if (sx + w >= 0 && sy + h >= 0 && sx < g_sw && sy < g_sh) in_b++;
}
engine_log("shot cam=(%.0f,%.0f) crop=%dx%d clash_enemies=%d "
"in_frame units=%d blds=%d alive H/E=%d/%d",
camx, camy, g_sw, g_sh, ne, in_u, in_b, alive_h, alive_e);
/* terrain */
int tx0 = (int)camx / TILE, ty0 = (int)camy / TILE;
int tx1 = (int)(camx + g_sw) / TILE, ty1 = (int)(camy + g_sh) / TILE;
for (int ty = ty0; ty <= ty1; ty++) {
for (int tx = tx0; tx <= tx1; tx++) {
if (tx < 0 || ty < 0 || tx >= MAP_W || ty >= MAP_H) continue;
Tile *t = map_tile(&g->map, tx, ty);
uint8_t r,gv,b; terrain_color(t->terrain, &r,&gv,&b);
/* Mild checker — keep yellow bias (nudge R/G, never push B). */
if (((tx + ty) & 1) == 0) {
if (r < 250) r += 8; if (gv < 240) gv += 4;
} else {
if (r > 10) r -= 4; if (gv > 10) gv -= 3; if (b > 6) b -= 2;
}
int sx = tx*TILE - (int)camx, sy = ty*TILE - (int)camy;
rectf(sx, sy, sx+TILE-1, sy+TILE-1, r, gv, b);
/* Crack / grit lines so desert reads as KKND2 dirt, not flat fill. */
if (t->terrain == T_GRASS || t->terrain == T_SCRAP) {
line(sx + 4, sy + 10, sx + 14, sy + 18, (uint8_t)(r - 28), (uint8_t)(gv - 24), (uint8_t)(b - 12));
line(sx + 18, sy + 6, sx + 28, sy + 14, (uint8_t)(r - 22), (uint8_t)(gv - 18), (uint8_t)(b - 10));
disc(sx + 9, sy + 22, 1, (uint8_t)(r - 35), (uint8_t)(gv - 30), (uint8_t)(b - 15));
disc(sx + 24, sy + 26, 1, (uint8_t)(r - 30), (uint8_t)(gv - 26), (uint8_t)(b - 12));
}
if (t->terrain == T_RUBBLE) {
disc(sx + 8, sy + 10, 3, 120, 95, 60);
disc(sx + 22, sy + 18, 3, 138, 110, 70);
disc(sx + 14, sy + 24, 2, 105, 85, 55);
disc(sx + 26, sy + 8, 2, 155, 125, 80);
}
if (t->terrain == T_ROAD) {
fill_rect(sx + 3, sy + TILE/2 - 3, TILE - 6, 6, 195, 165, 105);
line(sx + 2, sy + TILE/2, sx + TILE - 3, sy + TILE/2, 225, 195, 130);
line(sx + TILE/2, sy + 2, sx + TILE/2, sy + TILE - 3, 165, 140, 95);
}
if (t->terrain == T_SCRAP && t->scrap > 0) {
/* stacked scrap mounds (KKND2 resource piles) */
int cc = sx + TILE / 2, cr = sy + TILE / 2 + 2;
int n = 3 + t->scrap / 28;
if (n > 9) n = 9;
for (int k = 0; k < n; k++) {
int ox = ((k * 7) % 14) - 7;
int oy = ((k * 5) % 10) - 5 - (k / 3) * 2;
int rad = 4 + (k & 1) + (t->scrap > 120 ? 1 : 0);
disc(cc + ox, cr + oy, rad, 235, 190, 85);
disc(cc + ox - 1, cr + oy - rad / 2, rad - 1, 210, 165, 70);
disc(cc + ox + 1, cr + oy - rad + 1, (rad > 3) ? rad - 2 : 2,
255, 215, 120);
}
}
}
}
/* Showcase shot: force-clear fog in the camera crop so buildings/units
* read like a playable KKND2 battle view (not a black fog plate). */
for (int ty = ty0; ty <= ty1; ty++)
for (int tx = tx0; tx <= tx1; tx++) {
if (tx < 0 || ty < 0 || tx >= MAP_W || ty >= MAP_H) continue;
g->map.fog[ty * MAP_W + tx] = 2;
}
/* Soft edge only outside live vision (kept for minimap consistency). */
for (int ty = ty0; ty <= ty1; ty++)
for (int tx = tx0; tx <= tx1; tx++) {
if (tx < 0 || ty < 0 || tx >= MAP_W || ty >= MAP_H) continue;
int f = g->map.fog[ty*MAP_W + tx];
if (f >= 2) continue;
float keep = (f == 1) ? 0.70f : 0.45f;
int sx = tx*TILE - (int)camx, sy = ty*TILE - (int)camy;
for (int yy = sy; yy < sy + TILE; yy++)
for (int xx = sx; xx < sx + TILE; xx++)
darken_px(xx, yy, keep);
}
/* buildings */
for (int i = 0; i < g->bld_count; i++) {
Building *b = &g->buildings[i];
if (b->dead) continue;
int sx = (int)(b->tx*TILE - camx), sy = (int)(b->ty*TILE - camy);
int w = b->w*TILE, h = b->h*TILE;
if (sx+w < 0 || sy+h < 0 || sx > g_sw || sy > g_sh) continue;
draw_bld(b, sx, sy);
}
/* units */
for (int i = 0; i < g->unit_count; i++) {
Unit *u = &g->units[i];
if (u->dead) continue;
int sx = (int)(u->x - camx), sy = (int)(u->y - camy);
if (sx < -20 || sy < -20 || sx > g_sw+20 || sy > g_sh+20) continue;
draw_unit(u, sx, sy);
}
/* projectiles — bright tracers / shells with streak tails (KKND2-like) */
for (int i = 0; i < g->bullet_count; i++) {
Bullet *p = &g->bullets[i];
int sx = (int)(p->x - camx), sy = (int)(p->y - camy);
float dx = p->tx - p->x, dy = p->ty - p->y;
float d = sqrtf(dx * dx + dy * dy);
if (d > 1.0f) {
float ux = dx / d, uy = dy / d;
int len = (p->kind == 2) ? 24 : (p->kind == 3) ? 18
: (p->kind == 1) ? 14 : 10;
for (int t = 1; t <= len; t++) {
int tx = sx - (int)(ux * (float)t * 0.75f);
int ty = sy - (int)(uy * (float)t * 0.75f);
uint8_t fade = (uint8_t)(255 - t * 9);
if (p->kind == 2) {
/* laser beam: cyan→magenta streak */
disc(tx, ty, (t < 3) ? 2 : 1, fade, 80, 255);
px(tx, ty, 220, 180, 255);
} else if (p->kind == 3) {
disc(tx, ty, (t < 4) ? 2 : 1, 255, (uint8_t)(120 - t * 4), 30);
} else {
disc(tx, ty, 1, 255, (uint8_t)(200 - t * 8), 50);
}
}
}
int rad = (p->kind == 1 || p->kind == 3) ? 6 : 4;
disc(sx, sy, rad + 1, 255, 160, 40);
disc(sx, sy, rad, 255, 240, 140);
px(sx + 1, sy, 255, 255, 220);
if (p->kind == 3) {
disc(sx - 3, sy + 1, 3, 255, 70, 30);
disc(sx + 2, sy - 2, 2, 255, 200, 80);
} else if (p->kind == 2) {
disc(sx, sy, rad + 1, 120, 200, 255);
disc(sx, sy, rad - 1, 240, 220, 255);
}
}
/* particles (smoke / fire / sparks / debris) — larger combat FX */
for (int i = 0; i < g->particle_count; i++) {
Particle *p = &g->particles[i];
if (p->life <= 0) continue;
int sx = (int)(p->x - camx), sy = (int)(p->y - camy);
if (sx < -8 || sy < -8 || sx > g_sw + 8 || sy > g_sh + 8) continue;
float a = p->life / (p->max_life > 0 ? p->max_life : 1.0f);
if (a < 0.12f) continue;
int sz = (p->kind == 1) ? 5 : (p->kind == 3) ? 3 : (p->kind == 0) ? 4 : 3;
uint8_t r = (uint8_t)(p->r * a), gv = (uint8_t)(p->g * a), b = (uint8_t)(p->b * a);
if (p->kind == 0 || p->kind == 1) disc(sx, sy, sz + 1, r, gv, b);
else fill_rect(sx - sz/2, sy - sz/2, sz + 1, sz + 1, r, gv, b);
}
/* KKND2-like chrome: top resource bar + corner minimap + bottom command strip.
* Procedural bars (no font) so the shot still reads as classic RTS UI. */
{
Faction hf = g->human;
uint8_t hr = faction_r(hf), hg = faction_g(hf), hb = faction_b(hf);
Faction ef = enemy_of(hf);
uint8_t er = faction_r(ef), eg = faction_g(ef), eb = faction_b(ef);
int scrap_h = g->scrap[hf];
if (scrap_h < 0) scrap_h = 0;
int scrap_cap = 8000;
if (scrap_h > scrap_cap) scrap_cap = scrap_h + 500;
float scrap_frac = (float)scrap_h / (float)scrap_cap;
if (scrap_frac > 1.0f) scrap_frac = 1.0f;
int pwr_gen = 0, pwr_use = 0;
for (int i = 0; i < g->bld_count; i++) {
Building *b = &g->buildings[i];
if (b->dead || b->faction != hf || b->build < 1.0f) continue;
if (b->type == BT_POWER) pwr_gen += 100;
else if (b->type == BT_BASE) pwr_use += 20;
else if (b->type == BT_WARFACTORY || b->type == BT_REFINERY) pwr_use += 40;
else if (b->type == BT_RESEARCH || b->type == BT_REPAIR) pwr_use += 30;
else if (b->type == BT_TURRET) pwr_use += 15;
else pwr_use += 5;
}
if (pwr_gen < 1) pwr_gen = 1;
float pwr_frac = (float)pwr_gen / (float)(pwr_gen + pwr_use);
if (pwr_frac > 1.0f) pwr_frac = 1.0f;
int pwr_ok = (pwr_gen >= pwr_use);
int n_sel_u = 0, n_sel_b = 0;
for (int i = 0; i < g->sel_count; i++) {
if (unit_by_id(g, g->selection[i])) n_sel_u++;
else if (bld_by_id(g, g->selection[i])) n_sel_b++;
}
/* —— top bar —— */
int bar_h = 22;
fill_rect(0, 0, g_sw, bar_h, 22, 26, 32);
fill_rect(0, 0, g_sw, 2, 55, 62, 70);
fill_rect(0, bar_h - 1, g_sw, 1, 90, 110, 70);
fill_rect(0, bar_h, g_sw, 1, 40, 48, 40);
/* faction badge */
fill_rect(3, 3, 16, 16, 12, 14, 16);
fill_rect(5, 5, 12, 12, hr, hg, hb);
fill_rect(7, 7, 8, 8, (uint8_t)(hr / 2 + 40), (uint8_t)(hg / 2 + 40),
(uint8_t)(hb / 2 + 40));
/* scrap icon (gold nugget) + segmented bar */
fill_rect(24, 5, 8, 12, 40, 34, 18);
fill_rect(26, 7, 4, 8, 220, 190, 70);
disc(28, 11, 3, 255, 220, 90);
int scrap_x = 36, scrap_w = (int)(g_sw * 0.28f);
if (scrap_w < 60) scrap_w = 60;
fill_rect(scrap_x, 6, scrap_w, 10, 28, 26, 18);
fill_rect(scrap_x, 6, (int)(scrap_w * scrap_frac), 10, 230, 200, 70);
/* segment ticks */
for (int s = 1; s < 8; s++) {
int sx = scrap_x + (scrap_w * s) / 8;
line(sx, 6, sx, 15, 18, 16, 12);
}
fill_rect(scrap_x, 6, scrap_w, 1, 255, 240, 160);
fill_rect(scrap_x, 15, scrap_w, 1, 80, 60, 20);
/* power bolt + bar (green surplus / red deficit) */
int pwr_x = scrap_x + scrap_w + 10;
fill_rect(pwr_x, 5, 3, 5, 240, 240, 120);
fill_rect(pwr_x - 1, 9, 5, 3, 240, 240, 120);
fill_rect(pwr_x + 1, 11, 3, 5, 240, 240, 120);
int pwr_bx = pwr_x + 10, pwr_bw = (int)(g_sw * 0.16f);
if (pwr_bw < 40) pwr_bw = 40;
fill_rect(pwr_bx, 6, pwr_bw, 10, 24, 28, 24);
uint8_t pr = pwr_ok ? 70 : 220, pg = pwr_ok ? 210 : 70, pb = pwr_ok ? 90 : 60;
fill_rect(pwr_bx, 6, (int)(pwr_bw * pwr_frac), 10, pr, pg, pb);
for (int s = 1; s < 5; s++) {
int sx = pwr_bx + (pwr_bw * s) / 5;
line(sx, 6, sx, 15, 14, 16, 14);
}
/* unit / building count chips */
int chip_x = pwr_bx + pwr_bw + 12;
fill_rect(chip_x, 4, 34, 14, 30, 36, 30);
fill_rect(chip_x + 2, 6, 6, 10, hr, hg, hb); /* unit glyph */
disc(chip_x + 5, 8, 2, 255, 255, 255);
int ufill = alive_h;
if (ufill > 20) ufill = 20;
fill_rect(chip_x + 12, 8, ufill, 6, hr, hg, hb);
fill_rect(chip_x + 38, 4, 34, 14, 30, 36, 30);
fill_rect(chip_x + 40, 7, 8, 8, hr, hg, hb); /* bld glyph */
int bfill = 0;
for (int i = 0; i < g->bld_count; i++)
if (!g->buildings[i].dead && g->buildings[i].faction == hf) bfill++;
if (bfill > 20) bfill = 20;
fill_rect(chip_x + 52, 8, bfill, 6, 180, 160, 70);
/* enemy threat pip */
fill_rect(g_sw - 48, 4, 42, 14, 36, 22, 28);
fill_rect(g_sw - 44, 6, 8, 10, er, eg, eb);
int eth = alive_e;
if (eth > 28) eth = 28;
fill_rect(g_sw - 34, 8, eth, 6, er, eg, eb);
disc(g_sw - 10, 11, 4, 255, 90, 40);
disc(g_sw - 10, 11, 2, 255, 220, 120);
/* —— corner minimap (compact, fight-plate friendly) —— */
int mw = 72, mh = 72;
int mx = g_sw - mw - 6, my = bar_h + 6;
fill_rect(mx - 2, my - 2, mw + 4, mh + 4, 8, 10, 12);
fill_rect(mx - 1, my - 1, mw + 2, mh + 2, 70, 90, 70);
fill_rect(mx, my, mw, mh, 28, 32, 28);
/* sample terrain around camera */
float map_sx = (float)mw / (float)g_sw;
float map_sy = (float)mh / (float)g_sh;
for (int py = 0; py < mh; py += 2) {
for (int px_ = 0; px_ < mw; px_ += 2) {
float wx = camx + (float)px_ / map_sx;
float wy = camy + (float)py / map_sy;
int tx = (int)(wx / TILE), ty = (int)(wy / TILE);
if (!map_in_bounds(&g->map, tx, ty)) continue;
Tile *t = map_tile(&g->map, tx, ty);
uint8_t cr = 120, cg = 104, cb = 78;
switch (t->terrain) {
case T_WATER: cr = 44; cg = 78; cb = 120; break;
case T_ROCK: cr = 110; cg = 104; cb = 98; break;
case T_SCRAP: cr = 200; cg = 170; cb = 70; break;
case T_GRASS: cr = 210; cg = 170; cb = 78; break;
case T_RUBBLE: cr = 90; cg = 78; cb = 60; break;
default: break;
}
fill_rect(mx + px_, my + py, 2, 2, cr, cg, cb);
}
}
for (int i = 0; i < g->bld_count; i++) {
Building *b = &g->buildings[i];
if (b->dead) continue;
int bx = mx + (int)((b->tx * TILE - camx) * map_sx);
int by = my + (int)((b->ty * TILE - camy) * map_sy);
if (bx < mx || by < my || bx >= mx + mw || by >= my + mh) continue;
uint8_t br = faction_r(b->faction), bg = faction_g(b->faction),
bb = faction_b(b->faction);
fill_rect(bx, by, 3, 3, br, bg, bb);
}
for (int i = 0; i < g->unit_count; i++) {
Unit *u = &g->units[i];
if (u->dead) continue;
int ux = mx + (int)((u->x - camx) * map_sx);
int uy = my + (int)((u->y - camy) * map_sy);
if (ux < mx || uy < my || ux >= mx + mw || uy >= my + mh) continue;
px(ux, uy, faction_r(u->faction), faction_g(u->faction),
faction_b(u->faction));
}
/* viewport outline = full minimap (crop view) */
line(mx, my, mx + mw - 1, my, 255, 255, 255);
line(mx, my + mh - 1, mx + mw - 1, my + mh - 1, 255, 255, 255);
line(mx, my, mx, my + mh - 1, 255, 255, 255);
line(mx + mw - 1, my, mx + mw - 1, my + mh - 1, 255, 255, 255);
/* —— bottom command / selection strip —— */
int bot_h = 28;
int bot_y = g_sh - bot_h;
fill_rect(0, bot_y, g_sw, bot_h, 18, 20, 24);
fill_rect(0, bot_y, g_sw, 1, 70, 90, 70);
fill_rect(0, bot_y + 1, g_sw, 1, 40, 48, 40);
/* portrait panel */
fill_rect(4, bot_y + 3, 22, 22, 12, 14, 16);
fill_rect(6, bot_y + 5, 18, 18, hr, hg, hb);
disc(15, bot_y + 12, 4, 255, 255, 255);
fill_rect(13, bot_y + 16, 4, 5, 40, 40, 40);
/* selection count bars */
int slots = (n_sel_u > 0 || n_sel_b > 0) ? (n_sel_u + n_sel_b) : 6;
if (slots > 10) slots = 10;
for (int s = 0; s < slots; s++) {
int sx = 32 + s * 22;
fill_rect(sx, bot_y + 4, 18, 20, 28, 32, 28);
fill_rect(sx + 1, bot_y + 5, 16, 18, 40, 48, 40);
if (s < n_sel_u || (n_sel_u == 0 && n_sel_b == 0 && s < 4)) {
fill_rect(sx + 4, bot_y + 8, 10, 12, hr, hg, hb);
disc(sx + 9, bot_y + 11, 2, 255, 255, 255);
} else if (s < n_sel_u + n_sel_b) {
fill_rect(sx + 3, bot_y + 8, 12, 12, hr, hg, hb);
}
}
/* ability / build button stubs (right) */
for (int s = 0; s < 4; s++) {
int sx = g_sw - 96 + s * 22;
fill_rect(sx, bot_y + 4, 18, 20, 32, 28, 22);
fill_rect(sx + 2, bot_y + 6, 14, 16, 70, 60, 40);
if (s == 0) fill_rect(sx + 6, bot_y + 10, 6, 8, 200, 180, 60); /* build */
if (s == 1) disc(sx + 9, bot_y + 14, 4, 255, 80, 40); /* attack */
if (s == 2) fill_rect(sx + 5, bot_y + 11, 8, 6, 80, 180, 255); /* move */
if (s == 3) ring(sx + 9, bot_y + 14, 5, 2, 180, 255, 120); /* stop */
}
}
/* Nearest-neighbour upscale → full VIEW (chunky KKND pixels) */
uint8_t *big = (uint8_t *)malloc((size_t)SW * SH * 4);
if (!big) { free(small); return; }
for (int y = 0; y < SH; y++) {
int sy = y / zoom;
if (sy >= g_sh) sy = g_sh - 1;
for (int x = 0; x < SW; x++) {
int sx = x / zoom;
if (sx >= g_sw) sx = g_sw - 1;
int di = (y * SW + x) * 4;
int si = (sy * g_sw + sx) * 4;
big[di]=small[si]; big[di+1]=small[si+1];
big[di+2]=small[si+2]; big[di+3]=255;
}
}
png_save(path, SW, SH, big);
free(big);
free(small);
g_buf = NULL;
g_sw = SW; g_sh = SH;
}