Created
January 27, 2025 06:19
-
-
Save satyambnsal/ead5fa5134bf953162c7195d83980a41 to your computer and use it in GitHub Desktop.
tileville-procedural-generation-map
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { HexGrid } from "./HexGrid"; | |
| import { Hex } from "./Hex"; | |
| import { Matrix2D } from "../util"; | |
| interface MapConfig { | |
| size: number; | |
| theme: MapTheme; | |
| difficulty: number; | |
| features: MapFeatures; | |
| } | |
| interface MapFeatures { | |
| hillDensity: number; | |
| waterBodies: boolean; | |
| specialStructures: boolean; | |
| } | |
| enum MapTheme { | |
| GRASSLAND = "grassland", | |
| VOLCANIC = "volcanic", | |
| COASTAL = "coastal", | |
| MOUNTAIN = "mountain" | |
| } | |
| export class ProceduralMapGenerator { | |
| private grid: Matrix2D<Hex>; | |
| private size: number; | |
| private scene: Phaser.Scene; | |
| private config: MapConfig; | |
| private noiseGenerator: Phaser.Math.RandomDataGenerator; | |
| constructor(scene: Phaser.Scene, config: MapConfig) { | |
| this.scene = scene; | |
| this.config = config; | |
| this.size = config.size; | |
| this.grid = new Matrix2D<Hex>(); | |
| this.noiseGenerator = new Phaser.Math.RandomDataGenerator([Date.now()]); | |
| } | |
| generate(): HexGrid { | |
| // Create base hexGrid | |
| const hexGrid = new HexGrid( | |
| this.scene, | |
| this.size, | |
| 0, // We'll place hills manually | |
| 0, | |
| 0 | |
| ); | |
| // Generate terrain heightmap | |
| const heightMap = this.generateHeightMap(); | |
| // Apply theme-specific generation | |
| switch(this.config.theme) { | |
| case MapTheme.VOLCANIC: | |
| this.generateVolcanicTerrain(hexGrid, heightMap); | |
| break; | |
| case MapTheme.COASTAL: | |
| this.generateCoastalTerrain(hexGrid, heightMap); | |
| break; | |
| case MapTheme.MOUNTAIN: | |
| this.generateMountainTerrain(hexGrid, heightMap); | |
| break; | |
| default: | |
| this.generateGrasslandTerrain(hexGrid, heightMap); | |
| } | |
| // Place special features based on config | |
| if (this.config.features.specialStructures) { | |
| this.placeSpecialStructures(hexGrid); | |
| } | |
| // Place hills based on heightmap and density | |
| this.placeHills(hexGrid, heightMap); | |
| // Ensure map is playable | |
| this.validateAndFixMap(hexGrid); | |
| return hexGrid; | |
| } | |
| private generateHeightMap(): number[][] { | |
| const heightMap: number[][] = []; | |
| const scale = 0.1; // Adjust for different terrain smoothness | |
| for (let r = 0; r < this.size * 2 + 1; r++) { | |
| heightMap[r] = []; | |
| for (let c = 0; c < this.size * 2 + 1; c++) { | |
| // Use multiple octaves of noise for more natural terrain | |
| const value = this.noiseGenerator.frac() * | |
| this.noiseGenerator.frac() * | |
| Math.sin(r * scale) * | |
| Math.cos(c * scale); | |
| heightMap[r][c] = value; | |
| } | |
| } | |
| return heightMap; | |
| } | |
| private generateVolcanicTerrain(hexGrid: HexGrid, heightMap: number[][]) { | |
| // Place volcanic features | |
| const volcanoCenters: {r: number, c: number}[] = []; | |
| const volcanoCount = Math.floor(this.size / 3); | |
| for (let i = 0; i < volcanoCount; i++) { | |
| const r = Math.floor(this.noiseGenerator.between(1, this.size * 2 - 1)); | |
| const c = Math.floor(this.noiseGenerator.between(1, this.size * 2 - 1)); | |
| volcanoCenters.push({r, c}); | |
| } | |
| // Create volcanic terrain patterns | |
| for (let r = 0; r < this.size * 2 + 1; r++) { | |
| for (let c = 0; c < this.size * 2 + 1; c++) { | |
| const hex = hexGrid.grid.get(r, c); | |
| if (!hex || hex.hexType !== 0) continue; | |
| // Calculate distance to nearest volcano | |
| const distToVolcano = Math.min(...volcanoCenters.map(vc => | |
| Math.sqrt(Math.pow(r - vc.r, 2) + Math.pow(c - vc.c, 2)) | |
| )); | |
| if (distToVolcano < 2) { | |
| hex.setType(7); // Volcano | |
| } else if (distToVolcano < 3 && heightMap[r][c] > 0.6) { | |
| hex.setHill(true); | |
| } | |
| } | |
| } | |
| } | |
| private generateCoastalTerrain(hexGrid: HexGrid, heightMap: number[][]) { | |
| // Generate coastline using heightmap | |
| for (let r = 0; r < this.size * 2 + 1; r++) { | |
| for (let c = 0; c < this.size * 2 + 1; c++) { | |
| const hex = hexGrid.grid.get(r, c); | |
| if (!hex || hex.hexType !== 0) continue; | |
| if (heightMap[r][c] < 0.3) { | |
| // Water tiles or ports | |
| if (this.hasLandNeighbor(hexGrid, r, c)) { | |
| hex.setType(5); // Port | |
| } | |
| } else if (heightMap[r][c] > 0.7) { | |
| hex.setHill(true); | |
| } | |
| } | |
| } | |
| } | |
| private generateMountainTerrain(hexGrid: HexGrid, heightMap: number[][]) { | |
| // Create mountain ranges | |
| for (let r = 0; r < this.size * 2 + 1; r++) { | |
| for (let c = 0; c < this.size * 2 + 1; c++) { | |
| const hex = hexGrid.grid.get(r, c); | |
| if (!hex || hex.hexType !== 0) continue; | |
| if (heightMap[r][c] > 0.6) { | |
| hex.setHill(true); | |
| } | |
| } | |
| } | |
| } | |
| private generateGrasslandTerrain(hexGrid: HexGrid, heightMap: number[][]) { | |
| // Create patches of different vegetation | |
| for (let r = 0; r < this.size * 2 + 1; r++) { | |
| for (let c = 0; c < this.size * 2 + 1; c++) { | |
| const hex = hexGrid.grid.get(r, c); | |
| if (!hex || hex.hexType !== 0) continue; | |
| if (heightMap[r][c] > 0.7) { | |
| hex.setHill(true); | |
| } else if (heightMap[r][c] < 0.3) { | |
| hex.setType(2); // Grass | |
| } | |
| } | |
| } | |
| } | |
| private placeHills(hexGrid: HexGrid, heightMap: number[][]) { | |
| const hillCount = Math.floor(this.size * this.size * this.config.features.hillDensity); | |
| let placed = 0; | |
| for (let r = 0; r < this.size * 2 + 1 && placed < hillCount; r++) { | |
| for (let c = 0; c < this.size * 2 + 1 && placed < hillCount; c++) { | |
| const hex = hexGrid.grid.get(r, c); | |
| if (!hex || hex.hexType !== 0 || hex.hasHill) continue; | |
| if (heightMap[r][c] > 0.7 && !this.hasAdjacentHill(hexGrid, r, c)) { | |
| hex.setHill(true); | |
| placed++; | |
| } | |
| } | |
| } | |
| } | |
| private placeSpecialStructures(hexGrid: HexGrid) { | |
| // Place various special structures based on theme | |
| const structureCount = Math.floor(this.size / 2); | |
| let placed = 0; | |
| while (placed < structureCount) { | |
| const r = Math.floor(this.noiseGenerator.between(1, this.size * 2 - 1)); | |
| const c = Math.floor(this.noiseGenerator.between(1, this.size * 2 - 1)); | |
| const hex = hexGrid.grid.get(r, c); | |
| if (hex && hex.hexType === 0 && !hex.hasHill) { | |
| if (this.config.theme === MapTheme.VOLCANIC) { | |
| hex.setType(7); // Volcano | |
| } else if (this.config.theme === MapTheme.COASTAL) { | |
| hex.setType(5); // Port | |
| } | |
| placed++; | |
| } | |
| } | |
| } | |
| private hasAdjacentHill(hexGrid: HexGrid, row: number, col: number): boolean { | |
| const neighbors = hexGrid.neighbors(row, col); | |
| return neighbors.some(n => n?.hasHill); | |
| } | |
| private hasLandNeighbor(hexGrid: HexGrid, row: number, col: number): boolean { | |
| const neighbors = hexGrid.neighbors(row, col); | |
| return neighbors.some(n => n && n.hexType === 0); | |
| } | |
| private validateAndFixMap(hexGrid: HexGrid) { | |
| // Ensure there's always a valid path to ports/center | |
| // Add necessary paths if missing | |
| const center = hexGrid.grid.get(this.size, this.size); | |
| if (!center) return; | |
| // Check each port's connectivity | |
| for (const hex of hexGrid.hexes) { | |
| if (hex.hexType === 5) { | |
| if (!this.hasPathToCenter(hexGrid, hex, center)) { | |
| this.createPathToCenter(hexGrid, hex, center); | |
| } | |
| } | |
| } | |
| } | |
| private hasPathToCenter(hexGrid: HexGrid, start: Hex, end: Hex): boolean { | |
| const visited = new Set<Hex>(); | |
| const queue: Hex[] = [start]; | |
| while (queue.length > 0) { | |
| const current = queue.shift()!; | |
| if (current === end) return true; | |
| visited.add(current); | |
| const neighbors = hexGrid.neighbors(current.row, current.col); | |
| for (const neighbor of neighbors) { | |
| if (neighbor && !visited.has(neighbor) && | |
| (neighbor.hexType === 0 || neighbor.hexType === 3)) { | |
| queue.push(neighbor); | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| private createPathToCenter(hexGrid: HexGrid, start: Hex, end: Hex) { | |
| // Create a path of road tiles to connect isolated ports | |
| const path = this.findPath(hexGrid, start, end); | |
| if (path) { | |
| for (const hex of path) { | |
| if (hex.hexType === 0) { | |
| hex.setType(3); // Road | |
| } | |
| } | |
| } | |
| } | |
| private findPath(hexGrid: HexGrid, start: Hex, end: Hex): Hex[] | null { | |
| const queue: {hex: Hex, path: Hex[]}[] = [{hex: start, path: [start]}]; | |
| const visited = new Set<Hex>(); | |
| while (queue.length > 0) { | |
| const {hex, path} = queue.shift()!; | |
| if (hex === end) return path; | |
| visited.add(hex); | |
| const neighbors = hexGrid.neighbors(hex.row, hex.col); | |
| for (const neighbor of neighbors) { | |
| if (neighbor && !visited.has(neighbor) && neighbor.hexType === 0) { | |
| queue.push({ | |
| hex: neighbor, | |
| path: [...path, neighbor] | |
| }); | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment