From 45cee1608144f95ad3d10f7e49f2bf7fae83eca7 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sat, 7 Feb 2026 22:21:32 +0000 Subject: [PATCH] Update "Grid-Interaction-Patterns.-" --- Grid-Interaction-Patterns.-.md | 303 +++++++++++++++++++++++ Grid-Interaction-Patterns.md | 429 --------------------------------- 2 files changed, 303 insertions(+), 429 deletions(-) create mode 100644 Grid-Interaction-Patterns.-.md delete mode 100644 Grid-Interaction-Patterns.md diff --git a/Grid-Interaction-Patterns.-.md b/Grid-Interaction-Patterns.-.md new file mode 100644 index 0000000..b5069e3 --- /dev/null +++ b/Grid-Interaction-Patterns.-.md @@ -0,0 +1,303 @@ +# 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` | `(cell_pos: Vector, button: MouseButton, action: InputState) -> None` | Cell clicked | +| `on_cell_enter` | `(cell_pos: Vector) -> None` | Mouse enters cell | +| `on_cell_exit` | `(cell_pos: Vector) -> None` | Mouse leaves cell | +| `hovered_cell` | `(x, y)` or `None` | Currently hovered cell (read-only) | + +Cell positions arrive as `Vector` objects. Use `int(cell_pos.x)`, `int(cell_pos.y)` to get grid coordinates. + +--- + +## Setup Template + +Most grid interaction patterns fit into this structure: + +```python +import mcrfpy + +# Scene setup +scene = mcrfpy.Scene("game") +ui = scene.children + +# Load tileset texture +texture = mcrfpy.Texture("assets/sprites/tileset.png", 16, 16) + +# Create layers +terrain = mcrfpy.TileLayer(name="terrain", z_index=-2, texture=texture) +highlight = mcrfpy.ColorLayer(name="highlight", z_index=-1) +overlay = mcrfpy.ColorLayer(name="overlay", z_index=1) + +# Create grid with layers +grid = mcrfpy.Grid( + grid_size=(20, 15), + pos=(50, 50), + size=(640, 480), + layers=[terrain, highlight, overlay] +) +grid.fill_color = mcrfpy.Color(20, 20, 30) +ui.append(grid) + +# Create player entity +player = mcrfpy.Entity(grid_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.current_scene = scene +``` + +--- + +## Cell Hover Highlighting + +Show which cell the mouse is over using a ColorLayer. + +```python +current_highlight = [None] + +def on_cell_enter(cell_pos): + x, y = int(cell_pos.x), int(cell_pos.y) + highlight.set((x, y), mcrfpy.Color(255, 255, 255, 40)) + current_highlight[0] = (x, y) + +def on_cell_exit(cell_pos): + x, y = int(cell_pos.x), int(cell_pos.y) + 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. Callbacks receive `(cell_pos, button, action)`. + +```python +def on_cell_click(cell_pos, button, action): + x, y = int(cell_pos.x), int(cell_pos.y) + point = grid.at(x, y) + + if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: + if point.walkable: + player.grid_x = x + player.grid_y = y + + elif button == mcrfpy.MouseButton.RIGHT and action == mcrfpy.InputState.PRESSED: + # Inspect cell + show_cell_info(x, y, point) + + elif button == mcrfpy.MouseButton.MIDDLE and action == mcrfpy.InputState.PRESSED: + # Toggle walkability (level editor) + point.walkable = not point.walkable + +grid.on_cell_click = on_cell_click +``` + +--- + +## WASD Movement + +Use `scene.on_key` with Key enums for movement. + +```python +move_map = { + mcrfpy.Key.W: (0, -1), + mcrfpy.Key.A: (-1, 0), + mcrfpy.Key.S: (0, 1), + mcrfpy.Key.D: (1, 0), +} + +def handle_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + if key in move_map: + dx, dy = move_map[key] + new_x = int(player.grid_x + dx) + new_y = int(player.grid_y + dy) + point = player.grid.at(new_x, new_y) + if point and point.walkable: + player.grid_x = new_x + player.grid_y = new_y + +scene.on_key = handle_key +``` + +### Smooth Animated Movement + +For visual smoothness, animate the entity position: + +```python +def handle_key(key, action): + if action != mcrfpy.InputState.PRESSED: + return + if key in move_map: + dx, dy = move_map[key] + new_x = int(player.grid_x + dx) + new_y = int(player.grid_y + dy) + point = player.grid.at(new_x, new_y) + if point and point.walkable: + player.animate("x", float(new_x), 0.15, mcrfpy.Easing.EASE_OUT_QUAD) + player.animate("y", float(new_y), 0.15, mcrfpy.Easing.EASE_OUT_QUAD) + player.grid_x = new_x + player.grid_y = new_y + +scene.on_key = handle_key +``` + +--- + +## Entity Selection + +Click to select entities using a ColorLayer overlay. + +```python +selected = [None] + +def select_entity(entity): + # Clear previous selection + if selected[0]: + ex, ey = int(selected[0].grid_x), int(selected[0].grid_y) + overlay.set((ex, ey), mcrfpy.Color(0, 0, 0, 0)) + + selected[0] = entity + + if entity: + overlay.set((int(entity.grid_x), int(entity.grid_y)), + mcrfpy.Color(255, 200, 0, 80)) + +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) + for entity in grid.entities: + if int(entity.grid_x) == x and int(entity.grid_y) == y: + select_entity(entity) + return + select_entity(None) + +grid.on_cell_click = on_cell_click +``` + +--- + +## Path Preview + +Show pathfinding path on hover using a ColorLayer. + +```python +current_path_cells = [] + +def show_path_to(target_x, target_y): + global current_path_cells + # Clear previous path + for cx, cy in current_path_cells: + highlight.set((cx, cy), mcrfpy.Color(0, 0, 0, 0)) + current_path_cells = [] + + # Calculate path + path = grid.find_path( + (int(player.grid_x), int(player.grid_y)), + (target_x, target_y) + ) + if not path: + return + + # Draw path cells + i = 0 + while len(path) > 0: + step = path.walk() + if step: + x, y = int(step.x), int(step.y) + alpha = max(30, 100 - (i * 5)) + highlight.set((x, y), mcrfpy.Color(100, 200, 255, alpha)) + current_path_cells.append((x, y)) + i += 1 + +def on_cell_enter(cell_pos): + show_path_to(int(cell_pos.x), int(cell_pos.y)) + +grid.on_cell_enter = on_cell_enter +``` + +--- + +## Tile Inspector Panel + +Click a cell to show information in a side panel. + +```python +# Create inspector UI +inspector = mcrfpy.Frame(pos=(700, 50), size=(200, 150), + fill_color=mcrfpy.Color(30, 30, 40, 230)) +inspector.outline = 1 +inspector.outline_color = mcrfpy.Color(80, 80, 100) +inspector.visible = False +ui.append(inspector) + +title = mcrfpy.Caption(text="Cell Info", pos=(10, 8)) +title.fill_color = mcrfpy.Color(180, 180, 200) +inspector.children.append(title) + +info_lines = [] +for i in range(4): + line = mcrfpy.Caption(text="", pos=(10, 30 + i * 20)) + line.fill_color = mcrfpy.Color(160, 160, 180) + inspector.children.append(line) + info_lines.append(line) + +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) + + title.text = f"Cell ({x}, {y})" + info_lines[0].text = f"Walkable: {point.walkable}" + info_lines[1].text = f"Transparent: {point.transparent}" + + entities_here = [e for e in grid.entities + if int(e.grid_x) == x and int(e.grid_y) == y] + info_lines[2].text = f"Entities: {len(entities_here)}" + + if entities_here: + info_lines[3].text = f" {entities_here[0].name or 'unnamed'}" + else: + info_lines[3].text = "" + + inspector.visible = True + +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: 2026-02-07* \ No newline at end of file diff --git a/Grid-Interaction-Patterns.md b/Grid-Interaction-Patterns.md deleted file mode 100644 index bf25296..0000000 --- a/Grid-Interaction-Patterns.md +++ /dev/null @@ -1,429 +0,0 @@ -# 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