584 lines
24 KiB
C
584 lines
24 KiB
C
#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 the raid locus 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;
|
|
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;
|
|
/* 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: turrets / walls / power take hits */
|
|
if (damaged_n < 6 && d2 < 160.0f * 160.0f &&
|
|
(b->type == BT_TURRET || b->type == BT_WALL || b->type == BT_POWER)) {
|
|
float frac = (damaged_n % 3 == 0) ? 0.35f
|
|
: (damaged_n % 3 == 1) ? 0.55f : 0.72f;
|
|
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. */
|
|
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_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_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);
|
|
for (int i = 0; i < 10; i++)
|
|
unit_spawn(g, UT_INFANTRY, human, wx + 88 + (i % 5) * 14, wy + 8 + (i / 5) * 16);
|
|
|
|
/* 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 */
|
|
}
|
|
|
|
/* 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 <= 28 && ((dx + dy * 3) & 3) == 0) {
|
|
t->terrain = T_SCRAP;
|
|
t->scrap = (uint8_t)(90 + ((dx * 13 + dy * 7) & 127));
|
|
} else if (d2 <= 32 && ((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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int raid[56];
|
|
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_ARTILLERY, enemy, rx + 85, ry + 10);
|
|
raid[rn++] = unit_spawn(g, UT_ARTILLERY, enemy, rx + 98, ry - 18);
|
|
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);
|
|
|
|
|
|
/* 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 (sim may add more) */
|
|
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;
|
|
if (dx * dx + dy * dy > 140.0f * 140.0f) continue;
|
|
float f = 0.35f + 0.45f * ((i * 17) % 100) / 100.0f;
|
|
u->hp = u->max_hp * f;
|
|
}
|
|
|
|
/* FX seeds at the clash locus */
|
|
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);
|
|
|
|
/* point raid at nearest human combat units / HQ */
|
|
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;
|
|
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;
|
|
prey = i; break;
|
|
}
|
|
for (int i = 0; i < rn; i++) {
|
|
if (raid[i] < 0) continue;
|
|
if (prey >= 0) unit_issue_attack_unit(g, raid[i], prey);
|
|
else if (hq >= 0) unit_issue_attack_bld(g, raid[i], hq);
|
|
}
|
|
|
|
/* human combat units engage nearest raiders (spread targets) */
|
|
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;
|
|
if (dx * dx + dy * dy > 220.0f * 220.0f) continue;
|
|
int tgt = -1;
|
|
for (int k = 0; k < rn; k++) {
|
|
int ri = raid[(ridx + k) % rn];
|
|
if (ri >= 0 && !g->units[ri].dead) { tgt = ri; ridx += k + 1; break; }
|
|
}
|
|
if (tgt >= 0) unit_issue_attack_unit(g, i, tgt);
|
|
}
|
|
|
|
/* select a front-line squad for yellow selection boxes in the shot */
|
|
unit_clear_selection(g);
|
|
int nsel = 0;
|
|
for (int i = 0; i < g->unit_count && nsel < 8; 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;
|
|
if (dx * dx + dy * dy > 120.0f * 120.0f) continue;
|
|
unit_select_single(g, i, 1);
|
|
nsel++;
|
|
}
|
|
}
|
|
|
|
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->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);
|
|
}
|