OpenKKnD/engine-c/src/main.c
2026-09-19 14:45:47 +02:00

366 lines
18 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; }
/* 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;
}