/* * 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; }