diff --git a/Grid-Interaction-Patterns.md b/Grid-Interaction-Patterns.md new file mode 100644 index 0000000..526ac96 --- /dev/null +++ b/Grid-Interaction-Patterns.md @@ -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* \ No newline at end of file