From 88c4f0a70a7bdac675c17bdc32f56a46db4d2c09 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Tue, 16 Dec 2025 21:23:42 +0000 Subject: [PATCH] Add Entity Management --- Entity-Management.md | 487 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 487 insertions(+) create mode 100644 Entity-Management.md diff --git a/Entity-Management.md b/Entity-Management.md new file mode 100644 index 0000000..b5eae96 --- /dev/null +++ b/Entity-Management.md @@ -0,0 +1,487 @@ +# Entity Management +*Last modified: 2025-12-01* + +# 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` + +**Related Issues:** +- [#115](../issues/115) - SpatialHash for fast queries (Open) +- [#117](../issues/117) - Memory Pool for entities (Open) + +--- + +## 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 +``` + +--- + +## 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 +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) +``` + +### 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 + +**Use case - Entity perspective queries:** + +```python +def can_entity_see_walkable_path(entity, x, y): + """Check if entity knows this cell is walkable.""" + state = entity.at((x, y)) + if state.point is None: + return None # Unknown - entity hasn't discovered it + return state.point.walkable + +def get_known_entities_at(entity, x, y): + """Get entities at cell if entity has discovered it.""" + state = entity.at((x, y)) + if state.point is None: + return [] # Entity doesn't know this cell + return state.point.entities +``` + +**Ground truth access:** + +If you need the actual cell data regardless of entity perspective, access it through the grid directly: + +```python +# Entity perspective (respects discovered state) +state = entity.at((x, y)) +point_or_none = state.point + +# Ground truth (always returns GridPoint) +point = entity.grid.at(x, y) +``` + +--- + +## 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 +for entity in grid.entities: + print(entity.pos) +``` + +--- + +## 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 will be rendered with the grid +``` + +### Movement + +```python +# Direct position change +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 +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 Entity + +```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 + grid.entities.append(self.entity) + + def update(self, player_pos): + dx = player_pos[0] - self.entity.x + dy = player_pos[1] - self.entity.y + dist = (dx*dx + dy*dy) ** 0.5 + + if dist < self.aggro_range: + self.chase(player_pos) + else: + self.wander() + + def chase(self, target): + # Use pathfinding + path = self.entity.path_to(target) + 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 +``` + +### 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 +path = entity.path_to((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 +``` + +Pathfinding respects `GridPoint.walkable` properties set on the grid. + +--- + +## Performance Considerations + +**Current:** Entity queries are O(n): +```python +# Finding entities at position requires iteration +def entities_at(grid, x, y): + return [e for e in grid.entities if e.x == x and e.y == y] +``` + +**New in v1.0:** Use `GridPoint.entities` for cell-based queries: +```python +# O(n) but more convenient - filters grid.entities by position +entities_here = grid.at(x, y).entities +``` + +**Workarounds:** +- Keep entity counts reasonable (< 200 for best performance) +- Use timer callbacks for AI updates, not per-frame +- Cache query results when possible + +**Future:** [#115](../issues/115) SpatialHash will provide O(1) position queries. + +See [[Performance-and-Profiling]] for 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-01* \ No newline at end of file