Skip to main content

proof_engine/world/
mod.rs

1//! Open-world management: zone streaming, portals, day/night cycle,
2//! weather simulation, and world-level event coordination.
3
4use glam::{Vec2, Vec3, Vec4};
5use std::collections::{HashMap, HashSet, VecDeque};
6
7// ─── Zone ─────────────────────────────────────────────────────────────────────
8
9/// Unique identifier for a world zone.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct ZoneId(pub u32);
12
13/// Streaming state of a zone.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ZoneStreamState {
16    Unloaded,
17    Loading,
18    Active,
19    Dormant,   // loaded but not ticked (background)
20    Unloading,
21}
22
23/// World axis-aligned bounding rectangle (XZ plane).
24#[derive(Debug, Clone, Copy)]
25pub struct WorldRect {
26    pub min: Vec2,
27    pub max: Vec2,
28}
29
30impl WorldRect {
31    pub fn new(min: Vec2, max: Vec2) -> Self { Self { min, max } }
32
33    pub fn from_center(center: Vec2, half_size: Vec2) -> Self {
34        Self { min: center - half_size, max: center + half_size }
35    }
36
37    pub fn contains_point(&self, p: Vec2) -> bool {
38        p.x >= self.min.x && p.x <= self.max.x &&
39        p.y >= self.min.y && p.y <= self.max.y
40    }
41
42    pub fn overlaps(&self, other: &WorldRect) -> bool {
43        self.min.x < other.max.x && self.max.x > other.min.x &&
44        self.min.y < other.max.y && self.max.y > other.min.y
45    }
46
47    pub fn center(&self) -> Vec2 { (self.min + self.max) * 0.5 }
48
49    pub fn size(&self) -> Vec2 { self.max - self.min }
50
51    pub fn expand(&self, margin: f32) -> Self {
52        Self {
53            min: self.min - Vec2::splat(margin),
54            max: self.max + Vec2::splat(margin),
55        }
56    }
57
58    pub fn distance_to(&self, p: Vec2) -> f32 {
59        let dx = (self.min.x - p.x).max(0.0).max(p.x - self.max.x);
60        let dy = (self.min.y - p.y).max(0.0).max(p.y - self.max.y);
61        Vec2::new(dx, dy).length()
62    }
63}
64
65/// A world zone definition.
66#[derive(Debug, Clone)]
67pub struct Zone {
68    pub id:         ZoneId,
69    pub name:       String,
70    pub bounds:     WorldRect,
71    pub state:      ZoneStreamState,
72    pub biome:      BiomeType,
73    pub neighbors:  Vec<ZoneId>,
74    pub portals:    Vec<PortalId>,
75    pub load_priority: f32,
76    pub is_indoor:  bool,
77    pub ambient_color: Vec4,
78    pub fog_color:     Vec4,
79    pub fog_density:   f32,
80    pub tick_count:    u64,
81}
82
83impl Zone {
84    pub fn new(id: ZoneId, name: impl Into<String>, bounds: WorldRect) -> Self {
85        Self {
86            id, name: name.into(), bounds,
87            state: ZoneStreamState::Unloaded,
88            biome: BiomeType::Temperate,
89            neighbors: Vec::new(),
90            portals: Vec::new(),
91            load_priority: 0.0,
92            is_indoor: false,
93            ambient_color: Vec4::new(0.2, 0.2, 0.3, 1.0),
94            fog_color:     Vec4::new(0.7, 0.8, 0.9, 1.0),
95            fog_density:   0.002,
96            tick_count:    0,
97        }
98    }
99
100    pub fn is_loaded(&self) -> bool {
101        matches!(self.state, ZoneStreamState::Active | ZoneStreamState::Dormant)
102    }
103}
104
105// ─── Biome ────────────────────────────────────────────────────────────────────
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
108pub enum BiomeType {
109    Temperate,
110    Desert,
111    Arctic,
112    Tropical,
113    Swamp,
114    Volcanic,
115    Underground,
116    Ocean,
117    Sky,
118    Corrupted,
119}
120
121impl BiomeType {
122    pub fn ambient_temperature(&self) -> f32 {
123        match self {
124            BiomeType::Desert      => 45.0,
125            BiomeType::Arctic      => -20.0,
126            BiomeType::Tropical    => 30.0,
127            BiomeType::Volcanic    => 80.0,
128            BiomeType::Underground => 12.0,
129            BiomeType::Ocean       => 18.0,
130            BiomeType::Sky         => -5.0,
131            _                      => 20.0,
132        }
133    }
134
135    pub fn base_weather(&self) -> WeatherType {
136        match self {
137            BiomeType::Desert   => WeatherType::Clear,
138            BiomeType::Arctic   => WeatherType::Blizzard,
139            BiomeType::Tropical => WeatherType::HeavyRain,
140            BiomeType::Volcanic => WeatherType::AshStorm,
141            _                   => WeatherType::Cloudy,
142        }
143    }
144}
145
146// ─── Portal ───────────────────────────────────────────────────────────────────
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
149pub struct PortalId(pub u32);
150
151/// A portal connecting two zones.
152#[derive(Debug, Clone)]
153pub struct Portal {
154    pub id:        PortalId,
155    pub from_zone: ZoneId,
156    pub to_zone:   ZoneId,
157    pub from_pos:  Vec3,
158    pub to_pos:    Vec3,
159    pub from_rot:  f32,  // yaw in radians
160    pub to_rot:    f32,
161    pub radius:    f32,  // trigger radius
162    pub bidirectional: bool,
163    pub locked:    bool,
164    pub unlock_condition: String,
165}
166
167impl Portal {
168    pub fn new(id: PortalId, from: ZoneId, to: ZoneId, from_pos: Vec3, to_pos: Vec3) -> Self {
169        Self {
170            id, from_zone: from, to_zone: to,
171            from_pos, to_pos,
172            from_rot: 0.0, to_rot: std::f32::consts::PI,
173            radius: 1.5, bidirectional: true, locked: false,
174            unlock_condition: String::new(),
175        }
176    }
177
178    pub fn player_in_trigger(&self, player_pos: Vec3) -> bool {
179        (player_pos - self.from_pos).length() <= self.radius
180    }
181}
182
183// ─── Day/night cycle ─────────────────────────────────────────────────────────
184
185/// Full day/night cycle state.
186#[derive(Debug, Clone)]
187pub struct DayNightCycle {
188    /// Time of day in range [0, 1) — 0=midnight, 0.25=6am, 0.5=noon, 0.75=6pm.
189    pub time_of_day: f32,
190    /// Duration of a full day in real seconds.
191    pub day_duration: f32,
192    /// Current day number.
193    pub day_count: u32,
194    /// Speed multiplier (1.0 = real-time).
195    pub speed: f32,
196    pub paused: bool,
197}
198
199impl DayNightCycle {
200    pub fn new(day_duration: f32) -> Self {
201        Self { time_of_day: 0.25, day_duration, day_count: 0, speed: 1.0, paused: false }
202    }
203
204    pub fn tick(&mut self, dt: f32) {
205        if self.paused { return; }
206        self.time_of_day += (dt * self.speed) / self.day_duration;
207        if self.time_of_day >= 1.0 {
208            self.time_of_day -= 1.0;
209            self.day_count += 1;
210        }
211    }
212
213    /// Hours since midnight (0–24).
214    pub fn hour(&self) -> f32 { self.time_of_day * 24.0 }
215
216    pub fn is_daytime(&self) -> bool {
217        let h = self.hour();
218        h >= 6.0 && h < 20.0
219    }
220
221    pub fn is_dawn(&self) -> bool { let h = self.hour(); h >= 5.5 && h < 7.5 }
222    pub fn is_dusk(&self) -> bool { let h = self.hour(); h >= 18.5 && h < 20.5 }
223    pub fn is_night(&self) -> bool { !self.is_daytime() }
224
225    /// Sun direction (Vec3, normalized).
226    pub fn sun_direction(&self) -> Vec3 {
227        let angle = (self.time_of_day - 0.25) * std::f32::consts::TAU;
228        Vec3::new(angle.cos(), angle.sin(), -0.3).normalize_or_zero()
229    }
230
231    /// Moon direction (opposite of sun, slightly offset).
232    pub fn moon_direction(&self) -> Vec3 {
233        let sun = self.sun_direction();
234        Vec3::new(-sun.x, -sun.y * 0.8, -sun.z + 0.2).normalize_or_zero()
235    }
236
237    /// Sky color based on time of day.
238    pub fn sky_color(&self) -> Vec4 {
239        let h = self.hour();
240        // Dawn: orange, Day: blue, Dusk: red-orange, Night: dark blue
241        let (r, g, b) = if h < 5.0 || h >= 21.0 {
242            (0.02, 0.02, 0.08)  // Night
243        } else if h < 7.0 {
244            let t = (h - 5.0) / 2.0;
245            let r = 0.02 + t * (1.0 - 0.02);
246            let g = 0.02 + t * (0.5 - 0.02);
247            let b = 0.08 + t * (0.3 - 0.08);
248            (r, g, b)
249        } else if h < 18.0 {
250            (0.3, 0.55, 0.9)  // Day
251        } else if h < 20.0 {
252            let t = (h - 18.0) / 2.0;
253            let r = 0.3 + t * (0.8 - 0.3);
254            let g = 0.55 + t * (0.3 - 0.55);
255            let b = 0.9 + t * (0.1 - 0.9);
256            (r, g, b)
257        } else {
258            let t = (h - 20.0) / 1.0;
259            (0.8 + t * (0.02 - 0.8), 0.3 + t * (0.02 - 0.3), 0.1 + t * (0.08 - 0.1))
260        };
261        Vec4::new(r.max(0.0).min(1.0), g.max(0.0).min(1.0), b.max(0.0).min(1.0), 1.0)
262    }
263
264    /// Ambient light intensity (0–1).
265    pub fn ambient_intensity(&self) -> f32 {
266        let h = self.hour();
267        if h < 6.0 || h >= 20.0 { return 0.05; }
268        if h < 8.0 { return 0.05 + (h - 6.0) / 2.0 * 0.95; }
269        if h > 18.0 { return 0.05 + (20.0 - h) / 2.0 * 0.95; }
270        1.0
271    }
272
273    /// Sun light intensity.
274    pub fn sun_intensity(&self) -> f32 {
275        let h = self.hour();
276        if h < 6.0 || h >= 20.0 { return 0.0; }
277        let t = if h < 13.0 { (h - 6.0) / 7.0 } else { (20.0 - h) / 7.0 };
278        t.max(0.0).min(1.0)
279    }
280
281    pub fn set_hour(&mut self, h: f32) {
282        self.time_of_day = (h / 24.0).fract();
283    }
284}
285
286// ─── Weather ──────────────────────────────────────────────────────────────────
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
289pub enum WeatherType {
290    Clear,
291    Cloudy,
292    Overcast,
293    LightRain,
294    HeavyRain,
295    Thunderstorm,
296    LightSnow,
297    HeavySnow,
298    Blizzard,
299    Fog,
300    Heatwave,
301    AshStorm,
302    MagicStorm,
303}
304
305/// Weather system with transition and effects.
306#[derive(Debug, Clone)]
307pub struct WeatherSystem {
308    pub current:    WeatherType,
309    pub target:     WeatherType,
310    pub blend:      f32,   // 0=current, 1=target
311    pub transition_speed: f32,
312    pub cloud_coverage: f32,  // 0..1
313    pub precipitation: f32,   // 0..1 (rain/snow intensity)
314    pub wind_speed:    f32,   // m/s
315    pub wind_dir:      Vec2,  // normalized
316    pub visibility:    f32,   // km
317    pub temperature:   f32,   // celsius
318    pub humidity:      f32,   // 0..1
319    pub lightning_chance: f32, // per-second probability during storm
320    pub thunder_delay:    f32, // seconds after lightning
321    time_in_state: f32,
322    forecast:      Vec<(WeatherType, f32)>, // upcoming weather + duration
323}
324
325impl WeatherSystem {
326    pub fn new() -> Self {
327        Self {
328            current: WeatherType::Clear,
329            target:  WeatherType::Clear,
330            blend:   1.0,
331            transition_speed: 0.1,
332            cloud_coverage: 0.1,
333            precipitation: 0.0,
334            wind_speed: 2.0,
335            wind_dir: Vec2::new(1.0, 0.0),
336            visibility: 20.0,
337            temperature: 20.0,
338            humidity: 0.4,
339            lightning_chance: 0.0,
340            thunder_delay: 3.0,
341            time_in_state: 0.0,
342            forecast: Vec::new(),
343        }
344    }
345
346    pub fn transition_to(&mut self, weather: WeatherType, speed: f32) {
347        if self.current == weather { return; }
348        self.target = weather;
349        self.blend = 0.0;
350        self.transition_speed = speed;
351    }
352
353    pub fn tick(&mut self, dt: f32) {
354        self.time_in_state += dt;
355
356        if self.blend < 1.0 {
357            self.blend = (self.blend + self.transition_speed * dt).min(1.0);
358            if self.blend >= 1.0 {
359                self.current = self.target;
360                self.time_in_state = 0.0;
361            }
362        }
363
364        // Update derived values from current + blend toward target
365        let (cc, pr, ws, vis, temp, hum, light) = Self::weather_params(self.current);
366        let (tcc, tpr, tws, tvis, ttemp, thum, tlight) = Self::weather_params(self.target);
367        let t = self.blend;
368        self.cloud_coverage   = cc + t * (tcc - cc);
369        self.precipitation    = pr + t * (tpr - pr);
370        self.wind_speed       = ws + t * (tws - ws);
371        self.visibility       = vis + t * (tvis - vis);
372        self.temperature      = temp + t * (ttemp - temp);
373        self.humidity         = hum + t * (thum - hum);
374        self.lightning_chance = light + t * (tlight - light);
375    }
376
377    fn weather_params(w: WeatherType) -> (f32, f32, f32, f32, f32, f32, f32) {
378        // (cloud_coverage, precipitation, wind_speed, visibility, temperature, humidity, lightning_chance)
379        match w {
380            WeatherType::Clear       => (0.1, 0.0, 2.0,  20.0, 22.0, 0.3, 0.0),
381            WeatherType::Cloudy      => (0.5, 0.0, 4.0,  15.0, 18.0, 0.5, 0.0),
382            WeatherType::Overcast    => (0.9, 0.0, 5.0,  10.0, 14.0, 0.7, 0.0),
383            WeatherType::LightRain   => (0.7, 0.3, 6.0,  8.0,  12.0, 0.8, 0.0),
384            WeatherType::HeavyRain   => (0.9, 0.8, 10.0, 3.0,  10.0, 0.95,0.0),
385            WeatherType::Thunderstorm=> (1.0, 0.9, 15.0, 2.0,  9.0,  1.0, 0.05),
386            WeatherType::LightSnow   => (0.7, 0.3, 5.0,  6.0, -2.0,  0.7, 0.0),
387            WeatherType::HeavySnow   => (0.9, 0.8, 8.0,  2.0, -8.0,  0.8, 0.0),
388            WeatherType::Blizzard    => (1.0, 1.0, 20.0, 0.5,-15.0,  0.9, 0.0),
389            WeatherType::Fog         => (0.3, 0.0, 1.0,  0.2, 10.0,  0.95,0.0),
390            WeatherType::Heatwave    => (0.0, 0.0, 3.0,  18.0, 42.0, 0.1, 0.0),
391            WeatherType::AshStorm    => (1.0, 0.0, 18.0, 0.5, 35.0,  0.1, 0.0),
392            WeatherType::MagicStorm  => (1.0, 0.5, 12.0, 1.0, 10.0,  0.7, 0.1),
393        }
394    }
395
396    pub fn is_raining(&self) -> bool {
397        matches!(self.current, WeatherType::LightRain | WeatherType::HeavyRain | WeatherType::Thunderstorm)
398    }
399
400    pub fn is_snowing(&self) -> bool {
401        matches!(self.current, WeatherType::LightSnow | WeatherType::HeavySnow | WeatherType::Blizzard)
402    }
403
404    pub fn push_forecast(&mut self, weather: WeatherType, duration_secs: f32) {
405        self.forecast.push((weather, duration_secs));
406    }
407
408    pub fn advance_forecast(&mut self) {
409        if !self.forecast.is_empty() {
410            let (next, _) = self.forecast.remove(0);
411            self.transition_to(next, 0.05);
412        }
413    }
414}
415
416// ─── World clock ─────────────────────────────────────────────────────────────
417
418/// In-game calendar tracking.
419#[derive(Debug, Clone)]
420pub struct WorldClock {
421    pub year:   u32,
422    pub month:  u32,  // 1-12
423    pub day:    u32,  // 1-30
424    pub hour:   f32,  // 0-24
425    pub days_per_month: u32,
426    pub months_per_year: u32,
427    pub epoch_name: String,
428}
429
430impl WorldClock {
431    pub fn new() -> Self {
432        Self { year: 1, month: 1, day: 1, hour: 6.0, days_per_month: 30, months_per_year: 12, epoch_name: "Age of Stars".into() }
433    }
434
435    pub fn advance_hours(&mut self, h: f32) {
436        self.hour += h;
437        while self.hour >= 24.0 {
438            self.hour -= 24.0;
439            self.advance_days(1);
440        }
441    }
442
443    pub fn advance_days(&mut self, d: u32) {
444        self.day += d;
445        while self.day > self.days_per_month {
446            self.day -= self.days_per_month;
447            self.month += 1;
448            if self.month > self.months_per_year {
449                self.month = 1;
450                self.year += 1;
451            }
452        }
453    }
454
455    pub fn total_days(&self) -> u64 {
456        let y = self.year as u64;
457        let m = self.month as u64;
458        let d = self.day as u64;
459        y * self.months_per_year as u64 * self.days_per_month as u64
460            + (m - 1) * self.days_per_month as u64 + (d - 1)
461    }
462
463    pub fn display(&self) -> String {
464        format!("Day {}/{}/{} {}  ({})",
465            self.day, self.month, self.year,
466            format!("{:02}:{:02}", self.hour as u32, ((self.hour.fract()) * 60.0) as u32),
467            self.epoch_name)
468    }
469
470    pub fn season(&self) -> Season {
471        match self.month {
472            3..=5  => Season::Spring,
473            6..=8  => Season::Summer,
474            9..=11 => Season::Autumn,
475            _      => Season::Winter,
476        }
477    }
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481pub enum Season { Spring, Summer, Autumn, Winter }
482
483// ─── Zone streaming manager ───────────────────────────────────────────────────
484
485/// Manages which zones are loaded/active based on player position.
486pub struct ZoneStreamer {
487    pub zones:         HashMap<ZoneId, Zone>,
488    pub portals:       HashMap<PortalId, Portal>,
489    pub active_zones:  HashSet<ZoneId>,
490    pub load_distance: f32,
491    pub unload_distance: f32,
492    next_zone_id:      u32,
493    next_portal_id:    u32,
494    pub load_queue:    VecDeque<ZoneId>,
495    pub unload_queue:  VecDeque<ZoneId>,
496}
497
498impl ZoneStreamer {
499    pub fn new(load_distance: f32) -> Self {
500        Self {
501            zones: HashMap::new(),
502            portals: HashMap::new(),
503            active_zones: HashSet::new(),
504            load_distance,
505            unload_distance: load_distance * 1.5,
506            next_zone_id: 1,
507            next_portal_id: 1,
508            load_queue: VecDeque::new(),
509            unload_queue: VecDeque::new(),
510        }
511    }
512
513    pub fn register_zone(&mut self, name: impl Into<String>, bounds: WorldRect) -> ZoneId {
514        let id = ZoneId(self.next_zone_id);
515        self.next_zone_id += 1;
516        self.zones.insert(id, Zone::new(id, name, bounds));
517        id
518    }
519
520    pub fn add_portal(&mut self, from: ZoneId, to: ZoneId, from_pos: Vec3, to_pos: Vec3) -> PortalId {
521        let id = PortalId(self.next_portal_id);
522        self.next_portal_id += 1;
523        let portal = Portal::new(id, from, to, from_pos, to_pos);
524        // Link zones
525        if let Some(z) = self.zones.get_mut(&from) { z.portals.push(id); }
526        if let Some(z) = self.zones.get_mut(&to) {
527            if portal.bidirectional { z.portals.push(id); }
528        }
529        self.portals.insert(id, portal);
530        id
531    }
532
533    /// Update streaming based on player world position.
534    pub fn update(&mut self, player_pos: Vec3, dt: f32) {
535        let player_xz = Vec2::new(player_pos.x, player_pos.z);
536
537        // Determine which zones should be loaded
538        let should_load: HashSet<ZoneId> = self.zones.iter()
539            .filter(|(_, z)| {
540                let dist = z.bounds.distance_to(player_xz);
541                dist <= self.load_distance
542            })
543            .map(|(id, _)| *id)
544            .collect();
545
546        let should_unload: HashSet<ZoneId> = self.active_zones.iter()
547            .filter(|id| {
548                if let Some(z) = self.zones.get(id) {
549                    z.bounds.distance_to(player_xz) > self.unload_distance
550                } else { true }
551            })
552            .cloned()
553            .collect();
554
555        // Queue loads
556        for id in &should_load {
557            if !self.active_zones.contains(id) {
558                if !self.load_queue.contains(id) {
559                    self.load_queue.push_back(*id);
560                }
561            }
562        }
563
564        // Queue unloads
565        for id in &should_unload {
566            if !self.unload_queue.contains(id) {
567                self.unload_queue.push_back(*id);
568            }
569        }
570
571        // Process load queue (one per frame to avoid spikes)
572        if let Some(id) = self.load_queue.pop_front() {
573            if let Some(z) = self.zones.get_mut(&id) {
574                z.state = ZoneStreamState::Active;
575            }
576            self.active_zones.insert(id);
577        }
578
579        // Process unload queue
580        if let Some(id) = self.unload_queue.pop_front() {
581            if let Some(z) = self.zones.get_mut(&id) {
582                z.state = ZoneStreamState::Unloaded;
583            }
584            self.active_zones.remove(&id);
585        }
586
587        // Tick active zones
588        for id in &self.active_zones {
589            if let Some(z) = self.zones.get_mut(id) {
590                z.tick_count = z.tick_count.wrapping_add(1);
591                let _ = dt;
592            }
593        }
594    }
595
596    pub fn zone_at(&self, world_pos: Vec3) -> Option<ZoneId> {
597        let xz = Vec2::new(world_pos.x, world_pos.z);
598        self.zones.iter()
599            .find(|(_, z)| z.bounds.contains_point(xz))
600            .map(|(id, _)| *id)
601    }
602
603    pub fn get_zone(&self, id: ZoneId) -> Option<&Zone> { self.zones.get(&id) }
604    pub fn get_zone_mut(&mut self, id: ZoneId) -> Option<&mut Zone> { self.zones.get_mut(&id) }
605
606    pub fn portals_near(&self, pos: Vec3, radius: f32) -> Vec<&Portal> {
607        self.portals.values()
608            .filter(|p| (p.from_pos - pos).length() <= radius)
609            .collect()
610    }
611
612    pub fn check_portal_transitions(&self, player_pos: Vec3) -> Option<&Portal> {
613        self.portals.values().find(|p| !p.locked && p.player_in_trigger(player_pos))
614    }
615}
616
617// ─── World state ─────────────────────────────────────────────────────────────
618
619/// Top-level world state coordinator.
620pub struct WorldState {
621    pub day_night:  DayNightCycle,
622    pub weather:    WeatherSystem,
623    pub clock:      WorldClock,
624    pub streamer:   ZoneStreamer,
625    pub ticks:      u64,
626    pub world_time: f64,  // total elapsed seconds
627    pub paused:     bool,
628    events:         Vec<WorldEvent>,
629}
630
631impl WorldState {
632    pub fn new() -> Self {
633        Self {
634            day_night: DayNightCycle::new(1200.0),  // 20-minute days
635            weather:   WeatherSystem::new(),
636            clock:     WorldClock::new(),
637            streamer:  ZoneStreamer::new(200.0),
638            ticks:     0,
639            world_time: 0.0,
640            paused:    false,
641            events:    Vec::new(),
642        }
643    }
644
645    pub fn tick(&mut self, dt: f32, player_pos: Vec3) {
646        if self.paused { return; }
647        let dt = dt.min(0.1);  // clamp to avoid spiral
648
649        self.world_time += dt as f64;
650        self.ticks += 1;
651
652        self.day_night.tick(dt);
653        self.weather.tick(dt);
654        self.clock.advance_hours(dt / 3600.0 * self.day_night.speed);
655        self.streamer.update(player_pos, dt);
656
657        // Weather forecast advance (every in-game hour)
658        if self.ticks % 3600 == 0 {
659            self.weather.advance_forecast();
660        }
661
662        // Lightning events during storms
663        if self.weather.lightning_chance > 0.0 {
664            // Simple deterministic check using time
665            let should_lightning = (self.world_time * 1000.0) as u64 % 1000 < (self.weather.lightning_chance * 1000.0) as u64;
666            if should_lightning {
667                self.events.push(WorldEvent::Lightning {
668                    position: Vec3::new(
669                        ((self.ticks * 17) % 1000) as f32 - 500.0,
670                        100.0,
671                        ((self.ticks * 31) % 1000) as f32 - 500.0,
672                    ),
673                });
674            }
675        }
676    }
677
678    /// Drain and return world events since last call.
679    pub fn drain_events(&mut self) -> Vec<WorldEvent> {
680        std::mem::take(&mut self.events)
681    }
682
683    pub fn current_zone(&self, player_pos: Vec3) -> Option<ZoneId> {
684        self.streamer.zone_at(player_pos)
685    }
686
687    pub fn sky_color(&self) -> Vec4 {
688        self.day_night.sky_color()
689    }
690
691    pub fn fog_color(&self) -> Vec4 {
692        // Blend zone fog with weather fog
693        Vec4::new(0.7, 0.8, 0.9, 1.0)
694    }
695
696    pub fn fog_density(&self) -> f32 {
697        let base = 0.002;
698        let weather_mult = 1.0 + (1.0 - self.weather.visibility / 20.0) * 5.0;
699        base * weather_mult.max(1.0)
700    }
701
702    pub fn ambient_color(&self) -> Vec4 {
703        let sky = self.day_night.sky_color();
704        let intensity = self.day_night.ambient_intensity();
705        Vec4::new(sky.x * intensity, sky.y * intensity, sky.z * intensity, 1.0)
706    }
707}
708
709/// World-level events dispatched during tick.
710#[derive(Debug, Clone)]
711pub enum WorldEvent {
712    ZoneLoaded(ZoneId),
713    ZoneUnloaded(ZoneId),
714    DayStart { day: u32 },
715    NightStart { day: u32 },
716    WeatherChanged { from: WeatherType, to: WeatherType },
717    Lightning { position: Vec3 },
718    SeasonChanged(Season),
719    NewYear { year: u32 },
720}
721
722impl Default for WorldState {
723    fn default() -> Self { Self::new() }
724}
725
726impl Default for WeatherSystem {
727    fn default() -> Self { Self::new() }
728}
729
730impl Default for DayNightCycle {
731    fn default() -> Self { Self::new(1200.0) }
732}
733
734impl Default for WorldClock {
735    fn default() -> Self { Self::new() }
736}