// ============ map.js — tile world, terrain gen, object placement ============ import { makeRng } from '../core/util.js'; export const T_GRASS = 0, T_SAND = 1, T_ROCK = 2, T_WATER = 3; export class GameMap { constructor(size = 52) { this.size = size; this.terrain = new Uint8Array(size * size); // terrain type this.pathType = new Uint8Array(size * size); // 0 none, 1 pavement, 2 cobble this.litter = new Float32Array(size * size); // 0..1 litter amount this.vomit = new Float32Array(size * size); // occupancy: null or {kind:'ride'|'shop'|'scenery'|'guild', id, ox, oy} where id indexes state arrays this.objects = new Array(size * size).fill(null); // coaster track occupies cells too: {kind:'track', rideId, pieceIndex} this.beauty = new Float32Array(size * size); // accumulated scenery beauty for rating } idx(x, y) { return y * this.size + x; } inBounds(x, y) { return x >= 0 && y >= 0 && x < this.size && y < this.size; } terrainAt(x, y) { return this.inBounds(x, y) ? this.terrain[this.idx(x, y)] : -1; } isPath(x, y) { return this.inBounds(x, y) && this.pathType[this.idx(x, y)] > 0; } isWalkable(x, y) { if (!this.inBounds(x, y)) return false; const i = this.idx(x, y); if (this.pathType[i] > 0) return true; return false; } isBuildable(x, y) { if (!this.inBounds(x, y)) return false; const i = this.idx(x, y); if (this.terrain[i] === T_WATER) return false; return true; } occupied(x, y) { if (!this.inBounds(x, y)) return true; return this.objects[this.idx(x, y)] !== null; } setObject(x, y, obj) { this.objects[this.idx(x, y)] = obj; } getObject(x, y) { return this.inBounds(x, y) ? this.objects[this.idx(x, y)] : { kind: 'out' }; } clearObject(x, y) { this.objects[this.idx(x, y)] = null; } /** Generate terrain from scenario gen spec */ generate(gen, seed) { const rng = makeRng(seed); const n = this.size; // value-noise-ish rolling grass with rock patches const rockNoise = makeRng(seed ^ 0x9e3779b9); const lakeCount = gen.lake ?? 1; const lakes = []; for (let i = 0; i < lakeCount; i++) { lakes.push({ x: 8 + rng() * (n - 20), y: 8 + rng() * (n - 20), r: 4 + rng() * 4 }); } // entrance at south edge center this.entranceX = Math.floor(n / 2); this.entranceY = n - 3; for (let y = 0; y < n; y++) { for (let x = 0; x < n; x++) { const i = this.idx(x, y); let t = T_GRASS; // rocky regions if (gen.rocks) { const v = rockNoise(); if (v < 0.10 + (gen.rocky ? 0.14 : 0)) t = T_ROCK; } else if (rng() < 0.03) t = T_ROCK; // sand near water for (const L of lakes) { const d = Math.hypot(x - L.x, y - L.y); if (d < L.r) t = T_WATER; else if (d < L.r + 1.6 && gen.sand) t = T_SAND; } this.terrain[i] = t; } } // scatter rocks as scenery later by renderer using deterministic rng this.scatterSeed = seed; } /** find a free rect area of w×h buildable & unoccupied tiles near cx,cy */ findFreeRect(w, h, cx, cy, maxR = 40) { for (let r = 0; r < maxR; r++) { for (let dy = -r; dy <= r; dy++) { for (let dx = -r; dx <= r; dx++) { const x = cx + dx, y = cy + dy; let ok = true; for (let yy = 0; yy < h && ok; yy++) { for (let xx = 0; xx < w && ok; xx++) { if (!this.isBuildable(x + xx, y + yy) || this.occupied(x + xx, y + yy)) ok = false; } } if (ok) return { x, y }; } } } return null; } countLitter() { let c = 0; for (let i = 0; i < this.litter.length; i++) if (this.litter[i] > 0.25) c++; return c; } }