1use serde::{Deserialize, Serialize};
5
6#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct ZoneCullingProfile {
165 #[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 pub fn is_layer_visible(&self, layer: u8) -> bool {
191 layer < 10 && (self.layer_visibility_mask & (1 << layer)) != 0
192 }
193
194 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#[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 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 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#[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}