OpenKKnD/engine-c/src/snapshot.c

1524 lines
78 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:
/* boots/knees → torso/belt/pouches → pads → pack → helmet/visor/antenna → rifle+bayonet */
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);
fill_rect(LX(10,24)-SI(1), LY(10,24)-1, SI(3), SI(2), 40, 36, 30); /* boot L */
fill_rect(LX(18,24)-SI(1), LY(18,24)-1, SI(3), SI(2), 40, 36, 30); /* boot R */
fill_rect(LX(10,20)-SI(1), LY(10,20)-1, SI(3), SI(2), 55, 50, 42); /* knee pad L */
fill_rect(LX(18,20)-SI(1), LY(18,20)-1, SI(3), SI(2), 55, 50, 42); /* knee pad R */
disc(sx, sy, SI(9), ok, og, ob);
disc(sx, sy, SI(7), br, bg, bb);
fill_rect(LX(11,17)-SI(1), LY(11,17)-1, SI(8), SI(2), 45, 42, 36); /* belt */
fill_rect(LX(12,16)-SI(1), LY(12,16)-1, SI(2), SI(2), 60, 55, 45); /* pouch L */
fill_rect(LX(16,16)-SI(1), LY(16,16)-1, SI(2), SI(2), 60, 55, 45); /* pouch R */
fill_rect(LX(9,12)-SI(1), LY(9,12)-SI(1), SI(3), SI(3), 50, 46, 40); /* shoulder pad L */
fill_rect(LX(19,12)-SI(1), LY(19,12)-SI(1), SI(3), SI(3), 50, 46, 40); /* shoulder pad R */
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);
fill_rect(LX(8,15)-SI(1), LY(8,15)-SI(1), SI(2), SI(3), 40, 38, 32); /* pack strap */
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(12,7)-SI(1), LY(12,7)-1, SI(5), SI(2), 30, 40, 50); /* visor */
line(LX(14,6), LY(14,6), LX(14,1), LY(14,1), 50, 46, 40); /* antenna */
line(LX(15,6), LY(15,6), LX(15,0), LY(15,0), lr, lg, lb);
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 */
line(LX(17,2), LY(17,2), LX(20,0), LY(20,0), 70, 65, 55); /* bayonet */
line(LX(18,2), LY(18,2), LX(21,1), LY(21,1), 55, 50, 40);
break;
case UT_BUGGY:
/* fenders + bumper + cabin + roll bar + spare + exhaust + lights + 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(3,6)-SI(1), LY(3,6)-1, SI(6), SI(3), 45, 42, 36); /* front fender L */
fill_rect(LX(21,6)-SI(1), LY(21,6)-1, SI(6), SI(3), 45, 42, 36); /* front fender R */
fill_rect(LX(3,22)-SI(1), LY(3,22)-1, SI(6), SI(3), 45, 42, 36); /* rear fender L */
fill_rect(LX(21,22)-SI(1), LY(21,22)-1, SI(6), SI(3), 45, 42, 36);
fill_rect(LX(1,12)-SI(1), LY(1,12)-SI(1), SI(3), SI(6), 55, 50, 40); /* bumper */
fill_rect(LX(2,13)-SI(1), LY(2,13)-1, SI(1), SI(4), 90, 80, 55);
fill_rect(LX(6,9)-SI(1), LY(6,9)-SI(1), SI(10), SI(6), lr, lg, lb);
fill_rect(LX(7,8)-SI(1), LY(7,8)-1, SI(8), SI(3), 35, 55, 75); /* windshield */
line(LX(8,10), LY(8,10), LX(8,4), LY(8,4), lr, lg, lb); /* roll bar L */
line(LX(9,10), LY(9,10), LX(9,4), LY(9,4), 50, 46, 40);
line(LX(18,10), LY(18,10), LX(18,4), LY(18,4), lr, lg, lb); /* roll bar R */
line(LX(17,10), LY(17,10), LX(17,4), LY(17,4), 50, 46, 40);
line(LX(8,4), LY(8,4), LX(18,4), LY(18,4), lr, lg, lb);
line(LX(8,5), LY(8,5), LX(18,5), LY(18,5), 50, 46, 40);
disc(LX(25,16), LY(25,16), SI(3), ok, og, ob); /* spare tyre */
disc(LX(25,16), LY(25,16), SI(2), mr, mg, mb);
disc(LX(25,16), LY(25,16), SI(1), 90, 85, 75);
fill_rect(LX(26,20)-SI(1), LY(26,20)-1, SI(3), SI(2), 40, 38, 32); /* exhaust */
fill_rect(LX(27,19)-SI(1), LY(27,19)-1, SI(2), SI(1), 70, 65, 55);
disc(LX(4,11), LY(4,11), SI(2), 220, 200, 80); /* headlight L */
disc(LX(4,19), LY(4,19), SI(2), 220, 200, 80); /* headlight R */
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);
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:
/* skirts + road wheels + glacis + mantlet + coax + exhaust + antenna + barrel */
fill_rect(LX(2,8)-SI(1), LY(2,8)-SI(2), SI(5), SI(12), ok, og, ob);
fill_rect(LX(23,8)-SI(1), LY(23,8)-SI(2), SI(5), SI(12), ok, og, ob);
fill_rect(LX(3,10)-SI(1), LY(3,10)-SI(1), SI(3), SI(10), mr, mg, mb);
fill_rect(LX(24,10)-SI(1), LY(24,10)-SI(1), SI(3), SI(10), mr, mg, mb);
fill_rect(LX(2,9)-SI(1), LY(2,9)-1, SI(5), SI(2), 50, 46, 40); /* side skirt L top */
fill_rect(LX(23,9)-SI(1), LY(23,9)-1, SI(5), SI(2), 50, 46, 40);
fill_rect(LX(2,18)-SI(1), LY(2,18)-1, SI(5), SI(2), 50, 46, 40);
fill_rect(LX(23,18)-SI(1), LY(23,18)-1, SI(5), SI(2), 50, 46, 40);
disc(LX(5,11), LY(5,11), SI(2), 40, 38, 32); /* sprocket */
disc(LX(5,15), LY(5,15), SI(2), 40, 38, 32); /* road wheel */
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,15), LY(26,15), 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);
fill_rect(LX(5,10)-SI(1), LY(5,10)-1, SI(8), SI(3), 55, 50, 42); /* glacis */
fill_rect(LX(6,11)-SI(1), LY(6,11)-1, SI(6), SI(2), lr, lg, lb);
disc(sx, sy, SI(7), ok, og, ob);
disc(sx, sy, SI(6), lr, lg, lb); /* cupola */
fill_rect(LX(18,13)-SI(1), LY(18,13)-SI(1), SI(4), SI(5), 45, 42, 36); /* mantlet */
fill_rect(LX(19,14)-SI(1), LY(19,14)-1, SI(2), SI(3), 60, 55, 45);
disc(LX(15,12), LY(15,12), SI(2), 40, 38, 32); /* hatch */
line(LX(14,12), LY(14,12), LX(14,2), LY(14,2), 50, 46, 40); /* antenna */
line(LX(15,12), LY(15,12), LX(15,1), LY(15,1), lr, lg, lb);
fill_rect(LX(7,20)-SI(1), LY(7,20)-1, SI(3), SI(2), 40, 38, 32); /* exhaust */
fill_rect(LX(8,19)-SI(1), LY(8,19)-1, SI(1), SI(1), 70, 65, 55);
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);
line(LX(18,12), LY(18,12), LX(27,11), LY(27,11), 40, 36, 30); /* coax MG */
line(LX(18,12), LY(18,12), LX(27,12), LY(27,12), 50, 45, 35);
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:
/* howitzer: outriggers + shield + ammo rack + elevated tube + brake */
fill_rect(LX(1,18)-SI(1), LY(1,18)-1, SI(8), SI(3), ok, og, ob);
fill_rect(LX(21,18)-SI(1), LY(21,18)-1, SI(8), SI(3), ok, og, ob);
fill_rect(LX(0,19)-SI(1), LY(0,19)-1, SI(4), SI(2), 40, 38, 32);
fill_rect(LX(26,19)-SI(1), LY(26,19)-1, SI(4), SI(2), 40, 38, 32);
fill_rect(sx-SI(11), sy-SI(4), SI(22), SI(10), ok, og, ob);
fill_rect(sx-SI(9), sy-SI(2), SI(18), SI(7), br, bg, bb);
fill_rect(LX(22,12)-SI(1), LY(22,12)-SI(2), SI(6), SI(8), 55, 50, 40);
line(LX(23,13), LY(23,13), LX(27,13), LY(27,13), 90, 80, 55);
line(LX(23,15), LY(23,15), LX(27,15), LY(27,15), 90, 80, 55);
line(LX(23,17), LY(23,17), LX(27,17), LY(27,17), 90, 80, 55);
fill_rect(LX(10,8)-SI(1), LY(10,8)-SI(1), SI(10), SI(5), ok, og, ob);
fill_rect(LX(11,9)-SI(1), LY(11,9)-1, SI(8), SI(3), lr, lg, lb);
disc(sx, sy-SI(1), SI(4), ok, og, ob);
disc(sx, sy-SI(1), SI(3), lr, lg, lb);
line(LX(15,11), LY(15,11), LX(3,1), LY(3,1), 45, 40, 32);
line(LX(15,12), LY(15,12), LX(3,2), LY(3,2), 50, 45, 35);
line(LX(15,13), LY(15,13), LX(2,1), LY(2,1), 55, 50, 40);
line(LX(15,14), LY(15,14), LX(3,3), LY(3,3), 50, 45, 35);
line(LX(15,15), LY(15,15), LX(4,4), LY(4,4), 45, 40, 32);
fill_rect(LX(1,0)-SI(1), LY(1,0)-1, SI(5), SI(4), 60, 55, 45);
fill_rect(LX(2,1)-SI(1), LY(2,1)-1, SI(3), SI(2), 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 + boom/scoop + conveyor + hopper panels + exhaust/lights + hubbed 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(3,8)-SI(1), LY(3,8)-SI(2), SI(8), SI(8), lr, lg, lb); /* cabin */
fill_rect(LX(4,8)-SI(1), LY(4,8)-SI(1), SI(6), SI(3), 35, 55, 75); /* glass */
fill_rect(LX(5,12)-SI(1), LY(5,12)-1, SI(4), SI(2), 45, 42, 36); /* cabin panel */
line(LX(2,14), LY(2,14), LX(8,11), LY(8,11), 50, 46, 40); /* boom arm */
line(LX(2,15), LY(2,15), LX(8,12), LY(8,12), 70, 65, 55);
fill_rect(LX(0,14)-SI(1), LY(0,14)-SI(1), SI(5), SI(5), 90, 80, 55); /* scoop lip */
fill_rect(LX(1,15)-SI(1), LY(1,15)-SI(1), SI(3), SI(3), 70, 60, 40);
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(24,10), LY(24,10), 160, 130, 70);
line(LX(14,13), LY(14,13), LX(24,13), LY(24,13), 160, 130, 70);
line(LX(14,16), LY(14,16), LX(24,16), LY(24,16), 160, 130, 70);
}
fill_rect(LX(12,9)-SI(1), LY(12,9)-1, SI(12), SI(1), 45, 40, 32); /* hopper rim */
fill_rect(LX(24,11)-SI(1), LY(24,11)-SI(2), SI(3), SI(8), 50, 46, 40); /* side panel */
fill_rect(LX(25,12)-SI(1), LY(25,12)-SI(1), SI(1), SI(6), 70, 65, 55);
fill_rect(LX(20,18)-SI(1), LY(20,18)-1, SI(4), SI(2), 40, 38, 32); /* exhaust */
fill_rect(LX(21,17)-SI(1), LY(21,17)-1, SI(2), SI(1), 70, 65, 55);
disc(LX(4,10), LY(4,10), SI(2), 220, 200, 80); /* cabin light */
disc(LX(2,16), LY(2,16), SI(1), 220, 180, 60); /* scoop lamp */
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:
/* bumper + grill + seats + jerry cans + spare + lights + antenna + cage + 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(3,12)-SI(1), LY(3,12)-SI(1), SI(3), SI(6), 55, 50, 40); /* bumper */
fill_rect(LX(4,13)-SI(1), LY(4,13)-1, SI(1), SI(4), 90, 80, 55);
fill_rect(LX(5,13)-SI(1), LY(5,13)-1, SI(3), SI(4), 40, 38, 32); /* grill */
line(LX(5,14), LY(5,14), LX(7,14), LY(7,14), 70, 65, 55);
line(LX(5,16), LY(5,16), LX(7,16), LY(7,16), 70, 65, 55);
fill_rect(LX(7,10)-SI(1), LY(7,10)-SI(1), SI(8), SI(5), lr, lg, lb);
fill_rect(LX(8,12)-SI(1), LY(8,12)-1, SI(3), SI(3), 45, 40, 35); /* seat L */
fill_rect(LX(14,12)-SI(1), LY(14,12)-1, SI(3), SI(3), 45, 40, 35); /* seat R */
fill_rect(LX(22,11)-SI(1), LY(22,11)-SI(1), SI(3), SI(4), 70, 90, 50); /* jerry can */
fill_rect(LX(23,12)-SI(1), LY(23,12)-1, SI(1), SI(2), 90, 110, 60);
disc(LX(24,18), LY(24,18), SI(3), ok, og, ob); /* spare */
disc(LX(24,18), LY(24,18), SI(2), mr, mg, mb);
disc(LX(24,18), LY(24,18), SI(1), 90, 85, 75);
disc(LX(5,11), LY(5,11), SI(2), 220, 200, 80); /* light L */
disc(LX(5,19), LY(5,19), SI(2), 220, 200, 80); /* light R */
line(LX(16,10), LY(16,10), LX(16,1), LY(16,1), 50, 46, 40); /* antenna */
line(LX(17,10), LY(17,10), LX(17,2), LY(17,2), 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:
/* plates + eyes/jaw + spikes + dual claws + tail */
disc(sx, sy+SI(2), SI(11), ok, og, ob);
disc(sx, sy, SI(9), br, bg, bb);
fill_rect(LX(10,14)-SI(1), LY(10,14)-SI(1), SI(4), SI(3), 45, 40, 35); /* chest plate L */
fill_rect(LX(17,14)-SI(1), LY(17,14)-SI(1), SI(4), SI(3), 45, 40, 35); /* chest plate R */
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(11,7)-SI(1), LY(11,7)-1, SI(7), SI(2), 30, 40, 50); /* brow */
disc(LX(12,9), LY(12,9), SI(1), 200, 60, 40); /* eye L */
disc(LX(18,9), LY(18,9), SI(1), 200, 60, 40); /* eye R */
fill_rect(LX(13,11)-SI(1), LY(13,11)-1, SI(5), SI(2), 40, 30, 28); /* jaw */
line(LX(14,12), LY(14,12), LX(14,13), LY(14,13), 70, 55, 45); /* fang */
line(LX(16,12), LY(16,12), LX(16,13), LY(16,13), 70, 55, 45);
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(LX(14,4), LY(14,4), LX(12,0), LY(12,0), 50, 45, 35); /* extra spike */
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 R */
line(LX(12,16), LY(12,16), LX(4,10), LY(4,10), 40, 36, 30); /* second claw L */
line(LX(12,17), LY(12,17), LX(4,11), LY(4,11), 50, 45, 35);
line(LX(5,10), LY(5,10), LX(1,7), LY(1,7), 55, 50, 40);
line(LX(5,11), LY(5,11), LX(1,13), LY(1,13), 55, 50, 40);
disc(LX(2,10), LY(2,10), SI(2), 70, 60, 45);
line(LX(15,20), LY(15,20), LX(15,27), LY(15,27), 45, 40, 35); /* tail */
line(LX(16,20), LY(16,20), LX(18,28), LY(18,28), 55, 50, 40);
disc(LX(17,27), LY(17,27), SI(2), 60, 50, 40);
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:
/* carapace plates/spines + eye + vents + 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 */
fill_rect(LX(8,13)-SI(1), LY(8,13)-SI(1), SI(5), SI(3), 50, 46, 40); /* plate L */
fill_rect(LX(17,13)-SI(1), LY(17,13)-SI(1), SI(5), SI(3), 50, 46, 40); /* plate R */
line(LX(10,9), LY(10,9), LX(10,5), LY(10,5), 40, 36, 30); /* spine */
line(LX(13,9), LY(13,9), LX(13,4), LY(13,4), 55, 50, 40);
line(LX(16,9), LY(16,9), LX(16,4), LY(16,4), 55, 50, 40);
line(LX(19,9), LY(19,9), LX(19,5), LY(19,5), 40, 36, 30);
fill_rect(LX(11,16)-SI(1), LY(11,16)-1, SI(2), SI(2), 30, 28, 24); /* vent L */
fill_rect(LX(17,16)-SI(1), LY(17,16)-1, SI(2), SI(2), 30, 28, 24); /* vent R */
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);
disc(LX(9,21), LY(9,21), SI(1), 90, 85, 75); /* hub */
disc(LX(14,22), LY(14,22), SI(1), 90, 85, 75);
disc(LX(19,21), LY(19,21), SI(1), 90, 85, 75);
disc(LX(23,22), LY(23,22), SI(1), 90, 85, 75);
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 */
disc(LX(26,11), LY(26,11), SI(1), 200, 80, 40); /* eye */
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:
/* prow + ammo boxes + triple gatling + exhaust (vs tank cupola) */
fill_rect(LX(0,7)-SI(1), LY(0,7)-SI(2), SI(5), SI(14), ok, og, ob);
fill_rect(LX(25,7)-SI(1), LY(25,7)-SI(2), SI(5), SI(14), ok, og, ob);
fill_rect(LX(1,10)-SI(1), LY(1,10)-SI(1), SI(3), SI(10), mr, mg, mb);
fill_rect(LX(26,10)-SI(1), LY(26,10)-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(12), sy-SI(6), SI(24), SI(12), ok, og, ob);
fill_rect(sx-SI(10), sy-SI(5), SI(20), SI(10), br, bg, bb);
fill_rect(LX(24,8)-SI(1), LY(24,8)-SI(1), SI(6), SI(6), 55, 48, 38);
line(LX(25,9), LY(25,9), LX(29,11), LY(29,11), 70, 60, 45);
line(LX(25,13), LY(25,13), LX(29,11), LY(29,11), 70, 60, 45);
fill_rect(LX(4,8)-SI(1), LY(4,8)-1, SI(5), SI(4), 70, 55, 35);
fill_rect(LX(5,9)-SI(1), LY(5,9)-1, SI(3), SI(2), 200, 160, 60);
fill_rect(LX(10,8)-SI(1), LY(10,8)-1, SI(5), SI(4), 70, 55, 35);
fill_rect(LX(11,9)-SI(1), LY(11,9)-1, SI(3), SI(2), 200, 160, 60);
fill_rect(LX(12,12)-SI(1), LY(12,12)-SI(1), SI(10), SI(8), ok, og, ob);
fill_rect(LX(13,13)-SI(1), LY(13,13)-SI(1), SI(8), SI(6), lr, lg, lb);
line(LX(20,13), LY(20,13), LX(29,12), LY(29,12), 45, 40, 32);
line(LX(20,15), LY(20,15), LX(29,15), LY(29,15), 55, 50, 40);
line(LX(20,17), LY(20,17), LX(29,18), LY(29,18), 45, 40, 32);
disc(LX(29,12), LY(29,12), SI(1), 60, 55, 45);
disc(LX(29,15), LY(29,15), SI(2), 60, 55, 45);
disc(LX(29,18), LY(29,18), SI(1), 60, 55, 45);
fill_rect(LX(2,14)-SI(1), LY(2,14)-1, SI(3), SI(4), 40, 38, 32);
disc(LX(2,15), LY(2,15), SI(2), 90, 70, 40);
disc(LX(2,17), LY(2,17), SI(1), 200, 100, 40);
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:
/* dual tanks + hose + heat shield + nozzle jet */
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(12,8)-SI(1), LY(12,8)-1, SI(5), SI(2), 30, 40, 50);
disc(LX(6,13), LY(6,13), SI(5), ok, og, ob);
disc(LX(6,13), LY(6,13), SI(4), 90, 70, 40);
disc(LX(6,13), LY(6,13), SI(2), 200, 120, 40);
disc(LX(6,19), LY(6,19), SI(5), ok, og, ob);
disc(LX(6,19), LY(6,19), SI(4), 90, 70, 40);
disc(LX(6,19), LY(6,19), SI(2), 200, 120, 40);
disc(LX(6,11), LY(6,11), SI(1), 60, 55, 45);
disc(LX(6,17), LY(6,17), SI(1), 60, 55, 45);
disc(LX(6,21), LY(6,21), SI(1), 60, 55, 45);
line(LX(10,14), LY(10,14), LX(16,16), LY(16,16), 50, 45, 35);
line(LX(10,15), LY(10,15), LX(16,17), LY(16,17), 70, 55, 35);
fill_rect(LX(16,12)-SI(1), LY(16,12)-SI(1), SI(5), SI(8), 55, 50, 40);
fill_rect(LX(17,13)-SI(1), LY(17,13)-SI(1), SI(3), SI(6), 90, 70, 40);
line(LX(20,14), LY(20,14), LX(28,19), LY(28,19), 255, 120, 40);
line(LX(20,15), LY(20,15), LX(28,20), LY(28,20), 255, 150, 50);
line(LX(20,16), LY(20,16), LX(27,21), LY(27,21), 255, 180, 60);
line(LX(20,17), LY(20,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:
/* MLRS box launcher + rails + warning stripes + hubbed chassis */
fill_rect(sx-SI(12), sy-SI(3), SI(24), SI(12), ok, og, ob);
fill_rect(sx-SI(10), sy-SI(1), SI(20), SI(8), br, bg, bb);
fill_rect(LX(8,4)-SI(1), LY(8,4)-SI(2), SI(16), SI(10), ok, og, ob);
fill_rect(LX(9,5)-SI(1), LY(9,5)-SI(1), SI(14), SI(8), 50, 48, 42);
line(LX(10,6), LY(10,6), LX(22,6), LY(22,6), 90, 85, 70);
line(LX(10,8), LY(10,8), LX(22,8), LY(22,8), 90, 85, 70);
line(LX(10,10), LY(10,10), LX(22,10), LY(22,10), 90, 85, 70);
line(LX(10,12), LY(10,12), LX(22,12), LY(22,12), 90, 85, 70);
fill_rect(LX(9,5)-SI(1), LY(9,5)-SI(1), SI(3), SI(8), 255, 200, 40);
fill_rect(LX(12,5)-SI(1), LY(12,5)-SI(1), SI(2), SI(8), 30, 28, 24);
fill_rect(LX(14,5)-SI(1), LY(14,5)-SI(1), SI(3), SI(8), 255, 200, 40);
disc(LX(23,7), LY(23,7), SI(2), 255, 120, 80);
disc(LX(23,10), LY(23,10), 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(LX(22,8), LY(22,8), LX(29,4), LY(29,4), 255, 80, 60);
line(LX(22,10), LY(22,10), LX(29,6), LY(29,6), 255, 100, 70);
disc(LX(29,5), LY(29,5), 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) {
/* hive: lobes + membrane ridges + spine + pit eye (KKND HQ read) */
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);
/* membrane ridges between lobes */
line(sx + 18, sy + 20, sx + W/2 - 8, sy + H/2 - 4, ok, og, ob);
line(sx + W - 18, sy + 20, sx + W/2 + 8, sy + H/2 - 4, ok, og, ob);
line(sx + 18, sy + H - 18, sx + W/2 - 6, sy + H/2 + 6, ok, og, ob);
line(sx + W - 18, sy + H - 18, sx + W/2 + 6, sy + H/2 + 6, ok, og, ob);
/* dorsal spine + tendrils */
fill_rect(sx + W/2 - 2, sy + 4, 4, H/2 - 8, ok, og, ob);
fill_rect(sx + W/2 - 1, sy + 5, 2, H/2 - 10, 90, 40, 55);
line(sx + W/2, sy + 8, sx + W/2 - 10, sy + 18, 70, 30, 45);
line(sx + W/2, sy + 8, sx + W/2 + 10, sy + 18, 70, 30, 45);
/* dark pit + glowing eye */
disc(sx + W/2, sy + H/2, 8, 28, 18, 22);
disc(sx + W/2, sy + H/2, 5, 60, 20, 30);
disc(sx + W/2, sy + H/2, 3, 220, 60, 90);
disc(sx + W/2, sy + H/2, 1, 255, 180, 200);
ring(sx + W/2, sy + H/2, 10, 2, 50, 25, 35); /* pit rim */
/* extra blisters */
disc(sx + W/2 - 10, sy + 10, 5, lr, lg, lb);
disc(sx + W/2 + 12, sy + H - 12, 4, lr, lg, lb);
disc(sx + 10, sy + H/2 + 2, 3, lr, lg, lb);
disc(sx + W - 10, sy + H/2 - 4, 3, lr, lg, lb);
disc(sx + W/2 + 8, sy + 14, 2, 180, 70, 100);
} else {
/* Survivors HQ: towers + walkways + dome + dish + entry (KKND HQ read) */
fill_rect(sx + 4, sy + 4, 14, 14, ok, og, ob);
fill_rect(sx + 6, sy + 6, 10, 10, br, bg, bb);
fill_rect(sx + W - 18, sy + 4, 14, 14, ok, og, ob);
fill_rect(sx + W - 16, sy + 6, 10, 10, br, bg, bb);
fill_rect(sx + 4, sy + H - 18, 14, 14, ok, og, ob);
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);
fill_rect(sx + W - 16, sy + H - 16, 10, 10, br, bg, bb);
/* tower windows + corner lights */
fill_rect(sx + 8, sy + 8, 3, 3, 200, 230, 255);
fill_rect(sx + W - 13, sy + 8, 3, 3, 200, 230, 255);
fill_rect(sx + 8, sy + H - 13, 3, 3, 200, 230, 255);
fill_rect(sx + W - 13, sy + H - 13, 3, 3, 200, 230, 255);
disc(sx + 7, sy + 5, 2, 255, 220, 80);
disc(sx + W - 7, sy + 5, 2, 255, 220, 80);
/* courtyard body + roof band */
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);
/* walkways between towers */
fill_rect(sx + 16, sy + 8, W - 32, 3, 90, 85, 70);
fill_rect(sx + 16, sy + H - 11, W - 32, 3, 90, 85, 70);
fill_rect(sx + 8, sy + 16, 3, H - 32, 90, 85, 70);
fill_rect(sx + W - 11, sy + 16, 3, H - 32, 90, 85, 70);
/* command dome + mast + radar dish */
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);
fill_rect(sx + W/2 - 2, sy + 2, 4, H/2 - 6, 200, 180, 60);
fill_rect(sx + W/2 - 6, sy + 2, 12, 3, 220, 210, 80);
disc(sx + W/2 + 8, sy + 8, 5, ok, og, ob); /* dish */
disc(sx + W/2 + 8, sy + 8, 3, 180, 200, 220);
line(sx + W/2 + 2, sy + 6, sx + W/2 + 8, sy + 8, 160, 150, 100);
/* entry bunker + blast doors */
fill_rect(sx + W/2 - 8, sy + H - 14, 16, 8, ok, og, ob);
fill_rect(sx + W/2 - 6, sy + H - 12, 12, 6, 40, 38, 34);
fill_rect(sx + W/2 - 4, sy + H - 11, 3, 4, 70, 65, 55);
fill_rect(sx + W/2 + 1, sy + H - 11, 3, 4, 70, 65, 55);
/* side annex windows */
fill_rect(sx + 18, sy + H/2 + 2, 6, 4, 160, 200, 230);
fill_rect(sx + W - 24, sy + H/2 + 2, 6, 4, 160, 200, 230);
}
break;
case BT_WARFACTORY:
/* hangar + crane mast/jib + segmented bay doors (KKND factory read) */
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, 12, lr, lg, lb); /* roof ridge */
fill_rect(sx + 10, sy + 12, 5, 5, 55, 50, 42); /* ridge vents */
fill_rect(sx + W/2 - 8, sy + 12, 5, 5, 55, 50, 42);
fill_rect(sx + W/2 + 3, sy + 12, 5, 5, 55, 50, 42);
fill_rect(sx + W - 15, sy + 12, 5, 5, 55, 50, 42);
/* side workshop annex */
fill_rect(sx + 4, sy + H/2 - 4, 10, H/2 - 6, ok, og, ob);
{
uint8_t dr, dg, db;
darken3(br, bg, bb, 0.5f, &dr, &dg, &db);
fill_rect(sx + 5, sy + H/2 - 2, 8, H/2 - 10, dr, dg, db);
}
fill_rect(sx + 6, sy + H/2, 6, 4, 90, 80, 55); /* window slit */
/* overhead crane mast + jib */
fill_rect(sx + W - 14, sy + 4, 6, H - 18, ok, og, ob);
fill_rect(sx + W - 13, sy + 5, 4, H - 20, 95, 88, 70);
fill_rect(sx + 14, sy + 6, W - 28, 4, ok, og, ob);
fill_rect(sx + 15, sy + 7, W - 30, 2, 120, 110, 85);
line(sx + 18, sy + 10, sx + 18, sy + 22, 70, 65, 50);
disc(sx + 18, sy + 24, 3, 80, 75, 55); /* hoist block */
/* segmented bay doors */
fill_rect(sx + 12, sy + H - 30, W - 28, 20, ok, og, ob);
fill_rect(sx + 14, sy + H - 28, W - 32, 16, 32, 32, 38);
line(sx + W/2 - 6, sy + H - 28, sx + W/2 - 6, sy + H - 12, 70, 68, 60);
line(sx + W/2, sy + H - 28, sx + W/2, sy + H - 12, 70, 68, 60);
line(sx + W/2 + 6, sy + H - 28, sx + W/2 + 6, sy + H - 12, 70, 68, 60);
line(sx + 14, sy + H - 20, sx + W - 18, sy + H - 20, 55, 52, 48);
fill_rect(sx + 10, sy + H - 10, W - 20, 5, 50, 48, 42); /* ramp lip */
fill_rect(sx + 12, sy + H - 9, W - 24, 2, 90, 75, 45);
break;
case BT_REFINERY:
/* oil-field: derrick + pumpjack + twin tanks + pipes + hopper (KKND read) */
fill_rect(sx + 4, sy + H - 12, W - 8, 10, ok, og, ob); /* slab */
fill_rect(sx + 6, sy + H - 10, W - 12, 6, 70, 62, 48);
/* latticed derrick mast */
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 - 6, sy + 38, sx + W/2 + 6, sy + H - 18, 55, 48, 38);
line(sx + W/2 + 6, sy + 38, sx + W/2 - 6, sy + H - 18, 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 */
/* pumpjack horse-head (left of mast) */
fill_rect(sx + 8, sy + H/2 - 2, 4, H/2 - 8, ok, og, ob);
fill_rect(sx + 9, sy + H/2, 2, H/2 - 12, 90, 80, 60);
fill_rect(sx + 6, sy + H/2 - 6, 14, 4, ok, og, ob);
fill_rect(sx + 7, sy + H/2 - 5, 12, 2, 110, 95, 70);
disc(sx + 8, sy + H/2 - 8, 4, ok, og, ob); /* horse head */
disc(sx + 8, sy + H/2 - 8, 2, 130, 110, 80);
line(sx + 10, sy + H/2 - 4, sx + W/2 - 4, sy + 16, 80, 70, 50); /* bridle cable */
/* gantry arm + walkway 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);
fill_rect(sx + W/2 + 4, sy + 18, W/2 - 16, 2, 70, 60, 45); /* catwalk */
line(sx + W/2 + 4, sy + 18, sx + W - 12, sy + H - 22, 90, 75, 50);
/* hopper / processing shed + windows */
fill_rect(sx + W - 24, sy + H - 32, 18, 18, ok, og, ob);
fill_rect(sx + W - 22, sy + H - 30, 14, 14, br, bg, bb);
fill_rect(sx + W - 20, sy + H - 28, 10, 6, 140, 110, 55);
fill_rect(sx + W - 18, sy + H - 26, 3, 3, 180, 220, 255); /* window */
fill_rect(sx + W - 13, sy + H - 26, 3, 3, 180, 220, 255);
fill_rect(sx + W - 16, sy + H - 18, 4, 4, 40, 36, 30); /* chute */
/* twin scrap tanks */
disc(sx + 14, sy + H - 18, 9, ok, og, ob);
disc(sx + 14, sy + H - 18, 7, 200, 165, 85);
ring(sx + 14, sy + H - 18, 4, 2, 90, 70, 45);
disc(sx + 28, sy + H - 16, 7, ok, og, ob);
disc(sx + 28, sy + H - 16, 5, 180, 150, 75);
ring(sx + 28, sy + H - 16, 3, 1, 80, 60, 40);
/* pipe runs tank→mast→hopper */
line(sx + 20, sy + H - 16, sx + W/2 - 4, sy + H - 12, 110, 90, 50);
line(sx + 28, sy + H - 14, sx + W/2 - 2, sy + H - 14, 100, 85, 45);
line(sx + W/2 + 4, sy + H - 14, sx + W - 20, sy + H - 20, 100, 85, 45);
fill_rect(sx + W/2 - 1, sy + H - 16, 3, 4, 80, 70, 50); /* valve */
/* oil stain pool under slab */
disc(sx + W/2 - 8, sy + H - 6, 4, 40, 32, 20);
disc(sx + W/2 + 6, sy + H - 5, 3, 35, 28, 18);
break;
case BT_POWER:
/* tall cooling towers + reactor block + steam plumes (KKND plant read) */
fill_rect(sx + 3, sy + H - 10, W - 6, 8, ok, og, ob);
fill_rect(sx + 4, sy + H - 8, W - 8, 5, 55, 50, 42); /* pad */
/* left cooling tower (taller cone) */
fill_rect(sx + 4, sy + 8, 10, H - 18, ok, og, ob);
fill_rect(sx + 5, sy + 9, 8, H - 20, 95, 145, 195);
fill_rect(sx + 6, sy + 6, 6, 5, ok, og, ob);
fill_rect(sx + 7, sy + 5, 4, 4, 70, 120, 170); /* rim */
disc(sx + 9, sy + 4, 3, 240, 248, 255); /* steam puff */
disc(sx + 7, sy + 2, 2, 220, 235, 250);
/* right cooling tower */
fill_rect(sx + W - 14, sy + 10, 10, H - 20, ok, og, ob);
fill_rect(sx + W - 13, sy + 11, 8, H - 22, 85, 135, 185);
fill_rect(sx + W - 12, sy + 8, 6, 4, ok, og, ob);
fill_rect(sx + W - 11, sy + 7, 4, 3, 65, 115, 165);
disc(sx + W - 9, sy + 5, 3, 235, 245, 255);
disc(sx + W - 7, sy + 3, 2, 210, 230, 250);
/* central reactor block + glow */
fill_rect(sx + W/2 - 5, sy + H/2 - 4, 10, H/2 - 2, ok, og, ob);
fill_rect(sx + W/2 - 4, sy + H/2 - 2, 8, H/2 - 6, 55, 95, 150);
disc(sx + W/2, sy + H/2 + 2, 4, 120, 200, 255);
disc(sx + W/2, sy + H/2 + 2, 2, 250, 252, 255);
line(sx + W/2, sy + 8, sx + W/2, sy + H - 10, 200, 230, 250);
line(sx + 8, sy + H/2 + 2, sx + W - 8, sy + H/2 + 2, 180, 220, 245);
break;
case BT_TURRET:
/* armored bunker + ringed cupola + thick barrel (KKND turret read) */
fill_rect(sx + 2, sy + H - 14, W - 4, 12, ok, og, ob);
fill_rect(sx + 3, sy + H - 12, W - 6, 9, 48, 44, 38); /* bunker slab */
fill_rect(sx + 4, sy + H - 11, 4, 4, 70, 65, 55); /* armor plate L */
fill_rect(sx + W - 8, sy + H - 11, 4, 4, 70, 65, 55); /* armor plate R */
fill_rect(sx + W/2 - 5, sy + H - 8, 10, 3, 35, 32, 28); /* embrasure */
/* ringed cupola */
disc(sx + W/2, sy + H/2, W/3 + 3, ok, og, ob);
disc(sx + W/2, sy + H/2, W/3 + 1, br, bg, bb);
{
uint8_t dr, dg, db;
darken3(br, bg, bb, 0.45f, &dr, &dg, &db);
ring(sx + W/2, sy + H/2, W/4 + 1, 2, dr, dg, db);
}
disc(sx + W/2, sy + H/2, W/5, lr, lg, lb);
disc(sx + W/2, sy + H/2 - 1, 3, 35, 32, 28); /* hatch */
/* thick armored barrel */
fill_rect(sx + W/2 + 2, sy + H/2 - 3, 12, 5, ok, og, ob);
fill_rect(sx + W/2 + 3, sy + H/2 - 2, 11, 3, 40, 36, 30);
line(sx + W/2, sy + H/2, sx + W/2 + 14, sy + H/2 - 10, 28, 26, 22);
line(sx + W/2 + 1, sy + H/2 + 1, sx + W/2 + 14, sy + H/2 - 9, 55, 48, 38);
disc(sx + W/2 + 13, sy + H/2 - 10, 2, 255, 200, 80); /* muzzle tip */
break;
case BT_WALL:
/* thick fort wall + crenels + seams + embrasure (KKND fort read) */
fill_rect(sx + 0, sy + 4, W, H - 4, ok, og, ob);
fill_rect(sx + 1, sy + 6, W - 2, H - 8, 165, 145, 110);
fill_rect(sx + 2, sy + H - 6, W - 4, 4, 90, 78, 58); /* plinth */
/* thick merlons */
fill_rect(sx + 1, sy + 1, 8, 10, ok, og, ob);
fill_rect(sx + 2, sy + 2, 6, 8, 195, 175, 140);
fill_rect(sx + W/2 - 4, sy + 1, 8, 10, ok, og, ob);
fill_rect(sx + W/2 - 3, sy + 2, 6, 8, 200, 180, 145);
fill_rect(sx + W - 9, sy + 1, 8, 10, ok, og, ob);
fill_rect(sx + W - 8, sy + 2, 6, 8, 190, 170, 135);
fill_rect(sx + 2, sy + 10, W - 4, 3, 215, 195, 155); /* coping */
/* block seams + arrow slit */
line(sx + 2, sy + H/2 + 2, sx + W - 2, sy + H/2 + 2, 55, 48, 38);
line(sx + W/3, sy + 8, sx + W/3, sy + H - 4, 65, 55, 42);
line(sx + 2 * W/3, sy + 8, sx + 2 * W/3, sy + H - 4, 65, 55, 42);
fill_rect(sx + W/2 - 2, sy + H/2 - 2, 4, 8, 35, 30, 24); /* embrasure */
fill_rect(sx + W/2 - 1, sy + H/2, 2, 4, 20, 18, 14);
break;
case BT_REPAIR:
/* garage + vehicle lift + tool rack + crane (KKND depot read) */
fill_rect(sx + 4, sy + 6, W - 8, H - 10, ok, og, ob);
fill_rect(sx + 6, sy + 8, W - 12, H - 14, br, bg, bb);
fill_rect(sx + 8, sy + 10, W - 16, 8, lr, lg, lb); /* roof */
fill_rect(sx + 10, sy + 12, 4, 4, 50, 48, 40); /* roof vents */
fill_rect(sx + W/2 - 2, sy + 12, 4, 4, 50, 48, 40);
fill_rect(sx + W - 14, sy + 12, 4, 4, 50, 48, 40);
/* open bay + lift pad */
fill_rect(sx + 10, sy + H - 28, W - 28, 18, ok, og, ob);
fill_rect(sx + 12, sy + H - 26, W - 32, 14, 38, 36, 32);
fill_rect(sx + W/2 - 10, sy + H - 22, 20, 8, 70, 68, 58); /* lift platform */
fill_rect(sx + W/2 - 8, sy + H - 20, 16, 4, 110, 100, 70);
line(sx + W/2 - 10, sy + H - 22, sx + W/2 - 10, sy + H - 12, 90, 85, 70);
line(sx + W/2 + 10, sy + H - 22, sx + W/2 + 10, sy + H - 12, 90, 85, 70);
/* tool rack annex (left) */
fill_rect(sx + 4, sy + H/2 - 2, 10, H/2 - 6, ok, og, ob);
{
uint8_t dr, dg, db;
darken3(br, bg, bb, 0.5f, &dr, &dg, &db);
fill_rect(sx + 5, sy + H/2, 8, H/2 - 10, dr, dg, db);
}
fill_rect(sx + 6, sy + H/2 + 2, 2, 6, 200, 160, 60); /* tools */
fill_rect(sx + 9, sy + H/2 + 1, 2, 7, 180, 180, 190);
/* overhead crane mast + jib + hook */
fill_rect(sx + W - 16, sy + 4, 8, H - 20, ok, og, ob);
fill_rect(sx + W - 14, sy + 5, 5, H - 22, 95, 88, 70);
fill_rect(sx + 16, sy + 6, W - 32, 4, ok, og, ob);
fill_rect(sx + 17, sy + 7, W - 34, 2, 120, 110, 85);
line(sx + W/2, sy + 10, sx + W/2, sy + 20, 70, 65, 50);
disc(sx + W/2, sy + 22, 3, 80, 75, 55);
/* green service cross (clearer) */
ring(sx + W/2 - 6, sy + H/2 + 4, 9, 2, 90, 200, 90);
fill_rect(sx + W/2 - 14, sy + H/2 + 2, 16, 4, 120, 220, 120);
fill_rect(sx + W/2 - 8, sy + H/2 - 4, 4, 16, 120, 220, 120);
/* parts crates */
fill_rect(sx + W - 22, sy + H - 14, 8, 6, 140, 100, 55);
fill_rect(sx + W - 20, sy + H - 12, 4, 3, 180, 140, 70);
break;
case BT_RESEARCH:
/* tall lab + dish mast + instrument bank (KKND lab read) */
fill_rect(sx + 5, sy + 12, W - 10, H - 16, ok, og, ob);
fill_rect(sx + 7, sy + 14, W - 14, H - 20, br, bg, bb);
fill_rect(sx + 9, sy + 16, W - 18, 10, lr, lg, lb); /* upper band */
/* glowing lab windows */
fill_rect(sx + 10, sy + H/2 - 2, 8, 6, 180, 230, 255);
fill_rect(sx + 22, sy + H/2 - 2, 8, 6, 160, 220, 250);
fill_rect(sx + 34, sy + H/2 - 2, 8, 6, 180, 230, 255);
fill_rect(sx + 12, sy + H/2, 4, 2, 240, 250, 255);
fill_rect(sx + 24, sy + H/2, 4, 2, 230, 245, 255);
/* dish on tall mast */
fill_rect(sx + W/2 + 4, sy + 2, 5, 18, ok, og, ob);
fill_rect(sx + W/2 + 5, sy + 3, 3, 16, 110, 100, 75);
disc(sx + W/2 + 6, sy + 12, 13, ok, og, ob);
disc(sx + W/2 + 6, sy + 12, 11, 215, 185, 85);
ring(sx + W/2 + 6, sy + 12, 8, 2, 170, 140, 55);
disc(sx + W/2 + 6, sy + 12, 4, 255, 245, 170);
disc(sx + W/2 + 6, sy + 12, 2, 255, 255, 220); /* feed */
line(sx + W/2 + 6, sy + 2, sx + W/2 + 6, sy + 12, 200, 190, 120);
/* side antenna stubs */
fill_rect(sx + 10, sy + 6, 3, 10, 200, 200, 140);
fill_rect(sx + 18, sy + 4, 2, 12, 220, 220, 150);
disc(sx + 11, sy + 5, 2, 255, 240, 120);
disc(sx + 19, sy + 3, 2, 255, 230, 100);
/* instrument bank / console strip */
{
uint8_t dr, dg, db;
darken3(br, bg, bb, 0.5f, &dr, &dg, &db);
fill_rect(sx + 10, sy + H - 18, W - 28, 8, dr, dg, db);
}
fill_rect(sx + 12, sy + H - 16, 4, 4, 80, 220, 120);
fill_rect(sx + 18, sy + H - 16, 4, 4, 80, 160, 255);
fill_rect(sx + 24, sy + H - 16, 4, 4, 255, 180, 80);
fill_rect(sx + 30, sy + H - 16, 4, 4, 200, 100, 255);
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 + cracks + rubble + ember (reads in KKND_SHOT) */
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);
disc(sx + W / 3, sy + H / 2, pw / 2 + 1, 38, 30, 22);
if (dmg > 0.25f) {
fill_rect(sx + W / 2, sy + H / 2, pw, ph, 55, 40, 28);
disc(sx + (W * 2) / 3, sy + H / 3, pw / 3 + 1, 48, 36, 24);
}
if (dmg > 0.45f) {
fill_rect(sx + W / 6, sy + (H * 2) / 3, pw - 1, ph - 1, 50, 38, 26);
disc(sx + W / 5, sy + H / 4, 2, 60, 42, 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);
line(sx + W / 5, sy + 4, sx + (W * 3) / 5, sy + H / 2, 32, 26, 20);
if (dmg > 0.5f) {
line(sx + W / 3, sy + 2, sx + W / 3 + 3, sy + H - 2, 22, 18, 14);
line(sx + (W * 2) / 3, sy + H / 5, sx + W / 4, sy + H - 3, 24, 20, 16);
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);
fill_rect(sx + W / 2 - 2, sy + H - 3, 6, 3, 90, 72, 48);
}
if (dmg > 0.65f) {
/* ember glow in heavy breaches */
disc(sx + W / 2, sy + H / 2, 2, 220, 110, 40);
disc(sx + W / 3 + 2, sy + (H * 2) / 3, 1, 255, 160, 60);
fill_rect(sx + 4, sy + 3, 3, 2, 40, 32, 24);
fill_rect(sx + W - 8, sy + 4, 4, 3, 35, 28, 20);
}
/* 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 — lofted shell/missile sample parabola; else linear streak */
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);
int lofted = (p->kind == 1 || p->kind == 3) && p->loft > 0.5f;
if (lofted) {
int len = (p->kind == 3) ? 28 : 22;
float step = 0.018f;
for (int t = 1; t <= len; t++) {
float tp = p->prog - step * (float)t;
if (tp < 0.0f) break;
float wx = p->ox + (p->tx - p->ox) * tp;
float wy = p->oy + (p->ty - p->oy) * tp
- p->loft * 4.0f * tp * (1.0f - tp);
int tx = (int)(wx - camx);
int ty = (int)(wy - camy);
if (p->kind == 3)
disc(tx, ty, (t < 4) ? 2 : 1, 255, (uint8_t)(120 - t * 3), 30);
else
disc(tx, ty, 1, 255, (uint8_t)(200 - t * 7), 50);
}
} else {
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 : 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) {
disc(tx, ty, (t < 3) ? 2 : 1, fade, 80, 255);
px(tx, ty, 220, 180, 255);
} 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;
/* larger bloom discs — fire/smoke read as KKND2 explosion cores */
int sz = (p->kind == 1) ? 14 : (p->kind == 3) ? 7 : (p->kind == 0) ? 11 : 5;
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 + 3, r / 2, gv / 2, b / 2);
disc(sx, sy, sz + 1, r, gv, b);
if (p->kind == 1 && a > 0.40f)
disc(sx, sy - 2, sz / 2 + 2, 255, (uint8_t)(230 * a), 90);
if (p->kind == 1 && a > 0.65f)
disc(sx + 1, sy - 3, sz / 3 + 1, 255, 255, 180);
} 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;
}