OpenKKnD/engine-c/src/main.c
2026-09-19 17:49:23 +02:00

413 lines
21 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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.51.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;
}