nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
use crate::tui::ecs::world::*;
use freecs::Entity;
use std::collections::{HashMap, HashSet};

pub fn movement_system(world: &mut World) {
    let updates: Vec<(Entity, f64, f64)> = world
        .query_entities(POSITION | VELOCITY)
        .filter_map(|entity| {
            world
                .get_velocity(entity)
                .map(|velocity| (entity, velocity.column, velocity.row))
        })
        .collect();

    for (entity, delta_column, delta_row) in updates {
        if let Some(position) = world.get_position_mut(entity) {
            position.column += delta_column;
            position.row += delta_row;
        }
    }
}

pub struct Contact {
    pub entity_a: Entity,
    pub entity_b: Entity,
    pub normal_column: f64,
    pub normal_row: f64,
    pub penetration: f64,
}

const GRID_CELL_SIZE: f64 = 4.0;

struct ColliderAabb {
    entity: Entity,
    left: f64,
    right: f64,
    top: f64,
    bottom: f64,
    layer: u32,
    mask: u32,
}

pub fn collision_pairs(world: &World) -> Vec<Contact> {
    let entities: Vec<Entity> = world.query_entities(POSITION | COLLIDER).collect();

    let aabbs: Vec<ColliderAabb> = entities
        .iter()
        .filter_map(|&entity| {
            let position = world.get_position(entity)?;
            let collider = world.get_collider(entity)?;
            let left = position.column + collider.offset_column;
            let top = position.row + collider.offset_row;
            Some(ColliderAabb {
                entity,
                left,
                right: left + collider.width as f64,
                top,
                bottom: top + collider.height as f64,
                layer: collider.layer,
                mask: collider.mask,
            })
        })
        .collect();

    let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
    for (index, aabb) in aabbs.iter().enumerate() {
        let min_cell_column = (aabb.left / GRID_CELL_SIZE).floor() as i32;
        let max_cell_column = (aabb.right / GRID_CELL_SIZE).floor() as i32;
        let min_cell_row = (aabb.top / GRID_CELL_SIZE).floor() as i32;
        let max_cell_row = (aabb.bottom / GRID_CELL_SIZE).floor() as i32;

        for cell_column in min_cell_column..=max_cell_column {
            for cell_row in min_cell_row..=max_cell_row {
                grid.entry((cell_column, cell_row)).or_default().push(index);
            }
        }
    }

    let mut checked: HashSet<(usize, usize)> = HashSet::new();
    let mut contacts = Vec::new();

    for cell_indices in grid.values() {
        for outer in 0..cell_indices.len() {
            for inner in (outer + 1)..cell_indices.len() {
                let index_a = cell_indices[outer];
                let index_b = cell_indices[inner];
                let pair = if index_a < index_b {
                    (index_a, index_b)
                } else {
                    (index_b, index_a)
                };

                if !checked.insert(pair) {
                    continue;
                }

                let a = &aabbs[pair.0];
                let b = &aabbs[pair.1];

                if (a.layer & b.mask) == 0 || (b.layer & a.mask) == 0 {
                    continue;
                }

                let overlap_left = a.right - b.left;
                let overlap_right = b.right - a.left;
                let overlap_top = a.bottom - b.top;
                let overlap_bottom = b.bottom - a.top;

                if overlap_left <= 0.0
                    || overlap_right <= 0.0
                    || overlap_top <= 0.0
                    || overlap_bottom <= 0.0
                {
                    continue;
                }

                let overlap_x = overlap_left.min(overlap_right);
                let overlap_y = overlap_top.min(overlap_bottom);

                let (normal_column, normal_row, penetration) = if overlap_x < overlap_y {
                    let direction = if overlap_left < overlap_right {
                        -1.0
                    } else {
                        1.0
                    };
                    (direction, 0.0, overlap_x)
                } else {
                    let direction = if overlap_top < overlap_bottom {
                        -1.0
                    } else {
                        1.0
                    };
                    (0.0, direction, overlap_y)
                };

                contacts.push(Contact {
                    entity_a: a.entity,
                    entity_b: b.entity,
                    normal_column,
                    normal_row,
                    penetration,
                });
            }
        }
    }

    contacts
}

pub fn resolve_collision(world: &mut World, contact: &Contact) {
    let half_penetration = contact.penetration / 2.0;
    let push_column = contact.normal_column * half_penetration;
    let push_row = contact.normal_row * half_penetration;

    if let Some(position) = world.get_position_mut(contact.entity_a) {
        position.column -= push_column;
        position.row -= push_row;
    }
    if let Some(position) = world.get_position_mut(contact.entity_b) {
        position.column += push_column;
        position.row += push_row;
    }
}

pub fn resolve_collision_static(world: &mut World, contact: &Contact, static_entity: Entity) {
    let push_column = contact.normal_column * contact.penetration;
    let push_row = contact.normal_row * contact.penetration;

    if contact.entity_a == static_entity {
        if let Some(position) = world.get_position_mut(contact.entity_b) {
            position.column += push_column;
            position.row += push_row;
        }
    } else if let Some(position) = world.get_position_mut(contact.entity_a) {
        position.column -= push_column;
        position.row -= push_row;
    }
}

pub fn parent_transform_system(world: &mut World) {
    let updates: Vec<(Entity, f64, f64)> = world
        .query_entities(PARENT | LOCAL_OFFSET | POSITION)
        .filter_map(|entity| {
            let parent_component = world.get_parent(entity)?;
            let parent_entity = parent_component.0;
            let parent_position = world.get_position(parent_entity)?;
            let local_offset = world.get_local_offset(entity)?;
            let target_column = parent_position.column + local_offset.column;
            let target_row = parent_position.row + local_offset.row;
            Some((entity, target_column, target_row))
        })
        .collect();

    for (entity, column, row) in updates {
        if let Some(position) = world.get_position_mut(entity) {
            position.column = column;
            position.row = row;
        }
    }
}

pub fn cascade_despawn(world: &mut World, parent_entity: Entity) {
    let children: Vec<Entity> = world
        .query_entities(PARENT)
        .filter(|&entity| {
            world
                .get_parent(entity)
                .is_some_and(|parent| parent.0 == parent_entity)
        })
        .collect();

    for child in &children {
        cascade_despawn(world, *child);
    }

    if !children.is_empty() {
        world.despawn_entities(&children);
    }
}