UNPKG

shellquest

Version:

Terminal-based procedurally generated dungeon crawler

164 lines (143 loc) 5.28 kB
import type {PRNG} from 'seedrandom'; import {createNoise2D} from 'simplex-noise'; import type {LevelTile} from '../types.ts'; import {getTileName} from '../types.ts'; export class TerrainGenerator { constructor( private tiles: LevelTile[][], private width: number, private height: number, ) {} generateTerrain(rng: PRNG): void { const noise2D = createNoise2D(rng); const getNoiseValue = (x: number, y: number): number => { let value = 0; let amplitude = 1; let frequency = 0.03; let maxValue = 0; for (let i = 0; i < 4; i++) { value += noise2D(x * frequency, y * frequency) * amplitude; maxValue += amplitude; amplitude *= 0.5; frequency *= 2; } return value / maxValue; }; const moistureNoise = createNoise2D(rng); const getMoisture = (x: number, y: number): number => { return moistureNoise(x * 0.02, y * 0.02); }; for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { const noiseValue = getNoiseValue(x, y); const moisture = getMoisture(x, y); let tileName: string; if (noiseValue < -0.3) { const rand = rng(); const dirtVariant = rand < 0.7 ? 1 : rand < 0.85 ? 2 : rand < 0.93 ? 3 : rand < 0.97 ? 4 : 5; tileName = `dirt-${dirtVariant}`; } else if (noiseValue < 0.2) { if (moisture < -0.2) { const rand = rng(); const dirtVariant = rand < 0.7 ? 1 : rand < 0.85 ? 2 : rand < 0.93 ? 3 : rand < 0.97 ? 4 : 5; tileName = `dirt-${dirtVariant}`; } else { const rand = rng(); const grassVariant = rand < 0.7 ? 1 : rand < 0.9 ? 2 : 3; tileName = `grass-${grassVariant}`; } } else { const rand = rng(); const grassVariant = rand < 0.7 ? 1 : rand < 0.9 ? 2 : 3; tileName = `grass-${grassVariant}`; } this.tiles[y][x] = { bottomTile: tileName, solid: false, }; } } const patchNoise = createNoise2D(rng); for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { const patchValue = patchNoise(x * 0.1, y * 0.1); if (patchValue > 0.6) { const currentTileName = getTileName(this.tiles[y][x].bottomTile); if (currentTileName.startsWith('grass')) { const rand = rng(); const dirtVariant = rand < 0.7 ? 1 : rand < 0.85 ? 2 : rand < 0.93 ? 3 : rand < 0.97 ? 4 : 5; this.tiles[y][x].bottomTile = `dirt-${dirtVariant}`; } } const pathValue = patchNoise(x * 0.05 + 100, y * 0.05 + 100); if (Math.abs(pathValue) < 0.08) { this.tiles[y][x].bottomTile = 'grass-walking-path'; } } } this.applyBorders(); } private applyBorders(): void { const binaryMap: number[][] = []; for (let y = 0; y < this.height; y++) { binaryMap[y] = []; for (let x = 0; x < this.width; x++) { const tileName = getTileName(this.tiles[y][x].bottomTile); binaryMap[y][x] = tileName.startsWith('dirt') ? 1 : 0; } } for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { if (binaryMap[y][x] === 1) { const top = y > 0 ? binaryMap[y - 1][x] : 0; const bottom = y < this.height - 1 ? binaryMap[y + 1][x] : 0; const left = x > 0 ? binaryMap[y][x - 1] : 0; const right = x < this.width - 1 ? binaryMap[y][x + 1] : 0; const edgeConfig = ((top === 0 ? 1 : 0) << 0) | ((right === 0 ? 1 : 0) << 1) | ((bottom === 0 ? 1 : 0) << 2) | ((left === 0 ? 1 : 0) << 3); let borderTile: string | null = null; switch (edgeConfig) { case 0b1001: borderTile = 'grass-outer-dirt-inner-9slice-top-left'; break; case 0b0001: borderTile = 'grass-outer-dirt-inner-9slice-top'; break; case 0b0011: borderTile = 'grass-outer-dirt-inner-9slice-top-right'; break; case 0b1000: borderTile = 'grass-outer-dirt-inner-9slice-middle-left'; break; case 0b0010: borderTile = 'grass-outer-dirt-inner-9slice-middle-right'; break; case 0b1100: borderTile = 'grass-outer-dirt-inner-9slice-bottom-left'; break; case 0b0100: borderTile = 'grass-outer-dirt-inner-9slice-bottom'; break; case 0b0110: borderTile = 'grass-outer-dirt-inner-9slice-bottom-right'; break; case 0: break; default: borderTile = 'grass-1'; break; } if (borderTile) { this.tiles[y][x].bottomTile = borderTile; } } } } } }