Add "Grid Interaction Patterns"

John McCardle 2025-11-29 23:46:07 +00:00
parent be816ac30a
commit bb60cf56a2
1 changed files with 429 additions and 0 deletions

@ -0,0 +1,429 @@
# Grid Interaction Patterns
Patterns for handling mouse and keyboard interaction with Grids, cells, and entities. These patterns build on the grid-specific event handlers.
**Related Pages:**
- [[Grid-System]] - Grid architecture and layers
- [[Entity-Management]] - Entity behavior patterns
- [[Input-and-Events]] - General event handling
---
## Grid Cell Events
Grids provide cell-level mouse events in addition to standard UIDrawable events:
| Property | Signature | Description |
|----------|-----------|-------------|
| `on_cell_click` | `(grid_x, grid_y, button) -> None` | Cell clicked (0=left, 1=right, 2=middle) |
| `on_cell_enter` | `(grid_x, grid_y) -> None` | Mouse enters cell |
| `on_cell_exit` | `(grid_x, grid_y) -> None` | Mouse leaves cell |
| `hovered_cell` | `(x, y)` or `None` | Currently hovered cell (read-only) |
These events use **grid coordinates** (cell indices), not pixel coordinates.
---
## Setup Template
Most grid interaction patterns fit into this structure:
```python
import mcrfpy
# Scene setup
mcrfpy.createScene("game")
ui = mcrfpy.sceneUI("game")
# Create grid with layers
grid = mcrfpy.Grid(
grid_size=(20, 15),
pos=(50, 50),
size=(640, 480),
layers={}
)
grid.fill_color = mcrfpy.Color(20, 20, 30)
ui.append(grid)
# Add terrain layer
terrain = grid.add_layer("tile", z_index=-2)
# terrain.fill(0) # Fill with default tile
# Add highlight layer (above terrain, below entities)
highlight = grid.add_layer("color", z_index=-1)
# Add overlay layer (above entities, for fog/selection)
overlay = grid.add_layer("color", z_index=1)
# Create player entity
player = mcrfpy.Entity(pos=(10, 7), sprite_index=0)
grid.entities.append(player)
# Wire up events (patterns below fill these in)
# grid.on_cell_click = ...
# grid.on_cell_enter = ...
# grid.on_cell_exit = ...
mcrfpy.setScene("game")
```
---
## Cell Hover Highlighting
Show which cell the mouse is over.
```python
# Track currently highlighted cell
current_highlight = [None] # Use list for closure mutability
def on_cell_enter(x, y):
# Highlight new cell
highlight.set(x, y, mcrfpy.Color(255, 255, 255, 40))
current_highlight[0] = (x, y)
def on_cell_exit(x, y):
# Clear old highlight
highlight.set(x, y, mcrfpy.Color(0, 0, 0, 0))
current_highlight[0] = None
grid.on_cell_enter = on_cell_enter
grid.on_cell_exit = on_cell_exit
```
---
## Cell Click Actions
Respond to clicks on specific cells.
```python
def on_cell_click(x, y, button):
point = grid.at(x, y)
if button == 0: # Left click
if point.walkable:
# Move player to clicked cell
player.pos = (x, y)
elif button == 1: # Right click
# Inspect cell
show_cell_info(x, y, point)
elif button == 2: # Middle click
# Toggle walkability (for level editor)
point.walkable = not point.walkable
grid.on_cell_click = on_cell_click
```
---
## WASD Movement
Track key states for smooth entity movement.
```python
class MovementController:
def __init__(self, entity):
self.entity = entity
self.keys = {"W": False, "A": False, "S": False, "D": False}
self.move_delay = 150 # ms between moves
self.last_move = 0
def handle_key(self, key, pressed):
if key in self.keys:
self.keys[key] = pressed
def update(self, current_time):
if current_time - self.last_move < self.move_delay:
return
dx, dy = 0, 0
if self.keys["W"]: dy -= 1
if self.keys["S"]: dy += 1
if self.keys["A"]: dx -= 1
if self.keys["D"]: dx += 1
if dx == 0 and dy == 0:
return
new_x = self.entity.x + dx
new_y = self.entity.y + dy
# Check walkability
point = self.entity.grid.at(new_x, new_y)
if point and point.walkable:
self.entity.pos = (new_x, new_y)
self.last_move = current_time
# Setup
controller = MovementController(player)
mcrfpy.keypressScene(controller.handle_key)
def game_update(dt):
import time
controller.update(time.time() * 1000)
mcrfpy.setTimer("movement", game_update, 16) # ~60fps
```
---
## Entity Selection
Click to select entities, show selection indicator.
```python
class SelectionManager:
def __init__(self, grid, overlay_layer):
self.grid = grid
self.overlay = overlay_layer
self.selected = None
def select(self, entity):
# Clear previous selection
if self.selected:
self._clear_indicator(self.selected)
self.selected = entity
if entity:
self._draw_indicator(entity)
def _draw_indicator(self, entity):
x, y = entity.x, entity.y
self.overlay.set(x, y, mcrfpy.Color(255, 200, 0, 80))
def _clear_indicator(self, entity):
x, y = entity.x, entity.y
self.overlay.set(x, y, mcrfpy.Color(0, 0, 0, 0))
def update_indicator(self):
"""Call after selected entity moves."""
if self.selected:
# Clear all overlay first (simple approach)
self.overlay.fill(mcrfpy.Color(0, 0, 0, 0))
self._draw_indicator(self.selected)
selection = SelectionManager(grid, overlay)
def on_cell_click(x, y, button):
if button == 0: # Left click
# Find entity at position
for entity in grid.entities:
if entity.x == x and entity.y == y:
selection.select(entity)
return
# Clicked empty cell - deselect
selection.select(None)
grid.on_cell_click = on_cell_click
```
---
## Path Preview
Show pathfinding path on hover.
```python
class PathPreview:
def __init__(self, grid, highlight_layer, source_entity):
self.grid = grid
self.highlight = highlight_layer
self.source = source_entity
self.current_path = []
def show_path_to(self, target_x, target_y):
# Clear previous path
self.clear()
# Calculate path
path = self.source.path_to((target_x, target_y))
if not path:
return
self.current_path = path
# Draw path cells
for i, (x, y) in enumerate(path):
if i == 0:
continue # Skip source cell
alpha = 100 - (i * 5) # Fade with distance
alpha = max(30, alpha)
self.highlight.set(x, y, mcrfpy.Color(100, 200, 255, alpha))
def clear(self):
for x, y in self.current_path:
self.highlight.set(x, y, mcrfpy.Color(0, 0, 0, 0))
self.current_path = []
path_preview = PathPreview(grid, highlight, player)
def on_cell_enter(x, y):
path_preview.show_path_to(x, y)
def on_cell_exit(x, y):
pass # Path updates on enter, so no action needed
grid.on_cell_enter = on_cell_enter
```
---
## Context Menu
Right-click to show context-sensitive options.
```python
class ContextMenu:
def __init__(self, scene_ui):
self.ui = scene_ui
self.frame = None
self.options = []
def show(self, x, y, options):
"""
options: list of (label, callback) tuples
x, y: screen coordinates
"""
self.close() # Close any existing menu
height = len(options) * 24 + 8
self.frame = mcrfpy.Frame(pos=(x, y), size=(120, height))
self.frame.fill_color = mcrfpy.Color(40, 40, 50)
self.frame.outline = 1
self.frame.outline_color = mcrfpy.Color(80, 80, 100)
self.frame.z_index = 500
self.ui.append(self.frame)
for i, (label, callback) in enumerate(options):
item = mcrfpy.Frame(pos=(2, 2 + i * 24), size=(116, 22))
item.fill_color = mcrfpy.Color(40, 40, 50)
self.frame.children.append(item)
text = mcrfpy.Caption(pos=(8, 3), text=label)
text.fill_color = mcrfpy.Color(200, 200, 200)
item.children.append(text)
# Hover effect
item.on_enter = lambda i=item: setattr(i, 'fill_color', mcrfpy.Color(60, 60, 80))
item.on_exit = lambda i=item: setattr(i, 'fill_color', mcrfpy.Color(40, 40, 50))
# Click handler
def make_handler(cb):
return lambda x, y, btn: (cb(), self.close())
item.on_click = make_handler(callback)
def close(self):
if self.frame:
self.ui.remove(self.frame)
self.frame = None
context_menu = ContextMenu(ui)
def on_cell_click(x, y, button):
if button == 1: # Right click
# Find entity at position
target = None
for entity in grid.entities:
if entity.x == x and entity.y == y:
target = entity
break
# Build context menu
mouse_x, mouse_y = mcrfpy.getMousePos()
if target and target != player:
options = [
("Examine", lambda: examine(target)),
("Attack", lambda: attack(target)),
("Talk", lambda: talk(target)),
]
else:
point = grid.at(x, y)
options = [
("Move here", lambda: player.pos.__setitem__(slice(None), (x, y))),
("Examine ground", lambda: examine_cell(x, y)),
]
context_menu.show(mouse_x, mouse_y, options)
elif button == 0: # Left click closes menu
context_menu.close()
grid.on_cell_click = on_cell_click
```
---
## Tile Inspector Panel
Click cell to show information panel.
```python
class TileInspector:
def __init__(self, parent, pos):
self.frame = mcrfpy.Frame(pos=pos, size=(200, 150))
self.frame.fill_color = mcrfpy.Color(30, 30, 40, 230)
self.frame.outline = 1
self.frame.outline_color = mcrfpy.Color(80, 80, 100)
self.frame.visible = False
parent.append(self.frame)
self.title = mcrfpy.Caption(pos=(10, 8), text="Cell Info")
self.title.fill_color = mcrfpy.Color(180, 180, 200)
self.frame.children.append(self.title)
self.info_lines = []
for i in range(5):
line = mcrfpy.Caption(pos=(10, 30 + i * 20), text="")
line.fill_color = mcrfpy.Color(160, 160, 180)
self.frame.children.append(line)
self.info_lines.append(line)
def show(self, grid, x, y):
point = grid.at(x, y)
self.title.text = f"Cell ({x}, {y})"
self.info_lines[0].text = f"Walkable: {point.walkable}"
self.info_lines[1].text = f"Transparent: {point.transparent}"
# Count entities at this position
entities_here = [e for e in grid.entities if e.x == x and e.y == y]
self.info_lines[2].text = f"Entities: {len(entities_here)}"
if entities_here:
self.info_lines[3].text = f" {entities_here[0].name or 'unnamed'}"
else:
self.info_lines[3].text = ""
self.info_lines[4].text = ""
self.frame.visible = True
def hide(self):
self.frame.visible = False
inspector = TileInspector(ui, (700, 50))
def on_cell_click(x, y, button):
if button == 0:
inspector.show(grid, x, y)
grid.on_cell_click = on_cell_click
```
---
## Related Pages
- [[Entity-Management]] - Entity behavior and pathfinding
- [[Grid-System]] - Layer management and rendering
- [[UI-Widget-Patterns]] - Non-grid UI patterns
- [[Input-and-Events]] - Event API reference
---
*Last updated: 2025-11-29*