UNPKG

shellquest

Version:

Terminal-based procedurally generated dungeon crawler

403 lines (349 loc) 10.6 kB
/** * Dungeon Utilities - Constants, Types, and Noise Functions * Ported from web/src/app/routes/wall-gen/tileUtils.ts for TUI game */ // ============================================================================= // CONSTANTS // ============================================================================= export const DUNGEON_TILE_SIZE = 16; export const DUNGEON_MAP_WIDTH = 100; export const DUNGEON_MAP_HEIGHT = 100; // Cell types export const CELL_FLOOR = 0; export const CELL_WALL = 1; export type CellType = typeof CELL_FLOOR | typeof CELL_WALL; export type TileCoords = [col: number, row: number]; export type TileGrid = CellType[][]; // ============================================================================= // SEEDED RANDOM NUMBER GENERATOR // ============================================================================= export class SeededRandom { private seed: number; constructor(seed: number) { this.seed = seed; } next(): number { let t = (this.seed += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; } nextInt(min: number, max: number): number { return Math.floor(this.next() * (max - min + 1)) + min; } nextFloat(min: number, max: number): number { return this.next() * (max - min) + min; } nextBool(p: number = 0.5): boolean { return this.next() < p; } shuffle<T>(array: T[]): T[] { const result = [...array]; for (let i = result.length - 1; i > 0; i--) { const j = Math.floor(this.next() * (i + 1)); [result[i], result[j]] = [result[j], result[i]]; } return result; } pick<T>(array: T[]): T { return array[Math.floor(this.next() * array.length)]; } } // ============================================================================= // SIMPLE NOISE IMPLEMENTATION (no external dependencies) // ============================================================================= // Simple seeded noise implementation let noiseSeed = 12345; let noisePermutation: number[] = []; function initNoisePermutation(seed: number): void { const rng = new SeededRandom(seed); noisePermutation = []; for (let i = 0; i < 256; i++) { noisePermutation[i] = i; } // Shuffle for (let i = 255; i > 0; i--) { const j = Math.floor(rng.next() * (i + 1)); [noisePermutation[i], noisePermutation[j]] = [noisePermutation[j], noisePermutation[i]]; } // Duplicate for overflow for (let i = 0; i < 256; i++) { noisePermutation[256 + i] = noisePermutation[i]; } } function fade(t: number): number { return t * t * t * (t * (t * 6 - 15) + 10); } function lerp(a: number, b: number, t: number): number { return a + t * (b - a); } function grad(hash: number, x: number, y: number): number { const h = hash & 3; const u = h < 2 ? x : y; const v = h < 2 ? y : x; return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v); } function perlin2D(x: number, y: number): number { const X = Math.floor(x) & 255; const Y = Math.floor(y) & 255; const xf = x - Math.floor(x); const yf = y - Math.floor(y); const u = fade(xf); const v = fade(yf); const aa = noisePermutation[noisePermutation[X] + Y]; const ab = noisePermutation[noisePermutation[X] + Y + 1]; const ba = noisePermutation[noisePermutation[X + 1] + Y]; const bb = noisePermutation[noisePermutation[X + 1] + Y + 1]; const x1 = lerp(grad(aa, xf, yf), grad(ba, xf - 1, yf), u); const x2 = lerp(grad(ab, xf, yf - 1), grad(bb, xf - 1, yf - 1), u); return lerp(x1, x2, v); } export function setNoiseSeed(seed: number): void { noiseSeed = seed; initNoisePermutation(seed); } export function getNoiseSeed(): number { return noiseSeed; } // Initialize with default seed initNoisePermutation(noiseSeed); // Get 2D noise value in range [0, 1] export function getNoise2D(x: number, y: number, scale: number = 0.1): number { return (perlin2D(x * scale, y * scale) + 1) / 2; } // Get layered noise (octaves) for more natural variation export function getLayeredNoise( x: number, y: number, octaves: number = 3, persistence: number = 0.5, scale: number = 0.1 ): number { let total = 0; let frequency = scale; let amplitude = 1; let maxValue = 0; for (let i = 0; i < octaves; i++) { total += ((perlin2D(x * frequency, y * frequency) + 1) / 2) * amplitude; maxValue += amplitude; amplitude *= persistence; frequency *= 2; } return total / maxValue; } // Generate deterministic noise value for a tile (for texture variation) export function getTileNoise(x: number, y: number): number { return Math.floor(getNoise2D(x, y, 0.15) * 100); } // ============================================================================= // NEIGHBOR UTILITIES // ============================================================================= export interface Neighbors { n: boolean; // north s: boolean; // south e: boolean; // east w: boolean; // west ne: boolean; // northeast nw: boolean; // northwest se: boolean; // southeast sw: boolean; // southwest } export function getCell(grid: TileGrid, x: number, y: number): CellType { if (x < 0 || x >= grid[0].length || y < 0 || y >= grid.length) { return CELL_WALL; } return grid[y][x]; } export function isWall(grid: TileGrid, x: number, y: number): boolean { return getCell(grid, x, y) === CELL_WALL; } export function isFloor(grid: TileGrid, x: number, y: number): boolean { return getCell(grid, x, y) === CELL_FLOOR; } export function getNeighbors(grid: TileGrid, x: number, y: number): Neighbors { return { n: isWall(grid, x, y - 1), s: isWall(grid, x, y + 1), e: isWall(grid, x + 1, y), w: isWall(grid, x - 1, y), ne: isWall(grid, x + 1, y - 1), nw: isWall(grid, x - 1, y - 1), se: isWall(grid, x + 1, y + 1), sw: isWall(grid, x - 1, y + 1), }; } export function isWallVisible(grid: TileGrid, x: number, y: number): boolean { return ( isFloor(grid, x, y - 1) || isFloor(grid, x, y + 1) || isFloor(grid, x + 1, y) || isFloor(grid, x - 1, y) || isWall(grid, x, y + 1) ); } // ============================================================================= // DECAL SYSTEM TYPES // ============================================================================= export interface DecalPlacement { definitionId: string; x: number; y: number; } export type TrackDirection = 'up' | 'down' | 'left' | 'right'; export interface TrackSegment { x: number; y: number; tileId: string; // Use tile name instead of coords for TUI } export interface TrackPath { segments: TrackSegment[]; } export interface FenceSegment { x: number; y: number; tileId: string; // Use tile name instead of coords for TUI } export interface FencePlacement { segments: FenceSegment[]; } // ============================================================================= // DECAL DEFINITIONS // ============================================================================= export interface DecalDefinition { id: string; name: string; tiles: string[]; // Tile names from bottom to top width: 1 | 2; frequency: number; minSpacing: number; placement: 'wall_face' | 'ground' | 'wall_to_ground'; nearWall?: boolean; group?: string; } export const DECAL_DEFINITIONS: DecalDefinition[] = [ // Ground decals { id: 'table', name: 'Table', tiles: ['dungeon-decal-table'], width: 1, frequency: 0.003, minSpacing: 2, placement: 'ground', }, { id: 'ground-knub', name: 'Ground Knub', tiles: ['dungeon-decal-ground-knub'], width: 1, frequency: 0.025, minSpacing: 3, placement: 'ground', }, { id: 'ground-anvil', name: 'Ground Anvil', tiles: ['dungeon-decal-ground-anvil'], width: 1, frequency: 0.004, minSpacing: 2, placement: 'ground', }, { id: 'ground-column', name: 'Ground Column', tiles: ['dungeon-decal-ground-column-bottom', 'dungeon-decal-ground-column-top'], width: 1, frequency: 0.01, minSpacing: 3, placement: 'ground', nearWall: false, }, { id: 'chest-open-tongue', name: 'Chest Open Tongue', tiles: ['dungeon-decal-chest-open'], width: 1, frequency: 0.004, minSpacing: 4, placement: 'ground', }, // Wall decals { id: 'wall_crack1', name: 'Wall Crack 1', tiles: ['dungeon-decal-wall-crack1'], width: 1, frequency: 0.04, minSpacing: 4, placement: 'wall_face', }, { id: 'wall_crack2', name: 'Wall Crack 2', tiles: ['dungeon-decal-wall-crack2'], width: 1, frequency: 0.04, minSpacing: 4, placement: 'wall_face', }, { id: 'wall-face-1', name: 'Wall Face 1', tiles: ['dungeon-decal-wall-face1-bottom', 'dungeon-decal-wall-face1-top'], width: 1, frequency: 0.03, minSpacing: 5, placement: 'wall_to_ground', }, { id: 'wall-face-2', name: 'Wall Face 2', tiles: ['dungeon-decal-wall-face2-bottom', 'dungeon-decal-wall-face2-top'], width: 1, frequency: 0.03, minSpacing: 5, placement: 'wall_to_ground', }, { id: 'wall_detail1', name: 'Wall Detail 1', tiles: ['dungeon-decal-wall-detail1'], width: 1, frequency: 0.02, minSpacing: 6, placement: 'wall_face', }, { id: 'wall_detail2', name: 'Wall Detail 2', tiles: ['dungeon-decal-wall-detail2'], width: 1, frequency: 0.02, minSpacing: 6, placement: 'wall_face', }, { id: 'wall_detail3', name: 'Wall Detail 3', tiles: ['dungeon-decal-wall-detail3'], width: 1, frequency: 0.02, minSpacing: 6, placement: 'wall_face', }, // Pillar (wall_to_ground) { id: 'pillar', name: 'Stone Pillar', tiles: ['dungeon-decal-pillar-base', 'dungeon-decal-pillar-middle', 'dungeon-decal-pillar-crown'], width: 1, frequency: 0.008, minSpacing: 6, placement: 'wall_to_ground', group: 'pillar', }, ]; export function getDecalDefinition(id: string): DecalDefinition | undefined { return DECAL_DEFINITIONS.find((d) => d.id === id); }