Skip to main content

concinnity_core/components/
room.rs

1// src/components/room.rs
2//
3// Runtime `Room` component. Its authored args live in the schema crate
4// (concinnity_asset::room).
5
6use concinnity_asset::cook;
7
8use crate::ecs::asset_id::AssetId;
9use crate::ecs::{Component, PayloadLocator, TextureHandle};
10
11/// A self-contained room (floor, ceiling, four walls), with optional texturing.
12///
13/// Prefer `Room` over a [ProceduralMesh](#proceduralmesh) (generator `"room"`) +
14/// [Prop](#prop) pair for a shorter declaration. The room is placed at the world
15/// origin.
16///
17/// Dimensions can be given as `size: [width, depth, height]` (full extents) or
18/// as `half_width`, `half_depth`, and `ceiling_height` individually.
19///
20/// `texture`, `wall_texture`, `floor_texture`, and `ceiling_texture` are checked
21/// in that order; the first set value wins. Generator names such as `"brick"` or
22/// `"concrete"` resolve to a matching [Texture](#texture) at build time.
23#[derive(Debug, serde::Serialize, serde::Deserialize)]
24pub struct Room {
25    /// Assigned by the loader; not authored.
26    pub asset_id: AssetId,
27    /// Half the room's width in world units.
28    pub half_width: f32,
29    /// Half the room's depth in world units.
30    pub half_depth: f32,
31    /// Floor-to-ceiling height in world units.
32    pub ceiling_height: f32,
33    /// Texture applied to every surface unless a surface overrides it.
34    pub texture: Option<TextureHandle>,
35    /// Texture for the four walls.
36    pub wall_texture: Option<TextureHandle>,
37    /// Texture for the floor.
38    pub floor_texture: Option<TextureHandle>,
39    /// Texture for the ceiling.
40    pub ceiling_texture: Option<TextureHandle>,
41    /// The generated geometry's place in the blob, injected at load.
42    pub locator: Option<PayloadLocator>,
43}
44
45impl Room {
46    /// Returns the first set texture reference across all texture fields.
47    pub fn effective_texture(&self) -> Option<TextureHandle> {
48        [
49            self.texture,
50            self.wall_texture,
51            self.floor_texture,
52            self.ceiling_texture,
53        ]
54        .into_iter()
55        .flatten()
56        .next()
57    }
58}
59
60impl Room {
61    /// Translate the authored args into the runtime room: resolve the `size`
62    /// shorthand into half extents. Run by cook at build time (the baked blob
63    /// record carries the result).
64    pub fn bake(args: cook::Room) -> Self {
65        let (half_width, half_depth, ceiling_height) = if let Some([w, d, h]) = args.size {
66            (w / 2.0, d / 2.0, h)
67        } else {
68            (args.half_width, args.half_depth, args.ceiling_height)
69        };
70        Self {
71            asset_id: AssetId::default(),
72            half_width,
73            half_depth,
74            ceiling_height,
75            texture: args.texture,
76            wall_texture: args.wall_texture,
77            floor_texture: args.floor_texture,
78            ceiling_texture: args.ceiling_texture,
79            locator: None,
80        }
81    }
82}
83
84impl Component for Room {
85    const NAME: &'static str = "Room";
86
87    fn from_baked(bytes: &[u8]) -> Result<Self, crate::result::CnResult> {
88        Ok(crate::blob::decode_exact(bytes)?)
89    }
90
91    fn inject_locator(&mut self, locator: PayloadLocator) {
92        self.locator = Some(locator);
93    }
94
95    fn inject_name(&mut self, id: AssetId) {
96        self.asset_id = id;
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn effective_texture_returns_texture_field_first() {
106        let room = Room {
107            asset_id: AssetId::default(),
108            half_width: 8.0,
109            half_depth: 10.0,
110            ceiling_height: 3.5,
111            texture: Some(TextureHandle(1)),
112            wall_texture: Some(TextureHandle(2)),
113            floor_texture: None,
114            ceiling_texture: None,
115            locator: None,
116        };
117        assert_eq!(room.effective_texture(), Some(TextureHandle(1)));
118    }
119
120    #[test]
121    fn effective_texture_falls_back_to_wall_texture() {
122        let room = Room {
123            asset_id: AssetId::default(),
124            half_width: 8.0,
125            half_depth: 10.0,
126            ceiling_height: 3.5,
127            texture: None,
128            wall_texture: Some(TextureHandle(7)),
129            floor_texture: None,
130            ceiling_texture: None,
131            locator: None,
132        };
133        assert_eq!(room.effective_texture(), Some(TextureHandle(7)));
134    }
135
136    #[test]
137    fn effective_texture_returns_none_when_all_unset() {
138        let room = Room::bake(cook::Room::default());
139        assert_eq!(room.effective_texture(), None);
140    }
141
142    #[test]
143    fn from_args_resolves_size_shorthand() {
144        let args = cook::Room {
145            size: Some([16.0, 20.0, 3.5]),
146            ..cook::Room::default()
147        };
148        let room = Room::bake(args);
149        assert_eq!(room.half_width, 8.0);
150        assert_eq!(room.half_depth, 10.0);
151        assert_eq!(room.ceiling_height, 3.5);
152    }
153
154    #[test]
155    fn from_args_uses_explicit_half_extents_when_no_size() {
156        let args = cook::Room {
157            half_width: 5.0,
158            half_depth: 7.0,
159            ceiling_height: 4.0,
160            size: None,
161            ..cook::Room::default()
162        };
163        let room = Room::bake(args);
164        assert_eq!(room.half_width, 5.0);
165        assert_eq!(room.half_depth, 7.0);
166        assert_eq!(room.ceiling_height, 4.0);
167    }
168}