#include "kknd.h" #include /* ========================================================================= * 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; }