extends Node ## Daily weather roll + queries. ## ## Rolls once per in-game day (triggered by EventBus.sim_tick), using ## season-weighted probabilities from design.md §"Weather — 4 types, daily roll". ## Emits EventBus.weather_changed(weather) when the kind changes. ## ## Public API (locked — Agents A + C compile against these names): ## const WEATHER_CLEAR / RAIN / STORM / COLD_SNAP : StringName ## current_weather: StringName ## is_raining() -> bool # rain OR storm ## is_storming() -> bool # storm only ## is_cold_snap() -> bool # cold_snap only ## save_dict() / apply_dict() # day_index + current_weather round-trip const WEATHER_CLEAR: StringName = &"clear" const WEATHER_RAIN: StringName = &"rain" const WEATHER_STORM: StringName = &"storm" const WEATHER_COLD_SNAP: StringName = &"cold_snap" ## Current weather kind. Changes once per in-game day. var current_weather: StringName = WEATHER_CLEAR ## Last day index we rolled for; -1 means no roll yet this session. ## Persisted via save_dict / apply_dict so a mid-day reload doesn't double-roll. var _last_day_index: int = -1 # Season-weighted probability tables. # Each entry is [clear_weight, rain_weight, storm_weight, cold_snap_weight]. # Weights are fractions that sum to 1.0 — design.md values (placeholders, tune Phase 20). # # spring: 60% clear, 35% rain, 5% storm # summer: 75% clear, 18% rain, 7% storm # autumn: 50% clear, 35% rain, 12% storm, 3% cold_snap # winter: 55% clear, 15% rain, 10% storm, 20% cold_snap # # Note: design.md lists slightly different values (e.g. summer 70/20/10). # Agent B's briefing numbers take precedence as they were authored after the # design doc placeholder row — tune both together in Phase 20. const _SEASON_WEIGHTS: Dictionary = { &"spring": [0.60, 0.35, 0.05, 0.00], &"summer": [0.75, 0.18, 0.07, 0.00], &"autumn": [0.50, 0.35, 0.12, 0.03], &"winter": [0.55, 0.15, 0.10, 0.20], } # Maps weight-table index → weather kind constant. const _WEATHER_KINDS: Array[StringName] = [ WEATHER_CLEAR, WEATHER_RAIN, WEATHER_STORM, WEATHER_COLD_SNAP, ] func _ready() -> void: EventBus.sim_tick.connect(_on_sim_tick) # ── sim tick handler ───────────────────────────────────────────────────────── func _on_sim_tick(_tick_n: int) -> void: var day_idx: int = Clock.day_index_from_start() if day_idx == _last_day_index: return # same day — no roll needed _last_day_index = day_idx _roll(day_idx) func _roll(day_idx: int) -> void: var season: StringName = Clock.current_season() var weights: Array = _SEASON_WEIGHTS.get(season, _SEASON_WEIGHTS[&"spring"]) var roll: float = randf() var cumulative: float = 0.0 var new_weather: StringName = WEATHER_CLEAR for i in _WEATHER_KINDS.size(): cumulative += weights[i] if roll <= cumulative: new_weather = _WEATHER_KINDS[i] break Audit.log("weather", "day %d %s → %s (roll %.3f)" % [day_idx, season, new_weather, roll]) if new_weather != current_weather: current_weather = new_weather EventBus.weather_changed.emit(current_weather) # ── public queries ─────────────────────────────────────────────────────────── func is_raining() -> bool: return current_weather == WEATHER_RAIN or current_weather == WEATHER_STORM func is_storming() -> bool: return current_weather == WEATHER_STORM func is_cold_snap() -> bool: return current_weather == WEATHER_COLD_SNAP # ── save / load ────────────────────────────────────────────────────────────── func save_dict() -> Dictionary: return { "weather": String(current_weather), "last_day_index": _last_day_index, } func apply_dict(d: Dictionary) -> void: current_weather = StringName(d.get("weather", String(WEATHER_CLEAR))) _last_day_index = int(d.get("last_day_index", -1))