1 Input-and-Events
John McCardle edited this page 2025-10-25 22:44:42 +00:00

Input and Events

Overview

McRogueFace provides keyboard, mouse, and window event handling through Python callbacks. Events are dispatched through the scene system, allowing different scenes to have different input handlers.

Related Pages:

Key Files:

  • src/GameEngine.cpp::processEvent() - Event dispatch
  • src/Scene.cpp::input() - Scene input handling
  • src/McRFPy_API.cpp - Python callback registration

Related Issues:

  • #128 - Middle-click entity selection
  • #1 - Window resize events

Keyboard Input

Scene-Level Keyboard Callbacks

Register a callback that fires on every key press/release:

import mcrfpy

def handle_key(key: str, pressed: bool):
    """
    key: Key name (e.g., "W", "Space", "Escape", "Up")
    pressed: True on press, False on release
    """
    if key == "Escape" and pressed:
        print("Escape pressed!")
        mcrfpy.setScene("menu")
    
    if key == "W":
        if pressed:
            player.start_moving_north()
        else:
            player.stop_moving_north()

# Register for current scene
mcrfpy.keypressScene(handle_key)

Key Names

Common key names from SFML:

  • Letters: "A", "B", "C", ... "Z"
  • Numbers: "Num0", "Num1", ... "Num9"
  • Arrows: "Up", "Down", "Left", "Right"
  • Special: "Space", "Enter", "Escape", "Tab", "LShift", "RShift", "LControl", "RControl"
  • Function: "F1", "F2", ... "F12"

See src/McRFPy_API.cpp::McRFPy_API_keypressScene() for full key mapping.


Mouse Input

Click Events

Mouse clicks are dispatched to UI elements based on screen position:

import mcrfpy

# Create a frame with click handler
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 50))

def on_frame_click(button: int, x: int, y: int):
    """
    button: 0=left, 1=right, 2=middle
    x, y: Mouse position in window coordinates
    """
    print(f"Frame clicked with button {button} at ({x}, {y})")

frame.click = on_frame_click

Entity Click Handlers

Entities can register click callbacks for grid-based interaction:

player = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=1)

def on_entity_click(button: int):
    if button == 0:  # Left-click
        print("Selected player!")
    elif button == 1:  # Right-click
        print("Context menu for player")
    elif button == 2:  # Middle-click
        print("Middle-clicked entity")

player.click = on_entity_click

Issue #128 adds proper middle-click support for entity selection.

Mouse Position

Get current mouse position in game coordinates:

# Window coordinates (pixels)
window_x, window_y = mcrfpy.getMousePos()

# Convert to game coordinates (respects viewport)
game_x, game_y = mcrfpy.windowToGameCoords(window_x, window_y)

Window Events

Resize Events

Issue #1 tracks window resize event support. Current workaround:

def update_on_resize(ms):
    """Timer-based resize detection"""
    current_size = mcrfpy.getWindowSize()
    if current_size != last_size:
        on_window_resized(current_size)

mcrfpy.setTimer("resize_check", update_on_resize, 100)

Focus Events

Window focus/unfocus events are not currently exposed to Python. The engine does not pause automatically when focus is lost.


Event Priority and Propagation

Click Dispatch Order

Clicks are dispatched in reverse render order (top-to-bottom):

  1. UI elements with highest z_index receive clicks first
  2. If a UI element handles the click, propagation stops
  3. Entities on grids receive clicks if no UI element handled it
  4. Grid cells receive clicks last
# High z_index UI blocks clicks to entities below
overlay = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), z_index=100)
overlay.click = lambda btn, x, y: True  # Blocks all clicks

# Remove overlay to allow clicks through
ui.remove(overlay)

Keyboard Priority

Keyboard events are dispatched only to the current scene's registered callback. There is no concept of "focused" UI elements for keyboard input.


Common Patterns

Modal Dialog

Capture all input while a dialog is open:

class ModalDialog:
    def __init__(self, message: str):
        self.frame = mcrfpy.Frame(pos=(300, 200), size=(400, 200), z_index=1000)
        self.caption = mcrfpy.Caption(text=message, pos=(310, 220))
        self.frame.click = self._on_click
        
        # Save previous input handlers
        self.prev_keypress = mcrfpy.keypressScene(self._on_key)
        
        ui = mcrfpy.sceneUI("game")
        ui.append(self.frame)
        ui.append(self.caption)
    
    def _on_click(self, btn, x, y):
        return True  # Block all clicks
    
    def _on_key(self, key, pressed):
        if key == "Enter" and pressed:
            self.close()
        return True  # Block all keys
    
    def close(self):
        ui = mcrfpy.sceneUI("game")
        ui.remove(self.frame)
        ui.remove(self.caption)
        mcrfpy.keypressScene(self.prev_keypress)  # Restore

WASD Movement

Handle movement keys with press/release state:

class PlayerController:
    def __init__(self, entity):
        self.entity = entity
        self.moving = {"W": False, "A": False, "S": False, "D": False}
    
    def handle_key(self, key, pressed):
        if key in self.moving:
            self.moving[key] = pressed
            self._update_velocity()
    
    def _update_velocity(self):
        vx = 0
        vy = 0
        if self.moving["W"]: vy -= 1
        if self.moving["S"]: vy += 1
        if self.moving["A"]: vx -= 1
        if self.moving["D"]: vx += 1
        
        # Normalize diagonal movement
        import math
        magnitude = math.sqrt(vx*vx + vy*vy)
        if magnitude > 0:
            vx /= magnitude
            vy /= magnitude
        
        self.entity.velocity_x = vx * 5.0
        self.entity.velocity_y = vy * 5.0

controller = PlayerController(player)
mcrfpy.keypressScene(controller.handle_key)

Context Menu

Right-click to show context-sensitive options:

def show_context_menu(entity, x, y):
    """Show menu at mouse position"""
    menu_frame = mcrfpy.Frame(pos=(x, y), size=(150, 100), z_index=500)
    
    # Menu options
    option1 = mcrfpy.Caption(text="Examine", pos=(x+10, y+10))
    option2 = mcrfpy.Caption(text="Attack", pos=(x+10, y+30))
    option3 = mcrfpy.Caption(text="Talk", pos=(x+10, y+50))
    
    def on_menu_click(btn, mx, my):
        # Determine which option was clicked
        if y+10 <= my < y+30:
            entity.examine()
        elif y+30 <= my < y+50:
            entity.attack()
        elif y+50 <= my < y+70:
            entity.talk()
        
        # Close menu
        ui = mcrfpy.sceneUI("game")
        ui.remove(menu_frame)
        ui.remove(option1)
        ui.remove(option2)
        ui.remove(option3)
    
    menu_frame.click = on_menu_click
    
    ui = mcrfpy.sceneUI("game")
    ui.append(menu_frame)
    ui.append(option1)
    ui.append(option2)
    ui.append(option3)

# Entity right-click handler
def entity_right_click(button):
    if button == 1:  # Right-click
        mx, my = mcrfpy.getMousePos()
        show_context_menu(entity, mx, my)

entity.click = entity_right_click

Hotbar / Quick Slots

Number keys for quick item access:

hotbar = [None] * 10  # 10 slots

def handle_hotbar(key, pressed):
    if pressed and key.startswith("Num"):
        slot = int(key[3:])  # "Num1" -> 1
        if 0 <= slot < 10 and hotbar[slot]:
            hotbar[slot].use()

mcrfpy.keypressScene(handle_hotbar)

Testing Input

Automation API

Use the automation API to simulate input in tests:

import mcrfpy
from mcrfpy import automation

# Simulate keyboard input
automation.keypress("W", True)   # Press W
automation.keypress("W", False)  # Release W

# Simulate mouse clicks
automation.click(100, 200, button=0)  # Left-click at (100, 200)
automation.click(150, 250, button=1)  # Right-click

# Wait for effects
import time
time.sleep(0.1)

# Verify results with screenshot
automation.screenshot("test_result.png")

See Writing-Tests for complete testing patterns.


Performance Considerations

Event Handler Complexity

Event handlers run on the main thread and block rendering:

# BAD: Expensive computation in event handler
def handle_key(key, pressed):
    if key == "Space" and pressed:
        # This will freeze the game!
        for i in range(1000000):
            expensive_calculation()

# GOOD: Defer expensive work to timer
def handle_key(key, pressed):
    if key == "Space" and pressed:
        mcrfpy.setTimer("deferred_work", do_expensive_work, 10)

Click Handler Optimization

Avoid creating handlers that search large collections:

# BAD: O(n) search on every click
def on_click(btn, x, y):
    for entity in all_entities:  # Expensive!
        if entity.contains_point(x, y):
            entity.select()

# GOOD: Use spatial data structures (see [[Grid-System]])
def on_click(btn, x, y):
    grid_x, grid_y = screen_to_grid(x, y)
    entity = grid.get_entity_at(grid_x, grid_y)  # O(1)
    if entity:
        entity.select()

API Reference

See docs/api_reference_dynamic.html for complete input API documentation.

Key Functions:

  • mcrfpy.keypressScene(callback) - Register keyboard handler
  • mcrfpy.getMousePos() -> (int, int) - Get mouse position
  • mcrfpy.windowToGameCoords(x, y) -> (float, float) - Convert coordinates

UI Element Properties:

  • element.click = callback - Click handler for any UIDrawable
  • entity.click = callback - Click handler for entities

Navigation: