65ee23c5be
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
156 lines
5.5 KiB
TypeScript
156 lines
5.5 KiB
TypeScript
import { WORLD_WIDTH, WORLD_HEIGHT, Terrain } from '@dflike/shared';
|
|
import { industryConfig } from '../config/industryConfig.js';
|
|
|
|
export interface GeneratedMap {
|
|
terrain: number[];
|
|
decorations: number[];
|
|
trunkDecorations: number[];
|
|
obstacles: Set<string>;
|
|
foodPositions: { x: number; y: number }[];
|
|
resourceTiles: Array<{ x: number; y: number; resourceType: string }>;
|
|
}
|
|
|
|
// Simple seeded pseudo-random (mulberry32)
|
|
function createRng(seed: number) {
|
|
let s = seed | 0;
|
|
return () => {
|
|
s = (s + 0x6D2B79F5) | 0;
|
|
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
};
|
|
}
|
|
|
|
function randRange(rng: () => number, min: number, max: number): number {
|
|
return min + Math.floor(rng() * (max - min + 1));
|
|
}
|
|
|
|
export function generateMap(
|
|
width = WORLD_WIDTH,
|
|
height = WORLD_HEIGHT,
|
|
_seed?: number,
|
|
): GeneratedMap {
|
|
const rng = _seed !== undefined ? createRng(_seed) : Math.random;
|
|
const terrain = new Array<number>(width * height).fill(Terrain.GRASS);
|
|
const decorations = new Array<number>(width * height).fill(-1);
|
|
const trunkDecorations = new Array<number>(width * height).fill(-1);
|
|
const obstacles = new Set<string>();
|
|
const resourceTiles: Array<{ x: number; y: number; resourceType: string }> = [];
|
|
|
|
const foodPositions = [
|
|
{ x: Math.floor(width * 0.25), y: Math.floor(height * 0.25) },
|
|
{ x: Math.floor(width * 0.75), y: Math.floor(height * 0.75) },
|
|
];
|
|
// Reserve positions that must stay clear
|
|
const reserved = new Set<string>();
|
|
for (const p of [...foodPositions]) {
|
|
reserved.add(`${p.x},${p.y}`);
|
|
}
|
|
|
|
// --- Water ponds ---
|
|
const pondCount = randRange(rng, industryConfig.waterPondCount.min, industryConfig.waterPondCount.max);
|
|
for (let p = 0; p < pondCount; p++) {
|
|
const cx = randRange(rng, 4, width - 5);
|
|
const cy = randRange(rng, 4, height - 5);
|
|
const size = randRange(rng, industryConfig.waterPondSize.min, industryConfig.waterPondSize.max);
|
|
|
|
// Random walk to create organic pond shape
|
|
const pondTiles = new Set<string>();
|
|
let wx = cx, wy = cy;
|
|
for (let s = 0; s < size * 3; s++) {
|
|
const key = `${wx},${wy}`;
|
|
if (!reserved.has(key) && wx > 1 && wx < width - 2 && wy > 1 && wy < height - 2) {
|
|
pondTiles.add(key);
|
|
}
|
|
const dir = Math.floor(rng() * 4);
|
|
if (dir === 0) wx++;
|
|
else if (dir === 1) wx--;
|
|
else if (dir === 2) wy++;
|
|
else wy--;
|
|
}
|
|
|
|
for (const key of pondTiles) {
|
|
const [x, y] = key.split(',').map(Number);
|
|
terrain[y * width + x] = Terrain.WATER;
|
|
obstacles.add(key);
|
|
}
|
|
|
|
// Water gather points: walkable tiles adjacent to water
|
|
const waterResourceSet = new Set<string>();
|
|
for (const key of pondTiles) {
|
|
const [x, y] = key.split(',').map(Number);
|
|
for (const [nx, ny] of [[x-1,y],[x+1,y],[x,y-1],[x,y+1]] as [number,number][]) {
|
|
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
|
const nk = `${nx},${ny}`;
|
|
if (!pondTiles.has(nk) && !obstacles.has(nk) && terrain[ny * width + nx] !== Terrain.WATER) {
|
|
if (!waterResourceSet.has(nk)) {
|
|
waterResourceSet.add(nk);
|
|
resourceTiles.push({ x: nx, y: ny, resourceType: 'water' });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Post-process: remove water resource tiles that ended up under a later pond
|
|
for (let i = resourceTiles.length - 1; i >= 0; i--) {
|
|
const r = resourceTiles[i];
|
|
if (r.resourceType === 'water' && terrain[r.y * width + r.x] === Terrain.WATER) {
|
|
resourceTiles.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// --- Stone deposits ---
|
|
const stoneCount = randRange(rng, industryConfig.stoneClusterCount.min, industryConfig.stoneClusterCount.max);
|
|
for (let s = 0; s < stoneCount; s++) {
|
|
const cx = randRange(rng, 3, width - 4);
|
|
const cy = randRange(rng, 3, height - 4);
|
|
const size = randRange(rng, industryConfig.stoneClusterSize.min, industryConfig.stoneClusterSize.max);
|
|
|
|
for (let i = 0; i < size; i++) {
|
|
const ox = randRange(rng, -1, 1);
|
|
const oy = randRange(rng, -1, 1);
|
|
const x = cx + ox;
|
|
const y = cy + oy;
|
|
const key = `${x},${y}`;
|
|
if (!reserved.has(key) && !obstacles.has(key) && terrain[y * width + x] === Terrain.GRASS) {
|
|
terrain[y * width + x] = Terrain.STONE;
|
|
resourceTiles.push({ x, y, resourceType: 'stone' });
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Trees ---
|
|
const treeClusterCount = randRange(rng, 8, 14);
|
|
for (let c = 0; c < treeClusterCount; c++) {
|
|
const cx = randRange(rng, 3, width - 4);
|
|
const cy = randRange(rng, 3, height - 4);
|
|
const treeCount = randRange(rng, 2, 5);
|
|
|
|
for (let t = 0; t < treeCount; t++) {
|
|
const x = cx + randRange(rng, -2, 2);
|
|
const y = cy + randRange(rng, -2, 2);
|
|
if (x < 1 || x >= width - 1 || y < 1 || y >= height - 1) continue;
|
|
const key = `${x},${y}`;
|
|
if (reserved.has(key) || obstacles.has(key) || terrain[y * width + x] !== Terrain.GRASS) continue;
|
|
|
|
// Trunk at (x, y) — obstacle and resource tile
|
|
const trunkTile = randRange(rng, 0, 5);
|
|
trunkDecorations[y * width + x] = trunkTile;
|
|
obstacles.add(key);
|
|
|
|
// Canopy at (x, y-1) — visual only
|
|
if (y > 0) {
|
|
const canopyTile = randRange(rng, 0, 5);
|
|
decorations[(y - 1) * width + x] = canopyTile;
|
|
}
|
|
|
|
// Mark trunk tile as log resource
|
|
resourceTiles.push({ x, y, resourceType: 'log' });
|
|
}
|
|
}
|
|
|
|
return { terrain, decorations, trunkDecorations, obstacles, foodPositions, resourceTiles };
|
|
}
|