jugar_core/
ecs.rs

1//! Entity-Component-System implementation
2//!
3//! A lightweight, cache-friendly ECS designed for WASM targets.
4//! Follows Data-Oriented Design principles for optimal performance.
5
6use core::any::{Any, TypeId};
7use core::fmt;
8use std::collections::HashMap;
9
10use serde::{Deserialize, Serialize};
11
12use crate::{CoreError, Result};
13
14/// Unique identifier for an entity in the world.
15///
16/// Entities are lightweight handles - just a generation-tagged index.
17/// Components are stored separately in contiguous arrays.
18#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct Entity(pub u64);
20
21impl Entity {
22    /// Creates a new entity with the given ID
23    #[must_use]
24    pub const fn new(id: u64) -> Self {
25        Self(id)
26    }
27
28    /// Returns the raw ID of this entity
29    #[must_use]
30    pub const fn id(self) -> u64 {
31        self.0
32    }
33}
34
35impl fmt::Debug for Entity {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "Entity({})", self.0)
38    }
39}
40
41impl fmt::Display for Entity {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        write!(f, "e{}", self.0)
44    }
45}
46
47/// Storage for a single component type
48struct ComponentStorage {
49    data: HashMap<Entity, Box<dyn Any + Send + Sync>>,
50}
51
52impl ComponentStorage {
53    fn new() -> Self {
54        Self {
55            data: HashMap::new(),
56        }
57    }
58
59    fn insert<T: Any + Send + Sync>(&mut self, entity: Entity, component: T) {
60        let _ = self.data.insert(entity, Box::new(component));
61    }
62
63    fn get<T: Any>(&self, entity: Entity) -> Option<&T> {
64        self.data.get(&entity).and_then(|c| c.downcast_ref())
65    }
66
67    fn get_mut<T: Any>(&mut self, entity: Entity) -> Option<&mut T> {
68        self.data.get_mut(&entity).and_then(|c| c.downcast_mut())
69    }
70
71    fn remove(&mut self, entity: Entity) -> bool {
72        self.data.remove(&entity).is_some()
73    }
74
75    fn contains(&self, entity: Entity) -> bool {
76        self.data.contains_key(&entity)
77    }
78}
79
80/// The game world containing all entities and their components.
81///
82/// # Example
83///
84/// ```
85/// use jugar_core::{World, Entity, Position, Velocity};
86///
87/// let mut world = World::new();
88/// let entity = world.spawn();
89///
90/// world.add_component(entity, Position::new(0.0, 0.0));
91/// world.add_component(entity, Velocity::new(1.0, 0.0));
92///
93/// // Query and update
94/// if let Some(pos) = world.get_component_mut::<Position>(entity) {
95///     pos.x += 10.0;
96/// }
97/// ```
98pub struct World {
99    next_entity_id: u64,
100    entities: Vec<Entity>,
101    components: HashMap<TypeId, ComponentStorage>,
102}
103
104impl Default for World {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl World {
111    /// Creates a new empty world
112    #[must_use]
113    pub fn new() -> Self {
114        Self {
115            next_entity_id: 0,
116            entities: Vec::new(),
117            components: HashMap::new(),
118        }
119    }
120
121    /// Spawns a new entity and returns its handle
122    pub fn spawn(&mut self) -> Entity {
123        let entity = Entity::new(self.next_entity_id);
124        self.next_entity_id += 1;
125        self.entities.push(entity);
126        entity
127    }
128
129    /// Despawns an entity and removes all its components
130    ///
131    /// # Errors
132    ///
133    /// Returns `CoreError::EntityNotFound` if the entity doesn't exist.
134    pub fn despawn(&mut self, entity: Entity) -> Result<()> {
135        let idx = self
136            .entities
137            .iter()
138            .position(|&e| e == entity)
139            .ok_or(CoreError::EntityNotFound(entity))?;
140
141        let _ = self.entities.swap_remove(idx);
142
143        // Remove all components for this entity
144        for storage in self.components.values_mut() {
145            let _ = storage.remove(entity);
146        }
147
148        Ok(())
149    }
150
151    /// Adds a component to an entity
152    ///
153    /// If the entity already has this component type, it is replaced.
154    pub fn add_component<T: Any + Send + Sync>(&mut self, entity: Entity, component: T) {
155        let type_id = TypeId::of::<T>();
156        self.components
157            .entry(type_id)
158            .or_insert_with(ComponentStorage::new)
159            .insert(entity, component);
160    }
161
162    /// Gets a reference to a component on an entity
163    #[must_use]
164    pub fn get_component<T: Any>(&self, entity: Entity) -> Option<&T> {
165        let type_id = TypeId::of::<T>();
166        self.components.get(&type_id).and_then(|s| s.get(entity))
167    }
168
169    /// Gets a mutable reference to a component on an entity
170    pub fn get_component_mut<T: Any>(&mut self, entity: Entity) -> Option<&mut T> {
171        let type_id = TypeId::of::<T>();
172        self.components
173            .get_mut(&type_id)
174            .and_then(|s| s.get_mut(entity))
175    }
176
177    /// Checks if an entity has a specific component
178    #[must_use]
179    pub fn has_component<T: Any>(&self, entity: Entity) -> bool {
180        let type_id = TypeId::of::<T>();
181        self.components
182            .get(&type_id)
183            .is_some_and(|s| s.contains(entity))
184    }
185
186    /// Removes a component from an entity
187    ///
188    /// Returns true if the component was removed, false if it didn't exist.
189    pub fn remove_component<T: Any>(&mut self, entity: Entity) -> bool {
190        let type_id = TypeId::of::<T>();
191        self.components
192            .get_mut(&type_id)
193            .is_some_and(|s| s.remove(entity))
194    }
195
196    /// Returns the number of entities in the world
197    #[must_use]
198    pub fn entity_count(&self) -> usize {
199        self.entities.len()
200    }
201
202    /// Returns an iterator over all entities
203    pub fn entities(&self) -> impl Iterator<Item = Entity> + '_ {
204        self.entities.iter().copied()
205    }
206
207    /// Checks if an entity exists in the world
208    #[must_use]
209    pub fn contains(&self, entity: Entity) -> bool {
210        self.entities.contains(&entity)
211    }
212
213    // ========================================================================
214    // Probar introspection helpers (always available, but mainly used with feature)
215    // ========================================================================
216
217    /// Returns the number of registered component types
218    ///
219    /// This is used by the probar introspection module.
220    #[must_use]
221    pub fn component_type_count(&self) -> usize {
222        self.components.len()
223    }
224
225    /// Internal method for probar feature - same as component_type_count
226    #[must_use]
227    #[doc(hidden)]
228    pub fn component_type_count_internal(&self) -> usize {
229        self.components.len()
230    }
231
232    /// Returns the number of components attached to an entity
233    ///
234    /// This is used by the probar introspection module.
235    #[must_use]
236    pub fn entity_component_count_internal(&self, entity: Entity) -> usize {
237        self.components
238            .values()
239            .filter(|storage| storage.contains(entity))
240            .count()
241    }
242}
243
244impl fmt::Debug for World {
245    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246        f.debug_struct("World")
247            .field("entity_count", &self.entities.len())
248            .field("component_types", &self.components.len())
249            .finish_non_exhaustive()
250    }
251}
252
253#[cfg(test)]
254#[allow(
255    clippy::unwrap_used,
256    clippy::expect_used,
257    clippy::let_underscore_must_use
258)]
259mod tests {
260    use super::*;
261    use crate::components::{Position, Velocity};
262
263    // ==================== ENTITY TESTS ====================
264
265    #[test]
266    fn test_entity_creation() {
267        let e = Entity::new(42);
268        assert_eq!(e.id(), 42);
269    }
270
271    #[test]
272    fn test_entity_display() {
273        let e = Entity::new(123);
274        assert_eq!(format!("{e}"), "e123");
275    }
276
277    #[test]
278    fn test_entity_debug() {
279        let e = Entity::new(456);
280        assert_eq!(format!("{e:?}"), "Entity(456)");
281    }
282
283    #[test]
284    fn test_entity_equality() {
285        let e1 = Entity::new(1);
286        let e2 = Entity::new(1);
287        let e3 = Entity::new(2);
288        assert_eq!(e1, e2);
289        assert_ne!(e1, e3);
290    }
291
292    #[test]
293    fn test_entity_hash() {
294        use std::collections::HashSet;
295        let mut set = HashSet::new();
296        let _ = set.insert(Entity::new(1));
297        let _ = set.insert(Entity::new(2));
298        let _ = set.insert(Entity::new(1)); // Duplicate
299        assert_eq!(set.len(), 2);
300    }
301
302    // ==================== WORLD SPAWN/DESPAWN TESTS ====================
303
304    #[test]
305    fn test_world_spawn_increments_id() {
306        let mut world = World::new();
307        let e1 = world.spawn();
308        let e2 = world.spawn();
309        let e3 = world.spawn();
310
311        assert_eq!(e1.id(), 0);
312        assert_eq!(e2.id(), 1);
313        assert_eq!(e3.id(), 2);
314    }
315
316    #[test]
317    fn test_world_entity_count() {
318        let mut world = World::new();
319        assert_eq!(world.entity_count(), 0);
320
321        let _ = world.spawn();
322        assert_eq!(world.entity_count(), 1);
323
324        let _ = world.spawn();
325        let _ = world.spawn();
326        assert_eq!(world.entity_count(), 3);
327    }
328
329    #[test]
330    fn test_world_despawn() {
331        let mut world = World::new();
332        let e1 = world.spawn();
333        let e2 = world.spawn();
334
335        assert_eq!(world.entity_count(), 2);
336        assert!(world.despawn(e1).is_ok());
337        assert_eq!(world.entity_count(), 1);
338        assert!(!world.contains(e1));
339        assert!(world.contains(e2));
340    }
341
342    #[test]
343    fn test_world_despawn_nonexistent() {
344        let mut world = World::new();
345        let fake = Entity::new(999);
346        let result = world.despawn(fake);
347        assert!(matches!(result, Err(CoreError::EntityNotFound(_))));
348    }
349
350    #[test]
351    fn test_world_contains() {
352        let mut world = World::new();
353        let e = world.spawn();
354        assert!(world.contains(e));
355        assert!(!world.contains(Entity::new(999)));
356    }
357
358    // ==================== COMPONENT TESTS ====================
359
360    #[test]
361    fn test_add_and_get_component() {
362        let mut world = World::new();
363        let e = world.spawn();
364
365        world.add_component(e, Position::new(10.0, 20.0));
366
367        let pos = world.get_component::<Position>(e);
368        assert!(pos.is_some());
369        let pos = pos.unwrap();
370        assert!((pos.x - 10.0).abs() < f32::EPSILON);
371        assert!((pos.y - 20.0).abs() < f32::EPSILON);
372    }
373
374    #[test]
375    fn test_get_component_mut() {
376        let mut world = World::new();
377        let e = world.spawn();
378        world.add_component(e, Position::new(0.0, 0.0));
379
380        if let Some(pos) = world.get_component_mut::<Position>(e) {
381            pos.x = 100.0;
382            pos.y = 200.0;
383        }
384
385        let pos = world.get_component::<Position>(e).unwrap();
386        assert!((pos.x - 100.0).abs() < f32::EPSILON);
387        assert!((pos.y - 200.0).abs() < f32::EPSILON);
388    }
389
390    #[test]
391    fn test_has_component() {
392        let mut world = World::new();
393        let e = world.spawn();
394
395        assert!(!world.has_component::<Position>(e));
396        world.add_component(e, Position::new(0.0, 0.0));
397        assert!(world.has_component::<Position>(e));
398        assert!(!world.has_component::<Velocity>(e));
399    }
400
401    #[test]
402    fn test_remove_component() {
403        let mut world = World::new();
404        let e = world.spawn();
405        world.add_component(e, Position::new(0.0, 0.0));
406
407        assert!(world.has_component::<Position>(e));
408        assert!(world.remove_component::<Position>(e));
409        assert!(!world.has_component::<Position>(e));
410        assert!(!world.remove_component::<Position>(e)); // Already removed
411    }
412
413    #[test]
414    fn test_multiple_components() {
415        let mut world = World::new();
416        let e = world.spawn();
417
418        world.add_component(e, Position::new(1.0, 2.0));
419        world.add_component(e, Velocity::new(3.0, 4.0));
420
421        let pos = world.get_component::<Position>(e).unwrap();
422        let vel = world.get_component::<Velocity>(e).unwrap();
423
424        assert!((pos.x - 1.0).abs() < f32::EPSILON);
425        assert!((vel.x - 3.0).abs() < f32::EPSILON);
426    }
427
428    #[test]
429    fn test_despawn_removes_components() {
430        let mut world = World::new();
431        let e = world.spawn();
432        world.add_component(e, Position::new(0.0, 0.0));
433
434        world.despawn(e).unwrap();
435
436        // Entity gone, so component lookup should fail
437        assert!(world.get_component::<Position>(e).is_none());
438    }
439
440    #[test]
441    fn test_component_replacement() {
442        let mut world = World::new();
443        let e = world.spawn();
444
445        world.add_component(e, Position::new(1.0, 1.0));
446        world.add_component(e, Position::new(2.0, 2.0)); // Replace
447
448        let pos = world.get_component::<Position>(e).unwrap();
449        assert!((pos.x - 2.0).abs() < f32::EPSILON);
450    }
451
452    // ==================== ITERATOR TESTS ====================
453
454    #[test]
455    fn test_entities_iterator() {
456        let mut world = World::new();
457        let e1 = world.spawn();
458        let e2 = world.spawn();
459        let e3 = world.spawn();
460
461        let entities: Vec<_> = world.entities().collect();
462        assert_eq!(entities.len(), 3);
463        assert!(entities.contains(&e1));
464        assert!(entities.contains(&e2));
465        assert!(entities.contains(&e3));
466    }
467
468    // ==================== BEHAVIORAL TESTS (MUTATION-RESISTANT) ====================
469
470    #[test]
471    fn test_position_actually_moves_after_velocity_applied() {
472        let mut world = World::new();
473        let e = world.spawn();
474
475        world.add_component(e, Position::new(0.0, 0.0));
476        world.add_component(e, Velocity::new(10.0, 5.0));
477
478        // Simulate one physics step
479        let dt = 1.0;
480        let vel = *world.get_component::<Velocity>(e).unwrap();
481        if let Some(pos) = world.get_component_mut::<Position>(e) {
482            pos.x += vel.x * dt;
483            pos.y += vel.y * dt;
484        }
485
486        let pos = world.get_component::<Position>(e).unwrap();
487        assert!(
488            (pos.x - 10.0).abs() < f32::EPSILON,
489            "X should move by velocity"
490        );
491        assert!(
492            (pos.y - 5.0).abs() < f32::EPSILON,
493            "Y should move by velocity"
494        );
495    }
496}