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
214impl fmt::Debug for World {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        f.debug_struct("World")
217            .field("entity_count", &self.entities.len())
218            .field("component_types", &self.components.len())
219            .finish_non_exhaustive()
220    }
221}
222
223#[cfg(test)]
224#[allow(
225    clippy::unwrap_used,
226    clippy::expect_used,
227    clippy::let_underscore_must_use
228)]
229mod tests {
230    use super::*;
231    use crate::components::{Position, Velocity};
232
233    // ==================== ENTITY TESTS ====================
234
235    #[test]
236    fn test_entity_creation() {
237        let e = Entity::new(42);
238        assert_eq!(e.id(), 42);
239    }
240
241    #[test]
242    fn test_entity_display() {
243        let e = Entity::new(123);
244        assert_eq!(format!("{e}"), "e123");
245    }
246
247    #[test]
248    fn test_entity_debug() {
249        let e = Entity::new(456);
250        assert_eq!(format!("{e:?}"), "Entity(456)");
251    }
252
253    #[test]
254    fn test_entity_equality() {
255        let e1 = Entity::new(1);
256        let e2 = Entity::new(1);
257        let e3 = Entity::new(2);
258        assert_eq!(e1, e2);
259        assert_ne!(e1, e3);
260    }
261
262    #[test]
263    fn test_entity_hash() {
264        use std::collections::HashSet;
265        let mut set = HashSet::new();
266        let _ = set.insert(Entity::new(1));
267        let _ = set.insert(Entity::new(2));
268        let _ = set.insert(Entity::new(1)); // Duplicate
269        assert_eq!(set.len(), 2);
270    }
271
272    // ==================== WORLD SPAWN/DESPAWN TESTS ====================
273
274    #[test]
275    fn test_world_spawn_increments_id() {
276        let mut world = World::new();
277        let e1 = world.spawn();
278        let e2 = world.spawn();
279        let e3 = world.spawn();
280
281        assert_eq!(e1.id(), 0);
282        assert_eq!(e2.id(), 1);
283        assert_eq!(e3.id(), 2);
284    }
285
286    #[test]
287    fn test_world_entity_count() {
288        let mut world = World::new();
289        assert_eq!(world.entity_count(), 0);
290
291        let _ = world.spawn();
292        assert_eq!(world.entity_count(), 1);
293
294        let _ = world.spawn();
295        let _ = world.spawn();
296        assert_eq!(world.entity_count(), 3);
297    }
298
299    #[test]
300    fn test_world_despawn() {
301        let mut world = World::new();
302        let e1 = world.spawn();
303        let e2 = world.spawn();
304
305        assert_eq!(world.entity_count(), 2);
306        assert!(world.despawn(e1).is_ok());
307        assert_eq!(world.entity_count(), 1);
308        assert!(!world.contains(e1));
309        assert!(world.contains(e2));
310    }
311
312    #[test]
313    fn test_world_despawn_nonexistent() {
314        let mut world = World::new();
315        let fake = Entity::new(999);
316        let result = world.despawn(fake);
317        assert!(matches!(result, Err(CoreError::EntityNotFound(_))));
318    }
319
320    #[test]
321    fn test_world_contains() {
322        let mut world = World::new();
323        let e = world.spawn();
324        assert!(world.contains(e));
325        assert!(!world.contains(Entity::new(999)));
326    }
327
328    // ==================== COMPONENT TESTS ====================
329
330    #[test]
331    fn test_add_and_get_component() {
332        let mut world = World::new();
333        let e = world.spawn();
334
335        world.add_component(e, Position::new(10.0, 20.0));
336
337        let pos = world.get_component::<Position>(e);
338        assert!(pos.is_some());
339        let pos = pos.unwrap();
340        assert!((pos.x - 10.0).abs() < f32::EPSILON);
341        assert!((pos.y - 20.0).abs() < f32::EPSILON);
342    }
343
344    #[test]
345    fn test_get_component_mut() {
346        let mut world = World::new();
347        let e = world.spawn();
348        world.add_component(e, Position::new(0.0, 0.0));
349
350        if let Some(pos) = world.get_component_mut::<Position>(e) {
351            pos.x = 100.0;
352            pos.y = 200.0;
353        }
354
355        let pos = world.get_component::<Position>(e).unwrap();
356        assert!((pos.x - 100.0).abs() < f32::EPSILON);
357        assert!((pos.y - 200.0).abs() < f32::EPSILON);
358    }
359
360    #[test]
361    fn test_has_component() {
362        let mut world = World::new();
363        let e = world.spawn();
364
365        assert!(!world.has_component::<Position>(e));
366        world.add_component(e, Position::new(0.0, 0.0));
367        assert!(world.has_component::<Position>(e));
368        assert!(!world.has_component::<Velocity>(e));
369    }
370
371    #[test]
372    fn test_remove_component() {
373        let mut world = World::new();
374        let e = world.spawn();
375        world.add_component(e, Position::new(0.0, 0.0));
376
377        assert!(world.has_component::<Position>(e));
378        assert!(world.remove_component::<Position>(e));
379        assert!(!world.has_component::<Position>(e));
380        assert!(!world.remove_component::<Position>(e)); // Already removed
381    }
382
383    #[test]
384    fn test_multiple_components() {
385        let mut world = World::new();
386        let e = world.spawn();
387
388        world.add_component(e, Position::new(1.0, 2.0));
389        world.add_component(e, Velocity::new(3.0, 4.0));
390
391        let pos = world.get_component::<Position>(e).unwrap();
392        let vel = world.get_component::<Velocity>(e).unwrap();
393
394        assert!((pos.x - 1.0).abs() < f32::EPSILON);
395        assert!((vel.x - 3.0).abs() < f32::EPSILON);
396    }
397
398    #[test]
399    fn test_despawn_removes_components() {
400        let mut world = World::new();
401        let e = world.spawn();
402        world.add_component(e, Position::new(0.0, 0.0));
403
404        world.despawn(e).unwrap();
405
406        // Entity gone, so component lookup should fail
407        assert!(world.get_component::<Position>(e).is_none());
408    }
409
410    #[test]
411    fn test_component_replacement() {
412        let mut world = World::new();
413        let e = world.spawn();
414
415        world.add_component(e, Position::new(1.0, 1.0));
416        world.add_component(e, Position::new(2.0, 2.0)); // Replace
417
418        let pos = world.get_component::<Position>(e).unwrap();
419        assert!((pos.x - 2.0).abs() < f32::EPSILON);
420    }
421
422    // ==================== ITERATOR TESTS ====================
423
424    #[test]
425    fn test_entities_iterator() {
426        let mut world = World::new();
427        let e1 = world.spawn();
428        let e2 = world.spawn();
429        let e3 = world.spawn();
430
431        let entities: Vec<_> = world.entities().collect();
432        assert_eq!(entities.len(), 3);
433        assert!(entities.contains(&e1));
434        assert!(entities.contains(&e2));
435        assert!(entities.contains(&e3));
436    }
437
438    // ==================== BEHAVIORAL TESTS (MUTATION-RESISTANT) ====================
439
440    #[test]
441    fn test_position_actually_moves_after_velocity_applied() {
442        let mut world = World::new();
443        let e = world.spawn();
444
445        world.add_component(e, Position::new(0.0, 0.0));
446        world.add_component(e, Velocity::new(10.0, 5.0));
447
448        // Simulate one physics step
449        let dt = 1.0;
450        let vel = *world.get_component::<Velocity>(e).unwrap();
451        if let Some(pos) = world.get_component_mut::<Position>(e) {
452            pos.x += vel.x * dt;
453            pos.y += vel.y * dt;
454        }
455
456        let pos = world.get_component::<Position>(e).unwrap();
457        assert!(
458            (pos.x - 10.0).abs() < f32::EPSILON,
459            "X should move by velocity"
460        );
461        assert!(
462            (pos.y - 5.0).abs() < f32::EPSILON,
463            "Y should move by velocity"
464        );
465    }
466}