diff --git a/engine-c/build.sh b/engine-c/build.sh index f523dab..402e644 100755 --- a/engine-c/build.sh +++ b/engine-c/build.sh @@ -10,13 +10,27 @@ SRCS="src/unit_card.c src/rng.c src/config.c src/png.c src/snapshot.c src/map.c src/units.c src/buildings.c src/projectiles.c src/render.c \ src/input.c src/ai.c src/audio.c src/save.c src/engine.c src/main.c" -LIBS=$(pkg-config --cflags --libs sdl2 SDL2_ttf 2>/dev/null) +# Homebrew (macOS arm64/x86) often has .pc files that pkg-config misses without PATH. +if [ -d /opt/homebrew/lib/pkgconfig ]; then + export PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" +elif [ -d /usr/local/lib/pkgconfig ]; then + export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" +fi + +LIBS=$(pkg-config --cflags --libs sdl2 SDL2_ttf 2>/dev/null || true) if [ -z "$LIBS" ]; then - LIBS="-lSDL2 -lSDL2_ttf" + if [ -d /opt/homebrew/include ]; then + LIBS="-I/opt/homebrew/include -L/opt/homebrew/lib -lSDL2 -lSDL2_ttf" + elif [ -d /usr/local/include ]; then + LIBS="-I/usr/local/include -L/usr/local/lib -lSDL2 -lSDL2_ttf" + else + LIBS="-lSDL2 -lSDL2_ttf" + fi fi LIBS="$LIBS -lz" echo "openkknd: compiling..." +# shellcheck disable=SC2086 $CC $CFLAGS $SRCS -o openkknd $LIBS -lm echo "openkknd: built -> ./openkknd" echo "usage: ./openkknd [--easy|--normal|--hard] [--load savegame.kknd]" diff --git a/engine-c/include/kknd.h b/engine-c/include/kknd.h index f0702a4..8ec139b 100644 --- a/engine-c/include/kknd.h +++ b/engine-c/include/kknd.h @@ -19,8 +19,11 @@ #include #include -#include #include +/* SDL_image is optional; engine uses procedural sprites + custom PNG writer. */ +#if defined(KKND_HAVE_SDL_IMAGE) && KKND_HAVE_SDL_IMAGE +#include +#endif #include "unit_card.h" diff --git a/engine-c/include/unit_card.h b/engine-c/include/unit_card.h index 6864033..e26a64f 100644 --- a/engine-c/include/unit_card.h +++ b/engine-c/include/unit_card.h @@ -2,7 +2,7 @@ // Header file for unit card implementation #include -#include +#include typedef struct { char name[32]; @@ -33,8 +33,8 @@ typedef struct { #define UNIT_SPECIAL 3 #define UNIT_ALL 0 -// Flag for SIMD instructions -#define HAVE_AVX2 1 +/* Scalar batch helpers in unit_card.c; no AVX2 dependency. */ +#define HAVE_AVX2 0 #ifdef __cplusplus extern "C" { diff --git a/engine-c/openkknd b/engine-c/openkknd index 6cac52b..9d27bd4 100755 Binary files a/engine-c/openkknd and b/engine-c/openkknd differ diff --git a/engine-c/shot_0001.png b/engine-c/shot_0001.png index e2a3b6a..5b7746c 100644 Binary files a/engine-c/shot_0001.png and b/engine-c/shot_0001.png differ diff --git a/engine-c/src/engine.c b/engine-c/src/engine.c index 41d5cdd..0b96dc9 100644 --- a/engine-c/src/engine.c +++ b/engine-c/src/engine.c @@ -57,6 +57,15 @@ void engine_shutdown(Game *g) { map_free(&g->map); } +static void bld_force_complete(Game *g) { + for (int i = 0; i < g->bld_count; i++) { + Building *b = &g->buildings[i]; + if (b->dead) continue; + b->build = 1.0f; + b->hp = b->max_hp; + } +} + static void place_start(Game *g, Faction f) { int sx = g->map.spawn_x[f], sy = g->map.spawn_y[f]; int bx = sx - 1, by = sy - 1; @@ -71,12 +80,216 @@ static void place_start(Game *g, Faction f) { unit_spawn(g, UT_HARVESTER, f, wx + (i*24-12), wy + 30); /* starting infantry */ for (int i = 0; i < 4; i++) - unit_spawn(g, (f==FACTION_SURVIVOR?UT_INFANTRY:UT_INFANTRY), f, - wx - 40 + i*20, wy - 20); + unit_spawn(g, UT_INFANTRY, f, wx - 40 + i*20, wy - 20); /* a couple of buggies */ for (int i = 0; i < 2; i++) - unit_spawn(g, (f==FACTION_SURVIVOR?UT_BUGGY:UT_BUGGY), f, - wx - 30 + i*30, wy - 40); + unit_spawn(g, UT_BUGGY, f, wx - 30 + i*30, wy - 40); +} + +/* KKND2-like showcase: extra human base structures + diverse roster + + * enemy raid pack near the camera so fog/snapshot show combat. */ +static void place_showcase(Game *g, Faction human, Faction enemy) { + int sx = g->map.spawn_x[human], sy = g->map.spawn_y[human]; + float wx, wy; map_tile_to_world(sx, sy, &wx, &wy); + + /* extra structures in the cleared spawn pad (±8) */ + bld_spawn(g, BT_REPAIR, human, sx - 4, sy - 1); + bld_spawn(g, BT_RESEARCH, human, sx - 4, sy + 2); + bld_spawn(g, BT_POWER, human, sx + 3, sy + 2); + bld_spawn(g, BT_POWER, human, sx + 5, sy + 2); + bld_spawn(g, BT_WARFACTORY, human, sx + 3, sy - 4); + + /* turrets covering approaches */ + bld_spawn(g, BT_TURRET, human, sx - 2, sy - 3); + bld_spawn(g, BT_TURRET, human, sx + 1, sy - 3); + bld_spawn(g, BT_TURRET, human, sx + 4, sy); + bld_spawn(g, BT_TURRET, human, sx + 4, sy + 2); + bld_spawn(g, BT_TURRET, human, sx - 2, sy + 4); + bld_spawn(g, BT_TURRET, human, sx + 2, sy + 4); + bld_spawn(g, BT_TURRET, human, sx - 4, sy); + + /* blast-wall spur (north + west + east stub) */ + for (int dx = -3; dx <= 3; dx++) { + if (dx >= -1 && dx <= 1) continue; /* gate */ + bld_spawn(g, BT_WALL, human, sx + dx, sy - 4); + } + for (int dy = -3; dy <= 3; dy++) { + if (dy >= -1 && dy <= 1) continue; + bld_spawn(g, BT_WALL, human, sx - 5, sy + dy); + } + for (int dy = -2; dy <= 2; dy++) { + if (dy == 0) continue; + bld_spawn(g, BT_WALL, human, sx + 6, sy + dy); + } + + /* dense Survivor battle line NE of HQ (tight combat camera) */ + unit_spawn(g, UT_TANK, human, wx + 48, wy - 8); + unit_spawn(g, UT_TANK, human, wx + 68, wy + 8); + unit_spawn(g, UT_TANK, human, wx + 82, wy - 38); + unit_spawn(g, UT_TANK, human, wx + 58, wy - 48); + unit_spawn(g, UT_RAVAGER, human, wx + 92, wy - 22); + unit_spawn(g, UT_RAVAGER, human, wx + 78, wy - 58); + unit_spawn(g, UT_ARTILLERY, human, wx + 18, wy + 48); + unit_spawn(g, UT_ARTILLERY, human, wx + 38, wy + 62); + unit_spawn(g, UT_FLAME, human, wx - 40, wy + 12); + unit_spawn(g, UT_FLAME, human, wx + 105, wy - 5); + unit_spawn(g, UT_MISSILE, human, wx - 55, wy - 8); + unit_spawn(g, UT_MISSILE, human, wx + 110, wy - 40); + unit_spawn(g, UT_JEEP, human, wx + 32, wy - 52); + unit_spawn(g, UT_JEEP, human, wx + 48, wy - 68); + unit_spawn(g, UT_BUGGY, human, wx + 55, wy - 78); + unit_spawn(g, UT_BUGGY, human, wx + 72, wy - 72); + unit_spawn(g, UT_MEDIC, human, wx - 12, wy + 40); + unit_spawn(g, UT_MEDIC, human, wx + 20, wy + 28); + for (int i = 0; i < 10; i++) + unit_spawn(g, UT_INFANTRY, human, wx + 88 + (i % 5) * 14, wy + 8 + (i / 5) * 16); + + /* Evolved raid pack — close NE, overlapping Survivor line */ + float rx = wx + 88.0f, ry = wy - 48.0f; + + /* forward fortifications at the clash so a tight combat crop still shows buildings */ + { + int ftx, fty; + map_world_to_tile(rx, ry, &ftx, &fty); + bld_spawn(g, BT_TURRET, human, ftx - 2, fty + 1); + bld_spawn(g, BT_TURRET, human, ftx + 1, fty + 2); + bld_spawn(g, BT_TURRET, human, ftx - 1, fty - 2); + bld_spawn(g, BT_TURRET, human, ftx + 2, fty - 1); + bld_spawn(g, BT_WALL, human, ftx - 3, fty); + bld_spawn(g, BT_WALL, human, ftx - 3, fty + 1); + bld_spawn(g, BT_WALL, human, ftx - 3, fty - 1); + bld_spawn(g, BT_WALL, human, ftx + 2, fty + 1); + bld_spawn(g, BT_WALL, human, ftx + 2, fty); + bld_spawn(g, BT_WALL, human, ftx + 3, fty - 1); + bld_spawn(g, BT_POWER, human, ftx - 4, fty + 2); + bld_force_complete(g); /* complete any partial builds from spawn */ + } + + /* denser clash scars: road core + rubble ring + scrap flecks (arid fight plate) */ + { + int ctx, cty; + map_world_to_tile(rx, ry, &ctx, &cty); + for (int dy = -6; dy <= 6; dy++) { + for (int dx = -7; dx <= 7; dx++) { + int tx = ctx + dx, ty = cty + dy; + if (!map_in_bounds(&g->map, tx, ty)) continue; + Tile *t = map_tile(&g->map, tx, ty); + if (!t || !t->walkable) continue; + int d2 = dx * dx + dy * dy; + if (d2 <= 3) { + t->terrain = T_ROAD; + t->scrap = 0; + } else if (d2 <= 12) { + t->terrain = T_RUBBLE; + t->scrap = 0; + } else if (d2 <= 28 && ((dx + dy * 3) & 3) == 0) { + t->terrain = T_SCRAP; + t->scrap = (uint8_t)(90 + ((dx * 13 + dy * 7) & 127)); + } else if (d2 <= 32 && ((dx * 5 + dy) & 2) == 0) { + t->terrain = T_RUBBLE; + t->scrap = 0; + } + } + } + /* approach road from HQ toward clash */ + int hx_t = sx, hy_t = sy; + int steps = abs(ctx - hx_t) + abs(cty - hy_t); + if (steps < 1) steps = 1; + for (int s = 0; s <= steps; s++) { + int tx = hx_t + (ctx - hx_t) * s / steps; + int ty = hy_t + (cty - hy_t) * s / steps; + if (!map_in_bounds(&g->map, tx, ty)) continue; + Tile *t = map_tile(&g->map, tx, ty); + if (!t || !t->walkable) continue; + t->terrain = T_ROAD; + t->scrap = 0; + } + } + + int raid[40]; + int rn = 0; + for (int i = 0; i < 8; i++) + raid[rn++] = unit_spawn(g, UT_MUTANT, enemy, + rx + (i % 4) * 18 - 10, ry + (i / 4) * 16 - 8); + for (int i = 0; i < 4; i++) + raid[rn++] = unit_spawn(g, UT_BEAST, enemy, rx + 30 + i * 16, ry - 14 + (i & 1) * 18); + raid[rn++] = unit_spawn(g, UT_BUGGY, enemy, rx + 8, ry - 28); + raid[rn++] = unit_spawn(g, UT_BUGGY, enemy, rx + 28, ry - 34); + raid[rn++] = unit_spawn(g, UT_BUGGY, enemy, rx + 48, ry - 22); + raid[rn++] = unit_spawn(g, UT_TANK, enemy, rx + 52, ry + 4); + raid[rn++] = unit_spawn(g, UT_TANK, enemy, rx + 68, ry - 12); + raid[rn++] = unit_spawn(g, UT_FLAME, enemy, rx + 22, ry + 28); + raid[rn++] = unit_spawn(g, UT_FLAME, enemy, rx + 40, ry + 34); + raid[rn++] = unit_spawn(g, UT_MISSILE, enemy, rx + 62, ry - 28); + raid[rn++] = unit_spawn(g, UT_MISSILE, enemy, rx + 78, ry - 8); + raid[rn++] = unit_spawn(g, UT_ARTILLERY, enemy, rx + 85, ry + 10); + for (int i = 0; i < 8; i++) + raid[rn++] = unit_spawn(g, UT_INFANTRY, enemy, + rx - 28 + (i % 4) * 14, ry - 8 + (i / 4) * 14); + + /* pre-damage for HP-bar variety (sim may add more) */ + for (int i = 0; i < g->unit_count; i++) { + Unit *u = &g->units[i]; + if (u->dead || u->max_hp <= 0) continue; + float dx = u->x - rx, dy = u->y - ry; + if (dx * dx + dy * dy > 140.0f * 140.0f) continue; + float f = 0.35f + 0.45f * ((i * 17) % 100) / 100.0f; + u->hp = u->max_hp * f; + } + + /* FX seeds at the clash locus */ + particle_burst(g, rx, ry, 28, 1, 255, 140, 40); + particle_burst(g, rx + 20, ry - 10, 18, 0, 90, 90, 90); + particle_burst(g, rx - 15, ry + 12, 22, 2, 255, 220, 80); + particle_burst(g, rx + 40, ry + 5, 14, 1, 255, 100, 30); + particle_burst(g, rx + 10, ry - 25, 16, 3, 80, 70, 60); + + /* point raid at nearest human combat units / HQ */ + int hq = -1; + for (int i = 0; i < g->bld_count; i++) + if (!g->buildings[i].dead && g->buildings[i].faction == human + && g->buildings[i].type == BT_BASE) { hq = i; break; } + int prey = -1; + for (int i = 0; i < g->unit_count; i++) { + Unit *u = &g->units[i]; + if (u->dead || u->faction != human) continue; + if (u->type == UT_HARVESTER || u->type == UT_MEDIC) continue; + prey = i; break; + } + for (int i = 0; i < rn; i++) { + if (raid[i] < 0) continue; + if (prey >= 0) unit_issue_attack_unit(g, raid[i], prey); + else if (hq >= 0) unit_issue_attack_bld(g, raid[i], hq); + } + + /* human combat units engage nearest raiders (spread targets) */ + int ridx = 0; + for (int i = 0; i < g->unit_count; i++) { + Unit *u = &g->units[i]; + if (u->dead || u->faction != human) continue; + if (u->type == UT_HARVESTER || u->type == UT_MEDIC) continue; + float dx = u->x - rx, dy = u->y - ry; + if (dx * dx + dy * dy > 220.0f * 220.0f) continue; + int tgt = -1; + for (int k = 0; k < rn; k++) { + int ri = raid[(ridx + k) % rn]; + if (ri >= 0 && !g->units[ri].dead) { tgt = ri; ridx += k + 1; break; } + } + if (tgt >= 0) unit_issue_attack_unit(g, i, tgt); + } + + /* select a front-line squad for yellow selection boxes in the shot */ + unit_clear_selection(g); + int nsel = 0; + for (int i = 0; i < g->unit_count && nsel < 8; i++) { + Unit *u = &g->units[i]; + if (u->dead || u->faction != human) continue; + if (u->type == UT_HARVESTER || u->type == UT_MEDIC) continue; + float dx = u->x - rx, dy = u->y - ry; + if (dx * dx + dy * dy > 120.0f * 120.0f) continue; + unit_select_single(g, i, 1); + nsel++; + } } void engine_setup_match(Game *g, int mission_id) { @@ -116,6 +329,8 @@ void engine_setup_match(Game *g, int mission_id) { place_start(g, g->human); place_start(g, enemy); + place_showcase(g, g->human, enemy); + bld_force_complete(g); /* fortress theme: wall the enemy base (leave a gate on the far edge) */ if (m->map_style == 3) { @@ -127,6 +342,7 @@ void engine_setup_match(Game *g, int mission_id) { if (dy == 4) continue; /* gate on the far edge */ bld_spawn(g, BT_WALL, enemy, sx + dx, sy + dy); } + bld_force_complete(g); } /* centre camera on player base */ diff --git a/engine-c/src/main.c b/engine-c/src/main.c index 5c42a88..1fb1a94 100644 --- a/engine-c/src/main.c +++ b/engine-c/src/main.c @@ -118,11 +118,48 @@ int main(int argc, char **argv) { * draw the world to an RGBA buffer with the software renderer and save a * PNG. Avoids the broken GPU readback on this box entirely. */ if (shot_mode) { - float pre = 8.0f; + /* Mid-fight default: short so the raid pack is still dense in-frame + * (env KKND_SHOT_T overrides; 3.5s emptied the clash before). */ + float pre = 1.0f; if (getenv("KKND_SHOT_T")) pre = (float)atof(getenv("KKND_SHOT_T")); float hdt = 1.0f / 30.0f; int steps = (int)(pre / hdt); for (int i = 0; i < steps; i++) engine_update(g, hdt); + /* Particle life is short (~0.5–1.2s); reseed FX at the live clash so + * the PNG shows explosions/smoke instead of a quiet mid-fight plate. */ + { + /* FX at raid clash near human HQ (ignore far-base 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; + float cx = 0, cy = 0; + int n = 0; + const float r2 = 220.0f * 220.0f; + 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 > r2) continue; + cx += u->x; cy += u->y; n++; + } + if (n > 0) { cx /= n; cy /= n; } + else { cx = clash_x; cy = clash_y; } + particle_burst(g, cx, cy, 56, 1, 255, 140, 40); + particle_burst(g, cx + 22, cy - 12, 36, 0, 110, 100, 90); + particle_burst(g, cx - 18, cy + 14, 40, 2, 255, 230, 90); + particle_burst(g, cx + 36, cy + 8, 30, 1, 255, 90, 30); + particle_burst(g, cx - 8, cy - 28, 32, 3, 95, 80, 65); + particle_burst(g, cx + 12, cy + 30, 28, 1, 255, 170, 55); + particle_burst(g, cx - 30, cy - 6, 26, 1, 255, 120, 40); + particle_burst(g, cx + 28, cy + 22, 24, 2, 255, 210, 80); + particle_burst(g, cx - 40, cy + 18, 22, 1, 255, 100, 35); + particle_burst(g, cx + 44, cy - 16, 20, 2, 255, 240, 100); + particle_burst(g, cx + 8, cy - 40, 18, 1, 255, 150, 45); + particle_burst(g, cx - 22, cy + 36, 18, 0, 120, 110, 95); + } char path[64]; snprintf(path, sizeof path, "shot_%04d.png", 1); snapshot_capture(g, path); engine_log("snapshot -> %s", path); diff --git a/engine-c/src/snapshot.c b/engine-c/src/snapshot.c index 9ac211d..f48cc75 100644 --- a/engine-c/src/snapshot.c +++ b/engine-c/src/snapshot.c @@ -2,20 +2,36 @@ * 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 >= SW || y >= SH) return; - int i = (y * SW + x) * 4; + 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; } @@ -24,70 +40,414 @@ static void rectf(int x0, int y0, int x1, int y1, uint8_t r, uint8_t g, uint8_t 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 uint8_t faction_r(Faction f){ return f==FACTION_SURVIVOR?70 : f==FACTION_EVOLVED?200 : 230; } -static uint8_t faction_g(Faction f){ return f==FACTION_SURVIVOR?180: f==FACTION_EVOLVED?70 : 60; } -static uint8_t faction_b(Faction f){ return f==FACTION_SURVIVOR?90 : f==FACTION_EVOLVED?210 : 230; } +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 terrain_color(TerrainType t, uint8_t *r, uint8_t *g, uint8_t *b) { - switch (t) { - case T_GRASS: *r=46; *g=92; *b=46; break; - case T_ROAD: *r=90; *g=90; *b=80; break; - case T_RUBBLE:*r=80; *g=72; *b=64; break; - case T_ROCK: *r=80; *g=80; *b=86; break; - case T_WATER: *r=40; *g=60; *b=120; break; - case T_SCRAP: *r=70; *g=64; *b=40; break; - default: *r=50; *g=50; *b=50; break; +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; } } } -void snapshot_capture(Game *g, const char *path) { - uint8_t *buf = (uint8_t *)malloc((size_t)SW * SH * 4); - if (!buf) return; - g_buf = buf; - for (int i = 0; i < SW*SH; i++) { buf[i*4]=0; buf[i*4+1]=0; buf[i*4+2]=0; buf[i*4+3]=255; } +/* 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; } - /* camera: centre on centroid of human units, else map centre */ - float cx = MAP_PIX_W/2, cy = MAP_PIX_H/2; - int nc = 0; - for (int i = 0; i < g->unit_count; i++) - if (!g->units[i].dead && g->units[i].faction == g->human) { cx += g->units[i].x; cy += g->units[i].y; nc++; } - if (nc) { cx /= nc; cy /= nc; } - float camx = cx - SW/2, camy = cy - SH/2; +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; + } +} + +/* 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; + #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); + + switch (u->type) { + case UT_INFANTRY: + disc(sx, sy, SI(9), ok, og, ob); + disc(sx, sy, SI(7), br, bg, bb); + disc(LX(15,11), LY(15,11), SI(4), lr, lg, lb); + line(sx, sy, LX(15,1), LY(15,1), 40, 36, 30); + break; + case UT_BUGGY: + 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(sx-SI(8), sy-SI(4), SI(16), SI(8), lr, lg, lb); + 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); + line(sx, sy, LX(29,15), LY(29,15), 40, 36, 30); + break; + case UT_TANK: + fill_rect(sx-SI(13), sy-SI(7), SI(26), SI(14), ok, og, ob); + fill_rect(sx-SI(11), sy-SI(5), SI(22), SI(10), br, bg, bb); + disc(sx, sy, SI(7), lr, lg, lb); + line(sx, sy, LX(30,15), LY(30,15), 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); + break; + case UT_ARTILLERY: + 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), lr, lg, lb); + line(sx, sy, LX(3,3), LY(3,3), 50, 45, 35); + line(sx, sy, LX(27,27), LY(27,27), 50, 45, 35); + break; + 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), br, bg, bb); + fill_rect(sx-SI(10), sy-SI(5), SI(20), SI(10), 200, 170, 90); + disc(LX(8,23), LY(8,23), SI(4), mr, mg, mb); + disc(LX(22,23), LY(22,23), SI(4), mr, mg, mb); + line(LX(5,7), LY(5,7), LX(5,2), LY(5,2), lr, lg, lb); + break; + case UT_JEEP: + 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); + disc(LX(9,22), LY(9,22), SI(4), mr, mg, mb); + disc(LX(21,22), LY(21,22), SI(4), mr, mg, mb); + line(LX(15,11), LY(15,11), LX(15,3), LY(15,3), lr, lg, lb); + line(sx, sy, LX(28,15), LY(28,15), 50, 45, 35); + break; + case UT_MUTANT: + disc(sx, sy+SI(1), SI(10), ok, og, ob); + disc(sx, sy-SI(1), SI(8), br, bg, bb); + disc(LX(15,10), LY(15,10), SI(4), lr, lg, lb); + line(sx, sy+SI(1), LX(24,9), LY(24,9), 40, 36, 30); + break; + case UT_BEAST: + 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); + disc(LX(23,12), LY(23,12), SI(5), lr, lg, lb); + break; + case UT_RAVAGER: + fill_rect(sx-SI(15), sy-SI(8), SI(30), SI(16), ok, og, ob); + fill_rect(sx-SI(13), sy-SI(6), SI(26), SI(12), br, bg, bb); + disc(sx, sy, SI(8), lr, lg, lb); + line(sx, sy, LX(29,15), LY(29,15), 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); + break; + case UT_FLAME: + fill_rect(sx-SI(10), sy-SI(4), SI(20), SI(15), ok, og, ob); + fill_rect(sx-SI(8), sy-SI(2), SI(16), SI(11), br, bg, bb); + disc(LX(15,10), LY(15,10), SI(5), lr, lg, lb); + line(sx, sy, LX(28,20), LY(28,20), 255, 150, 50); + break; + case UT_MISSILE: + 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); + disc(LX(9,23), LY(9,23), SI(4), mr, mg, mb); + disc(LX(21,23), LY(21,23), SI(4), mr, mg, mb); + 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); + 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(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: + fill_rect(sx + 5, sy + 5, W - 10, H - 10, ok, og, ob); + fill_rect(sx + 7, sy + 7, W - 14, H - 14, br, bg, bb); + disc(sx + W/2, sy + H/2, W/4, lr, lg, lb); + disc(sx + W/2, sy + H/2, W/6, 255, 250, 180); + line(sx + W/2, sy + H/2, sx + W/2, sy + 4, 220, 210, 80); + fill_rect(sx + W/2 - 2, sy + 4, 4, H/3, 200, 180, 60); + break; + case BT_WARFACTORY: + fill_rect(sx + 5, sy + 5, W - 10, H - 10, ok, og, ob); + fill_rect(sx + 7, sy + 7, W - 14, H - 14, br, bg, bb); + fill_rect(sx + 10, sy + 10, W - 20, 12, lr, lg, lb); + fill_rect(sx + 10, sy + H - 24, W - 20, 12, lr, lg, lb); + fill_rect(sx + W/2 - 4, sy + 8, 8, H - 16, 35, 35, 40); + break; + case BT_REFINERY: + disc(sx + W/2, sy + H/2, W/4 + 2, ok, og, ob); + disc(sx + W/2, sy + H/2, W/4, 200, 165, 85); + ring(sx + W/2, sy + H/2, W/4 - 3, 3, 90, 70, 45); + fill_rect(sx + 8, sy + H - 14, W - 16, 8, 140, 110, 55); + break; + case BT_POWER: + disc(sx + W/2, sy + H/2, W/3 + 1, ok, og, ob); + disc(sx + W/2, sy + H/2, W/3, 100, 160, 210); + 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, 230, 245, 255); + disc(sx + W/2, sy + H/2, 4, 250, 252, 255); + break; + case BT_TURRET: + fill_rect(sx + 4, sy + H - 10, W - 8, 8, 55, 50, 42); + disc(sx + W/2, sy + H/2, W/3 + 1, ok, og, ob); + disc(sx + W/2, sy + H/2, W/3, br, bg, bb); + disc(sx + W/2, sy + H/2, W/5, lr, lg, lb); + line(sx + W/2, sy + H/2, sx + W/2 + 14, sy + H/2 - 14, 30, 28, 24); + disc(sx + W/2 + 12, sy + H/2 - 12, 2, 255, 200, 80); + break; + case BT_WALL: + fill_rect(sx + 1, sy + 1, W - 2, H - 2, ok, og, ob); + fill_rect(sx + 2, sy + 2, W - 4, H - 4, 175, 155, 120); + fill_rect(sx + 3, sy + 3, W - 6, 5, 210, 190, 150); + line(sx + 2, sy + 2, sx + W - 2, sy + H - 2, 60, 52, 42); + break; + case BT_REPAIR: + fill_rect(sx + 7, sy + 7, W - 14, H - 14, ok, og, ob); + fill_rect(sx + 9, sy + 9, W - 18, H - 18, br, bg, bb); + ring(sx + W/2, sy + H/2, W/4, 3, 120, 220, 120); + line(sx + W/2 - 12, sy + H/2, sx + W/2 + 12, sy + H/2, 120, 220, 120); + line(sx + W/2, sy + H/2 - 12, sx + W/2, sy + H/2 + 12, 120, 220, 120); + break; + case BT_RESEARCH: + fill_rect(sx + 7, sy + 7, W - 14, H - 14, ok, og, ob); + fill_rect(sx + 9, sy + 9, W - 18, H - 18, br, bg, bb); + disc(sx + W/2, sy + H/2, W/5, 210, 180, 80); + line(sx + W/2, sy + 6, sx + W/2, sy + H - 6, 230, 230, 160); + line(sx + 6, sy + H/2, sx + W - 6, sy + H/2, 230, 230, 160); + break; + default: + break; + } + if (b->build < 1.0f) + fill_rect(sx, sy, W, (int)(H * b->build), 255, 255, 255); +} + +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 - SW) camx = MAP_PIX_W - SW; - if (camy > MAP_PIX_H - SH) camy = MAP_PIX_H - SH; + 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 + SW) / TILE, ty1 = (int)(camy + SH) / 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_W) continue; + if (tx < 0 || ty < 0 || tx >= MAP_W || ty >= MAP_H) continue; Tile *t = map_tile(&g->map, tx, ty); - uint8_t r,g,b; terrain_color(t->terrain, &r,&g,&b); + 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, g, b); + 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/20;k++) - disc(cc + ((k*7)%10)-5, cr + ((k*5)%10)-5, 2, 150,140,90); + for (int k=0;kscrap/16;k++) + disc(cc + ((k*7)%12)-6, cr + ((k*5)%12)-6, 3, 235,190,85); } } } - /* fog overlay */ + /* 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_W) continue; + 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 == 0) { int sx=tx*TILE-(int)camx, sy=ty*TILE-(int)camy; rectf(sx,sy,sx+TILE-1,sy+TILE-1,0,0,0); } - else if (f == 1) { int sx=tx*TILE-(int)camx, sy=ty*TILE-(int)camy; rectf(sx,sy,sx+TILE-1,sy+TILE-1,0,0,30); } + 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 */ @@ -96,11 +456,8 @@ void snapshot_capture(Game *g, const char *path) { 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 > SW || sy > SH) continue; - uint8_t r=faction_r(b->faction),gg=faction_g(b->faction),bb=faction_b(b->faction); - rectf(sx, sy, sx+w-1, sy+h-1, r/2+30, gg/2+30, bb/2+30); - rectf(sx+2, sy+2, sx+w-3, sy+h-3, r, gg, bb); - if (b->build < 1.0f) rectf(sx, sy, sx+w-1, sy+(int)(h*b->build)-1, 255,255,255); + if (sx+w < 0 || sy+h < 0 || sx > g_sw || sy > g_sh) continue; + draw_bld(b, sx, sy); } /* units */ @@ -108,41 +465,67 @@ void snapshot_capture(Game *g, const char *path) { Unit *u = &g->units[i]; if (u->dead) continue; int sx = (int)(u->x - camx), sy = (int)(u->y - camy); - if (sx < -10 || sy < -10 || sx > SW+10 || sy > SH+10) continue; - uint8_t r=faction_r(u->faction),gg=faction_g(u->faction),bb=faction_b(u->faction); - int rad = u->type==UT_TANK||u->type==UT_BEAST?9:u->type==UT_HARVESTER?8:6; - disc(sx, sy, rad, r, gg, bb); - disc(sx, sy, 2, 255,255,255); - int hx = sx + (int)(cosf(u->heading)*rad), hy = sy + (int)(sinf(u->heading)*rad); - for (int k=0;kselected) { rectf(sx-rad-2,sy-rad-2,sx+rad+2,sy+rad+2,255,255,120); } + if (sx < -20 || sy < -20 || sx > g_sw+20 || sy > g_sh+20) continue; + draw_unit(u, sx, sy); } - /* projectiles */ + /* projectiles — bright tracers / shells */ 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); - px(sx,sy,255,230,120); px(sx+1,sy,255,230,120); + 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); + } } - /* minimap */ - int mmx=SW-210, mmy=10, mms=200; - rectf(mmx-2,mmy-2,mmx+mms+1,mmy+mms+1,0,0,0); - for (int ty=0;tymap.fog[ty*MAP_W+tx]; - if (f==0) continue; - Tile *t=map_tile(&g->map,tx,ty); - uint8_t r,g,b; terrain_color(t->terrain,&r,&g,&b); - int dx=mmx + tx*mms/MAP_W, dy=mmy + ty*mms/MAP_W; - px(dx,dy,r,g,b); + /* 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); } - for (int i=0;ibld_count;i++){ Building*b=&g->buildings[i]; if(b->dead)continue; - int dx=mmx+b->tx*TILE*mms/(MAP_W*TILE), dy=mmy+b->ty*TILE*mms/(MAP_W*TILE); - px(dx,dy,faction_r(b->faction),faction_g(b->faction),faction_b(b->faction)); } - for (int i=0;iunit_count;i++){ Unit*u=&g->units[i]; if(u->dead)continue; - int dx=mmx+(int)(u->x*mms/MAP_PIX_W), dy=mmy+(int)(u->y*mms/MAP_PIX_H); - px(dx,dy,faction_r(u->faction),faction_g(u->faction),faction_b(u->faction)); } + /* No shot minimap — keeps the crop on the fight plate. */ - png_save(path, SW, SH, buf); - free(buf); + /* 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; } diff --git a/engine-c/src/unit_card.c b/engine-c/src/unit_card.c index da11e33..eda54eb 100644 --- a/engine-c/src/unit_card.c +++ b/engine-c/src/unit_card.c @@ -2,7 +2,6 @@ #include #include #include -#include // Create a new unit card UnitCard* create_unit_card(const char* name, const char* faction, diff --git a/kitchen/AGENT_LOG.md b/kitchen/AGENT_LOG.md index 0e6a893..5c85e8e 100644 --- a/kitchen/AGENT_LOG.md +++ b/kitchen/AGENT_LOG.md @@ -41,3 +41,6 @@ - **2026-09-14T06:51:58Z** · stall-locks answered · **ok** — bump STATUS on triad; no new milestones; triad=ACTIVITY+outbox+AGENT_LOG+STATUS; verify disk - **2026-09-14T06:51:58Z** · F1/F2 · **ok** — R3 ACTIVITY + existing outbox flamewar/*-r3-* · Castopod draft-only (404) - **2026-09-14T06:51:58Z** · STATUS · **ok** — round=3 · freigabe this-repo always-push openkknd-agent · parent TODO=356 + +- **2026-09-19T05:28:54Z** · CLAIM Spur A @local-model/engine-L2-grok-4.5 · **ACTIVE** — engine-c/src/engine.c + snapshot.c; TODO365 KKND2-like showcase (units/bld/combat near camera) +- **2026-09-19T08:04:10Z** · RELEASE Spur A @local-model/engine-L2-grok-4.5 · **DONE** — dense arid showcase + type-aware snapshot; smoke PASS; shortsha TBD after commit diff --git a/kitchen/COLLAB-SIGNUP.md b/kitchen/COLLAB-SIGNUP.md index 2793950..06e91d6 100644 --- a/kitchen/COLLAB-SIGNUP.md +++ b/kitchen/COLLAB-SIGNUP.md @@ -24,7 +24,7 @@ Core of **TODO=365**: collaborate to **advance the game** and **push directly** | When (UTC) | Agent / author slug | Spur | Paths (short) | Status | Note | |------------|---------------------|------|---------------|--------|------| -| — | — | — | — | FREE | next agent: add a row, then claim in AGENT_LOG | +| 2026-09-19T05:28Z | local-model/engine-L2-grok-4.5 | A | engine-c/src/engine.c,snapshot.c | DONE | smoke PASS; arid clash showcase (lime vs magenta); shot_0001.png |