Skip to main content

dreamwell_engine/
zone.rs

1// Zone runtime type — spatial containers for scene content, entry points,
2// physics defaults, and LOD/culling configuration.
3
4use serde::{Deserialize, Serialize};
5
6/// A runtime zone within the 9-layer topology.
7/// Zones are loaded/unloaded as the player traverses the world.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Zone {
10    pub id: String,
11    pub name: String,
12    #[serde(default = "default_topology_layer")]
13    pub topology_layer: u8,
14    #[serde(default)]
15    pub bounds: ZoneBounds,
16    #[serde(default)]
17    pub physics: ZonePhysicsDefaults,
18    #[serde(default)]
19    pub entry_points: Vec<EntryPoint>,
20    #[serde(default)]
21    pub poi_bindings: Vec<PoiBinding>,
22    #[serde(default)]
23    pub lod_profile: LodSuperpositionProfile,
24    #[serde(default)]
25    pub cull_profile: ZoneCullingProfile,
26    #[serde(default)]
27    pub parent_zone_id: Option<String>,
28}
29
30fn default_topology_layer() -> u8 {
31    6
32}
33
34/// Spawn/transition entry point within a zone.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct EntryPoint {
37    pub id: String,
38    pub position: [f32; 3],
39    #[serde(default)]
40    pub facing: [f32; 3],
41    #[serde(default)]
42    pub kind: EntryPointKind,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
46pub enum EntryPointKind {
47    #[default]
48    Spawn,
49    Portal,
50    Waypoint,
51    Checkpoint,
52}
53
54/// Axis-aligned bounding region for a zone.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ZoneBounds {
57    #[serde(default)]
58    pub min: [f32; 3],
59    #[serde(default = "default_bounds_max")]
60    pub max: [f32; 3],
61}
62
63fn default_bounds_max() -> [f32; 3] {
64    [256.0, 128.0, 256.0]
65}
66
67impl Default for ZoneBounds {
68    fn default() -> Self {
69        Self {
70            min: [0.0; 3],
71            max: default_bounds_max(),
72        }
73    }
74}
75
76/// Per-zone physics overrides.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ZonePhysicsDefaults {
79    #[serde(default = "default_gravity")]
80    pub gravity: [f32; 3],
81    #[serde(default = "default_friction")]
82    pub friction: f32,
83    #[serde(default)]
84    pub atmosphere_density: f32,
85    #[serde(default = "default_step_height")]
86    pub step_height: f32,
87    #[serde(default = "default_slope_limit")]
88    pub slope_limit_degrees: f32,
89}
90
91fn default_gravity() -> [f32; 3] {
92    [0.0, -9.81, 0.0]
93}
94fn default_friction() -> f32 {
95    0.5
96}
97fn default_step_height() -> f32 {
98    0.35
99}
100fn default_slope_limit() -> f32 {
101    45.0
102}
103
104impl Default for ZonePhysicsDefaults {
105    fn default() -> Self {
106        Self {
107            gravity: default_gravity(),
108            friction: default_friction(),
109            atmosphere_density: 0.0,
110            step_height: default_step_height(),
111            slope_limit_degrees: default_slope_limit(),
112        }
113    }
114}
115
116/// Binds a POI definition to a zone location.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct PoiBinding {
119    pub poi_id: String,
120    pub position: [f32; 3],
121    #[serde(default = "default_interaction_radius")]
122    pub interaction_radius: f32,
123    #[serde(default)]
124    pub property_tag: Option<String>,
125}
126
127fn default_interaction_radius() -> f32 {
128    2.0
129}
130
131/// Per-zone LOD distance thresholds.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct LodSuperpositionProfile {
134    #[serde(default = "default_lod1_distance")]
135    pub lod1_distance: f32,
136    #[serde(default = "default_lod2_distance")]
137    pub lod2_distance: f32,
138    #[serde(default = "default_cull_distance")]
139    pub cull_distance: f32,
140}
141
142fn default_lod1_distance() -> f32 {
143    30.0
144}
145fn default_lod2_distance() -> f32 {
146    80.0
147}
148fn default_cull_distance() -> f32 {
149    200.0
150}
151
152impl Default for LodSuperpositionProfile {
153    fn default() -> Self {
154        Self {
155            lod1_distance: default_lod1_distance(),
156            lod2_distance: default_lod2_distance(),
157            cull_distance: default_cull_distance(),
158        }
159    }
160}
161
162/// Per-zone visibility culling configuration.
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct ZoneCullingProfile {
165    /// Bitmask of visible topology layers (default: all 10 visible = 0x3FF).
166    #[serde(default = "default_layer_mask")]
167    pub layer_visibility_mask: u32,
168    #[serde(default = "default_max_visible")]
169    pub max_visible_objects: u32,
170}
171
172fn default_layer_mask() -> u32 {
173    0x3FF
174}
175fn default_max_visible() -> u32 {
176    65536
177}
178
179impl Default for ZoneCullingProfile {
180    fn default() -> Self {
181        Self {
182            layer_visibility_mask: default_layer_mask(),
183            max_visible_objects: default_max_visible(),
184        }
185    }
186}
187
188impl ZoneCullingProfile {
189    /// Convert layer mask to per-layer visibility array.
190    pub fn is_layer_visible(&self, layer: u8) -> bool {
191        layer < 10 && (self.layer_visibility_mask & (1 << layer)) != 0
192    }
193
194    /// Create a mask from a list of visible layer indices.
195    pub fn from_visible_layers(layers: &[u8]) -> Self {
196        let mut mask = 0u32;
197        for &l in layers {
198            if l < 10 {
199                mask |= 1 << l;
200            }
201        }
202        Self {
203            layer_visibility_mask: mask,
204            max_visible_objects: default_max_visible(),
205        }
206    }
207}
208
209/// Scene transition state machine.
210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211pub struct SceneTransitionState {
212    pub phase: TransitionPhase,
213    pub source_zone_id: Option<String>,
214    pub target_zone_id: Option<String>,
215    #[serde(default)]
216    pub progress: f32,
217}
218
219impl Default for SceneTransitionState {
220    fn default() -> Self {
221        Self {
222            phase: TransitionPhase::Idle,
223            source_zone_id: None,
224            target_zone_id: None,
225            progress: 0.0,
226        }
227    }
228}
229
230impl SceneTransitionState {
231    /// Begin a transition from source to target zone.
232    pub fn begin(&mut self, source: &str, target: &str) {
233        self.phase = TransitionPhase::FadeOut;
234        self.source_zone_id = Some(source.to_string());
235        self.target_zone_id = Some(target.to_string());
236        self.progress = 0.0;
237    }
238
239    /// Advance transition by dt seconds (assumes 1.0s per phase).
240    pub fn advance(&mut self, dt: f32) {
241        self.progress += dt;
242        if self.progress >= 1.0 {
243            self.progress = 0.0;
244            self.phase = match self.phase {
245                TransitionPhase::Idle => TransitionPhase::Idle,
246                TransitionPhase::FadeOut => TransitionPhase::Loading,
247                TransitionPhase::Loading => TransitionPhase::FadeIn,
248                TransitionPhase::FadeIn => TransitionPhase::Idle,
249            };
250        }
251    }
252
253    pub fn is_active(&self) -> bool {
254        self.phase != TransitionPhase::Idle
255    }
256}
257
258/// Phases of a scene transition.
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
260pub enum TransitionPhase {
261    #[default]
262    Idle,
263    FadeOut,
264    Loading,
265    FadeIn,
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn zone_serde_roundtrip() {
274        let zone = Zone {
275            id: "zone_01".into(),
276            name: "Test Zone".into(),
277            topology_layer: 6,
278            bounds: ZoneBounds::default(),
279            physics: ZonePhysicsDefaults::default(),
280            entry_points: vec![EntryPoint {
281                id: "spawn_a".into(),
282                position: [10.0, 0.0, 5.0],
283                facing: [0.0, 0.0, 1.0],
284                kind: EntryPointKind::Spawn,
285            }],
286            poi_bindings: vec![],
287            lod_profile: LodSuperpositionProfile::default(),
288            cull_profile: ZoneCullingProfile::default(),
289            parent_zone_id: None,
290        };
291        let json = serde_json::to_string(&zone).unwrap();
292        let back: Zone = serde_json::from_str(&json).unwrap();
293        assert_eq!(back.id, "zone_01");
294        assert_eq!(back.topology_layer, 6);
295        assert_eq!(back.entry_points.len(), 1);
296    }
297
298    #[test]
299    fn default_gravity() {
300        let phys = ZonePhysicsDefaults::default();
301        assert!((phys.gravity[1] - (-9.81)).abs() < 0.001);
302        assert!((phys.step_height - 0.35).abs() < 0.001);
303    }
304
305    #[test]
306    fn transition_state_cycle() {
307        let mut ts = SceneTransitionState::default();
308        assert!(!ts.is_active());
309        ts.begin("zone_a", "zone_b");
310        assert!(ts.is_active());
311        assert_eq!(ts.phase, TransitionPhase::FadeOut);
312        ts.advance(1.0);
313        assert_eq!(ts.phase, TransitionPhase::Loading);
314        ts.advance(1.0);
315        assert_eq!(ts.phase, TransitionPhase::FadeIn);
316        ts.advance(1.0);
317        assert_eq!(ts.phase, TransitionPhase::Idle);
318        assert!(!ts.is_active());
319    }
320
321    #[test]
322    fn cull_profile_layer_mask() {
323        let profile = ZoneCullingProfile::from_visible_layers(&[0, 3, 6, 9]);
324        assert!(profile.is_layer_visible(0));
325        assert!(profile.is_layer_visible(3));
326        assert!(profile.is_layer_visible(6));
327        assert!(profile.is_layer_visible(9));
328        assert!(!profile.is_layer_visible(1));
329        assert!(!profile.is_layer_visible(10));
330    }
331}