concinnity_engine/ecs/
schedule.rs1use crate::ecs::World;
12
13pub(crate) fn overlay(world: &World) -> Option<crate::gfx::overlay::OverlaySystem> {
17 world
18 .query::<crate::components::GraphicsConfig>()
19 .next()
20 .map(|_| crate::gfx::overlay::OverlaySystem::new())
21}
22
23pub(crate) fn behavior(world: &World) -> Option<concinnity_core::behavior::BehaviorSystem> {
28 world
29 .query::<crate::components::Behavior>()
30 .next()
31 .map(|_| crate::behavior::build(crate::ecs::state_tree(world)))
32}
33
34pub(crate) fn spawn(world: &World) -> Option<crate::spawn::SpawnSystem> {
39 world
40 .query::<crate::components::GraphicsConfig>()
41 .next()
42 .map(|_| crate::spawn::SpawnSystem::new())
43}
44
45pub(crate) fn settings(world: &World) -> Option<crate::gfx::settings_system::SettingsSystem> {
50 world
51 .query::<crate::components::GraphicsConfig>()
52 .next()
53 .map(|_| crate::gfx::settings_system::SettingsSystem::new())
54}
55
56pub(crate) fn streaming(world: &World) -> Option<crate::gfx::streaming_system::StreamingSystem> {
61 world
62 .query::<crate::components::GraphicsConfig>()
63 .next()
64 .map(|_| crate::gfx::streaming_system::StreamingSystem::new())
65}
66
67pub(crate) fn graphics(world: &World) -> Option<crate::gfx::graphics_system::GraphicsSystem> {
70 world
71 .query::<crate::components::GraphicsConfig>()
72 .next()
73 .map(|_| crate::gfx::graphics_system::GraphicsSystem::new(crate::ecs::state_tree(world)))
74}
75
76pub(crate) fn input(world: &World) -> Option<crate::gfx::input_system::InputSystem> {
81 world
82 .query::<crate::components::GraphicsConfig>()
83 .next()
84 .map(|_| crate::gfx::input_system::InputSystem::new())
85}
86
87pub(crate) fn stat_hud(world: &World) -> Option<crate::hud::stat_hud::StatHudSystem> {
90 world
91 .query::<crate::components::StatHud>()
92 .next()
93 .cloned()
94 .map(crate::hud::stat_hud::StatHudSystem::new)
95}
96
97pub(crate) fn debug_hud(world: &World) -> Option<crate::hud::debug_hud::DebugHudSystem> {
103 if !(cfg!(debug_assertions) || crate::app::dev_flags::enabled()) {
104 return None;
105 }
106 world
107 .query::<crate::components::DebugHud>()
108 .next()
109 .cloned()
110 .map(crate::hud::debug_hud::DebugHudSystem::new)
111}
112
113pub(crate) fn loading_overlay(
116 world: &World,
117) -> Option<crate::hud::loading_overlay::LoadingOverlaySystem> {
118 world
119 .query::<crate::components::LoadingOverlay>()
120 .next()
121 .cloned()
122 .map(crate::hud::loading_overlay::LoadingOverlaySystem::new)
123}
124
125pub(crate) fn physics(world: &World) -> Option<concinnity_core::physics::PhysicsSystem> {
131 let needs = world
132 .query::<crate::components::PhysicsConfig>()
133 .next()
134 .is_some()
135 || world.query::<crate::components::RigidBody>().next().is_some()
136 || world.query::<crate::components::PropBody>().next().is_some()
137 || world
138 .query::<crate::components::TriggerVolume>()
139 .next()
140 .is_some()
141 || world
145 .resource::<crate::resource::SkinnedMeshTable>()
146 .is_some_and(|t| t.has_capsule());
147 if !needs {
148 return None;
149 }
150 let config = world
154 .query::<crate::components::PhysicsConfig>()
155 .next()
156 .cloned()
157 .unwrap_or_default();
158 Some(crate::physics::build(config))
159}
160
161pub(crate) fn camera3d(world: &World) -> Option<crate::gfx::camera_controller::Camera3DSystem> {
166 let ctrl = controlled_camera(world)?;
167 ctrl.follow
168 .is_none()
169 .then(|| crate::gfx::camera_controller::Camera3DSystem::new(ctrl))
170}
171
172pub(crate) fn third_person(world: &World) -> Option<crate::gfx::third_person::ThirdPersonSystem> {
175 let ctrl = controlled_camera(world)?;
176 ctrl.follow
177 .is_some()
178 .then(|| crate::gfx::third_person::ThirdPersonSystem::new(&ctrl))
179}
180
181fn controlled_camera(world: &World) -> Option<crate::components::CameraController> {
182 world
183 .query::<crate::components::Camera3D>()
184 .find_map(|c| c.controller.clone())
185}
186
187pub(crate) fn fps_counter(world: &World) -> Option<crate::hud::fps_counter::FpsCounterSystem> {
190 world
191 .query::<crate::components::FpsCounter>()
192 .next()
193 .cloned()
194 .map(crate::hud::fps_counter::FpsCounterSystem::new)
195}
196
197pub(crate) fn animation(world: &World) -> Option<crate::gfx::animation::AnimationSystem> {
202 let declared = world
203 .query::<crate::components::Animation>()
204 .next()
205 .is_some()
206 || world
207 .query::<crate::components::AnimationGraph>()
208 .next()
209 .is_some();
210 declared.then(crate::gfx::animation::AnimationSystem::new)
211}
212
213pub(crate) fn story(world: &World) -> Option<crate::story::StorySystem> {
218 world
219 .query::<crate::components::Story>()
220 .next()
221 .cloned()
222 .map(|story| crate::story::StorySystem::new(story, crate::ecs::state_tree(world)))
223}
224
225pub(crate) fn audio(world: &World) -> Option<crate::audio::AudioSystem> {
230 let needs = world
231 .query::<crate::components::AudioEmitter>()
232 .next()
233 .is_some()
234 || world
235 .query::<crate::components::AudioCue>()
236 .next()
237 .is_some()
238 || world
239 .query::<crate::components::Story>()
240 .next()
241 .is_some_and(|s| {
242 s.nodes.iter().any(|n| {
243 n.choice_music.is_some()
244 || !n.choice_sounds.is_empty()
245 || n.pages
246 .iter()
247 .any(|p| p.music.is_some() || !p.sounds.is_empty())
248 })
249 })
250 || world
251 .query::<crate::components::Behavior>()
252 .any(crate::components::Behavior::plays_sound);
253 if !needs {
254 return None;
255 }
256 let audio = crate::config::Settings::load(crate::ecs::state_tree(world)).audio;
260 Some(crate::audio::AudioSystem::new(crate::audio::AudioVolumes {
261 master: audio.master_volume,
262 music: audio.music_volume,
263 sfx: audio.sfx_volume,
264 voice: audio.voice_volume,
265 }))
266}
267
268pub(crate) fn ui_input(world: &World) -> Option<crate::ui::UiInputSystem> {
271 let needs = world
272 .query::<crate::components::HitRegion>()
273 .next()
274 .is_some()
275 || world.query::<crate::components::Screen>().next().is_some()
276 || world
277 .query::<crate::components::KeyBinding>()
278 .next()
279 .is_some();
280 needs.then(crate::ui::UiInputSystem::new)
281}
282
283pub(crate) fn text_input(world: &World) -> Option<crate::text_input_system::TextInputSystem> {
287 world
288 .query::<crate::components::TextInput>()
289 .next()
290 .map(|_| crate::text_input_system::TextInputSystem::new())
291}
292
293#[cfg(test)]
294mod tests {
295 use crate::components::{Camera3D, CameraController, PhysicsConfig, RigidBody};
296 use crate::ecs::{SYSTEMS, World};
297
298 fn controlled_camera() -> Camera3D {
299 Camera3D {
300 fov_y_degrees: 75.0,
301 near: 0.05,
302 far: 200.0,
303 view_matrix: [[0.0; 4]; 4],
304 position: [0.0, 1.0, 0.0],
305 yaw: 0.0,
306 pitch: 0.0,
307 desired_move: [0.0; 3],
308 jump_requested: false,
309 interact_requested: false,
310 controller: Some(CameraController::default()),
311 }
312 }
313
314 #[test]
316 fn physics_config_spawns_internal_system() {
317 let mut world = World::new();
318 world.add_component(PhysicsConfig::default());
319 world.start(SYSTEMS).unwrap();
320 let names: Vec<&str> = world.systems().iter().map(|s| s.name()).collect();
321 assert_eq!(names, ["PhysicsSystem"]);
322 }
323
324 #[test]
326 fn rigid_body_spawns_internal_system() {
327 let mut world = World::new();
328 world.add_component(RigidBody::default());
329 world.start(SYSTEMS).unwrap();
330 let names: Vec<&str> = world.systems().iter().map(|s| s.name()).collect();
331 assert_eq!(names, ["PhysicsSystem"]);
332 }
333
334 #[test]
336 fn no_physics_content_no_system() {
337 let mut world = World::new();
338 world.start(SYSTEMS).unwrap();
339 assert!(world.systems().is_empty());
340 }
341
342 #[test]
345 fn physics_runs_before_camera_controller() {
346 let mut world = World::new();
347 world.add_component(PhysicsConfig::default());
348 world.add_component(controlled_camera());
349 world.start(SYSTEMS).unwrap();
350 let names: Vec<&str> = world.systems().iter().map(|s| s.name()).collect();
351 assert_eq!(names, ["PhysicsSystem", "Camera3DSystem"]);
352 }
353
354 #[test]
357 fn audio_emitter_spawns_internal_system() {
358 let mut world = World::new();
359 world.add_component(crate::components::AudioEmitter::default());
360 world.start(SYSTEMS).unwrap();
361
362 let names: Vec<&str> = world.systems().iter().map(|s| s.name()).collect();
363 assert_eq!(names, ["AudioSystem"]);
364 }
365
366 #[test]
368 fn no_audio_emitter_means_no_system() {
369 let mut world = World::new();
370 world.start(SYSTEMS).unwrap();
371 assert!(world.systems().is_empty());
372 }
373
374 #[test]
377 fn audio_cue_spawns_internal_system() {
378 let mut world = World::new();
379 world.add_component(crate::components::AudioCue::default());
380 world.start(SYSTEMS).unwrap();
381
382 let names: Vec<&str> = world.systems().iter().map(|s| s.name()).collect();
383 assert!(names.contains(&"AudioSystem"), "{names:?}");
384 }
385
386 #[test]
391 fn initial_view_fires_its_cue() {
392 use crate::components::{AudioCue, Screen};
393 use crate::ecs::AudioClipHandle;
394 use crate::ecs::asset_id::AssetId;
395
396 let mut world = World::new();
397 let screen = AssetId(90);
398 world.add_component(Screen {
402 asset_id: screen,
403 initial: true,
404 fade_in_secs: 0.0,
405 ..Default::default()
406 });
407 world.add_component(AudioCue {
408 screen: Some(screen),
409 clip: Some(AudioClipHandle(0)),
410 ..Default::default()
411 });
412 world.start(SYSTEMS).unwrap();
413 world.step();
414
415 let matched = world
416 .systems()
417 .iter()
418 .find_map(|s| s.downcast_ref::<crate::audio::AudioSystem>())
419 .map(|a| a.cues_matched())
420 .expect("world has an AudioSystem");
421 assert_eq!(matched, 1, "the initial screen's cue should have matched");
422 }
423}