1use glam::{Vec2, Vec3, Vec4};
5use std::collections::{HashMap, HashSet, VecDeque};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct ZoneId(pub u32);
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ZoneStreamState {
16 Unloaded,
17 Loading,
18 Active,
19 Dormant, Unloading,
21}
22
23#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
149pub struct PortalId(pub u32);
150
151#[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, pub to_rot: f32,
161 pub radius: f32, 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#[derive(Debug, Clone)]
187pub struct DayNightCycle {
188 pub time_of_day: f32,
190 pub day_duration: f32,
192 pub day_count: u32,
194 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 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 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 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 pub fn sky_color(&self) -> Vec4 {
239 let h = self.hour();
240 let (r, g, b) = if h < 5.0 || h >= 21.0 {
242 (0.02, 0.02, 0.08) } 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) } 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 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 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#[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#[derive(Debug, Clone)]
307pub struct WeatherSystem {
308 pub current: WeatherType,
309 pub target: WeatherType,
310 pub blend: f32, pub transition_speed: f32,
312 pub cloud_coverage: f32, pub precipitation: f32, pub wind_speed: f32, pub wind_dir: Vec2, pub visibility: f32, pub temperature: f32, pub humidity: f32, pub lightning_chance: f32, pub thunder_delay: f32, time_in_state: f32,
322 forecast: Vec<(WeatherType, f32)>, }
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 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 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#[derive(Debug, Clone)]
420pub struct WorldClock {
421 pub year: u32,
422 pub month: u32, pub day: u32, pub hour: f32, 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
483pub 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 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 pub fn update(&mut self, player_pos: Vec3, dt: f32) {
535 let player_xz = Vec2::new(player_pos.x, player_pos.z);
536
537 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 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 for id in &should_unload {
566 if !self.unload_queue.contains(id) {
567 self.unload_queue.push_back(*id);
568 }
569 }
570
571 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 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 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
617pub 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, 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), 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); 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 if self.ticks % 3600 == 0 {
659 self.weather.advance_forecast();
660 }
661
662 if self.weather.lightning_chance > 0.0 {
664 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 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 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#[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}