Skip to main content

concinnity_world/schema/
prefab.rs

1//! Prefab schema: a reusable template of props / lights / nested prefabs.
2
3use concinnity_core::components::PropCollider;
4
5/// A reusable template of [Prop](#prop)s, [PointLight](#pointlight)s, and nested
6/// prefabs.
7///
8/// Placed as a unit at a world-space transform. Add a `prefab` field to a
9/// [Prop](#prop) to instantiate it; each instance expands into concrete assets
10/// positioned relative to the instance's transform.
11///
12/// **Expanded asset names:** `<instance_name>_<entry_name>` (nested:
13/// `<instance>_<outer>_<inner>`).
14///
15/// **Instantiation:** add a `prefab` field to a [Prop](#prop). The prop's other
16/// fields (`position`, `rotation_deg`, `scale`) act as the instance's world
17/// transform.
18///
19/// **Library presets** (JSON files in `assets/prefabs/`):
20#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
21#[serde(default)]
22pub struct Prefab {
23    /// Ordered list of entries. Each is a prop, a point light, or a nested
24    /// prefab (selected by `kind`), placed relative to the instance transform.
25    pub props: Vec<PrefabEntry>,
26}
27
28/// Which kind of asset a [PrefabEntry] expands into.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
30#[serde(rename_all = "snake_case")]
31#[derive(Default)]
32pub enum PrefabKind {
33    /// A [Prop](#prop) built from the entry's `model` / `mesh` / `material` /
34    /// `texture` and transform fields.
35    #[default]
36    Prop,
37    /// A [PointLight](#pointlight) built from the entry's `light_*` fields at the
38    /// entry's `position`.
39    PointLight,
40    /// A nested prefab named by the entry's `prefab` field, expanded relative to
41    /// this entry's transform.
42    Prefab,
43}
44
45/// One entry in a [Prefab]'s `props` list. The fields consulted depend on
46/// `kind`: a `prop` uses the render / collision / transform fields, a
47/// `point_light` uses the `light_*` fields, and a `prefab` uses `prefab`. Names
48/// in `model` / `mesh` / `material` / `texture` / `parent` / `prefab` are
49/// unresolved references to other assets, resolved when the entry expands.
50#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
51#[serde(default)]
52pub struct PrefabEntry {
53    /// Entry name; the expanded asset is named `<instance>_<name>`.
54    pub name: String,
55    /// Which asset this entry expands into.
56    pub kind: PrefabKind,
57    /// Local position relative to the instance transform.
58    pub position: [f32; 3],
59    /// Local rotation, Euler degrees [pitch, yaw, roll], YXZ order.
60    pub rotation_deg: [f32; 3],
61    /// Local scale.
62    pub scale: [f32; 3],
63    /// `prop`: [Model](#model) name.
64    #[serde(skip_serializing_if = "String::is_empty")]
65    pub model: String,
66    /// `prop`: [Mesh](#mesh) / [ProceduralMesh](#proceduralmesh) name.
67    #[serde(skip_serializing_if = "String::is_empty")]
68    pub mesh: String,
69    /// `prop`: [Material](#material) name.
70    #[serde(skip_serializing_if = "String::is_empty")]
71    pub material: String,
72    /// `prop`: [Texture](#texture) name (older path; `material` takes priority).
73    #[serde(skip_serializing_if = "String::is_empty")]
74    pub texture: String,
75    /// `prop`: parent asset name for the expanded prop.
76    #[serde(skip_serializing_if = "String::is_empty")]
77    pub parent: String,
78    /// `prop`: optional collision shape for the expanded prop.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub collider: Option<PropCollider>,
81    /// `prop`: whether the expanded prop is interactable.
82    pub interactable: bool,
83    /// `prop`: whether the expanded prop is a pickup.
84    pub pickup: bool,
85    /// `point_light`: linear-space RGB colour.
86    pub light_color: [f32; 3],
87    /// `point_light`: intensity multiplier.
88    pub light_intensity: f32,
89    /// `point_light`: maximum reach in world units.
90    pub light_range: f32,
91    /// `prefab`: name of another [Prefab] to expand at this entry's transform.
92    #[serde(skip_serializing_if = "String::is_empty")]
93    pub prefab: String,
94}
95
96impl Default for PrefabEntry {
97    fn default() -> Self {
98        Self {
99            name: String::new(),
100            kind: PrefabKind::Prop,
101            position: [0.0, 0.0, 0.0],
102            rotation_deg: [0.0, 0.0, 0.0],
103            scale: [1.0, 1.0, 1.0],
104            model: String::new(),
105            mesh: String::new(),
106            material: String::new(),
107            texture: String::new(),
108            parent: String::new(),
109            collider: None,
110            interactable: false,
111            pickup: false,
112            light_color: [1.0, 1.0, 1.0],
113            light_intensity: 8.0,
114            light_range: 6.0,
115            prefab: String::new(),
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn each_kind_deserialises_with_its_fields() {
126        let prop: PrefabEntry = serde_json::from_str(
127            r#"{"name":"table","kind":"prop","model":"model_table","position":[1.0,0.0,2.0]}"#,
128        )
129        .unwrap();
130        assert_eq!(prop.kind, PrefabKind::Prop);
131        assert_eq!(prop.name, "table");
132        assert_eq!(prop.model, "model_table");
133        assert_eq!(prop.position, [1.0, 0.0, 2.0]);
134        // Omitted scale falls back to unit.
135        assert_eq!(prop.scale, [1.0, 1.0, 1.0]);
136
137        let light: PrefabEntry =
138            serde_json::from_str(r#"{"name":"lamp","kind":"point_light","light_intensity":5.0}"#)
139                .unwrap();
140        assert_eq!(light.kind, PrefabKind::PointLight);
141        assert_eq!(light.light_intensity, 5.0);
142        // Omitted light fields fall back to the point-light defaults.
143        assert_eq!(light.light_range, 6.0);
144        assert_eq!(light.light_color, [1.0, 1.0, 1.0]);
145
146        let nested: PrefabEntry =
147            serde_json::from_str(r#"{"name":"inner","kind":"prefab","prefab":"other"}"#).unwrap();
148        assert_eq!(nested.kind, PrefabKind::Prefab);
149        assert_eq!(nested.prefab, "other");
150    }
151
152    #[test]
153    fn kind_defaults_to_prop_when_omitted() {
154        let e: PrefabEntry = serde_json::from_str(r#"{"name":"x","mesh":"box"}"#).unwrap();
155        assert_eq!(e.kind, PrefabKind::Prop);
156        assert_eq!(e.mesh, "box");
157    }
158}