#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; }