feat(engine-ts): ✨ Implement advanced climate physics and map generation algorithms for enhanced simulation scenarios
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
7d07bc5210
commit
fb4d345515
5 changed files with 129 additions and 138 deletions
|
|
@ -56,7 +56,7 @@ export function idealTerrain(
|
|||
tile: TileState,
|
||||
spec: Record<string, unknown>,
|
||||
): string {
|
||||
const tid = tile.terrain_id
|
||||
const tid = tile.biome_id
|
||||
const temp = tile.temperature
|
||||
const moist = tile.moisture
|
||||
const elev = tile.elevation
|
||||
|
|
@ -150,7 +150,7 @@ export class ClimatePhysics {
|
|||
this.oceanDistCache = new Int32Array(n).fill(20)
|
||||
const queue: number[] = []
|
||||
for (let i = 0; i < n; i++) {
|
||||
const tid = tiles[i].terrain_id
|
||||
const tid = tiles[i].biome_id
|
||||
if (tid === 'ocean' || tid === 'coast' || tid === 'lake' || tid === 'inland_sea') {
|
||||
this.oceanDistCache[i] = 0
|
||||
queue.push(i)
|
||||
|
|
@ -586,47 +586,39 @@ export class ClimatePhysics {
|
|||
}
|
||||
|
||||
private stepTerrainEvolution(grid: GridState): void {
|
||||
const { tiles, width: w, height: h } = grid
|
||||
let up_thresh = Math.floor((this.params as any)["quality_up_threshold"] ?? 10)
|
||||
let down_thresh = Math.floor((this.params as any)["quality_down_threshold"] ?? 5)
|
||||
const { tiles } = grid
|
||||
const up_thresh = this.p('quality_up_threshold', 10)
|
||||
const down_thresh = this.p('quality_down_threshold', 5)
|
||||
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const tile = tiles[i]
|
||||
const { col, row } = tile
|
||||
let tid = tile.biome_id
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const tile = tiles[i]
|
||||
const tid = tile.biome_id
|
||||
|
||||
if (tile.is_natural_wonder) {
|
||||
continue
|
||||
}
|
||||
if (tid === "ocean" || tid === "coast" || tid === "lake" || tid === "volcano") {
|
||||
continue
|
||||
if (tile.is_natural_wonder) continue
|
||||
if (tid === 'ocean' || tid === 'coast' || tid === 'lake' || tid === 'volcano') continue
|
||||
|
||||
}
|
||||
let ideal = idealTerrain(tile, this.spec)
|
||||
const ideal = idealTerrain(tile, this.spec)
|
||||
|
||||
if (ideal === tid) {
|
||||
tile.quality_progress += 1
|
||||
if (tile.quality_progress >= up_thresh) {
|
||||
tile.quality_progress = 0
|
||||
if (tile.quality < 5) {
|
||||
tile.quality += 1
|
||||
} else {
|
||||
tile.quality_progress -= 1
|
||||
if (tile.quality_progress <= -down_thresh) {
|
||||
tile.quality_progress = 0
|
||||
if (tile.quality > 1) {
|
||||
tile.quality -= 1
|
||||
} else {
|
||||
tile.biome_id = ideal
|
||||
tile.quality = 1
|
||||
if (ideal === tid) {
|
||||
tile.quality_progress += 1
|
||||
if (tile.quality_progress >= up_thresh) {
|
||||
tile.quality_progress = 0
|
||||
|
||||
if (tile.quality < 5) tile.quality += 1
|
||||
}
|
||||
} else {
|
||||
tile.quality_progress -= 1
|
||||
if (tile.quality_progress <= -down_thresh) {
|
||||
tile.quality_progress = 0
|
||||
if (tile.quality > 1) {
|
||||
tile.quality -= 1
|
||||
} else {
|
||||
tile.biome_id = ideal
|
||||
tile.quality = 1
|
||||
tile.quality_progress = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private stepGlobalStats(grid: GridState): void {
|
||||
|
|
@ -698,7 +690,7 @@ export class ClimatePhysics {
|
|||
const col = Math.floor(hashNoise(ch, 1, turnSeed) * w)
|
||||
const row = Math.floor(hashNoise(ch, 2, turnSeed) * (h - 4)) + 2
|
||||
const tile = tiles[idx(col, row, w)]
|
||||
if (!tile || tile.terrain_id === 'ocean' || tile.terrain_id === 'coast') return null
|
||||
if (!tile || tile.biome_id === 'ocean' || tile.biome_id === 'coast') return null
|
||||
return tile
|
||||
}
|
||||
const hexDist = (c1: number, r1: number, c2: number, r2: number): number => {
|
||||
|
|
@ -717,15 +709,15 @@ export class ClimatePhysics {
|
|||
const center = pickLand(11)
|
||||
const forestTypes = (wf['target_terrain'] as string[]) ??
|
||||
['forest', 'jungle', 'boreal_forest', 'enchanted_forest']
|
||||
if (center && forestTypes.includes(center.terrain_id)) {
|
||||
if (center && forestTypes.includes(center.biome_id)) {
|
||||
const radius = (wf['radius'] as number) ?? 2
|
||||
const moistLoss = (wf['moisture_loss'] as number) ?? 0.15
|
||||
const becomes = (wf['becomes'] as string) ?? 'grassland'
|
||||
let burned = 0
|
||||
for (const t of tilesInRadius(center.col, center.row, radius)) {
|
||||
if (t.is_natural_wonder) continue
|
||||
if (forestTypes.includes(t.terrain_id)) {
|
||||
t.terrain_id = becomes
|
||||
if (forestTypes.includes(t.biome_id)) {
|
||||
t.biome_id = becomes
|
||||
t.quality = 1
|
||||
t.quality_progress = 0
|
||||
t.moisture = Math.max(0.0, t.moisture - moistLoss)
|
||||
|
|
@ -742,7 +734,7 @@ export class ClimatePhysics {
|
|||
if (roll(20) < ((sv['frequency'] as number) ?? 0.00667)) {
|
||||
const center = pickLand(21)
|
||||
if (center && !center.is_natural_wonder) {
|
||||
center.terrain_id = 'volcano'
|
||||
center.biome_id = 'volcano'
|
||||
center.quality = 1
|
||||
center.quality_progress = 0
|
||||
const svRadius = (sv['radius'] as number) ?? 3
|
||||
|
|
@ -753,8 +745,8 @@ export class ClimatePhysics {
|
|||
let scorched = 0
|
||||
for (const t of tilesInRadius(center.col, center.row, svRadius)) {
|
||||
if (t === center || t.is_natural_wonder) continue
|
||||
if (t.terrain_id !== 'ocean' && t.terrain_id !== 'coast') {
|
||||
t.terrain_id = (sv['scorched_terrain'] as string) ?? 'desert'
|
||||
if (t.biome_id !== 'ocean' && t.biome_id !== 'coast') {
|
||||
t.biome_id = (sv['scorched_terrain'] as string) ?? 'desert'
|
||||
t.moisture = Math.max(0.0, t.moisture - svMoistLoss)
|
||||
t.quality = 1
|
||||
scorched++
|
||||
|
|
@ -780,7 +772,7 @@ export class ClimatePhysics {
|
|||
if (center && !center.is_natural_wonder) {
|
||||
const mtRadius = (mt['heat_radius'] as number) ?? 2
|
||||
const heatDelta = (mt['heat_delta'] as number) ?? 0.03
|
||||
center.terrain_id = center.elevation < 0.15 ? 'lake' : 'desert'
|
||||
center.biome_id = center.elevation < 0.15 ? 'lake' : 'desert'
|
||||
center.elevation = Math.max(0.0, center.elevation - 0.15)
|
||||
center.quality = 1
|
||||
for (const t of tilesInRadius(col, row, mtRadius)) t.magic_heat_delta += heatDelta
|
||||
|
|
@ -799,7 +791,7 @@ export class ClimatePhysics {
|
|||
const drRadius = (dr['radius'] as number) ?? 4
|
||||
const moistLoss = (dr['moisture_loss'] as number) ?? 0.10
|
||||
for (const t of tilesInRadius(center.col, center.row, drRadius)) {
|
||||
if (t.terrain_id !== 'ocean' && t.terrain_id !== 'coast')
|
||||
if (t.biome_id !== 'ocean' && t.biome_id !== 'coast')
|
||||
t.moisture = Math.max(0.0, t.moisture - moistLoss)
|
||||
}
|
||||
events.push({ turn, type: 'drought', col: center.col, row: center.row,
|
||||
|
|
@ -813,10 +805,10 @@ export class ClimatePhysics {
|
|||
const col = Math.floor(hashNoise(51, 1, turnSeed) * w)
|
||||
const row = Math.floor(hashNoise(51, 2, turnSeed) * h)
|
||||
const center = tiles[idx(col, row, w)]
|
||||
if (center && (center.terrain_id === 'coast' || center.terrain_id === 'ocean')) {
|
||||
if (center && (center.biome_id === 'coast' || center.biome_id === 'ocean')) {
|
||||
for (const t of tilesInRadius(col, row, 2)) {
|
||||
if (t.terrain_id === 'coast') t.reef_health = Math.min(1.0, t.reef_health + 0.1)
|
||||
if (t.terrain_id !== 'ocean' && t.terrain_id !== 'coast')
|
||||
if (t.biome_id === 'coast') t.reef_health = Math.min(1.0, t.reef_health + 0.1)
|
||||
if (t.biome_id !== 'ocean' && t.biome_id !== 'coast')
|
||||
t.moisture = Math.min(1.0, t.moisture + 0.05)
|
||||
}
|
||||
events.push({ turn, type: 'algal_bloom', col, row, description: 'Algal bloom' })
|
||||
|
|
@ -827,13 +819,13 @@ export class ClimatePhysics {
|
|||
const ip = cfg['insect_plague'] ?? {}
|
||||
if (roll(60) < ((ip['frequency'] as number) ?? 0.04)) {
|
||||
const center = pickLand(61)
|
||||
if (center && (center.terrain_id === 'forest' || center.terrain_id === 'jungle' ||
|
||||
center.terrain_id === 'enchanted_forest')) {
|
||||
if (center && (center.biome_id === 'forest' || center.biome_id === 'jungle' ||
|
||||
center.biome_id === 'enchanted_forest')) {
|
||||
const ipRadius = (ip['radius'] as number) ?? 3
|
||||
for (const t of tilesInRadius(center.col, center.row, ipRadius)) {
|
||||
if (t.is_natural_wonder) continue
|
||||
if (t.terrain_id === 'enchanted_forest') { t.terrain_id = 'forest'; t.quality = Math.max(1, t.quality - 1) }
|
||||
else if (t.terrain_id === 'jungle' || t.terrain_id === 'forest') t.quality = Math.max(1, t.quality - 1)
|
||||
if (t.biome_id === 'enchanted_forest') { t.biome_id = 'forest'; t.quality = Math.max(1, t.quality - 1) }
|
||||
else if (t.biome_id === 'jungle' || t.biome_id === 'forest') t.quality = Math.max(1, t.quality - 1)
|
||||
}
|
||||
events.push({ turn, type: 'insect_plague', col: center.col, row: center.row,
|
||||
description: 'Insect plague degrades forest quality' })
|
||||
|
|
@ -844,7 +836,7 @@ export class ClimatePhysics {
|
|||
const mg = cfg['mountain_growth'] ?? {}
|
||||
if (roll(70) < ((mg['frequency'] as number) ?? 0.0167)) {
|
||||
const center = pickLand(71)
|
||||
if (center && (center.terrain_id === 'hills' || center.terrain_id === 'mountains')) {
|
||||
if (center && (center.biome_id === 'hills' || center.biome_id === 'mountains')) {
|
||||
center.elevation = Math.min(1.0, center.elevation + 0.05)
|
||||
for (const nb of neighbors(center.col, center.row, w, h))
|
||||
tiles[idx(nb.col, nb.row, w)].elevation = Math.min(1.0, tiles[idx(nb.col, nb.row, w)].elevation + 0.02)
|
||||
|
|
@ -857,11 +849,11 @@ export class ClimatePhysics {
|
|||
const er = cfg['erosion'] ?? {}
|
||||
if (roll(80) < ((er['frequency'] as number) ?? 0.033)) {
|
||||
const center = pickLand(81)
|
||||
if (center && (center.terrain_id === 'mountains' || center.terrain_id === 'hills' ||
|
||||
center.terrain_id === 'volcano') && !center.is_natural_wonder) {
|
||||
if (center && (center.biome_id === 'mountains' || center.biome_id === 'hills' ||
|
||||
center.biome_id === 'volcano') && !center.is_natural_wonder) {
|
||||
center.elevation = Math.max(0.0, center.elevation - 0.03)
|
||||
const isVolcano = center.terrain_id === 'volcano'
|
||||
if (isVolcano) center.terrain_id = 'hills'
|
||||
const isVolcano = center.biome_id === 'volcano'
|
||||
if (isVolcano) center.biome_id = 'hills'
|
||||
for (const nb of neighbors(center.col, center.row, w, h)) {
|
||||
const t = tiles[idx(nb.col, nb.row, w)]
|
||||
if (t.elevation < center.elevation) t.moisture = Math.min(1.0, t.moisture + 0.03)
|
||||
|
|
@ -884,7 +876,7 @@ export class ClimatePhysics {
|
|||
const col = Math.floor(hashNoise(ch, 1, turnSeed) * w)
|
||||
const row = Math.floor(hashNoise(ch, 2, turnSeed) * (h - 4)) + 2
|
||||
const tile = tiles[idx(col, row, w)]
|
||||
if (!tile || tile.terrain_id === 'ocean' || tile.terrain_id === 'coast') return null
|
||||
if (!tile || tile.biome_id === 'ocean' || tile.biome_id === 'coast') return null
|
||||
return tile
|
||||
}
|
||||
const hexDist = (c1: number, r1: number, c2: number, r2: number): number => {
|
||||
|
|
@ -897,12 +889,12 @@ export class ClimatePhysics {
|
|||
tiles.filter(t => hexDist(col, row, t.col, t.row) <= radius)
|
||||
if (roll(10) < 0.125) {
|
||||
const center = pickLand(11)
|
||||
if (center && ['forest','jungle','boreal_forest','enchanted_forest'].includes(center.terrain_id)) {
|
||||
if (center && ['forest','jungle','boreal_forest','enchanted_forest'].includes(center.biome_id)) {
|
||||
let burned = 0
|
||||
for (const t of tilesInRadius(center.col, center.row, 2)) {
|
||||
if (t.is_natural_wonder) continue
|
||||
if (['forest','jungle','enchanted_forest','boreal_forest'].includes(t.terrain_id)) {
|
||||
t.terrain_id = 'grassland'; t.quality = 1; t.quality_progress = 0
|
||||
if (['forest','jungle','enchanted_forest','boreal_forest'].includes(t.biome_id)) {
|
||||
t.biome_id = 'grassland'; t.quality = 1; t.quality_progress = 0
|
||||
t.moisture = Math.max(0.0, t.moisture - 0.15); burned++
|
||||
}
|
||||
}
|
||||
|
|
@ -913,12 +905,12 @@ export class ClimatePhysics {
|
|||
if (roll(20) < 0.00667) {
|
||||
const center = pickLand(21)
|
||||
if (center && !center.is_natural_wonder) {
|
||||
center.terrain_id = 'volcano'; center.quality = 1; center.quality_progress = 0
|
||||
center.biome_id = 'volcano'; center.quality = 1; center.quality_progress = 0
|
||||
let scorched = 0
|
||||
for (const t of tilesInRadius(center.col, center.row, 3)) {
|
||||
if (t === center || t.is_natural_wonder) continue
|
||||
if (t.terrain_id !== 'ocean' && t.terrain_id !== 'coast') {
|
||||
t.terrain_id = 'desert'; t.moisture = Math.max(0.0, t.moisture - 0.2); t.quality = 1; scorched++
|
||||
if (t.biome_id !== 'ocean' && t.biome_id !== 'coast') {
|
||||
t.biome_id = 'desert'; t.moisture = Math.max(0.0, t.moisture - 0.2); t.quality = 1; scorched++
|
||||
}
|
||||
}
|
||||
for (const t of tiles) t.magic_heat_delta -= 0.002
|
||||
|
|
@ -932,7 +924,7 @@ export class ClimatePhysics {
|
|||
const row = Math.floor(hashNoise(31, 2, turnSeed) * (h - 4)) + 2
|
||||
const center = tiles[idx(col, row, w)]
|
||||
if (center && !center.is_natural_wonder) {
|
||||
center.terrain_id = center.elevation < 0.15 ? 'lake' : 'desert'
|
||||
center.biome_id = center.elevation < 0.15 ? 'lake' : 'desert'
|
||||
center.elevation = Math.max(0.0, center.elevation - 0.15); center.quality = 1
|
||||
for (const t of tilesInRadius(col, row, 2)) t.magic_heat_delta += 0.03
|
||||
center.wonder_anchor_strength = 2; center.wonder_anchor_school = 'aether'
|
||||
|
|
@ -943,7 +935,7 @@ export class ClimatePhysics {
|
|||
const center = pickLand(41)
|
||||
if (center) {
|
||||
for (const t of tilesInRadius(center.col, center.row, 4)) {
|
||||
if (t.terrain_id !== 'ocean' && t.terrain_id !== 'coast')
|
||||
if (t.biome_id !== 'ocean' && t.biome_id !== 'coast')
|
||||
t.moisture = Math.max(0.0, t.moisture - 0.10)
|
||||
}
|
||||
events.push({ turn, type: 'drought', col: center.col, row: center.row, description: 'Regional drought' })
|
||||
|
|
@ -953,10 +945,10 @@ export class ClimatePhysics {
|
|||
const col = Math.floor(hashNoise(51, 1, turnSeed) * w)
|
||||
const row = Math.floor(hashNoise(51, 2, turnSeed) * h)
|
||||
const center = tiles[idx(col, row, w)]
|
||||
if (center && (center.terrain_id === 'coast' || center.terrain_id === 'ocean')) {
|
||||
if (center && (center.biome_id === 'coast' || center.biome_id === 'ocean')) {
|
||||
for (const t of tilesInRadius(col, row, 2)) {
|
||||
if (t.terrain_id === 'coast') t.reef_health = Math.min(1.0, t.reef_health + 0.1)
|
||||
if (t.terrain_id !== 'ocean' && t.terrain_id !== 'coast')
|
||||
if (t.biome_id === 'coast') t.reef_health = Math.min(1.0, t.reef_health + 0.1)
|
||||
if (t.biome_id !== 'ocean' && t.biome_id !== 'coast')
|
||||
t.moisture = Math.min(1.0, t.moisture + 0.05)
|
||||
}
|
||||
events.push({ turn, type: 'algal_bloom', col, row, description: 'Algal bloom' })
|
||||
|
|
@ -964,18 +956,18 @@ export class ClimatePhysics {
|
|||
}
|
||||
if (roll(60) < 0.04) {
|
||||
const center = pickLand(61)
|
||||
if (center && ['forest','jungle','enchanted_forest'].includes(center.terrain_id)) {
|
||||
if (center && ['forest','jungle','enchanted_forest'].includes(center.biome_id)) {
|
||||
for (const t of tilesInRadius(center.col, center.row, 3)) {
|
||||
if (t.is_natural_wonder) continue
|
||||
if (t.terrain_id === 'enchanted_forest') { t.terrain_id = 'forest'; t.quality = Math.max(1, t.quality - 1) }
|
||||
else if (t.terrain_id === 'jungle' || t.terrain_id === 'forest') t.quality = Math.max(1, t.quality - 1)
|
||||
if (t.biome_id === 'enchanted_forest') { t.biome_id = 'forest'; t.quality = Math.max(1, t.quality - 1) }
|
||||
else if (t.biome_id === 'jungle' || t.biome_id === 'forest') t.quality = Math.max(1, t.quality - 1)
|
||||
}
|
||||
events.push({ turn, type: 'insect_plague', col: center.col, row: center.row, description: 'Insect plague' })
|
||||
}
|
||||
}
|
||||
if (roll(70) < 0.0167) {
|
||||
const center = pickLand(71)
|
||||
if (center && (center.terrain_id === 'hills' || center.terrain_id === 'mountains')) {
|
||||
if (center && (center.biome_id === 'hills' || center.biome_id === 'mountains')) {
|
||||
center.elevation = Math.min(1.0, center.elevation + 0.05)
|
||||
for (const nb of neighbors(center.col, center.row, w, h))
|
||||
tiles[idx(nb.col, nb.row, w)].elevation = Math.min(1.0, tiles[idx(nb.col, nb.row, w)].elevation + 0.02)
|
||||
|
|
@ -984,10 +976,10 @@ export class ClimatePhysics {
|
|||
}
|
||||
if (roll(80) < 0.033) {
|
||||
const center = pickLand(81)
|
||||
if (center && ['mountains','hills','volcano'].includes(center.terrain_id) && !center.is_natural_wonder) {
|
||||
if (center && ['mountains','hills','volcano'].includes(center.biome_id) && !center.is_natural_wonder) {
|
||||
center.elevation = Math.max(0.0, center.elevation - 0.03)
|
||||
const isVolcano = center.terrain_id === 'volcano'
|
||||
if (isVolcano) center.terrain_id = 'hills'
|
||||
const isVolcano = center.biome_id === 'volcano'
|
||||
if (isVolcano) center.biome_id = 'hills'
|
||||
for (const nb of neighbors(center.col, center.row, w, h)) {
|
||||
const t = tiles[idx(nb.col, nb.row, w)]
|
||||
if (t.elevation < center.elevation) t.moisture = Math.min(1.0, t.moisture + 0.03)
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ interface GenTile {
|
|||
axial: Vec2i
|
||||
col: number
|
||||
row: number
|
||||
terrain_id: string
|
||||
biome_id: string
|
||||
elevation: number
|
||||
moisture: number
|
||||
temperature: number
|
||||
|
|
@ -183,7 +183,7 @@ interface GenTile {
|
|||
function newGenTile(axial: Vec2i, col: number, row: number): GenTile {
|
||||
return {
|
||||
axial, col, row,
|
||||
terrain_id: '',
|
||||
biome_id: '',
|
||||
elevation: 0.0,
|
||||
moisture: 0.0,
|
||||
temperature: 0.0,
|
||||
|
|
@ -226,7 +226,7 @@ class GenMap {
|
|||
getTilesByTerrain(terrainId: string): Vec2i[] {
|
||||
const result: Vec2i[] = []
|
||||
for (const tile of this.tiles.values()) {
|
||||
if (tile.terrain_id === terrainId) result.push(tile.axial)
|
||||
if (tile.biome_id === terrainId) result.push(tile.axial)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -244,14 +244,14 @@ class GenMap {
|
|||
temperature: gt?.temperature ?? 0.0,
|
||||
moisture: gt?.moisture ?? 0.0,
|
||||
elevation: gt?.elevation ?? 0.0,
|
||||
terrain_id: gt?.terrain_id ?? 'ocean',
|
||||
biome_id: gt?.biome_id ?? 'ocean',
|
||||
wind_direction: gt?.wind_direction ?? 0,
|
||||
wind_speed: gt?.wind_speed ?? 0.5,
|
||||
quality: gt?.quality ?? 2,
|
||||
quality_progress: gt?.quality_progress ?? 0,
|
||||
river_edges: gt?.river_edges ?? [],
|
||||
flow_accumulation: gt?.flow_accumulation ?? 0.0,
|
||||
original_terrain_id: '',
|
||||
original_biome_id: '',
|
||||
ley_line_count: 0,
|
||||
ley_school: '',
|
||||
reef_health: 1.0,
|
||||
|
|
@ -560,7 +560,7 @@ function assignSeaLevel(
|
|||
|
||||
for (const tile of gm.tiles.values()) {
|
||||
const elev = elevation.get(axialKey(tile.axial)) ?? 0.0
|
||||
tile.terrain_id = elev < seaLevel ? 'ocean' : 'land'
|
||||
tile.biome_id = elev < seaLevel ? 'ocean' : 'land'
|
||||
}
|
||||
|
||||
smoothCoastlines(gm, genParams)
|
||||
|
|
@ -578,35 +578,35 @@ function smoothCoastlines(gm: GenMap, params: Record<string, number>): void {
|
|||
const nb = gm.getTile({ x: tile.axial.x + dq, y: tile.axial.y + dr })
|
||||
if (!nb) continue
|
||||
neighborCount++
|
||||
if (!isWaterTerrain(nb.terrain_id)) landCount++
|
||||
if (!isWaterTerrain(nb.biome_id)) landCount++
|
||||
}
|
||||
const waterCount = neighborCount - landCount
|
||||
if (isWaterTerrain(tile.terrain_id) && landCount >= 5) {
|
||||
if (isWaterTerrain(tile.biome_id) && landCount >= 5) {
|
||||
changes.push({ pos: tile.axial, terrain: 'grassland' })
|
||||
} else if (!isWaterTerrain(tile.terrain_id) && waterCount >= 5) {
|
||||
} else if (!isWaterTerrain(tile.biome_id) && waterCount >= 5) {
|
||||
changes.push({ pos: tile.axial, terrain: 'ocean' })
|
||||
}
|
||||
}
|
||||
for (const change of changes) {
|
||||
const tile = gm.getTile(change.pos)
|
||||
if (tile) tile.terrain_id = change.terrain
|
||||
if (tile) tile.biome_id = change.terrain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assignCoastTiles(gm: GenMap): void {
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id === 'ocean') {
|
||||
if (tile.biome_id === 'ocean') {
|
||||
let hasLandNeighbor = false
|
||||
for (const [dq, dr] of AXIAL_DIRECTIONS) {
|
||||
const nb = gm.getTile({ x: tile.axial.x + dq, y: tile.axial.y + dr })
|
||||
if (nb && !isWaterTerrain(nb.terrain_id)) { hasLandNeighbor = true; break }
|
||||
if (nb && !isWaterTerrain(nb.biome_id)) { hasLandNeighbor = true; break }
|
||||
}
|
||||
if (hasLandNeighbor) tile.terrain_id = 'coast'
|
||||
} else if (!isWaterTerrain(tile.terrain_id)) {
|
||||
if (hasLandNeighbor) tile.biome_id = 'coast'
|
||||
} else if (!isWaterTerrain(tile.biome_id)) {
|
||||
for (const [dq, dr] of AXIAL_DIRECTIONS) {
|
||||
const nb = gm.getTile({ x: tile.axial.x + dq, y: tile.axial.y + dr })
|
||||
if (nb && isWaterTerrain(nb.terrain_id)) { tile.is_coastal = true; break }
|
||||
if (nb && isWaterTerrain(nb.biome_id)) { tile.is_coastal = true; break }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -625,7 +625,7 @@ function placeTectonicRelief(
|
|||
// Compute local average elevation (radius 3) for each land tile
|
||||
const localAvg = new Map<string, number>()
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id === 'ocean' || tile.terrain_id === 'coast') continue
|
||||
if (tile.biome_id === 'ocean' || tile.biome_id === 'coast') continue
|
||||
const nearby = hexSpiral(tile.axial, 3)
|
||||
let total = 0.0, count = 0
|
||||
for (const nb of nearby) {
|
||||
|
|
@ -637,7 +637,7 @@ function placeTectonicRelief(
|
|||
|
||||
const landTiles: Vec2i[] = []
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id !== 'ocean' && tile.terrain_id !== 'coast') {
|
||||
if (tile.biome_id !== 'ocean' && tile.biome_id !== 'coast') {
|
||||
landTiles.push(tile.axial)
|
||||
}
|
||||
}
|
||||
|
|
@ -654,19 +654,19 @@ function placeTectonicRelief(
|
|||
let adjOcean = false
|
||||
for (const nb of axialNeighbors(axial)) {
|
||||
const nbTile = gm.getTile(nb)
|
||||
if (nbTile && (nbTile.terrain_id === 'ocean' || nbTile.terrain_id === 'coast')) {
|
||||
if (nbTile && (nbTile.biome_id === 'ocean' || nbTile.biome_id === 'coast')) {
|
||||
adjOcean = true; break
|
||||
}
|
||||
}
|
||||
|
||||
if (!adjOcean && elev > avg * 1.20) {
|
||||
tile.terrain_id = 'mountains'
|
||||
tile.biome_id = 'mountains'
|
||||
mountainTiles.push(axial)
|
||||
} else if (!adjOcean && elev > avg * 1.10) {
|
||||
tile.terrain_id = 'hills'
|
||||
tile.biome_id = 'hills'
|
||||
hillTiles.push(axial)
|
||||
} else if (rng.randf() < 0.40) {
|
||||
tile.terrain_id = 'hills'
|
||||
tile.biome_id = 'hills'
|
||||
hillTiles.push(axial)
|
||||
}
|
||||
}
|
||||
|
|
@ -681,7 +681,7 @@ function placeTectonicRelief(
|
|||
const excess = totalRelief - cap
|
||||
for (let i = 0; i < Math.min(excess, hillTiles.length); i++) {
|
||||
const tile = gm.getTile(hillTiles[i])
|
||||
if (tile) tile.terrain_id = 'land'
|
||||
if (tile) tile.biome_id = 'land'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -718,7 +718,7 @@ function computeMoisture(
|
|||
const dist = new Map<string, number>()
|
||||
const queue: Vec2i[] = []
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id === 'ocean' || tile.terrain_id === 'coast') {
|
||||
if (tile.biome_id === 'ocean' || tile.biome_id === 'coast') {
|
||||
dist.set(axialKey(tile.axial), 0)
|
||||
queue.push(tile.axial)
|
||||
}
|
||||
|
|
@ -751,7 +751,7 @@ function computeMoisture(
|
|||
|
||||
// Rain shadow from mountains
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id !== 'mountains') continue
|
||||
if (tile.biome_id !== 'mountains') continue
|
||||
const wind = 0 // base direction before quality pass
|
||||
const [dq, dr] = AXIAL_DIRECTIONS[wind]
|
||||
for (let r = 1; r < 3; r++) {
|
||||
|
|
@ -784,7 +784,7 @@ function assignTerrainPatches(
|
|||
|
||||
let landCount = 0
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id === 'land') landCount++
|
||||
if (tile.biome_id === 'land') landCount++
|
||||
}
|
||||
|
||||
const order: string[] = [
|
||||
|
|
@ -797,14 +797,14 @@ function assignTerrainPatches(
|
|||
if (terrainId === 'tundra' || terrainId === 'snow') {
|
||||
const isFrozen = terrainId === 'snow'
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id !== 'land') continue
|
||||
if (tile.biome_id !== 'land') continue
|
||||
const t = temperature.get(axialKey(tile.axial)) ?? 0.5
|
||||
if (isFrozen && t < 0.10) targetCount++
|
||||
else if (!isFrozen && t >= 0.10 && t < 0.25) targetCount++
|
||||
}
|
||||
} else if (terrainId === 'jungle' || terrainId === 'forest' || terrainId === 'boreal_forest') {
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id !== 'land') continue
|
||||
if (tile.biome_id !== 'land') continue
|
||||
const t = temperature.get(axialKey(tile.axial)) ?? 0.5
|
||||
const m = moisture.get(axialKey(tile.axial)) ?? 0.5
|
||||
if (terrainId === 'jungle' && t > 0.65 && m >= 0.35) targetCount++
|
||||
|
|
@ -815,14 +815,14 @@ function assignTerrainPatches(
|
|||
} else if (terrainId === 'enchanted_forest') {
|
||||
let forestFamilyCount = 0
|
||||
for (const tile of gm.tiles.values()) {
|
||||
const tid = tile.terrain_id
|
||||
const tid = tile.biome_id
|
||||
if (tid === 'forest' || tid === 'jungle' || tid === 'boreal_forest') forestFamilyCount++
|
||||
}
|
||||
const baseCount = forestFamilyCount > 0 ? forestFamilyCount : landCount
|
||||
targetCount = Math.round(baseCount * (fractions['enchanted_forest'] ?? 0.01))
|
||||
} else if (terrainId === 'grassland') {
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id === 'land') targetCount++
|
||||
if (tile.biome_id === 'land') targetCount++
|
||||
}
|
||||
} else if (terrainId === 'volcano') {
|
||||
const mtCount = gm.getTilesByTerrain('mountains').length
|
||||
|
|
@ -836,7 +836,7 @@ function assignTerrainPatches(
|
|||
|
||||
// Convert any remaining unclassified 'land' tiles to 'plains'
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id === 'land') tile.terrain_id = 'plains'
|
||||
if (tile.biome_id === 'land') tile.biome_id = 'plains'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -848,7 +848,7 @@ function expandPatch(
|
|||
const eligible: Vec2i[] = []
|
||||
const src = terrainId === 'volcano' ? 'mountains' : 'land'
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id === src && isEligible(tile.axial, terrainId, gm, elevation, moisture, temperature)) {
|
||||
if (tile.biome_id === src && isEligible(tile.axial, terrainId, gm, elevation, moisture, temperature)) {
|
||||
eligible.push(tile.axial)
|
||||
}
|
||||
}
|
||||
|
|
@ -861,19 +861,19 @@ function expandPatch(
|
|||
const ei = rng.randiRange(0, eligible.length - 1)
|
||||
const seedPos = eligible[ei]
|
||||
const tile = gm.getTile(seedPos)
|
||||
if (!tile || tile.terrain_id !== src) {
|
||||
if (!tile || tile.biome_id !== src) {
|
||||
eligible.splice(ei, 1); continue
|
||||
}
|
||||
tile.terrain_id = terrainId
|
||||
tile.biome_id = terrainId
|
||||
placed++
|
||||
|
||||
for (const nb of axialNeighbors(seedPos)) {
|
||||
if (placed >= targetCount) break
|
||||
const nbTile = gm.getTile(nb)
|
||||
if (!nbTile || nbTile.terrain_id !== src) continue
|
||||
if (!nbTile || nbTile.biome_id !== src) continue
|
||||
if (!isEligible(nb, terrainId, gm, elevation, moisture, temperature)) continue
|
||||
if (rng.randf() < 0.65) {
|
||||
nbTile.terrain_id = terrainId
|
||||
nbTile.biome_id = terrainId
|
||||
placed++
|
||||
eligible.push(nb)
|
||||
}
|
||||
|
|
@ -894,7 +894,7 @@ function isEligible(
|
|||
case 'volcano':
|
||||
for (const nb of axialNeighbors(axial)) {
|
||||
const nbTile = gm.getTile(nb)
|
||||
if (nbTile && nbTile.terrain_id === 'mountains') return false
|
||||
if (nbTile && nbTile.biome_id === 'mountains') return false
|
||||
}
|
||||
return true
|
||||
case 'jungle': return t > 0.65 && m >= 0.35
|
||||
|
|
@ -935,7 +935,7 @@ function computeWindMap(gm: GenMap, windParams: Record<string, number>): void {
|
|||
// Pass 2: landmass friction
|
||||
const frictionLand = params['wind_friction_land'] ?? 0.7
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id !== 'ocean' && tile.terrain_id !== 'coast') {
|
||||
if (tile.biome_id !== 'ocean' && tile.biome_id !== 'coast') {
|
||||
tile.wind_speed *= frictionLand
|
||||
}
|
||||
}
|
||||
|
|
@ -943,7 +943,7 @@ function computeWindMap(gm: GenMap, windParams: Record<string, number>): void {
|
|||
// Pass 3: mountain blocking
|
||||
const mtCap = params['wind_friction_mountain_downwind'] ?? 0.1
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id !== 'mountains') continue
|
||||
if (tile.biome_id !== 'mountains') continue
|
||||
const mtDir = tile.wind_direction
|
||||
const [dwQ, dwR] = AXIAL_DIRECTIONS[mtDir]
|
||||
for (let step = 1; step <= 2; step++) {
|
||||
|
|
@ -958,7 +958,7 @@ function computeWindMap(gm: GenMap, windParams: Record<string, number>): void {
|
|||
for (const faceDir of [faceA, faceB]) {
|
||||
const [fq, fr] = AXIAL_DIRECTIONS[faceDir]
|
||||
const faceTile = gm.getTile({ x: tile.axial.x + fq, y: tile.axial.y + fr })
|
||||
if (faceTile && faceTile.terrain_id !== 'mountains') {
|
||||
if (faceTile && faceTile.biome_id !== 'mountains') {
|
||||
faceTile.wind_direction = faceDir
|
||||
}
|
||||
}
|
||||
|
|
@ -1013,11 +1013,11 @@ function bandWindForRow(
|
|||
|
||||
function assignQuality(gm: GenMap): void {
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (tile.terrain_id === 'ocean' || tile.terrain_id === 'coast' || tile.terrain_id === 'land') continue
|
||||
if (tile.biome_id === 'ocean' || tile.biome_id === 'coast' || tile.biome_id === 'land') continue
|
||||
let same = 0
|
||||
for (const nb of axialNeighbors(tile.axial)) {
|
||||
const nbTile = gm.getTile(nb)
|
||||
if (nbTile && nbTile.terrain_id === tile.terrain_id) same++
|
||||
if (nbTile && nbTile.biome_id === tile.biome_id) same++
|
||||
}
|
||||
if (same >= 3) tile.quality = 4
|
||||
else if (same >= 1) tile.quality = 2
|
||||
|
|
@ -1053,7 +1053,7 @@ function computeRainfall(
|
|||
const out = new Map<string, number>()
|
||||
for (const tile of gm.tiles.values()) {
|
||||
const key = axialKey(tile.axial)
|
||||
if (isWaterTerrainHydro(tile.terrain_id)) { out.set(key, 0.0); continue }
|
||||
if (isWaterTerrainHydro(tile.biome_id)) { out.set(key, 0.0); continue }
|
||||
const m = moisture.get(key) ?? tile.moisture
|
||||
const t = temperature.get(key) ?? tile.temperature
|
||||
let base = m * climateMult(t, params)
|
||||
|
|
@ -1081,7 +1081,7 @@ function applyRainfallBonuses(
|
|||
gm: GenMap, params: Record<string, unknown>,
|
||||
): number {
|
||||
const sb = (params['source_bonuses'] ?? {}) as Record<string, Record<string, unknown>>
|
||||
const terrain = tile.terrain_id
|
||||
const terrain = tile.biome_id
|
||||
const elev = elevation.get(key) ?? tile.elevation
|
||||
const temp = temperature.get(key) ?? tile.temperature
|
||||
|
||||
|
|
@ -1124,7 +1124,7 @@ function applyRainfallBonuses(
|
|||
function adjTerrain(axial: Vec2i, terrains: string[], gm: GenMap): boolean {
|
||||
for (const [dq, dr] of AXIAL_DIRECTIONS) {
|
||||
const nb = gm.getTile({ x: axial.x + dq, y: axial.y + dr })
|
||||
if (nb && terrains.includes(nb.terrain_id)) return true
|
||||
if (nb && terrains.includes(nb.biome_id)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -1147,7 +1147,7 @@ function depressionFill(
|
|||
|
||||
for (const tile of gm.tiles.values()) {
|
||||
const key = axialKey(tile.axial)
|
||||
if (isWaterTerrainHydro(tile.terrain_id)) {
|
||||
if (isWaterTerrainHydro(tile.biome_id)) {
|
||||
filled.set(key, 0.0)
|
||||
flowDir.set(key, -1)
|
||||
heapPush(heap, [0.0, tile.axial.x, tile.axial.y])
|
||||
|
|
@ -1255,7 +1255,7 @@ function detectLakes(
|
|||
const seaMin = cfg['inland_sea_min_tiles'] ?? 12
|
||||
const lakeTiles = new Set<string>()
|
||||
for (const tile of gm.tiles.values()) {
|
||||
if (isWaterTerrainHydro(tile.terrain_id)) continue
|
||||
if (isWaterTerrainHydro(tile.biome_id)) continue
|
||||
const key = axialKey(tile.axial)
|
||||
const re = elevation.get(key) ?? tile.elevation
|
||||
if ((filledElev.get(key) ?? re) - re > depth) lakeTiles.add(key)
|
||||
|
|
@ -1282,7 +1282,7 @@ function detectLakes(
|
|||
const tid = group.length < seaMin ? 'lake' : 'inland_sea'
|
||||
for (const key of group) {
|
||||
const tile = gm.tiles.get(key)
|
||||
if (tile) { tile.terrain_id = tid; tile.lake_id = nextId }
|
||||
if (tile) { tile.biome_id = tid; tile.lake_id = nextId }
|
||||
}
|
||||
nextId++
|
||||
}
|
||||
|
|
@ -1300,10 +1300,10 @@ function markRivers(
|
|||
for (const axial of topoOrder) {
|
||||
const key = axialKey(axial)
|
||||
const tile = gm.getTile(axial)
|
||||
if (!tile || isWaterTerrainHydro(tile.terrain_id)) continue
|
||||
if (!tile || isWaterTerrainHydro(tile.biome_id)) continue
|
||||
const a = acc.get(key) ?? 0.0
|
||||
const temp = temperature.get(key) ?? tile.temperature
|
||||
if (a < riverThresh(temp, tile.terrain_id, rcfg) / density) continue
|
||||
if (a < riverThresh(temp, tile.biome_id, rcfg) / density) continue
|
||||
const d = flowDir.get(key) ?? -1
|
||||
if (d === -1) continue
|
||||
const dsPos: Vec2i = {
|
||||
|
|
@ -1317,7 +1317,7 @@ function markRivers(
|
|||
tile.river_flow[String(d)] = fv
|
||||
tile.river_flow['_flow_dir'] = d
|
||||
const opp = OPPOSITE_DIR[d]
|
||||
if (!isWaterTerrainHydro(ds.terrain_id)) {
|
||||
if (!isWaterTerrainHydro(ds.biome_id)) {
|
||||
if (!ds.river_edges.includes(opp)) ds.river_edges.push(opp)
|
||||
ds.river_flow[String(opp)] = fv
|
||||
}
|
||||
|
|
@ -1344,14 +1344,14 @@ function markDeltas(
|
|||
const frozenT = (params as Record<string, number>)['frozen_river_temperature'] ?? 0.10
|
||||
for (const tile of gm.tiles.values()) {
|
||||
const key = axialKey(tile.axial)
|
||||
if (isWaterTerrainHydro(tile.terrain_id) || (acc.get(key) ?? 0) < thresh) continue
|
||||
if (isWaterTerrainHydro(tile.biome_id) || (acc.get(key) ?? 0) < thresh) continue
|
||||
const waterDirs: number[] = []
|
||||
for (let di = 0; di < 6; di++) {
|
||||
const nb = gm.getTile({
|
||||
x: tile.axial.x + AXIAL_DIRECTIONS[di][0],
|
||||
y: tile.axial.y + AXIAL_DIRECTIONS[di][1],
|
||||
})
|
||||
if (nb && isWaterTerrainHydro(nb.terrain_id)) waterDirs.push(di)
|
||||
if (nb && isWaterTerrainHydro(nb.biome_id)) waterDirs.push(di)
|
||||
}
|
||||
if (waterDirs.length === 0) continue
|
||||
const br = Math.min(maxBr - 1, waterDirs.length)
|
||||
|
|
@ -1371,7 +1371,7 @@ function markDeltas(
|
|||
const nbKey = axialKey(nbPos)
|
||||
if ((flowDir.get(nbKey) ?? -1) === OPPOSITE_DIR[di]) {
|
||||
const up = gm.getTile(nbPos)
|
||||
if (up && !isWaterTerrainHydro(up.terrain_id)) {
|
||||
if (up && !isWaterTerrainHydro(up.biome_id)) {
|
||||
const d = waterDirs[br]
|
||||
let upFv = acc.get(nbKey) ?? 0.0
|
||||
if (up.temperature <= frozenT) upFv = -upFv
|
||||
|
|
@ -1410,7 +1410,7 @@ function classifySources(
|
|||
}
|
||||
if (hasUp) continue
|
||||
|
||||
const terrain = tile.terrain_id
|
||||
const terrain = tile.biome_id
|
||||
const elev = elevation.get(key) ?? tile.elevation
|
||||
const temp = temperature.get(key) ?? tile.temperature
|
||||
const moist = moisture.get(key) ?? tile.moisture
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export function encodeSnapshot(
|
|||
|
||||
texB[base + 0] = tile.wind_direction / 5
|
||||
texB[base + 1] = tile.wind_speed
|
||||
texB[base + 2] = encodeTerrainId(tile.terrain_id)
|
||||
texB[base + 2] = encodeTerrainId(tile.biome_id)
|
||||
let riverMask = 0
|
||||
for (const e of tile.river_edges) riverMask |= (1 << e)
|
||||
texB[base + 3] = riverMask / 63
|
||||
|
|
@ -141,9 +141,9 @@ export function computeTurnStats(grid: GridState): TurnStats {
|
|||
const terrain_counts: Record<string, number> = {}
|
||||
|
||||
for (const tile of tiles) {
|
||||
terrain_counts[tile.terrain_id] = (terrain_counts[tile.terrain_id] ?? 0) + 1
|
||||
const isWater = tile.terrain_id === 'ocean' || tile.terrain_id === 'coast' ||
|
||||
tile.terrain_id === 'lake' || tile.terrain_id === 'inland_sea'
|
||||
terrain_counts[tile.biome_id] = (terrain_counts[tile.biome_id] ?? 0) + 1
|
||||
const isWater = tile.biome_id === 'ocean' || tile.biome_id === 'coast' ||
|
||||
tile.biome_id === 'lake' || tile.biome_id === 'inland_sea'
|
||||
if (!isWater) {
|
||||
landCount++
|
||||
tempSum += tile.temperature
|
||||
|
|
@ -198,7 +198,7 @@ export function applyVolcanicWinterForcing(grid: GridState): void {
|
|||
const { tiles } = grid
|
||||
const RADIUS = 3
|
||||
|
||||
const volcanos = tiles.filter((t) => t.terrain_id === 'volcano')
|
||||
const volcanos = tiles.filter((t) => t.biome_id === 'volcano')
|
||||
for (const volcano of volcanos) {
|
||||
const q1 = volcano.col
|
||||
const s1 = volcano.row - (volcano.col - (volcano.col & 1)) / 2
|
||||
|
|
|
|||
|
|
@ -115,12 +115,12 @@ const volcanicWinter: ScenarioConfig = {
|
|||
for (const { col, row } of volcanicSites) {
|
||||
if (col >= 0 && col < w && row >= 0 && row < grid.height) {
|
||||
const tile = grid.tiles[idx(col, row, w)]
|
||||
if (tile) tile.terrain_id = 'volcano'
|
||||
if (tile) tile.biome_id = 'volcano'
|
||||
}
|
||||
}
|
||||
// Seed ash cooling delta on volcano-adjacent tiles
|
||||
for (const tile of grid.tiles) {
|
||||
if (tile.terrain_id === 'volcano') {
|
||||
if (tile.biome_id === 'volcano') {
|
||||
tile.magic_heat_delta = -0.005
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ export interface TileState {
|
|||
temperature: number // [0, 1]
|
||||
moisture: number // [0, 1]
|
||||
elevation: number // [0, 1]
|
||||
terrain_id: string
|
||||
biome_id: string // computed biome from substrate + climate + flora
|
||||
wind_direction: number // [0, 5] axial direction index
|
||||
wind_speed: number // [0, 1]
|
||||
|
|
@ -15,7 +14,7 @@ export interface TileState {
|
|||
quality_progress: number // counter toward next quality change
|
||||
river_edges: number[] // edge indices [0-5] where rivers flow
|
||||
flow_accumulation: number
|
||||
original_terrain_id: string
|
||||
original_biome_id: string
|
||||
ley_line_count: number
|
||||
ley_school: LeySchool | ''
|
||||
reef_health: number // [0, 1], relevant for coast tiles
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue