diff --git a/Entity-Management.-.md b/Entity-Management.md similarity index 96% rename from Entity-Management.-.md rename to Entity-Management.md index 8674b79..b53de1f 100644 --- a/Entity-Management.-.md +++ b/Entity-Management.md @@ -1,538 +1,538 @@ -# Entity Management - -Entities are game objects that implement behavior and live on Grids. While Grids handle rendering and mediate interactions, Entities encapsulate game logic like movement, combat, and AI. - -## Quick Reference - -**Parent System:** [[Grid-System]] - -**Key Types:** -- `mcrfpy.Entity` - Game entities on grids -- `mcrfpy.Grid` - Spatial container for entities -- `mcrfpy.EntityCollection` - Collection of entities on a grid - -**Key Files:** -- `src/UIEntity.h` / `src/UIEntity.cpp` -- `src/UIEntityCollection.h` / `.cpp` -- `src/SpatialHash.h` / `src/SpatialHash.cpp` - Spatial indexing - -**Related Issues:** -- [#115](../issues/115) - SpatialHash for fast queries ✅ Implemented -- [#117](../issues/117) - Memory Pool for entities (Deferred) -- [#159](../issues/159) - EntityCollection iterator optimization ✅ Fixed - ---- - -## What Are Entities? - -Entities are game objects that: -- **Live on a Grid** (0 or 1 grid at a time) -- **Have a sprite** for visual rendering -- **Have grid position** (integer cell coordinates) -- **Implement behavior** (movement, AI, combat, inventory) -- **Track visibility** (which cells they can see / have seen) - -**Key distinction:** Entities implement behavior. Grids mediate interaction between entities and render them to screen. - ---- - -## Entity-Grid Relationship - -The Entity-Grid relationship mirrors the UIDrawable parent-child pattern: - -| Relationship | Property | Automatic Behavior | -|--------------|----------|-------------------| -| Entity → Grid | `entity.grid` | Set when added to `grid.entities` | -| Grid → Entities | `grid.entities` | Collection of all entities on grid | - -```python -import mcrfpy - -# Create grid and entity -grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400)) -player = mcrfpy.Entity(pos=(10, 10), sprite_index=0) - -# Before adding: entity has no grid -print(player.grid) # None - -# Add to grid -grid.entities.append(player) - -# After adding: bidirectional link established -print(player.grid == grid) # True -print(player in grid.entities) # True - -# Removing breaks the link -grid.entities.remove(player) -print(player.grid) # None -``` - -**Important:** An entity can only be on 0 or 1 grids at a time. Adding to a new grid automatically removes from the old one. - ---- - -## Entity Properties - -### Position - -```python -# Grid coordinates (integer cells) -entity.x = 15 -entity.y = 20 -entity.pos = (15, 20) # Tuple form - -# Draw position (float, for animation interpolation) -print(entity.draw_pos) # Actual render position -``` - -### Sprite - -```python -entity.sprite_index = 5 # Index in texture sprite sheet -``` - -### Visibility - -```python -entity.visible = True -entity.opacity = 0.8 # 0.0 to 1.0 -``` - -### Grid Reference - -```python -current_grid = entity.grid # Read-only, set by collection operations -``` - ---- - -## Spatial Queries with SpatialHash - -As of commit 7d57ce2, grids use **SpatialHash** for efficient spatial queries. This provides O(k) query time where k is the number of nearby entities, instead of O(n) scanning all entities. - -### entities_in_radius() - -```python -# Query entities within a radius (uses SpatialHash internally) -nearby = grid.entities_in_radius(x, y, radius) - -# Example: Find all entities within 10 cells of position (50, 50) -threats = grid.entities_in_radius(50, 50, 10) -for entity in threats: - print(f"Entity at ({entity.x}, {entity.y})") -``` - -### Performance Comparison - -| Entity Count | O(n) Query | SpatialHash | Speedup | -|--------------|------------|-------------|---------| -| 100 | 0.037ms | 0.008ms | 4.6× | -| 500 | 0.061ms | 0.009ms | 7.2× | -| 1,000 | 0.028ms | 0.004ms | 7.8× | -| 2,000 | 0.043ms | 0.003ms | 13× | -| 5,000 | 0.109ms | 0.003ms | **37×** | - -### N×N Visibility (AI "What can everyone see?") - -| Entity Count | O(n) approach | SpatialHash | Speedup | -|--------------|---------------|-------------|---------| -| 1,000 | 21ms | 1ms | 35× | -| 2,000 | 85ms | 1ms | 87× | -| 5,000 | 431ms | 2ms | **217×** | - -### When to Use Which Method - -| Use Case | Method | Complexity | -|----------|--------|------------| -| Nearby entities (AI, combat) | `grid.entities_in_radius(x, y, r)` | O(k) | -| FOV-based visibility | `entity.visible_entities()` | O(n) + FOV | -| All entities iteration | `for e in grid.entities` | O(n) | -| Single cell lookup | `grid.at(x, y).entities` | O(n) filter | - ---- - -## Field of View & Visibility - -Entities track what they can see via `gridstate` - a per-cell record of visible and discovered states. - -### FOV Configuration - -```python -# Grid-level FOV settings -grid.fov = mcrfpy.FOV.SHADOW # Algorithm (BASIC, DIAMOND, SHADOW, etc.) -grid.fov_radius = 10 # Default view radius - -# Module-level default -mcrfpy.default_fov = mcrfpy.FOV.PERMISSIVE_2 -``` - -### Updating Visibility - -```python -# Compute FOV from entity's position and update gridstate -entity.update_visibility() - -# This also updates any ColorLayers bound via apply_perspective() -``` - -### Querying Visible Entities - -```python -# Get list of other entities this entity can see (uses FOV + line-of-sight) -visible_enemies = entity.visible_entities() - -# With custom FOV settings -nearby = entity.visible_entities(radius=5) -visible = entity.visible_entities(fov=mcrfpy.FOV.BASIC, radius=8) -``` - -**Note:** `visible_entities()` checks FOV and line-of-sight. For pure distance queries without FOV, use `grid.entities_in_radius()`. - -### Fog of War with ColorLayers - -```python -# Create a ColorLayer for fog of war -fov_layer = grid.add_layer('color', z_index=-1) -fov_layer.fill((0, 0, 0, 255)) # Start black (unknown) - -# Bind to entity - layer auto-updates when entity.update_visibility() is called -fov_layer.apply_perspective( - entity=player, - visible=(0, 0, 0, 0), # Transparent when visible - discovered=(40, 40, 60, 180), # Dark overlay when discovered - unknown=(0, 0, 0, 255) # Black when never seen -) - -# Now whenever player moves: -player.x = new_x -player.y = new_y -player.update_visibility() # Automatically updates the fog layer -``` - -### One-Time FOV Draw - -```python -# Draw FOV without binding (useful for previews, spell ranges, etc.) -fov_layer.draw_fov( - source=(player.x, player.y), - radius=10, - fov=mcrfpy.FOV.SHADOW, - visible=(255, 255, 200, 64), - discovered=(100, 100, 100, 128), - unknown=(0, 0, 0, 255) -) -``` - -### Gridstate Access - -```python -# Entity's per-cell visibility memory -for state in entity.gridstate: - print(f"visible={state.visible}, discovered={state.discovered}") - -# Access specific cell state -state = entity.at((x, y)) -if state.visible: - print("Entity can currently see this cell") -elif state.discovered: - print("Entity has seen this cell before") -``` - -### GridPointState.point - Accessing Cell Data (#16) - -The `GridPointState.point` property provides access to the underlying `GridPoint` from an entity's perspective: - -```python -state = entity.at((x, y)) - -# If entity has NOT discovered this cell, point is None -if not state.discovered: - print(state.point) # None - entity doesn't know what's here - -# If entity HAS discovered the cell, point gives access to GridPoint -if state.discovered: - point = state.point # Live reference to GridPoint - print(f"walkable: {point.walkable}") - print(f"transparent: {point.transparent}") - print(f"entities here: {point.entities}") # List of entities at cell -``` - -**Key behaviors:** -- Returns `None` if `discovered=False` (entity has never seen this cell) -- Returns live `GridPoint` reference if `discovered=True` -- Changes to the `GridPoint` are immediately visible through `state.point` -- This is intentionally **not** a cached copy - for historical memory, implement your own system in Python - ---- - -## EntityCollection - -`grid.entities` is an `EntityCollection` with list-like operations: - -```python -# Add entities -grid.entities.append(entity) -grid.entities.extend([entity1, entity2, entity3]) -grid.entities.insert(0, entity) # Insert at index - -# Remove entities -grid.entities.remove(entity) -entity = grid.entities.pop() # Remove and return last -entity = grid.entities.pop(0) # Remove and return at index - -# Query -count = len(grid.entities) -idx = grid.entities.index(entity) -n = grid.entities.count(entity) -found = grid.entities.find("entity_name") # Find by name - -# Iteration (O(n) - optimized in #159) -for entity in grid.entities: - print(entity.pos) -``` - -### Iterator Performance (#159) - -EntityCollection iteration was optimized in commit 8f2407b: -- **Before:** O(n²) due to index-based list traversal -- **After:** O(n) using proper list iterators -- **Speedup:** 103× at 2,000 entities - ---- - -## Entity Lifecycle - -### Creation - -```python -# Basic creation -entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0) - -# With name for later lookup -entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0, name="player") -``` - -### Adding to Grid - -```python -grid.entities.append(entity) -# entity.grid is now set to grid -# Entity is automatically added to SpatialHash for fast queries -``` - -### Movement - -```python -# Direct position change (automatically updates SpatialHash) -entity.pos = (new_x, new_y) - -# Animated movement -mcrfpy.Animation("x", target_x, 0.3, "easeOutQuad").start(entity) -mcrfpy.Animation("y", target_y, 0.3, "easeOutQuad").start(entity) - -# Update visibility after movement -entity.update_visibility() -``` - -### Removal - -```python -# Method 1: Remove from collection -grid.entities.remove(entity) - -# Method 2: Entity.die() - removes from parent grid and SpatialHash -entity.die() - -# After removal: entity.grid is None -``` - -### Transfer Between Grids - -```python -def transfer_entity(entity, to_grid, new_pos): - """Move entity to a different grid.""" - entity.die() # Remove from current grid - entity.pos = new_pos - to_grid.entities.append(entity) -``` - ---- - -## Common Patterns - -### Player Entity with FOV - -```python -class Player: - def __init__(self, grid, start_pos): - self.entity = mcrfpy.Entity(pos=start_pos, sprite_index=0, name="player") - grid.entities.append(self.entity) - - # Set up fog of war - self.fov_layer = grid.add_layer('color', z_index=-1) - self.fov_layer.fill((0, 0, 0, 255)) - self.fov_layer.apply_perspective( - entity=self.entity, - visible=(0, 0, 0, 0), - discovered=(30, 30, 50, 180), - unknown=(0, 0, 0, 255) - ) - self.entity.update_visibility() - - def move(self, dx, dy): - new_x = self.entity.x + dx - new_y = self.entity.y + dy - - point = self.entity.grid.at(new_x, new_y) - if point and point.walkable: - self.entity.pos = (new_x, new_y) - self.entity.update_visibility() # Update FOV after move - return True - return False - - def get_visible_enemies(self): - """Get enemies this player can currently see.""" - return [e for e in self.entity.visible_entities() - if e.name and e.name.startswith("enemy")] -``` - -### Enemy AI with SpatialHash - -```python -class Enemy: - def __init__(self, grid, pos, aggro_range=10): - self.entity = mcrfpy.Entity(pos=pos, sprite_index=1, name="enemy") - self.aggro_range = aggro_range - self.health = 100 - self.grid = grid - grid.entities.append(self.entity) - - def update(self): - # Use SpatialHash for efficient nearby entity detection - nearby = self.grid.entities_in_radius( - self.entity.x, self.entity.y, self.aggro_range - ) - - # Find player in nearby entities - player = None - for e in nearby: - if e.name == "player": - player = e - break - - if player: - self.chase((player.x, player.y)) - else: - self.wander() - - def chase(self, target): - # Use pathfinding - path = self.entity.grid.find_path( - self.entity.x, self.entity.y, target[0], target[1] - ) - if path and len(path) > 1: - next_cell = path[1] # path[0] is current position - self.entity.pos = next_cell - - def wander(self): - import random - dx = random.choice([-1, 0, 1]) - dy = random.choice([-1, 0, 1]) - - new_pos = (self.entity.x + dx, self.entity.y + dy) - point = self.entity.grid.at(*new_pos) - if point and point.walkable: - self.entity.pos = new_pos -``` - -### Efficient Multi-Entity AI Loop - -```python -def update_all_enemies(grid, enemies): - """Update all enemies efficiently using SpatialHash.""" - for enemy in enemies: - # Each query is O(k) not O(n) - nearby = grid.entities_in_radius(enemy.x, enemy.y, enemy.aggro_range) - enemy.react_to_nearby(nearby) -``` - -### Item Entity - -```python -class Item: - def __init__(self, grid, pos, item_type): - self.entity = mcrfpy.Entity(pos=pos, sprite_index=10 + item_type) - self.item_type = item_type - grid.entities.append(self.entity) - - def pickup(self, collector): - """Called when another entity picks up this item.""" - collector.inventory.append(self.item_type) - self.entity.die() # Remove from grid -``` - -For more interaction patterns (click handling, selection, context menus), see [[Grid-Interaction-Patterns]]. - ---- - -## Pathfinding - -Entities have built-in pathfinding via libtcod: - -```python -# A* pathfinding to target (via Grid) -path = grid.find_path(entity.x, entity.y, target_x, target_y) -# Returns list of (x, y) tuples, or empty if no path - -if path: - next_step = path[1] # path[0] is current position - entity.pos = next_step - -# Dijkstra for multi-target pathfinding -grid.compute_dijkstra(goal_x, goal_y) -distance = grid.get_dijkstra_distance(entity.x, entity.y) -path = grid.get_dijkstra_path(entity.x, entity.y) -``` - -Pathfinding respects `GridPoint.walkable` properties set on the grid. - ---- - -## Performance Considerations - -### Current Performance (as of 2025-12-28) - -| Operation | Performance | Notes | -|-----------|-------------|-------| -| Entity Creation | ~90,000/sec | Sufficient for level generation | -| Iteration | ~9M reads/sec | Optimized iterators (#159) | -| Spatial Query | 0.003ms | SpatialHash O(k) (#115) | -| N×N Visibility (5000) | 2ms | 217× faster than O(n) | - -### Recommendations - -1. **Use `entities_in_radius()` for AI** - O(k) queries instead of iterating all entities -2. **Batch visibility updates** - Call `update_visibility()` once after all moves -3. **Use timer callbacks for AI** - Don't run expensive logic every frame -4. **Entity counts up to 5,000+** - SpatialHash makes large counts feasible - -### Internal Architecture - -- **SpatialHash:** Bucket-based spatial indexing (32-cell buckets) -- **Automatic updates:** Hash updates on entity add/remove/move -- **Weak references:** Hash doesn't prevent entity garbage collection - -See [[Performance-and-Profiling]] for detailed optimization guidance. - ---- - -## Related Systems - -- [[Grid-System]] - Spatial container for entities -- [[Grid-Interaction-Patterns]] - Click handling, selection, context menus -- [[Animation-System]] - Smooth entity movement -- [[Performance-and-Profiling]] - Entity performance metrics - ---- - +# Entity Management + +Entities are game objects that implement behavior and live on Grids. While Grids handle rendering and mediate interactions, Entities encapsulate game logic like movement, combat, and AI. + +## Quick Reference + +**Parent System:** [[Grid-System]] + +**Key Types:** +- `mcrfpy.Entity` - Game entities on grids +- `mcrfpy.Grid` - Spatial container for entities +- `mcrfpy.EntityCollection` - Collection of entities on a grid + +**Key Files:** +- `src/UIEntity.h` / `src/UIEntity.cpp` +- `src/UIEntityCollection.h` / `.cpp` +- `src/SpatialHash.h` / `src/SpatialHash.cpp` - Spatial indexing + +**Related Issues:** +- [#115](../issues/115) - SpatialHash for fast queries ✅ Implemented +- [#117](../issues/117) - Memory Pool for entities (Deferred) +- [#159](../issues/159) - EntityCollection iterator optimization ✅ Fixed + +--- + +## What Are Entities? + +Entities are game objects that: +- **Live on a Grid** (0 or 1 grid at a time) +- **Have a sprite** for visual rendering +- **Have grid position** (integer cell coordinates) +- **Implement behavior** (movement, AI, combat, inventory) +- **Track visibility** (which cells they can see / have seen) + +**Key distinction:** Entities implement behavior. Grids mediate interaction between entities and render them to screen. + +--- + +## Entity-Grid Relationship + +The Entity-Grid relationship mirrors the UIDrawable parent-child pattern: + +| Relationship | Property | Automatic Behavior | +|--------------|----------|-------------------| +| Entity → Grid | `entity.grid` | Set when added to `grid.entities` | +| Grid → Entities | `grid.entities` | Collection of all entities on grid | + +```python +import mcrfpy + +# Create grid and entity +grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400)) +player = mcrfpy.Entity(pos=(10, 10), sprite_index=0) + +# Before adding: entity has no grid +print(player.grid) # None + +# Add to grid +grid.entities.append(player) + +# After adding: bidirectional link established +print(player.grid == grid) # True +print(player in grid.entities) # True + +# Removing breaks the link +grid.entities.remove(player) +print(player.grid) # None +``` + +**Important:** An entity can only be on 0 or 1 grids at a time. Adding to a new grid automatically removes from the old one. + +--- + +## Entity Properties + +### Position + +```python +# Grid coordinates (integer cells) +entity.x = 15 +entity.y = 20 +entity.pos = (15, 20) # Tuple form + +# Draw position (float, for animation interpolation) +print(entity.draw_pos) # Actual render position +``` + +### Sprite + +```python +entity.sprite_index = 5 # Index in texture sprite sheet +``` + +### Visibility + +```python +entity.visible = True +entity.opacity = 0.8 # 0.0 to 1.0 +``` + +### Grid Reference + +```python +current_grid = entity.grid # Read-only, set by collection operations +``` + +--- + +## Spatial Queries with SpatialHash + +As of commit 7d57ce2, grids use **SpatialHash** for efficient spatial queries. This provides O(k) query time where k is the number of nearby entities, instead of O(n) scanning all entities. + +### entities_in_radius() + +```python +# Query entities within a radius (uses SpatialHash internally) +nearby = grid.entities_in_radius(x, y, radius) + +# Example: Find all entities within 10 cells of position (50, 50) +threats = grid.entities_in_radius(50, 50, 10) +for entity in threats: + print(f"Entity at ({entity.x}, {entity.y})") +``` + +### Performance Comparison + +| Entity Count | O(n) Query | SpatialHash | Speedup | +|--------------|------------|-------------|---------| +| 100 | 0.037ms | 0.008ms | 4.6× | +| 500 | 0.061ms | 0.009ms | 7.2× | +| 1,000 | 0.028ms | 0.004ms | 7.8× | +| 2,000 | 0.043ms | 0.003ms | 13× | +| 5,000 | 0.109ms | 0.003ms | **37×** | + +### N×N Visibility (AI "What can everyone see?") + +| Entity Count | O(n) approach | SpatialHash | Speedup | +|--------------|---------------|-------------|---------| +| 1,000 | 21ms | 1ms | 35× | +| 2,000 | 85ms | 1ms | 87× | +| 5,000 | 431ms | 2ms | **217×** | + +### When to Use Which Method + +| Use Case | Method | Complexity | +|----------|--------|------------| +| Nearby entities (AI, combat) | `grid.entities_in_radius(x, y, r)` | O(k) | +| FOV-based visibility | `entity.visible_entities()` | O(n) + FOV | +| All entities iteration | `for e in grid.entities` | O(n) | +| Single cell lookup | `grid.at(x, y).entities` | O(n) filter | + +--- + +## Field of View & Visibility + +Entities track what they can see via `gridstate` - a per-cell record of visible and discovered states. + +### FOV Configuration + +```python +# Grid-level FOV settings +grid.fov = mcrfpy.FOV.SHADOW # Algorithm (BASIC, DIAMOND, SHADOW, etc.) +grid.fov_radius = 10 # Default view radius + +# Module-level default +mcrfpy.default_fov = mcrfpy.FOV.PERMISSIVE_2 +``` + +### Updating Visibility + +```python +# Compute FOV from entity's position and update gridstate +entity.update_visibility() + +# This also updates any ColorLayers bound via apply_perspective() +``` + +### Querying Visible Entities + +```python +# Get list of other entities this entity can see (uses FOV + line-of-sight) +visible_enemies = entity.visible_entities() + +# With custom FOV settings +nearby = entity.visible_entities(radius=5) +visible = entity.visible_entities(fov=mcrfpy.FOV.BASIC, radius=8) +``` + +**Note:** `visible_entities()` checks FOV and line-of-sight. For pure distance queries without FOV, use `grid.entities_in_radius()`. + +### Fog of War with ColorLayers + +```python +# Create a ColorLayer for fog of war +fov_layer = grid.add_layer('color', z_index=-1) +fov_layer.fill((0, 0, 0, 255)) # Start black (unknown) + +# Bind to entity - layer auto-updates when entity.update_visibility() is called +fov_layer.apply_perspective( + entity=player, + visible=(0, 0, 0, 0), # Transparent when visible + discovered=(40, 40, 60, 180), # Dark overlay when discovered + unknown=(0, 0, 0, 255) # Black when never seen +) + +# Now whenever player moves: +player.x = new_x +player.y = new_y +player.update_visibility() # Automatically updates the fog layer +``` + +### One-Time FOV Draw + +```python +# Draw FOV without binding (useful for previews, spell ranges, etc.) +fov_layer.draw_fov( + source=(player.x, player.y), + radius=10, + fov=mcrfpy.FOV.SHADOW, + visible=(255, 255, 200, 64), + discovered=(100, 100, 100, 128), + unknown=(0, 0, 0, 255) +) +``` + +### Gridstate Access + +```python +# Entity's per-cell visibility memory +for state in entity.gridstate: + print(f"visible={state.visible}, discovered={state.discovered}") + +# Access specific cell state +state = entity.at((x, y)) +if state.visible: + print("Entity can currently see this cell") +elif state.discovered: + print("Entity has seen this cell before") +``` + +### GridPointState.point - Accessing Cell Data (#16) + +The `GridPointState.point` property provides access to the underlying `GridPoint` from an entity's perspective: + +```python +state = entity.at((x, y)) + +# If entity has NOT discovered this cell, point is None +if not state.discovered: + print(state.point) # None - entity doesn't know what's here + +# If entity HAS discovered the cell, point gives access to GridPoint +if state.discovered: + point = state.point # Live reference to GridPoint + print(f"walkable: {point.walkable}") + print(f"transparent: {point.transparent}") + print(f"entities here: {point.entities}") # List of entities at cell +``` + +**Key behaviors:** +- Returns `None` if `discovered=False` (entity has never seen this cell) +- Returns live `GridPoint` reference if `discovered=True` +- Changes to the `GridPoint` are immediately visible through `state.point` +- This is intentionally **not** a cached copy - for historical memory, implement your own system in Python + +--- + +## EntityCollection + +`grid.entities` is an `EntityCollection` with list-like operations: + +```python +# Add entities +grid.entities.append(entity) +grid.entities.extend([entity1, entity2, entity3]) +grid.entities.insert(0, entity) # Insert at index + +# Remove entities +grid.entities.remove(entity) +entity = grid.entities.pop() # Remove and return last +entity = grid.entities.pop(0) # Remove and return at index + +# Query +count = len(grid.entities) +idx = grid.entities.index(entity) +n = grid.entities.count(entity) +found = grid.entities.find("entity_name") # Find by name + +# Iteration (O(n) - optimized in #159) +for entity in grid.entities: + print(entity.pos) +``` + +### Iterator Performance (#159) + +EntityCollection iteration was optimized in commit 8f2407b: +- **Before:** O(n²) due to index-based list traversal +- **After:** O(n) using proper list iterators +- **Speedup:** 103× at 2,000 entities + +--- + +## Entity Lifecycle + +### Creation + +```python +# Basic creation +entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0) + +# With name for later lookup +entity = mcrfpy.Entity(pos=(10, 10), sprite_index=0, name="player") +``` + +### Adding to Grid + +```python +grid.entities.append(entity) +# entity.grid is now set to grid +# Entity is automatically added to SpatialHash for fast queries +``` + +### Movement + +```python +# Direct position change (automatically updates SpatialHash) +entity.pos = (new_x, new_y) + +# Animated movement +mcrfpy.Animation("x", target_x, 0.3, "easeOutQuad").start(entity) +mcrfpy.Animation("y", target_y, 0.3, "easeOutQuad").start(entity) + +# Update visibility after movement +entity.update_visibility() +``` + +### Removal + +```python +# Method 1: Remove from collection +grid.entities.remove(entity) + +# Method 2: Entity.die() - removes from parent grid and SpatialHash +entity.die() + +# After removal: entity.grid is None +``` + +### Transfer Between Grids + +```python +def transfer_entity(entity, to_grid, new_pos): + """Move entity to a different grid.""" + entity.die() # Remove from current grid + entity.pos = new_pos + to_grid.entities.append(entity) +``` + +--- + +## Common Patterns + +### Player Entity with FOV + +```python +class Player: + def __init__(self, grid, start_pos): + self.entity = mcrfpy.Entity(pos=start_pos, sprite_index=0, name="player") + grid.entities.append(self.entity) + + # Set up fog of war + self.fov_layer = grid.add_layer('color', z_index=-1) + self.fov_layer.fill((0, 0, 0, 255)) + self.fov_layer.apply_perspective( + entity=self.entity, + visible=(0, 0, 0, 0), + discovered=(30, 30, 50, 180), + unknown=(0, 0, 0, 255) + ) + self.entity.update_visibility() + + def move(self, dx, dy): + new_x = self.entity.x + dx + new_y = self.entity.y + dy + + point = self.entity.grid.at(new_x, new_y) + if point and point.walkable: + self.entity.pos = (new_x, new_y) + self.entity.update_visibility() # Update FOV after move + return True + return False + + def get_visible_enemies(self): + """Get enemies this player can currently see.""" + return [e for e in self.entity.visible_entities() + if e.name and e.name.startswith("enemy")] +``` + +### Enemy AI with SpatialHash + +```python +class Enemy: + def __init__(self, grid, pos, aggro_range=10): + self.entity = mcrfpy.Entity(pos=pos, sprite_index=1, name="enemy") + self.aggro_range = aggro_range + self.health = 100 + self.grid = grid + grid.entities.append(self.entity) + + def update(self): + # Use SpatialHash for efficient nearby entity detection + nearby = self.grid.entities_in_radius( + self.entity.x, self.entity.y, self.aggro_range + ) + + # Find player in nearby entities + player = None + for e in nearby: + if e.name == "player": + player = e + break + + if player: + self.chase((player.x, player.y)) + else: + self.wander() + + def chase(self, target): + # Use pathfinding + path = self.entity.grid.find_path( + self.entity.x, self.entity.y, target[0], target[1] + ) + if path and len(path) > 1: + next_cell = path[1] # path[0] is current position + self.entity.pos = next_cell + + def wander(self): + import random + dx = random.choice([-1, 0, 1]) + dy = random.choice([-1, 0, 1]) + + new_pos = (self.entity.x + dx, self.entity.y + dy) + point = self.entity.grid.at(*new_pos) + if point and point.walkable: + self.entity.pos = new_pos +``` + +### Efficient Multi-Entity AI Loop + +```python +def update_all_enemies(grid, enemies): + """Update all enemies efficiently using SpatialHash.""" + for enemy in enemies: + # Each query is O(k) not O(n) + nearby = grid.entities_in_radius(enemy.x, enemy.y, enemy.aggro_range) + enemy.react_to_nearby(nearby) +``` + +### Item Entity + +```python +class Item: + def __init__(self, grid, pos, item_type): + self.entity = mcrfpy.Entity(pos=pos, sprite_index=10 + item_type) + self.item_type = item_type + grid.entities.append(self.entity) + + def pickup(self, collector): + """Called when another entity picks up this item.""" + collector.inventory.append(self.item_type) + self.entity.die() # Remove from grid +``` + +For more interaction patterns (click handling, selection, context menus), see [[Grid-Interaction-Patterns]]. + +--- + +## Pathfinding + +Entities have built-in pathfinding via libtcod: + +```python +# A* pathfinding to target (via Grid) +path = grid.find_path(entity.x, entity.y, target_x, target_y) +# Returns list of (x, y) tuples, or empty if no path + +if path: + next_step = path[1] # path[0] is current position + entity.pos = next_step + +# Dijkstra for multi-target pathfinding +grid.compute_dijkstra(goal_x, goal_y) +distance = grid.get_dijkstra_distance(entity.x, entity.y) +path = grid.get_dijkstra_path(entity.x, entity.y) +``` + +Pathfinding respects `GridPoint.walkable` properties set on the grid. + +--- + +## Performance Considerations + +### Current Performance (as of 2025-12-28) + +| Operation | Performance | Notes | +|-----------|-------------|-------| +| Entity Creation | ~90,000/sec | Sufficient for level generation | +| Iteration | ~9M reads/sec | Optimized iterators (#159) | +| Spatial Query | 0.003ms | SpatialHash O(k) (#115) | +| N×N Visibility (5000) | 2ms | 217× faster than O(n) | + +### Recommendations + +1. **Use `entities_in_radius()` for AI** - O(k) queries instead of iterating all entities +2. **Batch visibility updates** - Call `update_visibility()` once after all moves +3. **Use timer callbacks for AI** - Don't run expensive logic every frame +4. **Entity counts up to 5,000+** - SpatialHash makes large counts feasible + +### Internal Architecture + +- **SpatialHash:** Bucket-based spatial indexing (32-cell buckets) +- **Automatic updates:** Hash updates on entity add/remove/move +- **Weak references:** Hash doesn't prevent entity garbage collection + +See [[Performance-and-Profiling]] for detailed optimization guidance. + +--- + +## Related Systems + +- [[Grid-System]] - Spatial container for entities +- [[Grid-Interaction-Patterns]] - Click handling, selection, context menus +- [[Animation-System]] - Smooth entity movement +- [[Performance-and-Profiling]] - Entity performance metrics + +--- + *Last updated: 2025-12-28* \ No newline at end of file