#include "kknd.h" /* ========================================================================= * engine.c - Match setup, main simulation step and win/lose logic. * ========================================================================= */ int g_headless = 0; int g_auto = 0; void engine_log(const char *fmt, ...) { va_list ap; va_start(ap, fmt); char buf[512]; vsnprintf(buf, sizeof buf, fmt, ap); va_end(ap); printf("[kknd] %s\n", buf); fflush(stdout); } void game_log(Game *g, const char *fmt, ...) { va_list ap; va_start(ap, fmt); vsnprintf(g->log_msg[g->log_next], 96, fmt, ap); va_end(ap); g->log_next = (g->log_next + 1) % 32; if (g->log_count < 32) g->log_count++; } void engine_init(Game *g, const char *title) { memset(g, 0, sizeof(Game)); g->wrecks = (Wreck *)calloc(MAX_WRECKS, sizeof(Wreck)); config_init(); if (!g_headless) { audio_init(); if (!render_init(g)) { fprintf(stderr, "Failed to initialise renderer\n"); exit(1); } } else { (void)title; } g->state = GAME_MENU; g->human = FACTION_SURVIVOR; g->diff = DIFF_NORMAL; g->speed_mul = 1.0f; g->unlocked = 1; g->menu_sel = 0; } void engine_shutdown(Game *g) { free(g->wrecks); sprites_free(&g->spr); if (g->ren) SDL_DestroyRenderer(g->ren); if (g->win) SDL_DestroyWindow(g->win); audio_shutdown(); SDL_Quit(); map_free(&g->map); } static void bld_force_complete(Game *g) { for (int i = 0; i < g->bld_count; i++) { Building *b = &g->buildings[i]; if (b->dead) continue; b->build = 1.0f; b->hp = b->max_hp; } } /* After force-complete: leave a few incomplete yards + battle-worn * fortifications near both raid loci so scaffold/damage FX show in KKND_SHOT. */ static void showcase_bld_battle_wear(Game *g) { int incomplete_n = 0, damaged_n = 0; float wx, wy; map_tile_to_world(g->map.spawn_x[g->human], g->map.spawn_y[g->human], &wx, &wy); float rx = wx + 88.0f, ry = wy - 48.0f; float rx2 = wx + 140.0f, ry2 = wy + 96.0f; for (int i = 0; i < g->bld_count; i++) { Building *b = &g->buildings[i]; if (b->dead) continue; float cx = (b->tx + b->w * 0.5f) * (float)TILE; float cy = (b->ty + b->h * 0.5f) * (float)TILE; float dx = cx - rx, dy = cy - ry; float d2 = dx * dx + dy * dy; float dx2 = cx - rx2, dy2 = cy - ry2; float d2b = dx2 * dx2 + dy2 * dy2; float dmin = d2 < d2b ? d2 : d2b; /* mid-base construction yards (away from clash) */ if (incomplete_n < 2 && b->faction == g->human && d2 > 180.0f * 180.0f && (b->type == BT_REPAIR || b->type == BT_RESEARCH || b->type == BT_WARFACTORY)) { b->build = (incomplete_n == 0) ? 0.55f : 0.72f; b->hp = b->max_hp * b->build * 0.9f; incomplete_n++; continue; } /* clash fortifications + economy pads take hits at both fronts */ if (damaged_n < 16 && dmin < 220.0f * 220.0f && (b->type == BT_TURRET || b->type == BT_WALL || b->type == BT_POWER || b->type == BT_REFINERY || b->type == BT_BASE || b->type == BT_WARFACTORY)) { float frac = (damaged_n % 4 == 0) ? 0.22f : (damaged_n % 4 == 1) ? 0.40f : (damaged_n % 4 == 2) ? 0.58f : 0.74f; b->hp = b->max_hp * frac; damaged_n++; } } } static void place_start(Game *g, Faction f) { int sx = g->map.spawn_x[f], sy = g->map.spawn_y[f]; int bx = sx - 1, by = sy - 1; bld_spawn(g, BT_BASE, f, bx, by); bld_spawn(g, BT_REFINERY, f, bx, sy + 2); bld_spawn(g, BT_WARFACTORY, f, sx + 2, by); bld_spawn(g, BT_POWER, f, sx + 2, sy + 2); float wx, wy; map_tile_to_world(sx, sy, &wx, &wy); /* harvesters */ for (int i = 0; i < 2; i++) unit_spawn(g, UT_HARVESTER, f, wx + (i*24-12), wy + 30); /* starting infantry */ for (int i = 0; i < 4; i++) unit_spawn(g, UT_INFANTRY, f, wx - 40 + i*20, wy - 20); /* a couple of buggies */ for (int i = 0; i < 2; i++) unit_spawn(g, UT_BUGGY, f, wx - 30 + i*30, wy - 40); } /* KKND2-like showcase: extra human base structures + diverse roster + * enemy raid pack near the camera so fog/snapshot show combat. */ /* Seed a battlefield wreck (type-aware hulls drawn in snapshot/live render). */ static void spawn_showcase_wreck(Game *g, UnitType type, Faction faction, float x, float y, float rot) { if (g->wreck_count >= MAX_WRECKS) return; Wreck *w = &g->wrecks[g->wreck_next]; w->x = x; w->y = y; w->type = type; w->faction = faction; w->rot = rot; g->wreck_next = (g->wreck_next + 1) % MAX_WRECKS; if (g->wreck_count < MAX_WRECKS) g->wreck_count++; } /* ~22 charred hulls around primary + secondary clash for KKND2 plate shots. */ static void seed_clash_wrecks(Game *g, Faction human, Faction enemy, float rx, float ry, float rx2, float ry2) { /* primary NE clash — mixed sides, varied chassis */ spawn_showcase_wreck(g, UT_TANK, human, rx - 22, ry + 8, 0.6f); spawn_showcase_wreck(g, UT_TANK, enemy, rx + 14, ry - 12, 2.4f); spawn_showcase_wreck(g, UT_RAVAGER, enemy, rx + 28, ry + 6, 1.1f); spawn_showcase_wreck(g, UT_BUGGY, human, rx - 8, ry - 18, 3.5f); spawn_showcase_wreck(g, UT_BUGGY, enemy, rx + 6, ry + 20, 0.2f); spawn_showcase_wreck(g, UT_JEEP, human, rx - 30, ry - 4, 4.8f); spawn_showcase_wreck(g, UT_ARTILLERY, human, rx - 40, ry + 16, 5.2f); spawn_showcase_wreck(g, UT_FLAME, enemy, rx + 36, ry - 8, 2.0f); spawn_showcase_wreck(g, UT_MISSILE, enemy, rx + 18, ry + 28, 1.7f); spawn_showcase_wreck(g, UT_INFANTRY, human, rx - 14, ry + 24, 0.9f); spawn_showcase_wreck(g, UT_INFANTRY, enemy, rx + 4, ry - 26, 3.1f); spawn_showcase_wreck(g, UT_MUTANT, enemy, rx + 22, ry + 14, 4.2f); spawn_showcase_wreck(g, UT_BEAST, enemy, rx + 42, ry + 10, 0.4f); spawn_showcase_wreck(g, UT_HARVESTER, human, rx - 48, ry - 10, 5.9f); /* secondary SE clash */ spawn_showcase_wreck(g, UT_TANK, human, rx2 - 16, ry2 + 6, 1.3f); spawn_showcase_wreck(g, UT_TANK, enemy, rx2 + 12, ry2 - 8, 3.8f); spawn_showcase_wreck(g, UT_BUGGY, enemy, rx2 + 22, ry2 + 14, 0.7f); spawn_showcase_wreck(g, UT_JEEP, human, rx2 - 24, ry2 - 4, 2.6f); spawn_showcase_wreck(g, UT_FLAME, human, rx2 - 8, ry2 + 18, 4.5f); spawn_showcase_wreck(g, UT_RAVAGER, enemy, rx2 + 30, ry2 + 4, 1.9f); spawn_showcase_wreck(g, UT_INFANTRY, human, rx2 + 4, ry2 - 16, 5.5f); spawn_showcase_wreck(g, UT_MUTANT, enemy, rx2 + 16, ry2 + 22, 0.1f); } static void place_showcase(Game *g, Faction human, Faction enemy) { int sx = g->map.spawn_x[human], sy = g->map.spawn_y[human]; float wx, wy; map_tile_to_world(sx, sy, &wx, &wy); /* extra structures in the cleared spawn pad (±8) */ bld_spawn(g, BT_REPAIR, human, sx - 4, sy - 1); bld_spawn(g, BT_RESEARCH, human, sx - 4, sy + 2); bld_spawn(g, BT_POWER, human, sx + 3, sy + 2); bld_spawn(g, BT_POWER, human, sx + 5, sy + 2); bld_spawn(g, BT_WARFACTORY, human, sx + 3, sy - 4); /* turrets covering approaches */ bld_spawn(g, BT_TURRET, human, sx - 2, sy - 3); bld_spawn(g, BT_TURRET, human, sx + 1, sy - 3); bld_spawn(g, BT_TURRET, human, sx + 4, sy); bld_spawn(g, BT_TURRET, human, sx + 4, sy + 2); bld_spawn(g, BT_TURRET, human, sx - 2, sy + 4); bld_spawn(g, BT_TURRET, human, sx + 2, sy + 4); bld_spawn(g, BT_TURRET, human, sx - 4, sy); /* blast-wall spur (north + west + east stub) */ for (int dx = -3; dx <= 3; dx++) { if (dx >= -1 && dx <= 1) continue; /* gate */ bld_spawn(g, BT_WALL, human, sx + dx, sy - 4); } for (int dy = -3; dy <= 3; dy++) { if (dy >= -1 && dy <= 1) continue; bld_spawn(g, BT_WALL, human, sx - 5, sy + dy); } for (int dy = -2; dy <= 2; dy++) { if (dy == 0) continue; bld_spawn(g, BT_WALL, human, sx + 6, sy + dy); } /* dense Survivor battle line NE of HQ (tight combat camera) */ unit_spawn(g, UT_TANK, human, wx + 48, wy - 8); unit_spawn(g, UT_TANK, human, wx + 68, wy + 8); unit_spawn(g, UT_TANK, human, wx + 82, wy - 38); unit_spawn(g, UT_TANK, human, wx + 58, wy - 48); unit_spawn(g, UT_RAVAGER, human, wx + 92, wy - 22); unit_spawn(g, UT_RAVAGER, human, wx + 78, wy - 58); unit_spawn(g, UT_ARTILLERY, human, wx + 18, wy + 48); unit_spawn(g, UT_ARTILLERY, human, wx + 38, wy + 62); unit_spawn(g, UT_ARTILLERY, human, wx + 8, wy + 28); unit_spawn(g, UT_ARTILLERY, human, wx + 52, wy + 44); unit_spawn(g, UT_FLAME, human, wx - 40, wy + 12); unit_spawn(g, UT_FLAME, human, wx + 105, wy - 5); unit_spawn(g, UT_MISSILE, human, wx - 55, wy - 8); unit_spawn(g, UT_MISSILE, human, wx + 110, wy - 40); unit_spawn(g, UT_MISSILE, human, wx + 96, wy - 52); unit_spawn(g, UT_MISSILE, human, wx + 120, wy - 18); unit_spawn(g, UT_JEEP, human, wx + 32, wy - 52); unit_spawn(g, UT_JEEP, human, wx + 48, wy - 68); unit_spawn(g, UT_BUGGY, human, wx + 55, wy - 78); unit_spawn(g, UT_BUGGY, human, wx + 72, wy - 72); unit_spawn(g, UT_MEDIC, human, wx - 12, wy + 40); unit_spawn(g, UT_MEDIC, human, wx + 20, wy + 28); /* denser MEDIC support screen at primary clash (white vans + cross) */ unit_spawn(g, UT_MEDIC, human, wx + 64, wy - 18); unit_spawn(g, UT_MEDIC, human, wx + 86, wy - 36); unit_spawn(g, UT_MEDIC, human, wx + 74, wy + 6); unit_spawn(g, UT_MEDIC, human, wx + 98, wy - 8); for (int i = 0; i < 10; i++) unit_spawn(g, UT_INFANTRY, human, wx + 88 + (i % 5) * 14, wy + 8 + (i / 5) * 16); /* dense melee screen at primary clash (riflemen packed for KKND2 crop) */ for (int i = 0; i < 12; i++) unit_spawn(g, UT_INFANTRY, human, wx + 72 + (i % 6) * 11, wy - 28 + (i / 6) * 12); /* Evolved raid pack — close NE, overlapping Survivor line */ float rx = wx + 88.0f, ry = wy - 48.0f; /* forward fortifications at the clash so a tight combat crop still shows buildings */ { int ftx, fty; map_world_to_tile(rx, ry, &ftx, &fty); bld_spawn(g, BT_TURRET, human, ftx - 2, fty + 1); bld_spawn(g, BT_TURRET, human, ftx + 1, fty + 2); bld_spawn(g, BT_TURRET, human, ftx - 1, fty - 2); bld_spawn(g, BT_TURRET, human, ftx + 2, fty - 1); bld_spawn(g, BT_WALL, human, ftx - 3, fty); bld_spawn(g, BT_WALL, human, ftx - 3, fty + 1); bld_spawn(g, BT_WALL, human, ftx - 3, fty - 1); bld_spawn(g, BT_WALL, human, ftx + 2, fty + 1); bld_spawn(g, BT_WALL, human, ftx + 2, fty); bld_spawn(g, BT_WALL, human, ftx + 3, fty - 1); bld_spawn(g, BT_POWER, human, ftx - 4, fty + 2); bld_force_complete(g); /* complete any partial builds from spawn */ /* Survivor forward HQ cluster (SW of clash) — visible in combat crop * alongside Evolved twin BT_BASE; pad clear before place. */ { int hqx = ftx - 6, hqy = fty + 3; for (int dy = -1; dy <= 4; dy++) { for (int dx = -1; dx <= 5; dx++) { int tx = hqx + dx, ty = hqy + dy; if (!map_in_bounds(&g->map, tx, ty)) continue; Tile *t = map_tile(&g->map, tx, ty); if (!t || t->occupied) continue; t->terrain = T_WASTELAND; t->walkable = 1; t->scrap = 0; } } bld_spawn(g, BT_BASE, human, hqx, hqy); bld_spawn(g, BT_BASE, human, hqx + 3, hqy); bld_spawn(g, BT_POWER, human, hqx - 1, hqy + 2); bld_spawn(g, BT_WARFACTORY, human, hqx + 1, hqy + 3); bld_spawn(g, BT_REFINERY, human, hqx + 4, hqy + 3); /* derrick near clash crop */ bld_spawn(g, BT_TURRET, human, hqx + 2, hqy - 1); bld_spawn(g, BT_TURRET, human, hqx + 5, hqy + 1); bld_spawn(g, BT_WALL, human, hqx - 1, hqy); bld_spawn(g, BT_WALL, human, hqx - 1, hqy + 1); bld_spawn(g, BT_WALL, human, hqx + 5, hqy); bld_force_complete(g); /* dense scrap oil-field SE of derrick (visible in clash crop) */ for (int dy = 3; dy <= 8; dy++) { for (int dx = 4; dx <= 11; dx++) { int tx = hqx + dx, ty = hqy + dy; if (!map_in_bounds(&g->map, tx, ty)) continue; Tile *t = map_tile(&g->map, tx, ty); if (!t || !t->walkable || t->occupied) continue; int d2 = (dx - 7) * (dx - 7) + (dy - 5) * (dy - 5); if (d2 > 20) continue; t->terrain = T_SCRAP; t->scrap = (uint8_t)(140 + ((dx * 17 + dy * 11) & 95)); } } /* harvesters on the scrap oil-field approach (cargo seeded) */ { float rwx, rwy; map_tile_to_world(hqx + 6, hqy + 5, &rwx, &rwy); for (int hi = 0; hi < 2; hi++) { int ui = unit_spawn(g, UT_HARVESTER, human, rwx + (hi * 22 - 8), rwy + 6.0f); if (ui >= 0 && ui < MAX_UNITS) { Unit *hu = &g->units[ui]; if (hu && !hu->dead) hu->cargo = (uint16_t)(620 + hi * 9); } } } } } /* denser clash scars: road core + rubble ring + scrap flecks (arid fight plate) */ { int ctx, cty; map_world_to_tile(rx, ry, &ctx, &cty); for (int dy = -6; dy <= 6; dy++) { for (int dx = -7; dx <= 7; dx++) { int tx = ctx + dx, ty = cty + dy; if (!map_in_bounds(&g->map, tx, ty)) continue; Tile *t = map_tile(&g->map, tx, ty); if (!t || !t->walkable) continue; int d2 = dx * dx + dy * dy; if (d2 <= 3) { t->terrain = T_ROAD; t->scrap = 0; } else if (d2 <= 12) { t->terrain = T_RUBBLE; t->scrap = 0; } else if (d2 <= 30 && ((dx + dy * 3) & 1) == 0) { /* denser scrap flecks around clash (oil-field read) */ t->terrain = T_SCRAP; t->scrap = (uint8_t)(110 + ((dx * 13 + dy * 7) & 127)); } else if (d2 <= 36 && ((dx * 5 + dy) & 2) == 0) { t->terrain = T_RUBBLE; t->scrap = 0; } } } /* approach road from HQ toward clash */ int hx_t = sx, hy_t = sy; int steps = abs(ctx - hx_t) + abs(cty - hy_t); if (steps < 1) steps = 1; for (int s = 0; s <= steps; s++) { int tx = hx_t + (ctx - hx_t) * s / steps; int ty = hy_t + (cty - hy_t) * s / steps; if (!map_in_bounds(&g->map, tx, ty)) continue; Tile *t = map_tile(&g->map, tx, ty); if (!t || !t->walkable) continue; t->terrain = T_ROAD; t->scrap = 0; } /* Evolved forward outpost NW of clash (visible in combat crop). * Clear pad to wasteland BEFORE buildings; ROCK scars only after * (T_ROCK is non-walkable and bld_can_place rejects it). */ { int erx = ctx - 10, ery = cty - 9; for (int dy = -3; dy <= 5; dy++) { for (int dx = -3; dx <= 6; dx++) { int tx = erx + dx, ty = ery + dy; if (!map_in_bounds(&g->map, tx, ty)) continue; Tile *t = map_tile(&g->map, tx, ty); if (!t) continue; if (t->occupied) continue; t->terrain = T_WASTELAND; t->walkable = 1; t->scrap = 0; } } bld_spawn(g, BT_BASE, enemy, erx, ery); bld_spawn(g, BT_BASE, enemy, erx + 3, ery); bld_spawn(g, BT_POWER, enemy, erx - 1, ery + 2); bld_spawn(g, BT_POWER, enemy, erx + 5, ery + 2); bld_spawn(g, BT_WARFACTORY, enemy, erx, ery + 3); bld_spawn(g, BT_WARFACTORY, enemy, erx + 3, ery + 3); bld_spawn(g, BT_RESEARCH, enemy, erx - 2, ery); bld_spawn(g, BT_REFINERY, enemy, erx + 5, ery + 3); bld_spawn(g, BT_TURRET, enemy, erx + 2, ery - 2); bld_spawn(g, BT_TURRET, enemy, erx - 2, ery + 3); bld_spawn(g, BT_TURRET, enemy, erx + 6, ery + 1); /* denser Evolved wall perimeter (N/W/E/S stubs + SE gate) */ bld_spawn(g, BT_WALL, enemy, erx - 2, ery - 1); bld_spawn(g, BT_WALL, enemy, erx - 1, ery - 1); bld_spawn(g, BT_WALL, enemy, erx, ery - 2); bld_spawn(g, BT_WALL, enemy, erx + 1, ery - 2); bld_spawn(g, BT_WALL, enemy, erx + 4, ery - 2); bld_spawn(g, BT_WALL, enemy, erx + 5, ery - 1); bld_spawn(g, BT_WALL, enemy, erx + 6, ery - 1); bld_spawn(g, BT_WALL, enemy, erx + 7, ery); bld_spawn(g, BT_WALL, enemy, erx + 7, ery + 1); bld_spawn(g, BT_WALL, enemy, erx + 7, ery + 2); bld_spawn(g, BT_WALL, enemy, erx - 3, ery); bld_spawn(g, BT_WALL, enemy, erx - 3, ery + 1); bld_spawn(g, BT_WALL, enemy, erx - 3, ery + 2); bld_spawn(g, BT_WALL, enemy, erx - 1, ery + 5); bld_spawn(g, BT_WALL, enemy, erx, ery + 5); bld_spawn(g, BT_WALL, enemy, erx + 2, ery + 5); bld_spawn(g, BT_WALL, enemy, erx + 4, ery + 5); bld_spawn(g, BT_WALL, enemy, erx + 6, ery + 4); bld_spawn(g, BT_WALL, enemy, erx + 5, ery + 5); /* SE approach gate toward clash */ bld_spawn(g, BT_WALL, enemy, erx + 8, ery + 3); bld_spawn(g, BT_WALL, enemy, erx + 8, ery + 5); bld_spawn(g, BT_TURRET, enemy, erx + 8, ery + 4); bld_force_complete(g); /* pad roads: internal lanes + corridor toward clash core */ for (int dy = 0; dy <= 4; dy++) { for (int dx = -1; dx <= 5; dx++) { int tx = erx + dx, ty = ery + dy; if (!map_in_bounds(&g->map, tx, ty)) continue; Tile *t = map_tile(&g->map, tx, ty); if (!t || t->occupied || !t->walkable) continue; if (dy == 1 || dy == 3 || dx == 1 || dx == 4) { t->terrain = T_ROAD; t->scrap = 0; } } } { int steps = abs(ctx - (erx + 6)) + abs(cty - (ery + 4)); if (steps < 1) steps = 1; for (int s = 0; s <= steps; s++) { int tx = (erx + 6) + (ctx - (erx + 6)) * s / steps; int ty = (ery + 4) + (cty - (ery + 4)) * s / steps; for (int o = -1; o <= 1; o++) { int ox = tx + ((s & 1) ? 0 : o); int oy = ty + ((s & 1) ? o : 0); if (!map_in_bounds(&g->map, ox, oy)) continue; Tile *t = map_tile(&g->map, ox, oy); if (!t || t->occupied || !t->walkable) continue; t->terrain = T_ROAD; t->scrap = 0; } } } /* outer ROCK / crater flecks — skip occupied tiles */ for (int dy = -5; dy <= 7; dy++) { for (int dx = -5; dx <= 8; dx++) { int tx = erx + dx, ty = ery + dy; if (!map_in_bounds(&g->map, tx, ty)) continue; Tile *t = map_tile(&g->map, tx, ty); if (!t || t->occupied) continue; int d2 = dx * dx + dy * dy; if (d2 < 16 || d2 > 55) continue; if (((dx * 7 + dy * 11) & 5) != 0) continue; t->terrain = T_ROCK; t->walkable = 0; t->scrap = 0; } } /* crater rubble + scrap flecks between Evolved pad and clash */ for (int dy = -3; dy <= 6; dy++) { for (int dx = 3; dx <= 14; dx++) { int tx = erx + dx, ty = ery + dy + 3; if (!map_in_bounds(&g->map, tx, ty)) continue; Tile *t = map_tile(&g->map, tx, ty); if (!t || t->occupied || !t->walkable) continue; if (t->terrain == T_ROAD) continue; int h = (dx * 13 + dy * 7) & 7; if (h == 0) { t->terrain = T_SCRAP; t->scrap = (uint8_t)(70 + ((dx * 11 + dy * 5) & 127)); } else if (h <= 3) { t->terrain = T_RUBBLE; t->scrap = 0; } } } } /* scrap mound fields: SE of HQ + corridor toward primary clash */ { int hx_t = sx, hy_t = sy; for (int dy = -2; dy <= 8; dy++) { for (int dx = -2; dx <= 10; dx++) { int tx = hx_t + dx, ty = hy_t + dy; if (!map_in_bounds(&g->map, tx, ty)) continue; Tile *t = map_tile(&g->map, tx, ty); if (!t || t->occupied || !t->walkable) continue; if (t->terrain == T_ROAD) continue; int h = (dx * 11 + dy * 9) & 7; if (h == 0 || h == 3) { t->terrain = T_SCRAP; t->scrap = (uint8_t)(110 + ((dx * 17 + dy * 13) & 127)); } } } } /* denser corridor fill HQ → primary clash: scrap mounds, rubble, rock outcrops * so arid yellow plate is broken up (KKND2 wasteland read). */ { int hx_t = sx, hy_t = sy; int minx = hx_t < ctx ? hx_t : ctx; int maxx = hx_t > ctx ? hx_t : ctx; int miny = hy_t < cty ? hy_t : cty; int maxy = hy_t > cty ? hy_t : cty; minx -= 4; maxx += 6; miny -= 5; maxy += 4; for (int ty = miny; ty <= maxy; ty++) { for (int tx = minx; tx <= maxx; tx++) { if (!map_in_bounds(&g->map, tx, ty)) continue; Tile *t = map_tile(&g->map, tx, ty); if (!t || t->occupied || !t->walkable) continue; if (t->terrain == T_ROAD) continue; /* keep clash-core scars already set */ if (t->terrain == T_RUBBLE || t->terrain == T_SCRAP) continue; int dx = tx - hx_t, dy = ty - hy_t; int h = (tx * 17 + ty * 13) & 15; int along = abs((ty - hy_t) * (ctx - hx_t) - (tx - hx_t) * (cty - hy_t)); int span = abs(ctx - hx_t) + abs(cty - hy_t); if (span < 1) span = 1; /* band around approach road */ if (along > span * 3) continue; if (h == 0 || h == 7) { t->terrain = T_SCRAP; t->scrap = (uint8_t)(95 + ((dx * 19 + dy * 11) & 127)); } else if (h == 2 || h == 9) { t->terrain = T_RUBBLE; t->scrap = 0; } else if (h == 4 && ((tx + ty) & 3) == 0) { /* sparse rock outcrops — non-walkable */ t->terrain = T_ROCK; t->walkable = 0; t->scrap = 0; } } } } } /* secondary clash front SE of HQ (second fight plate in crop) */ float rx2 = wx + 40.0f, ry2 = wy + 70.0f; { int ctx2, cty2; map_world_to_tile(rx2, ry2, &ctx2, &cty2); for (int dy = -5; dy <= 5; dy++) { for (int dx = -6; dx <= 6; dx++) { int tx = ctx2 + dx, ty = cty2 + dy; if (!map_in_bounds(&g->map, tx, ty)) continue; Tile *t = map_tile(&g->map, tx, ty); if (!t || !t->walkable) continue; int d2 = dx * dx + dy * dy; if (d2 <= 2) { t->terrain = T_ROAD; t->scrap = 0; } else if (d2 <= 10) { t->terrain = T_RUBBLE; t->scrap = 0; } else if (d2 <= 24 && ((dx + dy * 2) & 3) == 0) { t->terrain = T_SCRAP; t->scrap = (uint8_t)(100 + ((dx * 19 + dy * 11) & 127)); } } } /* short approach from HQ south gate toward secondary */ { int steps = abs(ctx2 - sx) + abs(cty2 - sy); if (steps < 1) steps = 1; for (int s = 0; s <= steps; s++) { int tx = sx + (ctx2 - sx) * s / steps; int ty = sy + (cty2 - sy) * s / steps; if (!map_in_bounds(&g->map, tx, ty)) continue; Tile *t = map_tile(&g->map, tx, ty); if (!t || !t->walkable) continue; if (t->occupied) continue; t->terrain = T_ROAD; t->scrap = 0; } } /* denser corridor fill HQ → secondary SE clash */ { int hx_t = sx, hy_t = sy; int minx = hx_t < ctx2 ? hx_t : ctx2; int maxx = hx_t > ctx2 ? hx_t : ctx2; int miny = hy_t < cty2 ? hy_t : cty2; int maxy = hy_t > cty2 ? hy_t : cty2; minx -= 3; maxx += 5; miny -= 3; maxy += 5; for (int ty = miny; ty <= maxy; ty++) { for (int tx = minx; tx <= maxx; tx++) { if (!map_in_bounds(&g->map, tx, ty)) continue; Tile *t = map_tile(&g->map, tx, ty); if (!t || t->occupied || !t->walkable) continue; if (t->terrain == T_ROAD || t->terrain == T_RUBBLE || t->terrain == T_SCRAP) continue; int dx = tx - hx_t, dy = ty - hy_t; int h = (tx * 19 + ty * 11) & 15; int along = abs((ty - hy_t) * (ctx2 - hx_t) - (tx - hx_t) * (cty2 - hy_t)); int span = abs(ctx2 - hx_t) + abs(cty2 - hy_t); if (span < 1) span = 1; if (along > span * 3) continue; if (h == 1 || h == 8) { t->terrain = T_SCRAP; t->scrap = (uint8_t)(90 + ((dx * 23 + dy * 7) & 127)); } else if (h == 3 || h == 10) { t->terrain = T_RUBBLE; t->scrap = 0; } else if (h == 5 && ((tx + ty) & 3) == 1) { t->terrain = T_ROCK; t->walkable = 0; t->scrap = 0; } } } } bld_spawn(g, BT_TURRET, human, ctx2 - 2, cty2); bld_spawn(g, BT_TURRET, human, ctx2 + 1, cty2 + 1); bld_spawn(g, BT_WALL, human, ctx2 - 3, cty2); bld_spawn(g, BT_WALL, human, ctx2 - 3, cty2 + 1); bld_force_complete(g); } /* Survivor SE defenders */ unit_spawn(g, UT_TANK, human, rx2 - 18, ry2 - 8); unit_spawn(g, UT_TANK, human, rx2 - 6, ry2 + 10); unit_spawn(g, UT_JEEP, human, rx2 - 28, ry2 + 4); unit_spawn(g, UT_FLAME, human, rx2 - 22, ry2 + 18); unit_spawn(g, UT_BUGGY, human, rx2 - 34, ry2 - 12); for (int i = 0; i < 5; i++) unit_spawn(g, UT_INFANTRY, human, rx2 - 12 + (i % 3) * 12, ry2 - 16 + (i / 3) * 14); /* denser Survivor melee at SE clash */ for (int i = 0; i < 8; i++) unit_spawn(g, UT_INFANTRY, human, rx2 - 22 + (i % 4) * 10, ry2 - 6 + (i / 4) * 11); /* MEDIC triage vans at SE clash */ unit_spawn(g, UT_MEDIC, human, rx2 - 30, ry2 + 2); unit_spawn(g, UT_MEDIC, human, rx2 - 16, ry2 - 14); int raid[128]; int rn = 0; for (int i = 0; i < 8; i++) raid[rn++] = unit_spawn(g, UT_MUTANT, enemy, rx + (i % 4) * 18 - 10, ry + (i / 4) * 16 - 8); for (int i = 0; i < 4; i++) raid[rn++] = unit_spawn(g, UT_BEAST, enemy, rx + 30 + i * 16, ry - 14 + (i & 1) * 18); raid[rn++] = unit_spawn(g, UT_BUGGY, enemy, rx + 8, ry - 28); raid[rn++] = unit_spawn(g, UT_BUGGY, enemy, rx + 28, ry - 34); raid[rn++] = unit_spawn(g, UT_BUGGY, enemy, rx + 48, ry - 22); raid[rn++] = unit_spawn(g, UT_JEEP, enemy, rx + 16, ry + 18); raid[rn++] = unit_spawn(g, UT_JEEP, enemy, rx + 36, ry + 8); raid[rn++] = unit_spawn(g, UT_TANK, enemy, rx + 52, ry + 4); raid[rn++] = unit_spawn(g, UT_TANK, enemy, rx + 68, ry - 12); raid[rn++] = unit_spawn(g, UT_TANK, enemy, rx + 80, ry + 18); raid[rn++] = unit_spawn(g, UT_RAVAGER, enemy, rx + 58, ry - 36); raid[rn++] = unit_spawn(g, UT_RAVAGER, enemy, rx + 74, ry - 24); raid[rn++] = unit_spawn(g, UT_RAVAGER, enemy, rx + 42, ry - 44); raid[rn++] = unit_spawn(g, UT_FLAME, enemy, rx + 22, ry + 28); raid[rn++] = unit_spawn(g, UT_FLAME, enemy, rx + 40, ry + 34); raid[rn++] = unit_spawn(g, UT_FLAME, enemy, rx + 8, ry + 40); raid[rn++] = unit_spawn(g, UT_MISSILE, enemy, rx + 62, ry - 28); raid[rn++] = unit_spawn(g, UT_MISSILE, enemy, rx + 78, ry - 8); raid[rn++] = unit_spawn(g, UT_MISSILE, enemy, rx + 94, ry + 6); raid[rn++] = unit_spawn(g, UT_MISSILE, enemy, rx + 108, ry - 20); raid[rn++] = unit_spawn(g, UT_MISSILE, enemy, rx + 70, ry + 14); raid[rn++] = unit_spawn(g, UT_ARTILLERY, enemy, rx + 85, ry + 10); raid[rn++] = unit_spawn(g, UT_ARTILLERY, enemy, rx + 98, ry - 18); raid[rn++] = unit_spawn(g, UT_ARTILLERY, enemy, rx + 112, ry + 4); raid[rn++] = unit_spawn(g, UT_ARTILLERY, enemy, rx + 76, ry - 40); for (int i = 0; i < 8; i++) raid[rn++] = unit_spawn(g, UT_INFANTRY, enemy, rx - 28 + (i % 4) * 14, ry - 8 + (i / 4) * 14); /* dense Evolved melee wedge into Survivor rifle screen */ for (int i = 0; i < 6; i++) raid[rn++] = unit_spawn(g, UT_MUTANT, enemy, rx - 8 + (i % 3) * 12, ry + 6 + (i / 3) * 11); for (int i = 0; i < 4; i++) raid[rn++] = unit_spawn(g, UT_BEAST, enemy, rx + 4 + i * 13, ry + 18 + (i & 1) * 8); for (int i = 0; i < 8; i++) raid[rn++] = unit_spawn(g, UT_INFANTRY, enemy, rx - 18 + (i % 4) * 10, ry + 10 + (i / 4) * 9); /* Evolved Mendbeast support (UT_MEDIC) behind melee wedge */ for (int i = 0; i < 4; i++) raid[rn++] = unit_spawn(g, UT_MEDIC, enemy, rx + 18 + (i % 2) * 16, ry + 24 + (i / 2) * 12); /* secondary Evolved pack at SE clash — melee-heavy */ int rn_pri = rn; for (int i = 0; i < 4; i++) raid[rn++] = unit_spawn(g, UT_MUTANT, enemy, rx2 + 10 + (i % 2) * 16, ry2 + 8 + (i / 2) * 14); for (int i = 0; i < 2; i++) raid[rn++] = unit_spawn(g, UT_BEAST, enemy, rx2 + 28 + i * 14, ry2 + 4 + i * 10); raid[rn++] = unit_spawn(g, UT_BUGGY, enemy, rx2 + 18, ry2 - 10); raid[rn++] = unit_spawn(g, UT_BUGGY, enemy, rx2 + 34, ry2 + 16); raid[rn++] = unit_spawn(g, UT_JEEP, enemy, rx2 + 42, ry2 + 2); raid[rn++] = unit_spawn(g, UT_TANK, enemy, rx2 + 50, ry2 + 12); raid[rn++] = unit_spawn(g, UT_TANK, enemy, rx2 + 58, ry2 - 6); raid[rn++] = unit_spawn(g, UT_RAVAGER, enemy, rx2 + 46, ry2 + 22); raid[rn++] = unit_spawn(g, UT_FLAME, enemy, rx2 + 22, ry2 + 26); raid[rn++] = unit_spawn(g, UT_MISSILE, enemy, rx2 + 64, ry2 + 8); for (int i = 0; i < 2; i++) raid[rn++] = unit_spawn(g, UT_INFANTRY, enemy, rx2 + 6 + i * 12, ry2 + 20 + i * 6); for (int i = 0; i < 4; i++) raid[rn++] = unit_spawn(g, UT_MUTANT, enemy, rx2 - 4 + (i % 2) * 14, ry2 + 14 + (i / 2) * 12); for (int i = 0; i < 2; i++) raid[rn++] = unit_spawn(g, UT_BEAST, enemy, rx2 + 12 + i * 16, ry2 + 28 + i * 6); for (int i = 0; i < 6; i++) raid[rn++] = unit_spawn(g, UT_INFANTRY, enemy, rx2 - 10 + (i % 3) * 11, ry2 + 8 + (i / 3) * 10); /* SE Mendbeast triage */ for (int i = 0; i < 3; i++) raid[rn++] = unit_spawn(g, UT_MEDIC, enemy, rx2 + 20 + i * 14, ry2 + 32 + (i & 1) * 8); /* seed Survivor harvester beds so cargo silhouette is visible in shot */ { int hi = 0; for (int i = 0; i < g->unit_count; i++) { Unit *u = &g->units[i]; if (u->dead || u->type != UT_HARVESTER || u->faction != human) continue; float cap = (float)unit_def(u->type, u->faction)->cargo; /* stagger: empty-ish / mid / full across harvesters */ float fracs[] = { 0.25f, 0.70f, 1.0f, 0.45f }; u->cargo = cap * fracs[hi % 4]; hi++; } } /* pre-damage for HP-bar variety at both clash loci */ for (int i = 0; i < g->unit_count; i++) { Unit *u = &g->units[i]; if (u->dead || u->max_hp <= 0) continue; float dx = u->x - rx, dy = u->y - ry; float dx2 = u->x - rx2, dy2 = u->y - ry2; int near = (dx * dx + dy * dy <= 140.0f * 140.0f) || (dx2 * dx2 + dy2 * dy2 <= 110.0f * 110.0f); if (!near) continue; float f = 0.35f + 0.45f * ((i * 17) % 100) / 100.0f; u->hp = u->max_hp * f; } /* FX seeds at primary + secondary clash loci */ particle_burst(g, rx, ry, 28, 1, 255, 140, 40); particle_burst(g, rx + 20, ry - 10, 18, 0, 90, 90, 90); particle_burst(g, rx - 15, ry + 12, 22, 2, 255, 220, 80); particle_burst(g, rx + 40, ry + 5, 14, 1, 255, 100, 30); particle_burst(g, rx + 10, ry - 25, 16, 3, 80, 70, 60); particle_burst(g, rx2, ry2, 24, 1, 255, 130, 35); particle_burst(g, rx2 + 16, ry2 - 8, 16, 0, 100, 95, 85); particle_burst(g, rx2 - 12, ry2 + 10, 18, 2, 255, 210, 70); particle_burst(g, rx2 + 28, ry2 + 6, 12, 1, 255, 90, 25); particle_burst(g, rx2 + 8, ry2 + 20, 14, 3, 85, 75, 55); /* point primary raid at nearest human combat / HQ; secondary at SE prey */ int hq = -1; for (int i = 0; i < g->bld_count; i++) if (!g->buildings[i].dead && g->buildings[i].faction == human && g->buildings[i].type == BT_BASE) { hq = i; break; } int prey = -1, prey2 = -1; float prey_d2 = 1e12f, prey2_d2 = 1e12f; for (int i = 0; i < g->unit_count; i++) { Unit *u = &g->units[i]; if (u->dead || u->faction != human) continue; if (u->type == UT_HARVESTER || u->type == UT_MEDIC) continue; float d1 = (u->x - rx) * (u->x - rx) + (u->y - ry) * (u->y - ry); float d2 = (u->x - rx2) * (u->x - rx2) + (u->y - ry2) * (u->y - ry2); if (d1 < prey_d2) { prey_d2 = d1; prey = i; } if (d2 < prey2_d2) { prey2_d2 = d2; prey2 = i; } } int tur2 = -1; for (int i = 0; i < g->bld_count; i++) { Building *b = &g->buildings[i]; if (b->dead || b->faction != human || b->type != BT_TURRET) continue; float bx = (float)(b->tx * TILE + b->w * TILE / 2); float by = (float)(b->ty * TILE + b->h * TILE / 2); float d2 = (bx - rx2) * (bx - rx2) + (by - ry2) * (by - ry2); if (d2 < 90.0f * 90.0f) { tur2 = i; break; } } for (int i = 0; i < rn; i++) { if (raid[i] < 0) continue; if (i < rn_pri) { if (prey >= 0) unit_issue_attack_unit(g, raid[i], prey); else if (hq >= 0) unit_issue_attack_bld(g, raid[i], hq); } else { if (prey2 >= 0) unit_issue_attack_unit(g, raid[i], prey2); else if (tur2 >= 0) unit_issue_attack_bld(g, raid[i], tur2); else if (hq >= 0) unit_issue_attack_bld(g, raid[i], hq); } } /* human combat units engage nearest raiders (both fronts) */ int ridx = 0; for (int i = 0; i < g->unit_count; i++) { Unit *u = &g->units[i]; if (u->dead || u->faction != human) continue; if (u->type == UT_HARVESTER || u->type == UT_MEDIC) continue; float dx = u->x - rx, dy = u->y - ry; float dx2 = u->x - rx2, dy2 = u->y - ry2; int near = (dx * dx + dy * dy <= 220.0f * 220.0f) || (dx2 * dx2 + dy2 * dy2 <= 160.0f * 160.0f); if (!near) continue; int prefer_sec = (dx2 * dx2 + dy2 * dy2) < (dx * dx + dy * dy); int lo = prefer_sec ? rn_pri : 0; int hi = prefer_sec ? rn : rn_pri; if (hi <= lo) { lo = 0; hi = rn; } int tgt = -1; int span = hi - lo; for (int k = 0; k < span; k++) { int ri = raid[lo + (ridx + k) % span]; if (ri >= 0 && !g->units[ri].dead) { tgt = ri; ridx += k + 1; break; } } if (tgt >= 0) unit_issue_attack_unit(g, i, tgt); } /* select front-line squads on both clashes for yellow selection boxes */ unit_clear_selection(g); int nsel = 0; for (int i = 0; i < g->unit_count && nsel < 10; i++) { Unit *u = &g->units[i]; if (u->dead || u->faction != human) continue; if (u->type == UT_HARVESTER || u->type == UT_MEDIC) continue; float dx = u->x - rx, dy = u->y - ry; float dx2 = u->x - rx2, dy2 = u->y - ry2; int near = (dx * dx + dy * dy <= 120.0f * 120.0f) || (dx2 * dx2 + dy2 * dy2 <= 90.0f * 90.0f); if (!near) continue; unit_select_single(g, i, 1); nsel++; } /* lasting hulls already on the plate (SHOT_T too short for natural deaths) */ seed_clash_wrecks(g, human, enemy, rx, ry, rx2, ry2); } void engine_setup_match(Game *g, int mission_id) { const Mission *m = mission_get(mission_id); g->mission_id = m->id; g->objective = m->objective; g->diff = (Difficulty)m->diff; g->obj_timer = 0; /* resolve the enemy faction (never the player's own side) */ Faction enemy = m->enemy; if (enemy == g->human) enemy = (g->human == FACTION_SURVIVOR) ? FACTION_EVOLVED : FACTION_SURVIVOR; g->enemy = enemy; RNG rng; rng_seed(&rng, (uint64_t)m->seed ^ 0x4B4B4E44ULL); g->map.style = m->map_style; map_init(&g->map, &rng); g->unit_count = 0; g->unit_next_id = 1; g->bld_count = 0; g->bld_next_id = 1; g->bullet_count = 0; g->particle_count = 0; g->wreck_count = 0; g->wreck_next = 0; g->sel_count = 0; g->placing = 0; g->time = 0; g->frames = 0; g->winner = 0; g->ai_timer = 5.0f; g->log_count = 0; g->log_next = 0; for (int f = 1; f < MAX_FACTIONS; f++) { g->scrap[f] = START_SCRAP; g->power[f] = 0; g->pop[f] = 0; g->pop_cap[f] = 0; for (int u = 0; u < UP_COUNT; u++) g->upg[f].lvl[u] = 0; g->research_lab[f] = 0; g->research_cur[f] = -1; g->research_prog[f] = 0; } place_start(g, g->human); place_start(g, enemy); place_showcase(g, g->human, enemy); bld_force_complete(g); /* fortress theme: wall the enemy base (leave a gate on the far edge) */ if (m->map_style == 3) { int sx = g->map.spawn_x[enemy], sy = g->map.spawn_y[enemy]; for (int dx = -2; dx <= 4; dx++) for (int dy = -2; dy <= 4; dy++) { int perimeter = (dx == -2 || dx == 4 || dy == -2 || dy == 4); if (!perimeter) continue; if (dy == 4) continue; /* gate on the far edge */ bld_spawn(g, BT_WALL, enemy, sx + dx, sy + dy); } bld_force_complete(g); } /* demo incomplete yards + damaged clash fortifications for scaffold/FX */ showcase_bld_battle_wear(g); /* centre camera on player base */ int hx = g->map.spawn_x[g->human], hy = g->map.spawn_y[g->human]; g->cam.x = (float)(hx*TILE - VIEW_W/2); g->cam.y = (float)(hy*TILE - VIEW_H/2); if (g->cam.x < 0) g->cam.x = 0; if (g->cam.y < 0) g->cam.y = 0; g->state = GAME_PLAYING; engine_log("Mission %d '%s': %s vs %s (%s)", m->id, m->name, faction_name(g->human), faction_name(enemy), m->diff==DIFF_EASY?"Easy":m->diff==DIFF_HARD?"Hard":"Normal"); } void engine_check_win(Game *g) { Faction human = g->human; Faction enemy = g->enemy; int human_base = 0, enemy_base = 0; int human_bld = 0, enemy_bld = 0; for (int i = 0; i < g->bld_count; i++) { Building *b = &g->buildings[i]; if (b->dead) continue; if (b->faction == human) { human_bld++; if (b->type==BT_BASE) human_base=1; } if (b->faction == enemy) { enemy_bld++; if (b->type==BT_BASE) enemy_base=1; } } if (!human_base || human_bld == 0) { g->state = GAME_LOST; g->winner = enemy; return; } switch (g->objective) { case OBJ_SURVIVE: if (g->time >= ((const Mission*)mission_get(g->mission_id))->survive_time) { g->state = GAME_WON; g->winner = human; } break; case OBJ_GATHER: if (g->scrap[human] >= ((const Mission*)mission_get(g->mission_id))->scrap_goal) { g->state = GAME_WON; g->winner = human; } break; default: /* OBJ_DESTROY_ALL */ if (!enemy_base || enemy_bld == 0) { g->state = GAME_WON; g->winner = human; } break; } } void engine_update(Game *g, float dt) { if (g->state != GAME_PLAYING) return; dt *= g->speed_mul; g->time += dt; g->frames++; unit_update(g, dt); bld_update(g, dt); bullet_update(g, dt); particle_update(g, dt); ai_update(g, dt); map_update_fog(g, g->human); audio_music_tick(dt); if (g->cam.shake > 0) { g->cam.shake -= dt * 24.0f; if (g->cam.shake < 0) g->cam.shake = 0; } engine_check_win(g); }