Files
mice/engine/collision_system.py
T
enne2 2f8e3e8b28 Fix gas poisoning, explosion hit detection and render order
- Move gas poisoning from Gas.move() to Gas.collisions() so all rats are
  registered in the collision system before the poison query runs.
- Shrink gas and explosion bboxes so rats must be well inside the tile to
  be poisoned/killed.
- Use AABB overlap instead of partial_move threshold for gas poisoning.
- Draw mobile units first, then top-layer effects (gas, mines, bombs,
  explosions) so rats appear under the gas.
- Add draw_on_top hint to Unit base class and top-layer units.
2026-06-16 19:32:50 +02:00

185 lines
6.5 KiB
Python

"""
Native Python collision detection system using Spatial Hashing.
This module provides efficient collision detection without NumPy.
It uses a grid-based approach (buckets) to ensure O(1) or O(n) complexity.
This structure is designed to be easily portable to Nim.
"""
from typing import Dict, List, Tuple, Set
from dataclasses import dataclass
def shrink_bbox(bbox: Tuple[float, float, float, float], ratio: float) -> Tuple[float, float, float, float]:
"""Return a smaller bbox centered on the original one.
ratio is the fraction of width/height removed from each side.
E.g. ratio=0.25 keeps the central 50% of the original area.
"""
x1, y1, x2, y2 = bbox
dx = (x2 - x1) * ratio / 2
dy = (y2 - y1) * ratio / 2
return (x1 + dx, y1 + dy, x2 - dx, y2 - dy)
@dataclass
class CollisionLayer:
"""Define which types of units can collide with each other."""
RAT = 0
BOMB = 1
GAS = 2
MINE = 3
POINT = 4
EXPLOSION = 5
class CollisionSystem:
"""
Manages collision detection using a Spatial Grid.
"""
def __init__(self, cell_size: int, grid_width: int, grid_height: int):
self.cell_size = cell_size
self.grid_width = grid_width
self.grid_height = grid_height
# Grid: Maps (x, y) coordinates to list of Unit objects/IDs
self.grid: Dict[Tuple[int, int], List[int]] = {}
self.grid_before: Dict[Tuple[int, int], List[int]] = {}
# Unit storage
self.units_data: Dict[int, dict] = {}
self.unit_ids: List[int] = [] # Stable list of IDs for parity
# Collision matrix (Native Python dict of sets for speed)
self._setup_collision_matrix()
def _setup_collision_matrix(self):
"""Define which collision layers interact with each other."""
L = CollisionLayer
# Interaction rules: layer -> set of target layers
self.interaction_map = {
L.RAT: {L.RAT, L.GAS, L.MINE, L.POINT, L.EXPLOSION},
L.GAS: {L.RAT},
L.MINE: {L.RAT},
L.POINT: {L.RAT},
L.EXPLOSION: {L.RAT},
L.BOMB: set() # Bombs are passive until they explode
}
def clear(self):
"""Clear all collision data for new frame."""
self.grid.clear()
self.grid_before.clear()
self.units_data.clear()
self.unit_ids.clear()
def register_unit(self, unit_id, bbox: Tuple[float, float, float, float],
position: Tuple[int, int], position_before: Tuple[int, int],
layer: int):
"""
Register a unit in the spatial grid.
"""
self.unit_ids.append(unit_id)
self.units_data[unit_id] = {
"bbox": bbox,
"pos": position,
"pos_before": position_before,
"layer": layer
}
# Add to spatial buckets
if position not in self.grid:
self.grid[position] = []
self.grid[position].append(unit_id)
if position_before not in self.grid_before:
self.grid_before[position_before] = []
self.grid_before[position_before].append(unit_id)
def get_collisions_for_unit(self, unit_id, layer: int,
tolerance: int = 0) -> List[Tuple[int, any]]:
"""
Get all units colliding with the specified unit using grid lookup.
"""
if unit_id not in self.units_data:
return []
data = self.units_data[unit_id]
bbox = data["bbox"]
pos = data["pos"]
pos_before = data["pos_before"]
colliding_units = []
target_layers = self.interaction_map.get(layer, set())
# Candidate search: look in current and previous grid buckets
# This covers units that moved into our space or were there before
candidates = set()
for p in [pos, pos_before]:
if p in self.grid:
candidates.update(self.grid[p])
if p in self.grid_before:
candidates.update(self.grid_before[p])
candidates.discard(unit_id)
for other_id in candidates:
other_data = self.units_data[other_id]
# 1. Filter by layer
if other_data["layer"] not in target_layers:
continue
# 2. AABB Check
other_bbox = other_data["bbox"]
if (bbox[0] < other_bbox[2] - tolerance and
bbox[2] > other_bbox[0] + tolerance and
bbox[1] < other_bbox[3] - tolerance and
bbox[3] > other_bbox[1] + tolerance):
# Return dummy index (for parity) and ID
colliding_units.append((0, other_id))
return colliding_units
def get_units_in_cell(self, position: Tuple[int, int],
use_before: bool = False) -> List[any]:
"""Get all unit IDs in a specific grid cell."""
target_grid = self.grid_before if use_before else self.grid
return target_grid.get(position, [])
def get_units_in_area(self, positions: List[Tuple[int, int]],
layer_filter: int = None) -> Set[any]:
"""Get all units in multiple grid cells (vectorized lookup replacement)."""
found = set()
for pos in positions:
# Check current grid
if pos in self.grid:
for uid in self.grid[pos]:
if layer_filter is None or self.units_data[uid]["layer"] == layer_filter:
found.add(uid)
# Check previous grid
if pos in self.grid_before:
for uid in self.grid_before[pos]:
if layer_filter is None or self.units_data[uid]["layer"] == layer_filter:
found.add(uid)
return found
def check_partial_move_collision(self, unit_id, partial_move: float,
threshold: float = 0.5) -> List[any]:
"""Collision check considering movement progress."""
if unit_id not in self.units_data:
return []
data = self.units_data[unit_id]
pos = data["pos"] if partial_move >= threshold else data["pos_before"]
found = set()
if pos in self.grid:
found.update(self.grid[pos])
if pos in self.grid_before:
found.update(self.grid_before[pos])
found.discard(unit_id)
return list(found)