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