shellquest
Version:
Terminal-based procedurally generated dungeon crawler
164 lines (143 loc) • 5.28 kB
text/typescript
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;
}
}
}
}
}
}