366 lines
18 KiB
C
366 lines
18 KiB
C
#include "kknd.h"
|
||
#include <unistd.h>
|
||
|
||
/* =========================================================================
|
||
* main.c - Entry point, headless sim mode and the real-time game loop.
|
||
* ========================================================================= */
|
||
|
||
static void print_stats(Game *g) {
|
||
Faction human = g->human, enemy = g->enemy;
|
||
int hu = 0, eu = 0, hb = 0, eb = 0;
|
||
for (int i = 0; i < g->unit_count; i++) {
|
||
if (g->units[i].dead) continue;
|
||
if (g->units[i].faction == human) hu++;
|
||
else if (g->units[i].faction == enemy) eu++;
|
||
}
|
||
for (int i = 0; i < g->bld_count; i++) {
|
||
if (g->buildings[i].dead) continue;
|
||
if (g->buildings[i].faction == human) hb++;
|
||
else if (g->buildings[i].faction == enemy) eb++;
|
||
}
|
||
const char *st = (g->state == GAME_WON) ? "WON" :
|
||
(g->state == GAME_LOST) ? "LOST" :
|
||
(g->state == GAME_PAUSED) ? "PAUSED" : "PLAY";
|
||
printf("[t=%6.1f] state=%-6s %s u=%3d b=%2d scrap=%5d | %s u=%3d b=%2d scrap=%5d | bullets=%d parts=%d obj=%s\n",
|
||
g->time, st, faction_name(human), hu, hb, g->scrap[human],
|
||
faction_name(enemy), eu, eb, g->scrap[enemy], g->bullet_count, g->particle_count,
|
||
g->objective == OBJ_SURVIVE ? "survive" :
|
||
g->objective == OBJ_GATHER ? "scrap" : "destroy");
|
||
fflush(stdout);
|
||
}
|
||
|
||
static void save_snapshot(Game *g, int n) {
|
||
char path[64];
|
||
snprintf(path, sizeof path, "shot_%04d.png", n);
|
||
void *pixels = malloc((size_t)VIEW_W * VIEW_H * 4);
|
||
if (!pixels) return;
|
||
if (SDL_RenderReadPixels(g->ren, NULL, SDL_PIXELFORMAT_RGBA32,
|
||
pixels, VIEW_W * 4) == 0) {
|
||
if (png_save(path, VIEW_W, VIEW_H, (const uint8_t *)pixels) == 0)
|
||
engine_log("snapshot -> %s", path);
|
||
}
|
||
free(pixels);
|
||
}
|
||
|
||
int main(int argc, char **argv) {
|
||
Game *g = (Game *)malloc(sizeof(Game));
|
||
if (!g) { fprintf(stderr, "Out of memory\n"); return 1; }
|
||
|
||
int headless = 0; float sim_seconds = 0;
|
||
Difficulty diff = DIFF_NORMAL;
|
||
int mission = 0;
|
||
int loaded = 0;
|
||
float speed = 1.0f;
|
||
Faction faction_override = FACTION_NONE;
|
||
for (int i = 1; i < argc; i++) {
|
||
if (strcmp(argv[i], "--speed") == 0 && i + 1 < argc)
|
||
speed = (float)atof(argv[++i]);
|
||
}
|
||
for (int i = 1; i < argc; i++) {
|
||
if (strcmp(argv[i], "--easy") == 0) diff = DIFF_EASY;
|
||
else if (strcmp(argv[i], "--hard") == 0) diff = DIFF_HARD;
|
||
else if (strcmp(argv[i], "--mission") == 0 && i + 1 < argc)
|
||
mission = atoi(argv[++i]);
|
||
else if (strcmp(argv[i], "--headless") == 0 && i + 1 < argc)
|
||
{ headless = 1; sim_seconds = (float)atof(argv[++i]); }
|
||
else if (strcmp(argv[i], "--auto") == 0)
|
||
{ g_auto = 1; }
|
||
else if (strcmp(argv[i], "--faction") == 0 && i + 1 < argc) {
|
||
i++;
|
||
if (!strcmp(argv[i], "evolved")) faction_override = FACTION_EVOLVED;
|
||
else if (!strcmp(argv[i], "series9")) faction_override = FACTION_SERIES9;
|
||
else faction_override = FACTION_SURVIVOR;
|
||
}
|
||
else if (strcmp(argv[i], "--load") == 0 && i + 1 < argc) {
|
||
g_headless = headless;
|
||
engine_init(g, KKND_NAME);
|
||
engine_setup_match(g, mission);
|
||
load_game(g, argv[++i]);
|
||
loaded = 1;
|
||
}
|
||
}
|
||
|
||
g_headless = headless;
|
||
if (getenv("KKND_SHOT")) g_headless = 1; /* snapshot mode = pure software, no SDL */
|
||
if (!loaded) engine_init(g, KKND_NAME);
|
||
if (faction_override != FACTION_NONE) g->human = faction_override;
|
||
if (getenv("KKND_SPEED")) speed = (float)atof(getenv("KKND_SPEED"));
|
||
g->speed_mul = speed;
|
||
/* load campaign progress (highest unlocked mission id + 1) */
|
||
{
|
||
FILE *pf = fopen("progress.kknd", "r");
|
||
if (pf) { int u = 0; if (fscanf(pf, "%d", &u) == 1 && u > 0) g->unlocked = u; fclose(pf); }
|
||
}
|
||
int shot_mode = (!headless && getenv("KKND_SHOT")) ? 1 : 0;
|
||
if (shot_mode) engine_setup_match(g, mission);
|
||
|
||
if (headless) {
|
||
if (!loaded) engine_setup_match(g, mission);
|
||
engine_log("Headless simulation for %.0f s (mission %d)", sim_seconds, mission);
|
||
float t = 0, step = 1.0f/30.0f, stat = 0;
|
||
print_stats(g);
|
||
while (t < sim_seconds && g->state == GAME_PLAYING) {
|
||
engine_update(g, step);
|
||
t += step; stat += step;
|
||
if (stat >= 1.0f) { print_stats(g); stat -= 1.0f; }
|
||
}
|
||
print_stats(g);
|
||
engine_log("Simulation complete (winner=%s)",
|
||
g->winner ? faction_name(g->winner) : "none");
|
||
engine_shutdown(g);
|
||
free(g);
|
||
return 0;
|
||
}
|
||
|
||
engine_log("%s %s - choose a mission, SPACE pause, F5 save, RMB command", KKND_NAME, KKND_VERSION);
|
||
|
||
/* Snapshot mode: fast-forward the simulation headlessly (no SDL), then
|
||
* 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) {
|
||
/* 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; }
|
||
/* denser mid-fight plate: fire cores + smoke columns + sparks */
|
||
particle_burst(g, cx, cy, 72, 1, 255, 140, 40);
|
||
particle_burst(g, cx + 22, cy - 12, 48, 0, 110, 100, 90);
|
||
particle_burst(g, cx - 18, cy + 14, 52, 2, 255, 230, 90);
|
||
particle_burst(g, cx + 36, cy + 8, 40, 1, 255, 90, 30);
|
||
particle_burst(g, cx - 8, cy - 28, 42, 3, 95, 80, 65);
|
||
particle_burst(g, cx + 12, cy + 30, 36, 1, 255, 170, 55);
|
||
particle_burst(g, cx - 30, cy - 6, 34, 1, 255, 120, 40);
|
||
particle_burst(g, cx + 28, cy + 22, 32, 2, 255, 210, 80);
|
||
particle_burst(g, cx - 40, cy + 18, 30, 1, 255, 100, 35);
|
||
particle_burst(g, cx + 44, cy - 16, 28, 2, 255, 240, 100);
|
||
particle_burst(g, cx + 8, cy - 40, 26, 1, 255, 150, 45);
|
||
particle_burst(g, cx - 22, cy + 36, 26, 0, 120, 110, 95);
|
||
particle_burst(g, cx + 52, cy + 4, 24, 1, 255, 110, 30);
|
||
particle_burst(g, cx - 48, cy - 14, 22, 0, 100, 95, 85);
|
||
particle_burst(g, cx + 16, cy + 48, 20, 3, 85, 70, 55);
|
||
particle_burst(g, cx - 12, cy - 48, 20, 1, 255, 160, 50);
|
||
/* building smoke / fire columns near clash (HQ, walls, turrets) */
|
||
for (int i = 0; i < g->bld_count; i++) {
|
||
Building *bl = &g->buildings[i];
|
||
if (bl->dead || bl->build < 1.0f) continue;
|
||
float bx = (float)(bl->tx * TILE + bl->w * TILE / 2);
|
||
float by = (float)(bl->ty * TILE + bl->h * TILE / 2);
|
||
float bdx = bx - clash_x, bdy = by - clash_y;
|
||
if (bdx * bdx + bdy * bdy > r2) continue;
|
||
int smoke_n = (bl->type == BT_BASE || bl->type == BT_TURRET) ? 18 : 12;
|
||
particle_burst(g, bx, by - 10.0f, smoke_n, 0, 105, 95, 85);
|
||
if (bl->hp < bl->max_hp * 0.85f || bl->type == BT_TURRET) {
|
||
particle_burst(g, bx + 4.0f, by - 6.0f, 14, 1, 255, 130, 40);
|
||
particle_burst(g, bx - 3.0f, by - 14.0f, 8, 3, 90, 75, 60);
|
||
}
|
||
}
|
||
/* muzzle flashes on units in the clash radius */
|
||
{
|
||
int muz = 0;
|
||
for (int i = 0; i < g->unit_count && muz < 28; i++) {
|
||
Unit *u = &g->units[i];
|
||
if (u->dead) continue;
|
||
float ux = u->x - clash_x, uy = u->y - clash_y;
|
||
if (ux * ux + uy * uy > r2) continue;
|
||
if ((i + muz) % 3 != 0) continue;
|
||
particle_burst(g, u->x + 6.0f, u->y - 4.0f, 6, 2, 255, 230, 120);
|
||
particle_burst(g, u->x + 4.0f, u->y - 2.0f, 4, 1, 255, 160, 50);
|
||
muz++;
|
||
}
|
||
}
|
||
/* Mid-flight shells/missiles/lasers: after ~1s sim most live
|
||
* bullets have already impacted. Reseed between nearby opposing
|
||
* pairs so the PNG shows KKND2-like tracers across the clash. */
|
||
{
|
||
int spawned = 0;
|
||
const float pair_r2 = 180.0f * 180.0f;
|
||
for (int i = 0; i < g->unit_count && spawned < 72; i++) {
|
||
Unit *a = &g->units[i];
|
||
if (a->dead) continue;
|
||
float ax = a->x - clash_x, ay = a->y - clash_y;
|
||
if (ax * ax + ay * ay > r2) continue;
|
||
int shots = 0;
|
||
for (int j = 0; j < g->unit_count && spawned < 72 && shots < 3; j++) {
|
||
Unit *b = &g->units[j];
|
||
if (b->dead || b->faction == a->faction) continue;
|
||
float dx = b->x - a->x, dy = b->y - a->y;
|
||
float d2 = dx * dx + dy * dy;
|
||
if (d2 > pair_r2 || d2 < 20.0f * 20.0f) continue;
|
||
int kind = (spawned % 4 == 0) ? 3
|
||
: (spawned % 4 == 1) ? 1
|
||
: (spawned % 4 == 2) ? 2 : 0;
|
||
float t = 0.18f + 0.60f * ((spawned * 37) % 100) / 100.0f;
|
||
float sx = a->x + dx * t;
|
||
float sy = a->y + dy * t;
|
||
bullet_spawn(g, sx, sy, b->id, -1, 1.0f, a->faction, kind);
|
||
spawned++;
|
||
shots++;
|
||
}
|
||
}
|
||
/* turret / base guns → nearest enemy in clash */
|
||
for (int i = 0; i < g->bld_count && spawned < 88; i++) {
|
||
Building *bl = &g->buildings[i];
|
||
if (bl->dead || (bl->type != BT_TURRET && bl->type != BT_BASE))
|
||
continue;
|
||
float bx = (float)(bl->tx * TILE + bl->w * TILE / 2);
|
||
float by = (float)(bl->ty * TILE + bl->h * TILE / 2);
|
||
float bdx = bx - clash_x, bdy = by - clash_y;
|
||
if (bdx * bdx + bdy * bdy > r2) continue;
|
||
int best = -1; float bestd = 1e12f;
|
||
for (int j = 0; j < g->unit_count; j++) {
|
||
Unit *u = &g->units[j];
|
||
if (u->dead || u->faction == bl->faction) continue;
|
||
float dx = u->x - bx, dy = u->y - by;
|
||
float d2 = dx * dx + dy * dy;
|
||
if (d2 < bestd && d2 < pair_r2) { bestd = d2; best = j; }
|
||
}
|
||
if (best < 0) continue;
|
||
Unit *tgt = &g->units[best];
|
||
float dx = tgt->x - bx, dy = tgt->y - by;
|
||
float t = 0.25f + 0.45f * ((spawned * 19) % 100) / 100.0f;
|
||
bullet_spawn(g, bx + dx * t, by + dy * t, tgt->id, -1,
|
||
2.0f, bl->faction, (spawned & 1) ? 1 : 2);
|
||
particle_burst(g, bx, by - 4.0f, 5, 2, 255, 220, 100);
|
||
spawned++;
|
||
}
|
||
}
|
||
/* secondary SE clash: lingering columns + tracers */
|
||
float clash2_x = hx + 40.0f, clash2_y = hy + 70.0f;
|
||
float c2x = clash2_x, c2y = clash2_y;
|
||
int n2 = 0;
|
||
float sx2 = 0, sy2 = 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 - clash2_x, dy = u->y - clash2_y;
|
||
if (dx * dx + dy * dy > 140.0f * 140.0f) continue;
|
||
sx2 += u->x; sy2 += u->y; n2++;
|
||
}
|
||
if (n2 > 0) { c2x = sx2 / n2; c2y = sy2 / n2; }
|
||
particle_burst(g, c2x, c2y, 52, 1, 255, 135, 35);
|
||
particle_burst(g, c2x + 18, c2y - 10, 36, 0, 115, 105, 90);
|
||
particle_burst(g, c2x - 14, c2y + 12, 40, 2, 255, 220, 85);
|
||
particle_burst(g, c2x + 26, c2y + 8, 30, 1, 255, 95, 28);
|
||
particle_burst(g, c2x - 6, c2y - 20, 32, 3, 90, 78, 62);
|
||
particle_burst(g, c2x + 10, c2y + 24, 28, 1, 255, 160, 50);
|
||
particle_burst(g, c2x - 28, c2y + 8, 24, 0, 100, 95, 80);
|
||
particle_burst(g, c2x + 32, c2y - 16, 22, 1, 255, 120, 40);
|
||
{
|
||
int spawned2 = 0;
|
||
const float pair_r2 = 160.0f * 160.0f;
|
||
const float r2b = 140.0f * 140.0f;
|
||
for (int i = 0; i < g->unit_count && spawned2 < 48; i++) {
|
||
Unit *a = &g->units[i];
|
||
if (a->dead) continue;
|
||
float ax = a->x - clash2_x, ay = a->y - clash2_y;
|
||
if (ax * ax + ay * ay > r2b) continue;
|
||
int shots = 0;
|
||
for (int j = 0; j < g->unit_count && spawned2 < 48 && shots < 3; j++) {
|
||
Unit *b = &g->units[j];
|
||
if (b->dead || b->faction == a->faction) continue;
|
||
float dx = b->x - a->x, dy = b->y - a->y;
|
||
float d2 = dx * dx + dy * dy;
|
||
if (d2 > pair_r2 || d2 < 18.0f * 18.0f) continue;
|
||
int kind = (spawned2 % 4 == 0) ? 3
|
||
: (spawned2 % 4 == 1) ? 1
|
||
: (spawned2 % 4 == 2) ? 2 : 0;
|
||
float t = 0.18f + 0.58f * ((spawned2 * 41) % 100) / 100.0f;
|
||
bullet_spawn(g, a->x + dx * t, a->y + dy * t,
|
||
b->id, -1, 1.0f, a->faction, kind);
|
||
spawned2++;
|
||
shots++;
|
||
}
|
||
}
|
||
for (int i = 0; i < g->bld_count && spawned2 < 56; i++) {
|
||
Building *bl = &g->buildings[i];
|
||
if (bl->dead || (bl->type != BT_TURRET && bl->type != BT_BASE))
|
||
continue;
|
||
float bx = (float)(bl->tx * TILE + bl->w * TILE / 2);
|
||
float by = (float)(bl->ty * TILE + bl->h * TILE / 2);
|
||
float bdx = bx - clash2_x, bdy = by - clash2_y;
|
||
if (bdx * bdx + bdy * bdy > r2b) continue;
|
||
int best = -1; float bestd = 1e12f;
|
||
for (int j = 0; j < g->unit_count; j++) {
|
||
Unit *u = &g->units[j];
|
||
if (u->dead || u->faction == bl->faction) continue;
|
||
float dx = u->x - bx, dy = u->y - by;
|
||
float d2 = dx * dx + dy * dy;
|
||
if (d2 < bestd && d2 < pair_r2) { bestd = d2; best = j; }
|
||
}
|
||
if (best < 0) continue;
|
||
Unit *tgt = &g->units[best];
|
||
float dx = tgt->x - bx, dy = tgt->y - by;
|
||
float t = 0.26f + 0.44f * ((spawned2 * 23) % 100) / 100.0f;
|
||
bullet_spawn(g, bx + dx * t, by + dy * t, tgt->id, -1,
|
||
2.0f, bl->faction, (spawned2 & 1) ? 1 : 2);
|
||
spawned2++;
|
||
}
|
||
}
|
||
}
|
||
char path[64]; snprintf(path, sizeof path, "shot_%04d.png", 1);
|
||
snapshot_capture(g, path);
|
||
engine_log("snapshot -> %s", path);
|
||
_exit(0);
|
||
}
|
||
|
||
Uint32 last = SDL_GetTicks();
|
||
int running = 1;
|
||
while (running) {
|
||
SDL_Event e;
|
||
while (SDL_PollEvent(&e)) {
|
||
if (e.type == SDL_QUIT) { running = 0; break; }
|
||
input_handle(g, &e);
|
||
}
|
||
if (!running) break;
|
||
Uint32 now = SDL_GetTicks();
|
||
float dt = (float)(now - last) / 1000.0f;
|
||
last = now;
|
||
if (dt > 0.05f) dt = 0.05f;
|
||
if (g->state != GAME_MENU) {
|
||
int prev_state = g->state;
|
||
input_camera(g, dt);
|
||
engine_update(g, dt);
|
||
if (g->state == GAME_WON && prev_state != GAME_WON) {
|
||
int next = g->mission_id + 2; /* unlock the following mission */
|
||
if (next > g->unlocked) {
|
||
g->unlocked = next;
|
||
FILE *pf = fopen("progress.kknd", "w");
|
||
if (pf) { fprintf(pf, "%d\n", g->unlocked); fclose(pf); }
|
||
engine_log("Unlocked next mission (%d)", g->unlocked - 1);
|
||
}
|
||
g->menu_sel = (g->mission_id + 1 < g->unlocked) ? g->mission_id + 1 : g->mission_id;
|
||
}
|
||
}
|
||
render_frame(g);
|
||
}
|
||
|
||
save_game(g, "savegame.kknd");
|
||
engine_shutdown(g);
|
||
free(g);
|
||
return 0;
|
||
}
|