concinnity_core/components/
room.rs1use crate::ecs::TextureHandle;
7use crate::ecs::asset_id::AssetId;
8use crate::ecs::de_opt_texture_handle;
9use crate::ecs::{Component, PayloadLocator};
10use alloc::vec::Vec;
11
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
23#[serde(default)]
24pub struct RoomArgs {
25 pub half_width: f32,
27 pub half_depth: f32,
29 pub ceiling_height: f32,
31 pub size: Option<[f32; 3]>,
34 #[serde(deserialize_with = "de_opt_texture_handle")]
38 pub texture: Option<TextureHandle>,
39 #[serde(deserialize_with = "de_opt_texture_handle")]
42 pub wall_texture: Option<TextureHandle>,
43 #[serde(deserialize_with = "de_opt_texture_handle")]
45 pub floor_texture: Option<TextureHandle>,
46 #[serde(deserialize_with = "de_opt_texture_handle")]
48 pub ceiling_texture: Option<TextureHandle>,
49 pub lod_levels: u32,
52 #[serde(default)]
55 pub lod_distances: Vec<f32>,
56}
57
58impl Default for RoomArgs {
59 fn default() -> Self {
60 Self {
61 half_width: 8.0,
62 half_depth: 10.0,
63 ceiling_height: 3.5,
64 size: None,
65 texture: None,
66 wall_texture: None,
67 floor_texture: None,
68 ceiling_texture: None,
69 lod_levels: 1,
70 lod_distances: Vec::new(),
71 }
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn a_blank_room_is_an_untextured_box_at_the_default_dimensions() {
81 let r = RoomArgs::default();
82 assert_eq!(r.half_width, 8.0);
83 assert_eq!(r.half_depth, 10.0);
84 assert_eq!(r.ceiling_height, 3.5);
85 assert_eq!(r.size, None);
87 assert!(r.texture.is_none());
88 assert!(r.wall_texture.is_none());
89 assert!(r.floor_texture.is_none());
90 assert!(r.ceiling_texture.is_none());
91 assert_eq!(r.lod_levels, 1);
92 assert!(r.lod_distances.is_empty());
93 }
94
95 #[test]
96 fn each_surface_takes_its_own_texture_and_falls_back_to_the_shared_one() {
97 crate::test_support::install_resolvers();
98 let r: RoomArgs = serde_json::from_str(
99 r#"{"texture":"tex_base","wall_texture":"tex_brick","floor_texture":"tex_stone"}"#,
100 )
101 .unwrap();
102 assert_eq!(r.texture, Some(TextureHandle(8)));
103 assert_eq!(r.wall_texture, Some(TextureHandle(9)));
104 assert_eq!(r.floor_texture, Some(TextureHandle(9)));
105 assert_eq!(r.ceiling_texture, None);
107 }
108
109 #[test]
110 fn an_authored_room_round_trips_through_postcard() {
111 let r: RoomArgs =
112 serde_json::from_str(r#"{"size":[20,4,30],"lod_levels":2,"lod_distances":[25]}"#)
113 .unwrap();
114 assert_eq!(r.size, Some([20.0, 4.0, 30.0]));
115
116 let bytes = postcard::to_allocvec(&r).unwrap();
117 let back: RoomArgs = postcard::from_bytes(&bytes).unwrap();
118 assert_eq!(back.size, Some([20.0, 4.0, 30.0]));
119 assert_eq!(back.lod_levels, 2);
120 assert_eq!(back.lod_distances, [25.0]);
121 assert_eq!(back.half_width, 8.0);
123 }
124}
125
126#[derive(Debug, serde::Serialize, serde::Deserialize)]
139pub struct Room {
140 pub asset_id: AssetId,
142 pub half_width: f32,
144 pub half_depth: f32,
146 pub ceiling_height: f32,
148 pub texture: Option<TextureHandle>,
150 pub wall_texture: Option<TextureHandle>,
152 pub floor_texture: Option<TextureHandle>,
154 pub ceiling_texture: Option<TextureHandle>,
156 pub locator: Option<PayloadLocator>,
158}
159
160impl Room {
161 pub fn effective_texture(&self) -> Option<TextureHandle> {
163 [
164 self.texture,
165 self.wall_texture,
166 self.floor_texture,
167 self.ceiling_texture,
168 ]
169 .into_iter()
170 .flatten()
171 .next()
172 }
173}
174
175impl Room {
176 pub fn bake(args: RoomArgs) -> Self {
180 let (half_width, half_depth, ceiling_height) = if let Some([w, d, h]) = args.size {
181 (w / 2.0, d / 2.0, h)
182 } else {
183 (args.half_width, args.half_depth, args.ceiling_height)
184 };
185 Self {
186 asset_id: AssetId::default(),
187 half_width,
188 half_depth,
189 ceiling_height,
190 texture: args.texture,
191 wall_texture: args.wall_texture,
192 floor_texture: args.floor_texture,
193 ceiling_texture: args.ceiling_texture,
194 locator: None,
195 }
196 }
197}
198
199impl Component for Room {
200 const NAME: &'static str = "Room";
201
202 fn from_baked(bytes: &[u8]) -> Result<Self, crate::result::CnResult> {
203 Ok(crate::blob::decode_exact(bytes)?)
204 }
205
206 fn inject_locator(&mut self, locator: PayloadLocator) {
207 self.locator = Some(locator);
208 }
209
210 fn inject_name(&mut self, id: AssetId) {
211 self.asset_id = id;
212 }
213}
214
215#[cfg(test)]
216mod runtime_tests {
217 use super::*;
218
219 #[test]
220 fn effective_texture_returns_texture_field_first() {
221 let room = Room {
222 asset_id: AssetId::default(),
223 half_width: 8.0,
224 half_depth: 10.0,
225 ceiling_height: 3.5,
226 texture: Some(TextureHandle(1)),
227 wall_texture: Some(TextureHandle(2)),
228 floor_texture: None,
229 ceiling_texture: None,
230 locator: None,
231 };
232 assert_eq!(room.effective_texture(), Some(TextureHandle(1)));
233 }
234
235 #[test]
236 fn effective_texture_falls_back_to_wall_texture() {
237 let room = Room {
238 asset_id: AssetId::default(),
239 half_width: 8.0,
240 half_depth: 10.0,
241 ceiling_height: 3.5,
242 texture: None,
243 wall_texture: Some(TextureHandle(7)),
244 floor_texture: None,
245 ceiling_texture: None,
246 locator: None,
247 };
248 assert_eq!(room.effective_texture(), Some(TextureHandle(7)));
249 }
250
251 #[test]
252 fn effective_texture_returns_none_when_all_unset() {
253 let room = Room::bake(RoomArgs::default());
254 assert_eq!(room.effective_texture(), None);
255 }
256
257 #[test]
258 fn from_args_resolves_size_shorthand() {
259 let args = RoomArgs {
260 size: Some([16.0, 20.0, 3.5]),
261 ..RoomArgs::default()
262 };
263 let room = Room::bake(args);
264 assert_eq!(room.half_width, 8.0);
265 assert_eq!(room.half_depth, 10.0);
266 assert_eq!(room.ceiling_height, 3.5);
267 }
268
269 #[test]
270 fn from_args_uses_explicit_half_extents_when_no_size() {
271 let args = RoomArgs {
272 half_width: 5.0,
273 half_depth: 7.0,
274 ceiling_height: 4.0,
275 size: None,
276 ..RoomArgs::default()
277 };
278 let room = Room::bake(args);
279 assert_eq!(room.half_width, 5.0);
280 assert_eq!(room.half_depth, 7.0);
281 assert_eq!(room.ceiling_height, 4.0);
282 }
283}