diff --git a/docs/API_REFERENCE_COMPLETE.md b/docs/API_REFERENCE_COMPLETE.md
new file mode 100644
index 0000000..e50d008
--- /dev/null
+++ b/docs/API_REFERENCE_COMPLETE.md
@@ -0,0 +1,1156 @@
+# McRogueFace API Reference
+
+## Overview
+
+McRogueFace Python API
+
+Core game engine interface for creating roguelike games with Python.
+
+This module provides:
+- Scene management (createScene, setScene, currentScene)
+- UI components (Frame, Caption, Sprite, Grid)
+- Entity system for game objects
+- Audio playback (sound effects and music)
+- Timer system for scheduled events
+- Input handling
+- Performance metrics
+
+Example:
+ import mcrfpy
+
+ # Create a new scene
+ mcrfpy.createScene('game')
+ mcrfpy.setScene('game')
+
+ # Add UI elements
+ frame = mcrfpy.Frame(10, 10, 200, 100)
+ caption = mcrfpy.Caption('Hello World', 50, 50)
+ mcrfpy.sceneUI().extend([frame, caption])
+
+
+## Table of Contents
+
+- [Functions](#functions)
+ - [Scene Management](#scene-management)
+ - [Audio](#audio)
+ - [UI Utilities](#ui-utilities)
+ - [System](#system)
+- [Classes](#classes)
+ - [UI Components](#ui-components)
+ - [Collections](#collections)
+ - [System Types](#system-types)
+ - [Other Classes](#other-classes)
+- [Automation Module](#automation-module)
+
+## Functions
+
+### Scene Management
+
+### `createScene(name: str) -> None`
+
+Create a new empty scene with the given name.
+
+**Arguments:**
+- `name` (*str*): Unique name for the new scene
+
+**Raises:** ValueError: If a scene with this name already exists
+
+**Note:** The scene is created but not made active. Use setScene() to switch to it.
+
+**Example:**
+```python
+mcrfpy.createScene("game_over")
+```
+
+---
+
+### `setScene(scene: str, transition: str = None, duration: float = 0.0) -> None`
+
+Switch to a different scene with optional transition effect.
+
+**Arguments:**
+- `scene` (*str*): Name of the scene to switch to
+- `transition` (*str*): Transition type: "fade", "slide_left", "slide_right", "slide_up", "slide_down"
+- `duration` (*float*): Transition duration in seconds (default: 0.0 for instant)
+
+**Raises:** KeyError: If the scene doesn't exist
+
+**Example:**
+```python
+mcrfpy.setScene("game", "fade", 0.5)
+```
+
+---
+
+### `currentScene() -> str`
+
+Get the name of the currently active scene.
+
+**Returns:** str: Name of the current scene
+
+**Example:**
+```python
+scene_name = mcrfpy.currentScene()
+```
+
+---
+
+### `sceneUI(scene: str = None) -> UICollection`
+
+Get all UI elements for a scene.
+
+**Arguments:**
+- `scene` (*str*): Scene name. If None, uses current scene
+
+**Returns:** UICollection: All UI elements in the scene
+
+**Raises:** KeyError: If the specified scene doesn't exist
+
+**Example:**
+```python
+ui_elements = mcrfpy.sceneUI("game")
+```
+
+---
+
+### `keypressScene(handler: callable) -> None`
+
+Set the keyboard event handler for the current scene.
+
+**Arguments:**
+- `handler` (*callable*): Function that receives (key_name: str, is_pressed: bool)
+
+**Example:**
+```python
+def on_key(key, pressed):
+ if key == "SPACE" and pressed:
+ player.jump()
+mcrfpy.keypressScene(on_key)
+```
+
+---
+
+### Audio
+
+### `createSoundBuffer(filename: str) -> int`
+
+Load a sound effect from a file and return its buffer ID.
+
+**Arguments:**
+- `filename` (*str*): Path to the sound file (WAV, OGG, FLAC)
+
+**Returns:** int: Buffer ID for use with playSound()
+
+**Raises:** RuntimeError: If the file cannot be loaded
+
+**Example:**
+```python
+jump_sound = mcrfpy.createSoundBuffer("assets/jump.wav")
+```
+
+---
+
+### `loadMusic(filename: str, loop: bool = True) -> None`
+
+Load and immediately play background music from a file.
+
+**Arguments:**
+- `filename` (*str*): Path to the music file (WAV, OGG, FLAC)
+- `loop` (*bool*): Whether to loop the music (default: True)
+
+**Note:** Only one music track can play at a time. Loading new music stops the current track.
+
+**Example:**
+```python
+mcrfpy.loadMusic("assets/background.ogg", True)
+```
+
+---
+
+### `playSound(buffer_id: int) -> None`
+
+Play a sound effect using a previously loaded buffer.
+
+**Arguments:**
+- `buffer_id` (*int*): Sound buffer ID returned by createSoundBuffer()
+
+**Raises:** RuntimeError: If the buffer ID is invalid
+
+**Example:**
+```python
+mcrfpy.playSound(jump_sound)
+```
+
+---
+
+### `getMusicVolume() -> int`
+
+Get the current music volume level.
+
+**Returns:** int: Current volume (0-100)
+
+**Example:**
+```python
+current_volume = mcrfpy.getMusicVolume()
+```
+
+---
+
+### `getSoundVolume() -> int`
+
+Get the current sound effects volume level.
+
+**Returns:** int: Current volume (0-100)
+
+**Example:**
+```python
+current_volume = mcrfpy.getSoundVolume()
+```
+
+---
+
+### `setMusicVolume(volume: int) -> None`
+
+Set the global music volume.
+
+**Arguments:**
+- `volume` (*int*): Volume level from 0 (silent) to 100 (full volume)
+
+**Example:**
+```python
+mcrfpy.setMusicVolume(50) # Set to 50% volume
+```
+
+---
+
+### `setSoundVolume(volume: int) -> None`
+
+Set the global sound effects volume.
+
+**Arguments:**
+- `volume` (*int*): Volume level from 0 (silent) to 100 (full volume)
+
+**Example:**
+```python
+mcrfpy.setSoundVolume(75) # Set to 75% volume
+```
+
+---
+
+### UI Utilities
+
+### `find(name: str, scene: str = None) -> UIDrawable | None`
+
+Find the first UI element with the specified name.
+
+**Arguments:**
+- `name` (*str*): Exact name to search for
+- `scene` (*str*): Scene to search in (default: current scene)
+
+**Returns:** UIDrawable or None: The found element, or None if not found
+
+**Note:** Searches scene UI elements and entities within grids.
+
+**Example:**
+```python
+button = mcrfpy.find("start_button")
+```
+
+---
+
+### `findAll(pattern: str, scene: str = None) -> list`
+
+Find all UI elements matching a name pattern.
+
+**Arguments:**
+- `pattern` (*str*): Name pattern with optional wildcards (* matches any characters)
+- `scene` (*str*): Scene to search in (default: current scene)
+
+**Returns:** list: All matching UI elements and entities
+
+**Example:**
+```python
+enemies = mcrfpy.findAll("enemy_*")
+```
+
+---
+
+### System
+
+### `exit() -> None`
+
+Cleanly shut down the game engine and exit the application.
+
+**Note:** This immediately closes the window and terminates the program.
+
+**Example:**
+```python
+mcrfpy.exit()
+```
+
+---
+
+### `getMetrics() -> dict`
+
+Get current performance metrics.
+
+**Returns:** dict: Performance data with keys:
+- frame_time: Last frame duration in seconds
+- avg_frame_time: Average frame time
+- fps: Frames per second
+- draw_calls: Number of draw calls
+- ui_elements: Total UI element count
+- visible_elements: Visible element count
+- current_frame: Frame counter
+- runtime: Total runtime in seconds
+
+**Example:**
+```python
+metrics = mcrfpy.getMetrics()
+```
+
+---
+
+### `setTimer(name: str, handler: callable, interval: int) -> None`
+
+Create or update a recurring timer.
+
+**Arguments:**
+- `name` (*str*): Unique identifier for the timer
+- `handler` (*callable*): Function called with (runtime: float) parameter
+- `interval` (*int*): Time between calls in milliseconds
+
+**Note:** If a timer with this name exists, it will be replaced.
+
+**Example:**
+```python
+def update_score(runtime):
+ score += 1
+mcrfpy.setTimer("score_update", update_score, 1000)
+```
+
+---
+
+### `delTimer(name: str) -> None`
+
+Stop and remove a timer.
+
+**Arguments:**
+- `name` (*str*): Timer identifier to remove
+
+**Note:** No error is raised if the timer doesn't exist.
+
+**Example:**
+```python
+mcrfpy.delTimer("score_update")
+```
+
+---
+
+### `setScale(multiplier: float) -> None`
+
+Scale the game window size.
+
+**Arguments:**
+- `multiplier` (*float*): Scale factor (e.g., 2.0 for double size)
+
+**Note:** The internal resolution remains 1024x768, but the window is scaled.
+
+**Example:**
+```python
+mcrfpy.setScale(2.0) # Double the window size
+```
+
+---
+
+## Classes
+
+### UI Components
+
+### class `Frame`
+
+A rectangular frame UI element that can contain other drawable elements.
+
+#### Methods
+
+#### `get_bounds()`
+
+Get the bounding rectangle of this drawable element.
+
+**Returns:** tuple: (x, y, width, height) representing the element's bounds
+
+**Note:** The bounds are in screen coordinates and account for current position and size.
+
+#### `resize(width, height)`
+
+Resize the element to new dimensions.
+
+**Arguments:**
+- `width` (*float*): New width in pixels
+- `height` (*float*): New height in pixels
+
+**Note:** For Caption and Sprite, this may not change actual size if determined by content.
+
+#### `move(dx, dy)`
+
+Move the element by a relative offset.
+
+**Arguments:**
+- `dx` (*float*): Horizontal offset in pixels
+- `dy` (*float*): Vertical offset in pixels
+
+**Note:** This modifies the x and y position properties by the given amounts.
+
+---
+
+### class `Caption`
+
+A text display UI element with customizable font and styling.
+
+#### Methods
+
+#### `get_bounds()`
+
+Get the bounding rectangle of this drawable element.
+
+**Returns:** tuple: (x, y, width, height) representing the element's bounds
+
+**Note:** The bounds are in screen coordinates and account for current position and size.
+
+#### `resize(width, height)`
+
+Resize the element to new dimensions.
+
+**Arguments:**
+- `width` (*float*): New width in pixels
+- `height` (*float*): New height in pixels
+
+**Note:** For Caption and Sprite, this may not change actual size if determined by content.
+
+#### `move(dx, dy)`
+
+Move the element by a relative offset.
+
+**Arguments:**
+- `dx` (*float*): Horizontal offset in pixels
+- `dy` (*float*): Vertical offset in pixels
+
+**Note:** This modifies the x and y position properties by the given amounts.
+
+---
+
+### class `Sprite`
+
+A sprite UI element that displays a texture or portion of a texture atlas.
+
+#### Methods
+
+#### `get_bounds()`
+
+Get the bounding rectangle of this drawable element.
+
+**Returns:** tuple: (x, y, width, height) representing the element's bounds
+
+**Note:** The bounds are in screen coordinates and account for current position and size.
+
+#### `resize(width, height)`
+
+Resize the element to new dimensions.
+
+**Arguments:**
+- `width` (*float*): New width in pixels
+- `height` (*float*): New height in pixels
+
+**Note:** For Caption and Sprite, this may not change actual size if determined by content.
+
+#### `move(dx, dy)`
+
+Move the element by a relative offset.
+
+**Arguments:**
+- `dx` (*float*): Horizontal offset in pixels
+- `dy` (*float*): Vertical offset in pixels
+
+**Note:** This modifies the x and y position properties by the given amounts.
+
+---
+
+### class `Grid`
+
+A grid-based tilemap UI element for rendering tile-based levels and game worlds.
+
+#### Methods
+
+#### `at(x, y)`
+
+Get the GridPoint at the specified grid coordinates.
+
+**Arguments:**
+- `x` (*int*): Grid x coordinate
+- `y` (*int*): Grid y coordinate
+
+**Returns:** GridPoint or None: The grid point at (x, y), or None if out of bounds
+
+#### `get_bounds()`
+
+Get the bounding rectangle of this drawable element.
+
+**Returns:** tuple: (x, y, width, height) representing the element's bounds
+
+**Note:** The bounds are in screen coordinates and account for current position and size.
+
+#### `resize(width, height)`
+
+Resize the element to new dimensions.
+
+**Arguments:**
+- `width` (*float*): New width in pixels
+- `height` (*float*): New height in pixels
+
+**Note:** For Caption and Sprite, this may not change actual size if determined by content.
+
+#### `move(dx, dy)`
+
+Move the element by a relative offset.
+
+**Arguments:**
+- `dx` (*float*): Horizontal offset in pixels
+- `dy` (*float*): Vertical offset in pixels
+
+**Note:** This modifies the x and y position properties by the given amounts.
+
+---
+
+### class `Entity`
+
+Game entity that can be placed in a Grid.
+
+#### Methods
+
+#### `die()`
+
+Remove this entity from its parent grid.
+
+**Note:** The entity object remains valid but is no longer rendered or updated.
+
+#### `move(dx, dy)`
+
+Move the element by a relative offset.
+
+**Arguments:**
+- `dx` (*float*): Horizontal offset in pixels
+- `dy` (*float*): Vertical offset in pixels
+
+**Note:** This modifies the x and y position properties by the given amounts.
+
+#### `at(x, y)`
+
+Check if this entity is at the specified grid coordinates.
+
+**Arguments:**
+- `x` (*int*): Grid x coordinate to check
+- `y` (*int*): Grid y coordinate to check
+
+**Returns:** bool: True if entity is at position (x, y), False otherwise
+
+#### `get_bounds()`
+
+Get the bounding rectangle of this drawable element.
+
+**Returns:** tuple: (x, y, width, height) representing the element's bounds
+
+**Note:** The bounds are in screen coordinates and account for current position and size.
+
+#### `index()`
+
+Get the index of this entity in its parent grid's entity list.
+
+**Returns:** int: Index position, or -1 if not in a grid
+
+#### `resize(width, height)`
+
+Resize the element to new dimensions.
+
+**Arguments:**
+- `width` (*float*): New width in pixels
+- `height` (*float*): New height in pixels
+
+**Note:** For Caption and Sprite, this may not change actual size if determined by content.
+
+---
+
+### Collections
+
+### class `EntityCollection`
+
+Container for Entity objects in a Grid. Supports iteration and indexing.
+
+#### Methods
+
+#### `append(entity)`
+
+Add an entity to the end of the collection.
+
+**Arguments:**
+- `entity` (*Entity*): The entity to add
+
+#### `remove(entity)`
+
+Remove the first occurrence of an entity from the collection.
+
+**Arguments:**
+- `entity` (*Entity*): The entity to remove
+
+**Raises:** ValueError: If entity is not in collection
+
+#### `count(entity)`
+
+Count the number of occurrences of an entity in the collection.
+
+**Arguments:**
+- `entity` (*Entity*): The entity to count
+
+**Returns:** int: Number of times entity appears in collection
+
+#### `index(entity)`
+
+Find the index of the first occurrence of an entity.
+
+**Arguments:**
+- `entity` (*Entity*): The entity to find
+
+**Returns:** int: Index of entity in collection
+
+**Raises:** ValueError: If entity is not in collection
+
+#### `extend(iterable)`
+
+Add all entities from an iterable to the collection.
+
+**Arguments:**
+- `iterable` (*Iterable[Entity]*): Entities to add
+
+---
+
+### class `UICollection`
+
+Container for UI drawable elements. Supports iteration and indexing.
+
+#### Methods
+
+#### `append(drawable)`
+
+Add a drawable element to the end of the collection.
+
+**Arguments:**
+- `drawable` (*UIDrawable*): The drawable element to add
+
+#### `remove(drawable)`
+
+Remove the first occurrence of a drawable from the collection.
+
+**Arguments:**
+- `drawable` (*UIDrawable*): The drawable to remove
+
+**Raises:** ValueError: If drawable is not in collection
+
+#### `count(drawable)`
+
+Count the number of occurrences of a drawable in the collection.
+
+**Arguments:**
+- `drawable` (*UIDrawable*): The drawable to count
+
+**Returns:** int: Number of times drawable appears in collection
+
+#### `index(drawable)`
+
+Find the index of the first occurrence of a drawable.
+
+**Arguments:**
+- `drawable` (*UIDrawable*): The drawable to find
+
+**Returns:** int: Index of drawable in collection
+
+**Raises:** ValueError: If drawable is not in collection
+
+#### `extend(iterable)`
+
+Add all drawables from an iterable to the collection.
+
+**Arguments:**
+- `iterable` (*Iterable[UIDrawable]*): Drawables to add
+
+---
+
+### class `UICollectionIter`
+
+Iterator for UICollection. Automatically created when iterating over a UICollection.
+
+---
+
+### class `UIEntityCollectionIter`
+
+Iterator for EntityCollection. Automatically created when iterating over an EntityCollection.
+
+---
+
+### System Types
+
+### class `Color`
+
+RGBA color representation.
+
+#### Methods
+
+#### `from_hex(hex_string)`
+
+Create a Color from a hexadecimal color string.
+
+**Arguments:**
+- `hex_string` (*str*): Hex color string (e.g., "#FF0000" or "FF0000")
+
+**Returns:** Color: New Color object from hex string
+
+**Example:**
+```python
+red = Color.from_hex("#FF0000")
+```
+
+#### `to_hex()`
+
+Convert this Color to a hexadecimal string.
+
+**Returns:** str: Hex color string in format "#RRGGBB"
+
+**Example:**
+```python
+hex_str = color.to_hex() # Returns "#FF0000"
+```
+
+#### `lerp(other, t)`
+
+Linearly interpolate between this color and another.
+
+**Arguments:**
+- `other` (*Color*): The color to interpolate towards
+- `t` (*float*): Interpolation factor from 0.0 to 1.0
+
+**Returns:** Color: New interpolated Color object
+
+**Example:**
+```python
+mixed = red.lerp(blue, 0.5) # 50% between red and blue
+```
+
+---
+
+### class `Vector`
+
+2D vector for positions and directions.
+
+#### Methods
+
+#### `magnitude()`
+
+Calculate the length/magnitude of this vector.
+
+**Returns:** float: The magnitude of the vector
+
+#### `distance_to(other)`
+
+Calculate the distance to another vector.
+
+**Arguments:**
+- `other` (*Vector*): The other vector
+
+**Returns:** float: Distance between the two vectors
+
+#### `dot(other)`
+
+Calculate the dot product with another vector.
+
+**Arguments:**
+- `other` (*Vector*): The other vector
+
+**Returns:** float: Dot product of the two vectors
+
+#### `angle()`
+
+Get the angle of this vector in radians.
+
+**Returns:** float: Angle in radians from positive x-axis
+
+#### `magnitude_squared()`
+
+Calculate the squared magnitude of this vector.
+
+**Returns:** float: The squared magnitude (faster than magnitude())
+
+**Note:** Use this for comparisons to avoid expensive square root calculation.
+
+#### `copy()`
+
+Create a copy of this vector.
+
+**Returns:** Vector: New Vector object with same x and y values
+
+#### `normalize()`
+
+Return a unit vector in the same direction.
+
+**Returns:** Vector: New normalized vector with magnitude 1.0
+
+**Raises:** ValueError: If vector has zero magnitude
+
+---
+
+### class `Texture`
+
+Texture object for image data.
+
+---
+
+### class `Font`
+
+Font object for text rendering.
+
+---
+
+### Other Classes
+
+### class `Animation`
+
+Animate UI element properties over time.
+
+#### Properties
+
+- **`property`**: str: Name of the property being animated (e.g., "x", "y", "scale")
+- **`duration`**: float: Total duration of the animation in seconds
+- **`elapsed_time`**: float: Time elapsed since animation started (read-only)
+- **`current_value`**: float: Current interpolated value of the animation (read-only)
+- **`is_running`**: bool: True if animation is currently running (read-only)
+- **`is_finished`**: bool: True if animation has completed (read-only)
+
+#### Methods
+
+#### `update(delta_time)`
+
+Update the animation by the given time delta.
+
+**Arguments:**
+- `delta_time` (*float*): Time elapsed since last update in seconds
+
+**Returns:** bool: True if animation is still running, False if finished
+
+#### `start(target)`
+
+Start the animation on a target UI element.
+
+**Arguments:**
+- `target` (*UIDrawable*): The UI element to animate
+
+**Note:** The target must have the property specified in the animation constructor.
+
+#### `get_current_value()`
+
+Get the current interpolated value of the animation.
+
+**Returns:** float: Current animation value between start and end
+
+---
+
+### class `Drawable`
+
+Base class for all drawable UI elements.
+
+#### Methods
+
+#### `get_bounds()`
+
+Get the bounding rectangle of this drawable element.
+
+**Returns:** tuple: (x, y, width, height) representing the element's bounds
+
+**Note:** The bounds are in screen coordinates and account for current position and size.
+
+#### `resize(width, height)`
+
+Resize the element to new dimensions.
+
+**Arguments:**
+- `width` (*float*): New width in pixels
+- `height` (*float*): New height in pixels
+
+**Note:** For Caption and Sprite, this may not change actual size if determined by content.
+
+#### `move(dx, dy)`
+
+Move the element by a relative offset.
+
+**Arguments:**
+- `dx` (*float*): Horizontal offset in pixels
+- `dy` (*float*): Vertical offset in pixels
+
+**Note:** This modifies the x and y position properties by the given amounts.
+
+---
+
+### class `GridPoint`
+
+Represents a single tile in a Grid.
+
+#### Properties
+
+- **`x`**: int: Grid x coordinate of this point
+- **`y`**: int: Grid y coordinate of this point
+- **`texture_index`**: int: Index of the texture/sprite to display at this point
+- **`solid`**: bool: Whether this point blocks movement
+- **`transparent`**: bool: Whether this point allows light/vision through
+- **`color`**: Color: Color tint applied to the texture at this point
+
+---
+
+### class `GridPointState`
+
+State information for a GridPoint.
+
+#### Properties
+
+- **`visible`**: bool: Whether this point is currently visible to the player
+- **`discovered`**: bool: Whether this point has been discovered/explored
+- **`custom_flags`**: int: Bitfield for custom game-specific flags
+
+---
+
+### class `Scene`
+
+Base class for object-oriented scenes.
+
+#### Methods
+
+#### `register_keyboard(callable)`
+
+Register a keyboard event handler function for the scene.
+
+**Arguments:**
+- `callable` (*callable*): Function that takes (key: str, action: str) parameters
+
+**Note:** Alternative to overriding the on_keypress method when subclassing Scene objects.
+
+**Example:**
+```python
+def handle_keyboard(key, action):
+ print(f"Key '{key}' was {action}")
+scene.register_keyboard(handle_keyboard)
+```
+
+#### `activate()`
+
+Make this scene the active scene.
+
+**Note:** Equivalent to calling setScene() with this scene's name.
+
+#### `get_ui()`
+
+Get the UI element collection for this scene.
+
+**Returns:** UICollection: Collection of all UI elements in this scene
+
+#### `keypress(handler)`
+
+Register a keyboard handler function for this scene.
+
+**Arguments:**
+- `handler` (*callable*): Function that takes (key_name: str, is_pressed: bool)
+
+**Note:** Alternative to overriding the on_keypress method.
+
+---
+
+### class `Timer`
+
+Timer object for scheduled callbacks.
+
+#### Methods
+
+#### `restart()`
+
+Restart the timer from the beginning.
+
+**Note:** Resets the timer's internal clock to zero.
+
+#### `cancel()`
+
+Cancel the timer and remove it from the system.
+
+**Note:** After cancelling, the timer object cannot be reused.
+
+#### `pause()`
+
+Pause the timer, stopping its callback execution.
+
+**Note:** Use resume() to continue the timer from where it was paused.
+
+#### `resume()`
+
+Resume a paused timer.
+
+**Note:** Has no effect if timer is not paused.
+
+---
+
+### class `Window`
+
+Window singleton for accessing and modifying the game window properties.
+
+#### Methods
+
+#### `get()`
+
+Get the Window singleton instance.
+
+**Returns:** Window: The singleton window object
+
+**Note:** This is a static method that returns the same instance every time.
+
+#### `screenshot(filename)`
+
+Take a screenshot and save it to a file.
+
+**Arguments:**
+- `filename` (*str*): Path where to save the screenshot
+
+**Note:** Supports PNG, JPG, and BMP formats based on file extension.
+
+#### `center()`
+
+Center the window on the screen.
+
+**Note:** Only works if the window is not fullscreen.
+
+---
+
+## Automation Module
+
+The `mcrfpy.automation` module provides testing and automation capabilities.
+
+### `automation.click`
+
+Click at position
+
+---
+
+### `automation.doubleClick`
+
+Double click at position
+
+---
+
+### `automation.dragRel`
+
+Drag mouse relative to current position
+
+---
+
+### `automation.dragTo`
+
+Drag mouse to position
+
+---
+
+### `automation.hotkey`
+
+Press a hotkey combination (e.g., hotkey('ctrl', 'c'))
+
+---
+
+### `automation.keyDown`
+
+Press and hold a key
+
+---
+
+### `automation.keyUp`
+
+Release a key
+
+---
+
+### `automation.middleClick`
+
+Middle click at position
+
+---
+
+### `automation.mouseDown`
+
+Press mouse button
+
+---
+
+### `automation.mouseUp`
+
+Release mouse button
+
+---
+
+### `automation.moveRel`
+
+Move mouse relative to current position
+
+---
+
+### `automation.moveTo`
+
+Move mouse to absolute position
+
+---
+
+### `automation.onScreen`
+
+Check if coordinates are within screen bounds
+
+---
+
+### `automation.position`
+
+Get current mouse position as (x, y) tuple
+
+---
+
+### `automation.rightClick`
+
+Right click at position
+
+---
+
+### `automation.screenshot`
+
+Save a screenshot to the specified file
+
+---
+
+### `automation.scroll`
+
+Scroll wheel at position
+
+---
+
+### `automation.size`
+
+Get screen size as (width, height) tuple
+
+---
+
+### `automation.tripleClick`
+
+Triple click at position
+
+---
+
+### `automation.typewrite`
+
+Type text with optional interval between keystrokes
+
+---
diff --git a/docs/api_reference_complete.html b/docs/api_reference_complete.html
index da95fee..73dd72a 100644
--- a/docs/api_reference_complete.html
+++ b/docs/api_reference_complete.html
@@ -183,7 +183,7 @@
McRogueFace API Reference - Complete Documentation
-
Generated on 2025-07-08 11:53:54
+
Generated on 2025-07-10 01:04:50
Table of Contents
@@ -632,13 +632,6 @@ mcrfpy.setScale(2.0) # Double the window size
Methods:
-
get_current_value()
-
Get the current interpolated value of the animation.
-
-Returns: float: Current animation value between start and end
-
-
-
update(delta_time)
Update the animation by the given time delta.
@@ -662,6 +655,13 @@ The UI element to animate
Note: The target must have the property specified in the animation constructor.
+
+
get_current_value()
+
Get the current interpolated value of the animation.
+
+Returns: float: Current animation value between start and end
+
+
Caption
@@ -701,23 +701,6 @@ Attributes:
-
resize(width, height)
-
Resize the element to new dimensions.
-
-width
-(float):
-New width in pixels
-
-
-height
-(float):
-New height in pixels
-
-
-Note: For Caption and Sprite, this may not change actual size if determined by content.
-
-
-
move(dx, dy)
Move the element by a relative offset.
@@ -734,12 +717,47 @@ Vertical offset in pixels
Note: This modifies the x and y position properties by the given amounts.
+
+
resize(width, height)
+
Resize the element to new dimensions.
+
+width
+(float):
+New width in pixels
+
+
+height
+(float):
+New height in pixels
+
+
+Note: For Caption and Sprite, this may not change actual size if determined by content.
+
+
Color
SFML Color Object
Methods:
+
from_hex(hex_string)
+
Create a Color from a hexadecimal color string.
+
+hex_string
+(str):
+Hex color string (e.g., "#FF0000" or "FF0000")
+
+
+Returns: Color: New Color object from hex string
+
+
+
Example:
+
+red = Color.from_hex("#FF0000")
+
+
+
+
lerp(other, t)
Linearly interpolate between this color and another.
@@ -775,24 +793,6 @@ hex_str = color.to_hex() # Returns "#FF0000"
-
-
from_hex(hex_string)
-
Create a Color from a hexadecimal color string.
-
-hex_string
-(str):
-Hex color string (e.g., "#FF0000" or "FF0000")
-
-
-Returns: Color: New Color object from hex string
-
-
-
Example:
-
-red = Color.from_hex("#FF0000")
-
-
-
Drawable
@@ -809,6 +809,23 @@ red = Color.from_hex("#FF0000")
+
move(dx, dy)
+
Move the element by a relative offset.
+
+dx
+(float):
+Horizontal offset in pixels
+
+
+dy
+(float):
+Vertical offset in pixels
+
+
+Note: This modifies the x and y position properties by the given amounts.
+
+
+
resize(width, height)
Resize the element to new dimensions.
@@ -825,39 +842,12 @@ New height in pixels
Note: For Caption and Sprite, this may not change actual size if determined by content.
-
-
move(dx, dy)
-
Move the element by a relative offset.
-
-dx
-(float):
-Horizontal offset in pixels
-
-
-dy
-(float):
-Vertical offset in pixels
-
-
-Note: This modifies the x and y position properties by the given amounts.
-
-
Entity
UIEntity objects
Methods:
-
get_bounds()
-
Get the bounding rectangle of this drawable element.
-
-Returns: tuple: (x, y, width, height) representing the element's bounds
-
-
-Note: The bounds are in screen coordinates and account for current position and size.
-
-
-
move(dx, dy)
Move the element by a relative offset.
@@ -875,6 +865,36 @@ Vertical offset in pixels
+
resize(width, height)
+
Resize the element to new dimensions.
+
+width
+(float):
+New width in pixels
+
+
+height
+(float):
+New height in pixels
+
+
+Note: For Caption and Sprite, this may not change actual size if determined by content.
+
+
+
+update_visibility(...)
+
+
+
index()
+
Get the index of this entity in its parent grid's entity list.
+
+Returns: int: Index position, or -1 if not in a grid
+
+
+
+path_to(...)
+
+
at(x, y)
Check if this entity is at the specified grid coordinates.
@@ -892,27 +912,13 @@ Grid y coordinate to check
-
resize(width, height)
-
Resize the element to new dimensions.
-
-width
-(float):
-New width in pixels
-
-
-
height
-
(float):
-New height in pixels
+
get_bounds()
+
Get the bounding rectangle of this drawable element.
+
+Returns: tuple: (x, y, width, height) representing the element's bounds
-Note: For Caption and Sprite, this may not change actual size if determined by content.
-
-
-
-
index()
-
Get the index of this entity in its parent grid's entity list.
-
-Returns: int: Index position, or -1 if not in a grid
+Note: The bounds are in screen coordinates and account for current position and size.
@@ -937,21 +943,15 @@ The entity to remove
-
extend(iterable)
-
Add all entities from an iterable to the collection.
-
-iterable
-(Iterable[Entity]):
-Entities to add
-
-
-
-
append(entity)
-
Add an entity to the end of the collection.
+
count(entity)
+
Count the number of occurrences of an entity in the collection.
entity
(Entity):
-The entity to add
+The entity to count
+
+
+Returns: int: Number of times entity appears in collection
@@ -967,15 +967,21 @@ The entity to find
-
count(entity)
-
Count the number of occurrences of an entity in the collection.
+
extend(iterable)
+
Add all entities from an iterable to the collection.
+
+iterable
+(Iterable[Entity]):
+Entities to add
+
+
+
+
append(entity)
+
Add an entity to the end of the collection.
entity
(Entity):
-The entity to count
-
-
-Returns: int: Number of times entity appears in collection
+The entity to add
@@ -1022,23 +1028,6 @@ Attributes:
-
resize(width, height)
-
Resize the element to new dimensions.
-
-width
-(float):
-New width in pixels
-
-
-height
-(float):
-New height in pixels
-
-
-Note: For Caption and Sprite, this may not change actual size if determined by content.
-
-
-
move(dx, dy)
Move the element by a relative offset.
@@ -1055,6 +1044,23 @@ Vertical offset in pixels
Note: This modifies the x and y position properties by the given amounts.
+
+
resize(width, height)
+
Resize the element to new dimensions.
+
+width
+(float):
+New width in pixels
+
+
+height
+(float):
+New height in pixels
+
+
+Note: For Caption and Sprite, this may not change actual size if determined by content.
+
+
Grid
@@ -1086,31 +1092,24 @@ Attributes:
z_index (int): Rendering order
Methods:
-
get_bounds()
-
Get the bounding rectangle of this drawable element.
-
-
Returns: tuple: (x, y, width, height) representing the element's bounds
+
move(dx, dy)
+
Move the element by a relative offset.
+
+dx
+(float):
+Horizontal offset in pixels
+
+
+dy
+(float):
+Vertical offset in pixels
-Note: The bounds are in screen coordinates and account for current position and size.
+Note: This modifies the x and y position properties by the given amounts.
-
-
at(x, y)
-
Get the GridPoint at the specified grid coordinates.
-
-x
-(int):
-Grid x coordinate
-
-
-y
-(int):
-Grid y coordinate
-
-
-Returns: GridPoint or None: The grid point at (x, y), or None if out of bounds
-
+
+compute_fov(...)
resize(width, height)
@@ -1129,21 +1128,49 @@ New height in pixels
Note: For Caption and Sprite, this may not change actual size if determined by content.
+
+compute_dijkstra(...)
+
+
+get_dijkstra_path(...)
+
+
+is_in_fov(...)
+
+
+find_path(...)
+
+
+compute_astar_path(...)
+
-
move(dx, dy)
-
Move the element by a relative offset.
+
at(x, y)
+
Get the GridPoint at the specified grid coordinates.
-dx
-(float):
-Horizontal offset in pixels
+x
+(int):
+Grid x coordinate
-dy
-(float):
-Vertical offset in pixels
+y
+(int):
+Grid y coordinate
+
+
+Returns: GridPoint or None: The grid point at (x, y), or None if out of bounds
+
+
+
+get_dijkstra_distance(...)
+
+
+
get_bounds()
+
Get the bounding rectangle of this drawable element.
+
+Returns: tuple: (x, y, width, height) representing the element's bounds
-Note: This modifies the x and y position properties by the given amounts.
+Note: The bounds are in screen coordinates and account for current position and size.
@@ -1273,23 +1300,6 @@ Attributes:
-
resize(width, height)
-
Resize the element to new dimensions.
-
-width
-(float):
-New width in pixels
-
-
-height
-(float):
-New height in pixels
-
-
-Note: For Caption and Sprite, this may not change actual size if determined by content.
-
-
-
move(dx, dy)
Move the element by a relative offset.
@@ -1306,6 +1316,23 @@ Vertical offset in pixels
Note: This modifies the x and y position properties by the given amounts.
+
+
resize(width, height)
+
Resize the element to new dimensions.
+
+width
+(float):
+New width in pixels
+
+
+height
+(float):
+New height in pixels
+
+
+Note: For Caption and Sprite, this may not change actual size if determined by content.
+
+
Texture
@@ -1323,6 +1350,13 @@ Vertical offset in pixels
+
restart()
+
Restart the timer from the beginning.
+
+Note: Resets the timer's internal clock to zero.
+
+
+
pause()
Pause the timer, stopping its callback execution.
@@ -1336,13 +1370,6 @@ Vertical offset in pixels
Note: After cancelling, the timer object cannot be reused.
-
-
restart()
-
Restart the timer from the beginning.
-
-Note: Resets the timer's internal clock to zero.
-
-
UICollection
@@ -1358,6 +1385,30 @@ The drawable to remove
+
count(drawable)
+
Count the number of occurrences of a drawable in the collection.
+
+drawable
+(UIDrawable):
+The drawable to count
+
+
+Returns: int: Number of times drawable appears in collection
+
+
+
+
index(drawable)
+
Find the index of the first occurrence of a drawable.
+
+drawable
+(UIDrawable):
+The drawable to find
+
+
+Returns: int: Index of drawable in collection
+
+
+
extend(iterable)
Add all drawables from an iterable to the collection.
@@ -1375,30 +1426,6 @@ Drawables to add
The drawable element to add
-
-
index(drawable)
-
Find the index of the first occurrence of a drawable.
-
-drawable
-(UIDrawable):
-The drawable to find
-
-
-Returns: int: Index of drawable in collection
-
-
-
-
count(drawable)
-
Count the number of occurrences of a drawable in the collection.
-
-drawable
-(UIDrawable):
-The drawable to count
-
-
-Returns: int: Number of times drawable appears in collection
-
-
UICollectionIter
@@ -1413,28 +1440,10 @@ The drawable to count
SFML Vector Object
Methods:
-
magnitude()
-
Calculate the length/magnitude of this vector.
+
copy()
+
Create a copy of this vector.
-Returns: float: The magnitude of the vector
-
-
-
Example:
-
-length = vector.magnitude()
-
-
-
-
-
distance_to(other)
-
Calculate the distance to another vector.
-
-other
-(Vector):
-The other vector
-
-
-Returns: float: Distance between the two vectors
+Returns: Vector: New Vector object with same x and y values
@@ -1457,6 +1466,19 @@ The other vector
+
magnitude()
+
Calculate the length/magnitude of this vector.
+
+Returns: float: The magnitude of the vector
+
+
+
Example:
+
+length = vector.magnitude()
+
+
+
+
normalize()
Return a unit vector in the same direction.
@@ -1474,10 +1496,15 @@ The other vector
-
copy()
-
Create a copy of this vector.
+
distance_to(other)
+
Calculate the distance to another vector.
+
+other
+(Vector):
+The other vector
+
-Returns: Vector: New Vector object with same x and y values
+Returns: float: Distance between the two vectors
@@ -1486,6 +1513,16 @@ The other vector
Window singleton for accessing and modifying the game window properties
Methods:
+
get()
+
Get the Window singleton instance.
+
+Returns: Window: The singleton window object
+
+
+Note: This is a static method that returns the same instance every time.
+
+
+
screenshot(filename)
Take a screenshot and save it to a file.
@@ -1504,16 +1541,6 @@ Path where to save the screenshot
Note: Only works if the window is not fullscreen.
-
-
get()
-
Get the Window singleton instance.
-
-Returns: Window: The singleton window object
-
-
-Note: This is a static method that returns the same instance every time.
-
-
Automation Module
The mcrfpy.automation
module provides testing and automation capabilities.
diff --git a/src/PyTimer.cpp b/src/PyTimer.cpp
index 10e2f77..df80bc5 100644
--- a/src/PyTimer.cpp
+++ b/src/PyTimer.cpp
@@ -35,7 +35,7 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
PyObject* callback = nullptr;
int interval = 0;
- if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi", kwlist,
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi", const_cast(kwlist),
&name, &callback, &interval)) {
return -1;
}
diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp
index c8a053b..4143ed0 100644
--- a/src/UIEntity.cpp
+++ b/src/UIEntity.cpp
@@ -508,8 +508,22 @@ PyMethodDef UIEntity::methods[] = {
{"at", (PyCFunction)UIEntity::at, METH_O},
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
- {"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS, "Find path from entity to target position using Dijkstra pathfinding"},
- {"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS, "Update entity's visibility state based on current FOV"},
+ {"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS,
+ "path_to(x: int, y: int) -> bool\n\n"
+ "Find and follow path to target position using A* pathfinding.\n\n"
+ "Args:\n"
+ " x: Target X coordinate\n"
+ " y: Target Y coordinate\n\n"
+ "Returns:\n"
+ " True if a path was found and the entity started moving, False otherwise\n\n"
+ "The entity will automatically move along the path over multiple frames.\n"
+ "Call this again to change the target or repath."},
+ {"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS,
+ "update_visibility() -> None\n\n"
+ "Update entity's visibility state based on current FOV.\n\n"
+ "Recomputes which cells are visible from the entity's position and updates\n"
+ "the entity's gridstate to track explored areas. This is called automatically\n"
+ "when the entity moves if it has a grid with perspective set."},
{NULL, NULL, 0, NULL}
};
@@ -522,8 +536,22 @@ PyMethodDef UIEntity_all_methods[] = {
{"at", (PyCFunction)UIEntity::at, METH_O},
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
- {"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS, "Find path from entity to target position using Dijkstra pathfinding"},
- {"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS, "Update entity's visibility state based on current FOV"},
+ {"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS,
+ "path_to(x: int, y: int) -> bool\n\n"
+ "Find and follow path to target position using A* pathfinding.\n\n"
+ "Args:\n"
+ " x: Target X coordinate\n"
+ " y: Target Y coordinate\n\n"
+ "Returns:\n"
+ " True if a path was found and the entity started moving, False otherwise\n\n"
+ "The entity will automatically move along the path over multiple frames.\n"
+ "Call this again to change the target or repath."},
+ {"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS,
+ "update_visibility() -> None\n\n"
+ "Update entity's visibility state based on current FOV.\n\n"
+ "Recomputes which cells are visible from the entity's position and updates\n"
+ "the entity's gridstate to track explored areas. This is called automatically\n"
+ "when the entity moves if it has a grid with perspective set."},
{NULL} // Sentinel
};
diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp
index 251bba2..d6a109e 100644
--- a/src/UIGrid.cpp
+++ b/src/UIGrid.cpp
@@ -972,7 +972,7 @@ PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject*
int light_walls = 1;
int algorithm = FOV_BASIC;
- if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|ipi", kwlist,
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|ipi", const_cast(kwlist),
&x, &y, &radius, &light_walls, &algorithm)) {
return NULL;
}
@@ -998,7 +998,7 @@ PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* k
int x1, y1, x2, y2;
float diagonal_cost = 1.41f;
- if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", kwlist,
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", const_cast(kwlist),
&x1, &y1, &x2, &y2, &diagonal_cost)) {
return NULL;
}
@@ -1026,7 +1026,7 @@ PyObject* UIGrid::py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyOb
int root_x, root_y;
float diagonal_cost = 1.41f;
- if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|f", kwlist,
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|f", const_cast(kwlist),
&root_x, &root_y, &diagonal_cost)) {
return NULL;
}
@@ -1075,7 +1075,7 @@ PyObject* UIGrid::py_compute_astar_path(PyUIGridObject* self, PyObject* args, Py
static const char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL};
- if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", kwlist,
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", const_cast(kwlist),
&x1, &y1, &x2, &y2, &diagonal_cost)) {
return NULL;
}
@@ -1096,19 +1096,77 @@ PyObject* UIGrid::py_compute_astar_path(PyUIGridObject* self, PyObject* args, Py
PyMethodDef UIGrid::methods[] = {
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
- "Compute field of view from a position. Args: x, y, radius=0, light_walls=True, algorithm=FOV_BASIC"},
+ "compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
+ "Compute field of view from a position.\n\n"
+ "Args:\n"
+ " x: X coordinate of the viewer\n"
+ " y: Y coordinate of the viewer\n"
+ " radius: Maximum view distance (0 = unlimited)\n"
+ " light_walls: Whether walls are lit when visible\n"
+ " algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
+ "Updates the internal FOV state. Use is_in_fov() to check visibility after calling this.\n"
+ "When perspective is set, this also updates visibility overlays automatically."},
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
- "Check if a cell is in the field of view. Args: x, y"},
+ "is_in_fov(x: int, y: int) -> bool\n\n"
+ "Check if a cell is in the field of view.\n\n"
+ "Args:\n"
+ " x: X coordinate to check\n"
+ " y: Y coordinate to check\n\n"
+ "Returns:\n"
+ " True if the cell is visible, False otherwise\n\n"
+ "Must call compute_fov() first to calculate visibility."},
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
- "Find A* path between two points. Args: x1, y1, x2, y2, diagonal_cost=1.41"},
+ "find_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
+ "Find A* path between two points.\n\n"
+ "Args:\n"
+ " x1: Starting X coordinate\n"
+ " y1: Starting Y coordinate\n"
+ " x2: Target X coordinate\n"
+ " y2: Target Y coordinate\n"
+ " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
+ "Returns:\n"
+ " List of (x, y) tuples representing the path, empty list if no path exists\n\n"
+ "Uses A* algorithm with walkability from grid cells."},
{"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS,
- "Compute Dijkstra map from root position. Args: root_x, root_y, diagonal_cost=1.41"},
+ "compute_dijkstra(root_x: int, root_y: int, diagonal_cost: float = 1.41) -> None\n\n"
+ "Compute Dijkstra map from root position.\n\n"
+ "Args:\n"
+ " root_x: X coordinate of the root/target\n"
+ " root_y: Y coordinate of the root/target\n"
+ " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
+ "Precomputes distances from all reachable cells to the root.\n"
+ "Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n"
+ "Useful for multiple entities pathfinding to the same target."},
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS,
- "Get distance from Dijkstra root to position. Args: x, y. Returns float or None if invalid."},
+ "get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n"
+ "Get distance from Dijkstra root to position.\n\n"
+ "Args:\n"
+ " x: X coordinate to query\n"
+ " y: Y coordinate to query\n\n"
+ "Returns:\n"
+ " Distance as float, or None if position is unreachable or invalid\n\n"
+ "Must call compute_dijkstra() first."},
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS,
- "Get path from position to Dijkstra root. Args: x, y. Returns list of (x,y) tuples."},
+ "get_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]\n\n"
+ "Get path from position to Dijkstra root.\n\n"
+ "Args:\n"
+ " x: Starting X coordinate\n"
+ " y: Starting Y coordinate\n\n"
+ "Returns:\n"
+ " List of (x, y) tuples representing path to root, empty if unreachable\n\n"
+ "Must call compute_dijkstra() first. Path includes start but not root position."},
{"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS,
- "Compute A* path between two points. Args: x1, y1, x2, y2, diagonal_cost=1.41. Returns list of (x,y) tuples. Note: diagonal_cost is currently ignored (uses default 1.41)."},
+ "compute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
+ "Compute A* path between two points.\n\n"
+ "Args:\n"
+ " x1: Starting X coordinate\n"
+ " y1: Starting Y coordinate\n"
+ " x2: Target X coordinate\n"
+ " y2: Target Y coordinate\n"
+ " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
+ "Returns:\n"
+ " List of (x, y) tuples representing the path, empty list if no path exists\n\n"
+ "Alternative A* implementation. Prefer find_path() for consistency."},
{NULL, NULL, 0, NULL}
};
@@ -1120,19 +1178,77 @@ PyMethodDef UIGrid_all_methods[] = {
UIDRAWABLE_METHODS,
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
- "Compute field of view from a position. Args: x, y, radius=0, light_walls=True, algorithm=FOV_BASIC"},
+ "compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
+ "Compute field of view from a position.\n\n"
+ "Args:\n"
+ " x: X coordinate of the viewer\n"
+ " y: Y coordinate of the viewer\n"
+ " radius: Maximum view distance (0 = unlimited)\n"
+ " light_walls: Whether walls are lit when visible\n"
+ " algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
+ "Updates the internal FOV state. Use is_in_fov() to check visibility after calling this.\n"
+ "When perspective is set, this also updates visibility overlays automatically."},
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
- "Check if a cell is in the field of view. Args: x, y"},
+ "is_in_fov(x: int, y: int) -> bool\n\n"
+ "Check if a cell is in the field of view.\n\n"
+ "Args:\n"
+ " x: X coordinate to check\n"
+ " y: Y coordinate to check\n\n"
+ "Returns:\n"
+ " True if the cell is visible, False otherwise\n\n"
+ "Must call compute_fov() first to calculate visibility."},
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
- "Find A* path between two points. Args: x1, y1, x2, y2, diagonal_cost=1.41"},
+ "find_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
+ "Find A* path between two points.\n\n"
+ "Args:\n"
+ " x1: Starting X coordinate\n"
+ " y1: Starting Y coordinate\n"
+ " x2: Target X coordinate\n"
+ " y2: Target Y coordinate\n"
+ " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
+ "Returns:\n"
+ " List of (x, y) tuples representing the path, empty list if no path exists\n\n"
+ "Uses A* algorithm with walkability from grid cells."},
{"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS,
- "Compute Dijkstra map from root position. Args: root_x, root_y, diagonal_cost=1.41"},
+ "compute_dijkstra(root_x: int, root_y: int, diagonal_cost: float = 1.41) -> None\n\n"
+ "Compute Dijkstra map from root position.\n\n"
+ "Args:\n"
+ " root_x: X coordinate of the root/target\n"
+ " root_y: Y coordinate of the root/target\n"
+ " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
+ "Precomputes distances from all reachable cells to the root.\n"
+ "Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n"
+ "Useful for multiple entities pathfinding to the same target."},
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS,
- "Get distance from Dijkstra root to position. Args: x, y. Returns float or None if invalid."},
+ "get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n"
+ "Get distance from Dijkstra root to position.\n\n"
+ "Args:\n"
+ " x: X coordinate to query\n"
+ " y: Y coordinate to query\n\n"
+ "Returns:\n"
+ " Distance as float, or None if position is unreachable or invalid\n\n"
+ "Must call compute_dijkstra() first."},
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS,
- "Get path from position to Dijkstra root. Args: x, y. Returns list of (x,y) tuples."},
+ "get_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]\n\n"
+ "Get path from position to Dijkstra root.\n\n"
+ "Args:\n"
+ " x: Starting X coordinate\n"
+ " y: Starting Y coordinate\n\n"
+ "Returns:\n"
+ " List of (x, y) tuples representing path to root, empty if unreachable\n\n"
+ "Must call compute_dijkstra() first. Path includes start but not root position."},
{"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS,
- "Compute A* path between two points. Args: x1, y1, x2, y2, diagonal_cost=1.41. Returns list of (x,y) tuples. Note: diagonal_cost is currently ignored (uses default 1.41)."},
+ "compute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
+ "Compute A* path between two points.\n\n"
+ "Args:\n"
+ " x1: Starting X coordinate\n"
+ " y1: Starting Y coordinate\n"
+ " x2: Target X coordinate\n"
+ " y2: Target Y coordinate\n"
+ " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
+ "Returns:\n"
+ " List of (x, y) tuples representing the path, empty list if no path exists\n\n"
+ "Alternative A* implementation. Prefer find_path() for consistency."},
{NULL} // Sentinel
};
@@ -1161,7 +1277,10 @@ PyGetSetDef UIGrid::getsetters[] = {
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5
{"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color, "Background fill color of the grid", NULL},
- {"perspective", (getter)UIGrid::get_perspective, (setter)UIGrid::set_perspective, "Entity perspective index (-1 for omniscient view)", NULL},
+ {"perspective", (getter)UIGrid::get_perspective, (setter)UIGrid::set_perspective,
+ "Entity perspective index for FOV rendering (-1 for omniscient view, 0+ for entity index). "
+ "When set to an entity index, only cells visible to that entity are rendered normally; "
+ "explored but not visible cells are darkened, and unexplored cells are black.", NULL},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
UIDRAWABLE_GETSETTERS,
diff --git a/tests/animation_demo.py b/tests/animation_demo.py
index f12fc70..716cded 100644
--- a/tests/animation_demo.py
+++ b/tests/animation_demo.py
@@ -1,165 +1,208 @@
#!/usr/bin/env python3
-"""Animation System Demo - Shows all animation capabilities"""
+"""
+Animation Demo: Grid Center & Entity Movement
+=============================================
+
+Demonstrates:
+- Animated grid centering following entity
+- Smooth entity movement along paths
+- Perspective shifts with zoom transitions
+- Field of view updates
+"""
import mcrfpy
-import math
+import sys
-# Create main scene
-mcrfpy.createScene("animation_demo")
-ui = mcrfpy.sceneUI("animation_demo")
-mcrfpy.setScene("animation_demo")
+# Setup scene
+mcrfpy.createScene("anim_demo")
-# Title
-title = mcrfpy.Caption((400, 30), "McRogueFace Animation System Demo", mcrfpy.default_font)
-title.size = 24
-title.fill_color = (255, 255, 255)
-# Note: centered property doesn't exist for Caption
+# Create grid
+grid = mcrfpy.Grid(grid_x=30, grid_y=20)
+grid.fill_color = mcrfpy.Color(20, 20, 30)
+
+# Simple map
+for y in range(20):
+ for x in range(30):
+ cell = grid.at(x, y)
+ # Create walls around edges and some obstacles
+ if x == 0 or x == 29 or y == 0 or y == 19:
+ cell.walkable = False
+ cell.transparent = False
+ cell.color = mcrfpy.Color(40, 30, 30)
+ elif (x == 10 and 5 <= y <= 15) or (y == 10 and 5 <= x <= 25):
+ cell.walkable = False
+ cell.transparent = False
+ cell.color = mcrfpy.Color(60, 40, 40)
+ else:
+ cell.walkable = True
+ cell.transparent = True
+ cell.color = mcrfpy.Color(80, 80, 100)
+
+# Create entities
+player = mcrfpy.Entity(5, 5, grid=grid)
+player.sprite_index = 64 # @
+
+enemy = mcrfpy.Entity(25, 15, grid=grid)
+enemy.sprite_index = 69 # E
+
+# Update visibility
+player.update_visibility()
+enemy.update_visibility()
+
+# UI setup
+ui = mcrfpy.sceneUI("anim_demo")
+ui.append(grid)
+grid.position = (100, 100)
+grid.size = (600, 400)
+
+title = mcrfpy.Caption("Animation Demo - Grid Center & Entity Movement", 200, 20)
+title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
-# 1. Position Animation Demo
-pos_frame = mcrfpy.Frame(50, 100, 80, 80)
-pos_frame.fill_color = (255, 100, 100)
-pos_frame.outline = 2
-ui.append(pos_frame)
+status = mcrfpy.Caption("Press 1: Move Player | 2: Move Enemy | 3: Perspective Shift | Q: Quit", 100, 50)
+status.fill_color = mcrfpy.Color(200, 200, 200)
+ui.append(status)
-pos_label = mcrfpy.Caption((50, 80), "Position Animation", mcrfpy.default_font)
-pos_label.fill_color = (200, 200, 200)
-ui.append(pos_label)
-
-# 2. Size Animation Demo
-size_frame = mcrfpy.Frame(200, 100, 50, 50)
-size_frame.fill_color = (100, 255, 100)
-size_frame.outline = 2
-ui.append(size_frame)
-
-size_label = mcrfpy.Caption((200, 80), "Size Animation", mcrfpy.default_font)
-size_label.fill_color = (200, 200, 200)
-ui.append(size_label)
-
-# 3. Color Animation Demo
-color_frame = mcrfpy.Frame(350, 100, 80, 80)
-color_frame.fill_color = (255, 0, 0)
-ui.append(color_frame)
-
-color_label = mcrfpy.Caption((350, 80), "Color Animation", mcrfpy.default_font)
-color_label.fill_color = (200, 200, 200)
-ui.append(color_label)
-
-# 4. Easing Functions Demo
-easing_y = 250
-easing_frames = []
-easings = ["linear", "easeIn", "easeOut", "easeInOut", "easeInElastic", "easeOutBounce"]
-
-for i, easing in enumerate(easings):
- x = 50 + i * 120
-
- frame = mcrfpy.Frame(x, easing_y, 20, 20)
- frame.fill_color = (100, 150, 255)
- ui.append(frame)
- easing_frames.append((frame, easing))
-
- label = mcrfpy.Caption((x, easing_y - 20), easing, mcrfpy.default_font)
- label.size = 12
- label.fill_color = (200, 200, 200)
- ui.append(label)
-
-# 5. Complex Animation Demo
-complex_frame = mcrfpy.Frame(300, 350, 100, 100)
-complex_frame.fill_color = (128, 128, 255)
-complex_frame.outline = 3
-ui.append(complex_frame)
-
-complex_label = mcrfpy.Caption((300, 330), "Complex Multi-Property", mcrfpy.default_font)
-complex_label.fill_color = (200, 200, 200)
-ui.append(complex_label)
-
-# Start animations
-def start_animations(runtime):
- # 1. Position animation - back and forth
- x_anim = mcrfpy.Animation("x", 500.0, 3.0, "easeInOut")
- x_anim.start(pos_frame)
-
- # 2. Size animation - pulsing
- w_anim = mcrfpy.Animation("w", 150.0, 2.0, "easeInOut")
- h_anim = mcrfpy.Animation("h", 150.0, 2.0, "easeInOut")
- w_anim.start(size_frame)
- h_anim.start(size_frame)
-
- # 3. Color animation - rainbow cycle
- color_anim = mcrfpy.Animation("fill_color", (0, 255, 255, 255), 2.0, "linear")
- color_anim.start(color_frame)
-
- # 4. Easing demos - all move up with different easings
- for frame, easing in easing_frames:
- y_anim = mcrfpy.Animation("y", 150.0, 2.0, easing)
- y_anim.start(frame)
-
- # 5. Complex animation - multiple properties
- cx_anim = mcrfpy.Animation("x", 500.0, 4.0, "easeInOut")
- cy_anim = mcrfpy.Animation("y", 400.0, 4.0, "easeOut")
- cw_anim = mcrfpy.Animation("w", 150.0, 4.0, "easeInElastic")
- ch_anim = mcrfpy.Animation("h", 150.0, 4.0, "easeInElastic")
- outline_anim = mcrfpy.Animation("outline", 10.0, 4.0, "linear")
-
- cx_anim.start(complex_frame)
- cy_anim.start(complex_frame)
- cw_anim.start(complex_frame)
- ch_anim.start(complex_frame)
- outline_anim.start(complex_frame)
-
- # Individual color component animations
- r_anim = mcrfpy.Animation("fill_color.r", 255.0, 4.0, "easeInOut")
- g_anim = mcrfpy.Animation("fill_color.g", 100.0, 4.0, "easeInOut")
- b_anim = mcrfpy.Animation("fill_color.b", 50.0, 4.0, "easeInOut")
-
- r_anim.start(complex_frame)
- g_anim.start(complex_frame)
- b_anim.start(complex_frame)
-
- print("All animations started!")
-
-# Reverse some animations
-def reverse_animations(runtime):
- # Position back
- x_anim = mcrfpy.Animation("x", 50.0, 3.0, "easeInOut")
- x_anim.start(pos_frame)
-
- # Size back
- w_anim = mcrfpy.Animation("w", 50.0, 2.0, "easeInOut")
- h_anim = mcrfpy.Animation("h", 50.0, 2.0, "easeInOut")
- w_anim.start(size_frame)
- h_anim.start(size_frame)
-
- # Color cycle continues
- color_anim = mcrfpy.Animation("fill_color", (255, 0, 255, 255), 2.0, "linear")
- color_anim.start(color_frame)
-
- # Easing frames back down
- for frame, easing in easing_frames:
- y_anim = mcrfpy.Animation("y", 250.0, 2.0, easing)
- y_anim.start(frame)
-
-# Continue color cycle
-def cycle_colors(runtime):
- color_anim = mcrfpy.Animation("fill_color", (255, 255, 0, 255), 2.0, "linear")
- color_anim.start(color_frame)
-
-# Info text
-info = mcrfpy.Caption((400, 550), "Watch as different properties animate with various easing functions!", mcrfpy.default_font)
-info.fill_color = (255, 255, 200)
-# Note: centered property doesn't exist for Caption
+info = mcrfpy.Caption("Perspective: Player", 500, 70)
+info.fill_color = mcrfpy.Color(100, 255, 100)
ui.append(info)
-# Schedule animations
-mcrfpy.setTimer("start", start_animations, 500)
-mcrfpy.setTimer("reverse", reverse_animations, 4000)
-mcrfpy.setTimer("cycle", cycle_colors, 2500)
+# Movement functions
+def move_player_demo():
+ """Demo player movement with camera follow"""
+ # Calculate path to a destination
+ path = player.path_to(20, 10)
+ if not path:
+ status.text = "No path available!"
+ return
+
+ status.text = f"Moving player along {len(path)} steps..."
+
+ # Animate along path
+ for i, (x, y) in enumerate(path[:5]): # First 5 steps
+ delay = i * 500 # 500ms between steps
+
+ # Schedule movement
+ def move_step(dt, px=x, py=y):
+ # Animate entity position
+ anim_x = mcrfpy.Animation("x", float(px), 0.4, "easeInOut")
+ anim_y = mcrfpy.Animation("y", float(py), 0.4, "easeInOut")
+ anim_x.start(player)
+ anim_y.start(player)
+
+ # Update visibility
+ player.update_visibility()
+
+ # Animate camera to follow
+ center_x = px * 16 # Assuming 16x16 tiles
+ center_y = py * 16
+ cam_anim = mcrfpy.Animation("center", (center_x, center_y), 0.4, "easeOut")
+ cam_anim.start(grid)
+
+ mcrfpy.setTimer(f"player_move_{i}", move_step, delay)
-# Exit handler
-def on_key(key):
- if key == "Escape":
- mcrfpy.exit()
+def move_enemy_demo():
+ """Demo enemy movement"""
+ # Calculate path
+ path = enemy.path_to(10, 5)
+ if not path:
+ status.text = "Enemy has no path!"
+ return
+
+ status.text = f"Moving enemy along {len(path)} steps..."
+
+ # Animate along path
+ for i, (x, y) in enumerate(path[:5]): # First 5 steps
+ delay = i * 500
+
+ def move_step(dt, ex=x, ey=y):
+ anim_x = mcrfpy.Animation("x", float(ex), 0.4, "easeInOut")
+ anim_y = mcrfpy.Animation("y", float(ey), 0.4, "easeInOut")
+ anim_x.start(enemy)
+ anim_y.start(enemy)
+ enemy.update_visibility()
+
+ # If following enemy, update camera
+ if grid.perspective == 1:
+ center_x = ex * 16
+ center_y = ey * 16
+ cam_anim = mcrfpy.Animation("center", (center_x, center_y), 0.4, "easeOut")
+ cam_anim.start(grid)
+
+ mcrfpy.setTimer(f"enemy_move_{i}", move_step, delay)
-mcrfpy.keypressScene(on_key)
+def perspective_shift_demo():
+ """Demo dramatic perspective shift"""
+ status.text = "Perspective shift in progress..."
+
+ # Phase 1: Zoom out
+ zoom_out = mcrfpy.Animation("zoom", 0.5, 1.5, "easeInExpo")
+ zoom_out.start(grid)
+
+ # Phase 2: Switch perspective at peak
+ def switch_perspective(dt):
+ if grid.perspective == 0:
+ grid.perspective = 1
+ info.text = "Perspective: Enemy"
+ info.fill_color = mcrfpy.Color(255, 100, 100)
+ target = enemy
+ else:
+ grid.perspective = 0
+ info.text = "Perspective: Player"
+ info.fill_color = mcrfpy.Color(100, 255, 100)
+ target = player
+
+ # Update camera to new target
+ center_x = target.x * 16
+ center_y = target.y * 16
+ cam_anim = mcrfpy.Animation("center", (center_x, center_y), 0.5, "linear")
+ cam_anim.start(grid)
+
+ mcrfpy.setTimer("switch_persp", switch_perspective, 1600)
+
+ # Phase 3: Zoom back in
+ def zoom_in(dt):
+ zoom_in_anim = mcrfpy.Animation("zoom", 1.0, 1.5, "easeOutExpo")
+ zoom_in_anim.start(grid)
+ status.text = "Perspective shift complete!"
+
+ mcrfpy.setTimer("zoom_in", zoom_in, 2100)
-print("Animation demo started! Press Escape to exit.")
\ No newline at end of file
+# Input handler
+def handle_input(key, state):
+ if state != "start":
+ return
+
+ if key == "q":
+ print("Exiting demo...")
+ sys.exit(0)
+ elif key == "1":
+ move_player_demo()
+ elif key == "2":
+ move_enemy_demo()
+ elif key == "3":
+ perspective_shift_demo()
+
+# Set scene
+mcrfpy.setScene("anim_demo")
+mcrfpy.keypressScene(handle_input)
+
+# Initial setup
+grid.perspective = 0
+grid.zoom = 1.0
+
+# Center on player initially
+center_x = player.x * 16
+center_y = player.y * 16
+initial_cam = mcrfpy.Animation("center", (center_x, center_y), 0.5, "easeOut")
+initial_cam.start(grid)
+
+print("Animation Demo Started!")
+print("======================")
+print("Press 1: Animate player movement with camera follow")
+print("Press 2: Animate enemy movement")
+print("Press 3: Dramatic perspective shift with zoom")
+print("Press Q: Quit")
+print()
+print("Watch how the grid center smoothly follows entities")
+print("and how perspective shifts create cinematic effects!")
\ No newline at end of file