diff --git a/Grid-System.md b/Grid-System.md index ae3da2d..46f6988 100644 --- a/Grid-System.md +++ b/Grid-System.md @@ -1,245 +1,433 @@ -# Grid System - -The Grid System is McRogueFace's core spatial container for roguelike game maps. It provides tilemap rendering, entity management, FOV (field of view), and pathfinding integration with libtcod. - -## Quick Reference - -**Related Issues:** -- [#113](../issues/113) - Batch Operations for Grid (Open - Tier 1) -- [#124](../issues/124) - Grid Point Animation (Open - Tier 1) -- [#150](../issues/150) - User-driven Layer Rendering (Closed - Implemented) -- [#148](../issues/148) - Dirty Flag RenderTexture Caching (Closed - Implemented) -- [#147](../issues/147) - Dynamic Layer System (Closed - Implemented) -- [#123](../issues/123) - Chunk-based Grid Rendering (Closed - Implemented) - -**Key Files:** -- `src/UIGrid.h` / `src/UIGrid.cpp` - Main grid implementation -- `src/GridLayers.h` / `src/GridLayers.cpp` - ColorLayer and TileLayer -- `src/UIGridPoint.h` - Individual grid cell (walkability, transparency) -- `src/UIGridPointState.h` - Per-entity perspective/knowledge -- `src/UIEntity.h` / `src/UIEntity.cpp` - Entity system (lives on grid) - -**API Reference:** -- See [mcrfpy.Grid](../docs/api_reference_dynamic.html#Grid) in generated API docs -- See [mcrfpy.Entity](../docs/api_reference_dynamic.html#Entity) in generated API docs - -## Architecture Overview - -### Three-Layer Design - -The Grid System uses a three-layer architecture for sophisticated roguelike features: - -1. **Visual Layer** (Rendering Layers) - - What's displayed: tile sprites, colors, overlays - - Implemented via `ColorLayer` and `TileLayer` objects - - Multiple layers per grid with z_index ordering - - Files: `src/GridLayers.h`, `src/GridLayers.cpp` - -2. **World State Layer** (`TCODMap`) - - Physical properties: walkable, transparent, cost - - Used for pathfinding and FOV calculations - - Integration: libtcod via `src/UIGrid.cpp` - -3. **Perspective Layer** (`UIGridPointState`) - - Per-entity knowledge: what each entity has seen/explored - - Enables fog of war, asymmetric information - - File: `src/UIGridPointState.h` - - **Note:** Python access currently limited - see Known Issues - -This architecture follows proven patterns from Caves of Qud, Cogmind, and DCSS. - -### Dynamic Layer System - -Grids support multiple rendering layers that can be added/removed at runtime: - -| Layer Type | Purpose | Methods | -|------------|---------|---------| -| `ColorLayer` | Solid colors per cell (fog, highlights) | `set(x, y, color)`, `at(x, y)`, `fill(color)` | -| `TileLayer` | Texture sprites per cell (terrain, items) | `set(x, y, sprite_index)`, `at(x, y)`, `fill(index)` | - -**z_index semantics:** -- Negative z_index: Renders **below** entities -- Zero or positive z_index: Renders **above** entities -- Lower z_index renders first (behind higher z_index) - -```python -# Create grid with no default layers -grid = mcrfpy.Grid(grid_size=(50, 50), pos=(100, 100), size=(400, 400), layers={}) - -# Add layers explicitly -background = grid.add_layer("color", z_index=-2) # Furthest back -tiles = grid.add_layer("tile", z_index=-1) # Above background, below entities -fog_overlay = grid.add_layer("color", z_index=1) # Above entities (fog of war) - -# Access existing layers -for layer in grid.layers: - print(f"{type(layer).__name__} at z={layer.z_index}") - -# Remove a layer -grid.remove_layer(fog_overlay) -``` - -### Grid → Entity Relationship - -**Entity Lifecycle:** -- Entities live on exactly 0 or 1 grids -- `grid.entities.append(entity)` sets `entity.grid = grid` -- `grid.entities.remove(entity)` sets `entity.grid = None` -- Entity removal handled automatically on grid destruction - -**Spatial Queries:** -- Currently: Linear iteration through entity list -- Planned: SpatialHash for O(1) lookups (see [#115](../issues/115)) - -## Sub-Pages - -- [[Grid-Rendering-Pipeline]] - How grid renders each frame (chunks, dirty flags, caching) -- [[Grid-TCOD-Integration]] - FOV, pathfinding, walkability -- [[Grid-Entity-Lifecycle]] - Entity creation, movement, removal - -## Common Tasks - -### Creating a Grid - -```python -import mcrfpy - -# Create grid with default tile layer -grid = mcrfpy.Grid( - grid_size=(50, 50), # 50x50 cells - pos=(100, 100), # Screen position - size=(400, 400), # Viewport size in pixels - texture=my_texture # Texture for default TileLayer -) - -# Or create with specific layer configuration -grid = mcrfpy.Grid( - grid_size=(50, 50), - pos=(100, 100), - size=(400, 400), - layers={"terrain": "tile", "highlights": "color"} -) - -# Or start empty and add layers manually -grid = mcrfpy.Grid(grid_size=(50, 50), pos=(100, 100), size=(400, 400), layers={}) -terrain = grid.add_layer("tile", z_index=-1, texture=my_texture) - -# Add to scene -mcrfpy.sceneUI("game").append(grid) -``` - -### Setting Tile Properties - -```python -# Access layers for visual properties -tile_layer = grid.layers[0] # First layer (usually tiles) -tile_layer.set(x, y, 42) # Set sprite index at cell - -color_layer = grid.add_layer("color", z_index=-2) -color_layer.set(x, y, mcrfpy.Color(64, 64, 128)) # Blue tint - -# Access grid point for world properties -point = grid.at(x, y) -point.walkable = True # Can entities walk here? -point.transparent = True # Can see through for FOV? -``` - -### FOV and Pathfinding - -```python -# Compute field of view from position -grid.compute_fov(entity.grid_x, entity.grid_y, radius=10) - -# Check if cell is visible -if grid.is_in_fov(target_x, target_y): - print("Target is visible!") - -# A* pathfinding -path = grid.find_path(start_x, start_y, end_x, end_y) - -# Dijkstra maps for AI -grid.compute_dijkstra([(goal_x, goal_y)]) -distance = grid.get_dijkstra_distance(entity.grid_x, entity.grid_y) -path_to_goal = grid.get_dijkstra_path(entity.grid_x, entity.grid_y) -``` - -### Mouse Events - -Grids support mouse interaction at both the grid level and cell level: - -```python -# Grid-level events (screen coordinates) -def on_grid_click(x, y, button): - print(f"Grid clicked at pixel ({x}, {y}), button {button}") - -grid.on_click = on_grid_click -grid.on_enter = lambda: print("Mouse entered grid") -grid.on_exit = lambda: print("Mouse left grid") -grid.on_move = lambda x, y: print(f"Mouse at ({x}, {y})") - -# Cell-level events (grid coordinates) -def on_cell_click(grid_x, grid_y): - print(f"Cell ({grid_x}, {grid_y}) clicked!") - point = grid.at(grid_x, grid_y) - # Do something with the cell... - -grid.on_cell_click = on_cell_click -grid.on_cell_enter = lambda x, y: print(f"Entered cell ({x}, {y})") -grid.on_cell_exit = lambda x, y: print(f"Left cell ({x}, {y})") - -# Query currently hovered cell -if grid.hovered_cell: - hx, hy = grid.hovered_cell - print(f"Hovering over ({hx}, {hy})") -``` - -### Camera Control - -```python -# Center viewport on a position (pixel coordinates within grid space) -grid.center = (player.grid_x * 16 + 8, player.grid_y * 16 + 8) - -# Or set components individually -grid.center_x = player.grid_x * 16 + 8 -grid.center_y = player.grid_y * 16 + 8 - -# Zoom (1.0 = normal, 2.0 = 2x zoom in, 0.5 = zoom out) -grid.zoom = 1.5 -``` - -## Performance Characteristics - -**Implemented Optimizations:** -- **Chunk-based rendering** ([#123](../issues/123)): Large grids divided into chunks, only visible chunks rendered -- **Dirty flag system** ([#148](../issues/148)): Layers track changes, skip redraw when unchanged -- **RenderTexture caching**: Each chunk cached to texture, reused until dirty -- **Viewport culling**: Only cells within viewport are processed - -**Current Performance:** -- Grids of 1000x1000+ cells render efficiently -- Static scenes near-zero CPU (cached textures reused) -- Entity rendering: O(visible entities) - -**Profiling:** Use the F3 overlay or `mcrfpy.setTimer()` with `mcrfpy.getMetrics()` to measure grid performance. See [[Performance-and-Profiling]]. - -## Known Issues & Limitations - -**Current Limitations:** -- **FOV Python access limited:** `compute_fov()` works but fog-of-war overlay must be managed manually via color layers. Per-entity perspective (`UIGridPointState`) not yet exposed to Python. See [#113 discussion](../issues/113). -- No tile-level animation yet (planned: [#124](../issues/124)) -- Entity spatial queries are O(n) (planned: SpatialHash [#115](../issues/115)) - -**Workarounds:** -- For fog of war: Use a `ColorLayer` with positive z_index, update cell colors based on `is_in_fov()` results -- For entity queries: Use list comprehension filtering on `grid.entities` - -## Related Systems - -- [[UI-Component-Hierarchy]] - Grid inherits from UIDrawable -- [[Animation-System]] - Grid properties are animatable (pos, zoom, center, etc.) -- [[Performance-and-Profiling]] - Grid rendering instrumented with metrics -- [[Entity-Management]] - Entities live within Grid containers - ---- - -*Last updated: 2025-11-29* \ No newline at end of file +# Grid System + +The Grid System is McRogueFace's core spatial container for roguelike game maps. It provides tilemap rendering, entity management, FOV (field of view), and pathfinding integration with libtcod. + +## Quick Reference + +**Related Issues:** +- [#113](../issues/113) - Batch Operations for Grid (Open - Tier 1) +- [#124](../issues/124) - Grid Point Animation (Open - Tier 1) +- [#150](../issues/150) - User-driven Layer Rendering (Closed - Implemented) +- [#148](../issues/148) - Dirty Flag RenderTexture Caching (Closed - Implemented) +- [#147](../issues/147) - Dynamic Layer System (Closed - Implemented) +- [#123](../issues/123) - Chunk-based Grid Rendering (Closed - Implemented) +- [#115](../issues/115) - SpatialHash for Entity Queries (Closed - Implemented) + +**Key Files:** +- `src/UIGrid.h` / `src/UIGrid.cpp` - Main grid implementation +- `src/GridLayers.h` / `src/GridLayers.cpp` - ColorLayer and TileLayer +- `src/UIGridPoint.h` - Individual grid cell (walkability, transparency) +- `src/UIGridPointState.h` - Per-entity perspective/knowledge +- `src/SpatialHash.h` / `src/SpatialHash.cpp` - Spatial indexing for entities + +--- + +## Architecture Overview + +### Three-Layer Design + +The Grid System uses a three-layer architecture for sophisticated roguelike features: + +1. **Visual Layer** (Rendering Layers) + - What's displayed: tile sprites, colors, overlays + - Implemented via `ColorLayer` and `TileLayer` objects + - Multiple layers per grid with z_index ordering + - Files: `src/GridLayers.h`, `src/GridLayers.cpp` + +2. **World State Layer** (`TCODMap`) + - Physical properties: walkable, transparent + - Used for pathfinding and FOV calculations + - Integration: libtcod via `src/UIGrid.cpp` + +3. **Perspective Layer** (`UIGridPointState`) + - Per-entity knowledge: what each entity has seen/explored + - Enables fog of war, asymmetric information + - File: `src/UIGridPointState.h` + +--- + +## Creating a Grid + +```python +import mcrfpy + +# Basic grid (gets a default TileLayer at z_index=-1) +grid = mcrfpy.Grid( + grid_size=(50, 50), # 50x50 cells + pos=(100, 100), # Screen position + size=(400, 400) # Viewport size in pixels +) + +# Grid with specific layers passed at creation +terrain = mcrfpy.TileLayer(name="terrain", z_index=-1) +fog = mcrfpy.ColorLayer(name="fog", z_index=1) +grid = mcrfpy.Grid( + grid_size=(50, 50), + pos=(100, 100), + size=(400, 400), + layers=[terrain, fog] +) + +# Grid with no layers (add them later) +grid = mcrfpy.Grid(grid_size=(50, 50), pos=(100, 100), size=(400, 400), layers=[]) + +# Add to scene +scene = mcrfpy.Scene("game") +scene.children.append(grid) +mcrfpy.current_scene = scene +``` + +--- + +## Layer System + +Layers are standalone objects created independently, then added to grids: + +### TileLayer + +Renders per-cell sprite indices from a texture atlas: + +```python +texture = mcrfpy.Texture("assets/kenney_tinydungeon.png") + +# Create standalone layer +terrain = mcrfpy.TileLayer(name="terrain", z_index=-1, texture=texture) + +# Add to grid +grid.add_layer(terrain) + +# Set individual tiles +terrain.set((5, 3), 42) # Set sprite index at cell (5, 3) +index = terrain.at((5, 3)) # Get sprite index: 42 +terrain.fill(0) # Fill entire layer with sprite 0 +terrain.set((5, 3), -1) # Set to -1 for transparent (no tile drawn) +``` + +### ColorLayer + +Renders per-cell RGBA colors (fog of war, highlights, overlays): + +```python +fog = mcrfpy.ColorLayer(name="fog", z_index=1) +grid.add_layer(fog) + +# Set individual cells +fog.set((5, 3), mcrfpy.Color(0, 0, 0, 200)) # Dark fog +fog.set((5, 3), mcrfpy.Color(0, 0, 0, 0)) # Clear (transparent) +color = fog.at((5, 3)) # Get color +fog.fill(mcrfpy.Color(0, 0, 0, 255)) # Fill entire layer black +``` + +### z_index Semantics + +- **Negative z_index:** Renders **below** entities +- **Zero or positive z_index:** Renders **above** entities +- Lower z_index renders first (behind higher z_index) + +```python +# Typical layer stack +background = mcrfpy.ColorLayer(name="bg", z_index=-3) # Furthest back +terrain = mcrfpy.TileLayer(name="terrain", z_index=-2) # Terrain tiles +items = mcrfpy.TileLayer(name="items", z_index=-1) # Items below entities +# --- entities render here (z_index = 0) --- +fog = mcrfpy.ColorLayer(name="fog", z_index=1) # Fog above entities +``` + +### Managing Layers + +```python +# List all layers +for layer in grid.layers: + print(f"{type(layer).__name__} '{layer.name}' at z={layer.z_index}") + +# Get layer by name +terrain = grid.layer("terrain") + +# Remove a layer +grid.remove_layer(fog) +``` + +--- + +## Cell Properties (GridPoint) + +Each cell has world-state properties accessed via `grid.at(x, y)`: + +```python +point = grid.at(10, 15) +point.walkable = True # Can entities walk here? +point.transparent = True # Can see through for FOV? + +# Read properties +print(point.walkable) # True +print(point.transparent) # True + +# List entities at this cell +entities_here = point.entities # List of Entity objects +``` + +--- + +## FOV (Field of View) + +```python +# Set up transparent/opaque cells +for x in range(50): + for y in range(50): + grid.at(x, y).transparent = True + +# Mark walls as opaque +grid.at(5, 5).transparent = False + +# Compute FOV from position +grid.compute_fov((10, 10), radius=8) + +# Query visibility +if grid.is_in_fov((12, 14)): + print("Cell is visible!") +``` + +### FOV Algorithms (`mcrfpy.FOV`) + +- `FOV.BASIC` - Simple raycasting +- `FOV.DIAMOND` - Diamond-shaped +- `FOV.SHADOW` - Shadow casting (recommended) +- `FOV.PERMISSIVE_0` through `FOV.PERMISSIVE_8` - Permissive variants +- `FOV.RESTRICTIVE` - Restrictive precise angle + +```python +grid.compute_fov((10, 10), radius=10, algorithm=mcrfpy.FOV.SHADOW) +``` + +--- + +## Pathfinding + +### A* Pathfinding + +```python +# Set walkable cells +for x in range(50): + for y in range(50): + grid.at(x, y).walkable = True +grid.at(5, 5).walkable = False # Wall + +# Find path (returns AStarPath object) +path = grid.find_path((0, 0), (10, 10)) + +if path: + print(f"Path length: {len(path)} steps") + print(f"Origin: {path.origin}") + print(f"Destination: {path.destination}") + + # Iterate all steps + for step in path: + print(f" Step: ({step.x}, {step.y})") + + # Or walk step-by-step + next_step = path.walk() # Advances and returns next Vector + upcoming = path.peek() # Look at next step without advancing + remaining = path.remaining # Steps remaining +``` + +### Dijkstra Maps + +For multi-target or AI pathfinding: + +```python +# Create Dijkstra map from a root position +dm = grid.get_dijkstra_map((5, 5)) + +# Query distance from root to any position +distance = dm.distance((10, 10)) # Float distance +print(f"Distance: {distance}") + +# Get path from a position back to the root +path = dm.path_from((10, 10)) # List of Vector objects +for step in path: + print(f" ({step.x}, {step.y})") + +# Get single next step toward root +next_step = dm.step_from((10, 10)) # Single Vector + +# Clear cached maps when grid changes +grid.clear_dijkstra_maps() +``` + +--- + +## Entity Management + +Entities live on grids via the `entities` collection: + +```python +# Create and add entity +player = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=0, name="player") +grid.entities.append(player) +print(player.grid == grid) # True + +# Query entities +for entity in grid.entities: + print(f"{entity.name} at ({entity.grid_x}, {entity.grid_y})") + +# Spatial queries (SpatialHash - O(k) complexity) +nearby = grid.entities_in_radius((10, 10), 5.0) +for entity in nearby: + print(f"Nearby: {entity.name}") + +# Remove entity +player.die() # Removes from grid and SpatialHash +``` + +See [[Entity-Management]] for detailed entity documentation. + +--- + +## Camera Control + +```python +# Center viewport on pixel coordinates within grid space +grid.center = (player.grid_x * 16 + 8, player.grid_y * 16 + 8) + +# Or set components individually +grid.center_x = player.grid_x * 16 + 8 +grid.center_y = player.grid_y * 16 + 8 + +# Center on tile coordinates (convenience method) +grid.center_camera((14.5, 8.5)) # Centers on middle of tile (14, 8) + +# Zoom (1.0 = normal, 2.0 = 2x zoom in, 0.5 = zoom out) +grid.zoom = 1.5 + +# Animate camera movement +grid.animate("center_x", target_x, 0.5, mcrfpy.Easing.EASE_IN_OUT) +grid.animate("center_y", target_y, 0.5, mcrfpy.Easing.EASE_IN_OUT) +grid.animate("zoom", 2.0, 0.3, mcrfpy.Easing.EASE_OUT_QUAD) +``` + +--- + +## Mouse Events + +Grids support mouse interaction at both element and cell levels: + +```python +# Element-level events (screen coordinates) +def on_grid_click(pos, button, action): + if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: + print(f"Grid clicked at pixel ({pos.x}, {pos.y})") + +grid.on_click = on_grid_click +grid.on_enter = lambda pos: print("Mouse entered grid") +grid.on_exit = lambda pos: print("Mouse left grid") + +# Cell-level events (grid coordinates) +def on_cell_click(cell_pos, button, action): + if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: + x, y = int(cell_pos.x), int(cell_pos.y) + point = grid.at(x, y) + point.walkable = not point.walkable # Toggle walkability + +grid.on_cell_click = on_cell_click +grid.on_cell_enter = lambda cell_pos: highlight_cell(cell_pos) +grid.on_cell_exit = lambda cell_pos: clear_highlight(cell_pos) + +# Query currently hovered cell +if grid.hovered_cell: + hx, hy = grid.hovered_cell + print(f"Hovering over ({hx}, {hy})") +``` + +See [[Input-and-Events]] for callback signature details. + +--- + +## Perspective System + +Set a perspective entity to enable FOV-based rendering: + +```python +# Set perspective (fog of war from this entity's viewpoint) +grid.perspective = player_entity + +# Disable perspective (show everything) +grid.perspective = None +``` + +--- + +## Level Import Integration + +Grids integrate with external level editors via Tiled and LDtk import systems: + +### Tiled Import + +```python +tileset = mcrfpy.TileSetFile("assets/dungeon.tsj") +tilemap = mcrfpy.TileMapFile("assets/level1.tmj") + +# Apply tilemap layers to grid +for layer_data in tilemap.layers: + tile_layer = mcrfpy.TileLayer(name=layer_data.name, z_index=-1, texture=tileset.texture) + grid.add_layer(tile_layer) + # ... apply tile data +``` + +### Wang Tile Terrain Generation + +```python +wang_set = tileset.wang_set("Terrain") +terrain_data = wang_set.resolve(intgrid) # Resolve terrain to tile indices +wang_set.apply(tile_layer, terrain_data) # Apply to layer +``` + +--- + +## Performance Characteristics + +**Implemented Optimizations:** +- **Chunk-based rendering** ([#123](../issues/123)): Large grids divided into chunks +- **Dirty flag system** ([#148](../issues/148)): Layers track changes, skip redraw when unchanged +- **RenderTexture caching**: Each chunk cached to texture, reused until dirty +- **Viewport culling**: Only cells within viewport are processed +- **SpatialHash** ([#115](../issues/115)): O(k) entity queries instead of O(n) + +**Current Performance:** +- Grids of 1000x1000+ cells render efficiently +- Static scenes near-zero CPU (cached textures reused) +- Entity queries: O(k) where k is nearby entities (not total) + +--- + +## Grid Properties Reference + +| Property | Type | Description | +|----------|------|-------------| +| `grid_size` | `(int, int)` | Grid dimensions (read-only) | +| `grid_w`, `grid_h` | int | Width/height in cells (read-only) | +| `pos` | Vector | Screen position | +| `size` | Vector | Viewport size in pixels | +| `center` | Vector | Camera center (pixel coordinates) | +| `center_x`, `center_y` | float | Camera center components | +| `zoom` | float | Camera zoom level | +| `fill_color` | Color | Background color | +| `perspective` | Entity or None | FOV perspective entity | +| `entities` | EntityCollection | Entities on this grid | +| `layers` | list | Rendering layers (sorted by z_index) | +| `children` | UICollection | UI overlays | +| `hovered_cell` | `(x, y)` or None | Currently hovered cell (read-only) | + +--- + +## Related Systems + +- [[Entity-Management]] - Entities live within Grid containers +- [[Grid-Rendering-Pipeline]] - How grid renders each frame +- [[Grid-TCOD-Integration]] - FOV, pathfinding, walkability details +- [[Grid-Interaction-Patterns]] - Click handling, selection, context menus +- [[Animation-System]] - Grid properties are animatable (pos, zoom, center) +- [[Input-and-Events]] - Mouse callback signatures + +--- + +*Last updated: 2026-02-07* \ No newline at end of file