OpenKKnD/engine-c/src/map.c
2026-09-19 11:50:41 +02:00

481 lines
16 KiB
C

#include "kknd.h"
/* =========================================================================
* map.c - Wasteland terrain generation and A* pathfinding.
* ========================================================================= */
static Tile *g_tiles = NULL; /* owned by GameMap but accessed via map_tile */
void map_init(GameMap *m, RNG *rng) {
m->w = MAP_W;
m->h = MAP_H;
m->tiles = (Tile *)calloc((size_t)m->w * m->h, sizeof(Tile));
g_tiles = m->tiles;
m->fog = (uint8_t *)calloc((size_t)m->w * m->h, 1);
m->scrap_fields = 0;
/* base terrain: wasteland with scattered rubble */
for (int y = 0; y < m->h; y++) {
for (int x = 0; x < m->w; x++) {
Tile *t = &m->tiles[y * m->w + x];
t->terrain = T_WASTELAND;
t->variant = (uint8_t)rng_range(rng, 0, 7);
t->scrap = 0;
t->occupied = 0;
t->walkable = 1;
}
}
/* terrain theme knobs */
int rubble_n = 40, grass_n = 14, water_n = 8;
if (m->style == 1) { rubble_n = 22; grass_n = 20; water_n = 4; } /* open */
else if (m->style == 2) { rubble_n = 30; grass_n = 10; water_n = 16; } /* islands */
else if (m->style == 3) { rubble_n = 55; grass_n = 8; water_n = 8; } /* fortress */
/* rubble patches */
for (int i = 0; i < rubble_n; i++) {
int cx = rng_range(rng, 4, m->w - 5);
int cy = rng_range(rng, 4, m->h - 5);
int r = rng_range(rng, 3, 9);
for (int y = cy - r; y <= cy + r; y++)
for (int x = cx - r; x <= cx + r; x++) {
if (!map_in_bounds(m, x, y)) continue;
if (kknd_dist2((float)x,(float)y,(float)cx,(float)cy) <= r*r) {
Tile *t = &m->tiles[y*m->w + x];
if (rng_f32(rng) < 0.7f) t->terrain = T_RUBBLE;
}
}
}
/* grass oases */
for (int i = 0; i < grass_n; i++) {
int cx = rng_range(rng, 4, m->w - 5);
int cy = rng_range(rng, 4, m->h - 5);
int r = rng_range(rng, 3, 7);
for (int y = cy - r; y <= cy + r; y++)
for (int x = cx - r; x <= cx + r; x++) {
if (!map_in_bounds(m, x, y)) continue;
if (kknd_dist2((float)x,(float)y,(float)cx,(float)cy) <= r*r)
m->tiles[y*m->w + x].terrain = T_GRASS;
}
}
/* roads: a few straight-ish lines */
for (int i = 0; i < 6; i++) {
int horiz = rng_range(rng, 0, 1);
int pos = rng_range(rng, 8, m->w - 9);
int len = rng_range(rng, 20, 60);
int x = rng_range(rng, 2, m->w - 3);
int y = rng_range(rng, 2, m->h - 3);
for (int s = 0; s < len; s++) {
if (map_in_bounds(m, x, y)) m->tiles[y*m->w + x].terrain = T_ROAD;
if (horiz) x += (rng_f32(rng) < 0.85f) ? 1 : 0, y += rng_range(rng,-1,1);
else y += (rng_f32(rng) < 0.85f) ? 1 : 0, x += rng_range(rng,-1,1);
}
}
/* water lakes (blocked) */
for (int i = 0; i < water_n; i++) {
int cx = rng_range(rng, 6, m->w - 7);
int cy = rng_range(rng, 6, m->h - 7);
int r = rng_range(rng, 3, 8);
for (int y = cy - r; y <= cy + r; y++)
for (int x = cx - r; x <= cx + r; x++) {
if (!map_in_bounds(m, x, y)) continue;
if (kknd_dist2((float)x,(float)y,(float)cx,(float)cy) <= r*r) {
Tile *t = &m->tiles[y*m->w + x];
t->terrain = T_WATER;
t->walkable = 0;
}
}
}
/* rock mountains (blocked) */
for (int i = 0; i < 10; i++) {
int cx = rng_range(rng, 6, m->w - 7);
int cy = rng_range(rng, 6, m->h - 7);
int r = rng_range(rng, 3, 9);
for (int y = cy - r; y <= cy + r; y++)
for (int x = cx - r; x <= cx + r; x++) {
if (!map_in_bounds(m, x, y)) continue;
if (kknd_dist2((float)x,(float)y,(float)cx,(float)cy) <= r*r) {
Tile *t = &m->tiles[y*m->w + x];
if (t->terrain != T_WATER) {
t->terrain = T_ROCK;
t->walkable = 0;
}
}
}
}
/* scrap fields (resource) scattered, avoid spawn corners */
int attempts = 0;
while (m->scrap_fields < 22 && attempts < 4000) {
attempts++;
int cx = rng_range(rng, 6, m->w - 7);
int cy = rng_range(rng, 6, m->h - 7);
int r = rng_range(rng, 2, 4);
if (kknd_dist2((float)cx,(float)cy, 10, m->h-10) < 20*20) continue;
if (kknd_dist2((float)cx,(float)cy, m->w-10, 10) < 20*20) continue;
for (int y = cy - r; y <= cy + r; y++)
for (int x = cx - r; x <= cx + r; x++) {
if (!map_in_bounds(m, x, y)) continue;
if (kknd_dist2((float)x,(float)y,(float)cx,(float)cy) <= r*r) {
Tile *t = &m->tiles[y*m->w + x];
if (t->walkable && t->terrain != T_WATER) {
t->terrain = T_SCRAP;
t->scrap = 255;
m->scrap_fields++;
}
}
}
}
/* spawn points: Survivor bottom-left, Evolved top-right */
m->spawn_x[FACTION_SURVIVOR] = 10;
m->spawn_y[FACTION_SURVIVOR] = m->h - 10;
m->spawn_x[FACTION_EVOLVED] = m->w - 10;
m->spawn_y[FACTION_EVOLVED] = 10;
m->spawn_x[FACTION_SERIES9] = 10;
m->spawn_y[FACTION_SERIES9] = 10;
/* clear spawn areas to wasteland & walkable */
for (int f = 1; f <= 2; f++) {
int sx = m->spawn_x[f], sy = m->spawn_y[f];
for (int y = sy - 8; y <= sy + 8; y++)
for (int x = sx - 8; x <= sx + 8; x++) {
if (!map_in_bounds(m, x, y)) continue;
Tile *t = &m->tiles[y*m->w + x];
t->terrain = T_WASTELAND;
t->walkable = 1;
t->occupied = 0;
}
}
}
void map_free(GameMap *m) {
free(m->tiles);
free(m->fog);
m->tiles = NULL;
m->fog = NULL;
g_tiles = NULL;
}
void map_update_fog(Game *g, Faction f) {
uint8_t *vis = g->map.fog;
int n = g->map.w * g->map.h;
for (int i = 0; i < n; i++) if (vis[i] == 2) vis[i] = 1;
/* reveal around human units */
for (int i = 0; i < g->unit_count; i++) {
Unit *u = &g->units[i];
if (u->dead || u->faction != f) continue;
int cx = (int)(u->x / TILE), cy = (int)(u->y / TILE);
int r = (int)(u->sight / TILE) + 1;
if (r < 4) r = 4;
for (int y = cy - r; y <= cy + r; y++)
for (int x = cx - r; x <= cx + r; x++) {
if (!map_in_bounds(&g->map, x, y)) continue;
if (kknd_dist2((float)x,(float)y,(float)cx,(float)cy) <= r*r) {
int idx = y*g->map.w + x;
vis[idx] = 2;
}
}
}
/* reveal around human buildings */
for (int i = 0; i < g->bld_count; i++) {
Building *b = &g->buildings[i];
if (b->dead || b->faction != f) continue;
int cx = b->tx + b->w/2, cy = b->ty + b->h/2;
int r = 8 + b->w;
for (int y = cy - r; y <= cy + r; y++)
for (int x = cx - r; x <= cx + r; x++) {
if (!map_in_bounds(&g->map, x, y)) continue;
if (kknd_dist2((float)x,(float)y,(float)cx,(float)cy) <= r*r) {
int idx = y*g->map.w + x;
vis[idx] = 2;
}
}
}
}
Tile *map_tile(GameMap *m, int tx, int ty) {
if (!map_in_bounds(m, tx, ty)) return NULL;
return &m->tiles[ty * m->w + tx];
}
int map_in_bounds(GameMap *m, int tx, int ty) {
return tx >= 0 && ty >= 0 && tx < m->w && ty < m->h;
}
int map_walkable(GameMap *m, int tx, int ty) {
if (!map_in_bounds(m, tx, ty)) return 0;
Tile *t = &m->tiles[ty * m->w + tx];
return t->walkable && !t->occupied;
}
void map_world_to_tile(float wx, float wy, int *tx, int *ty) {
*tx = (int)(wx / TILE);
*ty = (int)(wy / TILE);
}
void map_tile_to_world(int tx, int ty, float *wx, float *wy) {
*wx = (float)(tx * TILE + TILE / 2);
*wy = (float)(ty * TILE + TILE / 2);
}
int map_find_nearest_scrap(GameMap *m, float wx, float wy, float *outx, float *outy) {
int best_tx = -1, best_ty = -1;
float best = 1e18f;
for (int y = 0; y < m->h; y++) {
for (int x = 0; x < m->w; x++) {
Tile *t = &m->tiles[y*m->w + x];
if (t->terrain == T_SCRAP && t->scrap > 0) {
float dx = (float)(x*TILE) - wx, dy = (float)(y*TILE) - wy;
float d = dx*dx + dy*dy;
if (d < best) { best = d; best_tx = x; best_ty = y; }
}
}
}
if (best_tx < 0) return 0;
map_tile_to_world(best_tx, best_ty, outx, outy);
return 1;
}
void map_deplete_scrap(GameMap *m, int tx, int ty, int amount) {
Tile *t = map_tile(m, tx, ty);
if (!t || t->terrain != T_SCRAP) return;
int s = (int)t->scrap - amount;
if (s <= 0) {
t->scrap = 0;
t->terrain = T_RUBBLE; /* depleted field becomes rubble */
} else {
t->scrap = (uint8_t)s;
}
}
/* Soft scar: never overwrite WATER/ROCK; never clear walkable. */
static int map_scar_ok(Tile *t) {
if (!t) return 0;
if (t->terrain == T_WATER || t->terrain == T_ROCK) return 0;
return 1;
}
void map_scar_rubble(GameMap *m, int tx, int ty) {
Tile *t = map_tile(m, tx, ty);
if (!map_scar_ok(t)) return;
/* keep ROAD as approach cue; scrap fields stay harvestable */
if (t->terrain == T_ROAD || t->terrain == T_SCRAP) return;
t->terrain = T_RUBBLE;
t->scrap = 0;
t->walkable = 1;
}
void map_scar_scrap_fleck(GameMap *m, int tx, int ty, uint8_t amount) {
Tile *t = map_tile(m, tx, ty);
if (!map_scar_ok(t)) return;
if (t->terrain == T_ROAD) return;
if (t->terrain == T_SCRAP && t->scrap > 0) return; /* leave live fields alone */
t->terrain = T_SCRAP;
t->scrap = amount ? amount : 40;
t->walkable = 1;
}
void map_scar_unit_death(GameMap *m, float wx, float wy, UnitType type) {
int tx, ty;
map_world_to_tile(wx, wy, &tx, &ty);
map_scar_rubble(m, tx, ty);
int vehicle = (type == UT_BUGGY || type == UT_TANK || type == UT_HARVESTER
|| type == UT_ARTILLERY || type == UT_JEEP || type == UT_RAVAGER
|| type == UT_MISSILE || type == UT_BEAST);
int heavy = (type == UT_TANK || type == UT_RAVAGER || type == UT_ARTILLERY
|| type == UT_HARVESTER);
if (!vehicle) return;
/* adjacent rubble ring — still soft / walkable */
static const int ox[8] = { 1,-1, 0, 0, 1, 1,-1,-1 };
static const int oy[8] = { 0, 0, 1,-1, 1,-1, 1,-1 };
int n = heavy ? 6 : 3;
for (int i = 0; i < n; i++)
map_scar_rubble(m, tx + ox[i], ty + oy[i]);
/* small scrap flecks from wrecked chassis */
if (heavy) {
map_scar_scrap_fleck(m, tx + 1, ty - 1, (uint8_t)(55 + ((tx * 7 + ty) & 63)));
map_scar_scrap_fleck(m, tx - 1, ty + 1, (uint8_t)(40 + ((tx * 3 + ty * 5) & 47)));
} else {
map_scar_scrap_fleck(m, tx + ox[tx & 3], ty + oy[ty & 3],
(uint8_t)(35 + ((tx + ty) & 31)));
}
}
void map_scar_bld_death(GameMap *m, int tx, int ty, int w, int h) {
if (w < 1) w = 1;
if (h < 1) h = 1;
for (int y = ty; y < ty + h; y++) {
for (int x = tx; x < tx + w; x++) {
map_scar_rubble(m, x, y);
/* outer flecks just outside footprint */
if (x == tx) map_scar_rubble(m, x - 1, y);
if (x == tx + w - 1) map_scar_rubble(m, x + 1, y);
if (y == ty) map_scar_rubble(m, x, y - 1);
if (y == ty + h - 1) map_scar_rubble(m, x, y + 1);
}
}
/* one scrap pile from collapsed structure */
map_scar_scrap_fleck(m, tx + w / 2, ty + h / 2,
(uint8_t)(70 + ((tx * 11 + ty * 13) & 80)));
}
/* ----------------------------- A* ----------------------------- */
/* Binary heap of tile indices keyed by f-score in a parallel array. */
#define ASTAR_NODES (MAP_W * MAP_H)
typedef struct {
int g[ASTAR_NODES];
int f[ASTAR_NODES];
int came[ASTAR_NODES];
int closed[ASTAR_NODES];
int open[ASTAR_NODES];
int heap[ASTAR_NODES];
int heap_n;
} AStar;
static AStar *g_astar = NULL;
static void heap_push(AStar *a, int idx) {
int i = a->heap_n++;
a->heap[i] = idx;
while (i > 0) {
int p = (i - 1) / 2;
if (a->f[a->heap[p]] <= a->f[a->heap[i]]) break;
int tmp = a->heap[p]; a->heap[p] = a->heap[i]; a->heap[i] = tmp;
i = p;
}
}
static int heap_pop(AStar *a) {
int top = a->heap[0];
a->heap[0] = a->heap[--a->heap_n];
int i = 0;
for (;;) {
int l = 2*i+1, r = 2*i+2, m = i;
if (l < a->heap_n && a->f[a->heap[l]] < a->f[a->heap[m]]) m = l;
if (r < a->heap_n && a->f[a->heap[r]] < a->f[a->heap[m]]) m = r;
if (m == i) break;
int tmp = a->heap[m]; a->heap[m] = a->heap[i]; a->heap[i] = tmp;
i = m;
}
return top;
}
static int heuristic(int ax, int ay, int bx, int by) {
int dx = abs(ax - bx), dy = abs(ay - by);
return (dx + dy) + (14 - 20) * (dx < dy ? dx : dy); /* octile-ish */
}
static const int NX[8] = { 1,-1,0,0,1,1,-1,-1 };
static const int NY[8] = { 0,0,1,-1,1,-1,1,-1 };
int path_find(GameMap *m, int sx, int sy, int gx, int gy, Path *out) {
out->len = 0;
if (!map_in_bounds(m, sx, sy) || !map_in_bounds(m, gx, gy)) return 0;
/* if goal blocked, retarget to nearest walkable neighbour */
if (!map_walkable(m, gx, gy)) {
int best = -1, bestd = 1e9;
for (int d = 0; d < 8; d++) {
int nx = gx + NX[d], ny = gy + NY[d];
if (map_walkable(m, nx, ny)) {
int dist = abs(nx-sx)+abs(ny-sy);
if (dist < bestd) { bestd = dist; best = ny*m->w+nx; }
}
}
if (best < 0) return 0;
gx = best % m->w; gy = best / m->w;
}
if (g_astar == NULL) g_astar = (AStar *)calloc(1, sizeof(AStar));
AStar *a = g_astar;
for (int i = 0; i < m->w*m->h; i++) { a->closed[i]=0; a->came[i]=-1; a->g[i]=1e9; }
a->heap_n = 0;
int start = sy*m->w + sx;
int goal = gy*m->w + gx;
a->g[start] = 0;
a->f[start] = heuristic(sx, sy, gx, gy);
heap_push(a, start);
a->open[start] = 1;
int found = 0;
int iter = 0;
while (a->heap_n > 0 && iter++ < ASTAR_NODES * 4) {
int cur = heap_pop(a);
if (a->closed[cur]) continue;
a->closed[cur] = 1;
if (cur == goal) { found = 1; break; }
int cx = cur % m->w, cy = cur / m->w;
for (int d = 0; d < 8; d++) {
int nx = cx + NX[d], ny = cy + NY[d];
if (!map_in_bounds(m, nx, ny)) continue;
int ni = ny*m->w + nx;
if (a->closed[ni]) continue;
int step = (d < 4) ? 10 : 14;
if (!map_walkable(m, nx, ny) && ni != goal) continue;
int ng = a->g[cur] + step;
if (ng < a->g[ni]) {
a->g[ni] = ng;
a->came[ni] = cur;
a->f[ni] = ng + heuristic(nx, ny, gx, gy);
heap_push(a, ni);
}
}
}
if (!found) return 0;
/* reconstruct */
int buf[ASTAR_NODES];
int n = 0;
for (int c = goal; c != -1; c = a->came[c]) buf[n++] = c;
/* reverse */
int len = 0;
for (int i = n - 1; i >= 0 && len < MAX_PATH; i--) {
out->nodes[len].tx = buf[i] % m->w;
out->nodes[len].ty = buf[i] / m->w;
len++;
}
out->len = len;
return 1;
}
void path_smooth(GameMap *m, Path *p) {
if (p->len < 3) return;
/* simple line-of-sight smoothing: skip waypoints when straight clear */
int out_i = 0;
int i = 0;
while (i < p->len - 1) {
int j = p->len - 1;
while (j > i + 1) {
/* check tiles between i and j */
int x0 = p->nodes[i].tx, y0 = p->nodes[i].ty;
int x1 = p->nodes[j].tx, y1 = p->nodes[j].ty;
int steps = abs(x1-x0) > abs(y1-y0) ? abs(x1-x0) : abs(y1-y0);
int ok = 1;
for (int s = 1; s < steps; s++) {
int tx = x0 + (x1-x0)*s/steps;
int ty = y0 + (y1-y0)*s/steps;
if (!map_walkable(m, tx, ty)) { ok = 0; break; }
}
if (ok) break;
j--;
}
p->nodes[out_i] = p->nodes[i];
out_i++;
i = j;
}
p->nodes[out_i] = p->nodes[p->len-1];
out_i++;
p->len = out_i;
}