OpenKKnD/engine-c/src/snapshot.c
local-model/engine-L2-grok-4.5 57f95fe3a0
engine-c: dense scrap oil-field + latticed derrick BT_REFINERY near clash
Forward HQ REFINERY + harvesters; denser clash scrap flecks; derrick/gantry/hopper silhouette in snapshot+sprites.
2026-09-19 16:53:12 +02:00

1184 lines
57 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/visor → pack → thick rifle (KKND2 foot soldier) */
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(9), ok, og, ob);
disc(sx, sy, SI(7), br, bg, bb);
disc(LX(10,16), LY(10,16), SI(3), ok, og, ob); /* backpack */
disc(LX(10,16), LY(10,16), SI(2), 50, 46, 40);
disc(LX(15,8), LY(15,8), SI(5), ok, og, ob);
disc(LX(15,8), LY(15,8), SI(4), lr, lg, lb); /* helmet */
fill_rect(LX(13,8)-SI(1), LY(13,8)-1, SI(5), SI(2), 30, 40, 50); /* visor */
line(LX(16,14), LY(16,14), LX(16,0), LY(16,0), 40, 36, 30);
line(LX(17,14), LY(17,14), LX(17,1), LY(17,1), 55, 50, 40);
line(LX(18,14), LY(18,14), LX(18,2), LY(18,2), 45, 40, 34);
disc(LX(17,1), LY(17,1), SI(2), 60, 55, 45); /* muzzle tip */
break;
case UT_BUGGY:
/* cabin + windshield + chassis + hubbed wheels + thick MG */
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(5), ok, og, ob);
disc(LX(22,7), LY(22,7), SI(5), ok, og, ob);
disc(LX(8,23), LY(8,23), SI(5), ok, og, ob);
disc(LX(22,23), LY(22,23), SI(5), ok, og, ob);
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);
disc(LX(8,7), LY(8,7), SI(2), 90, 85, 75); /* hub */
disc(LX(22,7), LY(22,7), SI(2), 90, 85, 75);
disc(LX(8,23), LY(8,23), SI(2), 90, 85, 75);
disc(LX(22,23), LY(22,23), SI(2), 90, 85, 75);
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,14), LY(29,14), 40, 36, 30);
line(sx, sy, LX(29,15), LY(29,15), 50, 45, 35);
line(sx, sy, LX(29,16), LY(29,16), 40, 36, 30);
disc(LX(29,15), LY(29,15), SI(2), 60, 55, 45);
break;
case UT_TANK:
/* side tracks + sprockets + 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);
disc(LX(5,11), LY(5,11), SI(2), 40, 38, 32); /* sprocket */
disc(LX(5,20), LY(5,20), SI(2), 40, 38, 32);
disc(LX(26,11), LY(26,11), SI(2), 40, 38, 32);
disc(LX(26,20), LY(26,20), SI(2), 40, 38, 32);
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(7), ok, og, ob);
disc(sx, sy, SI(6), lr, lg, lb); /* cupola */
disc(LX(15,12), LY(15,12), SI(2), 40, 38, 32); /* hatch */
line(LX(15,13), LY(15,13), LX(29,13), LY(29,13), 45, 40, 32);
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), 55, 50, 40);
line(LX(15,16), LY(15,16), LX(29,16), LY(29,16), 50, 45, 35);
line(LX(15,17), LY(15,17), LX(28,17), LY(28,17), 45, 40, 32);
disc(LX(29,15), LY(29,15), SI(2), 60, 55, 45); /* muzzle */
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 thick barrel tube + muzzle brake */
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(6), ok, og, ob);
disc(sx, sy, SI(5), lr, lg, lb);
line(LX(15,13), LY(15,13), LX(4,2), LY(4,2), 45, 40, 32);
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);
line(LX(15,17), LY(15,17), LX(5,5), LY(5,5), 45, 40, 32);
disc(LX(4,3), LY(4,3), SI(3), 60, 55, 45); /* muzzle */
disc(LX(4,3), LY(4,3), SI(1), 30, 28, 24);
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_HARVESTER: {
/* cabin + scoop lip + cargo bed (gold by cargo) + hubbed dual 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); /* glass */
fill_rect(LX(2,14)-SI(1), LY(2,14)-SI(1), SI(4), SI(5), 90, 80, 55); /* scoop */
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);
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);
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(5), ok, og, ob);
disc(LX(22,23), LY(22,23), SI(5), ok, og, ob);
disc(LX(8,23), LY(8,23), SI(4), mr, mg, mb);
disc(LX(22,23), LY(22,23), SI(4), mr, mg, mb);
disc(LX(8,23), LY(8,23), SI(2), 90, 85, 75);
disc(LX(22,23), LY(22,23), SI(2), 90, 85, 75);
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);
line(LX(6,7), LY(6,7), LX(6,3), LY(6,3), 50, 46, 40);
break;
}
case UT_JEEP:
/* open cabin + thick roll cage + hubbed wheels + twin 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(5), ok, og, ob);
disc(LX(21,22), LY(21,22), SI(5), ok, og, ob);
disc(LX(9,22), LY(9,22), SI(4), mr, mg, mb);
disc(LX(21,22), LY(21,22), SI(4), mr, mg, mb);
disc(LX(9,22), LY(9,22), SI(2), 90, 85, 75);
disc(LX(21,22), LY(21,22), SI(2), 90, 85, 75);
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(12,11), LY(12,11), LX(12,3), LY(12,3), 50, 46, 40);
line(LX(19,11), LY(19,11), LX(19,3), LY(19,3), lr, lg, lb);
line(LX(18,11), LY(18,11), LX(18,3), LY(18,3), 50, 46, 40);
line(LX(11,3), LY(11,3), LX(19,3), LY(19,3), lr, lg, lb);
line(LX(11,4), LY(11,4), LX(19,4), LY(19,4), 50, 46, 40);
line(sx, sy, LX(28,14), LY(28,14), 40, 36, 30);
line(sx, sy, LX(28,15), LY(28,15), 50, 45, 35);
line(sx, sy, LX(28,16), LY(28,16), 40, 36, 30);
disc(LX(28,15), LY(28,15), SI(2), 60, 55, 45);
break;
case UT_MUTANT:
/* hunched bulk + brow ridge + thick spikes + claw hand */
disc(sx, sy+SI(2), SI(11), ok, og, ob);
disc(sx, sy, SI(9), br, bg, bb);
disc(LX(15,9), LY(15,9), SI(5), ok, og, ob);
disc(LX(15,9), LY(15,9), SI(4), lr, lg, lb);
fill_rect(LX(12,8)-SI(1), LY(12,8)-1, SI(7), SI(2), 30, 40, 50); /* brow */
line(LX(12,6), LY(12,6), LX(9,0), LY(9,0), 40, 36, 30);
line(LX(13,6), LY(13,6), LX(10,1), LY(10,1), 55, 50, 40);
line(LX(15,5), LY(15,5), LX(15,0), LY(15,0), 40, 36, 30);
line(LX(16,5), LY(16,5), LX(16,0), LY(16,0), 55, 50, 40);
line(LX(18,6), LY(18,6), LX(21,0), LY(21,0), 40, 36, 30);
line(LX(17,6), LY(17,6), LX(20,1), LY(20,1), 55, 50, 40);
line(sx, sy+SI(1), LX(26,8), LY(26,8), 40, 36, 30);
line(sx, sy+SI(2), LX(26,9), LY(26,9), 50, 45, 35);
line(LX(24,9), LY(24,9), LX(29,5), LY(29,5), 55, 50, 40);
line(LX(24,9), LY(24,9), LX(29,11), LY(29,11), 55, 50, 40);
line(LX(24,10), LY(24,10), LX(28,8), LY(28,8), 70, 60, 45);
disc(LX(28,8), LY(28,8), SI(2), 70, 60, 45); /* claw tip */
disc(LX(10,22), LY(10,22), SI(4), ok, og, ob);
disc(LX(20,22), LY(20,22), SI(4), ok, og, ob);
disc(LX(10,22), LY(10,22), SI(3), br, bg, bb);
disc(LX(20,22), LY(20,22), SI(3), br, bg, bb);
break;
case UT_BEAST:
/* armored carapace + thick snout/tusks + hubbed 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);
fill_rect(sx-SI(6), sy-SI(5), SI(12), SI(3), lr, lg, lb); /* carapace ridge */
disc(LX(9,21), LY(9,21), SI(4), ok, og, ob);
disc(LX(14,22), LY(14,22), SI(4), ok, og, ob);
disc(LX(19,21), LY(19,21), SI(4), ok, og, ob);
disc(LX(23,22), LY(23,22), SI(4), ok, og, ob);
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(6), ok, og, ob);
disc(LX(24,12), LY(24,12), SI(5), lr, lg, lb); /* snout */
line(LX(26,10), LY(26,10), LX(29,8), LY(29,8), 40, 36, 30); /* tusk */
line(LX(26,14), LY(26,14), LX(29,16), LY(29,16), 40, 36, 30);
line(LX(26,12), LY(26,12), LX(29,12), LY(29,12), 55, 50, 40);
disc(LX(29,12), LY(29,12), SI(2), 60, 55, 45);
break;
case UT_RAVAGER:
/* heavy skirts + sprockets + cupola hatch + thick multi-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(LX(2,11)-SI(1), LY(2,11)-SI(1), SI(3), SI(10), mr, mg, mb);
fill_rect(LX(27,11)-SI(1), LY(27,11)-SI(1), SI(3), SI(10), mr, mg, mb);
disc(LX(3,12), LY(3,12), SI(2), 40, 38, 32);
disc(LX(3,20), LY(3,20), SI(2), 40, 38, 32);
disc(LX(28,12), LY(28,12), SI(2), 40, 38, 32);
disc(LX(28,20), LY(28,20), SI(2), 40, 38, 32);
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(8), ok, og, ob);
disc(sx, sy, SI(7), lr, lg, lb);
disc(LX(15,12), LY(15,12), SI(2), 40, 38, 32); /* hatch */
line(LX(15,13), LY(15,13), LX(29,13), LY(29,13), 45, 40, 32);
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), 55, 50, 40);
line(LX(15,16), LY(15,16), LX(29,16), LY(29,16), 50, 45, 35);
line(LX(15,17), LY(15,17), LX(28,17), LY(28,17), 45, 40, 32);
disc(LX(29,15), LY(29,15), SI(2), 60, 55, 45);
disc(LX(7,23), LY(7,23), SI(5), ok, og, ob);
disc(LX(23,23), LY(23,23), SI(5), ok, og, ob);
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:
/* fuel pack valves + thick nozzle jet + helmet */
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(5), ok, og, ob);
disc(LX(15,9), LY(15,9), SI(4), lr, lg, lb);
fill_rect(LX(13,9)-SI(1), LY(13,9)-1, SI(5), SI(2), 30, 40, 50); /* visor */
disc(LX(8,14), LY(8,14), SI(6), ok, og, ob); /* fuel tank */
disc(LX(8,14), LY(8,14), SI(5), 90, 70, 40);
disc(LX(8,14), LY(8,14), SI(3), 200, 120, 40);
disc(LX(8,12), LY(8,12), SI(1), 60, 55, 45); /* valve */
disc(LX(8,16), LY(8,16), SI(1), 60, 55, 45);
line(LX(18,14), LY(18,14), LX(28,19), LY(28,19), 255, 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);
line(LX(18,17), LY(18,17), LX(26,21), LY(26,21), 255, 200, 80);
disc(LX(28,20), LY(28,20), SI(3), 255, 200, 80);
disc(LX(28,20), LY(28,20), SI(1), 255, 240, 160);
break;
case UT_MISSILE:
/* dual tube pods + rail tips + hubbed 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,7)-SI(1), LY(12,7)-SI(2), SI(10), SI(5), ok, og, ob);
fill_rect(LX(13,6)-SI(1), LY(13,6)-SI(1), SI(8), SI(3), 255, 80, 60);
fill_rect(LX(12,13)-SI(1), LY(12,13)-SI(2), SI(10), SI(5), ok, og, ob);
fill_rect(LX(13,12)-SI(1), LY(13,12)-SI(1), SI(8), SI(3), 255, 100, 70);
disc(LX(21,7), LY(21,7), SI(2), 255, 120, 80); /* nose cones */
disc(LX(21,13), LY(21,13), SI(2), 255, 140, 90);
disc(LX(9,23), LY(9,23), SI(5), ok, og, ob);
disc(LX(21,23), LY(21,23), SI(5), ok, og, ob);
disc(LX(9,23), LY(9,23), SI(4), mr, mg, mb);
disc(LX(21,23), LY(21,23), SI(4), mr, mg, mb);
disc(LX(9,23), LY(9,23), SI(2), 90, 85, 75);
disc(LX(21,23), LY(21,23), SI(2), 90, 85, 75);
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,5), LY(28,5), 255, 80, 60);
line(sx, sy, LX(28,6), LY(28,6), 255, 100, 70);
disc(LX(28,6), LY(28,6), SI(2), 255, 140, 90);
break;
case UT_MEDIC:
/* white van + thick red cross + hubbed wheels + light bar */
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);
fill_rect(LX(12,6)-SI(2), LY(12,6)-1, SI(8), SI(2), 240, 50, 50); /* light bar */
disc(LX(10,24), LY(10,24), SI(5), ok, og, ob);
disc(LX(20,24), LY(20,24), SI(5), ok, og, ob);
disc(LX(10,24), LY(10,24), SI(4), mr, mg, mb);
disc(LX(20,24), LY(20,24), SI(4), mr, mg, mb);
disc(LX(10,24), LY(10,24), SI(2), 90, 85, 75);
disc(LX(20,24), LY(20,24), SI(2), 90, 85, 75);
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(6), 240, 50, 50);
fill_rect(LX(14,5)-1, LY(14,5)-SI(4), SI(3), SI(9), 255, 255, 255);
fill_rect(LX(11,8)-SI(4), LY(11,8)-1, SI(9), 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
}
/* Charred wreck silhouettes — dim faction paint + scorch, type-aware hull. */
static void draw_wreck(Wreck *w, int sx, int sy) {
uint8_t br = faction_r(w->faction), bg = faction_g(w->faction), bb = faction_b(w->faction);
uint8_t dr, dg, db;
darken3(br, bg, bb, 0.38f, &dr, &dg, &db);
uint8_t ok = 28, og = 24, ob = 20;
uint8_t ch = 48, cg = 42, cb = 36; /* char */
float c = cosf(w->rot), s = sinf(w->rot);
const float sc = SHOT_UNIT_SCALE * 0.92f;
#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))
disc(sx + SI(2), sy + SI(4), SI(10), 42, 36, 28); /* scorch stain */
switch (w->type) {
case UT_HARVESTER:
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), dr, dg, db);
fill_rect(LX(6, 12) - SI(1), LY(6, 12) - SI(1), SI(10), SI(8), ch, cg, cb);
disc(LX(8, 8), LY(8, 8), SI(4), ok, og, ob);
disc(LX(22, 8), LY(22, 8), SI(4), ok, og, ob);
disc(LX(8, 22), LY(8, 22), SI(4), ok, og, ob);
disc(LX(22, 22), LY(22, 22), SI(4), ok, og, ob);
fill_rect(sx - SI(4), sy - SI(2), SI(10), SI(5), 55, 48, 40); /* torn bed */
break;
case UT_TANK:
case UT_RAVAGER:
case UT_BEAST:
fill_rect(sx - SI(13), sy - SI(8), SI(26), SI(16), ok, og, ob);
fill_rect(sx - SI(11), sy - SI(6), SI(22), SI(12), dr, dg, db);
fill_rect(sx - SI(12), sy - SI(3), SI(4), SI(8), ch, cg, cb); /* track */
fill_rect(sx + SI(8), sy - SI(3), SI(4), SI(8), ch, cg, cb);
disc(sx, sy - SI(2), SI(6), ok, og, ob);
disc(sx, sy - SI(2), SI(4), ch, cg, cb); /* dead turret */
line(LX(15, 8), LY(15, 8), LX(15, -2), LY(15, -2), 40, 36, 30); /* bent barrel */
break;
case UT_ARTILLERY:
case UT_MISSILE:
fill_rect(sx - SI(12), sy - SI(7), SI(24), SI(14), ok, og, ob);
fill_rect(sx - SI(10), sy - SI(5), SI(20), SI(10), dr, dg, db);
disc(LX(8, 20), LY(8, 20), SI(4), ok, og, ob);
disc(LX(22, 20), LY(22, 20), SI(4), ok, og, ob);
line(LX(12, 12), LY(12, 12), LX(22, 2), LY(22, 2), ch, cg, cb);
break;
case UT_BUGGY:
case UT_JEEP:
case UT_FLAME:
case UT_MEDIC:
fill_rect(sx - SI(11), sy - SI(7), SI(22), SI(14), ok, og, ob);
fill_rect(sx - SI(9), sy - SI(5), SI(18), SI(10), dr, dg, db);
disc(LX(8, 8), LY(8, 8), SI(4), ok, og, ob);
disc(LX(22, 8), LY(22, 8), SI(4), ok, og, ob);
disc(LX(8, 22), LY(8, 22), SI(4), ok, og, ob);
disc(LX(22, 22), LY(22, 22), SI(4), ok, og, ob);
fill_rect(LX(10, 10) - SI(1), LY(10, 10) - SI(1), SI(8), SI(5), ch, cg, cb);
break;
case UT_INFANTRY:
case UT_MUTANT:
disc(sx + SI(1), sy + SI(2), SI(6), ok, og, ob);
disc(sx, sy, SI(5), dr, dg, db);
disc(sx - SI(3), sy + SI(4), SI(3), ch, cg, cb); /* crumpled kit */
disc(sx + SI(4), sy + SI(3), SI(2), 40, 36, 30);
break;
default:
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), dr, dg, db);
disc(sx - SI(5), sy + SI(2), SI(3), ch, cg, cb);
disc(sx + SI(6), sy - SI(1), SI(3), ch, cg, cb);
break;
}
/* smoke puff cue */
disc(sx + SI(3), sy - SI(8), SI(3), 70, 68, 64);
disc(sx + SI(5), sy - SI(12), SI(2), 90, 88, 82);
#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:
if (b->faction == FACTION_EVOLVED) {
/* organic lobes + dark central pit (mutant hive) */
disc(sx + 14, sy + 18, 14, ok, og, ob);
disc(sx + 14, sy + 18, 12, br, bg, bb);
disc(sx + W - 14, sy + 18, 14, ok, og, ob);
disc(sx + W - 14, sy + 18, 12, br, bg, bb);
disc(sx + 16, sy + H - 16, 12, ok, og, ob);
disc(sx + 16, sy + H - 16, 10, br, bg, bb);
disc(sx + W - 16, sy + H - 16, 12, ok, og, ob);
disc(sx + W - 16, sy + H - 16, 10, br, bg, bb);
disc(sx + W/2, sy + H/2, 16, ok, og, ob);
disc(sx + W/2, sy + H/2, 14, br, bg, bb);
disc(sx + W/2, sy + H/2, 8, 28, 18, 22); /* dark pit */
disc(sx + W/2, sy + H/2, 4, 60, 20, 30);
disc(sx + W/2 - 10, sy + 10, 5, lr, lg, lb); /* blister */
disc(sx + W/2 + 12, sy + H - 12, 4, lr, lg, lb);
} else {
/* Survivors: lime body + corner towers + central mast */
fill_rect(sx + 4, sy + 4, 14, 14, ok, og, ob); /* NW tower */
fill_rect(sx + 6, sy + 6, 10, 10, br, bg, bb);
fill_rect(sx + W - 18, sy + 4, 14, 14, ok, og, ob); /* NE */
fill_rect(sx + W - 16, sy + 6, 10, 10, br, bg, bb);
fill_rect(sx + 4, sy + H - 18, 14, 14, ok, og, ob); /* SW */
fill_rect(sx + 6, sy + H - 16, 10, 10, br, bg, bb);
fill_rect(sx + W - 18, sy + H - 18, 14, 14, ok, og, ob); /* SE */
fill_rect(sx + W - 16, sy + H - 16, 10, 10, br, bg, bb);
fill_rect(sx + 14, sy + 14, W - 28, H - 28, ok, og, ob);
fill_rect(sx + 16, sy + 16, W - 32, H - 32, br, bg, bb);
fill_rect(sx + 18, sy + 18, W - 36, 8, lr, lg, lb); /* roof band */
disc(sx + W/2, sy + H/2 - 2, 10, ok, og, ob);
disc(sx + W/2, sy + H/2 - 2, 8, lr, lg, lb);
disc(sx + W/2, sy + H/2 - 2, 4, 255, 250, 180); /* command dome */
fill_rect(sx + W/2 - 2, sy + 2, 4, H/2 - 6, 200, 180, 60); /* mast */
fill_rect(sx + W/2 - 6, sy + 2, 12, 3, 220, 210, 80); /* antenna */
fill_rect(sx + W/2 - 6, sy + H - 12, 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:
/* latticed derrick tower + gantry + hopper + scrap tank (KKND oil-field read) */
fill_rect(sx + 6, sy + H - 14, W - 12, 10, ok, og, ob); /* slab */
fill_rect(sx + 8, sy + H - 12, W - 16, 6, 70, 62, 48);
/* derrick mast + lattice */
fill_rect(sx + W/2 - 3, sy + 4, 6, H - 18, ok, og, ob);
fill_rect(sx + W/2 - 2, sy + 5, 4, H - 20, 95, 85, 65);
line(sx + W/2 - 6, sy + 10, sx + W/2 + 6, sy + 22, 55, 48, 38);
line(sx + W/2 + 6, sy + 10, sx + W/2 - 6, sy + 22, 55, 48, 38);
line(sx + W/2 - 6, sy + 24, sx + W/2 + 6, sy + 36, 55, 48, 38);
line(sx + W/2 + 6, sy + 24, sx + W/2 - 6, sy + 36, 55, 48, 38);
line(sx + W/2 - 5, sy + 8, sx + W/2 - 5, sy + H - 16, 40, 36, 30);
line(sx + W/2 + 5, sy + 8, sx + W/2 + 5, sy + H - 16, 40, 36, 30);
fill_rect(sx + W/2 - 7, sy + 2, 14, 5, ok, og, ob); /* crown */
fill_rect(sx + W/2 - 5, sy + 3, 10, 3, 200, 170, 80); /* beacon */
/* gantry arm toward hopper */
fill_rect(sx + W/2 + 2, sy + 14, W/2 - 10, 4, ok, og, ob);
fill_rect(sx + W/2 + 3, sy + 15, W/2 - 12, 2, 120, 100, 60);
line(sx + W/2 + 4, sy + 18, sx + W - 12, sy + H - 22, 90, 75, 50);
/* hopper / processing shed */
fill_rect(sx + W - 22, sy + H - 30, 16, 16, ok, og, ob);
fill_rect(sx + W - 20, sy + H - 28, 12, 12, br, bg, bb);
fill_rect(sx + W - 18, sy + H - 26, 8, 6, 140, 110, 55);
fill_rect(sx + W - 16, sy + H - 18, 4, 4, 40, 36, 30); /* chute */
/* scrap tank (left) */
disc(sx + 14, sy + H - 20, 10, ok, og, ob);
disc(sx + 14, sy + H - 20, 8, 200, 165, 85);
ring(sx + 14, sy + H - 20, 5, 2, 90, 70, 45);
line(sx + 20, sy + H - 18, sx + W/2 - 4, sy + H - 14, 110, 90, 50);
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; stronger hx so Survivor forward HQ stays in crop. */
float cx = clash_x * 0.40f + ecx * 0.10f + hx * 0.50f;
float cy = clash_y * 0.40f + ecy * 0.10f + hy * 0.50f;
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);
}
/* wrecks under living units (battlefield litter) */
for (int i = 0; i < g->wreck_count; i++) {
Wreck *w = &g->wrecks[i];
int sx = (int)(w->x - camx), sy = (int)(w->y - camy);
if (sx < -24 || sy < -24 || sx > g_sw + 24 || sy > g_sh + 24) continue;
draw_wreck(w, 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) ? 32 : (p->kind == 3) ? 24
: (p->kind == 1) ? 20 : 14;
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) ? 8 : 5;
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, 4, 255, 70, 30);
disc(sx + 2, sy - 2, 3, 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 < -12 || sy < -12 || sx > g_sw + 12 || sy > g_sh + 12) continue;
float a = p->life / (p->max_life > 0 ? p->max_life : 1.0f);
if (a < 0.10f) continue;
int sz = (p->kind == 1) ? 9 : (p->kind == 3) ? 5 : (p->kind == 0) ? 7 : 4;
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 + 2, r / 2, gv / 2, b / 2);
disc(sx, sy, sz + 1, r, gv, b);
if (p->kind == 1 && a > 0.45f)
disc(sx, sy - 1, sz / 2 + 1, 255, (uint8_t)(220 * a), 80);
} 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;
}