feat(world): ✨ Introduce procedural biome generation, species traits, and dynamic narrative flavor text for world simulation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
2e1e38a133
commit
da7a90f646
3 changed files with 117 additions and 18 deletions
|
|
@ -1,9 +1,8 @@
|
|||
class_name BiomeClassifier
|
||||
extends RefCounted
|
||||
## Classifies tiles into biome_id from substrate + climate + flora state.
|
||||
## Implements ~5 proof biomes for M2a, remainder return "unclassified".
|
||||
|
||||
const WaterBodyScript = preload("res://engine/src/models/world/water_body.gd")
|
||||
## Covers all 26 biomes: 8 aquatic, 4 tropical, 5 temperate, 3 cold,
|
||||
## 5 elevation, 1 subterranean. No fallback/unclassified returns.
|
||||
|
||||
|
||||
## Main classification entry point. tile is Variant (Tile RefCounted).
|
||||
|
|
@ -12,10 +11,13 @@ static func classify(tile: Variant) -> String:
|
|||
return _classify_aquatic(tile)
|
||||
if _get_substrate(tile) == "volcanic":
|
||||
return "volcanic"
|
||||
# Subterranean check before land — cave tiles get cave biome regardless of surface climate
|
||||
if _has_cave(tile):
|
||||
return "subterranean"
|
||||
return _classify_land(tile)
|
||||
|
||||
|
||||
## Aquatic classification: water body type + temperature + depth.
|
||||
## Aquatic classification: water body type + temperature + depth + adjacency flags.
|
||||
static func _classify_aquatic(tile: Variant) -> String:
|
||||
var wb_type: String = _get_water_body_type(tile)
|
||||
|
||||
|
|
@ -30,18 +32,27 @@ static func _classify_aquatic(tile: Variant) -> String:
|
|||
var depth: int = _get_depth(tile)
|
||||
var temp: float = _get_temperature(tile)
|
||||
|
||||
# Estuary: river mouth meeting ocean — pre-computed flag on tile
|
||||
if _is_river_mouth(tile) and depth <= 1:
|
||||
return "estuary"
|
||||
|
||||
# Mangrove: tropical + coastal wetland substrate — check substrate override
|
||||
# Mangrove tiles have wetland substrate adjacent to ocean with warm temp
|
||||
if _get_substrate(tile) == "wetland" and temp > 0.55:
|
||||
return "mangrove"
|
||||
|
||||
# Coral reef: tropical shallow ocean
|
||||
if temp > 0.55 and depth <= 2:
|
||||
return "coral_reef"
|
||||
|
||||
# Shallow vs deep ocean by depth from coast
|
||||
if depth <= 3:
|
||||
return "shallow_ocean"
|
||||
if depth > 3:
|
||||
return "deep_ocean"
|
||||
|
||||
return "shallow_ocean"
|
||||
return "deep_ocean"
|
||||
|
||||
|
||||
## Land classification: elevation x temperature x moisture x canopy.
|
||||
## M2a proof biomes: temperate_forest, temperate_grassland + basic elevation/tropical.
|
||||
## Evaluated in priority order: wetland override → elevation → tropical → temperate → cold.
|
||||
static func _classify_land(tile: Variant) -> String:
|
||||
var temp: float = _get_temperature(tile)
|
||||
var moisture: float = _get_moisture(tile)
|
||||
|
|
@ -82,7 +93,7 @@ static func _classify_land(tile: Variant) -> String:
|
|||
return "savanna"
|
||||
return "desert"
|
||||
|
||||
# Temperate proof biomes
|
||||
# Temperate
|
||||
if temp > 0.25:
|
||||
if canopy > 0.5:
|
||||
return "temperate_forest"
|
||||
|
|
@ -90,7 +101,7 @@ static func _classify_land(tile: Variant) -> String:
|
|||
return "temperate_grassland"
|
||||
return "chaparral"
|
||||
|
||||
# Cold biomes
|
||||
# Cold
|
||||
if temp > 0.1:
|
||||
if canopy > 0.3:
|
||||
return "boreal_forest"
|
||||
|
|
@ -152,3 +163,15 @@ static func _get_canopy(tile: Variant) -> float:
|
|||
if "canopy_cover" in tile:
|
||||
return tile.canopy_cover
|
||||
return 0.0
|
||||
|
||||
|
||||
static func _is_river_mouth(tile: Variant) -> bool:
|
||||
if "is_river_mouth" in tile:
|
||||
return tile.is_river_mouth
|
||||
return false
|
||||
|
||||
|
||||
static func _has_cave(tile: Variant) -> bool:
|
||||
if "has_cave" in tile:
|
||||
return tile.has_cave
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -27,6 +27,13 @@ const BIOME_PREFIXES: Dictionary = {
|
|||
"chaparral": ["Brush", "Dry", "Thorn", "Smoke", "Amber"],
|
||||
"coral_reef": ["Reef", "Pearl", "Bright", "Shell", "Surge"],
|
||||
"volcanic": ["Obsidian", "Cinder", "Ash", "Molten", "Slag"],
|
||||
"estuary": ["Brackish", "Marsh", "Delta", "Silt", "Murk"],
|
||||
"pond": ["Stagnant", "Puddle", "Scum", "Shallow", "Murk"],
|
||||
"river": ["Current", "Rapid", "Eddy", "Stone", "Rush"],
|
||||
"mangrove": ["Root", "Tangle", "Brackish", "Stilted", "Brine"],
|
||||
"cloud_forest": ["Mist", "Veil", "Drip", "Shroud", "Dew"],
|
||||
"permanent_ice": ["Glacier", "Null", "White", "Eternal", "Rime"],
|
||||
"subterranean": ["Pale", "Crystal", "Cave", "Blind", "Hollow"],
|
||||
}
|
||||
|
||||
# locomotion+diet+size → archetype name options
|
||||
|
|
@ -79,6 +86,24 @@ const BIOME_MOTIFS: Dictionary = {
|
|||
],
|
||||
"tundra": ["thick white pelt", "frost crystals", "wind-scarred hide", "pale eyes"],
|
||||
"swamp": ["rot-stained hide", "fungal growths", "bile-green tones", "murk camouflage"],
|
||||
"bog": ["peat-dark hide", "mist-damp fur", "sour-water stains", "lichen patches"],
|
||||
"tropical_rainforest": ["rot-stained markings", "vine-wrapped limbs", "iridescent plumage", "fungal patches"],
|
||||
"tropical_dry_forest": ["cracked bark hide", "ochre dust coating", "thorn protrusions", "dry leaf camouflage"],
|
||||
"savanna": ["dust-caked hide", "sun-bleached mane", "ember-gold eyes", "heat-cracked plates"],
|
||||
"montane_forest": ["stone-grey hide", "ridge-scarred flanks", "cloud-damp fur", "iron-hard hooves"],
|
||||
"chaparral": ["smoke-stained fur", "thorn scratches", "dry brush camouflage", "amber resin patches"],
|
||||
"alpine_meadow": ["wind-torn mane", "rime-crusted horns", "crest markings", "cold-hardened hide"],
|
||||
"alpine_tundra": ["frost-sharded scales", "gale-worn hide", "bare rock camouflage", "scree-colored plates"],
|
||||
"coral_reef": ["iridescent scales", "anemone frills", "bright warning colors", "shell fragments"],
|
||||
"estuary": ["mud-streaked hide", "brackish slime coat", "reed camouflage", "silted gills"],
|
||||
"pond": ["algae-coated skin", "stagnant water film", "dull coloration", "silt deposits"],
|
||||
"river": ["streamlined body", "smooth pebble markings", "current-adapted fins", "stone-grey tones"],
|
||||
"mangrove": ["root-bark texture", "salt-stained hide", "tangled growths", "brackish sheen"],
|
||||
"cloud_forest": ["mist-beaded fur", "dripping moss mantle", "veil-like frills", "dew-bright eyes"],
|
||||
"permanent_ice": ["glacier-blue veins", "frost armor plates", "snow-crystal growths", "rime-locked joints"],
|
||||
"polar_desert": ["ice-crusted hide", "translucent frost plates", "snow-blind pale eyes", "void-white fur"],
|
||||
"subterranean": ["translucent skin", "crystalline growths", "eyeless face", "pheromone sacs"],
|
||||
"volcanic": ["obsidian plates", "ember-glow cracks", "smoke wisps", "ash-coated hide"],
|
||||
}
|
||||
|
||||
# Trait → visual motif overlays
|
||||
|
|
@ -120,6 +145,17 @@ const LANDMARK_ARCHETYPES: Dictionary = {
|
|||
"desert": ["Sands", "Wastes", "Dunes"],
|
||||
"tundra": ["Reach", "Expanse", "Waste"],
|
||||
"lake": ["Depths", "Basin", "Mirror"],
|
||||
"estuary": ["Delta", "Confluence", "Mouth"],
|
||||
"pond": ["Pool", "Puddle", "Basin"],
|
||||
"river": ["Falls", "Bend", "Narrows"],
|
||||
"mangrove": ["Tangle", "Roots", "Maze"],
|
||||
"tropical_dry_forest": ["Thicket", "Grove", "Brake"],
|
||||
"chaparral": ["Scrub", "Brake", "Thicket"],
|
||||
"cloud_forest": ["Veil", "Mist", "Canopy"],
|
||||
"permanent_ice": ["Glacier", "Sheet", "Void"],
|
||||
"polar_desert": ["Waste", "Expanse", "Null"],
|
||||
"volcanic": ["Caldera", "Vent", "Flow"],
|
||||
"subterranean": ["Cavern", "Grotto", "Abyss"],
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,15 +24,20 @@ const TARGET_SPECIES_MIN: int = 8
|
|||
const TARGET_SPECIES_MAX: int = 12
|
||||
const MAX_GENERATION_ATTEMPTS: int = 100
|
||||
|
||||
## Amphibious weight boost multiplier when tile is adjacent to water.
|
||||
const AMPHIBIOUS_ADJACENCY_BOOST: float = 2.5
|
||||
|
||||
|
||||
## Generate species appropriate for a biome and tile quality.
|
||||
## biome_trait_weights: Dictionary from biome_trait_weights.json for this biome.
|
||||
## adjacent_to_water: true if this tile neighbors a water tile (boosts amphibious).
|
||||
## Returns array of valid, quality-filtered TraitSets.
|
||||
static func generate(
|
||||
_biome_id: String,
|
||||
tile_quality: int,
|
||||
seed: int,
|
||||
biome_trait_weights: Dictionary = {},
|
||||
adjacent_to_water: bool = false,
|
||||
) -> Array:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = seed
|
||||
|
|
@ -40,6 +45,11 @@ static func generate(
|
|||
var quality_clamped: int = clampi(tile_quality, 1, 5)
|
||||
var quality_probs: Dictionary = SPAWN_PROBABILITY.get(quality_clamped, SPAWN_PROBABILITY[3])
|
||||
|
||||
# Boost amphibious weight when tile is adjacent to water
|
||||
var effective_weights: Dictionary = biome_trait_weights.duplicate(true)
|
||||
if adjacent_to_water:
|
||||
_boost_amphibious_weights(effective_weights)
|
||||
|
||||
var results: Array = []
|
||||
var seen_hashes: Dictionary = {}
|
||||
var attempts: int = 0
|
||||
|
|
@ -48,13 +58,13 @@ static func generate(
|
|||
attempts += 1
|
||||
|
||||
var traits := TraitSetScript.new()
|
||||
traits.size = _roll_trait(rng, "size", biome_trait_weights)
|
||||
traits.diet = _roll_trait(rng, "diet", biome_trait_weights)
|
||||
traits.habitat = _roll_trait(rng, "habitat", biome_trait_weights)
|
||||
traits.locomotion = _roll_trait(rng, "locomotion", biome_trait_weights)
|
||||
traits.reproduction = _roll_trait(rng, "reproduction", biome_trait_weights)
|
||||
traits.thermal = _roll_trait(rng, "thermal", biome_trait_weights)
|
||||
traits.social = _roll_trait(rng, "social", biome_trait_weights)
|
||||
traits.size = _roll_trait(rng, "size", effective_weights)
|
||||
traits.diet = _roll_trait(rng, "diet", effective_weights)
|
||||
traits.habitat = _roll_trait(rng, "habitat", effective_weights)
|
||||
traits.locomotion = _roll_trait(rng, "locomotion", effective_weights)
|
||||
traits.reproduction = _roll_trait(rng, "reproduction", effective_weights)
|
||||
traits.thermal = _roll_trait(rng, "thermal", effective_weights)
|
||||
traits.social = _roll_trait(rng, "social", effective_weights)
|
||||
|
||||
if not traits.validate():
|
||||
continue
|
||||
|
|
@ -129,3 +139,33 @@ static func _default_trait(category: String) -> String:
|
|||
return "solitary"
|
||||
_:
|
||||
return ""
|
||||
|
||||
|
||||
## Boost amphibious habitat weight when tile borders water.
|
||||
## Modifies the weights dictionary in place.
|
||||
static func _boost_amphibious_weights(weights: Dictionary) -> void:
|
||||
var habitat_w: Dictionary = weights.get("habitat", {})
|
||||
if habitat_w.is_empty():
|
||||
return
|
||||
var current: float = float(habitat_w.get("amphibious", 0))
|
||||
if current <= 0.0:
|
||||
# If no amphibious weight, inject a baseline
|
||||
habitat_w["amphibious"] = 15
|
||||
else:
|
||||
habitat_w["amphibious"] = int(current * AMPHIBIOUS_ADJACENCY_BOOST)
|
||||
# Also boost swimming locomotion slightly for water-adjacent tiles
|
||||
var loco_w: Dictionary = weights.get("locomotion", {})
|
||||
if not loco_w.is_empty():
|
||||
var swim: float = float(loco_w.get("swimming", 0))
|
||||
if swim > 0.0:
|
||||
loco_w["swimming"] = int(swim * 1.5)
|
||||
|
||||
|
||||
## Determine migration pattern for a species based on traits.
|
||||
## Returns "seasonal" for aerial herd/swarm species, null otherwise.
|
||||
static func get_migration_pattern(traits: Variant) -> Variant:
|
||||
if traits.habitat != "aerial" and traits.locomotion != "flying":
|
||||
return null
|
||||
if traits.social == "herd" or traits.social == "swarm":
|
||||
return "seasonal"
|
||||
return null
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue