mud/src/mudlib/weather.py
Jared Miller 25339edbf5
Add weather-driven ambient messages
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.
2026-02-14 16:20:00 -05:00

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 []