Rain, storm, snow, and fog now have atmospheric ambient messages. Clear and cloudy conditions return empty list. Messages are evocative and lowercase, ready to be mixed with zone-specific ambience.
292 lines
7.9 KiB
Python
292 lines
7.9 KiB
Python
"""Weather system with procedural transitions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import enum
|
|
import random
|
|
from dataclasses import dataclass
|
|
|
|
|
|
class WeatherCondition(enum.Enum):
|
|
clear = "clear"
|
|
cloudy = "cloudy"
|
|
rain = "rain"
|
|
storm = "storm"
|
|
snow = "snow"
|
|
fog = "fog"
|
|
|
|
|
|
@dataclass
|
|
class WeatherState:
|
|
condition: WeatherCondition
|
|
intensity: float # 0.0 to 1.0
|
|
|
|
|
|
def get_weather_description(state: WeatherState) -> str:
|
|
"""Return atmospheric text for the current weather."""
|
|
condition = state.condition
|
|
intensity = state.intensity
|
|
|
|
if condition == WeatherCondition.clear:
|
|
return "the sky is clear"
|
|
|
|
elif condition == WeatherCondition.cloudy:
|
|
if intensity < 0.4:
|
|
return "thin clouds drift overhead"
|
|
elif intensity < 0.7:
|
|
return "clouds fill the sky"
|
|
else:
|
|
return "heavy clouds loom darkly"
|
|
|
|
elif condition == WeatherCondition.rain:
|
|
if intensity < 0.3:
|
|
return "a light drizzle falls"
|
|
elif intensity < 0.6:
|
|
return "rain patters steadily"
|
|
else:
|
|
return "rain hammers down relentlessly"
|
|
|
|
elif condition == WeatherCondition.storm:
|
|
if intensity < 0.5:
|
|
return "thunder rumbles in the distance"
|
|
else:
|
|
return "lightning splits the sky as the storm rages"
|
|
|
|
elif condition == WeatherCondition.snow:
|
|
if intensity < 0.3:
|
|
return "light snow drifts down"
|
|
elif intensity < 0.6:
|
|
return "snow falls steadily"
|
|
else:
|
|
return "heavy snow blankets everything"
|
|
|
|
elif condition == WeatherCondition.fog:
|
|
if intensity < 0.4:
|
|
return "thin mist hangs in the air"
|
|
elif intensity < 0.7:
|
|
return "fog obscures the distance"
|
|
else:
|
|
return "thick fog shrouds everything"
|
|
|
|
return ""
|
|
|
|
|
|
# Climate profiles: transition weights for each condition
|
|
# Format: {from_condition: {to_condition: weight, ...}, ...}
|
|
CLIMATE_PROFILES = {
|
|
"temperate": {
|
|
WeatherCondition.clear: {
|
|
"clear": 50,
|
|
"cloudy": 40,
|
|
"fog": 10,
|
|
},
|
|
WeatherCondition.cloudy: {
|
|
"clear": 30,
|
|
"cloudy": 35,
|
|
"rain": 20,
|
|
"snow": 10,
|
|
"fog": 5,
|
|
},
|
|
WeatherCondition.rain: {
|
|
"cloudy": 35,
|
|
"rain": 30,
|
|
"storm": 15,
|
|
"snow": 10,
|
|
"fog": 10,
|
|
},
|
|
WeatherCondition.storm: {
|
|
"rain": 60,
|
|
"cloudy": 40,
|
|
},
|
|
WeatherCondition.snow: {
|
|
"snow": 40,
|
|
"cloudy": 35,
|
|
"fog": 25,
|
|
},
|
|
WeatherCondition.fog: {
|
|
"fog": 30,
|
|
"cloudy": 40,
|
|
"clear": 30,
|
|
},
|
|
},
|
|
"arid": {
|
|
WeatherCondition.clear: {
|
|
"clear": 90,
|
|
"cloudy": 10,
|
|
},
|
|
WeatherCondition.cloudy: {
|
|
"clear": 70,
|
|
"cloudy": 20,
|
|
"rain": 5,
|
|
"fog": 5,
|
|
},
|
|
WeatherCondition.rain: {
|
|
"cloudy": 60,
|
|
"rain": 20,
|
|
"clear": 20,
|
|
},
|
|
WeatherCondition.storm: {
|
|
"rain": 50,
|
|
"cloudy": 50,
|
|
},
|
|
WeatherCondition.snow: {
|
|
"cloudy": 100,
|
|
},
|
|
WeatherCondition.fog: {
|
|
"clear": 60,
|
|
"fog": 20,
|
|
"cloudy": 20,
|
|
},
|
|
},
|
|
"arctic": {
|
|
WeatherCondition.clear: {
|
|
"cloudy": 50,
|
|
"fog": 30,
|
|
"clear": 20,
|
|
},
|
|
WeatherCondition.cloudy: {
|
|
"snow": 40,
|
|
"cloudy": 35,
|
|
"fog": 25,
|
|
},
|
|
WeatherCondition.rain: {
|
|
"snow": 40,
|
|
"cloudy": 40,
|
|
"fog": 20,
|
|
},
|
|
WeatherCondition.storm: {
|
|
"snow": 60,
|
|
"cloudy": 40,
|
|
},
|
|
WeatherCondition.snow: {
|
|
"snow": 50,
|
|
"cloudy": 30,
|
|
"fog": 20,
|
|
},
|
|
WeatherCondition.fog: {
|
|
"fog": 40,
|
|
"cloudy": 40,
|
|
"snow": 20,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def advance_weather(
|
|
current: WeatherState,
|
|
season: str = "summer",
|
|
rng: random.Random | None = None,
|
|
climate: str = "temperate",
|
|
) -> WeatherState:
|
|
"""Advance weather by one step (one game hour)."""
|
|
if rng is None:
|
|
rng = random.Random()
|
|
|
|
# Get climate profile
|
|
profile = CLIMATE_PROFILES.get(climate, CLIMATE_PROFILES["temperate"])
|
|
|
|
# Get transition weights for current condition
|
|
transitions = profile.get(current.condition, {})
|
|
|
|
# Filter out snow if not winter/autumn
|
|
if season not in ("winter", "autumn"):
|
|
transitions = {k: v for k, v in transitions.items() if k != "snow"}
|
|
|
|
# If no valid transitions, stay in current condition
|
|
if not transitions:
|
|
return WeatherState(
|
|
condition=current.condition, intensity=rng.uniform(0.0, 1.0)
|
|
)
|
|
|
|
# Weighted random choice
|
|
conditions = list(transitions.keys())
|
|
weights = list(transitions.values())
|
|
|
|
# Convert string keys to WeatherCondition enums
|
|
resolved_conditions = []
|
|
for cond in conditions:
|
|
if isinstance(cond, str):
|
|
resolved_conditions.append(WeatherCondition[cond])
|
|
else:
|
|
resolved_conditions.append(cond)
|
|
|
|
new_condition = rng.choices(resolved_conditions, weights=weights, k=1)[0]
|
|
|
|
# Generate appropriate intensity for the new condition
|
|
if new_condition == WeatherCondition.clear:
|
|
new_intensity = 0.5 # Clear has no meaningful intensity variation
|
|
elif new_condition == WeatherCondition.storm:
|
|
new_intensity = rng.uniform(0.5, 1.0) # Storms are always intense
|
|
else:
|
|
new_intensity = rng.uniform(0.0, 1.0)
|
|
|
|
return WeatherState(condition=new_condition, intensity=new_intensity)
|
|
|
|
|
|
# Global weather state
|
|
_current_weather: WeatherState | None = None
|
|
|
|
|
|
def init_weather(
|
|
condition: WeatherCondition = WeatherCondition.clear, intensity: float = 0.5
|
|
) -> None:
|
|
"""Initialize the global weather state.
|
|
|
|
Args:
|
|
condition: Initial weather condition
|
|
intensity: Initial intensity (0.0 to 1.0)
|
|
"""
|
|
global _current_weather
|
|
_current_weather = WeatherState(condition=condition, intensity=intensity)
|
|
|
|
|
|
def get_current_weather() -> WeatherState:
|
|
"""Get the current global weather state.
|
|
|
|
Returns:
|
|
Current weather state (defaults to clear if not initialized)
|
|
"""
|
|
if _current_weather is None:
|
|
return WeatherState(condition=WeatherCondition.clear, intensity=0.5)
|
|
return _current_weather
|
|
|
|
|
|
def tick_weather(season: str = "summer", climate: str = "temperate") -> None:
|
|
"""Advance weather by one step. Called once per game hour.
|
|
|
|
Args:
|
|
season: Current season
|
|
climate: Climate profile to use
|
|
"""
|
|
global _current_weather
|
|
current = get_current_weather()
|
|
_current_weather = advance_weather(current, season=season, climate=climate)
|
|
|
|
|
|
def get_weather_ambience(condition: WeatherCondition) -> list[str]:
|
|
"""Return ambient messages appropriate for the weather."""
|
|
if condition == WeatherCondition.rain:
|
|
return [
|
|
"rain patters on the ground around you.",
|
|
"water drips from above.",
|
|
"you hear the steady rhythm of rainfall.",
|
|
]
|
|
elif condition == WeatherCondition.storm:
|
|
return [
|
|
"thunder cracks overhead.",
|
|
"lightning flashes in the distance.",
|
|
"the wind howls fiercely.",
|
|
]
|
|
elif condition == WeatherCondition.snow:
|
|
return [
|
|
"snowflakes drift silently down.",
|
|
"the world is muffled under falling snow.",
|
|
]
|
|
elif condition == WeatherCondition.fog:
|
|
return [
|
|
"mist swirls around your feet.",
|
|
"shapes loom and fade in the fog.",
|
|
]
|
|
else:
|
|
# clear or cloudy: no extra ambience
|
|
return []
|