engine-c: arid KKND2-like showcase shot + kitchen claim DONE
This commit is contained in:
parent
575d7d29b8
commit
b5a89c9b47
11 changed files with 739 additions and 84 deletions
|
|
@ -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
|
||||
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]"
|
||||
|
|
|
|||
|
|
@ -19,8 +19,11 @@
|
|||
#include <time.h>
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_image.h>
|
||||
#include <SDL2/SDL_ttf.h>
|
||||
/* SDL_image is optional; engine uses procedural sprites + custom PNG writer. */
|
||||
#if defined(KKND_HAVE_SDL_IMAGE) && KKND_HAVE_SDL_IMAGE
|
||||
#include <SDL2/SDL_image.h>
|
||||
#endif
|
||||
|
||||
#include "unit_card.h"
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// Header file for unit card implementation
|
||||
|
||||
#include <stdint.h>
|
||||
#include <immintrin.h>
|
||||
#include <stddef.h>
|
||||
|
||||
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" {
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 38 KiB |
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 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 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);
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;k<t->scrap/20;k++)
|
||||
disc(cc + ((k*7)%10)-5, cr + ((k*5)%10)-5, 2, 150,140,90);
|
||||
for (int k=0;k<t->scrap/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;k<rad;k++){ int xx=sx+(hx-sx)*k/rad, yy=sy+(hy-sy)*k/rad; px(xx,yy,230,230,230); }
|
||||
if (u->selected) { 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;ty<MAP_W;ty+=2) for (int tx=0;tx<MAP_W;tx+=2) {
|
||||
int f=g->map.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;i<g->bld_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;i<g->unit_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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <immintrin.h>
|
||||
|
||||
// Create a new unit card
|
||||
UnitCard* create_unit_card(const char* name, const char* faction,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
<!-- Example row:
|
||||
| 2026-09-17T12:00Z | local-model/engine-L2-jan-gemma4 | A | engine-c/… | ACTIVE | KEEP landed; unit X |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue