#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; } /* bigger impact blooms on plate: denser fire cores + smoke + debris */ particle_burst(g, cx, cy, 96, 1, 255, 140, 40); particle_burst(g, cx + 22, cy - 12, 64, 0, 110, 100, 90); particle_burst(g, cx - 18, cy + 14, 60, 2, 255, 230, 90); particle_burst(g, cx + 36, cy + 8, 52, 1, 255, 90, 30); particle_burst(g, cx - 8, cy - 28, 48, 3, 95, 80, 65); particle_burst(g, cx + 12, cy + 30, 48, 1, 255, 170, 55); particle_burst(g, cx - 30, cy - 6, 44, 1, 255, 120, 40); particle_burst(g, cx + 28, cy + 22, 40, 2, 255, 210, 80); particle_burst(g, cx - 40, cy + 18, 40, 1, 255, 100, 35); particle_burst(g, cx + 44, cy - 16, 36, 2, 255, 240, 100); particle_burst(g, cx + 8, cy - 40, 36, 1, 255, 150, 45); particle_burst(g, cx - 22, cy + 36, 34, 0, 120, 110, 95); particle_burst(g, cx + 52, cy + 4, 32, 1, 255, 110, 30); particle_burst(g, cx - 48, cy - 14, 30, 0, 100, 95, 85); particle_burst(g, cx + 16, cy + 48, 28, 3, 85, 70, 55); particle_burst(g, cx - 12, cy - 48, 28, 1, 255, 160, 50); particle_burst(g, cx + 18, cy + 14, 36, 1, 255, 110, 35); particle_burst(g, cx - 24, cy - 18, 32, 3, 255, 150, 45); /* tall stacked building smoke / fire plumes (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 heavy = (bl->type == BT_BASE || bl->type == BT_TURRET); int smoke_n = heavy ? 22 : 14; /* Y-offset stack: base → mid → high → tip */ particle_burst(g, bx, by - 10.0f, smoke_n, 0, 105, 95, 85); particle_burst(g, bx + 3.0f, by - 28.0f, smoke_n - 4, 0, 95, 88, 78); particle_burst(g, bx - 2.0f, by - 46.0f, smoke_n - 8, 0, 85, 80, 72); particle_burst(g, bx + 1.0f, by - 64.0f, smoke_n - 12, 0, 75, 70, 65); if (bl->hp < bl->max_hp * 0.85f || bl->type == BT_TURRET) { particle_burst(g, bx + 4.0f, by - 6.0f, 16, 1, 255, 130, 40); particle_burst(g, bx - 3.0f, by - 28.0f, 14, 1, 255, 110, 35); particle_burst(g, bx + 2.0f, by - 46.0f, 12, 1, 255, 150, 45); particle_burst(g, bx - 1.0f, by - 64.0f, 10, 1, 255, 90, 30); particle_burst(g, bx - 3.0f, by - 14.0f, 8, 3, 90, 75, 60); particle_burst(g, bx + 5.0f, by - 36.0f, 6, 3, 80, 65, 50); } } /* 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 from attacker origin with * mid-flight prog so lofted kinds keep their parabolic arc in PNG. */ { int spawned = 0; const float pair_r2 = 180.0f * 180.0f; for (int i = 0; i < g->unit_count && spawned < 110; 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 < 110 && shots < 4; 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; /* bias shell/missile arcs over MG/laser */ int kind = (spawned % 5 == 0) ? 0 : (spawned % 5 == 1) ? 2 : (spawned % 5 == 2) ? 1 : (spawned % 5 == 3) ? 3 : 1; float t = 0.18f + 0.60f * ((spawned * 37) % 100) / 100.0f; int before = g->bullet_count; bullet_spawn(g, a->x, a->y, b->id, -1, 1.0f, a->faction, kind); if (g->bullet_count > before) bullet_place_at_prog(&g->bullets[g->bullet_count - 1], t); spawned++; shots++; } } /* turret / base guns → nearest enemy in clash */ for (int i = 0; i < g->bld_count && spawned < 130; 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 t = 0.25f + 0.45f * ((spawned * 19) % 100) / 100.0f; int kind = (spawned % 3 == 0) ? 3 : 1; int before = g->bullet_count; bullet_spawn(g, bx, by, tgt->id, -1, 2.0f, bl->faction, kind); if (g->bullet_count > before) bullet_place_at_prog(&g->bullets[g->bullet_count - 1], t); 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, 72, 1, 255, 135, 35); particle_burst(g, c2x + 18, c2y - 10, 48, 0, 115, 105, 90); particle_burst(g, c2x - 14, c2y + 12, 52, 2, 255, 220, 85); particle_burst(g, c2x + 26, c2y + 8, 40, 1, 255, 95, 28); particle_burst(g, c2x - 6, c2y - 20, 40, 3, 90, 78, 62); particle_burst(g, c2x + 10, c2y + 24, 36, 1, 255, 160, 50); particle_burst(g, c2x - 28, c2y + 8, 32, 0, 100, 95, 80); particle_burst(g, c2x + 32, c2y - 16, 30, 1, 255, 120, 40); particle_burst(g, c2x + 14, c2y + 16, 28, 1, 255, 110, 35); particle_burst(g, c2x - 20, c2y - 12, 24, 3, 255, 150, 45); /* SE building fire/smoke columns (same tall stack) */ { const float r2b = 140.0f * 140.0f; 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 - clash2_x, bdy = by - clash2_y; if (bdx * bdx + bdy * bdy > r2b) continue; int heavy = (bl->type == BT_BASE || bl->type == BT_TURRET); int smoke_n = heavy ? 18 : 12; particle_burst(g, bx, by - 10.0f, smoke_n, 0, 100, 92, 82); particle_burst(g, bx + 2.0f, by - 28.0f, smoke_n - 4, 0, 90, 84, 74); particle_burst(g, bx - 2.0f, by - 46.0f, smoke_n - 8, 0, 80, 76, 68); particle_burst(g, bx + 1.0f, by - 64.0f, smoke_n - 10, 0, 70, 66, 60); if (bl->hp < bl->max_hp * 0.85f || bl->type == BT_TURRET) { particle_burst(g, bx + 3.0f, by - 8.0f, 14, 1, 255, 125, 38); particle_burst(g, bx - 2.0f, by - 28.0f, 12, 1, 255, 105, 32); particle_burst(g, bx + 2.0f, by - 46.0f, 10, 1, 255, 145, 42); particle_burst(g, bx - 1.0f, by - 64.0f, 8, 1, 255, 95, 28); } } } { 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 < 72; 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 < 72 && shots < 4; 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 % 5 == 0) ? 0 : (spawned2 % 5 == 1) ? 2 : (spawned2 % 5 == 2) ? 1 : (spawned2 % 5 == 3) ? 3 : 1; float t = 0.18f + 0.58f * ((spawned2 * 41) % 100) / 100.0f; int before = g->bullet_count; bullet_spawn(g, a->x, a->y, b->id, -1, 1.0f, a->faction, kind); if (g->bullet_count > before) bullet_place_at_prog(&g->bullets[g->bullet_count - 1], t); spawned2++; shots++; } } for (int i = 0; i < g->bld_count && spawned2 < 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 - 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 t = 0.26f + 0.44f * ((spawned2 * 23) % 100) / 100.0f; int kind = (spawned2 % 3 == 0) ? 3 : 1; int before = g->bullet_count; bullet_spawn(g, bx, by, tgt->id, -1, 2.0f, bl->faction, kind); if (g->bullet_count > before) bullet_place_at_prog(&g->bullets[g->bullet_count - 1], t); 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; }