481 lines
16 KiB
C
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;
|
|
}
|