1 Grid Interaction Patterns
John McCardle edited this page 2025-11-29 23:46:07 +00:00

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 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:

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.

# 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.

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.

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.

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.

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.

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.

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


Last updated: 2025-11-29