Skip to main content

dreamwell_engine/
game_object.rs

1// game_object.rs — Portable, serializable game object and scene model.
2//
3// Like Unity's GameObject + MonoBehaviour pattern, adapted for Dreamwell's
4// GPU-driven meshlet/particle/procedural rendering and ESCG (Event-Sourced
5// Compute Graph) backend. Every object is an empty container that gains
6// visual identity via MeshBinding and behavior via ComponentSlots.
7//
8// CODESPEC §2.3: Enums as tagged unions for MeshBinding, ComponentKind.
9// CODESPEC §3.1: Scene stores objects contiguously for cache-friendly iteration.
10// CODESPEC §0.1: All logic is pure — no RNG, no time, no I/O.
11
12use serde::{Deserialize, Serialize};
13
14/// Maximum scene objects per GameObjectScene. Prevents unbounded GPU buffer growth.
15pub const MAX_SCENE_OBJECTS: usize = 65536;
16
17/// Maximum components per GameObject.
18pub const MAX_COMPONENTS_PER_OBJECT: usize = 32;
19
20// ── Transform ──────────────────────────────────────────────────────────
21
22/// 3D transform: position, rotation (quaternion), scale.
23/// Column-major model matrix computed on demand via `to_matrix()`.
24#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
25pub struct Transform {
26    pub position: [f32; 3],
27    /// Quaternion `[x, y, z, w]`. Identity = `[0, 0, 0, 1]`.
28    pub rotation: [f32; 4],
29    pub scale: [f32; 3],
30}
31
32impl Default for Transform {
33    fn default() -> Self {
34        Self {
35            position: [0.0; 3],
36            rotation: [0.0, 0.0, 0.0, 1.0],
37            scale: [1.0, 1.0, 1.0],
38        }
39    }
40}
41
42impl Transform {
43    /// Compute the 4×4 column-major model matrix from TRS.
44    pub fn to_matrix(&self) -> [f32; 16] {
45        let [qx, qy, qz, qw] = self.rotation;
46        let [sx, sy, sz] = self.scale;
47        let [px, py, pz] = self.position;
48
49        let x2 = qx + qx;
50        let y2 = qy + qy;
51        let z2 = qz + qz;
52        let xx = qx * x2;
53        let xy = qx * y2;
54        let xz = qx * z2;
55        let yy = qy * y2;
56        let yz = qy * z2;
57        let zz = qz * z2;
58        let wx = qw * x2;
59        let wy = qw * y2;
60        let wz = qw * z2;
61
62        [
63            (1.0 - (yy + zz)) * sx,
64            (xy + wz) * sx,
65            (xz - wy) * sx,
66            0.0,
67            (xy - wz) * sy,
68            (1.0 - (xx + zz)) * sy,
69            (yz + wx) * sy,
70            0.0,
71            (xz + wy) * sz,
72            (yz - wx) * sz,
73            (1.0 - (xx + yy)) * sz,
74            0.0,
75            px,
76            py,
77            pz,
78            1.0,
79        ]
80    }
81
82    /// Validate: finite position, unit quaternion (±1%), positive finite scale.
83    pub fn validate(&self) -> Result<(), String> {
84        if self.position.iter().any(|p| !p.is_finite()) {
85            return Err("transform_invalid_position:contains NaN or Inf".into());
86        }
87        let [qx, qy, qz, qw] = self.rotation;
88        let len_sq = qx * qx + qy * qy + qz * qz + qw * qw;
89        if !len_sq.is_finite() || (len_sq - 1.0).abs() > 0.01 {
90            return Err(format!(
91                "transform_invalid_rotation:quaternion length² {len_sq} not ≈1.0"
92            ));
93        }
94        if self.scale.iter().any(|&s| s <= 0.0 || !s.is_finite()) {
95            return Err("transform_invalid_scale:must be positive and finite".into());
96        }
97        Ok(())
98    }
99}
100
101// ── Mesh binding ───────────────────────────────────────────────────────
102
103/// Primitive mesh type. Maps 1:1 to `dreamwell_gpu::primitives::PrimitiveShape`.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
105pub enum PrimitiveKind {
106    Cube,
107    Sphere,
108    Cylinder,
109    Cone,
110    Plane,
111    Torus,
112    Capsule,
113    Pyramid,
114    Wedge,
115}
116
117impl PrimitiveKind {
118    pub const ALL: &'static [PrimitiveKind] = &[
119        Self::Cube,
120        Self::Sphere,
121        Self::Cylinder,
122        Self::Cone,
123        Self::Plane,
124        Self::Torus,
125        Self::Capsule,
126        Self::Pyramid,
127        Self::Wedge,
128    ];
129
130    pub fn name(self) -> &'static str {
131        match self {
132            Self::Cube => "Cube",
133            Self::Sphere => "Sphere",
134            Self::Cylinder => "Cylinder",
135            Self::Cone => "Cone",
136            Self::Plane => "Plane",
137            Self::Torus => "Torus",
138            Self::Capsule => "Capsule",
139            Self::Pyramid => "Pyramid",
140            Self::Wedge => "Wedge",
141        }
142    }
143}
144
145/// What mesh to render for this object.
146#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
147pub enum MeshBinding {
148    /// No mesh — invisible container for components only.
149    #[default]
150    None,
151    /// Built-in primitive with pre-computed meshlets. Color is RGBA linear.
152    Primitive { kind: PrimitiveKind, color: [f32; 4] },
153    /// Custom mesh referenced by asset key (resolved at load time).
154    Custom { asset_key: String },
155}
156
157// ── Component system ───────────────────────────────────────────────────
158
159/// Component kind — typed categories of attachable game logic.
160/// Analogous to Unity MonoBehaviour categories, mapped to Dreamwell's
161/// ESCG backend systems (physics compute, particles, Waymark scripts).
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
163pub enum ComponentKind {
164    /// Rigid-body physics (integrates with PhysicsGpu simulation).
165    Physics,
166    /// Collision shape (box, sphere, capsule, mesh).
167    Collider,
168    /// Waymark script — event-driven logic from content packs.
169    /// Properties carry the pack-defined fields (like C# serialized fields).
170    WaymarkScript,
171    /// Audio source.
172    Audio,
173    /// Point, spot, or directional light.
174    Light,
175    /// Particle emitter (integrates with PhysicsGpu emitter pool).
176    Particle,
177    /// Camera override (when active, replaces scene camera).
178    Camera,
179    /// Trigger volume — fires ESCG events on enter/exit.
180    Trigger,
181    /// Animation controller (integrates with GpuTweenContext).
182    Animation,
183    /// DreamMatter particle attachment — dissolve/materialize particle effect.
184    /// When attached, the GPU spawns particles from this object's mesh surface.
185    /// Properties: emission_rate, lifetime, particle_count, effect_preset.
186    DreamMatter,
187    /// FBX animation source — skeletal animation from imported FBX binary.
188    /// Properties: fbx_asset_key, clip_name, loop_mode, playback_speed.
189    FbxAnimation,
190    /// User-defined via Waymark content packs.
191    Custom,
192}
193
194/// A single attached component with typed kind and Waymark-style property bag.
195/// Properties are key-value pairs that the ESCG runtime evaluates per-tick.
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ComponentSlot {
198    pub kind: ComponentKind,
199    pub enabled: bool,
200    /// Properties bag: Waymark script fields, physics params, emitter config, etc.
201    /// Serialized as JSON for portability across editor / runtime / server.
202    pub properties: serde_json::Value,
203}
204
205impl ComponentSlot {
206    pub fn new(kind: ComponentKind) -> Self {
207        Self {
208            kind,
209            enabled: true,
210            properties: serde_json::Value::Object(serde_json::Map::new()),
211        }
212    }
213
214    /// Set a named property. Bounded: max 256 properties per component.
215    pub fn set_property(&mut self, key: &str, value: serde_json::Value) -> Result<(), String> {
216        if let serde_json::Value::Object(ref mut map) = self.properties {
217            if map.len() >= 256 && !map.contains_key(key) {
218                return Err("component_property_limit:max 256 properties".into());
219            }
220            map.insert(key.to_string(), value);
221            Ok(())
222        } else {
223            Err("component_properties_not_object:must be a JSON object".into())
224        }
225    }
226
227    pub fn get_property(&self, key: &str) -> Option<&serde_json::Value> {
228        self.properties.get(key)
229    }
230}
231
232// ── GameObject ─────────────────────────────────────────────────────────
233
234/// A game object — empty container that gains visual and behavioral identity
235/// through `MeshBinding` and `ComponentSlot`s.
236///
237/// - `id`: deterministic u64, no UUIDs or RNG.
238/// - `transform`: inline hot data, accessed every frame.
239/// - `components`: bounded `Vec<ComponentSlot>` (max 32).
240/// - Fully serializable for scene save/load and network replication.
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct GameObject {
243    pub id: u64,
244    pub name: String,
245    pub transform: Transform,
246    pub mesh: MeshBinding,
247    pub visible: bool,
248    pub parent_id: Option<u64>,
249    pub components: Vec<ComponentSlot>,
250    /// Property tags (e.g., "isFlammable", "isDestructible"). Drive physics
251    /// heuristics, DreamMatter behavior, and event emission at runtime.
252    #[serde(default, skip_serializing_if = "Vec::is_empty")]
253    pub property_tags: Vec<String>,
254    /// Topology layer index (0=Universe, 9=Point). Determines which gameplay
255    /// channel this object belongs to. GPU quantum culling uses this to render
256    /// only the active layer's content.
257    #[serde(default = "default_topology_layer")]
258    pub topology_layer: u8,
259}
260
261fn default_topology_layer() -> u8 {
262    9
263}
264
265impl GameObject {
266    /// Create a new empty game object with default transform and no mesh.
267    pub fn new(id: u64, name: String) -> Self {
268        Self {
269            id,
270            name,
271            transform: Transform::default(),
272            mesh: MeshBinding::None,
273            visible: true,
274            parent_id: None,
275            components: Vec::new(),
276            property_tags: Vec::new(),
277            topology_layer: 9,
278        }
279    }
280
281    /// Create with a primitive mesh (default gray color).
282    pub fn with_primitive(id: u64, name: String, kind: PrimitiveKind) -> Self {
283        Self {
284            mesh: MeshBinding::Primitive {
285                kind,
286                color: [0.7, 0.7, 0.7, 1.0],
287            },
288            ..Self::new(id, name)
289        }
290    }
291
292    /// Attach a component. Returns error if `MAX_COMPONENTS_PER_OBJECT` exceeded.
293    pub fn add_component(&mut self, slot: ComponentSlot) -> Result<(), String> {
294        if self.components.len() >= MAX_COMPONENTS_PER_OBJECT {
295            return Err(format!(
296                "game_object_component_limit:max {MAX_COMPONENTS_PER_OBJECT} components"
297            ));
298        }
299        self.components.push(slot);
300        Ok(())
301    }
302
303    /// Remove the first component matching `kind`. Returns true if found.
304    pub fn remove_component(&mut self, kind: ComponentKind) -> bool {
305        if let Some(pos) = self.components.iter().position(|c| c.kind == kind) {
306            self.components.swap_remove(pos);
307            true
308        } else {
309            false
310        }
311    }
312
313    pub fn get_component(&self, kind: ComponentKind) -> Option<&ComponentSlot> {
314        self.components.iter().find(|c| c.kind == kind)
315    }
316
317    pub fn get_component_mut(&mut self, kind: ComponentKind) -> Option<&mut ComponentSlot> {
318        self.components.iter_mut().find(|c| c.kind == kind)
319    }
320
321    /// Check whether this object has any component of the given kind.
322    pub fn has_component(&self, kind: ComponentKind) -> bool {
323        self.components.iter().any(|c| c.kind == kind)
324    }
325
326    /// Validate data integrity.
327    pub fn validate(&self) -> Result<(), String> {
328        self.transform.validate()?;
329        if self.name.len() > 256 {
330            return Err("game_object_name_too_long:max 256 chars".into());
331        }
332        if self.components.len() > MAX_COMPONENTS_PER_OBJECT {
333            return Err(format!(
334                "game_object_too_many_components:{} > {MAX_COMPONENTS_PER_OBJECT}",
335                self.components.len()
336            ));
337        }
338        Ok(())
339    }
340}
341
342// ── GameObjectScene ────────────────────────────────────────────────────
343
344/// A bounded scene of game objects with hierarchy support.
345/// Serializable as JSON for `.wm` scene files, editor snapshots, and replication.
346#[derive(Debug, Clone, Default, Serialize, Deserialize)]
347pub struct GameObjectScene {
348    pub name: String,
349    pub objects: Vec<GameObject>,
350    next_id: u64,
351}
352
353impl GameObjectScene {
354    pub fn new(name: String) -> Self {
355        Self {
356            name,
357            objects: Vec::new(),
358            next_id: 0,
359        }
360    }
361
362    /// Spawn an empty game object. Returns its ID.
363    pub fn spawn(&mut self, name: String) -> Result<u64, String> {
364        if self.objects.len() >= MAX_SCENE_OBJECTS {
365            return Err(format!("scene_object_limit:max {MAX_SCENE_OBJECTS}"));
366        }
367        let id = self.next_id;
368        self.next_id = self.next_id.saturating_add(1);
369        self.objects.push(GameObject::new(id, name));
370        Ok(id)
371    }
372
373    /// Spawn a game object with a primitive mesh. Returns its ID.
374    pub fn spawn_primitive(&mut self, name: String, kind: PrimitiveKind) -> Result<u64, String> {
375        if self.objects.len() >= MAX_SCENE_OBJECTS {
376            return Err(format!("scene_object_limit:max {MAX_SCENE_OBJECTS}"));
377        }
378        let id = self.next_id;
379        self.next_id = self.next_id.saturating_add(1);
380        self.objects.push(GameObject::with_primitive(id, name, kind));
381        Ok(id)
382    }
383
384    pub fn find(&self, id: u64) -> Option<&GameObject> {
385        self.objects.iter().find(|o| o.id == id)
386    }
387
388    pub fn find_mut(&mut self, id: u64) -> Option<&mut GameObject> {
389        self.objects.iter_mut().find(|o| o.id == id)
390    }
391
392    /// Despawn an object by ID. Clears dangling parent references.
393    pub fn despawn(&mut self, id: u64) -> bool {
394        let Some(pos) = self.objects.iter().position(|o| o.id == id) else {
395            return false;
396        };
397        self.objects.swap_remove(pos);
398        for obj in &mut self.objects {
399            if obj.parent_id == Some(id) {
400                obj.parent_id = None;
401            }
402        }
403        true
404    }
405
406    pub fn len(&self) -> usize {
407        self.objects.len()
408    }
409
410    pub fn is_empty(&self) -> bool {
411        self.objects.is_empty()
412    }
413
414    /// Validate entire scene.
415    pub fn validate(&self) -> Result<(), String> {
416        if self.objects.len() > MAX_SCENE_OBJECTS {
417            return Err(format!(
418                "scene_object_limit:{} > {MAX_SCENE_OBJECTS}",
419                self.objects.len()
420            ));
421        }
422        for obj in &self.objects {
423            obj.validate().map_err(|e| format!("object '{}': {}", obj.name, e))?;
424        }
425        Ok(())
426    }
427
428    /// Root objects (no parent).
429    pub fn roots(&self) -> impl Iterator<Item = &GameObject> {
430        self.objects.iter().filter(|o| o.parent_id.is_none())
431    }
432
433    /// Children of a given parent.
434    pub fn children_of(&self, parent_id: u64) -> impl Iterator<Item = &GameObject> {
435        self.objects.iter().filter(move |o| o.parent_id == Some(parent_id))
436    }
437
438    /// Propagate transforms through the parent-child hierarchy.
439    /// Each child's world matrix = parent_world * child_local.
440    /// Stores results indexed by object position in `self.objects`.
441    ///
442    /// Uses topological BFS from roots — O(N) single pass, each object
443    /// computed exactly once. Parents are always processed before children.
444    pub fn propagate_transforms(&self) -> Vec<[f32; 16]> {
445        use crate::transform::mat4_mul;
446
447        let len = self.objects.len();
448        let mut world: Vec<[f32; 16]> =
449            vec![[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,]; len];
450        if len == 0 {
451            return world;
452        }
453
454        // Build id → index lookup and children adjacency list in one pass.
455        let mut id_to_idx: std::collections::HashMap<u64, usize> = std::collections::HashMap::with_capacity(len);
456        let mut children: Vec<Vec<usize>> = vec![Vec::new(); len];
457        let mut roots: Vec<usize> = Vec::new();
458
459        for (i, obj) in self.objects.iter().enumerate() {
460            id_to_idx.insert(obj.id, i);
461        }
462        for (i, obj) in self.objects.iter().enumerate() {
463            if let Some(pid) = obj.parent_id {
464                if let Some(&pi) = id_to_idx.get(&pid) {
465                    children[pi].push(i);
466                } else {
467                    roots.push(i); // orphaned parent ref → treat as root
468                }
469            } else {
470                roots.push(i);
471            }
472        }
473
474        // BFS from roots — parents always processed before children.
475        let mut queue = std::collections::VecDeque::with_capacity(len);
476        for &ri in &roots {
477            world[ri] = self.objects[ri].transform.to_matrix();
478            queue.push_back(ri);
479        }
480
481        while let Some(idx) = queue.pop_front() {
482            for &ci in &children[idx] {
483                let local = self.objects[ci].transform.to_matrix();
484                world[ci] = mat4_mul(&world[idx], &local);
485                queue.push_back(ci);
486            }
487        }
488
489        world
490    }
491
492    /// Count of visible objects with a mesh binding.
493    pub fn visible_mesh_count(&self) -> usize {
494        self.objects
495            .iter()
496            .filter(|o| o.visible && !matches!(o.mesh, MeshBinding::None))
497            .count()
498    }
499}
500
501// ── Tests ──────────────────────────────────────────────────────────────
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    #[test]
508    fn transform_default_identity() {
509        let t = Transform::default();
510        assert_eq!(t.position, [0.0; 3]);
511        assert_eq!(t.rotation, [0.0, 0.0, 0.0, 1.0]);
512        assert_eq!(t.scale, [1.0, 1.0, 1.0]);
513    }
514
515    #[test]
516    fn transform_identity_matrix() {
517        let t = Transform::default();
518        let m = t.to_matrix();
519        #[rustfmt::skip]
520        let expected = [
521            1.0, 0.0, 0.0, 0.0,
522            0.0, 1.0, 0.0, 0.0,
523            0.0, 0.0, 1.0, 0.0,
524            0.0, 0.0, 0.0, 1.0,
525        ];
526        for (a, b) in m.iter().zip(expected.iter()) {
527            assert!((a - b).abs() < 1e-6, "matrix mismatch: {a} vs {b}");
528        }
529    }
530
531    #[test]
532    fn transform_translation_in_matrix() {
533        let t = Transform {
534            position: [1.0, 2.0, 3.0],
535            ..Default::default()
536        };
537        let m = t.to_matrix();
538        assert_eq!(m[12], 1.0);
539        assert_eq!(m[13], 2.0);
540        assert_eq!(m[14], 3.0);
541    }
542
543    #[test]
544    fn transform_scale_in_matrix() {
545        let t = Transform {
546            scale: [2.0, 3.0, 4.0],
547            ..Default::default()
548        };
549        let m = t.to_matrix();
550        assert!((m[0] - 2.0).abs() < 1e-6);
551        assert!((m[5] - 3.0).abs() < 1e-6);
552        assert!((m[10] - 4.0).abs() < 1e-6);
553    }
554
555    #[test]
556    fn transform_validate_ok() {
557        assert!(Transform::default().validate().is_ok());
558    }
559
560    #[test]
561    fn transform_validate_zero_scale() {
562        let t = Transform {
563            scale: [0.0, 1.0, 1.0],
564            ..Default::default()
565        };
566        assert!(t.validate().is_err());
567    }
568
569    #[test]
570    fn transform_validate_nan_position() {
571        let t = Transform {
572            position: [f32::NAN, 0.0, 0.0],
573            ..Default::default()
574        };
575        assert!(t.validate().is_err());
576    }
577
578    #[test]
579    fn transform_validate_non_unit_quat() {
580        let t = Transform {
581            rotation: [1.0, 1.0, 1.0, 1.0],
582            ..Default::default()
583        };
584        assert!(t.validate().is_err());
585    }
586
587    #[test]
588    fn game_object_new_defaults() {
589        let obj = GameObject::new(0, "Test".into());
590        assert_eq!(obj.id, 0);
591        assert!(obj.visible);
592        assert!(obj.components.is_empty());
593        assert_eq!(obj.mesh, MeshBinding::None);
594        assert_eq!(obj.parent_id, None);
595    }
596
597    #[test]
598    fn game_object_with_primitive() {
599        let obj = GameObject::with_primitive(1, "Cube".into(), PrimitiveKind::Cube);
600        assert!(matches!(
601            obj.mesh,
602            MeshBinding::Primitive {
603                kind: PrimitiveKind::Cube,
604                ..
605            }
606        ));
607    }
608
609    #[test]
610    fn game_object_add_remove_component() {
611        let mut obj = GameObject::new(0, "T".into());
612        obj.add_component(ComponentSlot::new(ComponentKind::Physics)).unwrap();
613        assert!(obj.has_component(ComponentKind::Physics));
614        assert!(obj.remove_component(ComponentKind::Physics));
615        assert!(!obj.has_component(ComponentKind::Physics));
616    }
617
618    #[test]
619    fn game_object_component_limit() {
620        let mut obj = GameObject::new(0, "T".into());
621        for _ in 0..MAX_COMPONENTS_PER_OBJECT {
622            obj.add_component(ComponentSlot::new(ComponentKind::Custom)).unwrap();
623        }
624        assert!(obj.add_component(ComponentSlot::new(ComponentKind::Custom)).is_err());
625    }
626
627    #[test]
628    fn component_slot_properties() {
629        let mut slot = ComponentSlot::new(ComponentKind::WaymarkScript);
630        slot.set_property("speed", serde_json::json!(5.0)).unwrap();
631        slot.set_property("health", serde_json::json!(100)).unwrap();
632        assert_eq!(slot.get_property("speed"), Some(&serde_json::json!(5.0)));
633        assert_eq!(slot.get_property("missing"), None);
634    }
635
636    #[test]
637    fn scene_spawn_and_find() {
638        let mut scene = GameObjectScene::new("Test".into());
639        let id = scene.spawn("Player".into()).unwrap();
640        assert_eq!(scene.len(), 1);
641        assert_eq!(scene.find(id).unwrap().name, "Player");
642    }
643
644    #[test]
645    fn scene_spawn_primitive() {
646        let mut scene = GameObjectScene::new("Test".into());
647        let id = scene.spawn_primitive("Cube".into(), PrimitiveKind::Cube).unwrap();
648        let obj = scene.find(id).unwrap();
649        assert!(matches!(
650            obj.mesh,
651            MeshBinding::Primitive {
652                kind: PrimitiveKind::Cube,
653                ..
654            }
655        ));
656    }
657
658    #[test]
659    fn scene_despawn_clears_parent_refs() {
660        let mut scene = GameObjectScene::new("Test".into());
661        let parent = scene.spawn("Parent".into()).unwrap();
662        let child = scene.spawn("Child".into()).unwrap();
663        scene.find_mut(child).unwrap().parent_id = Some(parent);
664        scene.despawn(parent);
665        assert_eq!(scene.find(child).unwrap().parent_id, None);
666    }
667
668    #[test]
669    fn scene_hierarchy() {
670        let mut scene = GameObjectScene::new("Test".into());
671        let p = scene.spawn("Parent".into()).unwrap();
672        let c1 = scene.spawn("C1".into()).unwrap();
673        let c2 = scene.spawn("C2".into()).unwrap();
674        scene.find_mut(c1).unwrap().parent_id = Some(p);
675        scene.find_mut(c2).unwrap().parent_id = Some(p);
676        assert_eq!(scene.roots().count(), 1);
677        assert_eq!(scene.children_of(p).count(), 2);
678    }
679
680    #[test]
681    fn scene_serialize_roundtrip() {
682        let mut scene = GameObjectScene::new("TestScene".into());
683        let id = scene.spawn_primitive("Cube".into(), PrimitiveKind::Cube).unwrap();
684        scene.find_mut(id).unwrap().transform.position = [1.0, 2.0, 3.0];
685
686        let mut slot = ComponentSlot::new(ComponentKind::WaymarkScript);
687        slot.set_property("speed", serde_json::json!(5.0)).unwrap();
688        scene.find_mut(id).unwrap().add_component(slot).unwrap();
689
690        let json = serde_json::to_string_pretty(&scene).unwrap();
691        let restored: GameObjectScene = serde_json::from_str(&json).unwrap();
692
693        assert_eq!(restored.name, "TestScene");
694        assert_eq!(restored.len(), 1);
695        assert_eq!(restored.objects[0].transform.position, [1.0, 2.0, 3.0]);
696        assert_eq!(restored.objects[0].components[0].kind, ComponentKind::WaymarkScript);
697    }
698
699    #[test]
700    fn scene_validate_ok() {
701        let mut scene = GameObjectScene::new("Ok".into());
702        scene.spawn_primitive("Cube".into(), PrimitiveKind::Cube).unwrap();
703        assert!(scene.validate().is_ok());
704    }
705
706    #[test]
707    fn scene_validate_bad_transform() {
708        let mut scene = GameObjectScene::new("Bad".into());
709        let id = scene.spawn("Bad".into()).unwrap();
710        scene.find_mut(id).unwrap().transform.scale = [0.0, 1.0, 1.0];
711        assert!(scene.validate().is_err());
712    }
713
714    #[test]
715    fn primitive_kind_all_count() {
716        assert_eq!(PrimitiveKind::ALL.len(), 9);
717    }
718
719    #[test]
720    fn mesh_binding_default_is_none() {
721        assert_eq!(MeshBinding::default(), MeshBinding::None);
722    }
723
724    #[test]
725    fn game_object_validate_name_too_long() {
726        let obj = GameObject::new(0, "x".repeat(257));
727        assert!(obj.validate().is_err());
728    }
729
730    #[test]
731    fn visible_mesh_count() {
732        let mut scene = GameObjectScene::new("Test".into());
733        scene.spawn_primitive("A".into(), PrimitiveKind::Cube).unwrap();
734        scene.spawn("Empty".into()).unwrap(); // no mesh
735        let id = scene.spawn_primitive("Hidden".into(), PrimitiveKind::Sphere).unwrap();
736        scene.find_mut(id).unwrap().visible = false;
737        assert_eq!(scene.visible_mesh_count(), 1);
738    }
739
740    #[test]
741    fn propagate_transforms_root_only() {
742        let mut scene = GameObjectScene::new("Test".into());
743        let id = scene.spawn("A".into()).unwrap();
744        scene.find_mut(id).unwrap().transform.position = [1.0, 2.0, 3.0];
745        let world = scene.propagate_transforms();
746        assert_eq!(world.len(), 1);
747        assert_eq!(world[0][12], 1.0);
748        assert_eq!(world[0][13], 2.0);
749        assert_eq!(world[0][14], 3.0);
750    }
751
752    #[test]
753    fn propagate_transforms_parent_child() {
754        let mut scene = GameObjectScene::new("Test".into());
755        let p = scene.spawn("Parent".into()).unwrap();
756        scene.find_mut(p).unwrap().transform.position = [10.0, 0.0, 0.0];
757        let c = scene.spawn("Child".into()).unwrap();
758        scene.find_mut(c).unwrap().transform.position = [5.0, 0.0, 0.0];
759        scene.find_mut(c).unwrap().parent_id = Some(p);
760
761        let world = scene.propagate_transforms();
762        // Child world position = parent(10,0,0) + child(5,0,0) = (15,0,0)
763        assert!((world[1][12] - 15.0).abs() < 1e-5);
764    }
765
766    #[test]
767    fn propagate_transforms_empty_scene() {
768        let scene = GameObjectScene::new("Empty".into());
769        let world = scene.propagate_transforms();
770        assert!(world.is_empty());
771    }
772
773    #[test]
774    fn next_id_monotonic() {
775        let mut scene = GameObjectScene::new("Test".into());
776        let a = scene.spawn("A".into()).unwrap();
777        let b = scene.spawn("B".into()).unwrap();
778        assert_eq!(a, 0);
779        assert_eq!(b, 1);
780        scene.despawn(a);
781        let c = scene.spawn("C".into()).unwrap();
782        assert_eq!(c, 2); // never reuses
783    }
784}