Squashed commit of the following: [alpha_presentable]

Author: John McCardle <mccardle.john@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>

commit dc47f2474c7b2642d368f9772894aed857527807
    the UIEntity rant

commit 673ca8e1b089ea670257fc04ae1a676ed95a40ed
    I forget when these tests were written, but I want them in the squash merge

commit 70c71565c684fa96e222179271ecb13a156d80ad
    Fix UI object segfault by switching from managed to manual weakref management

    The UI types (Frame, Caption, Sprite, Grid, Entity) were using
    Py_TPFLAGS_MANAGED_WEAKREF while also trying to manually create weakrefs
    for the PythonObjectCache. This is fundamentally incompatible - when
    Python manages weakrefs internally, PyWeakref_NewRef() cannot access the
    weakref list properly, causing segfaults.

    Changed all UI types to use manual weakref management (like PyTimer):
    - Restored weakreflist field in all UI type structures
    - Removed Py_TPFLAGS_MANAGED_WEAKREF from all UI type flags
    - Added tp_weaklistoffset for all UI types in module initialization
    - Initialize weakreflist=NULL in tp_new and init methods
    - Call PyObject_ClearWeakRefs() in dealloc functions

    This allows the PythonObjectCache to continue working correctly,
    maintaining Python object identity for C++ objects across the boundary.

    Fixes segfault when creating UI objects (e.g., Caption, Grid) that was
    preventing tutorial scripts from running.

This is the bulk of the required behavior for Issue #126.
that issure isn't ready for closure yet; several other sub-issues left.
    closes #110
    mention issue #109 - resolves some __init__ related nuisances

commit 3dce3ec539ae99e32d869007bf3f49d03e4e2f89
    Refactor timer system for cleaner architecture and enhanced functionality

    Major improvements to the timer system:
    - Unified all timer logic in the Timer class (C++)
    - Removed PyTimerCallable subclass, now using PyCallable directly
    - Timer objects are now passed to callbacks as first argument
    - Added 'once' parameter for one-shot timers that auto-stop
    - Implemented proper PythonObjectCache integration with weakref support

    API enhancements:
    - New callback signature: callback(timer, runtime) instead of just (runtime)
    - Timer objects expose: name, interval, remaining, paused, active, once properties
    - Methods: pause(), resume(), cancel(), restart()
    - Comprehensive documentation with examples
    - Enhanced repr showing timer state (active/paused/once/remaining time)

    This cleanup follows the UIEntity/PyUIEntity pattern and makes the timer
    system more Pythonic while maintaining backward compatibility through
    the legacy setTimer/delTimer API.

    closes #121

commit 145834cfc31b8dabc4cb3591b9cb4ed99fc8b964
    Implement Python object cache to preserve derived types in collections

    Add a global cache system that maintains weak references to Python objects,
    ensuring that derived Python classes maintain their identity when stored in
    and retrieved from C++ collections.

    Key changes:
    - Add PythonObjectCache singleton with serial number system
    - Each cacheable object (UIDrawable, UIEntity, Timer, Animation) gets unique ID
    - Cache stores weak references to prevent circular reference memory leaks
    - Update all UI type definitions to support weak references (Py_TPFLAGS_MANAGED_WEAKREF)
    - Enable subclassing for all UI types (Py_TPFLAGS_BASETYPE)
    - Collections check cache before creating new Python wrappers
    - Register objects in cache during __init__ methods
    - Clean up cache entries in C++ destructors

    This ensures that Python code like:
    ```python
    class MyFrame(mcrfpy.Frame):
        def __init__(self):
            super().__init__()
            self.custom_data = "preserved"

    frame = MyFrame()
    scene.ui.append(frame)
    retrieved = scene.ui[0]  # Same MyFrame instance with custom_data intact
    ```

    Works correctly, with retrieved maintaining the derived type and custom attributes.

    Closes #112

commit c5e7e8e298
    Update test demos for new Python API and entity system

    - Update all text input demos to use new Entity constructor signature
    - Fix pathfinding showcase to work with new entity position handling
    - Remove entity_waypoints tracking in favor of simplified movement
    - Delete obsolete exhaustive_api_demo.py (superseded by newer demos)
    - Adjust entity creation calls to match Entity((x, y), texture, sprite_index) pattern

commit 6d29652ae7
    Update animation demo suite with crash fixes and improvements

    - Add warnings about AnimationManager segfault bug in sizzle_reel_final.py
    - Create sizzle_reel_final_fixed.py that works around the crash by hiding objects instead of removing them
    - Increase font sizes for better visibility in demos
    - Extend demo durations for better showcase of animations
    - Remove debug prints from animation_sizzle_reel_working.py
    - Minor cleanup and improvements to all animation demos

commit a010e5fa96
    Update game scripts for new Python API

    - Convert entity position access from tuple to x/y properties
    - Update caption size property to font_size
    - Fix grid boundary checks to use grid_size instead of exceptions
    - Clean up demo timer on menu exit to prevent callbacks

    These changes adapt the game scripts to work with the new standardized
    Python API constructors and property names.

commit 9c8d6c4591
    Fix click event z-order handling in PyScene

    Changed click detection to properly respect z-index by:
    - Sorting ui_elements in-place when needed (same as render order)
    - Using reverse iterators to check highest z-index elements first
    - This ensures top-most elements receive clicks before lower ones

commit dcd1b0ca33
    Add roguelike tutorial implementation files

    Implement Parts 0-2 of the classic roguelike tutorial adapted for McRogueFace:
    - Part 0: Basic grid setup and tile rendering
    - Part 1: Drawing '@' symbol and basic movement
    - Part 1b: Variant with sprite-based player
    - Part 2: Entity system and NPC implementation with three movement variants:
      - part_2.py: Standard implementation
      - part_2-naive.py: Naive movement approach
      - part_2-onemovequeued.py: Queued movement system

    Includes tutorial assets:
    - tutorial2.png: Tileset for dungeon tiles
    - tutorial_hero.png: Player sprite sheet

commit 6813fb5129
    Standardize Python API constructors and remove PyArgHelpers

    - Remove PyArgHelpers.h and all macro-based argument parsing
    - Convert all UI class constructors to use PyArg_ParseTupleAndKeywords
    - Standardize constructor signatures across UICaption, UIEntity, UIFrame, UIGrid, and UISprite
    - Replace PYARGHELPER_SINGLE/MULTI macros with explicit argument parsing
    - Improve error messages and argument validation
    - Maintain backward compatibility with existing Python code

    This change improves code maintainability and consistency across the Python API.

commit 6f67fbb51e
    Fix animation callback crashes from iterator invalidation (#119)

    Resolved segfaults caused by creating new animations from within
    animation callbacks. The issue was iterator invalidation in
    AnimationManager::update() when callbacks modified the active
    animations vector.

    Changes:
    - Add deferred animation queue to AnimationManager
    - New animations created during update are queued and added after
    - Set isUpdating flag to track when in update loop
    - Properly handle Animation destructor during callback execution
    - Add clearCallback() method for safe cleanup scenarios

    This fixes the "free(): invalid pointer" and "malloc(): unaligned
    fastbin chunk detected" errors that occurred with rapid animation
    creation in callbacks.

commit eb88c7b3aa
    Add animation completion callbacks (#119)

    Implement callbacks that fire when animations complete, enabling direct
    causality between animation end and game state changes. This eliminates
    race conditions from parallel timer workarounds.

    - Add optional callback parameter to Animation constructor
    - Callbacks execute synchronously when animation completes
    - Proper Python reference counting with GIL safety
    - Callbacks receive (anim, target) parameters (currently None)
    - Exception handling prevents crashes from Python errors

    Example usage:
    ```python
    def on_complete(anim, target):
        player_moving = False

    anim = mcrfpy.Animation("x", 300.0, 1.0, "easeOut", callback=on_complete)
    anim.start(player)
    ```

    closes #119

commit 9fb428dd01
    Update ROADMAP with GitHub issue numbers (#111-#125)

    Added issue numbers from GitHub tracker to roadmap items:
    - #111: Grid Click Events Broken in Headless
    - #112: Object Splitting Bug (Python type preservation)
    - #113: Batch Operations for Grid
    - #114: CellView API
    - #115: SpatialHash Implementation
    - #116: Dirty Flag System
    - #117: Memory Pool for Entities
    - #118: Scene as Drawable
    - #119: Animation Completion Callbacks
    - #120: Animation Property Locking
    - #121: Timer Object System
    - #122: Parent-Child UI System
    - #123: Grid Subgrid System
    - #124: Grid Point Animation
    - #125: GitHub Issues Automation

    Also updated existing references:
    - #101/#110: Constructor standardization
    - #109: Vector class indexing

    Note: Tutorial-specific items and Python-implementable features
    (input queue, collision reservation) are not tracked as engine issues.

commit 062e4dadc4
    Fix animation segfaults with RAII weak_ptr implementation

    Resolved two critical segmentation faults in AnimationManager:
    1. Race condition when creating multiple animations in timer callbacks
    2. Exit crash when animations outlive their target objects

    Changes:
    - Replace raw pointers with std::weak_ptr for automatic target invalidation
    - Add Animation::complete() to jump animations to final value
    - Add Animation::hasValidTarget() to check if target still exists
    - Update AnimationManager to auto-remove invalid animations
    - Add AnimationManager::clear() call to GameEngine::cleanup()
    - Update Python bindings to pass shared_ptr instead of raw pointers

    This ensures animations can never reference destroyed objects, following
    proper RAII principles. Tested with sizzle_reel_final.py and stress
    tests creating/destroying hundreds of animated objects.

commit 98fc49a978
    Directory structure cleanup and organization overhaul
This commit is contained in:
John McCardle 2025-07-15 21:30:49 -04:00
parent 1a143982e1
commit f4343e1e82
163 changed files with 12812 additions and 5441 deletions

1
.gitignore vendored
View File

@ -30,3 +30,4 @@ scripts/
test_*
tcod_reference
.archive

File diff suppressed because it is too large Load Diff

View File

@ -3,19 +3,27 @@
A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML.
* Core roguelike logic from libtcod: field of view, pathfinding
* Animate sprites with multiple frames. Smooth transitions for positions, sizes, zoom, and camera
* Simple GUI element system allows keyboard and mouse input, composition
* No compilation or installation necessary. The runtime is a full Python environment; "Zip And Ship"
![ Image ]()
**Pre-Alpha Release Demo**: my 7DRL 2025 entry *"Crypt of Sokoban"* - a prototype with buttons, boulders, enemies, and items.
## Tenets
- **Python & C++ Hand-in-Hand**: Create your game without ever recompiling. Your Python commands create C++ objects, and animations can occur without calling Python at all.
- **Simple Yet Flexible UI System**: Sprites, Grids, Frames, and Captions with full animation support
- **Entity-Component Architecture**: Implement your game objects with Python integration
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod (demos still under construction)
- **Automation API**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration
- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter
## Quick Start
**Download**:
- The entire McRogueFace visual framework:
- **Sprite**: an image file or one sprite from a shared sprite sheet
- **Caption**: load a font, display text
- **Frame**: A rectangle; put other things on it to move or manage GUIs as modules
- **Grid**: A 2D array of tiles with zoom + position control
- **Entity**: Lives on a Grid, displays a sprite, and can have a perspective or move along a path
- **Animation**: Change any property on any of the above over time
```bash
# Clone and build
git clone <wherever you found this repo>
@ -49,28 +57,59 @@ mcrfpy.setScene("intro")
## Documentation
### 📚 Full Documentation Site
For comprehensive documentation, tutorials, and API reference, visit:
**[https://mcrogueface.github.io](https://mcrogueface.github.io)**
## Requirements
The documentation site includes:
- **[Quickstart Guide](https://mcrogueface.github.io/quickstart/)** - Get running in 5 minutes
- **[McRogueFace Does The Entire Roguelike Tutorial](https://mcrogueface.github.io/tutorials/)** - Step-by-step game building
- **[Complete API Reference](https://mcrogueface.github.io/api/)** - Every function documented
- **[Cookbook](https://mcrogueface.github.io/cookbook/)** - Ready-to-use code recipes
- **[C++ Extension Guide](https://mcrogueface.github.io/extending-cpp/)** - For C++ developers: Add engine features
## Build Requirements
- C++17 compiler (GCC 7+ or Clang 5+)
- CMake 3.14+
- Python 3.12+
- SFML 2.5+
- SFML 2.6
- Linux or Windows (macOS untested)
## Project Structure
```
McRogueFace/
├── src/ # C++ engine source
├── scripts/ # Python game scripts
├── assets/ # Sprites, fonts, audio
├── build/ # Build output directory
├── build/ # Build output directory: zip + ship
│ ├─ (*)assets/ # (copied location of assets)
│ ├─ (*)scripts/ # (copied location of src/scripts)
│ └─ lib/ # SFML, TCOD libraries, Python + standard library / modules
├── deps/ # Python, SFML, and libtcod imports can be tossed in here to build
│ └─ platform/ # windows, linux subdirectories for OS-specific cpython config
├── docs/ # generated HTML, markdown docs
│ └─ stubs/ # .pyi files for editor integration
├── modules/ # git submodules, to build all of McRogueFace's dependencies from source
├── src/ # C++ engine source
│ └─ scripts/ # Python game scripts (copied during build)
└── tests/ # Automated test suite
└── tools/ # For the McRogueFace ecosystem: docs generation
```
If you are building McRogueFace to implement game logic or scene configuration in C++, you'll have to compile the project.
If you are writing a game in Python using McRogueFace, you only need to rename and zip/distribute the `build` directory.
## Philosophy
- **C++ every frame, Python every tick**: All rendering data is handled in C++. Structure your UI and program animations in Python, and they are rendered without Python. All game logic can be written in Python.
- **No Compiling Required; Zip And Ship**: Implement your game objects with Python, zip up McRogueFace with your "game.py" to ship
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod
- **Hands-Off Testing**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration
- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter
## Contributing
PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request.

42
build_windows_cmake.bat Normal file
View File

@ -0,0 +1,42 @@
@echo off
REM Windows build script using cmake --build (generator-agnostic)
REM This version works with any CMake generator
echo Building McRogueFace for Windows using CMake...
REM Set build directory
set BUILD_DIR=build_win
set CONFIG=Release
REM Clean previous build
if exist %BUILD_DIR% rmdir /s /q %BUILD_DIR%
mkdir %BUILD_DIR%
cd %BUILD_DIR%
REM Configure with CMake
REM You can change the generator here if needed:
REM -G "Visual Studio 17 2022" (VS 2022)
REM -G "Visual Studio 16 2019" (VS 2019)
REM -G "MinGW Makefiles" (MinGW)
REM -G "Ninja" (Ninja build system)
cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=%CONFIG% ..
if errorlevel 1 (
echo CMake configuration failed!
cd ..
exit /b 1
)
REM Build using cmake (works with any generator)
cmake --build . --config %CONFIG% --parallel
if errorlevel 1 (
echo Build failed!
cd ..
exit /b 1
)
echo.
echo Build completed successfully!
echo Executable: %BUILD_DIR%\%CONFIG%\mcrogueface.exe
echo.
cd ..

View File

@ -1,157 +0,0 @@
aqua #00FFFF
black #000000
blue #0000FF
fuchsia #FF00FF
gray #808080
green #008000
lime #00FF00
maroon #800000
navy #000080
olive #808000
purple #800080
red #FF0000
silver #C0C0C0
teal #008080
white #FFFFFF
yellow #FFFF00
aliceblue #F0F8FF
antiquewhite #FAEBD7
aqua #00FFFF
aquamarine #7FFFD4
azure #F0FFFF
beige #F5F5DC
bisque #FFE4C4
black #000000
blanchedalmond #FFEBCD
blue #0000FF
blueviolet #8A2BE2
brown #A52A2A
burlywood #DEB887
cadetblue #5F9EA0
chartreuse #7FFF00
chocolate #D2691E
coral #FF7F50
cornflowerblue #6495ED
cornsilk #FFF8DC
crimson #DC143C
cyan #00FFFF
darkblue #00008B
darkcyan #008B8B
darkgoldenrod #B8860B
darkgray #A9A9A9
darkgreen #006400
darkkhaki #BDB76B
darkmagenta #8B008B
darkolivegreen #556B2F
darkorange #FF8C00
darkorchid #9932CC
darkred #8B0000
darksalmon #E9967A
darkseagreen #8FBC8F
darkslateblue #483D8B
darkslategray #2F4F4F
darkturquoise #00CED1
darkviolet #9400D3
deeppink #FF1493
deepskyblue #00BFFF
dimgray #696969
dodgerblue #1E90FF
firebrick #B22222
floralwhite #FFFAF0
forestgreen #228B22
fuchsia #FF00FF
gainsboro #DCDCDC
ghostwhite #F8F8FF
gold #FFD700
goldenrod #DAA520
gray #7F7F7F
green #008000
greenyellow #ADFF2F
honeydew #F0FFF0
hotpink #FF69B4
indianred #CD5C5C
indigo #4B0082
ivory #FFFFF0
khaki #F0E68C
lavender #E6E6FA
lavenderblush #FFF0F5
lawngreen #7CFC00
lemonchiffon #FFFACD
lightblue #ADD8E6
lightcoral #F08080
lightcyan #E0FFFF
lightgoldenrodyellow #FAFAD2
lightgreen #90EE90
lightgrey #D3D3D3
lightpink #FFB6C1
lightsalmon #FFA07A
lightseagreen #20B2AA
lightskyblue #87CEFA
lightslategray #778899
lightsteelblue #B0C4DE
lightyellow #FFFFE0
lime #00FF00
limegreen #32CD32
linen #FAF0E6
magenta #FF00FF
maroon #800000
mediumaquamarine #66CDAA
mediumblue #0000CD
mediumorchid #BA55D3
mediumpurple #9370DB
mediumseagreen #3CB371
mediumslateblue #7B68EE
mediumspringgreen #00FA9A
mediumturquoise #48D1CC
mediumvioletred #C71585
midnightblue #191970
mintcream #F5FFFA
mistyrose #FFE4E1
moccasin #FFE4B5
navajowhite #FFDEAD
navy #000080
navyblue #9FAFDF
oldlace #FDF5E6
olive #808000
olivedrab #6B8E23
orange #FFA500
orangered #FF4500
orchid #DA70D6
palegoldenrod #EEE8AA
palegreen #98FB98
paleturquoise #AFEEEE
palevioletred #DB7093
papayawhip #FFEFD5
peachpuff #FFDAB9
peru #CD853F
pink #FFC0CB
plum #DDA0DD
powderblue #B0E0E6
purple #800080
red #FF0000
rosybrown #BC8F8F
royalblue #4169E1
saddlebrown #8B4513
salmon #FA8072
sandybrown #FA8072
seagreen #2E8B57
seashell #FFF5EE
sienna #A0522D
silver #C0C0C0
skyblue #87CEEB
slateblue #6A5ACD
slategray #708090
snow #FFFAFA
springgreen #00FF7F
steelblue #4682B4
tan #D2B48C
teal #008080
thistle #D8BFD8
tomato #FF6347
turquoise #40E0D0
violet #EE82EE
wheat #F5DEB3
white #FFFFFF
whitesmoke #F5F5F5
yellow #FFFF00
yellowgreen #9ACD32

View File

@ -1,5 +1,7 @@
# McRogueFace API Reference
*Generated on 2025-07-15 21:28:42*
## Overview
McRogueFace Python API
@ -373,14 +375,6 @@ 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.
@ -401,6 +395,14 @@ Move the element by a relative offset.
**Note:** This modifies the x and y position properties by the given amounts.
#### `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.
---
### class `Caption`
@ -409,14 +411,6 @@ 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.
@ -437,6 +431,14 @@ Move the element by a relative offset.
**Note:** This modifies the x and y position properties by the given amounts.
#### `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.
---
### class `Sprite`
@ -445,14 +447,6 @@ 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.
@ -473,6 +467,14 @@ Move the element by a relative offset.
**Note:** This modifies the x and y position properties by the given amounts.
#### `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.
---
### class `Grid`
@ -481,6 +483,16 @@ A grid-based tilemap UI element for rendering tile-based levels and game worlds.
#### Methods
#### `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.
#### `at(x, y)`
Get the GridPoint at the specified grid coordinates.
@ -491,24 +503,6 @@ Get the GridPoint at the specified grid coordinates.
**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.
@ -519,6 +513,14 @@ Move the element by a relative offset.
**Note:** This modifies the x and y position properties by the given amounts.
#### `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.
---
### class `Entity`
@ -527,12 +529,6 @@ 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.
@ -561,11 +557,11 @@ Get the bounding rectangle of this drawable element.
**Note:** The bounds are in screen coordinates and account for current position and size.
#### `index()`
#### `die()`
Get the index of this entity in its parent grid's entity list.
Remove this entity from its parent grid.
**Returns:** int: Index position, or -1 if not in a grid
**Note:** The entity object remains valid but is no longer rendered or updated.
#### `resize(width, height)`
@ -577,6 +573,12 @@ Resize the element to new dimensions.
**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
---
### Collections
@ -587,13 +589,6 @@ 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.
@ -603,6 +598,13 @@ Remove the first occurrence of an entity from the 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
#### `count(entity)`
Count the number of occurrences of an entity in the collection.
@ -623,12 +625,12 @@ Find the index of the first occurrence of an entity.
**Raises:** ValueError: If entity is not in collection
#### `extend(iterable)`
#### `append(entity)`
Add all entities from an iterable to the collection.
Add an entity to the end of the collection.
**Arguments:**
- `iterable` (*Iterable[Entity]*): Entities to add
- `entity` (*Entity*): The entity to add
---
@ -638,13 +640,6 @@ 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.
@ -654,6 +649,13 @@ Remove the first occurrence of a drawable from the 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
#### `count(drawable)`
Count the number of occurrences of a drawable in the collection.
@ -674,12 +676,12 @@ Find the index of the first occurrence of a drawable.
**Raises:** ValueError: If drawable is not in collection
#### `extend(iterable)`
#### `append(drawable)`
Add all drawables from an iterable to the collection.
Add a drawable element to the end of the collection.
**Arguments:**
- `iterable` (*Iterable[UIDrawable]*): Drawables to add
- `drawable` (*UIDrawable*): The drawable element to add
---
@ -703,6 +705,17 @@ RGBA color representation.
#### Methods
#### `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"
```
#### `from_hex(hex_string)`
Create a Color from a hexadecimal color string.
@ -717,17 +730,6 @@ Create a Color from a hexadecimal color string.
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.
@ -757,14 +759,13 @@ Calculate the length/magnitude of this vector.
**Returns:** float: The magnitude of the vector
#### `distance_to(other)`
#### `normalize()`
Calculate the distance to another vector.
Return a unit vector in the same direction.
**Arguments:**
- `other` (*Vector*): The other vector
**Returns:** Vector: New normalized vector with magnitude 1.0
**Returns:** float: Distance between the two vectors
**Raises:** ValueError: If vector has zero magnitude
#### `dot(other)`
@ -775,6 +776,21 @@ Calculate the dot product with another vector.
**Returns:** float: Dot product of the two vectors
#### `distance_to(other)`
Calculate the distance to another vector.
**Arguments:**
- `other` (*Vector*): The other vector
**Returns:** float: Distance between the two vectors
#### `copy()`
Create a copy of this vector.
**Returns:** Vector: New Vector object with same x and y values
#### `angle()`
Get the angle of this vector in radians.
@ -789,20 +805,6 @@ Calculate the squared magnitude of this vector.
**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`
@ -834,6 +836,12 @@ Animate UI element properties over time.
#### 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.
@ -852,12 +860,6 @@ Start the animation on a target UI element.
**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`
@ -866,14 +868,6 @@ 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.
@ -894,6 +888,14 @@ Move the element by a relative offset.
**Note:** This modifies the x and y position properties by the given amounts.
#### `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.
---
### class `GridPoint`
@ -945,18 +947,18 @@ def handle_keyboard(key, 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
#### `activate()`
Make this scene the active scene.
**Note:** Equivalent to calling setScene() with this scene's name.
#### `keypress(handler)`
Register a keyboard handler function for this scene.
@ -974,18 +976,6 @@ 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.
@ -998,6 +988,18 @@ Resume a paused timer.
**Note:** Has no effect if timer is not paused.
#### `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.
---
### class `Window`
@ -1006,14 +1008,6 @@ 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.
@ -1023,6 +1017,14 @@ Take a screenshot and save it to a file.
**Note:** Supports PNG, JPG, and BMP formats based on file extension.
#### `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.
#### `center()`
Center the window on the screen.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

532
docs/stubs/mcrfpy.pyi Normal file
View File

@ -0,0 +1,532 @@
"""Type stubs for McRogueFace Python API.
Core game engine interface for creating roguelike games with Python.
"""
from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
# Type aliases
UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid']
Transition = Union[str, None]
# Classes
class Color:
"""SFML Color Object for RGBA colors."""
r: int
g: int
b: int
a: int
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ...
def from_hex(self, hex_string: str) -> 'Color':
"""Create color from hex string (e.g., '#FF0000' or 'FF0000')."""
...
def to_hex(self) -> str:
"""Convert color to hex string format."""
...
def lerp(self, other: 'Color', t: float) -> 'Color':
"""Linear interpolation between two colors."""
...
class Vector:
"""SFML Vector Object for 2D coordinates."""
x: float
y: float
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float, y: float) -> None: ...
def add(self, other: 'Vector') -> 'Vector': ...
def subtract(self, other: 'Vector') -> 'Vector': ...
def multiply(self, scalar: float) -> 'Vector': ...
def divide(self, scalar: float) -> 'Vector': ...
def distance(self, other: 'Vector') -> float: ...
def normalize(self) -> 'Vector': ...
def dot(self, other: 'Vector') -> float: ...
class Texture:
"""SFML Texture Object for images."""
def __init__(self, filename: str) -> None: ...
filename: str
width: int
height: int
sprite_count: int
class Font:
"""SFML Font Object for text rendering."""
def __init__(self, filename: str) -> None: ...
filename: str
family: str
class Drawable:
"""Base class for all drawable UI elements."""
x: float
y: float
visible: bool
z_index: int
name: str
pos: Vector
def get_bounds(self) -> Tuple[float, float, float, float]:
"""Get bounding box as (x, y, width, height)."""
...
def move(self, dx: float, dy: float) -> None:
"""Move by relative offset (dx, dy)."""
...
def resize(self, width: float, height: float) -> None:
"""Resize to new dimensions (width, height)."""
...
class Frame(Drawable):
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)
A rectangular frame UI element that can contain other drawable elements.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0,
fill_color: Optional[Color] = None, outline_color: Optional[Color] = None,
outline: float = 0, click: Optional[Callable] = None,
children: Optional[List[UIElement]] = None) -> None: ...
w: float
h: float
fill_color: Color
outline_color: Color
outline: float
click: Optional[Callable[[float, float, int], None]]
children: 'UICollection'
clip_children: bool
class Caption(Drawable):
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)
A text display UI element with customizable font and styling.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, text: str = '', x: float = 0, y: float = 0,
font: Optional[Font] = None, fill_color: Optional[Color] = None,
outline_color: Optional[Color] = None, outline: float = 0,
click: Optional[Callable] = None) -> None: ...
text: str
font: Font
fill_color: Color
outline_color: Color
outline: float
click: Optional[Callable[[float, float, int], None]]
w: float # Read-only, computed from text
h: float # Read-only, computed from text
class Sprite(Drawable):
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)
A sprite UI element that displays a texture or portion of a texture atlas.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None,
sprite_index: int = 0, scale: float = 1.0,
click: Optional[Callable] = None) -> None: ...
texture: Texture
sprite_index: int
scale: float
click: Optional[Callable[[float, float, int], None]]
w: float # Read-only, computed from texture
h: float # Read-only, computed from texture
class Grid(Drawable):
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)
A grid-based tilemap UI element for rendering tile-based levels and game worlds.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20),
texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16,
scale: float = 1.0, click: Optional[Callable] = None) -> None: ...
grid_size: Tuple[int, int]
tile_width: int
tile_height: int
texture: Texture
scale: float
points: List[List['GridPoint']]
entities: 'EntityCollection'
background_color: Color
click: Optional[Callable[[int, int, int], None]]
def at(self, x: int, y: int) -> 'GridPoint':
"""Get grid point at tile coordinates."""
...
class GridPoint:
"""Grid point representing a single tile."""
texture_index: int
solid: bool
color: Color
class GridPointState:
"""State information for a grid point."""
texture_index: int
color: Color
class Entity(Drawable):
"""Entity(grid_x=0, grid_y=0, texture=None, sprite_index=0, name='')
Game entity that lives within a Grid.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, grid_x: float = 0, grid_y: float = 0, texture: Optional[Texture] = None,
sprite_index: int = 0, name: str = '') -> None: ...
grid_x: float
grid_y: float
texture: Texture
sprite_index: int
grid: Optional[Grid]
def at(self, grid_x: float, grid_y: float) -> None:
"""Move entity to grid position."""
...
def die(self) -> None:
"""Remove entity from its grid."""
...
def index(self) -> int:
"""Get index in parent grid's entity collection."""
...
class UICollection:
"""Collection of UI drawable elements (Frame, Caption, Sprite, Grid)."""
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> UIElement: ...
def __setitem__(self, index: int, value: UIElement) -> None: ...
def __delitem__(self, index: int) -> None: ...
def __contains__(self, item: UIElement) -> bool: ...
def __iter__(self) -> Any: ...
def __add__(self, other: 'UICollection') -> 'UICollection': ...
def __iadd__(self, other: 'UICollection') -> 'UICollection': ...
def append(self, item: UIElement) -> None: ...
def extend(self, items: List[UIElement]) -> None: ...
def remove(self, item: UIElement) -> None: ...
def index(self, item: UIElement) -> int: ...
def count(self, item: UIElement) -> int: ...
class EntityCollection:
"""Collection of Entity objects."""
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> Entity: ...
def __setitem__(self, index: int, value: Entity) -> None: ...
def __delitem__(self, index: int) -> None: ...
def __contains__(self, item: Entity) -> bool: ...
def __iter__(self) -> Any: ...
def __add__(self, other: 'EntityCollection') -> 'EntityCollection': ...
def __iadd__(self, other: 'EntityCollection') -> 'EntityCollection': ...
def append(self, item: Entity) -> None: ...
def extend(self, items: List[Entity]) -> None: ...
def remove(self, item: Entity) -> None: ...
def index(self, item: Entity) -> int: ...
def count(self, item: Entity) -> int: ...
class Scene:
"""Base class for object-oriented scenes."""
name: str
def __init__(self, name: str) -> None: ...
def activate(self) -> None:
"""Called when scene becomes active."""
...
def deactivate(self) -> None:
"""Called when scene becomes inactive."""
...
def get_ui(self) -> UICollection:
"""Get UI elements collection."""
...
def on_keypress(self, key: str, pressed: bool) -> None:
"""Handle keyboard events."""
...
def on_click(self, x: float, y: float, button: int) -> None:
"""Handle mouse clicks."""
...
def on_enter(self) -> None:
"""Called when entering the scene."""
...
def on_exit(self) -> None:
"""Called when leaving the scene."""
...
def on_resize(self, width: int, height: int) -> None:
"""Handle window resize events."""
...
def update(self, dt: float) -> None:
"""Update scene logic."""
...
class Timer:
"""Timer object for scheduled callbacks."""
name: str
interval: int
active: bool
def __init__(self, name: str, callback: Callable[[float], None], interval: int) -> None: ...
def pause(self) -> None:
"""Pause the timer."""
...
def resume(self) -> None:
"""Resume the timer."""
...
def cancel(self) -> None:
"""Cancel and remove the timer."""
...
class Window:
"""Window singleton for managing the game window."""
resolution: Tuple[int, int]
fullscreen: bool
vsync: bool
title: str
fps_limit: int
game_resolution: Tuple[int, int]
scaling_mode: str
@staticmethod
def get() -> 'Window':
"""Get the window singleton instance."""
...
class Animation:
"""Animation object for animating UI properties."""
target: Any
property: str
duration: float
easing: str
loop: bool
on_complete: Optional[Callable]
def __init__(self, target: Any, property: str, start_value: Any, end_value: Any,
duration: float, easing: str = 'linear', loop: bool = False,
on_complete: Optional[Callable] = None) -> None: ...
def start(self) -> None:
"""Start the animation."""
...
def update(self, dt: float) -> bool:
"""Update animation, returns True if still running."""
...
def get_current_value(self) -> Any:
"""Get the current interpolated value."""
...
# Module functions
def createSoundBuffer(filename: str) -> int:
"""Load a sound effect from a file and return its buffer ID."""
...
def loadMusic(filename: str) -> None:
"""Load and immediately play background music from a file."""
...
def setMusicVolume(volume: int) -> None:
"""Set the global music volume (0-100)."""
...
def setSoundVolume(volume: int) -> None:
"""Set the global sound effects volume (0-100)."""
...
def playSound(buffer_id: int) -> None:
"""Play a sound effect using a previously loaded buffer."""
...
def getMusicVolume() -> int:
"""Get the current music volume level (0-100)."""
...
def getSoundVolume() -> int:
"""Get the current sound effects volume level (0-100)."""
...
def sceneUI(scene: Optional[str] = None) -> UICollection:
"""Get all UI elements for a scene."""
...
def currentScene() -> str:
"""Get the name of the currently active scene."""
...
def setScene(scene: str, transition: Optional[str] = None, duration: float = 0.0) -> None:
"""Switch to a different scene with optional transition effect."""
...
def createScene(name: str) -> None:
"""Create a new empty scene."""
...
def keypressScene(handler: Callable[[str, bool], None]) -> None:
"""Set the keyboard event handler for the current scene."""
...
def setTimer(name: str, handler: Callable[[float], None], interval: int) -> None:
"""Create or update a recurring timer."""
...
def delTimer(name: str) -> None:
"""Stop and remove a timer."""
...
def exit() -> None:
"""Cleanly shut down the game engine and exit the application."""
...
def setScale(multiplier: float) -> None:
"""Scale the game window size (deprecated - use Window.resolution)."""
...
def find(name: str, scene: Optional[str] = None) -> Optional[UIElement]:
"""Find the first UI element with the specified name."""
...
def findAll(pattern: str, scene: Optional[str] = None) -> List[UIElement]:
"""Find all UI elements matching a name pattern (supports * wildcards)."""
...
def getMetrics() -> Dict[str, Union[int, float]]:
"""Get current performance metrics."""
...
# Submodule
class automation:
"""Automation API for testing and scripting."""
@staticmethod
def screenshot(filename: str) -> bool:
"""Save a screenshot to the specified file."""
...
@staticmethod
def position() -> Tuple[int, int]:
"""Get current mouse position as (x, y) tuple."""
...
@staticmethod
def size() -> Tuple[int, int]:
"""Get screen size as (width, height) tuple."""
...
@staticmethod
def onScreen(x: int, y: int) -> bool:
"""Check if coordinates are within screen bounds."""
...
@staticmethod
def moveTo(x: int, y: int, duration: float = 0.0) -> None:
"""Move mouse to absolute position."""
...
@staticmethod
def moveRel(xOffset: int, yOffset: int, duration: float = 0.0) -> None:
"""Move mouse relative to current position."""
...
@staticmethod
def dragTo(x: int, y: int, duration: float = 0.0, button: str = 'left') -> None:
"""Drag mouse to position."""
...
@staticmethod
def dragRel(xOffset: int, yOffset: int, duration: float = 0.0, button: str = 'left') -> None:
"""Drag mouse relative to current position."""
...
@staticmethod
def click(x: Optional[int] = None, y: Optional[int] = None, clicks: int = 1,
interval: float = 0.0, button: str = 'left') -> None:
"""Click mouse at position."""
...
@staticmethod
def mouseDown(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
"""Press mouse button down."""
...
@staticmethod
def mouseUp(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
"""Release mouse button."""
...
@staticmethod
def keyDown(key: str) -> None:
"""Press key down."""
...
@staticmethod
def keyUp(key: str) -> None:
"""Release key."""
...
@staticmethod
def press(key: str) -> None:
"""Press and release a key."""
...
@staticmethod
def typewrite(text: str, interval: float = 0.0) -> None:
"""Type text with optional interval between characters."""
...

View File

@ -0,0 +1,209 @@
"""Type stubs for McRogueFace Python API.
Auto-generated - do not edit directly.
"""
from typing import Any, List, Dict, Tuple, Optional, Callable, Union
# Module documentation
# McRogueFace Python API\n\nCore game engine interface for creating roguelike games with Python.\n\nThis module provides:\n- Scene management (createScene, setScene, currentScene)\n- UI components (Frame, Caption, Sprite, Grid)\n- Entity system for game objects\n- Audio playback (sound effects and music)\n- Timer system for scheduled events\n- Input handling\n- Performance metrics\n\nExample:\n import mcrfpy\n \n # Create a new scene\n mcrfpy.createScene('game')\n mcrfpy.setScene('game')\n \n # Add UI elements\n frame = mcrfpy.Frame(10, 10, 200, 100)\n caption = mcrfpy.Caption('Hello World', 50, 50)\n mcrfpy.sceneUI().extend([frame, caption])\n
# Classes
class Animation:
"""Animation object for animating UI properties"""
def __init__(selftype(self)) -> None: ...
def get_current_value(self, *args, **kwargs) -> Any: ...
def start(self, *args, **kwargs) -> Any: ...
def update(selfreturns True if still running) -> Any: ...
class Caption:
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Color:
"""SFML Color Object"""
def __init__(selftype(self)) -> None: ...
def from_hex(selfe.g., '#FF0000' or 'FF0000') -> Any: ...
def lerp(self, *args, **kwargs) -> Any: ...
def to_hex(self, *args, **kwargs) -> Any: ...
class Drawable:
"""Base class for all drawable UI elements"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Entity:
"""UIEntity objects"""
def __init__(selftype(self)) -> None: ...
def at(self, *args, **kwargs) -> Any: ...
def die(self, *args, **kwargs) -> Any: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def index(self, *args, **kwargs) -> Any: ...
def move(selfdx, dy) -> Any: ...
def path_to(selfx: int, y: int) -> bool: ...
def resize(selfwidth, height) -> Any: ...
def update_visibility(self) -> None: ...
class EntityCollection:
"""Iterable, indexable collection of Entities"""
def __init__(selftype(self)) -> None: ...
def append(self, *args, **kwargs) -> Any: ...
def count(self, *args, **kwargs) -> Any: ...
def extend(self, *args, **kwargs) -> Any: ...
def index(self, *args, **kwargs) -> Any: ...
def remove(self, *args, **kwargs) -> Any: ...
class Font:
"""SFML Font Object"""
def __init__(selftype(self)) -> None: ...
class Frame:
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Grid:
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)"""
def __init__(selftype(self)) -> None: ...
def at(self, *args, **kwargs) -> Any: ...
def compute_astar_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
def compute_dijkstra(selfroot_x: int, root_y: int, diagonal_cost: float = 1.41) -> None: ...
def compute_fov(selfx: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None: ...
def find_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def get_dijkstra_distance(selfx: int, y: int) -> Optional[float]: ...
def get_dijkstra_path(selfx: int, y: int) -> List[Tuple[int, int]]: ...
def is_in_fov(selfx: int, y: int) -> bool: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class GridPoint:
"""UIGridPoint object"""
def __init__(selftype(self)) -> None: ...
class GridPointState:
"""UIGridPointState object"""
def __init__(selftype(self)) -> None: ...
class Scene:
"""Base class for object-oriented scenes"""
def __init__(selftype(self)) -> None: ...
def activate(self, *args, **kwargs) -> Any: ...
def get_ui(self, *args, **kwargs) -> Any: ...
def register_keyboard(selfalternative to overriding on_keypress) -> Any: ...
class Sprite:
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Texture:
"""SFML Texture Object"""
def __init__(selftype(self)) -> None: ...
class Timer:
"""Timer object for scheduled callbacks"""
def __init__(selftype(self)) -> None: ...
def cancel(self, *args, **kwargs) -> Any: ...
def pause(self, *args, **kwargs) -> Any: ...
def restart(self, *args, **kwargs) -> Any: ...
def resume(self, *args, **kwargs) -> Any: ...
class UICollection:
"""Iterable, indexable collection of UI objects"""
def __init__(selftype(self)) -> None: ...
def append(self, *args, **kwargs) -> Any: ...
def count(self, *args, **kwargs) -> Any: ...
def extend(self, *args, **kwargs) -> Any: ...
def index(self, *args, **kwargs) -> Any: ...
def remove(self, *args, **kwargs) -> Any: ...
class UICollectionIter:
"""Iterator for a collection of UI objects"""
def __init__(selftype(self)) -> None: ...
class UIEntityCollectionIter:
"""Iterator for a collection of UI objects"""
def __init__(selftype(self)) -> None: ...
class Vector:
"""SFML Vector Object"""
def __init__(selftype(self)) -> None: ...
def angle(self, *args, **kwargs) -> Any: ...
def copy(self, *args, **kwargs) -> Any: ...
def distance_to(self, *args, **kwargs) -> Any: ...
def dot(self, *args, **kwargs) -> Any: ...
def magnitude(self, *args, **kwargs) -> Any: ...
def magnitude_squared(self, *args, **kwargs) -> Any: ...
def normalize(self, *args, **kwargs) -> Any: ...
class Window:
"""Window singleton for accessing and modifying the game window properties"""
def __init__(selftype(self)) -> None: ...
def center(self, *args, **kwargs) -> Any: ...
def get(self, *args, **kwargs) -> Any: ...
def screenshot(self, *args, **kwargs) -> Any: ...
# Functions
def createScene(name: str) -> None: ...
def createSoundBuffer(filename: str) -> int: ...
def currentScene() -> str: ...
def delTimer(name: str) -> None: ...
def exit() -> None: ...
def find(name: str, scene: str = None) -> UIDrawable | None: ...
def findAll(pattern: str, scene: str = None) -> list: ...
def getMetrics() -> dict: ...
def getMusicVolume() -> int: ...
def getSoundVolume() -> int: ...
def keypressScene(handler: callable) -> None: ...
def loadMusic(filename: str) -> None: ...
def playSound(buffer_id: int) -> None: ...
def sceneUI(scene: str = None) -> list: ...
def setMusicVolume(volume: int) -> None: ...
def setScale(multiplier: float) -> None: ...
def setScene(scene: str, transition: str = None, duration: float = 0.0) -> None: ...
def setSoundVolume(volume: int) -> None: ...
def setTimer(name: str, handler: callable, interval: int) -> None: ...
# Constants
FOV_BASIC: int
FOV_DIAMOND: int
FOV_PERMISSIVE_0: int
FOV_PERMISSIVE_1: int
FOV_PERMISSIVE_2: int
FOV_PERMISSIVE_3: int
FOV_PERMISSIVE_4: int
FOV_PERMISSIVE_5: int
FOV_PERMISSIVE_6: int
FOV_PERMISSIVE_7: int
FOV_PERMISSIVE_8: int
FOV_RESTRICTIVE: int
FOV_SHADOW: int
default_font: Any
default_texture: Any

View File

@ -0,0 +1,24 @@
"""Type stubs for McRogueFace automation API."""
from typing import Optional, Tuple
def click(x=None, y=None, clicks=1, interval=0.0, button='left') -> Any: ...
def doubleClick(x=None, y=None) -> Any: ...
def dragRel(xOffset, yOffset, duration=0.0, button='left') -> Any: ...
def dragTo(x, y, duration=0.0, button='left') -> Any: ...
def hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c')) -> Any: ...
def keyDown(key) -> Any: ...
def keyUp(key) -> Any: ...
def middleClick(x=None, y=None) -> Any: ...
def mouseDown(x=None, y=None, button='left') -> Any: ...
def mouseUp(x=None, y=None, button='left') -> Any: ...
def moveRel(xOffset, yOffset, duration=0.0) -> Any: ...
def moveTo(x, y, duration=0.0) -> Any: ...
def onScreen(x, y) -> Any: ...
def position() - Get current mouse position as (x, y) -> Any: ...
def rightClick(x=None, y=None) -> Any: ...
def screenshot(filename) -> Any: ...
def scroll(clicks, x=None, y=None) -> Any: ...
def size() - Get screen size as (width, height) -> Any: ...
def tripleClick(x=None, y=None) -> Any: ...
def typewrite(message, interval=0.0) -> Any: ...

0
docs/stubs/py.typed Normal file
View File

View File

@ -0,0 +1,80 @@
"""
McRogueFace Tutorial - Part 0: Introduction to Scene, Texture, and Grid
This tutorial introduces the basic building blocks:
- Scene: A container for UI elements and game state
- Texture: Loading image assets for use in the game
- Grid: A tilemap component for rendering tile-based worlds
"""
import mcrfpy
import random
# Create and activate a new scene
mcrfpy.createScene("tutorial")
mcrfpy.setScene("tutorial")
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
# Create a grid of tiles
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
grid_width, grid_height = 25, 20 # width, height in number of tiles
# calculating the size in pixels to fit the entire grid on-screen
zoom = 2.0
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
grid = mcrfpy.Grid(
pos=grid_position,
grid_size=(grid_width, grid_height),
texture=texture,
size=grid_size, # height and width on screen
)
grid.zoom = zoom
grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile
# Define tile types
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
WALL_TILES = [3, 7, 11]
# Fill the grid with a simple pattern
for y in range(grid_height):
for x in range(grid_width):
# Create walls around the edges
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
tile_index = random.choice(WALL_TILES)
else:
# Fill interior with floor tiles
tile_index = random.choice(FLOOR_TILES)
# Set the tile at this position
point = grid.at(x, y)
if point:
point.tilesprite = tile_index
# Add the grid to the scene
mcrfpy.sceneUI("tutorial").append(grid)
# Add a title caption
title = mcrfpy.Caption((320, 10),
text="McRogueFace Tutorial - Part 0",
)
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
mcrfpy.sceneUI("tutorial").append(title)
# Add instructions
instructions = mcrfpy.Caption((280, 750),
text="Scene + Texture + Grid = Tilemap!",
)
instructions.font_size=18
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
mcrfpy.sceneUI("tutorial").append(instructions)
print("Tutorial Part 0 loaded!")
print(f"Created a {grid.grid_size[0]}x{grid.grid_size[1]} grid")
print(f"Grid positioned at ({grid.x}, {grid.y})")

View File

@ -0,0 +1,116 @@
"""
McRogueFace Tutorial - Part 1: Entities and Keyboard Input
This tutorial builds on Part 0 by adding:
- Entity: A game object that can be placed in a grid
- Keyboard handling: Responding to key presses to move the entity
"""
import mcrfpy
import random
# Create and activate a new scene
mcrfpy.createScene("tutorial")
mcrfpy.setScene("tutorial")
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
# Load the hero sprite texture (32x32 sprite sheet)
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
# Create a grid of tiles
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
grid_width, grid_height = 25, 20 # width, height in number of tiles
# calculating the size in pixels to fit the entire grid on-screen
zoom = 2.0
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
grid = mcrfpy.Grid(
pos=grid_position,
grid_size=(grid_width, grid_height),
texture=texture,
size=grid_size, # height and width on screen
)
grid.zoom = zoom
grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile
# Define tile types
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
WALL_TILES = [3, 7, 11]
# Fill the grid with a simple pattern
for y in range(grid_height):
for x in range(grid_width):
# Create walls around the edges
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
tile_index = random.choice(WALL_TILES)
else:
# Fill interior with floor tiles
tile_index = random.choice(FLOOR_TILES)
# Set the tile at this position
point = grid.at(x, y)
if point:
point.tilesprite = tile_index
# Add the grid to the scene
mcrfpy.sceneUI("tutorial").append(grid)
# Create a player entity at position (4, 4)
player = mcrfpy.Entity(
(4, 4), # Entity positions are tile coordinates
texture=hero_texture,
sprite_index=0 # Use the first sprite in the texture
)
# Add the player entity to the grid
grid.entities.append(player)
# Define keyboard handler
def handle_keys(key, state):
"""Handle keyboard input to move the player"""
if state == "start": # Only respond to key press, not release
# Get current player position in grid coordinates
px, py = player.x, player.y
# Calculate new position based on key press
if key == "W" or key == "Up":
py -= 1
elif key == "S" or key == "Down":
py += 1
elif key == "A" or key == "Left":
px -= 1
elif key == "D" or key == "Right":
px += 1
# Update player position (no collision checking yet)
player.x = px
player.y = py
# Register the keyboard handler
mcrfpy.keypressScene(handle_keys)
# Add a title caption
title = mcrfpy.Caption((320, 10),
text="McRogueFace Tutorial - Part 1",
)
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
mcrfpy.sceneUI("tutorial").append(title)
# Add instructions
instructions = mcrfpy.Caption((200, 750),
text="Use WASD or Arrow Keys to move the hero!",
)
instructions.font_size=18
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
mcrfpy.sceneUI("tutorial").append(instructions)
print("Tutorial Part 1 loaded!")
print(f"Player entity created at grid position (4, 4)")
print("Use WASD or Arrow keys to move!")

View File

@ -0,0 +1,117 @@
"""
McRogueFace Tutorial - Part 1: Entities and Keyboard Input
This tutorial builds on Part 0 by adding:
- Entity: A game object that can be placed in a grid
- Keyboard handling: Responding to key presses to move the entity
"""
import mcrfpy
import random
# Create and activate a new scene
mcrfpy.createScene("tutorial")
mcrfpy.setScene("tutorial")
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
# Load the hero sprite texture (32x32 sprite sheet)
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
# Create a grid of tiles
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
grid_width, grid_height = 25, 20 # width, height in number of tiles
# calculating the size in pixels to fit the entire grid on-screen
zoom = 2.0
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
grid = mcrfpy.Grid(
pos=grid_position,
grid_size=(grid_width, grid_height),
texture=texture,
size=grid_size, # height and width on screen
)
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
# Define tile types
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
WALL_TILES = [3, 7, 11]
# Fill the grid with a simple pattern
for y in range(grid_height):
for x in range(grid_width):
# Create walls around the edges
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
tile_index = random.choice(WALL_TILES)
else:
# Fill interior with floor tiles
tile_index = random.choice(FLOOR_TILES)
# Set the tile at this position
point = grid.at(x, y)
if point:
point.tilesprite = tile_index
# Add the grid to the scene
mcrfpy.sceneUI("tutorial").append(grid)
# Create a player entity at position (4, 4)
player = mcrfpy.Entity(
(4, 4), # Entity positions are tile coordinates
texture=hero_texture,
sprite_index=0 # Use the first sprite in the texture
)
# Add the player entity to the grid
grid.entities.append(player)
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
# Define keyboard handler
def handle_keys(key, state):
"""Handle keyboard input to move the player"""
if state == "start": # Only respond to key press, not release
# Get current player position in grid coordinates
px, py = player.x, player.y
# Calculate new position based on key press
if key == "W" or key == "Up":
py -= 1
elif key == "S" or key == "Down":
py += 1
elif key == "A" or key == "Left":
px -= 1
elif key == "D" or key == "Right":
px += 1
# Update player position (no collision checking yet)
player.x = px
player.y = py
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
# Register the keyboard handler
mcrfpy.keypressScene(handle_keys)
# Add a title caption
title = mcrfpy.Caption((320, 10),
text="McRogueFace Tutorial - Part 1",
)
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
mcrfpy.sceneUI("tutorial").append(title)
# Add instructions
instructions = mcrfpy.Caption((200, 750),
text="Use WASD or Arrow Keys to move the hero!",
)
instructions.font_size=18
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
mcrfpy.sceneUI("tutorial").append(instructions)
print("Tutorial Part 1 loaded!")
print(f"Player entity created at grid position (4, 4)")
print("Use WASD or Arrow keys to move!")

View File

@ -0,0 +1,149 @@
"""
McRogueFace Tutorial - Part 2: Animated Movement
This tutorial builds on Part 1 by adding:
- Animation system for smooth movement
- Movement that takes 0.5 seconds per tile
- Input blocking during movement animation
"""
import mcrfpy
import random
# Create and activate a new scene
mcrfpy.createScene("tutorial")
mcrfpy.setScene("tutorial")
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
# Load the hero sprite texture (32x32 sprite sheet)
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
# Create a grid of tiles
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
grid_width, grid_height = 25, 20 # width, height in number of tiles
# calculating the size in pixels to fit the entire grid on-screen
zoom = 2.0
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
grid = mcrfpy.Grid(
pos=grid_position,
grid_size=(grid_width, grid_height),
texture=texture,
size=grid_size, # height and width on screen
)
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
# Define tile types
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
WALL_TILES = [3, 7, 11]
# Fill the grid with a simple pattern
for y in range(grid_height):
for x in range(grid_width):
# Create walls around the edges
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
tile_index = random.choice(WALL_TILES)
else:
# Fill interior with floor tiles
tile_index = random.choice(FLOOR_TILES)
# Set the tile at this position
point = grid.at(x, y)
if point:
point.tilesprite = tile_index
# Add the grid to the scene
mcrfpy.sceneUI("tutorial").append(grid)
# Create a player entity at position (4, 4)
player = mcrfpy.Entity(
(4, 4), # Entity positions are tile coordinates
texture=hero_texture,
sprite_index=0 # Use the first sprite in the texture
)
# Add the player entity to the grid
grid.entities.append(player)
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
# Movement state tracking
is_moving = False
move_animations = [] # Track active animations
# Animation completion callback
def movement_complete(runtime):
"""Called when movement animation completes"""
global is_moving
is_moving = False
# Ensure grid is centered on final position
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
motion_speed = 0.30 # seconds per tile
# Define keyboard handler
def handle_keys(key, state):
"""Handle keyboard input to move the player"""
global is_moving, move_animations
if state == "start" and not is_moving: # Only respond to key press when not moving
# Get current player position in grid coordinates
px, py = player.x, player.y
new_x, new_y = px, py
# Calculate new position based on key press
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
# If position changed, start movement animation
if new_x != px or new_y != py:
is_moving = True
# Create animations for player position
anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad")
anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
anim_x.start(player)
anim_y.start(player)
# Animate grid center to follow player
center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
center_x.start(grid)
center_y.start(grid)
# Set a timer to mark movement as complete
mcrfpy.setTimer("move_complete", movement_complete, 500)
# Register the keyboard handler
mcrfpy.keypressScene(handle_keys)
# Add a title caption
title = mcrfpy.Caption((320, 10),
text="McRogueFace Tutorial - Part 2",
)
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
mcrfpy.sceneUI("tutorial").append(title)
# Add instructions
instructions = mcrfpy.Caption((150, 750),
text="Smooth movement! Each step takes 0.5 seconds.",
)
instructions.font_size=18
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
mcrfpy.sceneUI("tutorial").append(instructions)
print("Tutorial Part 2 loaded!")
print(f"Player entity created at grid position (4, 4)")
print("Movement is now animated over 0.5 seconds per tile!")
print("Use WASD or Arrow keys to move!")

View File

@ -0,0 +1,241 @@
"""
McRogueFace Tutorial - Part 2: Enhanced with Single Move Queue
This tutorial builds on Part 2 by adding:
- Single queued move system for responsive input
- Debug display showing position and queue status
- Smooth continuous movement when keys are held
- Animation callbacks to prevent race conditions
"""
import mcrfpy
import random
# Create and activate a new scene
mcrfpy.createScene("tutorial")
mcrfpy.setScene("tutorial")
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
# Load the hero sprite texture (32x32 sprite sheet)
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
# Create a grid of tiles
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
grid_width, grid_height = 25, 20 # width, height in number of tiles
# calculating the size in pixels to fit the entire grid on-screen
zoom = 2.0
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
grid = mcrfpy.Grid(
pos=grid_position,
grid_size=(grid_width, grid_height),
texture=texture,
size=grid_size, # height and width on screen
)
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
# Define tile types
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
WALL_TILES = [3, 7, 11]
# Fill the grid with a simple pattern
for y in range(grid_height):
for x in range(grid_width):
# Create walls around the edges
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
tile_index = random.choice(WALL_TILES)
else:
# Fill interior with floor tiles
tile_index = random.choice(FLOOR_TILES)
# Set the tile at this position
point = grid.at(x, y)
if point:
point.tilesprite = tile_index
# Add the grid to the scene
mcrfpy.sceneUI("tutorial").append(grid)
# Create a player entity at position (4, 4)
player = mcrfpy.Entity(
(4, 4), # Entity positions are tile coordinates
texture=hero_texture,
sprite_index=0 # Use the first sprite in the texture
)
# Add the player entity to the grid
grid.entities.append(player)
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
# Movement state tracking
is_moving = False
move_queue = [] # List to store queued moves (max 1 item)
#last_position = (4, 4) # Track last position
current_destination = None # Track where we're currently moving to
current_move = None # Track current move direction
# Store animation references
player_anim_x = None
player_anim_y = None
grid_anim_x = None
grid_anim_y = None
# Debug display caption
debug_caption = mcrfpy.Caption((10, 40),
text="Last: (4, 4) | Queue: 0 | Dest: None",
)
debug_caption.font_size = 16
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
mcrfpy.sceneUI("tutorial").append(debug_caption)
# Additional debug caption for movement state
move_debug_caption = mcrfpy.Caption((10, 60),
text="Moving: False | Current: None | Queued: None",
)
move_debug_caption.font_size = 16
move_debug_caption.fill_color = mcrfpy.Color(255, 200, 0, 255)
mcrfpy.sceneUI("tutorial").append(move_debug_caption)
def key_to_direction(key):
"""Convert key to direction string"""
if key == "W" or key == "Up":
return "Up"
elif key == "S" or key == "Down":
return "Down"
elif key == "A" or key == "Left":
return "Left"
elif key == "D" or key == "Right":
return "Right"
return None
def update_debug_display():
"""Update the debug caption with current state"""
queue_count = len(move_queue)
dest_text = f"({current_destination[0]}, {current_destination[1]})" if current_destination else "None"
debug_caption.text = f"Last: ({player.x}, {player.y}) | Queue: {queue_count} | Dest: {dest_text}"
# Update movement state debug
current_dir = key_to_direction(current_move) if current_move else "None"
queued_dir = key_to_direction(move_queue[0]) if move_queue else "None"
move_debug_caption.text = f"Moving: {is_moving} | Current: {current_dir} | Queued: {queued_dir}"
# Animation completion callback
def movement_complete(anim, target):
"""Called when movement animation completes"""
global is_moving, move_queue, current_destination, current_move
global player_anim_x, player_anim_y
print(f"In callback for animation: {anim=} {target=}")
# Clear movement state
is_moving = False
current_move = None
current_destination = None
# Clear animation references
player_anim_x = None
player_anim_y = None
# Update last position to where we actually are now
#last_position = (int(player.x), int(player.y))
# Ensure grid is centered on final position
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
# Check if there's a queued move
if move_queue:
# Pop the next move from the queue
next_move = move_queue.pop(0)
print(f"Processing queued move: {next_move}")
# Process it like a fresh input
process_move(next_move)
update_debug_display()
motion_speed = 0.30 # seconds per tile
def process_move(key):
"""Process a move based on the key"""
global is_moving, current_move, current_destination, move_queue
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
# If already moving, just update the queue
if is_moving:
print(f"process_move processing {key=} as a queued move (is_moving = True)")
# Clear queue and add new move (only keep 1 queued move)
move_queue.clear()
move_queue.append(key)
update_debug_display()
return
print(f"process_move processing {key=} as a new, immediate animation (is_moving = False)")
# Calculate new position from current position
px, py = int(player.x), int(player.y)
new_x, new_y = px, py
# Calculate new position based on key press (only one tile movement)
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
# Start the move if position changed
if new_x != px or new_y != py:
is_moving = True
current_move = key
current_destination = (new_x, new_y)
# only animate a single axis, same callback from either
if new_x != px:
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
player_anim_x.start(player)
elif new_y != py:
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad", callback=movement_complete)
player_anim_y.start(player)
# Animate grid center to follow player
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
grid_anim_x.start(grid)
grid_anim_y.start(grid)
update_debug_display()
# Define keyboard handler
def handle_keys(key, state):
"""Handle keyboard input to move the player"""
if state == "start":
# Only process movement keys
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
print(f"handle_keys producing actual input: {key=}")
process_move(key)
# Register the keyboard handler
mcrfpy.keypressScene(handle_keys)
# Add a title caption
title = mcrfpy.Caption((320, 10),
text="McRogueFace Tutorial - Part 2 Enhanced",
)
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
mcrfpy.sceneUI("tutorial").append(title)
# Add instructions
instructions = mcrfpy.Caption((150, 750),
text="One-move queue system with animation callbacks!",
)
instructions.font_size=18
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
mcrfpy.sceneUI("tutorial").append(instructions)
print("Tutorial Part 2 Enhanced loaded!")
print(f"Player entity created at grid position (4, 4)")
print("Movement now uses animation callbacks to prevent race conditions!")
print("Use WASD or Arrow keys to move!")

View File

@ -0,0 +1,149 @@
"""
McRogueFace Tutorial - Part 2: Animated Movement
This tutorial builds on Part 1 by adding:
- Animation system for smooth movement
- Movement that takes 0.5 seconds per tile
- Input blocking during movement animation
"""
import mcrfpy
import random
# Create and activate a new scene
mcrfpy.createScene("tutorial")
mcrfpy.setScene("tutorial")
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
# Load the hero sprite texture (32x32 sprite sheet)
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
# Create a grid of tiles
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
grid_width, grid_height = 25, 20 # width, height in number of tiles
# calculating the size in pixels to fit the entire grid on-screen
zoom = 2.0
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
grid = mcrfpy.Grid(
pos=grid_position,
grid_size=(grid_width, grid_height),
texture=texture,
size=grid_size, # height and width on screen
)
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
# Define tile types
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
WALL_TILES = [3, 7, 11]
# Fill the grid with a simple pattern
for y in range(grid_height):
for x in range(grid_width):
# Create walls around the edges
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
tile_index = random.choice(WALL_TILES)
else:
# Fill interior with floor tiles
tile_index = random.choice(FLOOR_TILES)
# Set the tile at this position
point = grid.at(x, y)
if point:
point.tilesprite = tile_index
# Add the grid to the scene
mcrfpy.sceneUI("tutorial").append(grid)
# Create a player entity at position (4, 4)
player = mcrfpy.Entity(
(4, 4), # Entity positions are tile coordinates
texture=hero_texture,
sprite_index=0 # Use the first sprite in the texture
)
# Add the player entity to the grid
grid.entities.append(player)
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
# Movement state tracking
is_moving = False
move_animations = [] # Track active animations
# Animation completion callback
def movement_complete(runtime):
"""Called when movement animation completes"""
global is_moving
is_moving = False
# Ensure grid is centered on final position
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
motion_speed = 0.30 # seconds per tile
# Define keyboard handler
def handle_keys(key, state):
"""Handle keyboard input to move the player"""
global is_moving, move_animations
if state == "start" and not is_moving: # Only respond to key press when not moving
# Get current player position in grid coordinates
px, py = player.x, player.y
new_x, new_y = px, py
# Calculate new position based on key press
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
# If position changed, start movement animation
if new_x != px or new_y != py:
is_moving = True
# Create animations for player position
anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad")
anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
anim_x.start(player)
anim_y.start(player)
# Animate grid center to follow player
center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
center_x.start(grid)
center_y.start(grid)
# Set a timer to mark movement as complete
mcrfpy.setTimer("move_complete", movement_complete, 500)
# Register the keyboard handler
mcrfpy.keypressScene(handle_keys)
# Add a title caption
title = mcrfpy.Caption((320, 10),
text="McRogueFace Tutorial - Part 2",
)
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
mcrfpy.sceneUI("tutorial").append(title)
# Add instructions
instructions = mcrfpy.Caption((150, 750),
"Smooth movement! Each step takes 0.5 seconds.",
)
instructions.font_size=18
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
mcrfpy.sceneUI("tutorial").append(instructions)
print("Tutorial Part 2 loaded!")
print(f"Player entity created at grid position (4, 4)")
print("Movement is now animated over 0.5 seconds per tile!")
print("Use WASD or Arrow keys to move!")

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,6 +1,9 @@
#include "Animation.h"
#include "UIDrawable.h"
#include "UIEntity.h"
#include "PyAnimation.h"
#include "McRFPy_API.h"
#include "PythonObjectCache.h"
#include <cmath>
#include <algorithm>
#include <unordered_map>
@ -9,75 +12,105 @@
#define M_PI 3.14159265358979323846
#endif
// Forward declaration of PyAnimation type
namespace mcrfpydef {
extern PyTypeObject PyAnimationType;
}
// Animation implementation
Animation::Animation(const std::string& targetProperty,
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc,
bool delta)
bool delta,
PyObject* callback)
: targetProperty(targetProperty)
, targetValue(targetValue)
, duration(duration)
, easingFunc(easingFunc)
, delta(delta)
, pythonCallback(callback)
{
// Increase reference count for Python callback
if (pythonCallback) {
Py_INCREF(pythonCallback);
}
}
void Animation::start(UIDrawable* target) {
currentTarget = target;
Animation::~Animation() {
// Decrease reference count for Python callback if we still own it
PyObject* callback = pythonCallback;
if (callback) {
pythonCallback = nullptr;
PyGILState_STATE gstate = PyGILState_Ensure();
Py_DECREF(callback);
PyGILState_Release(gstate);
}
// Clean up cache entry
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
}
void Animation::start(std::shared_ptr<UIDrawable> target) {
if (!target) return;
targetWeak = target;
elapsed = 0.0f;
callbackTriggered = false; // Reset callback state
// Capture startValue from target based on targetProperty
if (!currentTarget) return;
// Try to get the current value based on the expected type
std::visit([this](const auto& targetVal) {
// Capture start value from target
std::visit([this, &target](const auto& targetVal) {
using T = std::decay_t<decltype(targetVal)>;
if constexpr (std::is_same_v<T, float>) {
float value;
if (currentTarget->getProperty(targetProperty, value)) {
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, int>) {
int value;
if (currentTarget->getProperty(targetProperty, value)) {
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// For sprite animation, get current sprite index
int value;
if (currentTarget->getProperty(targetProperty, value)) {
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, sf::Color>) {
sf::Color value;
if (currentTarget->getProperty(targetProperty, value)) {
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
sf::Vector2f value;
if (currentTarget->getProperty(targetProperty, value)) {
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, std::string>) {
std::string value;
if (currentTarget->getProperty(targetProperty, value)) {
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
}, targetValue);
}
void Animation::startEntity(UIEntity* target) {
currentEntityTarget = target;
currentTarget = nullptr; // Clear drawable target
void Animation::startEntity(std::shared_ptr<UIEntity> target) {
if (!target) return;
entityTargetWeak = target;
elapsed = 0.0f;
callbackTriggered = false; // Reset callback state
// Capture the starting value from the entity
std::visit([this, target](const auto& val) {
@ -99,8 +132,49 @@ void Animation::startEntity(UIEntity* target) {
}, targetValue);
}
bool Animation::hasValidTarget() const {
return !targetWeak.expired() || !entityTargetWeak.expired();
}
void Animation::clearCallback() {
// Safely clear the callback when PyAnimation is being destroyed
PyObject* callback = pythonCallback;
if (callback) {
pythonCallback = nullptr;
callbackTriggered = true; // Prevent future triggering
PyGILState_STATE gstate = PyGILState_Ensure();
Py_DECREF(callback);
PyGILState_Release(gstate);
}
}
void Animation::complete() {
// Jump to end of animation
elapsed = duration;
// Apply final value
if (auto target = targetWeak.lock()) {
AnimationValue finalValue = interpolate(1.0f);
applyValue(target.get(), finalValue);
}
else if (auto entity = entityTargetWeak.lock()) {
AnimationValue finalValue = interpolate(1.0f);
applyValue(entity.get(), finalValue);
}
}
bool Animation::update(float deltaTime) {
if ((!currentTarget && !currentEntityTarget) || isComplete()) {
// Try to lock weak_ptr to get shared_ptr
std::shared_ptr<UIDrawable> target = targetWeak.lock();
std::shared_ptr<UIEntity> entity = entityTargetWeak.lock();
// If both are null, target was destroyed
if (!target && !entity) {
return false; // Remove this animation
}
if (isComplete()) {
return false;
}
@ -114,39 +188,18 @@ bool Animation::update(float deltaTime) {
// Get interpolated value
AnimationValue currentValue = interpolate(easedT);
// Apply currentValue to target (either drawable or entity)
std::visit([this](const auto& value) {
using T = std::decay_t<decltype(value)>;
if (currentTarget) {
// Handle UIDrawable targets
if constexpr (std::is_same_v<T, float>) {
currentTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, int>) {
currentTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
currentTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
currentTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, std::string>) {
currentTarget->setProperty(targetProperty, value);
}
}
else if (currentEntityTarget) {
// Handle UIEntity targets
if constexpr (std::is_same_v<T, float>) {
currentEntityTarget->setProperty(targetProperty, value);
}
else if constexpr (std::is_same_v<T, int>) {
currentEntityTarget->setProperty(targetProperty, value);
}
// Entities don't support other types yet
}
}, currentValue);
// Apply to whichever target is valid
if (target) {
applyValue(target.get(), currentValue);
} else if (entity) {
applyValue(entity.get(), currentValue);
}
// Trigger callback when animation completes
// Check pythonCallback again in case it was cleared during update
if (isComplete() && !callbackTriggered && pythonCallback) {
triggerCallback();
}
return !isComplete();
}
@ -254,6 +307,77 @@ AnimationValue Animation::interpolate(float t) const {
}, targetValue);
}
void Animation::applyValue(UIDrawable* target, const AnimationValue& value) {
if (!target) return;
std::visit([this, target](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, int>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, std::string>) {
target->setProperty(targetProperty, val);
}
}, value);
}
void Animation::applyValue(UIEntity* entity, const AnimationValue& value) {
if (!entity) return;
std::visit([this, entity](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
entity->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, int>) {
entity->setProperty(targetProperty, val);
}
// Entities don't support other types yet
}, value);
}
void Animation::triggerCallback() {
if (!pythonCallback) return;
// Ensure we only trigger once
if (callbackTriggered) return;
callbackTriggered = true;
PyGILState_STATE gstate = PyGILState_Ensure();
// TODO: In future, create PyAnimation wrapper for this animation
// For now, pass None for both parameters
PyObject* args = PyTuple_New(2);
Py_INCREF(Py_None);
Py_INCREF(Py_None);
PyTuple_SetItem(args, 0, Py_None); // animation parameter
PyTuple_SetItem(args, 1, Py_None); // target parameter
PyObject* result = PyObject_CallObject(pythonCallback, args);
Py_DECREF(args);
if (!result) {
// Print error but don't crash
PyErr_Print();
PyErr_Clear(); // Clear the error state
} else {
Py_DECREF(result);
}
PyGILState_Release(gstate);
}
// Easing functions implementation
namespace EasingFunctions {
@ -502,26 +626,50 @@ AnimationManager& AnimationManager::getInstance() {
}
void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) {
activeAnimations.push_back(animation);
if (animation && animation->hasValidTarget()) {
if (isUpdating) {
// Defer adding during update to avoid iterator invalidation
pendingAnimations.push_back(animation);
} else {
activeAnimations.push_back(animation);
}
}
}
void AnimationManager::update(float deltaTime) {
for (auto& anim : activeAnimations) {
anim->update(deltaTime);
}
cleanup();
}
void AnimationManager::cleanup() {
// Set flag to defer new animations
isUpdating = true;
// Remove completed or invalid animations
activeAnimations.erase(
std::remove_if(activeAnimations.begin(), activeAnimations.end(),
[](const std::shared_ptr<Animation>& anim) {
return anim->isComplete();
[deltaTime](std::shared_ptr<Animation>& anim) {
return !anim || !anim->update(deltaTime);
}),
activeAnimations.end()
);
// Clear update flag
isUpdating = false;
// Add any animations that were created during update
if (!pendingAnimations.empty()) {
activeAnimations.insert(activeAnimations.end(),
pendingAnimations.begin(),
pendingAnimations.end());
pendingAnimations.clear();
}
}
void AnimationManager::clear() {
void AnimationManager::clear(bool completeAnimations) {
if (completeAnimations) {
// Complete all animations before clearing
for (auto& anim : activeAnimations) {
if (anim) {
anim->complete();
}
}
}
activeAnimations.clear();
}

View File

@ -6,6 +6,7 @@
#include <variant>
#include <vector>
#include <SFML/Graphics.hpp>
#include "Python.h"
// Forward declarations
class UIDrawable;
@ -36,13 +37,20 @@ public:
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc = EasingFunctions::linear,
bool delta = false);
bool delta = false,
PyObject* callback = nullptr);
// Destructor - cleanup Python callback reference
~Animation();
// Apply this animation to a drawable
void start(UIDrawable* target);
void start(std::shared_ptr<UIDrawable> target);
// Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable)
void startEntity(UIEntity* target);
void startEntity(std::shared_ptr<UIEntity> target);
// Complete the animation immediately (jump to final value)
void complete();
// Update animation (called each frame)
// Returns true if animation is still running, false if complete
@ -51,6 +59,12 @@ public:
// Get current interpolated value
AnimationValue getCurrentValue() const;
// Check if animation has valid target
bool hasValidTarget() const;
// Clear the callback (called when PyAnimation is deallocated)
void clearCallback();
// Animation properties
std::string getTargetProperty() const { return targetProperty; }
float getDuration() const { return duration; }
@ -67,11 +81,27 @@ private:
EasingFunction easingFunc; // Easing function to use
bool delta; // If true, targetValue is relative to start
UIDrawable* currentTarget = nullptr; // Current target being animated
UIEntity* currentEntityTarget = nullptr; // Current entity target (alternative to drawable)
// RAII: Use weak_ptr for safe target tracking
std::weak_ptr<UIDrawable> targetWeak;
std::weak_ptr<UIEntity> entityTargetWeak;
// Callback support
PyObject* pythonCallback = nullptr; // Python callback function (we own a reference)
bool callbackTriggered = false; // Ensure callback only fires once
PyObject* pyAnimationWrapper = nullptr; // Weak reference to PyAnimation if created from Python
// Python object cache support
uint64_t serial_number = 0;
// Helper to interpolate between values
AnimationValue interpolate(float t) const;
// Helper to apply value to target
void applyValue(UIDrawable* target, const AnimationValue& value);
void applyValue(UIEntity* entity, const AnimationValue& value);
// Trigger callback when animation completes
void triggerCallback();
};
// Easing functions library
@ -134,13 +164,12 @@ public:
// Update all animations
void update(float deltaTime);
// Remove completed animations
void cleanup();
// Clear all animations
void clear();
// Clear all animations (optionally completing them first)
void clear(bool completeAnimations = false);
private:
AnimationManager() = default;
std::vector<std::shared_ptr<Animation>> activeAnimations;
std::vector<std::shared_ptr<Animation>> pendingAnimations; // Animations to add after update
bool isUpdating = false; // Flag to track if we're in update loop
};

View File

@ -5,6 +5,7 @@
#include "UITestScene.h"
#include "Resources.h"
#include "Animation.h"
#include "Timer.h"
#include <cmath>
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
@ -16,7 +17,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
{
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
Resources::game = this;
window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine";
window_title = "McRogueFace Engine";
// Initialize rendering based on headless mode
if (headless) {
@ -91,6 +92,9 @@ void GameEngine::cleanup()
if (cleaned_up) return;
cleaned_up = true;
// Clear all animations first (RAII handles invalidation)
AnimationManager::getInstance().clear();
// Clear Python references before destroying C++ objects
// Clear all timers (they hold Python callables)
timers.clear();
@ -182,7 +186,7 @@ void GameEngine::setWindowScale(float multiplier)
void GameEngine::run()
{
std::cout << "GameEngine::run() starting main loop..." << std::endl;
//std::cout << "GameEngine::run() starting main loop..." << std::endl;
float fps = 0.0;
frameTime = 0.016f; // Initialize to ~60 FPS
clock.restart();
@ -259,7 +263,7 @@ void GameEngine::run()
int tenth_fps = (metrics.fps * 10) % 10;
if (!headless && window) {
window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
window->setTitle(window_title);
}
// In windowed mode, check if window was closed
@ -272,7 +276,7 @@ void GameEngine::run()
cleanup();
}
std::shared_ptr<PyTimerCallable> GameEngine::getTimer(const std::string& name)
std::shared_ptr<Timer> GameEngine::getTimer(const std::string& name)
{
auto it = timers.find(name);
if (it != timers.end()) {
@ -290,7 +294,7 @@ void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
{
// Delete: Overwrite existing timer with one that calls None. This will be deleted in the next timer check
// see gitea issue #4: this allows for a timer to be deleted during its own call to itself
timers[name] = std::make_shared<PyTimerCallable>(Py_None, 1000, runtime.getElapsedTime().asMilliseconds());
timers[name] = std::make_shared<Timer>(Py_None, 1000, runtime.getElapsedTime().asMilliseconds());
return;
}
}
@ -299,7 +303,7 @@ void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
std::cout << "Refusing to initialize timer to None. It's not an error, it's just pointless." << std::endl;
return;
}
timers[name] = std::make_shared<PyTimerCallable>(target, interval, runtime.getElapsedTime().asMilliseconds());
timers[name] = std::make_shared<Timer>(target, interval, runtime.getElapsedTime().asMilliseconds());
}
void GameEngine::testTimers()
@ -310,7 +314,8 @@ void GameEngine::testTimers()
{
it->second->test(now);
if (it->second->isNone())
// Remove timers that have been cancelled or are one-shot and fired
if (!it->second->getCallback() || it->second->getCallback() == Py_None)
{
it = timers.erase(it);
}

View File

@ -58,8 +58,7 @@ private:
public:
sf::Clock runtime;
//std::map<std::string, Timer> timers;
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
std::map<std::string, std::shared_ptr<Timer>> timers;
std::string scene;
// Profiling metrics
@ -116,7 +115,7 @@ public:
float getFrameTime() { return frameTime; }
sf::View getView() { return visible; }
void manageTimer(std::string, PyObject*, int);
std::shared_ptr<PyTimerCallable> getTimer(const std::string& name);
std::shared_ptr<Timer> getTimer(const std::string& name);
void setWindowScale(float);
bool isHeadless() const { return headless; }
void processEvent(const sf::Event& event);

View File

@ -267,6 +267,14 @@ PyObject* PyInit_mcrfpy()
PySceneType.tp_methods = PySceneClass::methods;
PySceneType.tp_getset = PySceneClass::getsetters;
// Set up weakref support for all types that need it
PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist);
PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);
PyUICaptionType.tp_weaklistoffset = offsetof(PyUICaptionObject, weakreflist);
PyUISpriteType.tp_weaklistoffset = offsetof(PyUISpriteObject, weakreflist);
PyUIGridType.tp_weaklistoffset = offsetof(PyUIGridObject, weakreflist);
PyUIEntityType.tp_weaklistoffset = offsetof(PyUIEntityObject, weakreflist);
int i = 0;
auto t = pytypes[i];
while (t != nullptr)

View File

@ -18,19 +18,31 @@ PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds
}
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", nullptr};
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr};
const char* property_name;
PyObject* target_value;
float duration;
const char* easing_name = "linear";
int delta = 0;
PyObject* callback = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|sp", const_cast<char**>(keywords),
&property_name, &target_value, &duration, &easing_name, &delta)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast<char**>(keywords),
&property_name, &target_value, &duration, &easing_name, &delta, &callback)) {
return -1;
}
// Validate callback is callable if provided
if (callback && callback != Py_None && !PyCallable_Check(callback)) {
PyErr_SetString(PyExc_TypeError, "callback must be callable");
return -1;
}
// Convert None to nullptr for C++
if (callback == Py_None) {
callback = nullptr;
}
// Convert Python target value to AnimationValue
AnimationValue animValue;
@ -90,7 +102,7 @@ int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
EasingFunction easingFunc = EasingFunctions::getByName(easing_name);
// Create the Animation
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0);
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
return 0;
}
@ -126,50 +138,50 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
return NULL;
}
// Get the UIDrawable from the Python object
UIDrawable* drawable = nullptr;
// Check type by comparing type names
const char* type_name = Py_TYPE(target_obj)->tp_name;
if (strcmp(type_name, "mcrfpy.Frame") == 0) {
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj;
drawable = frame->data.get();
if (frame->data) {
self->data->start(frame->data);
AnimationManager::getInstance().addAnimation(self->data);
}
}
else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
drawable = caption->data.get();
if (caption->data) {
self->data->start(caption->data);
AnimationManager::getInstance().addAnimation(self->data);
}
}
else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
drawable = sprite->data.get();
if (sprite->data) {
self->data->start(sprite->data);
AnimationManager::getInstance().addAnimation(self->data);
}
}
else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
drawable = grid->data.get();
if (grid->data) {
self->data->start(grid->data);
AnimationManager::getInstance().addAnimation(self->data);
}
}
else if (strcmp(type_name, "mcrfpy.Entity") == 0) {
// Special handling for Entity since it doesn't inherit from UIDrawable
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
// Start the animation directly on the entity
self->data->startEntity(entity->data.get());
// Add to AnimationManager
AnimationManager::getInstance().addAnimation(self->data);
Py_RETURN_NONE;
if (entity->data) {
self->data->startEntity(entity->data);
AnimationManager::getInstance().addAnimation(self->data);
}
}
else {
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity");
return NULL;
}
// Start the animation
self->data->start(drawable);
// Add to AnimationManager
AnimationManager::getInstance().addAnimation(self->data);
Py_RETURN_NONE;
}
@ -214,6 +226,20 @@ PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args
}, value);
}
PyObject* PyAnimation::complete(PyAnimationObject* self, PyObject* args) {
if (self->data) {
self->data->complete();
}
Py_RETURN_NONE;
}
PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args) {
if (self->data && self->data->hasValidTarget()) {
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
}
PyGetSetDef PyAnimation::getsetters[] = {
{"property", (getter)get_property, NULL, "Target property name", NULL},
{"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL},
@ -225,10 +251,23 @@ PyGetSetDef PyAnimation::getsetters[] = {
PyMethodDef PyAnimation::methods[] = {
{"start", (PyCFunction)start, METH_VARARGS,
"Start the animation on a target UIDrawable"},
"start(target) -> None\n\n"
"Start the animation on a target UI element.\n\n"
"Args:\n"
" target: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)\n\n"
"Note:\n"
" The animation will automatically stop if the target is destroyed."},
{"update", (PyCFunction)update, METH_VARARGS,
"Update the animation by deltaTime (returns True if still running)"},
{"get_current_value", (PyCFunction)get_current_value, METH_NOARGS,
"Get the current interpolated value"},
{"complete", (PyCFunction)complete, METH_NOARGS,
"complete() -> None\n\n"
"Complete the animation immediately by jumping to the final value."},
{"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS,
"hasValidTarget() -> bool\n\n"
"Check if the animation still has a valid target.\n\n"
"Returns:\n"
" True if the target still exists, False if it was destroyed."},
{NULL}
};

View File

@ -28,6 +28,8 @@ public:
static PyObject* start(PyAnimationObject* self, PyObject* args);
static PyObject* update(PyAnimationObject* self, PyObject* args);
static PyObject* get_current_value(PyAnimationObject* self, PyObject* args);
static PyObject* complete(PyAnimationObject* self, PyObject* args);
static PyObject* has_valid_target(PyAnimationObject* self, PyObject* args);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];

View File

@ -1,410 +0,0 @@
#pragma once
#include "Python.h"
#include "PyVector.h"
#include "PyColor.h"
#include <SFML/Graphics.hpp>
#include <string>
// Unified argument parsing helpers for Python API consistency
namespace PyArgHelpers {
// Position in pixels (float)
struct PositionResult {
float x, y;
bool valid;
const char* error;
};
// Size in pixels (float)
struct SizeResult {
float w, h;
bool valid;
const char* error;
};
// Grid position in tiles (float - for animation)
struct GridPositionResult {
float grid_x, grid_y;
bool valid;
const char* error;
};
// Grid size in tiles (int - can't have fractional tiles)
struct GridSizeResult {
int grid_w, grid_h;
bool valid;
const char* error;
};
// Color parsing
struct ColorResult {
sf::Color color;
bool valid;
const char* error;
};
// Helper to check if a keyword conflicts with positional args
static bool hasConflict(PyObject* kwds, const char* key, bool has_positional) {
if (!kwds || !has_positional) return false;
PyObject* value = PyDict_GetItemString(kwds, key);
return value != nullptr;
}
// Parse position with conflict detection
static PositionResult parsePosition(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
PositionResult result = {0.0f, 0.0f, false, nullptr};
int start_idx = next_arg ? *next_arg : 0;
bool has_positional = false;
// Check for positional tuple argument first
if (args && PyTuple_Size(args) > start_idx) {
PyObject* first = PyTuple_GetItem(args, start_idx);
// Is it a tuple/Vector?
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
// Extract from tuple
PyObject* x_obj = PyTuple_GetItem(first, 0);
PyObject* y_obj = PyTuple_GetItem(first, 1);
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
result.valid = true;
has_positional = true;
if (next_arg) (*next_arg)++;
}
} else if (PyObject_TypeCheck(first, (PyTypeObject*)PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Vector"))) {
// It's a Vector object
PyVectorObject* vec = (PyVectorObject*)first;
result.x = vec->data.x;
result.y = vec->data.y;
result.valid = true;
has_positional = true;
if (next_arg) (*next_arg)++;
}
}
// Check for keyword conflicts
if (has_positional) {
if (hasConflict(kwds, "pos", true) || hasConflict(kwds, "x", true) || hasConflict(kwds, "y", true)) {
result.valid = false;
result.error = "position specified both positionally and by keyword";
return result;
}
}
// If no positional, try keywords
if (!has_positional && kwds) {
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
// Check for conflicts between pos and x/y
if (pos_obj && (x_obj || y_obj)) {
result.valid = false;
result.error = "pos and x/y cannot both be specified";
return result;
}
if (pos_obj) {
// Parse pos keyword
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
result.x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
result.y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
result.valid = true;
}
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Vector"))) {
PyVectorObject* vec = (PyVectorObject*)pos_obj;
result.x = vec->data.x;
result.y = vec->data.y;
result.valid = true;
}
} else if (x_obj && y_obj) {
// Parse x, y keywords
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
result.valid = true;
}
}
}
return result;
}
// Parse size with conflict detection
static SizeResult parseSize(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
SizeResult result = {0.0f, 0.0f, false, nullptr};
int start_idx = next_arg ? *next_arg : 0;
bool has_positional = false;
// Check for positional tuple argument
if (args && PyTuple_Size(args) > start_idx) {
PyObject* first = PyTuple_GetItem(args, start_idx);
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
PyObject* w_obj = PyTuple_GetItem(first, 0);
PyObject* h_obj = PyTuple_GetItem(first, 1);
if ((PyFloat_Check(w_obj) || PyLong_Check(w_obj)) &&
(PyFloat_Check(h_obj) || PyLong_Check(h_obj))) {
result.w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : PyLong_AsLong(w_obj);
result.h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : PyLong_AsLong(h_obj);
result.valid = true;
has_positional = true;
if (next_arg) (*next_arg)++;
}
}
}
// Check for keyword conflicts
if (has_positional) {
if (hasConflict(kwds, "size", true) || hasConflict(kwds, "w", true) || hasConflict(kwds, "h", true)) {
result.valid = false;
result.error = "size specified both positionally and by keyword";
return result;
}
}
// If no positional, try keywords
if (!has_positional && kwds) {
PyObject* size_obj = PyDict_GetItemString(kwds, "size");
PyObject* w_obj = PyDict_GetItemString(kwds, "w");
PyObject* h_obj = PyDict_GetItemString(kwds, "h");
// Check for conflicts between size and w/h
if (size_obj && (w_obj || h_obj)) {
result.valid = false;
result.error = "size and w/h cannot both be specified";
return result;
}
if (size_obj) {
// Parse size keyword
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
result.w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
result.h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
result.valid = true;
}
}
} else if (w_obj && h_obj) {
// Parse w, h keywords
if ((PyFloat_Check(w_obj) || PyLong_Check(w_obj)) &&
(PyFloat_Check(h_obj) || PyLong_Check(h_obj))) {
result.w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : PyLong_AsLong(w_obj);
result.h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : PyLong_AsLong(h_obj);
result.valid = true;
}
}
}
return result;
}
// Parse grid position (float for smooth animation)
static GridPositionResult parseGridPosition(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
GridPositionResult result = {0.0f, 0.0f, false, nullptr};
int start_idx = next_arg ? *next_arg : 0;
bool has_positional = false;
// Check for positional tuple argument
if (args && PyTuple_Size(args) > start_idx) {
PyObject* first = PyTuple_GetItem(args, start_idx);
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
PyObject* x_obj = PyTuple_GetItem(first, 0);
PyObject* y_obj = PyTuple_GetItem(first, 1);
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
result.grid_x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
result.grid_y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
result.valid = true;
has_positional = true;
if (next_arg) (*next_arg)++;
}
}
}
// Check for keyword conflicts
if (has_positional) {
if (hasConflict(kwds, "grid_pos", true) || hasConflict(kwds, "grid_x", true) || hasConflict(kwds, "grid_y", true)) {
result.valid = false;
result.error = "grid position specified both positionally and by keyword";
return result;
}
}
// If no positional, try keywords
if (!has_positional && kwds) {
PyObject* grid_pos_obj = PyDict_GetItemString(kwds, "grid_pos");
PyObject* grid_x_obj = PyDict_GetItemString(kwds, "grid_x");
PyObject* grid_y_obj = PyDict_GetItemString(kwds, "grid_y");
// Check for conflicts between grid_pos and grid_x/grid_y
if (grid_pos_obj && (grid_x_obj || grid_y_obj)) {
result.valid = false;
result.error = "grid_pos and grid_x/grid_y cannot both be specified";
return result;
}
if (grid_pos_obj) {
// Parse grid_pos keyword
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
result.grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
result.grid_y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
result.valid = true;
}
}
} else if (grid_x_obj && grid_y_obj) {
// Parse grid_x, grid_y keywords
if ((PyFloat_Check(grid_x_obj) || PyLong_Check(grid_x_obj)) &&
(PyFloat_Check(grid_y_obj) || PyLong_Check(grid_y_obj))) {
result.grid_x = PyFloat_Check(grid_x_obj) ? PyFloat_AsDouble(grid_x_obj) : PyLong_AsLong(grid_x_obj);
result.grid_y = PyFloat_Check(grid_y_obj) ? PyFloat_AsDouble(grid_y_obj) : PyLong_AsLong(grid_y_obj);
result.valid = true;
}
}
}
return result;
}
// Parse grid size (int - no fractional tiles)
static GridSizeResult parseGridSize(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
GridSizeResult result = {0, 0, false, nullptr};
int start_idx = next_arg ? *next_arg : 0;
bool has_positional = false;
// Check for positional tuple argument
if (args && PyTuple_Size(args) > start_idx) {
PyObject* first = PyTuple_GetItem(args, start_idx);
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
PyObject* w_obj = PyTuple_GetItem(first, 0);
PyObject* h_obj = PyTuple_GetItem(first, 1);
if (PyLong_Check(w_obj) && PyLong_Check(h_obj)) {
result.grid_w = PyLong_AsLong(w_obj);
result.grid_h = PyLong_AsLong(h_obj);
result.valid = true;
has_positional = true;
if (next_arg) (*next_arg)++;
} else {
result.valid = false;
result.error = "grid size must be specified with integers";
return result;
}
}
}
// Check for keyword conflicts
if (has_positional) {
if (hasConflict(kwds, "grid_size", true) || hasConflict(kwds, "grid_w", true) || hasConflict(kwds, "grid_h", true)) {
result.valid = false;
result.error = "grid size specified both positionally and by keyword";
return result;
}
}
// If no positional, try keywords
if (!has_positional && kwds) {
PyObject* grid_size_obj = PyDict_GetItemString(kwds, "grid_size");
PyObject* grid_w_obj = PyDict_GetItemString(kwds, "grid_w");
PyObject* grid_h_obj = PyDict_GetItemString(kwds, "grid_h");
// Check for conflicts between grid_size and grid_w/grid_h
if (grid_size_obj && (grid_w_obj || grid_h_obj)) {
result.valid = false;
result.error = "grid_size and grid_w/grid_h cannot both be specified";
return result;
}
if (grid_size_obj) {
// Parse grid_size keyword
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
PyObject* w_val = PyTuple_GetItem(grid_size_obj, 0);
PyObject* h_val = PyTuple_GetItem(grid_size_obj, 1);
if (PyLong_Check(w_val) && PyLong_Check(h_val)) {
result.grid_w = PyLong_AsLong(w_val);
result.grid_h = PyLong_AsLong(h_val);
result.valid = true;
} else {
result.valid = false;
result.error = "grid size must be specified with integers";
return result;
}
}
} else if (grid_w_obj && grid_h_obj) {
// Parse grid_w, grid_h keywords
if (PyLong_Check(grid_w_obj) && PyLong_Check(grid_h_obj)) {
result.grid_w = PyLong_AsLong(grid_w_obj);
result.grid_h = PyLong_AsLong(grid_h_obj);
result.valid = true;
} else {
result.valid = false;
result.error = "grid size must be specified with integers";
return result;
}
}
}
return result;
}
// Parse color using existing PyColor infrastructure
static ColorResult parseColor(PyObject* obj, const char* param_name = nullptr) {
ColorResult result = {sf::Color::White, false, nullptr};
if (!obj) {
return result;
}
// Use existing PyColor::from_arg which handles tuple/Color conversion
auto py_color = PyColor::from_arg(obj);
if (py_color) {
result.color = py_color->data;
result.valid = true;
} else {
result.valid = false;
std::string error_msg = param_name
? std::string(param_name) + " must be a color tuple (r,g,b) or (r,g,b,a)"
: "Invalid color format - expected tuple (r,g,b) or (r,g,b,a)";
result.error = error_msg.c_str();
}
return result;
}
// Helper to validate a texture object
static bool isValidTexture(PyObject* obj) {
if (!obj) return false;
PyObject* texture_type = PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Texture");
bool is_texture = PyObject_IsInstance(obj, texture_type);
Py_DECREF(texture_type);
return is_texture;
}
// Helper to validate a click handler
static bool isValidClickHandler(PyObject* obj) {
return obj && PyCallable_Check(obj);
}
}

View File

@ -5,6 +5,21 @@ PyCallable::PyCallable(PyObject* _target)
target = Py_XNewRef(_target);
}
PyCallable::PyCallable(const PyCallable& other)
{
target = Py_XNewRef(other.target);
}
PyCallable& PyCallable::operator=(const PyCallable& other)
{
if (this != &other) {
PyObject* old_target = target;
target = Py_XNewRef(other.target);
Py_XDECREF(old_target);
}
return *this;
}
PyCallable::~PyCallable()
{
if (target)
@ -21,103 +36,6 @@ bool PyCallable::isNone() const
return (target == Py_None || target == NULL);
}
PyTimerCallable::PyTimerCallable(PyObject* _target, int _interval, int now)
: PyCallable(_target), interval(_interval), last_ran(now),
paused(false), pause_start_time(0), total_paused_time(0)
{}
PyTimerCallable::PyTimerCallable()
: PyCallable(Py_None), interval(0), last_ran(0),
paused(false), pause_start_time(0), total_paused_time(0)
{}
bool PyTimerCallable::hasElapsed(int now)
{
if (paused) return false;
return now >= last_ran + interval;
}
void PyTimerCallable::call(int now)
{
PyObject* args = Py_BuildValue("(i)", now);
PyObject* retval = PyCallable::call(args, NULL);
if (!retval)
{
PyErr_Print();
PyErr_Clear();
} else if (retval != Py_None)
{
std::cout << "timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
std::cout << PyUnicode_AsUTF8(PyObject_Repr(retval)) << std::endl;
}
}
bool PyTimerCallable::test(int now)
{
if(hasElapsed(now))
{
call(now);
last_ran = now;
return true;
}
return false;
}
void PyTimerCallable::pause(int current_time)
{
if (!paused) {
paused = true;
pause_start_time = current_time;
}
}
void PyTimerCallable::resume(int current_time)
{
if (paused) {
paused = false;
int paused_duration = current_time - pause_start_time;
total_paused_time += paused_duration;
// Adjust last_ran to account for the pause
last_ran += paused_duration;
}
}
void PyTimerCallable::restart(int current_time)
{
last_ran = current_time;
paused = false;
pause_start_time = 0;
total_paused_time = 0;
}
void PyTimerCallable::cancel()
{
// Cancel by setting target to None
if (target && target != Py_None) {
Py_DECREF(target);
}
target = Py_None;
Py_INCREF(Py_None);
}
int PyTimerCallable::getRemaining(int current_time) const
{
if (paused) {
// When paused, calculate time remaining from when it was paused
int elapsed_when_paused = pause_start_time - last_ran;
return interval - elapsed_when_paused;
}
int elapsed = current_time - last_ran;
return interval - elapsed;
}
void PyTimerCallable::setCallback(PyObject* new_callback)
{
if (target && target != Py_None) {
Py_DECREF(target);
}
target = Py_XNewRef(new_callback);
}
PyClickCallable::PyClickCallable(PyObject* _target)
: PyCallable(_target)

View File

@ -6,45 +6,15 @@ class PyCallable
{
protected:
PyObject* target;
public:
PyCallable(PyObject*);
PyCallable(const PyCallable& other);
PyCallable& operator=(const PyCallable& other);
~PyCallable();
PyObject* call(PyObject*, PyObject*);
public:
bool isNone() const;
};
class PyTimerCallable: public PyCallable
{
private:
int interval;
int last_ran;
void call(int);
// Pause/resume support
bool paused;
int pause_start_time;
int total_paused_time;
public:
bool hasElapsed(int);
bool test(int);
PyTimerCallable(PyObject*, int, int);
PyTimerCallable();
// Timer control methods
void pause(int current_time);
void resume(int current_time);
void restart(int current_time);
void cancel();
// Timer state queries
bool isPaused() const { return paused; }
bool isActive() const { return !isNone() && !paused; }
int getInterval() const { return interval; }
void setInterval(int new_interval) { interval = new_interval; }
int getRemaining(int current_time) const;
PyObject* getCallback() { return target; }
void setCallback(PyObject* new_callback);
PyObject* borrow() const { return target; }
};
class PyClickCallable: public PyCallable
@ -54,6 +24,11 @@ public:
PyObject* borrow();
PyClickCallable(PyObject*);
PyClickCallable();
PyClickCallable(const PyClickCallable& other) : PyCallable(other) {}
PyClickCallable& operator=(const PyClickCallable& other) {
PyCallable::operator=(other);
return *this;
}
};
class PyKeyCallable: public PyCallable

View File

@ -31,13 +31,18 @@ void PyScene::do_mouse_input(std::string button, std::string type)
// Convert window coordinates to game coordinates using the viewport
auto mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos));
// Create a sorted copy by z-index (highest first)
std::vector<std::shared_ptr<UIDrawable>> sorted_elements(*ui_elements);
std::sort(sorted_elements.begin(), sorted_elements.end(),
[](const auto& a, const auto& b) { return a->z_index > b->z_index; });
// Only sort if z_index values have changed
if (ui_elements_need_sort) {
// Sort in ascending order (same as render)
std::sort(ui_elements->begin(), ui_elements->end(),
[](const auto& a, const auto& b) { return a->z_index < b->z_index; });
ui_elements_need_sort = false;
}
// Check elements in z-order (top to bottom)
for (const auto& element : sorted_elements) {
// Check elements in reverse z-order (highest z_index first, top to bottom)
// Use reverse iterators to go from end to beginning
for (auto it = ui_elements->rbegin(); it != ui_elements->rend(); ++it) {
const auto& element = *it;
if (!element->visible) continue;
if (auto target = element->click_at(sf::Vector2f(mousepos))) {

View File

@ -1,7 +1,8 @@
#include "PyTimer.h"
#include "PyCallable.h"
#include "Timer.h"
#include "GameEngine.h"
#include "Resources.h"
#include "PythonObjectCache.h"
#include <sstream>
PyObject* PyTimer::repr(PyObject* self) {
@ -11,7 +12,22 @@ PyObject* PyTimer::repr(PyObject* self) {
if (timer->data) {
oss << "interval=" << timer->data->getInterval() << "ms ";
oss << (timer->data->isPaused() ? "paused" : "active");
if (timer->data->isOnce()) {
oss << "once=True ";
}
if (timer->data->isPaused()) {
oss << "paused";
// Get current time to show remaining
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
oss << " (remaining=" << timer->data->getRemaining(current_time) << "ms)";
} else if (timer->data->isActive()) {
oss << "active";
} else {
oss << "cancelled";
}
} else {
oss << "uninitialized";
}
@ -25,18 +41,20 @@ PyObject* PyTimer::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
if (self) {
new(&self->name) std::string(); // Placement new for std::string
self->data = nullptr;
self->weakreflist = nullptr; // Initialize weakref list
}
return (PyObject*)self;
}
int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"name", "callback", "interval", NULL};
static const char* kwlist[] = {"name", "callback", "interval", "once", NULL};
const char* name = nullptr;
PyObject* callback = nullptr;
int interval = 0;
int once = 0; // Use int for bool parameter
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi", const_cast<char**>(kwlist),
&name, &callback, &interval)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi|p", const_cast<char**>(kwlist),
&name, &callback, &interval, &once)) {
return -1;
}
@ -58,8 +76,18 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
// Create the timer callable
self->data = std::make_shared<PyTimerCallable>(callback, interval, current_time);
// Create the timer
self->data = std::make_shared<Timer>(callback, interval, current_time, (bool)once);
// Register in Python object cache
if (self->data->serial_number == 0) {
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
Py_DECREF(weakref); // Cache owns the reference now
}
}
// Register with game engine
if (Resources::game) {
@ -70,6 +98,11 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
}
void PyTimer::dealloc(PyTimerObject* self) {
// Clear weakrefs first
if (self->weakreflist != nullptr) {
PyObject_ClearWeakRefs((PyObject*)self);
}
// Remove from game engine if still registered
if (Resources::game && !self->name.empty()) {
auto it = Resources::game->timers.find(self->name);
@ -244,7 +277,37 @@ int PyTimer::set_callback(PyTimerObject* self, PyObject* value, void* closure) {
return 0;
}
PyObject* PyTimer::get_once(PyTimerObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
return PyBool_FromLong(self->data->isOnce());
}
int PyTimer::set_once(PyTimerObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return -1;
}
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "once must be a boolean");
return -1;
}
self->data->setOnce(PyObject_IsTrue(value));
return 0;
}
PyObject* PyTimer::get_name(PyTimerObject* self, void* closure) {
return PyUnicode_FromString(self->name.c_str());
}
PyGetSetDef PyTimer::getsetters[] = {
{"name", (getter)PyTimer::get_name, NULL,
"Timer name (read-only)", NULL},
{"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval,
"Timer interval in milliseconds", NULL},
{"remaining", (getter)PyTimer::get_remaining, NULL,
@ -255,17 +318,27 @@ PyGetSetDef PyTimer::getsetters[] = {
"Whether the timer is active and not paused", NULL},
{"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback,
"The callback function to be called", NULL},
{"once", (getter)PyTimer::get_once, (setter)PyTimer::set_once,
"Whether the timer stops after firing once", NULL},
{NULL}
};
PyMethodDef PyTimer::methods[] = {
{"pause", (PyCFunction)PyTimer::pause, METH_NOARGS,
"Pause the timer"},
"pause() -> None\n\n"
"Pause the timer, preserving the time remaining until next trigger.\n"
"The timer can be resumed later with resume()."},
{"resume", (PyCFunction)PyTimer::resume, METH_NOARGS,
"Resume a paused timer"},
"resume() -> None\n\n"
"Resume a paused timer from where it left off.\n"
"Has no effect if the timer is not paused."},
{"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS,
"Cancel the timer and remove it from the system"},
"cancel() -> None\n\n"
"Cancel the timer and remove it from the timer system.\n"
"The timer will no longer fire and cannot be restarted."},
{"restart", (PyCFunction)PyTimer::restart, METH_NOARGS,
"Restart the timer from the current time"},
"restart() -> None\n\n"
"Restart the timer from the beginning.\n"
"Resets the timer to fire after a full interval from now."},
{NULL}
};

View File

@ -4,12 +4,13 @@
#include <memory>
#include <string>
class PyTimerCallable;
class Timer;
typedef struct {
PyObject_HEAD
std::shared_ptr<PyTimerCallable> data;
std::shared_ptr<Timer> data;
std::string name;
PyObject* weakreflist; // Weak reference support
} PyTimerObject;
class PyTimer
@ -28,6 +29,7 @@ public:
static PyObject* restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
// Timer property getters
static PyObject* get_name(PyTimerObject* self, void* closure);
static PyObject* get_interval(PyTimerObject* self, void* closure);
static int set_interval(PyTimerObject* self, PyObject* value, void* closure);
static PyObject* get_remaining(PyTimerObject* self, void* closure);
@ -35,6 +37,8 @@ public:
static PyObject* get_active(PyTimerObject* self, void* closure);
static PyObject* get_callback(PyTimerObject* self, void* closure);
static int set_callback(PyTimerObject* self, PyObject* value, void* closure);
static PyObject* get_once(PyTimerObject* self, void* closure);
static int set_once(PyTimerObject* self, PyObject* value, void* closure);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
@ -49,7 +53,35 @@ namespace mcrfpydef {
.tp_dealloc = (destructor)PyTimer::dealloc,
.tp_repr = PyTimer::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Timer object for scheduled callbacks"),
.tp_doc = PyDoc_STR("Timer(name, callback, interval, once=False)\n\n"
"Create a timer that calls a function at regular intervals.\n\n"
"Args:\n"
" name (str): Unique identifier for the timer\n"
" callback (callable): Function to call - receives (timer, runtime) args\n"
" interval (int): Time between calls in milliseconds\n"
" once (bool): If True, timer stops after first call. Default: False\n\n"
"Attributes:\n"
" interval (int): Time between calls in milliseconds\n"
" remaining (int): Time until next call in milliseconds (read-only)\n"
" paused (bool): Whether timer is paused (read-only)\n"
" active (bool): Whether timer is active and not paused (read-only)\n"
" callback (callable): The callback function\n"
" once (bool): Whether timer stops after firing once\n\n"
"Methods:\n"
" pause(): Pause the timer, preserving time remaining\n"
" resume(): Resume a paused timer\n"
" cancel(): Stop and remove the timer\n"
" restart(): Reset timer to start from beginning\n\n"
"Example:\n"
" def on_timer(timer, runtime):\n"
" print(f'Timer {timer} fired at {runtime}ms')\n"
" if runtime > 5000:\n"
" timer.cancel()\n"
" \n"
" timer = mcrfpy.Timer('my_timer', on_timer, 1000)\n"
" timer.pause() # Pause timer\n"
" timer.resume() # Resume timer\n"
" timer.once = True # Make it one-shot"),
.tp_methods = PyTimer::methods,
.tp_getset = PyTimer::getsetters,
.tp_init = (initproc)PyTimer::init,

85
src/PythonObjectCache.cpp Normal file
View File

@ -0,0 +1,85 @@
#include "PythonObjectCache.h"
#include <iostream>
PythonObjectCache* PythonObjectCache::instance = nullptr;
PythonObjectCache& PythonObjectCache::getInstance() {
if (!instance) {
instance = new PythonObjectCache();
}
return *instance;
}
PythonObjectCache::~PythonObjectCache() {
clear();
}
uint64_t PythonObjectCache::assignSerial() {
return next_serial.fetch_add(1, std::memory_order_relaxed);
}
void PythonObjectCache::registerObject(uint64_t serial, PyObject* weakref) {
if (!weakref || serial == 0) return;
std::lock_guard<std::mutex> lock(serial_mutex);
// Clean up any existing entry
auto it = cache.find(serial);
if (it != cache.end()) {
Py_DECREF(it->second);
}
// Store the new weak reference
Py_INCREF(weakref);
cache[serial] = weakref;
}
PyObject* PythonObjectCache::lookup(uint64_t serial) {
if (serial == 0) return nullptr;
// No mutex needed for read - GIL protects PyWeakref_GetObject
auto it = cache.find(serial);
if (it != cache.end()) {
PyObject* obj = PyWeakref_GetObject(it->second);
if (obj && obj != Py_None) {
Py_INCREF(obj);
return obj;
}
}
return nullptr;
}
void PythonObjectCache::remove(uint64_t serial) {
if (serial == 0) return;
std::lock_guard<std::mutex> lock(serial_mutex);
auto it = cache.find(serial);
if (it != cache.end()) {
Py_DECREF(it->second);
cache.erase(it);
}
}
void PythonObjectCache::cleanup() {
std::lock_guard<std::mutex> lock(serial_mutex);
auto it = cache.begin();
while (it != cache.end()) {
PyObject* obj = PyWeakref_GetObject(it->second);
if (!obj || obj == Py_None) {
Py_DECREF(it->second);
it = cache.erase(it);
} else {
++it;
}
}
}
void PythonObjectCache::clear() {
std::lock_guard<std::mutex> lock(serial_mutex);
for (auto& pair : cache) {
Py_DECREF(pair.second);
}
cache.clear();
}

40
src/PythonObjectCache.h Normal file
View File

@ -0,0 +1,40 @@
#pragma once
#include <Python.h>
#include <unordered_map>
#include <mutex>
#include <atomic>
#include <cstdint>
class PythonObjectCache {
private:
static PythonObjectCache* instance;
std::mutex serial_mutex;
std::atomic<uint64_t> next_serial{1};
std::unordered_map<uint64_t, PyObject*> cache;
PythonObjectCache() = default;
~PythonObjectCache();
public:
static PythonObjectCache& getInstance();
// Assign a new serial number
uint64_t assignSerial();
// Register a Python object with a serial number
void registerObject(uint64_t serial, PyObject* weakref);
// Lookup a Python object by serial number
// Returns new reference or nullptr
PyObject* lookup(uint64_t serial);
// Remove an entry from the cache
void remove(uint64_t serial);
// Clean up dead weak references
void cleanup();
// Clear entire cache (for module cleanup)
void clear();
};

View File

@ -1,31 +1,140 @@
#include "Timer.h"
#include "PythonObjectCache.h"
#include "PyCallable.h"
Timer::Timer(PyObject* _target, int _interval, int now)
: target(_target), interval(_interval), last_ran(now)
Timer::Timer(PyObject* _target, int _interval, int now, bool _once)
: callback(std::make_shared<PyCallable>(_target)), interval(_interval), last_ran(now),
paused(false), pause_start_time(0), total_paused_time(0), once(_once)
{}
Timer::Timer()
: target(Py_None), interval(0), last_ran(0)
: callback(std::make_shared<PyCallable>(Py_None)), interval(0), last_ran(0),
paused(false), pause_start_time(0), total_paused_time(0), once(false)
{}
Timer::~Timer() {
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
}
bool Timer::hasElapsed(int now) const
{
if (paused) return false;
return now >= last_ran + interval;
}
bool Timer::test(int now)
{
if (!target || target == Py_None) return false;
if (now > last_ran + interval)
if (!callback || callback->isNone()) return false;
if (hasElapsed(now))
{
last_ran = now;
PyObject* args = Py_BuildValue("(i)", now);
PyObject* retval = PyObject_Call(target, args, NULL);
// Get the PyTimer wrapper from cache to pass to callback
PyObject* timer_obj = nullptr;
if (serial_number != 0) {
timer_obj = PythonObjectCache::getInstance().lookup(serial_number);
}
// Build args: (timer, runtime) or just (runtime) if no wrapper found
PyObject* args;
if (timer_obj) {
args = Py_BuildValue("(Oi)", timer_obj, now);
} else {
// Fallback to old behavior if no wrapper found
args = Py_BuildValue("(i)", now);
}
PyObject* retval = callback->call(args, NULL);
Py_DECREF(args);
if (!retval)
{
std::cout << "timer has raised an exception. It's going to STDERR and being dropped:" << std::endl;
std::cout << "Timer callback has raised an exception. It's going to STDERR and being dropped:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else if (retval != Py_None)
{
std::cout << "timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
std::cout << "Timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
Py_DECREF(retval);
}
// Handle one-shot timers
if (once) {
cancel();
}
return true;
}
return false;
}
void Timer::pause(int current_time)
{
if (!paused) {
paused = true;
pause_start_time = current_time;
}
}
void Timer::resume(int current_time)
{
if (paused) {
paused = false;
int paused_duration = current_time - pause_start_time;
total_paused_time += paused_duration;
// Adjust last_ran to account for the pause
last_ran += paused_duration;
}
}
void Timer::restart(int current_time)
{
last_ran = current_time;
paused = false;
pause_start_time = 0;
total_paused_time = 0;
}
void Timer::cancel()
{
// Cancel by setting callback to None
callback = std::make_shared<PyCallable>(Py_None);
}
bool Timer::isActive() const
{
return callback && !callback->isNone() && !paused;
}
int Timer::getRemaining(int current_time) const
{
if (paused) {
// When paused, calculate time remaining from when it was paused
int elapsed_when_paused = pause_start_time - last_ran;
return interval - elapsed_when_paused;
}
int elapsed = current_time - last_ran;
return interval - elapsed;
}
int Timer::getElapsed(int current_time) const
{
if (paused) {
return pause_start_time - last_ran;
}
return current_time - last_ran;
}
PyObject* Timer::getCallback()
{
if (!callback) return Py_None;
return callback->borrow();
}
void Timer::setCallback(PyObject* new_callback)
{
callback = std::make_shared<PyCallable>(new_callback);
}

View File

@ -1,15 +1,54 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <memory>
class PyCallable;
class GameEngine; // forward declare
class Timer
{
public:
PyObject* target;
private:
std::shared_ptr<PyCallable> callback;
int interval;
int last_ran;
// Pause/resume support
bool paused;
int pause_start_time;
int total_paused_time;
// One-shot timer support
bool once;
public:
uint64_t serial_number = 0; // For Python object cache
Timer(); // for map to build
Timer(PyObject*, int, int);
bool test(int);
Timer(PyObject* target, int interval, int now, bool once = false);
~Timer();
// Core timer functionality
bool test(int now);
bool hasElapsed(int now) const;
// Timer control methods
void pause(int current_time);
void resume(int current_time);
void restart(int current_time);
void cancel();
// Timer state queries
bool isPaused() const { return paused; }
bool isActive() const;
int getInterval() const { return interval; }
void setInterval(int new_interval) { interval = new_interval; }
int getRemaining(int current_time) const;
int getElapsed(int current_time) const;
bool isOnce() const { return once; }
void setOnce(bool value) { once = value; }
// Callback management
PyObject* getCallback();
void setCallback(PyObject* new_callback);
};

View File

@ -6,12 +6,14 @@ class UIEntity;
typedef struct {
PyObject_HEAD
std::shared_ptr<UIEntity> data;
PyObject* weakreflist; // Weak reference support
} PyUIEntityObject;
class UIFrame;
typedef struct {
PyObject_HEAD
std::shared_ptr<UIFrame> data;
PyObject* weakreflist; // Weak reference support
} PyUIFrameObject;
class UICaption;
@ -19,18 +21,21 @@ typedef struct {
PyObject_HEAD
std::shared_ptr<UICaption> data;
PyObject* font;
PyObject* weakreflist; // Weak reference support
} PyUICaptionObject;
class UIGrid;
typedef struct {
PyObject_HEAD
std::shared_ptr<UIGrid> data;
PyObject* weakreflist; // Weak reference support
} PyUIGridObject;
class UISprite;
typedef struct {
PyObject_HEAD
std::shared_ptr<UISprite> data;
PyObject* weakreflist; // Weak reference support
} PyUISpriteObject;
// Common Python method implementations for UIDrawable-derived classes

View File

@ -3,7 +3,7 @@
#include "PyColor.h"
#include "PyVector.h"
#include "PyFont.h"
#include "PyArgHelpers.h"
#include "PythonObjectCache.h"
// UIDrawable methods now in UIBase.h
#include <algorithm>
@ -303,183 +303,135 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
{
using namespace mcrfpydef;
// Try parsing with PyArgHelpers
int arg_idx = 0;
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
// Default values
float x = 0.0f, y = 0.0f, outline = 0.0f;
char* text = nullptr;
// Define all parameters with defaults
PyObject* pos_obj = nullptr;
PyObject* font = nullptr;
const char* text = "";
PyObject* fill_color = nullptr;
PyObject* outline_color = nullptr;
float outline = 0.0f;
float font_size = 16.0f;
PyObject* click_handler = nullptr;
int visible = 1;
float opacity = 1.0f;
int z_index = 0;
const char* name = nullptr;
float x = 0.0f, y = 0.0f;
// Case 1: Got position from helpers (tuple format)
if (pos_result.valid) {
x = pos_result.x;
y = pos_result.y;
// Parse remaining arguments
static const char* remaining_keywords[] = {
"text", "font", "fill_color", "outline_color", "outline", "click", nullptr
};
// Create new tuple with remaining args
Py_ssize_t total_args = PyTuple_Size(args);
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|zOOOfO",
const_cast<char**>(remaining_keywords),
&text, &font, &fill_color, &outline_color,
&outline, &click_handler)) {
Py_DECREF(remaining_args);
if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error);
// Keywords list matches the new spec: positional args first, then all keyword args
static const char* kwlist[] = {
"pos", "font", "text", // Positional args (as per spec)
// Keyword-only args
"fill_color", "outline_color", "outline", "font_size", "click",
"visible", "opacity", "z_index", "name", "x", "y",
nullptr
};
// Parse arguments with | for optional positional args
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizff", const_cast<char**>(kwlist),
&pos_obj, &font, &text, // Positional
&fill_color, &outline_color, &outline, &font_size, &click_handler,
&visible, &opacity, &z_index, &name, &x, &y)) {
return -1;
}
// Handle position argument (can be tuple, Vector, or use x/y keywords)
if (pos_obj) {
PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (vec) {
x = vec->data.x;
y = vec->data.y;
Py_DECREF(vec);
} else {
PyErr_Clear();
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
} else {
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
return -1;
}
} else {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
return -1;
}
}
}
// Handle font argument
std::shared_ptr<PyFont> pyfont = nullptr;
if (font && font != Py_None) {
if (!PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font"))) {
PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance");
return -1;
}
Py_DECREF(remaining_args);
}
// Case 2: Traditional format
else {
PyErr_Clear(); // Clear any errors from helpers
// First check if this is the old (text, x, y, ...) format
PyObject* first_arg = args && PyTuple_Size(args) > 0 ? PyTuple_GetItem(args, 0) : nullptr;
bool text_first = first_arg && PyUnicode_Check(first_arg);
if (text_first) {
// Pattern: (text, x, y, ...)
static const char* text_first_keywords[] = {
"text", "x", "y", "font", "fill_color", "outline_color",
"outline", "click", "pos", nullptr
};
PyObject* pos_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zffOOOfOO",
const_cast<char**>(text_first_keywords),
&text, &x, &y, &font, &fill_color, &outline_color,
&outline, &click_handler, &pos_obj)) {
return -1;
}
// Handle pos keyword override
if (pos_obj && pos_obj != Py_None) {
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
}
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
PyImport_ImportModule("mcrfpy"), "Vector"))) {
PyVectorObject* vec = (PyVectorObject*)pos_obj;
x = vec->data.x;
y = vec->data.y;
} else {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
return -1;
}
}
} else {
// Pattern: (x, y, text, ...)
static const char* xy_keywords[] = {
"x", "y", "text", "font", "fill_color", "outline_color",
"outline", "click", "pos", nullptr
};
PyObject* pos_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOfOO",
const_cast<char**>(xy_keywords),
&x, &y, &text, &font, &fill_color, &outline_color,
&outline, &click_handler, &pos_obj)) {
return -1;
}
// Handle pos keyword override
if (pos_obj && pos_obj != Py_None) {
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
}
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
PyImport_ImportModule("mcrfpy"), "Vector"))) {
PyVectorObject* vec = (PyVectorObject*)pos_obj;
x = vec->data.x;
y = vec->data.y;
} else {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
return -1;
}
}
}
auto obj = (PyFontObject*)font;
pyfont = obj->data;
}
self->data->position = sf::Vector2f(x, y); // Set base class position
self->data->text.setPosition(self->data->position); // Sync text position
// check types for font, fill_color, outline_color
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl;
if (font != NULL && font != Py_None && !PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font")/*(PyObject*)&PyFontType)*/)){
PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance or None");
return -1;
} else if (font != NULL && font != Py_None)
{
auto font_obj = (PyFontObject*)font;
self->data->text.setFont(font_obj->data->font);
self->font = font;
Py_INCREF(font);
} else
{
// Create the caption
self->data = std::make_shared<UICaption>();
self->data->position = sf::Vector2f(x, y);
self->data->text.setPosition(self->data->position);
self->data->text.setOutlineThickness(outline);
// Set the font
if (pyfont) {
self->data->text.setFont(pyfont->font);
} else {
// Use default font when None or not provided
if (McRFPy_API::default_font) {
self->data->text.setFont(McRFPy_API::default_font->font);
// Store reference to default font
PyObject* default_font_obj = PyObject_GetAttrString(McRFPy_API::mcrf_module, "default_font");
if (default_font_obj) {
self->font = default_font_obj;
// Don't need to DECREF since we're storing it
}
}
}
// Handle text - default to empty string if not provided
if (text && text != NULL) {
self->data->text.setString((std::string)text);
} else {
self->data->text.setString("");
// Set character size
self->data->text.setCharacterSize(static_cast<unsigned int>(font_size));
// Set text
if (text && strlen(text) > 0) {
self->data->text.setString(std::string(text));
}
self->data->text.setOutlineThickness(outline);
if (fill_color) {
auto fc = PyColor::from_arg(fill_color);
if (!fc) {
PyErr_SetString(PyExc_TypeError, "fill_color must be mcrfpy.Color or arguments to mcrfpy.Color.__init__");
// Handle fill_color
if (fill_color && fill_color != Py_None) {
PyColorObject* color_obj = PyColor::from_arg(fill_color);
if (!color_obj) {
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple");
return -1;
}
self->data->text.setFillColor(PyColor::fromPy(fc));
//Py_DECREF(fc);
self->data->text.setFillColor(color_obj->data);
Py_DECREF(color_obj);
} else {
self->data->text.setFillColor(sf::Color(0,0,0,255));
self->data->text.setFillColor(sf::Color(255, 255, 255, 255)); // Default: white
}
if (outline_color) {
auto oc = PyColor::from_arg(outline_color);
if (!oc) {
PyErr_SetString(PyExc_TypeError, "outline_color must be mcrfpy.Color or arguments to mcrfpy.Color.__init__");
// Handle outline_color
if (outline_color && outline_color != Py_None) {
PyColorObject* color_obj = PyColor::from_arg(outline_color);
if (!color_obj) {
PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or color tuple");
return -1;
}
self->data->text.setOutlineColor(PyColor::fromPy(oc));
//Py_DECREF(oc);
self->data->text.setOutlineColor(color_obj->data);
Py_DECREF(color_obj);
} else {
self->data->text.setOutlineColor(sf::Color(128,128,128,255));
self->data->text.setOutlineColor(sf::Color(0, 0, 0, 255)); // Default: black
}
// Process click handler if provided
// Set other properties
self->data->visible = visible;
self->data->opacity = opacity;
self->data->z_index = z_index;
if (name) {
self->data->name = std::string(name);
}
// Handle click handler
if (click_handler && click_handler != Py_None) {
if (!PyCallable_Check(click_handler)) {
PyErr_SetString(PyExc_TypeError, "click must be callable");
@ -487,10 +439,24 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
}
self->data->click_register(click_handler);
}
// Initialize weak reference list
self->weakreflist = NULL;
// Register in Python object cache
if (self->data->serial_number == 0) {
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
Py_DECREF(weakref); // Cache owns the reference now
}
}
return 0;
}
// Property system implementation for animations
bool UICaption::setProperty(const std::string& name, float value) {
if (name == "x") {

View File

@ -54,6 +54,10 @@ namespace mcrfpydef {
.tp_dealloc = (destructor)[](PyObject* self)
{
PyUICaptionObject* obj = (PyUICaptionObject*)self;
// Clear weak references
if (obj->weakreflist != NULL) {
PyObject_ClearWeakRefs(self);
}
// TODO - reevaluate with PyFont usage; UICaption does not own the font
// release reference to font object
if (obj->font) Py_DECREF(obj->font);
@ -64,27 +68,38 @@ namespace mcrfpydef {
//.tp_hash = NULL,
//.tp_iter
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)\n\n"
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = PyDoc_STR("Caption(pos=None, font=None, text='', **kwargs)\n\n"
"A text display UI element with customizable font and styling.\n\n"
"Args:\n"
" text (str): The text content to display. Default: ''\n"
" x (float): X position in pixels. Default: 0\n"
" y (float): Y position in pixels. Default: 0\n"
" font (Font): Font object for text rendering. Default: engine default font\n"
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
" font (Font, optional): Font object for text rendering. Default: engine default font\n"
" text (str, optional): The text content to display. Default: ''\n\n"
"Keyword Args:\n"
" fill_color (Color): Text fill color. Default: (255, 255, 255, 255)\n"
" outline_color (Color): Text outline color. Default: (0, 0, 0, 255)\n"
" outline (float): Text outline thickness. Default: 0\n"
" click (callable): Click event handler. Default: None\n\n"
" font_size (float): Font size in points. Default: 16\n"
" click (callable): Click event handler. Default: None\n"
" visible (bool): Visibility state. Default: True\n"
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
" z_index (int): Rendering order. Default: 0\n"
" name (str): Element name for finding. Default: None\n"
" x (float): X position override. Default: 0\n"
" y (float): Y position override. Default: 0\n\n"
"Attributes:\n"
" text (str): The displayed text content\n"
" x, y (float): Position in pixels\n"
" pos (Vector): Position as a Vector object\n"
" font (Font): Font used for rendering\n"
" font_size (float): Font size in points\n"
" fill_color, outline_color (Color): Text appearance\n"
" outline (float): Outline thickness\n"
" click (callable): Click event handler\n"
" visible (bool): Visibility state\n"
" opacity (float): Opacity value\n"
" z_index (int): Rendering order\n"
" name (str): Element name\n"
" w, h (float): Read-only computed size based on text and font"),
.tp_methods = UICaption_methods,
//.tp_members = PyUIFrame_members,
@ -95,7 +110,11 @@ namespace mcrfpydef {
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{
PyUICaptionObject* self = (PyUICaptionObject*)type->tp_alloc(type, 0);
if (self) self->data = std::make_shared<UICaption>();
if (self) {
self->data = std::make_shared<UICaption>();
self->font = nullptr;
self->weakreflist = nullptr;
}
return (PyObject*)self;
}
};

View File

@ -6,6 +6,7 @@
#include "UIGrid.h"
#include "McRFPy_API.h"
#include "PyObjectUtils.h"
#include "PythonObjectCache.h"
#include <climits>
#include <algorithm>
@ -17,6 +18,14 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
Py_RETURN_NONE;
}
// Check cache first
if (drawable->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(drawable->serial_number);
if (cached) {
return cached; // Already INCREF'd by lookup
}
}
PyTypeObject* type = nullptr;
PyObject* obj = nullptr;
@ -28,6 +37,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
auto pyObj = (PyUIFrameObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UIFrame>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
@ -40,6 +50,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
if (pyObj) {
pyObj->data = std::static_pointer_cast<UICaption>(drawable);
pyObj->font = nullptr;
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
@ -51,6 +62,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
auto pyObj = (PyUISpriteObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UISprite>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;
@ -62,6 +74,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
auto pyObj = (PyUIGridObject*)type->tp_alloc(type, 0);
if (pyObj) {
pyObj->data = std::static_pointer_cast<UIGrid>(drawable);
pyObj->weakreflist = NULL;
}
obj = (PyObject*)pyObj;
break;

View File

@ -5,9 +5,113 @@
#include "UIGrid.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include "PythonObjectCache.h"
UIDrawable::UIDrawable() : position(0.0f, 0.0f) { click_callable = NULL; }
UIDrawable::UIDrawable(const UIDrawable& other)
: z_index(other.z_index),
name(other.name),
position(other.position),
visible(other.visible),
opacity(other.opacity),
serial_number(0), // Don't copy serial number
use_render_texture(other.use_render_texture),
render_dirty(true) // Force redraw after copy
{
// Deep copy click_callable if it exists
if (other.click_callable) {
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
}
// Deep copy render texture if needed
if (other.render_texture && other.use_render_texture) {
auto size = other.render_texture->getSize();
enableRenderTexture(size.x, size.y);
}
}
UIDrawable& UIDrawable::operator=(const UIDrawable& other) {
if (this != &other) {
// Copy basic members
z_index = other.z_index;
name = other.name;
position = other.position;
visible = other.visible;
opacity = other.opacity;
use_render_texture = other.use_render_texture;
render_dirty = true; // Force redraw after copy
// Deep copy click_callable
if (other.click_callable) {
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
} else {
click_callable.reset();
}
// Deep copy render texture if needed
if (other.render_texture && other.use_render_texture) {
auto size = other.render_texture->getSize();
enableRenderTexture(size.x, size.y);
} else {
render_texture.reset();
use_render_texture = false;
}
}
return *this;
}
UIDrawable::UIDrawable(UIDrawable&& other) noexcept
: z_index(other.z_index),
name(std::move(other.name)),
position(other.position),
visible(other.visible),
opacity(other.opacity),
serial_number(other.serial_number),
click_callable(std::move(other.click_callable)),
render_texture(std::move(other.render_texture)),
render_sprite(std::move(other.render_sprite)),
use_render_texture(other.use_render_texture),
render_dirty(other.render_dirty)
{
// Clear the moved-from object's serial number to avoid cache issues
other.serial_number = 0;
}
UIDrawable& UIDrawable::operator=(UIDrawable&& other) noexcept {
if (this != &other) {
// Clear our own cache entry if we have one
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
// Move basic members
z_index = other.z_index;
name = std::move(other.name);
position = other.position;
visible = other.visible;
opacity = other.opacity;
serial_number = other.serial_number;
use_render_texture = other.use_render_texture;
render_dirty = other.render_dirty;
// Move unique_ptr members
click_callable = std::move(other.click_callable);
render_texture = std::move(other.render_texture);
render_sprite = std::move(other.render_sprite);
// Clear the moved-from object's serial number
other.serial_number = 0;
}
return *this;
}
UIDrawable::~UIDrawable() {
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
}
void UIDrawable::click_unregister()
{
click_callable.reset();

View File

@ -39,6 +39,15 @@ public:
void click_unregister();
UIDrawable();
virtual ~UIDrawable();
// Copy constructor and assignment operator
UIDrawable(const UIDrawable& other);
UIDrawable& operator=(const UIDrawable& other);
// Move constructor and assignment operator
UIDrawable(UIDrawable&& other) noexcept;
UIDrawable& operator=(UIDrawable&& other) noexcept;
static PyObject* get_click(PyObject* self, void* closure);
static int set_click(PyObject* self, PyObject* value, void* closure);
@ -90,6 +99,9 @@ public:
virtual bool getProperty(const std::string& name, sf::Vector2f& value) const { return false; }
virtual bool getProperty(const std::string& name, std::string& value) const { return false; }
// Python object cache support
uint64_t serial_number = 0;
protected:
// RenderTexture support (opt-in)
std::unique_ptr<sf::RenderTexture> render_texture;

View File

@ -4,7 +4,7 @@
#include <algorithm>
#include "PyObjectUtils.h"
#include "PyVector.h"
#include "PyArgHelpers.h"
#include "PythonObjectCache.h"
// UIDrawable methods now in UIBase.h
#include "UIEntityPyMethods.h"
@ -17,6 +17,12 @@ UIEntity::UIEntity()
// gridstate vector starts empty - will be lazily initialized when needed
}
UIEntity::~UIEntity() {
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
}
// Removed UIEntity(UIGrid&) constructor - using lazy initialization instead
void UIEntity::updateVisibility()
@ -121,81 +127,57 @@ PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
}
int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
// Try parsing with PyArgHelpers for grid position
int arg_idx = 0;
auto grid_pos_result = PyArgHelpers::parseGridPosition(args, kwds, &arg_idx);
// Default values
float grid_x = 0.0f, grid_y = 0.0f;
int sprite_index = 0;
// Define all parameters with defaults
PyObject* grid_pos_obj = nullptr;
PyObject* texture = nullptr;
int sprite_index = 0;
PyObject* grid_obj = nullptr;
int visible = 1;
float opacity = 1.0f;
const char* name = nullptr;
float x = 0.0f, y = 0.0f;
// Case 1: Got grid position from helpers (tuple format)
if (grid_pos_result.valid) {
grid_x = grid_pos_result.grid_x;
grid_y = grid_pos_result.grid_y;
// Parse remaining arguments
static const char* remaining_keywords[] = {
"texture", "sprite_index", "grid", nullptr
};
// Create new tuple with remaining args
Py_ssize_t total_args = PyTuple_Size(args);
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OiO",
const_cast<char**>(remaining_keywords),
&texture, &sprite_index, &grid_obj)) {
Py_DECREF(remaining_args);
if (grid_pos_result.error) PyErr_SetString(PyExc_TypeError, grid_pos_result.error);
return -1;
}
Py_DECREF(remaining_args);
// Keywords list matches the new spec: positional args first, then all keyword args
static const char* kwlist[] = {
"grid_pos", "texture", "sprite_index", // Positional args (as per spec)
// Keyword-only args
"grid", "visible", "opacity", "name", "x", "y",
nullptr
};
// Parse arguments with | for optional positional args
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOiOifzff", const_cast<char**>(kwlist),
&grid_pos_obj, &texture, &sprite_index, // Positional
&grid_obj, &visible, &opacity, &name, &x, &y)) {
return -1;
}
// Case 2: Traditional format
else {
PyErr_Clear(); // Clear any errors from helpers
static const char* keywords[] = {
"grid_x", "grid_y", "texture", "sprite_index", "grid", "grid_pos", nullptr
};
PyObject* grid_pos_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOiOO",
const_cast<char**>(keywords),
&grid_x, &grid_y, &texture, &sprite_index,
&grid_obj, &grid_pos_obj)) {
return -1;
}
// Handle grid_pos keyword override
if (grid_pos_obj && grid_pos_obj != Py_None) {
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
grid_y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
}
// Handle grid position argument (can be tuple or use x/y keywords)
if (grid_pos_obj) {
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
} else {
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)");
PyErr_SetString(PyExc_TypeError, "grid_pos tuple must contain numbers");
return -1;
}
} else {
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)");
return -1;
}
}
// check types for texture
//
// Set Texture - allow None or use default
//
// Handle texture argument
std::shared_ptr<PyTexture> texture_ptr = nullptr;
if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
return -1;
} else if (texture != NULL && texture != Py_None) {
if (texture && texture != Py_None) {
if (!PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
return -1;
}
auto pytexture = (PyTextureObject*)texture;
texture_ptr = pytexture->data;
} else {
@ -203,25 +185,33 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
texture_ptr = McRFPy_API::default_texture;
}
// Allow creation without texture for testing purposes
// if (!texture_ptr) {
// PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
// return -1;
// }
if (grid_obj != NULL && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
// Handle grid argument
if (grid_obj && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
return -1;
}
// Always use default constructor for lazy initialization
// Create the entity
self->data = std::make_shared<UIEntity>();
// Initialize weak reference list
self->weakreflist = NULL;
// Store reference to Python object
// Register in Python object cache
if (self->data->serial_number == 0) {
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
Py_DECREF(weakref); // Cache owns the reference now
}
}
// Store reference to Python object (legacy - to be removed)
self->data->self = (PyObject*)self;
Py_INCREF(self);
// TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers
// Set texture and sprite index
if (texture_ptr) {
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
} else {
@ -230,12 +220,20 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
}
// Set position using grid coordinates
self->data->position = sf::Vector2f(grid_x, grid_y);
self->data->position = sf::Vector2f(x, y);
if (grid_obj != NULL) {
// Set other properties (delegate to sprite)
self->data->sprite.visible = visible;
self->data->sprite.opacity = opacity;
if (name) {
self->data->sprite.name = std::string(name);
}
// Handle grid attachment
if (grid_obj) {
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj;
self->data->grid = pygrid->data;
// todone - on creation of Entity with Grid assignment, also append it to the entity list
// Append entity to grid's entity list
pygrid->data->entities->push_back(self->data);
// Don't initialize gridstate here - lazy initialization to support large numbers of entities

View File

@ -14,12 +14,37 @@
#include "PyFont.h"
#include "UIGridPoint.h"
#include "UIDrawable.h"
#include "UIBase.h"
#include "UISprite.h"
class UIGrid;
// UIEntity
/*
****************************************
* say it with me: *
* UIEntity is not a UIDrawable *
****************************************
**Why Not, John?**
Doesn't it say "UI" on the front of it?
It sure does. Probably should have called it "GridEntity", but it's a bit late now.
UIDrawables have positions in **screen pixel coordinates**. Their position is an offset from their parent's position, and they are deeply nestable (Scene -> Frame -> Frame -> ...)
However:
UIEntity has a position in **Grid tile coordinates**.
UIEntity is not nestable at all. Grid -> Entity.
UIEntity has a strict one/none relationship with a Grid: if you add it to another grid, it will have itself removed from the losing grid's collection.
UIEntity originally only allowed a single-tile sprite, but around mid-July 2025, I'm working to change that to allow any UIDrawable to go there, or multi-tile sprites.
UIEntity is, at its core, the container for *a perspective of map data*.
The Grid should contain the true, complete contents of the game's space, and the Entity should use pathfinding, field of view, and game logic to interact with the Grid's layer data.
In Conclusion, because UIEntity cannot be drawn on a Frame or Scene, and has the unique role of serving as a filter of the data contained in a Grid, UIEntity will not become a UIDrawable.
*/
//class UIEntity;
//typedef struct {
// PyObject_HEAD
@ -32,11 +57,11 @@ sf::Vector2f PyObject_to_sfVector2f(PyObject* obj);
PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state);
PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec);
// TODO: make UIEntity a drawable
class UIEntity//: public UIDrawable
class UIEntity
{
public:
PyObject* self = nullptr; // Reference to the Python object (if created from Python)
uint64_t serial_number = 0; // For Python object cache
std::shared_ptr<UIGrid> grid;
std::vector<UIGridPointState> gridstate;
UISprite sprite;
@ -44,6 +69,7 @@ public:
//void render(sf::Vector2f); //override final;
UIEntity();
~UIEntity();
// Visibility methods
void updateVisibility(); // Update gridstate from current FOV
@ -88,10 +114,31 @@ namespace mcrfpydef {
.tp_itemsize = 0,
.tp_repr = (reprfunc)UIEntity::repr,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = "UIEntity objects",
.tp_doc = PyDoc_STR("Entity(grid_pos=None, texture=None, sprite_index=0, **kwargs)\n\n"
"A game entity that exists on a grid with sprite rendering.\n\n"
"Args:\n"
" grid_pos (tuple, optional): Grid position as (x, y) tuple. Default: (0, 0)\n"
" texture (Texture, optional): Texture object for sprite. Default: default texture\n"
" sprite_index (int, optional): Index into texture atlas. Default: 0\n\n"
"Keyword Args:\n"
" grid (Grid): Grid to attach entity to. Default: None\n"
" visible (bool): Visibility state. Default: True\n"
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
" name (str): Element name for finding. Default: None\n"
" x (float): X grid position override. Default: 0\n"
" y (float): Y grid position override. Default: 0\n\n"
"Attributes:\n"
" pos (tuple): Grid position as (x, y) tuple\n"
" x, y (float): Grid position coordinates\n"
" draw_pos (tuple): Pixel position for rendering\n"
" gridstate (GridPointState): Visibility state for grid points\n"
" sprite_index (int): Current sprite index\n"
" visible (bool): Visibility state\n"
" opacity (float): Opacity value\n"
" name (str): Element name"),
.tp_methods = UIEntity_all_methods,
.tp_getset = UIEntity::getsetters,
.tp_base = &mcrfpydef::PyDrawableType,
.tp_base = NULL,
.tp_init = (initproc)UIEntity::init,
.tp_new = PyType_GenericNew,
};

View File

@ -6,7 +6,7 @@
#include "UISprite.h"
#include "UIGrid.h"
#include "McRFPy_API.h"
#include "PyArgHelpers.h"
#include "PythonObjectCache.h"
// UIDrawable methods now in UIBase.h
UIDrawable* UIFrame::click_at(sf::Vector2f point)
@ -432,67 +432,50 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
// Initialize children first
self->data->children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
// Try parsing with PyArgHelpers
int arg_idx = 0;
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx);
// Initialize weak reference list
self->weakreflist = NULL;
// Default values
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f, outline = 0.0f;
// Define all parameters with defaults
PyObject* pos_obj = nullptr;
PyObject* size_obj = nullptr;
PyObject* fill_color = nullptr;
PyObject* outline_color = nullptr;
float outline = 0.0f;
PyObject* children_arg = nullptr;
PyObject* click_handler = nullptr;
int visible = 1;
float opacity = 1.0f;
int z_index = 0;
const char* name = nullptr;
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
int clip_children = 0;
// Case 1: Got position and size from helpers (tuple format)
if (pos_result.valid && size_result.valid) {
x = pos_result.x;
y = pos_result.y;
w = size_result.w;
h = size_result.h;
// Parse remaining arguments
static const char* remaining_keywords[] = {
"fill_color", "outline_color", "outline", "children", "click", nullptr
};
// Create new tuple with remaining args
Py_ssize_t total_args = PyTuple_Size(args);
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OOfOO",
const_cast<char**>(remaining_keywords),
&fill_color, &outline_color, &outline,
&children_arg, &click_handler)) {
Py_DECREF(remaining_args);
if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error);
else if (size_result.error) PyErr_SetString(PyExc_TypeError, size_result.error);
return -1;
}
Py_DECREF(remaining_args);
// Keywords list matches the new spec: positional args first, then all keyword args
static const char* kwlist[] = {
"pos", "size", // Positional args (as per spec)
// Keyword-only args
"fill_color", "outline_color", "outline", "children", "click",
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children",
nullptr
};
// Parse arguments with | for optional positional args
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffi", const_cast<char**>(kwlist),
&pos_obj, &size_obj, // Positional
&fill_color, &outline_color, &outline, &children_arg, &click_handler,
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children)) {
return -1;
}
// Case 2: Traditional format (x, y, w, h, ...)
else {
PyErr_Clear(); // Clear any errors from helpers
static const char* keywords[] = {
"x", "y", "w", "h", "fill_color", "outline_color", "outline",
"children", "click", "pos", "size", nullptr
};
PyObject* pos_obj = nullptr;
PyObject* size_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffffOOfOOOO",
const_cast<char**>(keywords),
&x, &y, &w, &h, &fill_color, &outline_color,
&outline, &children_arg, &click_handler,
&pos_obj, &size_obj)) {
return -1;
}
// Handle pos keyword override
if (pos_obj && pos_obj != Py_None) {
// Handle position argument (can be tuple, Vector, or use x/y keywords)
if (pos_obj) {
PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (vec) {
x = vec->data.x;
y = vec->data.y;
Py_DECREF(vec);
} else {
PyErr_Clear();
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
@ -500,47 +483,87 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
} else {
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
return -1;
}
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
PyImport_ImportModule("mcrfpy"), "Vector"))) {
PyVectorObject* vec = (PyVectorObject*)pos_obj;
x = vec->data.x;
y = vec->data.y;
} else {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
return -1;
}
}
// Handle size keyword override
if (size_obj && size_obj != Py_None) {
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
}
}
// If no pos_obj but x/y keywords were provided, they're already in x, y variables
// Handle size argument (can be tuple or use w/h keywords)
if (size_obj) {
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
} else {
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers");
return -1;
}
} else {
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
return -1;
}
}
// If no size_obj but w/h keywords were provided, they're already in w, h variables
self->data->position = sf::Vector2f(x, y); // Set base class position
self->data->box.setPosition(self->data->position); // Sync box position
// Set the position and size
self->data->position = sf::Vector2f(x, y);
self->data->box.setPosition(self->data->position);
self->data->box.setSize(sf::Vector2f(w, h));
self->data->box.setOutlineThickness(outline);
// getsetter abuse because I haven't standardized Color object parsing (TODO)
int err_val = 0;
if (fill_color && fill_color != Py_None) err_val = UIFrame::set_color_member(self, fill_color, (void*)0);
else self->data->box.setFillColor(sf::Color(0,0,0,255));
if (err_val) return err_val;
if (outline_color && outline_color != Py_None) err_val = UIFrame::set_color_member(self, outline_color, (void*)1);
else self->data->box.setOutlineColor(sf::Color(128,128,128,255));
if (err_val) return err_val;
// Handle fill_color
if (fill_color && fill_color != Py_None) {
PyColorObject* color_obj = PyColor::from_arg(fill_color);
if (!color_obj) {
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple");
return -1;
}
self->data->box.setFillColor(color_obj->data);
Py_DECREF(color_obj);
} else {
self->data->box.setFillColor(sf::Color(0, 0, 0, 128)); // Default: semi-transparent black
}
// Handle outline_color
if (outline_color && outline_color != Py_None) {
PyColorObject* color_obj = PyColor::from_arg(outline_color);
if (!color_obj) {
PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or color tuple");
return -1;
}
self->data->box.setOutlineColor(color_obj->data);
Py_DECREF(color_obj);
} else {
self->data->box.setOutlineColor(sf::Color(255, 255, 255, 255)); // Default: white
}
// Set other properties
self->data->visible = visible;
self->data->opacity = opacity;
self->data->z_index = z_index;
self->data->clip_children = clip_children;
if (name) {
self->data->name = std::string(name);
}
// Handle click handler
if (click_handler && click_handler != Py_None) {
if (!PyCallable_Check(click_handler)) {
PyErr_SetString(PyExc_TypeError, "click must be callable");
return -1;
}
self->data->click_register(click_handler);
}
// Process children argument if provided
if (children_arg && children_arg != Py_None) {
@ -605,6 +628,16 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
self->data->click_register(click_handler);
}
// Register in Python object cache
if (self->data->serial_number == 0) {
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
Py_DECREF(weakref); // Cache owns the reference now
}
}
return 0;
}

View File

@ -78,6 +78,10 @@ namespace mcrfpydef {
.tp_dealloc = (destructor)[](PyObject* self)
{
PyUIFrameObject* obj = (PyUIFrameObject*)self;
// Clear weak references
if (obj->weakreflist != NULL) {
PyObject_ClearWeakRefs(self);
}
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
@ -85,28 +89,39 @@ namespace mcrfpydef {
//.tp_hash = NULL,
//.tp_iter
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)\n\n"
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = PyDoc_STR("Frame(pos=None, size=None, **kwargs)\n\n"
"A rectangular frame UI element that can contain other drawable elements.\n\n"
"Args:\n"
" x (float): X position in pixels. Default: 0\n"
" y (float): Y position in pixels. Default: 0\n"
" w (float): Width in pixels. Default: 0\n"
" h (float): Height in pixels. Default: 0\n"
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
" size (tuple, optional): Size as (width, height) tuple. Default: (0, 0)\n\n"
"Keyword Args:\n"
" fill_color (Color): Background fill color. Default: (0, 0, 0, 128)\n"
" outline_color (Color): Border outline color. Default: (255, 255, 255, 255)\n"
" outline (float): Border outline thickness. Default: 0\n"
" click (callable): Click event handler. Default: None\n"
" children (list): Initial list of child drawable elements. Default: None\n\n"
" children (list): Initial list of child drawable elements. Default: None\n"
" visible (bool): Visibility state. Default: True\n"
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
" z_index (int): Rendering order. Default: 0\n"
" name (str): Element name for finding. Default: None\n"
" x (float): X position override. Default: 0\n"
" y (float): Y position override. Default: 0\n"
" w (float): Width override. Default: 0\n"
" h (float): Height override. Default: 0\n"
" clip_children (bool): Whether to clip children to frame bounds. Default: False\n\n"
"Attributes:\n"
" x, y (float): Position in pixels\n"
" w, h (float): Size in pixels\n"
" pos (Vector): Position as a Vector object\n"
" fill_color, outline_color (Color): Visual appearance\n"
" outline (float): Border thickness\n"
" click (callable): Click event handler\n"
" children (list): Collection of child drawable elements\n"
" visible (bool): Visibility state\n"
" opacity (float): Opacity value\n"
" z_index (int): Rendering order\n"
" name (str): Element name\n"
" clip_children (bool): Whether to clip children to frame bounds"),
.tp_methods = UIFrame_methods,
//.tp_members = PyUIFrame_members,
@ -116,7 +131,10 @@ namespace mcrfpydef {
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{
PyUIFrameObject* self = (PyUIFrameObject*)type->tp_alloc(type, 0);
if (self) self->data = std::make_shared<UIFrame>();
if (self) {
self->data = std::make_shared<UIFrame>();
self->weakreflist = nullptr;
}
return (PyObject*)self;
}
};

View File

@ -1,7 +1,7 @@
#include "UIGrid.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include "PyArgHelpers.h"
#include "PythonObjectCache.h"
#include <algorithm>
// UIDrawable methods now in UIBase.h
@ -518,102 +518,49 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
// Default values
int grid_x = 0, grid_y = 0;
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
// Define all parameters with defaults
PyObject* pos_obj = nullptr;
PyObject* size_obj = nullptr;
PyObject* grid_size_obj = nullptr;
PyObject* textureObj = nullptr;
PyObject* fill_color = nullptr;
PyObject* click_handler = nullptr;
float center_x = 0.0f, center_y = 0.0f;
float zoom = 1.0f;
int perspective = -1; // perspective is a difficult __init__ arg; needs an entity in collection to work
int visible = 1;
float opacity = 1.0f;
int z_index = 0;
const char* name = nullptr;
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
int grid_x = 2, grid_y = 2; // Default to 2x2 grid
// Check if first argument is a tuple (for tuple-based initialization)
bool has_tuple_first_arg = false;
if (args && PyTuple_Size(args) > 0) {
PyObject* first_arg = PyTuple_GetItem(args, 0);
if (PyTuple_Check(first_arg)) {
has_tuple_first_arg = true;
}
// Keywords list matches the new spec: positional args first, then all keyword args
static const char* kwlist[] = {
"pos", "size", "grid_size", "texture", // Positional args (as per spec)
// Keyword-only args
"fill_color", "click", "center_x", "center_y", "zoom", "perspective",
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "grid_x", "grid_y",
nullptr
};
// Parse arguments with | for optional positional args
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffiifizffffii", const_cast<char**>(kwlist),
&pos_obj, &size_obj, &grid_size_obj, &textureObj, // Positional
&fill_color, &click_handler, &center_x, &center_y, &zoom, &perspective,
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_x, &grid_y)) {
return -1;
}
// Try tuple-based parsing if we have a tuple as first argument
if (has_tuple_first_arg) {
int arg_idx = 0;
auto grid_size_result = PyArgHelpers::parseGridSize(args, kwds, &arg_idx);
// If grid size parsing failed with an error, report it
if (!grid_size_result.valid) {
if (grid_size_result.error) {
PyErr_SetString(PyExc_TypeError, grid_size_result.error);
} else {
PyErr_SetString(PyExc_TypeError, "Invalid grid size tuple");
}
return -1;
}
// We got a valid grid size
grid_x = grid_size_result.grid_w;
grid_y = grid_size_result.grid_h;
// Try to parse position and size
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
if (pos_result.valid) {
x = pos_result.x;
y = pos_result.y;
}
auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx);
if (size_result.valid) {
w = size_result.w;
h = size_result.h;
// Handle position argument (can be tuple, Vector, or use x/y keywords)
if (pos_obj) {
PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (vec) {
x = vec->data.x;
y = vec->data.y;
Py_DECREF(vec);
} else {
// Default size based on grid dimensions
w = grid_x * 16.0f;
h = grid_y * 16.0f;
}
// Parse remaining arguments (texture)
static const char* remaining_keywords[] = { "texture", nullptr };
Py_ssize_t total_args = PyTuple_Size(args);
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|O",
const_cast<char**>(remaining_keywords),
&textureObj);
Py_DECREF(remaining_args);
}
// Traditional format parsing
else {
static const char* keywords[] = {
"grid_x", "grid_y", "texture", "pos", "size", "grid_size", nullptr
};
PyObject* pos_obj = nullptr;
PyObject* size_obj = nullptr;
PyObject* grid_size_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiOOOO",
const_cast<char**>(keywords),
&grid_x, &grid_y, &textureObj,
&pos_obj, &size_obj, &grid_size_obj)) {
return -1;
}
// Handle grid_size override
if (grid_size_obj && grid_size_obj != Py_None) {
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
PyObject* x_obj = PyTuple_GetItem(grid_size_obj, 0);
PyObject* y_obj = PyTuple_GetItem(grid_size_obj, 1);
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
grid_x = PyLong_AsLong(x_obj);
grid_y = PyLong_AsLong(y_obj);
} else {
PyErr_SetString(PyExc_TypeError, "grid_size must contain integers");
return -1;
}
} else {
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple of two integers");
return -1;
}
}
// Handle position
if (pos_obj && pos_obj != Py_None) {
PyErr_Clear();
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
@ -622,36 +569,50 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
} else {
PyErr_SetString(PyExc_TypeError, "pos must contain numbers");
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
return -1;
}
} else {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of two numbers");
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
return -1;
}
}
// Handle size
if (size_obj && size_obj != Py_None) {
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
} else {
PyErr_SetString(PyExc_TypeError, "size must contain numbers");
return -1;
}
}
// Handle size argument (can be tuple or use w/h keywords)
if (size_obj) {
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
} else {
PyErr_SetString(PyExc_TypeError, "size must be a tuple of two numbers");
PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers");
return -1;
}
} else {
// Default size based on grid
w = grid_x * 16.0f;
h = grid_y * 16.0f;
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
return -1;
}
}
// Handle grid_size argument (can be tuple or use grid_x/grid_y keywords)
if (grid_size_obj) {
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
PyObject* gx_val = PyTuple_GetItem(grid_size_obj, 0);
PyObject* gy_val = PyTuple_GetItem(grid_size_obj, 1);
if (PyLong_Check(gx_val) && PyLong_Check(gy_val)) {
grid_x = PyLong_AsLong(gx_val);
grid_y = PyLong_AsLong(gy_val);
} else {
PyErr_SetString(PyExc_TypeError, "grid_size tuple must contain integers");
return -1;
}
} else {
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple (grid_x, grid_y)");
return -1;
}
}
@ -661,12 +622,8 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
return -1;
}
// At this point we have x, y, w, h values from either parsing method
// Convert PyObject texture to shared_ptr<PyTexture>
// Handle texture argument
std::shared_ptr<PyTexture> texture_ptr = nullptr;
// Allow None or NULL for texture - use default texture in that case
if (textureObj && textureObj != Py_None) {
if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
@ -679,14 +636,64 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
texture_ptr = McRFPy_API::default_texture;
}
// Adjust size based on texture if available and size not explicitly set
if (texture_ptr && w == grid_x * 16.0f && h == grid_y * 16.0f) {
// If size wasn't specified, calculate based on grid dimensions and texture
if (!size_obj && texture_ptr) {
w = grid_x * texture_ptr->sprite_width;
h = grid_y * texture_ptr->sprite_height;
} else if (!size_obj) {
w = grid_x * 16.0f; // Default tile size
h = grid_y * 16.0f;
}
// Create the grid
self->data = std::make_shared<UIGrid>(grid_x, grid_y, texture_ptr,
sf::Vector2f(x, y), sf::Vector2f(w, h));
// Set additional properties
self->data->center_x = center_x;
self->data->center_y = center_y;
self->data->zoom = zoom;
self->data->perspective = perspective;
self->data->visible = visible;
self->data->opacity = opacity;
self->data->z_index = z_index;
if (name) {
self->data->name = std::string(name);
}
// Handle fill_color
if (fill_color && fill_color != Py_None) {
PyColorObject* color_obj = PyColor::from_arg(fill_color);
if (!color_obj) {
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple");
return -1;
}
self->data->box.setFillColor(color_obj->data);
Py_DECREF(color_obj);
}
// Handle click handler
if (click_handler && click_handler != Py_None) {
if (!PyCallable_Check(click_handler)) {
PyErr_SetString(PyExc_TypeError, "click must be callable");
return -1;
}
self->data->click_register(click_handler);
}
// Initialize weak reference list
self->weakreflist = NULL;
// Register in Python object cache
if (self->data->serial_number == 0) {
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
Py_DECREF(weakref); // Cache owns the reference now
}
}
return 0; // Success
}
@ -1401,7 +1408,15 @@ PyObject* UIEntityCollection::getitem(PyUIEntityCollectionObject* self, Py_ssize
std::advance(l_begin, index);
auto target = *l_begin; //auto target = (*vec)[index];
// If the entity has a stored Python object reference, return that to preserve derived class
// Check cache first to preserve derived class
if (target->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(target->serial_number);
if (cached) {
return cached; // Already INCREF'd by lookup
}
}
// Legacy: If the entity has a stored Python object reference, return that to preserve derived class
if (target->self != nullptr) {
Py_INCREF(target->self);
return target->self;

View File

@ -172,41 +172,65 @@ namespace mcrfpydef {
.tp_name = "mcrfpy.Grid",
.tp_basicsize = sizeof(PyUIGridObject),
.tp_itemsize = 0,
//.tp_dealloc = (destructor)[](PyObject* self)
//{
// PyUIGridObject* obj = (PyUIGridObject*)self;
// obj->data.reset();
// Py_TYPE(self)->tp_free(self);
//},
.tp_dealloc = (destructor)[](PyObject* self)
{
PyUIGridObject* obj = (PyUIGridObject*)self;
// Clear weak references
if (obj->weakreflist != NULL) {
PyObject_ClearWeakRefs(self);
}
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
//TODO - PyUIGrid REPR def:
.tp_repr = (reprfunc)UIGrid::repr,
//.tp_hash = NULL,
//.tp_iter
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)\n\n"
"A grid-based tilemap UI element for rendering tile-based levels and game worlds.\n\n"
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n"
"A grid-based UI element for tile-based rendering and entity management.\n\n"
"Args:\n"
" x (float): X position in pixels. Default: 0\n"
" y (float): Y position in pixels. Default: 0\n"
" grid_size (tuple): Grid dimensions as (width, height) in tiles. Default: (20, 20)\n"
" texture (Texture): Texture atlas containing tile sprites. Default: None\n"
" tile_width (int): Width of each tile in pixels. Default: 16\n"
" tile_height (int): Height of each tile in pixels. Default: 16\n"
" scale (float): Grid scaling factor. Default: 1.0\n"
" click (callable): Click event handler. Default: None\n\n"
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
" size (tuple, optional): Size as (width, height) tuple. Default: auto-calculated from grid_size\n"
" grid_size (tuple, optional): Grid dimensions as (grid_x, grid_y) tuple. Default: (2, 2)\n"
" texture (Texture, optional): Texture containing tile sprites. Default: default texture\n\n"
"Keyword Args:\n"
" fill_color (Color): Background fill color. Default: None\n"
" click (callable): Click event handler. Default: None\n"
" center_x (float): X coordinate of center point. Default: 0\n"
" center_y (float): Y coordinate of center point. Default: 0\n"
" zoom (float): Zoom level for rendering. Default: 1.0\n"
" perspective (int): Entity perspective index (-1 for omniscient). Default: -1\n"
" visible (bool): Visibility state. Default: True\n"
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
" z_index (int): Rendering order. Default: 0\n"
" name (str): Element name for finding. Default: None\n"
" x (float): X position override. Default: 0\n"
" y (float): Y position override. Default: 0\n"
" w (float): Width override. Default: auto-calculated\n"
" h (float): Height override. Default: auto-calculated\n"
" grid_x (int): Grid width override. Default: 2\n"
" grid_y (int): Grid height override. Default: 2\n\n"
"Attributes:\n"
" x, y (float): Position in pixels\n"
" w, h (float): Size in pixels\n"
" pos (Vector): Position as a Vector object\n"
" size (tuple): Size as (width, height) tuple\n"
" center (tuple): Center point as (x, y) tuple\n"
" center_x, center_y (float): Center point coordinates\n"
" zoom (float): Zoom level for rendering\n"
" grid_size (tuple): Grid dimensions (width, height) in tiles\n"
" tile_width, tile_height (int): Tile dimensions in pixels\n"
" grid_x, grid_y (int): Grid dimensions\n"
" texture (Texture): Tile texture atlas\n"
" scale (float): Scale multiplier\n"
" points (list): 2D array of GridPoint objects for tile data\n"
" entities (list): Collection of Entity objects in the grid\n"
" background_color (Color): Grid background color\n"
" fill_color (Color): Background color\n"
" entities (EntityCollection): Collection of entities in the grid\n"
" perspective (int): Entity perspective index\n"
" click (callable): Click event handler\n"
" visible (bool): Visibility state\n"
" z_index (int): Rendering order"),
" opacity (float): Opacity value\n"
" z_index (int): Rendering order\n"
" name (str): Element name"),
.tp_methods = UIGrid_all_methods,
//.tp_members = UIGrid::members,
.tp_getset = UIGrid::getsetters,

View File

@ -1,7 +1,7 @@
#include "UISprite.h"
#include "GameEngine.h"
#include "PyVector.h"
#include "PyArgHelpers.h"
#include "PythonObjectCache.h"
// UIDrawable methods now in UIBase.h
UIDrawable* UISprite::click_at(sf::Vector2f point)
@ -29,6 +29,42 @@ UISprite::UISprite(std::shared_ptr<PyTexture> _ptex, int _sprite_index, sf::Vect
sprite = ptex->sprite(sprite_index, position, sf::Vector2f(_scale, _scale));
}
UISprite::UISprite(const UISprite& other)
: UIDrawable(other),
sprite_index(other.sprite_index),
sprite(other.sprite),
ptex(other.ptex)
{
}
UISprite& UISprite::operator=(const UISprite& other) {
if (this != &other) {
UIDrawable::operator=(other);
sprite_index = other.sprite_index;
sprite = other.sprite;
ptex = other.ptex;
}
return *this;
}
UISprite::UISprite(UISprite&& other) noexcept
: UIDrawable(std::move(other)),
sprite_index(other.sprite_index),
sprite(std::move(other.sprite)),
ptex(std::move(other.ptex))
{
}
UISprite& UISprite::operator=(UISprite&& other) noexcept {
if (this != &other) {
UIDrawable::operator=(std::move(other));
sprite_index = other.sprite_index;
sprite = std::move(other.sprite);
ptex = std::move(other.ptex);
}
return *this;
}
/*
void UISprite::render(sf::Vector2f offset)
{
@ -327,57 +363,46 @@ PyObject* UISprite::repr(PyUISpriteObject* self)
int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
{
// Try parsing with PyArgHelpers
int arg_idx = 0;
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
// Default values
float x = 0.0f, y = 0.0f, scale = 1.0f;
int sprite_index = 0;
// Define all parameters with defaults
PyObject* pos_obj = nullptr;
PyObject* texture = nullptr;
int sprite_index = 0;
float scale = 1.0f;
float scale_x = 1.0f;
float scale_y = 1.0f;
PyObject* click_handler = nullptr;
int visible = 1;
float opacity = 1.0f;
int z_index = 0;
const char* name = nullptr;
float x = 0.0f, y = 0.0f;
// Case 1: Got position from helpers (tuple format)
if (pos_result.valid) {
x = pos_result.x;
y = pos_result.y;
// Parse remaining arguments
static const char* remaining_keywords[] = {
"texture", "sprite_index", "scale", "click", nullptr
};
// Create new tuple with remaining args
Py_ssize_t total_args = PyTuple_Size(args);
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OifO",
const_cast<char**>(remaining_keywords),
&texture, &sprite_index, &scale, &click_handler)) {
Py_DECREF(remaining_args);
if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error);
return -1;
}
Py_DECREF(remaining_args);
// Keywords list matches the new spec: positional args first, then all keyword args
static const char* kwlist[] = {
"pos", "texture", "sprite_index", // Positional args (as per spec)
// Keyword-only args
"scale", "scale_x", "scale_y", "click",
"visible", "opacity", "z_index", "name", "x", "y",
nullptr
};
// Parse arguments with | for optional positional args
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizff", const_cast<char**>(kwlist),
&pos_obj, &texture, &sprite_index, // Positional
&scale, &scale_x, &scale_y, &click_handler,
&visible, &opacity, &z_index, &name, &x, &y)) {
return -1;
}
// Case 2: Traditional format
else {
PyErr_Clear(); // Clear any errors from helpers
static const char* keywords[] = {
"x", "y", "texture", "sprite_index", "scale", "click", "pos", nullptr
};
PyObject* pos_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOifOO",
const_cast<char**>(keywords),
&x, &y, &texture, &sprite_index, &scale,
&click_handler, &pos_obj)) {
return -1;
}
// Handle pos keyword override
if (pos_obj && pos_obj != Py_None) {
// Handle position argument (can be tuple, Vector, or use x/y keywords)
if (pos_obj) {
PyVectorObject* vec = PyVector::from_arg(pos_obj);
if (vec) {
x = vec->data.x;
y = vec->data.y;
Py_DECREF(vec);
} else {
PyErr_Clear();
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
@ -385,12 +410,10 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
} else {
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
return -1;
}
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
PyImport_ImportModule("mcrfpy"), "Vector"))) {
PyVectorObject* vec = (PyVectorObject*)pos_obj;
x = vec->data.x;
y = vec->data.y;
} else {
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
return -1;
@ -400,10 +423,11 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
// Handle texture - allow None or use default
std::shared_ptr<PyTexture> texture_ptr = nullptr;
if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
return -1;
} else if (texture != NULL && texture != Py_None) {
if (texture && texture != Py_None) {
if (!PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
return -1;
}
auto pytexture = (PyTextureObject*)texture;
texture_ptr = pytexture->data;
} else {
@ -416,9 +440,27 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
return -1;
}
// Create the sprite
self->data = std::make_shared<UISprite>(texture_ptr, sprite_index, sf::Vector2f(x, y), scale);
// Set scale properties
if (scale_x != 1.0f || scale_y != 1.0f) {
// If scale_x or scale_y were explicitly set, use them
self->data->setScale(sf::Vector2f(scale_x, scale_y));
} else if (scale != 1.0f) {
// Otherwise use uniform scale
self->data->setScale(sf::Vector2f(scale, scale));
}
// Set other properties
self->data->visible = visible;
self->data->opacity = opacity;
self->data->z_index = z_index;
if (name) {
self->data->name = std::string(name);
}
// Process click handler if provided
// Handle click handler
if (click_handler && click_handler != Py_None) {
if (!PyCallable_Check(click_handler)) {
PyErr_SetString(PyExc_TypeError, "click must be callable");
@ -427,6 +469,19 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
self->data->click_register(click_handler);
}
// Initialize weak reference list
self->weakreflist = NULL;
// Register in Python object cache
if (self->data->serial_number == 0) {
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
Py_DECREF(weakref); // Cache owns the reference now
}
}
return 0;
}

View File

@ -25,6 +25,14 @@ protected:
public:
UISprite();
UISprite(std::shared_ptr<PyTexture>, int, sf::Vector2f, float);
// Copy constructor and assignment operator
UISprite(const UISprite& other);
UISprite& operator=(const UISprite& other);
// Move constructor and assignment operator
UISprite(UISprite&& other) noexcept;
UISprite& operator=(UISprite&& other) noexcept;
void update();
void render(sf::Vector2f, sf::RenderTarget&) override final;
virtual UIDrawable* click_at(sf::Vector2f point) override final;
@ -82,6 +90,10 @@ namespace mcrfpydef {
.tp_dealloc = (destructor)[](PyObject* self)
{
PyUISpriteObject* obj = (PyUISpriteObject*)self;
// Clear weak references
if (obj->weakreflist != NULL) {
PyObject_ClearWeakRefs(self);
}
// release reference to font object
//if (obj->texture) Py_DECREF(obj->texture);
obj->data.reset();
@ -91,24 +103,36 @@ namespace mcrfpydef {
//.tp_hash = NULL,
//.tp_iter
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)\n\n"
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = PyDoc_STR("Sprite(pos=None, texture=None, sprite_index=0, **kwargs)\n\n"
"A sprite UI element that displays a texture or portion of a texture atlas.\n\n"
"Args:\n"
" x (float): X position in pixels. Default: 0\n"
" y (float): Y position in pixels. Default: 0\n"
" texture (Texture): Texture object to display. Default: None\n"
" sprite_index (int): Index into texture atlas (if applicable). Default: 0\n"
" scale (float): Sprite scaling factor. Default: 1.0\n"
" click (callable): Click event handler. Default: None\n\n"
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
" texture (Texture, optional): Texture object to display. Default: default texture\n"
" sprite_index (int, optional): Index into texture atlas. Default: 0\n\n"
"Keyword Args:\n"
" scale (float): Uniform scale factor. Default: 1.0\n"
" scale_x (float): Horizontal scale factor. Default: 1.0\n"
" scale_y (float): Vertical scale factor. Default: 1.0\n"
" click (callable): Click event handler. Default: None\n"
" visible (bool): Visibility state. Default: True\n"
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
" z_index (int): Rendering order. Default: 0\n"
" name (str): Element name for finding. Default: None\n"
" x (float): X position override. Default: 0\n"
" y (float): Y position override. Default: 0\n\n"
"Attributes:\n"
" x, y (float): Position in pixels\n"
" pos (Vector): Position as a Vector object\n"
" texture (Texture): The texture being displayed\n"
" sprite_index (int): Current sprite index in texture atlas\n"
" scale (float): Scale multiplier\n"
" scale (float): Uniform scale factor\n"
" scale_x, scale_y (float): Individual scale factors\n"
" click (callable): Click event handler\n"
" visible (bool): Visibility state\n"
" opacity (float): Opacity value\n"
" z_index (int): Rendering order\n"
" name (str): Element name\n"
" w, h (float): Read-only computed size based on texture and scale"),
.tp_methods = UISprite_methods,
//.tp_members = PyUIFrame_members,
@ -118,7 +142,10 @@ namespace mcrfpydef {
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{
PyUISpriteObject* self = (PyUISpriteObject*)type->tp_alloc(type, 0);
//if (self) self->data = std::make_shared<UICaption>();
if (self) {
self->data = std::make_shared<UISprite>();
self->weakreflist = nullptr;
}
return (PyObject*)self;
}
};

View File

@ -67,10 +67,10 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
self.draw_pos = (tx, ty)
for e in self.game.entities:
if e is self: continue
if e.draw_pos == old_pos: e.ev_exit(self)
if e.draw_pos.x == old_pos.x and e.draw_pos.y == old_pos.y: e.ev_exit(self)
for e in self.game.entities:
if e is self: continue
if e.draw_pos == (tx, ty): e.ev_enter(self)
if e.draw_pos.x == tx and e.draw_pos.y == ty: e.ev_enter(self)
def act(self):
pass
@ -83,12 +83,12 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
def try_move(self, dx, dy, test=False):
x_max, y_max = self.grid.grid_size
tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy)
tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy)
#for e in iterable_entities(self.grid):
# sorting entities to test against the boulder instead of the button when they overlap.
for e in sorted(self.game.entities, key = lambda i: i.draw_order, reverse = True):
if e.draw_pos == (tx, ty):
if e.draw_pos.x == tx and e.draw_pos.y == ty:
#print(f"bumping {e}")
return e.bump(self, dx, dy)
@ -106,7 +106,7 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
return False
def _relative_move(self, dx, dy):
tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy)
tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy)
#self.draw_pos = (tx, ty)
self.do_move(tx, ty)
@ -181,7 +181,7 @@ class Equippable:
if self.zap_cooldown_remaining != 0:
print("zap is cooling down.")
return False
fx, fy = caster.draw_pos
fx, fy = caster.draw_pos.x, caster.draw_pos.y
x, y = int(fx), int (fy)
dist = lambda tx, ty: abs(int(tx) - x) + abs(int(ty) - y)
targets = []
@ -293,7 +293,7 @@ class PlayerEntity(COSEntity):
## TODO - find other entities to avoid spawning on top of
for spawn in spawn_points:
for e in avoid or []:
if e.draw_pos == spawn: break
if e.draw_pos.x == spawn[0] and e.draw_pos.y == spawn[1]: break
else:
break
self.draw_pos = spawn
@ -314,9 +314,9 @@ class BoulderEntity(COSEntity):
elif type(other) == EnemyEntity:
if not other.can_push: return False
#tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy)
tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy)
tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy)
# Is the boulder blocked the same direction as the bumper? If not, let's both move
old_pos = int(self.draw_pos[0]), int(self.draw_pos[1])
old_pos = int(self.draw_pos.x), int(self.draw_pos.y)
if self.try_move(dx, dy, test=test):
if not test:
other.do_move(*old_pos)
@ -342,7 +342,7 @@ class ButtonEntity(COSEntity):
# self.exit.unlock()
# TODO: unlock, and then lock again, when player steps on/off
if not test:
pos = int(self.draw_pos[0]), int(self.draw_pos[1])
pos = int(self.draw_pos.x), int(self.draw_pos.y)
other.do_move(*pos)
return True
@ -393,7 +393,7 @@ class EnemyEntity(COSEntity):
def bump(self, other, dx, dy, test=False):
if self.hp == 0:
if not test:
old_pos = int(self.draw_pos[0]), int(self.draw_pos[1])
old_pos = int(self.draw_pos.x), int(self.draw_pos.y)
other.do_move(*old_pos)
return True
if type(other) == PlayerEntity:
@ -415,7 +415,7 @@ class EnemyEntity(COSEntity):
print("Ouch, my entire body!!")
self._entity.sprite_number = self.base_sprite + 246
self.hp = 0
old_pos = int(self.draw_pos[0]), int(self.draw_pos[1])
old_pos = int(self.draw_pos.x), int(self.draw_pos.y)
if not test:
other.do_move(*old_pos)
return True
@ -423,8 +423,8 @@ class EnemyEntity(COSEntity):
def act(self):
if self.hp > 0:
# if player nearby: attack
x, y = self.draw_pos
px, py = self.game.player.draw_pos
x, y = self.draw_pos.x, self.draw_pos.y
px, py = self.game.player.draw_pos.x, self.game.player.draw_pos.y
for d in ((1, 0), (0, 1), (-1, 0), (1, 0)):
if int(x + d[0]) == int(px) and int(y + d[1]) == int(py):
self.try_move(*d)

View File

@ -22,12 +22,13 @@ class TileInfo:
@staticmethod
def from_grid(grid, xy:tuple):
values = {}
x_max, y_max = grid.grid_size
for d in deltas:
tx, ty = d[0] + xy[0], d[1] + xy[1]
try:
values[d] = grid.at((tx, ty)).walkable
except ValueError:
if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max:
values[d] = True
else:
values[d] = grid.at((tx, ty)).walkable
return TileInfo(values)
@staticmethod
@ -70,10 +71,10 @@ def special_rule_verify(rule, grid, xy, unverified_tiles, pass_unverified=False)
tx, ty = xy[0] + dxy[0], xy[1] + dxy[1]
#print(f"Special rule: {cardinal} {allowed_tile} {type(allowed_tile)} -> ({tx}, {ty}) [{grid.at((tx, ty)).tilesprite}]{'*' if (tx, ty) in unverified_tiles else ''}")
if (tx, ty) in unverified_tiles and cardinal in "nsew": return pass_unverified
try:
return grid.at((tx, ty)).tilesprite == allowed_tile
except ValueError:
x_max, y_max = grid.grid_size
if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max:
return False
return grid.at((tx, ty)).tilesprite == allowed_tile
import random
tile_of_last_resort = 431

View File

@ -87,7 +87,7 @@ class Crypt:
# Side Bar (inventory, level info) config
self.level_caption = mcrfpy.Caption((5,5), "Level: 1", font, fill_color=(255, 255, 255))
self.level_caption.size = 26
self.level_caption.font_size = 26
self.level_caption.outline = 3
self.level_caption.outline_color = (0, 0, 0)
self.sidebar.children.append(self.level_caption)
@ -103,7 +103,7 @@ class Crypt:
mcrfpy.Caption((25, 130 + 95 * i), "x", font, fill_color=(255, 255, 255)) for i in range(5)
]
for i in self.inv_captions:
i.size = 16
i.font_size = 16
self.sidebar.children.append(i)
liminal_void = mcrfpy.Grid(1, 1, t, (0, 0), (16, 16))
@ -382,7 +382,7 @@ class Crypt:
def pull_boulder_search(self):
for dx, dy in ( (0, -1), (-1, 0), (1, 0), (0, 1) ):
for e in self.entities:
if e.draw_pos != (self.player.draw_pos[0] + dx, self.player.draw_pos[1] + dy): continue
if e.draw_pos.x != self.player.draw_pos.x + dx or e.draw_pos.y != self.player.draw_pos.y + dy: continue
if type(e) == ce.BoulderEntity:
self.pull_boulder_move((dx, dy), e)
return self.enemy_turn()
@ -395,7 +395,7 @@ class Crypt:
if self.player.try_move(-p[0], -p[1], test=True):
old_pos = self.player.draw_pos
self.player.try_move(-p[0], -p[1])
target_boulder.do_move(*old_pos)
target_boulder.do_move(old_pos.x, old_pos.y)
def swap_level(self, new_level, spawn_point):
self.level = new_level
@ -451,7 +451,7 @@ class SweetButton:
# main button caption
self.caption = mcrfpy.Caption((40, 3), caption, font, fill_color=font_color)
self.caption.size = font_size
self.caption.font_size = font_size
self.caption.outline_color=font_outline_color
self.caption.outline=font_outline_width
self.main_button.children.append(self.caption)
@ -548,20 +548,20 @@ class MainMenu:
# title text
drop_shadow = mcrfpy.Caption((150, 10), "Crypt Of Sokoban", font, fill_color=(96, 96, 96), outline_color=(192, 0, 0))
drop_shadow.outline = 3
drop_shadow.size = 64
drop_shadow.font_size = 64
components.append(
drop_shadow
)
title_txt = mcrfpy.Caption((158, 18), "Crypt Of Sokoban", font, fill_color=(255, 255, 255))
title_txt.size = 64
title_txt.font_size = 64
components.append(
title_txt
)
# toast: text over the demo grid that fades out on a timer
self.toast = mcrfpy.Caption((150, 400), "", font, fill_color=(0, 0, 0))
self.toast.size = 28
self.toast.font_size = 28
self.toast.outline = 2
self.toast.outline_color = (255, 255, 255)
self.toast_event = None
@ -626,6 +626,7 @@ class MainMenu:
def play(self, sweet_btn, args):
#if args[3] == "start": return # DRAMATIC on release action!
if args[3] == "end": return
mcrfpy.delTimer("demo_motion") # Clean up the demo timer
self.crypt = Crypt()
#mcrfpy.setScene("play")
self.crypt.start()

215
tests/constructor_audit.py Normal file
View File

@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Audit current constructor argument handling for all UI classes"""
import mcrfpy
import sys
def audit_constructors():
"""Test current state of all UI constructors"""
print("=== CONSTRUCTOR AUDIT ===\n")
# Create test scene and texture
mcrfpy.createScene("audit")
texture = mcrfpy.Texture("assets/test_portraits.png", 32, 32)
# Test Frame
print("1. Frame Constructor Tests:")
print("-" * 40)
# No args
try:
f = mcrfpy.Frame()
print("✓ Frame() - works")
except Exception as e:
print(f"✗ Frame() - {e}")
# Traditional 4 args (x, y, w, h)
try:
f = mcrfpy.Frame(10, 20, 100, 50)
print("✓ Frame(10, 20, 100, 50) - works")
except Exception as e:
print(f"✗ Frame(10, 20, 100, 50) - {e}")
# Tuple pos + size
try:
f = mcrfpy.Frame((10, 20), (100, 50))
print("✓ Frame((10, 20), (100, 50)) - works")
except Exception as e:
print(f"✗ Frame((10, 20), (100, 50)) - {e}")
# Keywords
try:
f = mcrfpy.Frame(pos=(10, 20), size=(100, 50))
print("✓ Frame(pos=(10, 20), size=(100, 50)) - works")
except Exception as e:
print(f"✗ Frame(pos=(10, 20), size=(100, 50)) - {e}")
# Test Grid
print("\n2. Grid Constructor Tests:")
print("-" * 40)
# No args
try:
g = mcrfpy.Grid()
print("✓ Grid() - works")
except Exception as e:
print(f"✗ Grid() - {e}")
# Grid size only
try:
g = mcrfpy.Grid((10, 10))
print("✓ Grid((10, 10)) - works")
except Exception as e:
print(f"✗ Grid((10, 10)) - {e}")
# Grid size + texture
try:
g = mcrfpy.Grid((10, 10), texture)
print("✓ Grid((10, 10), texture) - works")
except Exception as e:
print(f"✗ Grid((10, 10), texture) - {e}")
# Full positional (expected: pos, size, grid_size, texture)
try:
g = mcrfpy.Grid((0, 0), (320, 320), (10, 10), texture)
print("✓ Grid((0, 0), (320, 320), (10, 10), texture) - works")
except Exception as e:
print(f"✗ Grid((0, 0), (320, 320), (10, 10), texture) - {e}")
# Keywords
try:
g = mcrfpy.Grid(pos=(0, 0), size=(320, 320), grid_size=(10, 10), texture=texture)
print("✓ Grid(pos=..., size=..., grid_size=..., texture=...) - works")
except Exception as e:
print(f"✗ Grid(pos=..., size=..., grid_size=..., texture=...) - {e}")
# Test Sprite
print("\n3. Sprite Constructor Tests:")
print("-" * 40)
# No args
try:
s = mcrfpy.Sprite()
print("✓ Sprite() - works")
except Exception as e:
print(f"✗ Sprite() - {e}")
# Position only
try:
s = mcrfpy.Sprite((10, 20))
print("✓ Sprite((10, 20)) - works")
except Exception as e:
print(f"✗ Sprite((10, 20)) - {e}")
# Position + texture
try:
s = mcrfpy.Sprite((10, 20), texture)
print("✓ Sprite((10, 20), texture) - works")
except Exception as e:
print(f"✗ Sprite((10, 20), texture) - {e}")
# Position + texture + sprite_index
try:
s = mcrfpy.Sprite((10, 20), texture, 5)
print("✓ Sprite((10, 20), texture, 5) - works")
except Exception as e:
print(f"✗ Sprite((10, 20), texture, 5) - {e}")
# Keywords
try:
s = mcrfpy.Sprite(pos=(10, 20), texture=texture, sprite_index=5)
print("✓ Sprite(pos=..., texture=..., sprite_index=...) - works")
except Exception as e:
print(f"✗ Sprite(pos=..., texture=..., sprite_index=...) - {e}")
# Test Caption
print("\n4. Caption Constructor Tests:")
print("-" * 40)
# No args
try:
c = mcrfpy.Caption()
print("✓ Caption() - works")
except Exception as e:
print(f"✗ Caption() - {e}")
# Text only
try:
c = mcrfpy.Caption("Hello")
print("✓ Caption('Hello') - works")
except Exception as e:
print(f"✗ Caption('Hello') - {e}")
# Position + text (expected order: pos, font, text)
try:
c = mcrfpy.Caption((10, 20), "Hello")
print("✓ Caption((10, 20), 'Hello') - works")
except Exception as e:
print(f"✗ Caption((10, 20), 'Hello') - {e}")
# Position + font + text
try:
c = mcrfpy.Caption((10, 20), 16, "Hello")
print("✓ Caption((10, 20), 16, 'Hello') - works")
except Exception as e:
print(f"✗ Caption((10, 20), 16, 'Hello') - {e}")
# Keywords
try:
c = mcrfpy.Caption(pos=(10, 20), font=16, text="Hello")
print("✓ Caption(pos=..., font=..., text=...) - works")
except Exception as e:
print(f"✗ Caption(pos=..., font=..., text=...) - {e}")
# Test Entity
print("\n5. Entity Constructor Tests:")
print("-" * 40)
# No args
try:
e = mcrfpy.Entity()
print("✓ Entity() - works")
except Exception as e:
print(f"✗ Entity() - {e}")
# Grid position only
try:
e = mcrfpy.Entity((5.0, 6.0))
print("✓ Entity((5.0, 6.0)) - works")
except Exception as e:
print(f"✗ Entity((5.0, 6.0)) - {e}")
# Grid position + texture
try:
e = mcrfpy.Entity((5.0, 6.0), texture)
print("✓ Entity((5.0, 6.0), texture) - works")
except Exception as e:
print(f"✗ Entity((5.0, 6.0), texture) - {e}")
# Grid position + texture + sprite_index
try:
e = mcrfpy.Entity((5.0, 6.0), texture, 3)
print("✓ Entity((5.0, 6.0), texture, 3) - works")
except Exception as e:
print(f"✗ Entity((5.0, 6.0), texture, 3) - {e}")
# Keywords
try:
e = mcrfpy.Entity(grid_pos=(5.0, 6.0), texture=texture, sprite_index=3)
print("✓ Entity(grid_pos=..., texture=..., sprite_index=...) - works")
except Exception as e:
print(f"✗ Entity(grid_pos=..., texture=..., sprite_index=...) - {e}")
print("\n=== AUDIT COMPLETE ===")
# Run audit
try:
audit_constructors()
print("\nPASS")
sys.exit(0)
except Exception as e:
print(f"\nFAIL: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
# Count format string characters
fmt = "|OOOOfOOifizfffi"
print(f"Format string: {fmt}")
# Remove the | prefix
fmt_chars = fmt[1:]
print(f"Format chars after |: {fmt_chars}")
print(f"Length: {len(fmt_chars)}")
# Count each type
o_count = fmt_chars.count('O')
f_count = fmt_chars.count('f')
i_count = fmt_chars.count('i')
z_count = fmt_chars.count('z')
s_count = fmt_chars.count('s')
print(f"\nCounts:")
print(f"O (objects): {o_count}")
print(f"f (floats): {f_count}")
print(f"i (ints): {i_count}")
print(f"z (strings): {z_count}")
print(f"s (strings): {s_count}")
print(f"Total: {o_count + f_count + i_count + z_count + s_count}")
# List out each position
print("\nPosition by position:")
for i, c in enumerate(fmt_chars):
print(f"{i+1}: {c}")

View File

@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""
Demonstration of animation callbacks solving race conditions.
Shows how callbacks enable direct causality for game state changes.
"""
import mcrfpy
# Game state
player_moving = False
move_queue = []
def movement_complete(anim, target):
"""Called when player movement animation completes"""
global player_moving, move_queue
print("Movement animation completed!")
player_moving = False
# Process next move if queued
if move_queue:
next_pos = move_queue.pop(0)
move_player_to(next_pos)
else:
print("Player is now idle and ready for input")
def move_player_to(new_pos):
"""Move player with animation and proper state management"""
global player_moving
if player_moving:
print(f"Queueing move to {new_pos}")
move_queue.append(new_pos)
return
player_moving = True
print(f"Moving player to {new_pos}")
# Get player entity (placeholder for demo)
ui = mcrfpy.sceneUI("game")
player = ui[0] # Assume first element is player
# Animate movement with callback
x, y = new_pos
anim_x = mcrfpy.Animation("x", float(x), 0.5, "easeInOutQuad", callback=movement_complete)
anim_y = mcrfpy.Animation("y", float(y), 0.5, "easeInOutQuad")
anim_x.start(player)
anim_y.start(player)
def setup_demo():
"""Set up the demo scene"""
# Create scene
mcrfpy.createScene("game")
mcrfpy.setScene("game")
# Create player sprite
player = mcrfpy.Frame((100, 100), (32, 32), fill_color=(0, 255, 0))
ui = mcrfpy.sceneUI("game")
ui.append(player)
print("Demo: Animation callbacks for movement queue")
print("=" * 40)
# Simulate rapid movement commands
mcrfpy.setTimer("move1", lambda r: move_player_to((200, 100)), 100)
mcrfpy.setTimer("move2", lambda r: move_player_to((200, 200)), 200) # Will be queued
mcrfpy.setTimer("move3", lambda r: move_player_to((100, 200)), 300) # Will be queued
# Exit after demo
mcrfpy.setTimer("exit", lambda r: exit_demo(), 3000)
def exit_demo():
"""Exit the demo"""
print("\nDemo completed successfully!")
print("Callbacks ensure proper movement sequencing without race conditions")
import sys
sys.exit(0)
# Run the demo
setup_demo()

View File

@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""
McRogueFace Animation Demo - Safe Version
=========================================
A safer, simpler version that demonstrates animations without crashes.
"""
import mcrfpy
import sys
# Configuration
DEMO_DURATION = 4.0
# Track state
current_demo = 0
subtitle = None
demo_items = []
def create_scene():
"""Create the demo scene"""
mcrfpy.createScene("demo")
mcrfpy.setScene("demo")
ui = mcrfpy.sceneUI("demo")
# Title
title = mcrfpy.Caption("Animation Demo", 500, 20)
title.fill_color = mcrfpy.Color(255, 255, 0)
title.outline = 2
ui.append(title)
# Subtitle
global subtitle
subtitle = mcrfpy.Caption("Starting...", 450, 60)
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(subtitle)
def clear_demo_items():
"""Clear demo items from scene"""
global demo_items
ui = mcrfpy.sceneUI("demo")
# Remove demo items by tracking what we added
for item in demo_items:
try:
# Find index of item
for i in range(len(ui)):
if i >= 2: # Skip title and subtitle
ui.remove(i)
break
except:
pass
demo_items = []
def demo1_basic():
"""Basic frame animations"""
global demo_items
clear_demo_items()
ui = mcrfpy.sceneUI("demo")
subtitle.text = "Demo 1: Basic Frame Animations"
# Create frame
f = mcrfpy.Frame(100, 150, 200, 100)
f.fill_color = mcrfpy.Color(50, 50, 150)
f.outline = 3
ui.append(f)
demo_items.append(f)
# Simple animations
mcrfpy.Animation("x", 600.0, 2.0, "easeInOut").start(f)
mcrfpy.Animation("w", 300.0, 2.0, "easeInOut").start(f)
mcrfpy.Animation("fill_color", (255, 100, 50, 200), 3.0, "linear").start(f)
def demo2_caption():
"""Caption animations"""
global demo_items
clear_demo_items()
ui = mcrfpy.sceneUI("demo")
subtitle.text = "Demo 2: Caption Animations"
# Moving caption
c1 = mcrfpy.Caption("Moving Text!", 100, 200)
c1.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(c1)
demo_items.append(c1)
mcrfpy.Animation("x", 700.0, 3.0, "easeOutBounce").start(c1)
# Typewriter
c2 = mcrfpy.Caption("", 100, 300)
c2.fill_color = mcrfpy.Color(0, 255, 255)
ui.append(c2)
demo_items.append(c2)
mcrfpy.Animation("text", "Typewriter effect...", 3.0, "linear").start(c2)
def demo3_multiple():
"""Multiple animations"""
global demo_items
clear_demo_items()
ui = mcrfpy.sceneUI("demo")
subtitle.text = "Demo 3: Multiple Animations"
# Create several frames
for i in range(5):
f = mcrfpy.Frame(100 + i * 120, 200, 80, 80)
f.fill_color = mcrfpy.Color(50 + i * 40, 100, 200 - i * 30)
ui.append(f)
demo_items.append(f)
# Animate each differently
target_y = 350 + i * 20
mcrfpy.Animation("y", float(target_y), 2.0, "easeInOut").start(f)
mcrfpy.Animation("opacity", 0.5, 3.0, "easeInOut").start(f)
def run_next_demo(runtime):
"""Run the next demo"""
global current_demo
demos = [demo1_basic, demo2_caption, demo3_multiple]
if current_demo < len(demos):
demos[current_demo]()
current_demo += 1
if current_demo < len(demos):
mcrfpy.setTimer("next", run_next_demo, int(DEMO_DURATION * 1000))
else:
subtitle.text = "Demo Complete!"
# Exit after a delay
def exit_program(rt):
print("Demo finished successfully!")
sys.exit(0)
mcrfpy.setTimer("exit", exit_program, 2000)
# Initialize
print("Starting Safe Animation Demo...")
create_scene()
# Start demos
mcrfpy.setTimer("start", run_next_demo, 500)

View File

@ -0,0 +1,616 @@
#!/usr/bin/env python3
"""
McRogueFace Animation Sizzle Reel
=================================
This script demonstrates EVERY animation type on EVERY UI object type.
It showcases all 30 easing functions, all animatable properties, and
special animation modes (delta, sprite sequences, text effects).
The script creates a comprehensive visual demonstration of the animation
system's capabilities, cycling through different objects and effects.
Author: Claude
Purpose: Complete animation system demonstration
"""
import mcrfpy
from mcrfpy import Color, Frame, Caption, Sprite, Grid, Entity, Texture, Animation
import sys
import math
# Configuration
SCENE_WIDTH = 1280
SCENE_HEIGHT = 720
DEMO_DURATION = 5.0 # Duration for each demo section
# All available easing functions
EASING_FUNCTIONS = [
"linear", "easeIn", "easeOut", "easeInOut",
"easeInQuad", "easeOutQuad", "easeInOutQuad",
"easeInCubic", "easeOutCubic", "easeInOutCubic",
"easeInQuart", "easeOutQuart", "easeInOutQuart",
"easeInSine", "easeOutSine", "easeInOutSine",
"easeInExpo", "easeOutExpo", "easeInOutExpo",
"easeInCirc", "easeOutCirc", "easeInOutCirc",
"easeInElastic", "easeOutElastic", "easeInOutElastic",
"easeInBack", "easeOutBack", "easeInOutBack",
"easeInBounce", "easeOutBounce", "easeInOutBounce"
]
# Track current demo state
current_demo = 0
demo_start_time = 0
demos = []
# Handle ESC key to exit
def handle_keypress(scene_name, keycode):
if keycode == 256: # ESC key
print("Exiting animation sizzle reel...")
sys.exit(0)
def create_demo_scene():
"""Create the main demo scene with title"""
mcrfpy.createScene("sizzle_reel")
mcrfpy.setScene("sizzle_reel")
mcrfpy.keypressScene(handle_keypress)
ui = mcrfpy.sceneUI("sizzle_reel")
# Title caption
title = Caption("McRogueFace Animation Sizzle Reel",
SCENE_WIDTH/2 - 200, 20)
title.fill_color = Color(255, 255, 0)
title.outline = 2
title.outline_color = Color(0, 0, 0)
ui.append(title)
# Subtitle showing current demo
global subtitle
subtitle = Caption("Initializing...",
SCENE_WIDTH/2 - 150, 60)
subtitle.fill_color = Color(200, 200, 200)
ui.append(subtitle)
return ui
def demo_frame_basic_animations(ui):
"""Demo 1: Basic frame animations - position, size, colors"""
subtitle.text = "Demo 1: Frame Basic Animations (Position, Size, Colors)"
# Create test frame
frame = Frame(100, 150, 200, 100)
frame.fill_color = Color(50, 50, 150)
frame.outline = 3
frame.outline_color = Color(255, 255, 255)
ui.append(frame)
# Position animations with different easings
x_anim = Animation("x", 800.0, 2.0, "easeInOutBack")
y_anim = Animation("y", 400.0, 2.0, "easeInOutElastic")
x_anim.start(frame)
y_anim.start(frame)
# Size animations
w_anim = Animation("w", 400.0, 3.0, "easeInOutCubic")
h_anim = Animation("h", 200.0, 3.0, "easeInOutCubic")
w_anim.start(frame)
h_anim.start(frame)
# Color animations - use tuples instead of Color objects
fill_anim = Animation("fill_color", (255, 100, 50, 200), 4.0, "easeInOutSine")
outline_anim = Animation("outline_color", (0, 255, 255, 255), 4.0, "easeOutBounce")
fill_anim.start(frame)
outline_anim.start(frame)
# Outline thickness animation
thickness_anim = Animation("outline", 10.0, 4.5, "easeInOutQuad")
thickness_anim.start(frame)
return frame
def demo_frame_opacity_zindex(ui):
"""Demo 2: Frame opacity and z-index animations"""
subtitle.text = "Demo 2: Frame Opacity & Z-Index Animations"
frames = []
colors = [
Color(255, 0, 0, 200),
Color(0, 255, 0, 200),
Color(0, 0, 255, 200),
Color(255, 255, 0, 200)
]
# Create overlapping frames
for i in range(4):
frame = Frame(200 + i*80, 200 + i*40, 200, 150)
frame.fill_color = colors[i]
frame.outline = 2
frame.z_index = i
ui.append(frame)
frames.append(frame)
# Animate opacity in waves
opacity_anim = Animation("opacity", 0.3, 2.0, "easeInOutSine")
opacity_anim.start(frame)
# Reverse opacity animation
opacity_back = Animation("opacity", 1.0, 2.0, "easeInOutSine", delta=False)
mcrfpy.setTimer(f"opacity_back_{i}", lambda t, f=frame, a=opacity_back: a.start(f), 2000)
# Z-index shuffle animation
z_anim = Animation("z_index", (i + 2) % 4, 3.0, "linear")
z_anim.start(frame)
return frames
def demo_caption_animations(ui):
"""Demo 3: Caption text animations and effects"""
subtitle.text = "Demo 3: Caption Animations (Text, Color, Position)"
# Basic caption with position animation
caption1 = Caption("Moving Text!", 100, 200)
caption1.fill_color = Color(255, 255, 255)
caption1.outline = 1
ui.append(caption1)
# Animate across screen with bounce
x_anim = Animation("x", 900.0, 3.0, "easeOutBounce")
x_anim.start(caption1)
# Color cycling caption
caption2 = Caption("Rainbow Colors", 400, 300)
caption2.outline = 2
ui.append(caption2)
# Cycle through colors - use tuples
color_anim1 = Animation("fill_color", (255, 0, 0, 255), 1.0, "linear")
color_anim2 = Animation("fill_color", (0, 255, 0, 255), 1.0, "linear")
color_anim3 = Animation("fill_color", (0, 0, 255, 255), 1.0, "linear")
color_anim4 = Animation("fill_color", (255, 255, 255, 255), 1.0, "linear")
color_anim1.start(caption2)
mcrfpy.setTimer("color2", lambda t: color_anim2.start(caption2), 1000)
mcrfpy.setTimer("color3", lambda t: color_anim3.start(caption2), 2000)
mcrfpy.setTimer("color4", lambda t: color_anim4.start(caption2), 3000)
# Typewriter effect caption
caption3 = Caption("", 100, 400)
caption3.fill_color = Color(0, 255, 255)
ui.append(caption3)
typewriter = Animation("text", "This text appears one character at a time...", 3.0, "linear")
typewriter.start(caption3)
# Size animation caption
caption4 = Caption("Growing Text", 400, 500)
caption4.fill_color = Color(255, 200, 0)
ui.append(caption4)
# Note: size animation would require font size property support
# For now, animate position to simulate growth
scale_sim = Animation("y", 480.0, 2.0, "easeInOutElastic")
scale_sim.start(caption4)
return [caption1, caption2, caption3, caption4]
def demo_sprite_animations(ui):
"""Demo 4: Sprite animations including sprite sequences"""
subtitle.text = "Demo 4: Sprite Animations (Position, Scale, Sprite Sequences)"
# Load a test texture (you'll need to adjust path)
try:
texture = Texture("assets/sprites/player.png", grid_size=(32, 32))
except:
# Fallback if texture not found
texture = None
if texture:
# Basic sprite with position animation
sprite1 = Sprite(100, 200, texture, sprite_index=0)
sprite1.scale = 2.0
ui.append(sprite1)
# Circular motion using sin/cos animations
# We'll use delta mode to create circular motion
x_circle = Animation("x", 300.0, 4.0, "easeInOutSine")
y_circle = Animation("y", 300.0, 4.0, "easeInOutCubic")
x_circle.start(sprite1)
y_circle.start(sprite1)
# Sprite sequence animation (walking cycle)
sprite2 = Sprite(500, 300, texture, sprite_index=0)
sprite2.scale = 3.0
ui.append(sprite2)
# Animate through sprite indices for animation
walk_cycle = Animation("sprite_index", [0, 1, 2, 3, 2, 1], 2.0, "linear")
walk_cycle.start(sprite2)
# Scale pulsing sprite
sprite3 = Sprite(800, 400, texture, sprite_index=4)
ui.append(sprite3)
# Note: scale animation would need to be supported
# For now use position to simulate
pulse_y = Animation("y", 380.0, 0.5, "easeInOutSine")
pulse_y.start(sprite3)
# Z-index animation for layering
sprite3_z = Animation("z_index", 10, 2.0, "linear")
sprite3_z.start(sprite3)
return [sprite1, sprite2, sprite3]
else:
# Create placeholder caption if no texture
no_texture = Caption("(Sprite demo requires texture file)", 400, 350)
no_texture.fill_color = Color(255, 100, 100)
ui.append(no_texture)
return [no_texture]
def demo_grid_animations(ui):
"""Demo 5: Grid animations (position, camera, zoom)"""
subtitle.text = "Demo 5: Grid Animations (Position, Camera Effects)"
# Create a grid
try:
texture = Texture("assets/sprites/tiles.png", grid_size=(16, 16))
except:
texture = None
# Grid constructor: Grid(grid_x, grid_y, texture, position, size)
# Note: tile dimensions are determined by texture's grid_size
grid = Grid(20, 15, texture, (100, 150), (480, 360)) # 20x24, 15x24
grid.fill_color = Color(20, 20, 40)
ui.append(grid)
# Fill with some test pattern
for y in range(15):
for x in range(20):
point = grid.at(x, y)
point.tilesprite = (x + y) % 4
point.walkable = ((x + y) % 3) != 0
if not point.walkable:
point.color = Color(100, 50, 50, 128)
# Animate grid position
grid_x = Animation("x", 400.0, 3.0, "easeInOutBack")
grid_x.start(grid)
# Camera pan animation (if supported)
# center_x = Animation("center", (10.0, 7.5), 4.0, "easeInOutCubic")
# center_x.start(grid)
# Create entities in the grid
if texture:
entity1 = Entity((5.0, 5.0), texture, 8) # position tuple, texture, sprite_index
entity1.scale = 1.5
grid.entities.append(entity1)
# Animate entity movement
entity_pos = Animation("position", (15.0, 10.0), 3.0, "easeInOutQuad")
entity_pos.start(entity1)
# Create patrolling entity
entity2 = Entity((10.0, 2.0), texture, 12) # position tuple, texture, sprite_index
grid.entities.append(entity2)
# Animate sprite changes
entity2_sprite = Animation("sprite_index", [12, 13, 14, 15, 14, 13], 2.0, "linear")
entity2_sprite.start(entity2)
return grid
def demo_complex_combinations(ui):
"""Demo 6: Complex multi-property animations"""
subtitle.text = "Demo 6: Complex Multi-Property Animations"
# Create a complex UI composition
main_frame = Frame(200, 200, 400, 300)
main_frame.fill_color = Color(30, 30, 60, 200)
main_frame.outline = 2
ui.append(main_frame)
# Child elements
title = Caption("Multi-Animation Demo", 20, 20)
title.fill_color = Color(255, 255, 255)
main_frame.children.append(title)
# Animate everything at once
# Frame animations
frame_x = Animation("x", 600.0, 3.0, "easeInOutElastic")
frame_w = Animation("w", 300.0, 2.5, "easeOutBack")
frame_fill = Animation("fill_color", (60, 30, 90, 220), 4.0, "easeInOutSine")
frame_outline = Animation("outline", 8.0, 3.0, "easeInOutQuad")
frame_x.start(main_frame)
frame_w.start(main_frame)
frame_fill.start(main_frame)
frame_outline.start(main_frame)
# Title animations
title_color = Animation("fill_color", (255, 200, 0, 255), 2.0, "easeOutBounce")
title_color.start(title)
# Add animated sub-frames
for i in range(3):
sub_frame = Frame(50 + i * 100, 100, 80, 80)
sub_frame.fill_color = Color(100 + i*50, 50, 200 - i*50, 180)
main_frame.children.append(sub_frame)
# Rotate positions using delta animations
sub_y = Animation("y", 50.0, 2.0, "easeInOutSine", delta=True)
sub_y.start(sub_frame)
return main_frame
def demo_easing_showcase(ui):
"""Demo 7: Showcase all 30 easing functions"""
subtitle.text = "Demo 7: All 30 Easing Functions Showcase"
# Create small frames for each easing function
frames_per_row = 6
frame_size = 180
spacing = 10
for i, easing in enumerate(EASING_FUNCTIONS[:12]): # First 12 easings
row = i // frames_per_row
col = i % frames_per_row
x = 50 + col * (frame_size + spacing)
y = 150 + row * (60 + spacing)
# Create indicator frame
frame = Frame(x, y, 20, 20)
frame.fill_color = Color(100, 200, 255)
frame.outline = 1
ui.append(frame)
# Label
label = Caption(easing, x, y - 20)
label.fill_color = Color(200, 200, 200)
ui.append(label)
# Animate using this easing
move_anim = Animation("x", x + frame_size - 20, 3.0, easing)
move_anim.start(frame)
# Continue with remaining easings after a delay
def show_more_easings(runtime):
for j, easing in enumerate(EASING_FUNCTIONS[12:24]): # Next 12
row = j // frames_per_row + 2
col = j % frames_per_row
x = 50 + col * (frame_size + spacing)
y = 150 + row * (60 + spacing)
frame2 = Frame(x, y, 20, 20)
frame2.fill_color = Color(255, 150, 100)
frame2.outline = 1
ui.append(frame2)
label2 = Caption(easing, x, y - 20)
label2.fill_color = Color(200, 200, 200)
ui.append(label2)
move_anim2 = Animation("x", x + frame_size - 20, 3.0, easing)
move_anim2.start(frame2)
mcrfpy.setTimer("more_easings", show_more_easings, 1000)
# Show final easings
def show_final_easings(runtime):
for k, easing in enumerate(EASING_FUNCTIONS[24:]): # Last 6
row = k // frames_per_row + 4
col = k % frames_per_row
x = 50 + col * (frame_size + spacing)
y = 150 + row * (60 + spacing)
frame3 = Frame(x, y, 20, 20)
frame3.fill_color = Color(150, 255, 150)
frame3.outline = 1
ui.append(frame3)
label3 = Caption(easing, x, y - 20)
label3.fill_color = Color(200, 200, 200)
ui.append(label3)
move_anim3 = Animation("x", x + frame_size - 20, 3.0, easing)
move_anim3.start(frame3)
mcrfpy.setTimer("final_easings", show_final_easings, 2000)
def demo_delta_animations(ui):
"""Demo 8: Delta mode animations (relative movements)"""
subtitle.text = "Demo 8: Delta Mode Animations (Relative Movements)"
# Create objects that will move relative to their position
frames = []
start_positions = [(100, 200), (300, 200), (500, 200), (700, 200)]
colors = [Color(255, 100, 100), Color(100, 255, 100),
Color(100, 100, 255), Color(255, 255, 100)]
for i, (x, y) in enumerate(start_positions):
frame = Frame(x, y, 80, 80)
frame.fill_color = colors[i]
frame.outline = 2
ui.append(frame)
frames.append(frame)
# Delta animations - move relative to current position
# Each frame moves by different amounts
dx = (i + 1) * 50
dy = math.sin(i) * 100
x_delta = Animation("x", dx, 2.0, "easeInOutBack", delta=True)
y_delta = Animation("y", dy, 2.0, "easeInOutElastic", delta=True)
x_delta.start(frame)
y_delta.start(frame)
# Create caption showing delta mode
delta_label = Caption("Delta mode: Relative animations from current position", 200, 400)
delta_label.fill_color = Color(255, 255, 255)
ui.append(delta_label)
# Animate the label with delta mode text append
text_delta = Animation("text", " - ANIMATED!", 2.0, "linear", delta=True)
text_delta.start(delta_label)
return frames
def demo_color_component_animations(ui):
"""Demo 9: Individual color channel animations"""
subtitle.text = "Demo 9: Color Component Animations (R, G, B, A channels)"
# Create frames to demonstrate individual color channel animations
base_frame = Frame(300, 200, 600, 300)
base_frame.fill_color = Color(128, 128, 128, 255)
base_frame.outline = 3
ui.append(base_frame)
# Labels for each channel
labels = ["Red", "Green", "Blue", "Alpha"]
positions = [(50, 50), (200, 50), (350, 50), (500, 50)]
for i, (label_text, (x, y)) in enumerate(zip(labels, positions)):
# Create label
label = Caption(label_text, x, y - 30)
label.fill_color = Color(255, 255, 255)
base_frame.children.append(label)
# Create demo frame for this channel
demo_frame = Frame(x, y, 100, 100)
demo_frame.fill_color = Color(100, 100, 100, 200)
demo_frame.outline = 2
base_frame.children.append(demo_frame)
# Animate individual color channel
if i == 0: # Red
r_anim = Animation("fill_color.r", 255, 3.0, "easeInOutSine")
r_anim.start(demo_frame)
elif i == 1: # Green
g_anim = Animation("fill_color.g", 255, 3.0, "easeInOutSine")
g_anim.start(demo_frame)
elif i == 2: # Blue
b_anim = Animation("fill_color.b", 255, 3.0, "easeInOutSine")
b_anim.start(demo_frame)
else: # Alpha
a_anim = Animation("fill_color.a", 50, 3.0, "easeInOutSine")
a_anim.start(demo_frame)
# Animate main frame outline color components in sequence
outline_r = Animation("outline_color.r", 255, 1.0, "linear")
outline_g = Animation("outline_color.g", 255, 1.0, "linear")
outline_b = Animation("outline_color.b", 0, 1.0, "linear")
outline_r.start(base_frame)
mcrfpy.setTimer("outline_g", lambda t: outline_g.start(base_frame), 1000)
mcrfpy.setTimer("outline_b", lambda t: outline_b.start(base_frame), 2000)
return base_frame
def demo_performance_stress_test(ui):
"""Demo 10: Performance test with many simultaneous animations"""
subtitle.text = "Demo 10: Performance Stress Test (100+ Simultaneous Animations)"
# Create many small objects with different animations
num_objects = 100
for i in range(num_objects):
# Random starting position
x = 100 + (i % 20) * 50
y = 150 + (i // 20) * 50
# Create small frame
size = 20 + (i % 3) * 10
frame = Frame(x, y, size, size)
# Random color
r = (i * 37) % 256
g = (i * 73) % 256
b = (i * 113) % 256
frame.fill_color = Color(r, g, b, 200)
frame.outline = 1
ui.append(frame)
# Random animation properties
target_x = 100 + (i % 15) * 70
target_y = 150 + (i // 15) * 70
duration = 2.0 + (i % 30) * 0.1
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
# Start multiple animations per object
x_anim = Animation("x", target_x, duration, easing)
y_anim = Animation("y", target_y, duration, easing)
opacity_anim = Animation("opacity", 0.3 + (i % 7) * 0.1, duration, "easeInOutSine")
x_anim.start(frame)
y_anim.start(frame)
opacity_anim.start(frame)
# Performance counter
perf_caption = Caption(f"Animating {num_objects * 3} properties simultaneously", 400, 600)
perf_caption.fill_color = Color(255, 255, 0)
ui.append(perf_caption)
def next_demo(runtime):
"""Cycle to the next demo"""
global current_demo, demo_start_time
# Clear the UI except title and subtitle
ui = mcrfpy.sceneUI("sizzle_reel")
# Keep only the first two elements (title and subtitle)
while len(ui) > 2:
# Remove from the end to avoid index issues
ui.remove(len(ui) - 1)
# Run the next demo
if current_demo < len(demos):
demos[current_demo](ui)
current_demo += 1
# Schedule next demo
if current_demo < len(demos):
mcrfpy.setTimer("next_demo", next_demo, int(DEMO_DURATION * 1000))
else:
# All demos complete
subtitle.text = "Animation Showcase Complete! Press ESC to exit."
complete = Caption("All animation types demonstrated!", 400, 350)
complete.fill_color = Color(0, 255, 0)
complete.outline = 2
ui.append(complete)
def run_sizzle_reel(runtime):
"""Main entry point - start the demo sequence"""
global demos
# List of all demo functions
demos = [
demo_frame_basic_animations,
demo_frame_opacity_zindex,
demo_caption_animations,
demo_sprite_animations,
demo_grid_animations,
demo_complex_combinations,
demo_easing_showcase,
demo_delta_animations,
demo_color_component_animations,
demo_performance_stress_test
]
# Start the first demo
next_demo(runtime)
# Initialize scene
ui = create_demo_scene()
# Start the sizzle reel after a short delay
mcrfpy.setTimer("start_sizzle", run_sizzle_reel, 500)
print("Starting McRogueFace Animation Sizzle Reel...")
print("This will demonstrate ALL animation types on ALL objects.")
print("Press ESC at any time to exit.")

View File

@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""
McRogueFace Animation Sizzle Reel (Fixed)
=========================================
This script demonstrates EVERY animation type on EVERY UI object type.
Fixed version that works properly with the game loop.
"""
import mcrfpy
# Configuration
SCENE_WIDTH = 1280
SCENE_HEIGHT = 720
DEMO_DURATION = 5.0 # Duration for each demo section
# All available easing functions
EASING_FUNCTIONS = [
"linear", "easeIn", "easeOut", "easeInOut",
"easeInQuad", "easeOutQuad", "easeInOutQuad",
"easeInCubic", "easeOutCubic", "easeInOutCubic",
"easeInQuart", "easeOutQuart", "easeInOutQuart",
"easeInSine", "easeOutSine", "easeInOutSine",
"easeInExpo", "easeOutExpo", "easeInOutExpo",
"easeInCirc", "easeOutCirc", "easeInOutCirc",
"easeInElastic", "easeOutElastic", "easeInOutElastic",
"easeInBack", "easeOutBack", "easeInOutBack",
"easeInBounce", "easeOutBounce", "easeInOutBounce"
]
# Track current demo state
current_demo = 0
subtitle = None
def create_demo_scene():
"""Create the main demo scene with title"""
mcrfpy.createScene("sizzle_reel")
mcrfpy.setScene("sizzle_reel")
ui = mcrfpy.sceneUI("sizzle_reel")
# Title caption
title = mcrfpy.Caption("McRogueFace Animation Sizzle Reel",
SCENE_WIDTH/2 - 200, 20)
title.fill_color = mcrfpy.Color(255, 255, 0)
title.outline = 2
title.outline_color = mcrfpy.Color(0, 0, 0)
ui.append(title)
# Subtitle showing current demo
global subtitle
subtitle = mcrfpy.Caption("Initializing...",
SCENE_WIDTH/2 - 150, 60)
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(subtitle)
return ui
def demo_frame_basic_animations():
"""Demo 1: Basic frame animations - position, size, colors"""
ui = mcrfpy.sceneUI("sizzle_reel")
subtitle.text = "Demo 1: Frame Basic Animations (Position, Size, Colors)"
# Create test frame
frame = mcrfpy.Frame(100, 150, 200, 100)
frame.fill_color = mcrfpy.Color(50, 50, 150)
frame.outline = 3
frame.outline_color = mcrfpy.Color(255, 255, 255)
ui.append(frame)
# Position animations with different easings
x_anim = mcrfpy.Animation("x", 800.0, 2.0, "easeInOutBack")
y_anim = mcrfpy.Animation("y", 400.0, 2.0, "easeInOutElastic")
x_anim.start(frame)
y_anim.start(frame)
# Size animations
w_anim = mcrfpy.Animation("w", 400.0, 3.0, "easeInOutCubic")
h_anim = mcrfpy.Animation("h", 200.0, 3.0, "easeInOutCubic")
w_anim.start(frame)
h_anim.start(frame)
# Color animations
fill_anim = mcrfpy.Animation("fill_color", mcrfpy.Color(255, 100, 50, 200), 4.0, "easeInOutSine")
outline_anim = mcrfpy.Animation("outline_color", mcrfpy.Color(0, 255, 255), 4.0, "easeOutBounce")
fill_anim.start(frame)
outline_anim.start(frame)
# Outline thickness animation
thickness_anim = mcrfpy.Animation("outline", 10.0, 4.5, "easeInOutQuad")
thickness_anim.start(frame)
def demo_caption_animations():
"""Demo 2: Caption text animations and effects"""
ui = mcrfpy.sceneUI("sizzle_reel")
subtitle.text = "Demo 2: Caption Animations (Text, Color, Position)"
# Basic caption with position animation
caption1 = mcrfpy.Caption("Moving Text!", 100, 200)
caption1.fill_color = mcrfpy.Color(255, 255, 255)
caption1.outline = 1
ui.append(caption1)
# Animate across screen with bounce
x_anim = mcrfpy.Animation("x", 900.0, 3.0, "easeOutBounce")
x_anim.start(caption1)
# Color cycling caption
caption2 = mcrfpy.Caption("Rainbow Colors", 400, 300)
caption2.outline = 2
ui.append(caption2)
# Cycle through colors
color_anim1 = mcrfpy.Animation("fill_color", mcrfpy.Color(255, 0, 0), 1.0, "linear")
color_anim1.start(caption2)
# Typewriter effect caption
caption3 = mcrfpy.Caption("", 100, 400)
caption3.fill_color = mcrfpy.Color(0, 255, 255)
ui.append(caption3)
typewriter = mcrfpy.Animation("text", "This text appears one character at a time...", 3.0, "linear")
typewriter.start(caption3)
def demo_sprite_animations():
"""Demo 3: Sprite animations (if texture available)"""
ui = mcrfpy.sceneUI("sizzle_reel")
subtitle.text = "Demo 3: Sprite Animations"
# Create placeholder caption since texture might not exist
no_texture = mcrfpy.Caption("(Sprite demo - textures may not be loaded)", 400, 350)
no_texture.fill_color = mcrfpy.Color(255, 100, 100)
ui.append(no_texture)
def demo_performance_stress_test():
"""Demo 4: Performance test with many simultaneous animations"""
ui = mcrfpy.sceneUI("sizzle_reel")
subtitle.text = "Demo 4: Performance Test (50+ Simultaneous Animations)"
# Create many small objects with different animations
num_objects = 50
for i in range(num_objects):
# Random starting position
x = 100 + (i % 10) * 100
y = 150 + (i // 10) * 80
# Create small frame
size = 20 + (i % 3) * 10
frame = mcrfpy.Frame(x, y, size, size)
# Random color
r = (i * 37) % 256
g = (i * 73) % 256
b = (i * 113) % 256
frame.fill_color = mcrfpy.Color(r, g, b, 200)
frame.outline = 1
ui.append(frame)
# Random animation properties
target_x = 100 + (i % 8) * 120
target_y = 150 + (i // 8) * 100
duration = 2.0 + (i % 30) * 0.1
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
# Start multiple animations per object
x_anim = mcrfpy.Animation("x", float(target_x), duration, easing)
y_anim = mcrfpy.Animation("y", float(target_y), duration, easing)
opacity_anim = mcrfpy.Animation("opacity", 0.3 + (i % 7) * 0.1, duration, "easeInOutSine")
x_anim.start(frame)
y_anim.start(frame)
opacity_anim.start(frame)
# Performance counter
perf_caption = mcrfpy.Caption(f"Animating {num_objects * 3} properties simultaneously", 400, 600)
perf_caption.fill_color = mcrfpy.Color(255, 255, 0)
ui.append(perf_caption)
def clear_scene():
"""Clear the scene except title and subtitle"""
ui = mcrfpy.sceneUI("sizzle_reel")
# Keep only the first two elements (title and subtitle)
while len(ui) > 2:
ui.remove(2)
def run_demo_sequence(runtime):
"""Run through all demos"""
global current_demo
# Clear previous demo
clear_scene()
# Demo list
demos = [
demo_frame_basic_animations,
demo_caption_animations,
demo_sprite_animations,
demo_performance_stress_test
]
if current_demo < len(demos):
# Run current demo
demos[current_demo]()
current_demo += 1
# Schedule next demo
if current_demo < len(demos):
mcrfpy.setTimer("next_demo", run_demo_sequence, int(DEMO_DURATION * 1000))
else:
# All demos complete
subtitle.text = "Animation Showcase Complete!"
complete = mcrfpy.Caption("All animation types demonstrated!", 400, 350)
complete.fill_color = mcrfpy.Color(0, 255, 0)
complete.outline = 2
ui = mcrfpy.sceneUI("sizzle_reel")
ui.append(complete)
# Initialize scene
print("Starting McRogueFace Animation Sizzle Reel...")
print("This will demonstrate animation types on various objects.")
ui = create_demo_scene()
# Start the demo sequence after a short delay
mcrfpy.setTimer("start_demos", run_demo_sequence, 500)

View File

@ -0,0 +1,307 @@
#!/usr/bin/env python3
"""
McRogueFace Animation Sizzle Reel v2
====================================
Fixed version with proper API usage for animations and collections.
"""
import mcrfpy
# Configuration
SCENE_WIDTH = 1280
SCENE_HEIGHT = 720
DEMO_DURATION = 5.0 # Duration for each demo section
# All available easing functions
EASING_FUNCTIONS = [
"linear", "easeIn", "easeOut", "easeInOut",
"easeInQuad", "easeOutQuad", "easeInOutQuad",
"easeInCubic", "easeOutCubic", "easeInOutCubic",
"easeInQuart", "easeOutQuart", "easeInOutQuart",
"easeInSine", "easeOutSine", "easeInOutSine",
"easeInExpo", "easeOutExpo", "easeInOutExpo",
"easeInCirc", "easeOutCirc", "easeInOutCirc",
"easeInElastic", "easeOutElastic", "easeInOutElastic",
"easeInBack", "easeOutBack", "easeInOutBack",
"easeInBounce", "easeOutBounce", "easeInOutBounce"
]
# Track current demo state
current_demo = 0
subtitle = None
demo_objects = [] # Track objects from current demo
def create_demo_scene():
"""Create the main demo scene with title"""
mcrfpy.createScene("sizzle_reel")
mcrfpy.setScene("sizzle_reel")
ui = mcrfpy.sceneUI("sizzle_reel")
# Title caption
title = mcrfpy.Caption("McRogueFace Animation Sizzle Reel",
SCENE_WIDTH/2 - 200, 20)
title.fill_color = mcrfpy.Color(255, 255, 0)
title.outline = 2
title.outline_color = mcrfpy.Color(0, 0, 0)
ui.append(title)
# Subtitle showing current demo
global subtitle
subtitle = mcrfpy.Caption("Initializing...",
SCENE_WIDTH/2 - 150, 60)
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(subtitle)
return ui
def demo_frame_basic_animations():
"""Demo 1: Basic frame animations - position, size, colors"""
global demo_objects
demo_objects = []
ui = mcrfpy.sceneUI("sizzle_reel")
subtitle.text = "Demo 1: Frame Basic Animations (Position, Size, Colors)"
# Create test frame
frame = mcrfpy.Frame(100, 150, 200, 100)
frame.fill_color = mcrfpy.Color(50, 50, 150)
frame.outline = 3
frame.outline_color = mcrfpy.Color(255, 255, 255)
ui.append(frame)
demo_objects.append(frame)
# Position animations with different easings
x_anim = mcrfpy.Animation("x", 800.0, 2.0, "easeInOutBack")
y_anim = mcrfpy.Animation("y", 400.0, 2.0, "easeInOutElastic")
x_anim.start(frame)
y_anim.start(frame)
# Size animations
w_anim = mcrfpy.Animation("w", 400.0, 3.0, "easeInOutCubic")
h_anim = mcrfpy.Animation("h", 200.0, 3.0, "easeInOutCubic")
w_anim.start(frame)
h_anim.start(frame)
# Color animations - use tuples instead of Color objects
fill_anim = mcrfpy.Animation("fill_color", (255, 100, 50, 200), 4.0, "easeInOutSine")
outline_anim = mcrfpy.Animation("outline_color", (0, 255, 255, 255), 4.0, "easeOutBounce")
fill_anim.start(frame)
outline_anim.start(frame)
# Outline thickness animation
thickness_anim = mcrfpy.Animation("outline", 10.0, 4.5, "easeInOutQuad")
thickness_anim.start(frame)
def demo_caption_animations():
"""Demo 2: Caption text animations and effects"""
global demo_objects
demo_objects = []
ui = mcrfpy.sceneUI("sizzle_reel")
subtitle.text = "Demo 2: Caption Animations (Text, Color, Position)"
# Basic caption with position animation
caption1 = mcrfpy.Caption("Moving Text!", 100, 200)
caption1.fill_color = mcrfpy.Color(255, 255, 255)
caption1.outline = 1
ui.append(caption1)
demo_objects.append(caption1)
# Animate across screen with bounce
x_anim = mcrfpy.Animation("x", 900.0, 3.0, "easeOutBounce")
x_anim.start(caption1)
# Color cycling caption
caption2 = mcrfpy.Caption("Rainbow Colors", 400, 300)
caption2.outline = 2
ui.append(caption2)
demo_objects.append(caption2)
# Cycle through colors using tuples
color_anim1 = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear")
color_anim1.start(caption2)
# Schedule color changes
def change_to_green(rt):
color_anim2 = mcrfpy.Animation("fill_color", (0, 255, 0, 255), 1.0, "linear")
color_anim2.start(caption2)
def change_to_blue(rt):
color_anim3 = mcrfpy.Animation("fill_color", (0, 0, 255, 255), 1.0, "linear")
color_anim3.start(caption2)
def change_to_white(rt):
color_anim4 = mcrfpy.Animation("fill_color", (255, 255, 255, 255), 1.0, "linear")
color_anim4.start(caption2)
mcrfpy.setTimer("color2", change_to_green, 1000)
mcrfpy.setTimer("color3", change_to_blue, 2000)
mcrfpy.setTimer("color4", change_to_white, 3000)
# Typewriter effect caption
caption3 = mcrfpy.Caption("", 100, 400)
caption3.fill_color = mcrfpy.Color(0, 255, 255)
ui.append(caption3)
demo_objects.append(caption3)
typewriter = mcrfpy.Animation("text", "This text appears one character at a time...", 3.0, "linear")
typewriter.start(caption3)
def demo_easing_showcase():
"""Demo 3: Showcase different easing functions"""
global demo_objects
demo_objects = []
ui = mcrfpy.sceneUI("sizzle_reel")
subtitle.text = "Demo 3: Easing Functions Showcase"
# Create small frames for each easing function
frames_per_row = 6
frame_width = 180
spacing = 10
# Show first 12 easings
for i, easing in enumerate(EASING_FUNCTIONS[:12]):
row = i // frames_per_row
col = i % frames_per_row
x = 50 + col * (frame_width + spacing)
y = 150 + row * (80 + spacing)
# Create indicator frame
frame = mcrfpy.Frame(x, y, 20, 20)
frame.fill_color = mcrfpy.Color(100, 200, 255)
frame.outline = 1
ui.append(frame)
demo_objects.append(frame)
# Label
label = mcrfpy.Caption(easing[:8], x, y - 20) # Truncate long names
label.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(label)
demo_objects.append(label)
# Animate using this easing
move_anim = mcrfpy.Animation("x", float(x + frame_width - 20), 3.0, easing)
move_anim.start(frame)
def demo_performance_stress_test():
"""Demo 4: Performance test with many simultaneous animations"""
global demo_objects
demo_objects = []
ui = mcrfpy.sceneUI("sizzle_reel")
subtitle.text = "Demo 4: Performance Test (50+ Simultaneous Animations)"
# Create many small objects with different animations
num_objects = 50
for i in range(num_objects):
# Starting position
x = 100 + (i % 10) * 100
y = 150 + (i // 10) * 80
# Create small frame
size = 20 + (i % 3) * 10
frame = mcrfpy.Frame(x, y, size, size)
# Random color
r = (i * 37) % 256
g = (i * 73) % 256
b = (i * 113) % 256
frame.fill_color = mcrfpy.Color(r, g, b, 200)
frame.outline = 1
ui.append(frame)
demo_objects.append(frame)
# Random animation properties
target_x = 100 + (i % 8) * 120
target_y = 150 + (i // 8) * 100
duration = 2.0 + (i % 30) * 0.1
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
# Start multiple animations per object
x_anim = mcrfpy.Animation("x", float(target_x), duration, easing)
y_anim = mcrfpy.Animation("y", float(target_y), duration, easing)
opacity_anim = mcrfpy.Animation("opacity", 0.3 + (i % 7) * 0.1, duration, "easeInOutSine")
x_anim.start(frame)
y_anim.start(frame)
opacity_anim.start(frame)
# Performance counter
perf_caption = mcrfpy.Caption(f"Animating {num_objects * 3} properties simultaneously", 350, 600)
perf_caption.fill_color = mcrfpy.Color(255, 255, 0)
ui.append(perf_caption)
demo_objects.append(perf_caption)
def clear_scene():
"""Clear the scene except title and subtitle"""
global demo_objects
ui = mcrfpy.sceneUI("sizzle_reel")
# Remove all demo objects
for obj in demo_objects:
try:
# Find index of object
for i in range(len(ui)):
if ui[i] is obj:
ui.remove(ui[i])
break
except:
pass # Object might already be removed
demo_objects = []
# Clean up any timers
for timer_name in ["color2", "color3", "color4"]:
try:
mcrfpy.delTimer(timer_name)
except:
pass
def run_demo_sequence(runtime):
"""Run through all demos"""
global current_demo
# Clear previous demo
clear_scene()
# Demo list
demos = [
demo_frame_basic_animations,
demo_caption_animations,
demo_easing_showcase,
demo_performance_stress_test
]
if current_demo < len(demos):
# Run current demo
demos[current_demo]()
current_demo += 1
# Schedule next demo
if current_demo < len(demos):
mcrfpy.setTimer("next_demo", run_demo_sequence, int(DEMO_DURATION * 1000))
else:
# Final demo completed
def show_complete(rt):
subtitle.text = "Animation Showcase Complete!"
complete = mcrfpy.Caption("All animation types demonstrated!", 400, 350)
complete.fill_color = mcrfpy.Color(0, 255, 0)
complete.outline = 2
ui = mcrfpy.sceneUI("sizzle_reel")
ui.append(complete)
mcrfpy.setTimer("complete", show_complete, 3000)
# Initialize scene
print("Starting McRogueFace Animation Sizzle Reel v2...")
print("This will demonstrate animation types on various objects.")
ui = create_demo_scene()
# Start the demo sequence after a short delay
mcrfpy.setTimer("start_demos", run_demo_sequence, 500)

View File

@ -0,0 +1,316 @@
#!/usr/bin/env python3
"""
McRogueFace Animation Sizzle Reel - Working Version
===================================================
Complete demonstration of all animation capabilities.
Fixed to work properly with the API.
"""
import mcrfpy
import sys
import math
# Configuration
DEMO_DURATION = 7.0 # Duration for each demo
# All available easing functions
EASING_FUNCTIONS = [
"linear", "easeIn", "easeOut", "easeInOut",
"easeInQuad", "easeOutQuad", "easeInOutQuad",
"easeInCubic", "easeOutCubic", "easeInOutCubic",
"easeInQuart", "easeOutQuart", "easeInOutQuart",
"easeInSine", "easeOutSine", "easeInOutSine",
"easeInExpo", "easeOutExpo", "easeInOutExpo",
"easeInCirc", "easeOutCirc", "easeInOutCirc",
"easeInElastic", "easeOutElastic", "easeInOutElastic",
"easeInBack", "easeOutBack", "easeInOutBack",
"easeInBounce", "easeOutBounce", "easeInOutBounce"
]
# Track state
current_demo = 0
subtitle = None
demo_objects = []
def create_scene():
"""Create the demo scene with title"""
mcrfpy.createScene("sizzle")
mcrfpy.setScene("sizzle")
ui = mcrfpy.sceneUI("sizzle")
# Title
title = mcrfpy.Caption("McRogueFace Animation Sizzle Reel", 340, 20)
title.fill_color = mcrfpy.Color(255, 255, 0)
title.outline = 2
title.outline_color = mcrfpy.Color(0, 0, 0)
ui.append(title)
# Subtitle
global subtitle
subtitle = mcrfpy.Caption("Initializing...", 400, 60)
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(subtitle)
def clear_demo():
"""Clear demo objects"""
global demo_objects
ui = mcrfpy.sceneUI("sizzle")
# Remove items starting from the end
# Skip first 2 (title and subtitle)
while len(ui) > 2:
ui.remove(len(ui) - 1)
demo_objects = []
def demo1_frame_basics():
"""Demo 1: Basic frame animations"""
clear_demo()
print("demo1")
subtitle.text = "Demo 1: Frame Animations (Position, Size, Color)"
ui = mcrfpy.sceneUI("sizzle")
# Create frame
frame = mcrfpy.Frame(100, 150, 200, 100)
frame.fill_color = mcrfpy.Color(50, 50, 150)
frame.outline = 3
frame.outline_color = mcrfpy.Color(255, 255, 255)
ui.append(frame)
# Animate properties
mcrfpy.Animation("x", 700.0, 2.5, "easeInOutBack").start(frame)
mcrfpy.Animation("y", 350.0, 2.5, "easeInOutElastic").start(frame)
mcrfpy.Animation("w", 350.0, 3.0, "easeInOutCubic").start(frame)
mcrfpy.Animation("h", 180.0, 3.0, "easeInOutCubic").start(frame)
mcrfpy.Animation("fill_color", (255, 100, 50, 200), 4.0, "easeInOutSine").start(frame)
mcrfpy.Animation("outline_color", (0, 255, 255, 255), 4.0, "easeOutBounce").start(frame)
mcrfpy.Animation("outline", 8.0, 4.0, "easeInOutQuad").start(frame)
def demo2_opacity_zindex():
"""Demo 2: Opacity and z-index animations"""
clear_demo()
print("demo2")
subtitle.text = "Demo 2: Opacity & Z-Index Animations"
ui = mcrfpy.sceneUI("sizzle")
# Create overlapping frames
colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)]
for i in range(4):
frame = mcrfpy.Frame(200 + i*80, 200 + i*40, 200, 150)
frame.fill_color = mcrfpy.Color(colors[i][0], colors[i][1], colors[i][2], 200)
frame.outline = 2
frame.z_index = i
ui.append(frame)
# Animate opacity
mcrfpy.Animation("opacity", 0.3, 2.0, "easeInOutSine").start(frame)
# Schedule opacity return
def return_opacity(rt):
for i in range(4):
mcrfpy.Animation("opacity", 1.0, 2.0, "easeInOutSine").start(ui[i])
mcrfpy.setTimer(f"opacity_{i}", return_opacity, 2100)
def demo3_captions():
"""Demo 3: Caption animations"""
clear_demo()
print("demo3")
subtitle.text = "Demo 3: Caption Animations"
ui = mcrfpy.sceneUI("sizzle")
# Moving caption
c1 = mcrfpy.Caption("Bouncing Text!", 100, 200)
c1.fill_color = mcrfpy.Color(255, 255, 255)
c1.outline = 1
ui.append(c1)
mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1)
# Color cycling caption
c2 = mcrfpy.Caption("Color Cycle", 400, 300)
c2.outline = 2
ui.append(c2)
# Animate through colors
def cycle_colors():
anim = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 0.5, "linear")
anim.start(c2)
def to_green(rt):
mcrfpy.Animation("fill_color", (0, 255, 0, 255), 0.5, "linear").start(c2)
def to_blue(rt):
mcrfpy.Animation("fill_color", (0, 0, 255, 255), 0.5, "linear").start(c2)
def to_white(rt):
mcrfpy.Animation("fill_color", (255, 255, 255, 255), 0.5, "linear").start(c2)
mcrfpy.setTimer("c_green", to_green, 600)
mcrfpy.setTimer("c_blue", to_blue, 1200)
mcrfpy.setTimer("c_white", to_white, 1800)
cycle_colors()
# Typewriter effect
c3 = mcrfpy.Caption("", 100, 400)
c3.fill_color = mcrfpy.Color(0, 255, 255)
ui.append(c3)
mcrfpy.Animation("text", "This text appears one character at a time...", 3.0, "linear").start(c3)
def demo4_easing_showcase():
"""Demo 4: Showcase easing functions"""
clear_demo()
print("demo4")
subtitle.text = "Demo 4: 30 Easing Functions"
ui = mcrfpy.sceneUI("sizzle")
# Show first 15 easings
for i in range(15):
row = i // 5
col = i % 5
x = 80 + col * 180
y = 150 + row * 120
# Create frame
f = mcrfpy.Frame(x, y, 20, 20)
f.fill_color = mcrfpy.Color(100, 150, 255)
f.outline = 1
ui.append(f)
# Label
label = mcrfpy.Caption(EASING_FUNCTIONS[i][:10], x, y - 20)
label.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(label)
# Animate with this easing
mcrfpy.Animation("x", float(x + 140), 3.0, EASING_FUNCTIONS[i]).start(f)
def demo5_performance():
"""Demo 5: Many simultaneous animations"""
clear_demo()
print("demo5")
subtitle.text = "Demo 5: 50+ Simultaneous Animations"
ui = mcrfpy.sceneUI("sizzle")
# Create many animated objects
for i in range(50):
print(f"{i}...",end='',flush=True)
x = 100 + (i % 10) * 90
y = 120 + (i // 10) * 80
f = mcrfpy.Frame(x, y, 25, 25)
r = (i * 37) % 256
g = (i * 73) % 256
b = (i * 113) % 256
f.fill_color = (r, g, b, 200) #mcrfpy.Color(r, g, b, 200)
f.outline = 1
ui.append(f)
# Random animations
target_x = 150 + (i % 8) * 100
target_y = 150 + (i // 8) * 85
duration = 2.0 + (i % 30) * 0.1
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
mcrfpy.Animation("x", float(target_x), duration, easing).start(f)
mcrfpy.Animation("y", float(target_y), duration, easing).start(f)
mcrfpy.Animation("opacity", 0.3 + (i % 7) * 0.1, 2.5, "easeInOutSine").start(f)
def demo6_delta_mode():
"""Demo 6: Delta mode animations"""
clear_demo()
print("demo6")
subtitle.text = "Demo 6: Delta Mode (Relative Movement)"
ui = mcrfpy.sceneUI("sizzle")
# Create frames that move relative to position
positions = [(100, 300), (300, 300), (500, 300), (700, 300)]
colors = [(255, 100, 100), (100, 255, 100), (100, 100, 255), (255, 255, 100)]
for i, ((x, y), color) in enumerate(zip(positions, colors)):
f = mcrfpy.Frame(x, y, 60, 60)
f.fill_color = mcrfpy.Color(color[0], color[1], color[2])
f.outline = 2
ui.append(f)
# Delta animations - move by amount, not to position
dx = (i + 1) * 30
dy = math.sin(i * 0.5) * 50
mcrfpy.Animation("x", float(dx), 2.0, "easeInOutBack", delta=True).start(f)
mcrfpy.Animation("y", float(dy), 2.0, "easeInOutElastic", delta=True).start(f)
# Caption explaining delta mode
info = mcrfpy.Caption("Delta mode: animations move BY amount, not TO position", 200, 450)
info.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(info)
def run_next_demo(runtime):
"""Run the next demo in sequence"""
global current_demo
demos = [
demo1_frame_basics,
demo2_opacity_zindex,
demo3_captions,
demo4_easing_showcase,
demo5_performance,
demo6_delta_mode
]
if current_demo < len(demos):
# Clean up timers from previous demo
for timer in ["opacity_0", "opacity_1", "opacity_2", "opacity_3",
"c_green", "c_blue", "c_white"]:
try:
mcrfpy.delTimer(timer)
except:
pass
# Run next demo
print(f"Run next: {current_demo}")
demos[current_demo]()
current_demo += 1
# Schedule next demo
if current_demo < len(demos):
#mcrfpy.setTimer("next_demo", run_next_demo, int(DEMO_DURATION * 1000))
pass
else:
current_demo = 0
# All done
#subtitle.text = "Animation Showcase Complete!"
#complete = mcrfpy.Caption("All animations demonstrated successfully!", 350, 350)
#complete.fill_color = mcrfpy.Color(0, 255, 0)
#complete.outline = 2
#ui = mcrfpy.sceneUI("sizzle")
#ui.append(complete)
#
## Exit after delay
#def exit_program(rt):
# print("\nSizzle reel completed successfully!")
# sys.exit(0)
#mcrfpy.setTimer("exit", exit_program, 3000)
# Handle ESC key
def handle_keypress(scene_name, keycode):
if keycode == 256: # ESC
print("\nExiting...")
sys.exit(0)
# Initialize
print("Starting McRogueFace Animation Sizzle Reel...")
print("This demonstrates all animation capabilities.")
print("Press ESC to exit at any time.")
create_scene()
mcrfpy.keypressScene(handle_keypress)
# Start the show
mcrfpy.setTimer("start", run_next_demo, int(DEMO_DURATION * 1000))

View File

@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""
McRogueFace API Demo - Final Version
====================================
Complete API demonstration with proper error handling.
Tests all constructors and methods systematically.
"""
import mcrfpy
import sys
def print_section(title):
"""Print a section header"""
print("\n" + "="*60)
print(f" {title}")
print("="*60)
def print_test(name, success=True):
"""Print test result"""
status = "" if success else ""
print(f" {status} {name}")
def test_colors():
"""Test Color API"""
print_section("COLOR TESTS")
try:
# Basic constructors
c1 = mcrfpy.Color(255, 0, 0) # RGB
print_test(f"Color(255,0,0) = ({c1.r},{c1.g},{c1.b},{c1.a})")
c2 = mcrfpy.Color(100, 150, 200, 128) # RGBA
print_test(f"Color(100,150,200,128) = ({c2.r},{c2.g},{c2.b},{c2.a})")
# Property modification
c1.r = 128
c1.g = 128
c1.b = 128
c1.a = 200
print_test(f"Modified color = ({c1.r},{c1.g},{c1.b},{c1.a})")
except Exception as e:
print_test(f"Color test failed: {e}", False)
def test_frames():
"""Test Frame API"""
print_section("FRAME TESTS")
# Create scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
ui = mcrfpy.sceneUI("test")
try:
# Constructors
f1 = mcrfpy.Frame()
print_test(f"Frame() at ({f1.x},{f1.y}) size ({f1.w},{f1.h})")
f2 = mcrfpy.Frame(100, 50)
print_test(f"Frame(100,50) at ({f2.x},{f2.y})")
f3 = mcrfpy.Frame(200, 100, 150, 75)
print_test(f"Frame(200,100,150,75) size ({f3.w},{f3.h})")
# Properties
f3.fill_color = mcrfpy.Color(100, 100, 200)
f3.outline = 3
f3.outline_color = mcrfpy.Color(255, 255, 0)
f3.opacity = 0.8
f3.visible = True
f3.z_index = 5
print_test(f"Frame properties set")
# Add to scene
ui.append(f3)
print_test(f"Frame added to scene")
# Children
child = mcrfpy.Frame(10, 10, 50, 50)
f3.children.append(child)
print_test(f"Child added, count = {len(f3.children)}")
except Exception as e:
print_test(f"Frame test failed: {e}", False)
def test_captions():
"""Test Caption API"""
print_section("CAPTION TESTS")
ui = mcrfpy.sceneUI("test")
try:
# Constructors
c1 = mcrfpy.Caption()
print_test(f"Caption() text='{c1.text}'")
c2 = mcrfpy.Caption("Hello World")
print_test(f"Caption('Hello World') at ({c2.x},{c2.y})")
c3 = mcrfpy.Caption("Test", 300, 200)
print_test(f"Caption with position at ({c3.x},{c3.y})")
# Properties
c3.text = "Modified"
c3.fill_color = mcrfpy.Color(255, 255, 0)
c3.outline = 2
c3.outline_color = mcrfpy.Color(0, 0, 0)
print_test(f"Caption text='{c3.text}'")
ui.append(c3)
print_test("Caption added to scene")
except Exception as e:
print_test(f"Caption test failed: {e}", False)
def test_animations():
"""Test Animation API"""
print_section("ANIMATION TESTS")
ui = mcrfpy.sceneUI("test")
try:
# Create target
frame = mcrfpy.Frame(50, 50, 100, 100)
frame.fill_color = mcrfpy.Color(100, 100, 100)
ui.append(frame)
# Basic animations
a1 = mcrfpy.Animation("x", 300.0, 2.0)
print_test("Animation created (position)")
a2 = mcrfpy.Animation("opacity", 0.5, 1.5, "easeInOut")
print_test("Animation with easing")
a3 = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 2.0)
print_test("Color animation (tuple)")
# Start animations
a1.start(frame)
a2.start(frame)
a3.start(frame)
print_test("Animations started")
# Check properties
print_test(f"Duration = {a1.duration}")
print_test(f"Elapsed = {a1.elapsed}")
print_test(f"Complete = {a1.is_complete}")
except Exception as e:
print_test(f"Animation test failed: {e}", False)
def test_collections():
"""Test collection operations"""
print_section("COLLECTION TESTS")
ui = mcrfpy.sceneUI("test")
try:
# Clear scene
while len(ui) > 0:
ui.remove(ui[len(ui)-1])
print_test(f"Scene cleared, length = {len(ui)}")
# Add items
for i in range(5):
f = mcrfpy.Frame(i*100, 50, 80, 80)
ui.append(f)
print_test(f"Added 5 frames, length = {len(ui)}")
# Access
first = ui[0]
print_test(f"Accessed ui[0] at ({first.x},{first.y})")
# Iteration
count = sum(1 for _ in ui)
print_test(f"Iteration count = {count}")
except Exception as e:
print_test(f"Collection test failed: {e}", False)
def run_tests():
"""Run all tests"""
print("\n" + "="*60)
print(" McRogueFace API Test Suite")
print("="*60)
test_colors()
test_frames()
test_captions()
test_animations()
test_collections()
print("\n" + "="*60)
print(" Tests Complete")
print("="*60)
# Exit after delay
def exit_program(runtime):
print("\nExiting...")
sys.exit(0)
mcrfpy.setTimer("exit", exit_program, 3000)
# Run tests
print("Starting API tests...")
run_tests()

View File

@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Debug the astar_vs_dijkstra demo issue"""
import mcrfpy
import sys
# Same setup as the demo
start_pos = (5, 10)
end_pos = (25, 10)
print("Debugging A* vs Dijkstra demo...")
print(f"Start: {start_pos}, End: {end_pos}")
# Create scene and grid
mcrfpy.createScene("debug")
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
# Initialize all as floor
print("\nInitializing 30x20 grid...")
for y in range(20):
for x in range(30):
grid.at(x, y).walkable = True
# Test path before obstacles
print("\nTest 1: Path with no obstacles")
path1 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
print(f" Path: {path1[:5]}...{path1[-3:] if len(path1) > 5 else ''}")
print(f" Length: {len(path1)}")
# Add obstacles from the demo
obstacles = [
# Vertical wall with gaps
[(15, y) for y in range(3, 17) if y not in [8, 12]],
# Horizontal walls
[(x, 5) for x in range(10, 20)],
[(x, 15) for x in range(10, 20)],
# Maze-like structure
[(x, 10) for x in range(20, 25)],
[(25, y) for y in range(5, 15)],
]
print("\nAdding obstacles...")
wall_count = 0
for obstacle_group in obstacles:
for x, y in obstacle_group:
grid.at(x, y).walkable = False
wall_count += 1
if wall_count <= 5:
print(f" Wall at ({x}, {y})")
print(f" Total walls added: {wall_count}")
# Check specific cells
print(f"\nChecking key positions:")
print(f" Start ({start_pos[0]}, {start_pos[1]}): walkable={grid.at(start_pos[0], start_pos[1]).walkable}")
print(f" End ({end_pos[0]}, {end_pos[1]}): walkable={grid.at(end_pos[0], end_pos[1]).walkable}")
# Check if path is blocked
print(f"\nChecking horizontal line at y=10:")
blocked_x = []
for x in range(30):
if not grid.at(x, 10).walkable:
blocked_x.append(x)
print(f" Blocked x positions: {blocked_x}")
# Test path with obstacles
print("\nTest 2: Path with obstacles")
path2 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
print(f" Path: {path2}")
print(f" Length: {len(path2)}")
# Check if there's any path at all
if not path2:
print("\n No path found! Checking why...")
# Check if we can reach the vertical wall gap
print("\n Testing path to wall gap at (15, 8):")
path_to_gap = grid.compute_astar_path(start_pos[0], start_pos[1], 15, 8)
print(f" Path to gap: {path_to_gap}")
# Check from gap to end
print("\n Testing path from gap (15, 8) to end:")
path_from_gap = grid.compute_astar_path(15, 8, end_pos[0], end_pos[1])
print(f" Path from gap: {path_from_gap}")
# Check walls more carefully
print("\nDetailed wall analysis:")
print(" Walls at x=25 (blocking end?):")
for y in range(5, 15):
print(f" ({25}, {y}): walkable={grid.at(25, y).walkable}")
def timer_cb(dt):
sys.exit(0)
ui = mcrfpy.sceneUI("debug")
ui.append(grid)
mcrfpy.setScene("debug")
mcrfpy.setTimer("exit", timer_cb, 100)

View File

@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
Working Dijkstra Demo with Clear Visual Feedback
================================================
This demo shows pathfinding with high-contrast colors.
"""
import mcrfpy
import sys
# High contrast colors
WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown for walls
FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray for floors
PATH_COLOR = mcrfpy.Color(0, 255, 0) # Pure green for paths
START_COLOR = mcrfpy.Color(255, 0, 0) # Red for start
END_COLOR = mcrfpy.Color(0, 0, 255) # Blue for end
print("Dijkstra Demo - High Contrast")
print("==============================")
# Create scene
mcrfpy.createScene("dijkstra_demo")
# Create grid with exact layout from user
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
grid.fill_color = mcrfpy.Color(0, 0, 0)
# Map layout
map_layout = [
"..............", # Row 0
"..W.....WWWW..", # Row 1
"..W.W...W.EW..", # Row 2
"..W.....W..W..", # Row 3
"..W...E.WWWW..", # Row 4
"E.W...........", # Row 5
"..W...........", # Row 6
"..W...........", # Row 7
"..W.WWW.......", # Row 8
"..............", # Row 9
]
# Create the map
entity_positions = []
for y, row in enumerate(map_layout):
for x, char in enumerate(row):
cell = grid.at(x, y)
if char == 'W':
cell.walkable = False
cell.color = WALL_COLOR
else:
cell.walkable = True
cell.color = FLOOR_COLOR
if char == 'E':
entity_positions.append((x, y))
print(f"Map created: {grid.grid_x}x{grid.grid_y}")
print(f"Entity positions: {entity_positions}")
# Create entities
entities = []
for i, (x, y) in enumerate(entity_positions):
entity = mcrfpy.Entity(x, y)
entity.sprite_index = 49 + i # '1', '2', '3'
grid.entities.append(entity)
entities.append(entity)
print(f"Entity {i+1} at ({x}, {y})")
# Highlight a path immediately
if len(entities) >= 2:
e1, e2 = entities[0], entities[1]
print(f"\nCalculating path from Entity 1 ({e1.x}, {e1.y}) to Entity 2 ({e2.x}, {e2.y})...")
path = e1.path_to(int(e2.x), int(e2.y))
print(f"Path found: {path}")
print(f"Path length: {len(path)} steps")
if path:
print("\nHighlighting path in bright green...")
# Color start and end specially
grid.at(int(e1.x), int(e1.y)).color = START_COLOR
grid.at(int(e2.x), int(e2.y)).color = END_COLOR
# Color the path
for i, (x, y) in enumerate(path):
if i > 0 and i < len(path) - 1: # Skip start and end
grid.at(x, y).color = PATH_COLOR
print(f" Colored ({x}, {y}) green")
# Keypress handler
def handle_keypress(scene_name, keycode):
if keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
print("\nExiting...")
sys.exit(0)
elif keycode == 32: # Space
print("\nRefreshing path colors...")
# Re-color the path to ensure it's visible
if len(entities) >= 2 and path:
for x, y in path[1:-1]:
grid.at(x, y).color = PATH_COLOR
# Set up UI
ui = mcrfpy.sceneUI("dijkstra_demo")
ui.append(grid)
# Scale grid
grid.size = (560, 400) # 14*40, 10*40
grid.position = (120, 100)
# Add title
title = mcrfpy.Caption("Dijkstra Pathfinding - High Contrast", 200, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add legend
legend1 = mcrfpy.Caption("Red=Start, Blue=End, Green=Path", 120, 520)
legend1.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(legend1)
legend2 = mcrfpy.Caption("Press Q to quit, SPACE to refresh", 120, 540)
legend2.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend2)
# Entity info
info = mcrfpy.Caption(f"Path: Entity 1 to 2 = {len(path) if 'path' in locals() else 0} steps", 120, 60)
info.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(info)
# Set up input
mcrfpy.keypressScene(handle_keypress)
mcrfpy.setScene("dijkstra_demo")
print("\nDemo ready! The path should be clearly visible in bright green.")
print("Red = Start, Blue = End, Green = Path")
print("Press SPACE to refresh colors if needed.")

View File

@ -0,0 +1,306 @@
#!/usr/bin/env python3
"""
McRogueFace Exhaustive API Demo (Fixed)
=======================================
Fixed version that properly exits after tests complete.
"""
import mcrfpy
import sys
# Test configuration
VERBOSE = True # Print detailed information about each test
def print_section(title):
"""Print a section header"""
print("\n" + "="*60)
print(f" {title}")
print("="*60)
def print_test(test_name, success=True):
"""Print test result"""
status = "✓ PASS" if success else "✗ FAIL"
print(f" {status} - {test_name}")
def test_color_api():
"""Test all Color constructors and methods"""
print_section("COLOR API TESTS")
# Constructor variants
print("\n Constructors:")
# Empty constructor (defaults to white)
c1 = mcrfpy.Color()
print_test(f"Color() = ({c1.r}, {c1.g}, {c1.b}, {c1.a})")
# Single value (grayscale)
c2 = mcrfpy.Color(128)
print_test(f"Color(128) = ({c2.r}, {c2.g}, {c2.b}, {c2.a})")
# RGB only (alpha defaults to 255)
c3 = mcrfpy.Color(255, 128, 0)
print_test(f"Color(255, 128, 0) = ({c3.r}, {c3.g}, {c3.b}, {c3.a})")
# Full RGBA
c4 = mcrfpy.Color(100, 150, 200, 128)
print_test(f"Color(100, 150, 200, 128) = ({c4.r}, {c4.g}, {c4.b}, {c4.a})")
# Property access
print("\n Properties:")
c = mcrfpy.Color(10, 20, 30, 40)
print_test(f"Initial: r={c.r}, g={c.g}, b={c.b}, a={c.a}")
c.r = 200
c.g = 150
c.b = 100
c.a = 255
print_test(f"After modification: r={c.r}, g={c.g}, b={c.b}, a={c.a}")
return True
def test_frame_api():
"""Test all Frame constructors and methods"""
print_section("FRAME API TESTS")
# Create a test scene
mcrfpy.createScene("api_test")
mcrfpy.setScene("api_test")
ui = mcrfpy.sceneUI("api_test")
# Constructor variants
print("\n Constructors:")
# Empty constructor
f1 = mcrfpy.Frame()
print_test(f"Frame() - pos=({f1.x}, {f1.y}), size=({f1.w}, {f1.h})")
ui.append(f1)
# Position only
f2 = mcrfpy.Frame(100, 50)
print_test(f"Frame(100, 50) - pos=({f2.x}, {f2.y}), size=({f2.w}, {f2.h})")
ui.append(f2)
# Position and size
f3 = mcrfpy.Frame(200, 100, 150, 75)
print_test(f"Frame(200, 100, 150, 75) - pos=({f3.x}, {f3.y}), size=({f3.w}, {f3.h})")
ui.append(f3)
# Full constructor
f4 = mcrfpy.Frame(300, 200, 200, 100,
fill_color=mcrfpy.Color(100, 100, 200),
outline_color=mcrfpy.Color(255, 255, 0),
outline=3)
print_test("Frame with all parameters")
ui.append(f4)
# Properties
print("\n Properties:")
# Position and size
f = mcrfpy.Frame(10, 20, 30, 40)
print_test(f"Initial: x={f.x}, y={f.y}, w={f.w}, h={f.h}")
f.x = 50
f.y = 60
f.w = 70
f.h = 80
print_test(f"Modified: x={f.x}, y={f.y}, w={f.w}, h={f.h}")
# Colors
f.fill_color = mcrfpy.Color(255, 0, 0, 128)
f.outline_color = mcrfpy.Color(0, 255, 0)
f.outline = 5.0
print_test(f"Colors set, outline={f.outline}")
# Visibility and opacity
f.visible = False
f.opacity = 0.5
print_test(f"visible={f.visible}, opacity={f.opacity}")
f.visible = True # Reset
# Z-index
f.z_index = 10
print_test(f"z_index={f.z_index}")
# Children collection
child1 = mcrfpy.Frame(5, 5, 20, 20)
child2 = mcrfpy.Frame(30, 5, 20, 20)
f.children.append(child1)
f.children.append(child2)
print_test(f"children.count = {len(f.children)}")
return True
def test_caption_api():
"""Test all Caption constructors and methods"""
print_section("CAPTION API TESTS")
ui = mcrfpy.sceneUI("api_test")
# Constructor variants
print("\n Constructors:")
# Empty constructor
c1 = mcrfpy.Caption()
print_test(f"Caption() - text='{c1.text}', pos=({c1.x}, {c1.y})")
ui.append(c1)
# Text only
c2 = mcrfpy.Caption("Hello World")
print_test(f"Caption('Hello World') - pos=({c2.x}, {c2.y})")
ui.append(c2)
# Text and position
c3 = mcrfpy.Caption("Positioned Text", 100, 50)
print_test(f"Caption('Positioned Text', 100, 50)")
ui.append(c3)
# Full constructor
c5 = mcrfpy.Caption("Styled Text", 300, 150,
fill_color=mcrfpy.Color(255, 255, 0),
outline_color=mcrfpy.Color(255, 0, 0),
outline=2)
print_test("Caption with all style parameters")
ui.append(c5)
# Properties
print("\n Properties:")
c = mcrfpy.Caption("Test Caption", 10, 20)
# Text
c.text = "Modified Text"
print_test(f"text = '{c.text}'")
# Position
c.x = 50
c.y = 60
print_test(f"position = ({c.x}, {c.y})")
# Colors and style
c.fill_color = mcrfpy.Color(0, 255, 255)
c.outline_color = mcrfpy.Color(255, 0, 255)
c.outline = 3.0
print_test("Colors and outline set")
# Size (read-only, computed from text)
print_test(f"size (computed) = ({c.w}, {c.h})")
return True
def test_animation_api():
"""Test Animation class API"""
print_section("ANIMATION API TESTS")
ui = mcrfpy.sceneUI("api_test")
print("\n Animation Constructors:")
# Basic animation
anim1 = mcrfpy.Animation("x", 100.0, 2.0)
print_test("Animation('x', 100.0, 2.0)")
# With easing
anim2 = mcrfpy.Animation("y", 200.0, 3.0, "easeInOut")
print_test("Animation with easing='easeInOut'")
# Delta mode
anim3 = mcrfpy.Animation("w", 50.0, 1.5, "linear", delta=True)
print_test("Animation with delta=True")
# Color animation (as tuple)
anim4 = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 2.0)
print_test("Animation with Color tuple target")
# Vector animation
anim5 = mcrfpy.Animation("position", (10.0, 20.0), 2.5, "easeOutBounce")
print_test("Animation with position tuple")
# Sprite sequence
anim6 = mcrfpy.Animation("sprite_index", [0, 1, 2, 3, 2, 1], 2.0)
print_test("Animation with sprite sequence")
# Properties
print("\n Animation Properties:")
# Check properties
print_test(f"property = '{anim1.property}'")
print_test(f"duration = {anim1.duration}")
print_test(f"elapsed = {anim1.elapsed}")
print_test(f"is_complete = {anim1.is_complete}")
print_test(f"is_delta = {anim3.is_delta}")
# Methods
print("\n Animation Methods:")
# Create test frame
frame = mcrfpy.Frame(50, 50, 100, 100)
frame.fill_color = mcrfpy.Color(100, 100, 100)
ui.append(frame)
# Start animation
anim1.start(frame)
print_test("start() called on frame")
# Test some easing functions
print("\n Sample Easing Functions:")
easings = ["linear", "easeIn", "easeOut", "easeInOut", "easeInBounce", "easeOutElastic"]
for easing in easings:
try:
test_anim = mcrfpy.Animation("x", 100.0, 1.0, easing)
print_test(f"Easing '{easing}'")
except:
print_test(f"Easing '{easing}' failed", False)
return True
def run_all_tests():
"""Run all API tests"""
print("\n" + "="*60)
print(" McRogueFace Exhaustive API Test Suite (Fixed)")
print(" Testing constructors and methods...")
print("="*60)
# Run each test category
test_functions = [
test_color_api,
test_frame_api,
test_caption_api,
test_animation_api
]
passed = 0
failed = 0
for test_func in test_functions:
try:
if test_func():
passed += 1
else:
failed += 1
except Exception as e:
print(f"\n ERROR in {test_func.__name__}: {e}")
failed += 1
# Summary
print("\n" + "="*60)
print(f" TEST SUMMARY: {passed} passed, {failed} failed")
print("="*60)
print("\n Visual elements are displayed in the 'api_test' scene.")
print(" The test is complete.")
# Exit after a short delay to allow output to be seen
def exit_test(runtime):
print("\nExiting API test suite...")
sys.exit(0)
mcrfpy.setTimer("exit", exit_test, 2000)
# Run the tests immediately
print("Starting McRogueFace Exhaustive API Demo (Fixed)...")
print("This will test constructors and methods.")
run_all_tests()

View File

@ -0,0 +1,391 @@
#!/usr/bin/env python3
"""
Path & Vision Sizzle Reel
=========================
A choreographed demo showing:
- Smooth entity movement along paths
- Camera following with grid center animation
- Field of view updates as entities move
- Dramatic perspective transitions with zoom effects
"""
import mcrfpy
import sys
# Colors
WALL_COLOR = mcrfpy.Color(40, 30, 30)
FLOOR_COLOR = mcrfpy.Color(80, 80, 100)
PATH_COLOR = mcrfpy.Color(120, 120, 180)
DARK_FLOOR = mcrfpy.Color(40, 40, 50)
# Global state
grid = None
player = None
enemy = None
sequence_step = 0
player_path = []
enemy_path = []
player_path_index = 0
enemy_path_index = 0
def create_scene():
"""Create the demo environment"""
global grid, player, enemy
mcrfpy.createScene("path_vision_demo")
# Create larger grid for more dramatic movement
grid = mcrfpy.Grid(grid_x=40, grid_y=25)
grid.fill_color = mcrfpy.Color(20, 20, 30)
# Map layout - interconnected rooms with corridors
map_layout = [
"########################################", # 0
"#......##########......################", # 1
"#......##########......################", # 2
"#......##########......################", # 3
"#......#.........#.....################", # 4
"#......#.........#.....################", # 5
"####.###.........####.#################", # 6
"####.....................##############", # 7
"####.....................##############", # 8
"####.###.........####.#################", # 9
"#......#.........#.....################", # 10
"#......#.........#.....################", # 11
"#......#.........#.....################", # 12
"#......###.....###.....################", # 13
"#......###.....###.....################", # 14
"#......###.....###.....#########......#", # 15
"#......###.....###.....#########......#", # 16
"#......###.....###.....#########......#", # 17
"#####.############.#############......#", # 18
"#####...........................#.....#", # 19
"#####...........................#.....#", # 20
"#####.############.#############......#", # 21
"#......###########.##########.........#", # 22
"#......###########.##########.........#", # 23
"########################################", # 24
]
# Build the map
for y, row in enumerate(map_layout):
for x, char in enumerate(row):
cell = grid.at(x, y)
if char == '#':
cell.walkable = False
cell.transparent = False
cell.color = WALL_COLOR
else:
cell.walkable = True
cell.transparent = True
cell.color = FLOOR_COLOR
# Create player in top-left room
player = mcrfpy.Entity(3, 3, grid=grid)
player.sprite_index = 64 # @
# Create enemy in bottom-right area
enemy = mcrfpy.Entity(35, 20, grid=grid)
enemy.sprite_index = 69 # E
# Initial visibility
player.update_visibility()
enemy.update_visibility()
# Set initial perspective to player
grid.perspective = 0
def setup_paths():
"""Define the paths for entities"""
global player_path, enemy_path
# Player path: Top-left room → corridor → middle room
player_waypoints = [
(3, 3), # Start
(3, 8), # Move down
(7, 8), # Enter corridor
(16, 8), # Through corridor
(16, 12), # Enter middle room
(12, 12), # Move in room
(12, 16), # Move down
(16, 16), # Move right
(16, 19), # Exit room
(25, 19), # Move right
(30, 19), # Continue
(35, 19), # Near enemy start
]
# Enemy path: Bottom-right → around → approach player area
enemy_waypoints = [
(35, 20), # Start
(30, 20), # Move left
(25, 20), # Continue
(20, 20), # Continue
(16, 20), # Corridor junction
(16, 16), # Move up (might see player)
(16, 12), # Continue up
(16, 8), # Top corridor
(10, 8), # Move left
(7, 8), # Continue
(3, 8), # Player's area
(3, 12), # Move down
]
# Calculate full paths using pathfinding
player_path = []
for i in range(len(player_waypoints) - 1):
x1, y1 = player_waypoints[i]
x2, y2 = player_waypoints[i + 1]
# Use grid's A* pathfinding
segment = grid.compute_astar_path(x1, y1, x2, y2)
if segment:
# Add segment (avoiding duplicates)
if not player_path or segment[0] != player_path[-1]:
player_path.extend(segment)
else:
player_path.extend(segment[1:])
enemy_path = []
for i in range(len(enemy_waypoints) - 1):
x1, y1 = enemy_waypoints[i]
x2, y2 = enemy_waypoints[i + 1]
segment = grid.compute_astar_path(x1, y1, x2, y2)
if segment:
if not enemy_path or segment[0] != enemy_path[-1]:
enemy_path.extend(segment)
else:
enemy_path.extend(segment[1:])
print(f"Player path: {len(player_path)} steps")
print(f"Enemy path: {len(enemy_path)} steps")
def setup_ui():
"""Create UI elements"""
ui = mcrfpy.sceneUI("path_vision_demo")
ui.append(grid)
# Position and size grid
grid.position = (50, 80)
grid.size = (700, 500) # Adjust based on zoom
# Title
title = mcrfpy.Caption("Path & Vision Sizzle Reel", 300, 20)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Status
global status_text, perspective_text
status_text = mcrfpy.Caption("Starting demo...", 50, 50)
status_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status_text)
perspective_text = mcrfpy.Caption("Perspective: Player", 550, 50)
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
ui.append(perspective_text)
# Controls
controls = mcrfpy.Caption("Space: Pause/Resume | R: Restart | Q: Quit", 250, 600)
controls.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(controls)
# Animation control
paused = False
move_timer = 0
zoom_transition = False
def move_entity_smooth(entity, target_x, target_y, duration=0.3):
"""Smoothly animate entity to position"""
# Create position animation
anim_x = mcrfpy.Animation("x", float(target_x), duration, "easeInOut")
anim_y = mcrfpy.Animation("y", float(target_y), duration, "easeInOut")
anim_x.start(entity)
anim_y.start(entity)
def update_camera_smooth(center_x, center_y, duration=0.3):
"""Smoothly move camera center"""
# Convert grid coords to pixel coords (assuming 16x16 tiles)
pixel_x = center_x * 16
pixel_y = center_y * 16
anim = mcrfpy.Animation("center", (pixel_x, pixel_y), duration, "easeOut")
anim.start(grid)
def start_perspective_transition():
"""Begin the dramatic perspective shift"""
global zoom_transition, sequence_step
zoom_transition = True
sequence_step = 100 # Special sequence number
status_text.text = "Perspective shift: Zooming out..."
# Zoom out with elastic easing
zoom_out = mcrfpy.Animation("zoom", 0.5, 2.0, "easeInExpo")
zoom_out.start(grid)
# Schedule the perspective switch
mcrfpy.setTimer("switch_perspective", switch_perspective, 2100)
def switch_perspective(dt):
"""Switch perspective at the peak of zoom"""
global sequence_step
# Switch to enemy perspective
grid.perspective = 1
perspective_text.text = "Perspective: Enemy"
perspective_text.fill_color = mcrfpy.Color(255, 100, 100)
status_text.text = "Perspective shift: Following enemy..."
# Update camera to enemy position
update_camera_smooth(enemy.x, enemy.y, 0.1)
# Zoom back in
zoom_in = mcrfpy.Animation("zoom", 1.2, 2.0, "easeOutExpo")
zoom_in.start(grid)
# Resume sequence
mcrfpy.setTimer("resume_enemy", resume_enemy_sequence, 2100)
# Cancel this timer
mcrfpy.delTimer("switch_perspective")
def resume_enemy_sequence(dt):
"""Resume following enemy after perspective shift"""
global sequence_step, zoom_transition
zoom_transition = False
sequence_step = 101 # Continue with enemy movement
mcrfpy.delTimer("resume_enemy")
def sequence_tick(dt):
"""Main sequence controller"""
global sequence_step, player_path_index, enemy_path_index, move_timer
if paused or zoom_transition:
return
move_timer += dt
if move_timer < 400: # Move every 400ms
return
move_timer = 0
if sequence_step < 50:
# Phase 1: Follow player movement
if player_path_index < len(player_path):
x, y = player_path[player_path_index]
move_entity_smooth(player, x, y)
player.update_visibility()
# Camera follows player
if grid.perspective == 0:
update_camera_smooth(player.x, player.y)
player_path_index += 1
status_text.text = f"Player moving... Step {player_path_index}/{len(player_path)}"
# Start enemy movement after player has moved a bit
if player_path_index == 10:
sequence_step = 1 # Enable enemy movement
else:
# Player reached destination, start perspective transition
start_perspective_transition()
if sequence_step >= 1 and sequence_step < 50:
# Phase 2: Enemy movement (concurrent with player)
if enemy_path_index < len(enemy_path):
x, y = enemy_path[enemy_path_index]
move_entity_smooth(enemy, x, y)
enemy.update_visibility()
# Check if enemy is visible to player
if grid.perspective == 0:
enemy_cell_idx = int(enemy.y) * grid.grid_x + int(enemy.x)
if enemy_cell_idx < len(player.gridstate) and player.gridstate[enemy_cell_idx].visible:
status_text.text = "Enemy spotted!"
enemy_path_index += 1
elif sequence_step == 101:
# Phase 3: Continue following enemy after perspective shift
if enemy_path_index < len(enemy_path):
x, y = enemy_path[enemy_path_index]
move_entity_smooth(enemy, x, y)
enemy.update_visibility()
# Camera follows enemy
update_camera_smooth(enemy.x, enemy.y)
enemy_path_index += 1
status_text.text = f"Following enemy... Step {enemy_path_index}/{len(enemy_path)}"
else:
status_text.text = "Demo complete! Press R to restart"
sequence_step = 200 # Done
def handle_keys(key, state):
"""Handle keyboard input"""
global paused, sequence_step, player_path_index, enemy_path_index, move_timer
key = key.lower()
if state != "start":
return
if key == "q":
print("Exiting sizzle reel...")
sys.exit(0)
elif key == "space":
paused = not paused
status_text.text = "PAUSED" if paused else "Running..."
elif key == "r":
# Reset everything
player.x, player.y = 3, 3
enemy.x, enemy.y = 35, 20
player.update_visibility()
enemy.update_visibility()
grid.perspective = 0
perspective_text.text = "Perspective: Player"
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
sequence_step = 0
player_path_index = 0
enemy_path_index = 0
move_timer = 0
update_camera_smooth(player.x, player.y, 0.5)
# Reset zoom
zoom_reset = mcrfpy.Animation("zoom", 1.2, 0.5, "easeOut")
zoom_reset.start(grid)
status_text.text = "Demo restarted!"
# Initialize everything
print("Path & Vision Sizzle Reel")
print("=========================")
print("Demonstrating:")
print("- Smooth entity movement along calculated paths")
print("- Camera following with animated grid centering")
print("- Field of view updates as entities move")
print("- Dramatic perspective transitions with zoom effects")
print()
create_scene()
setup_paths()
setup_ui()
# Set scene and input
mcrfpy.setScene("path_vision_demo")
mcrfpy.keypressScene(handle_keys)
# Initial camera setup
grid.zoom = 1.2
update_camera_smooth(player.x, player.y, 0.1)
# Start the sequence
mcrfpy.setTimer("sequence", sequence_tick, 50) # Tick every 50ms
print("Demo started!")
print("- Player (@) will navigate through rooms")
print("- Enemy (E) will move on a different path")
print("- Watch for the dramatic perspective shift!")
print()
print("Controls: Space=Pause, R=Restart, Q=Quit")

View File

@ -0,0 +1,377 @@
#!/usr/bin/env python3
"""
Pathfinding Showcase Demo
=========================
Demonstrates various pathfinding scenarios with multiple entities.
Features:
- Multiple entities pathfinding simultaneously
- Chase mode: entities pursue targets
- Flee mode: entities avoid threats
- Patrol mode: entities follow waypoints
- Visual debugging: show Dijkstra distance field
"""
import mcrfpy
import sys
import random
# Colors
WALL_COLOR = mcrfpy.Color(40, 40, 40)
FLOOR_COLOR = mcrfpy.Color(220, 220, 240)
PATH_COLOR = mcrfpy.Color(180, 250, 180)
THREAT_COLOR = mcrfpy.Color(255, 100, 100)
GOAL_COLOR = mcrfpy.Color(100, 255, 100)
DIJKSTRA_COLORS = [
mcrfpy.Color(50, 50, 100), # Far
mcrfpy.Color(70, 70, 150),
mcrfpy.Color(90, 90, 200),
mcrfpy.Color(110, 110, 250),
mcrfpy.Color(150, 150, 255),
mcrfpy.Color(200, 200, 255), # Near
]
# Entity types
PLAYER = 64 # @
ENEMY = 69 # E
TREASURE = 36 # $
PATROL = 80 # P
# Global state
grid = None
player = None
enemies = []
treasures = []
patrol_entities = []
mode = "CHASE"
show_dijkstra = False
animation_speed = 3.0
# Track waypoints separately since Entity doesn't have custom attributes
entity_waypoints = {} # entity -> [(x, y), ...]
entity_waypoint_indices = {} # entity -> current index
def create_dungeon():
"""Create a dungeon-like map"""
global grid
mcrfpy.createScene("pathfinding_showcase")
# Create larger grid for showcase
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
grid.fill_color = mcrfpy.Color(0, 0, 0)
# Initialize all as floor
for y in range(20):
for x in range(30):
grid.at(x, y).walkable = True
grid.at(x, y).transparent = True
grid.at(x, y).color = FLOOR_COLOR
# Create rooms and corridors
rooms = [
(2, 2, 8, 6), # Top-left room
(20, 2, 8, 6), # Top-right room
(11, 8, 8, 6), # Center room
(2, 14, 8, 5), # Bottom-left room
(20, 14, 8, 5), # Bottom-right room
]
# Create room walls
for rx, ry, rw, rh in rooms:
# Top and bottom walls
for x in range(rx, rx + rw):
if 0 <= x < 30:
grid.at(x, ry).walkable = False
grid.at(x, ry).color = WALL_COLOR
grid.at(x, ry + rh - 1).walkable = False
grid.at(x, ry + rh - 1).color = WALL_COLOR
# Left and right walls
for y in range(ry, ry + rh):
if 0 <= y < 20:
grid.at(rx, y).walkable = False
grid.at(rx, y).color = WALL_COLOR
grid.at(rx + rw - 1, y).walkable = False
grid.at(rx + rw - 1, y).color = WALL_COLOR
# Create doorways
doorways = [
(6, 2), (24, 2), # Top room doors
(6, 7), (24, 7), # Top room doors bottom
(15, 8), (15, 13), # Center room doors
(6, 14), (24, 14), # Bottom room doors
(11, 11), (18, 11), # Center room side doors
]
for x, y in doorways:
if 0 <= x < 30 and 0 <= y < 20:
grid.at(x, y).walkable = True
grid.at(x, y).color = FLOOR_COLOR
# Add some corridors
# Horizontal corridors
for x in range(10, 20):
grid.at(x, 5).walkable = True
grid.at(x, 5).color = FLOOR_COLOR
grid.at(x, 16).walkable = True
grid.at(x, 16).color = FLOOR_COLOR
# Vertical corridors
for y in range(5, 17):
grid.at(10, y).walkable = True
grid.at(10, y).color = FLOOR_COLOR
grid.at(19, y).walkable = True
grid.at(19, y).color = FLOOR_COLOR
def spawn_entities():
"""Spawn various entity types"""
global player, enemies, treasures, patrol_entities
# Clear existing entities
#grid.entities.clear()
enemies = []
treasures = []
patrol_entities = []
# Spawn player in center room
player = mcrfpy.Entity((15, 11), mcrfpy.default_texture, PLAYER)
grid.entities.append(player)
# Spawn enemies in corners
enemy_positions = [(4, 4), (24, 4), (4, 16), (24, 16)]
for x, y in enemy_positions:
enemy = mcrfpy.Entity((x, y), mcrfpy.default_texture, ENEMY)
grid.entities.append(enemy)
enemies.append(enemy)
# Spawn treasures
treasure_positions = [(6, 5), (24, 5), (15, 10)]
for x, y in treasure_positions:
treasure = mcrfpy.Entity((x, y), mcrfpy.default_texture, TREASURE)
grid.entities.append(treasure)
treasures.append(treasure)
# Spawn patrol entities
patrol = mcrfpy.Entity((10, 10), mcrfpy.default_texture, PATROL)
# Store waypoints separately since Entity doesn't support custom attributes
entity_waypoints[patrol] = [(10, 10), (19, 10), (19, 16), (10, 16)] # Square patrol
entity_waypoint_indices[patrol] = 0
grid.entities.append(patrol)
patrol_entities.append(patrol)
def visualize_dijkstra(target_x, target_y):
"""Visualize Dijkstra distance field"""
if not show_dijkstra:
return
# Compute Dijkstra from target
grid.compute_dijkstra(target_x, target_y)
# Color tiles based on distance
max_dist = 30.0
for y in range(20):
for x in range(30):
if grid.at(x, y).walkable:
dist = grid.get_dijkstra_distance(x, y)
if dist is not None and dist < max_dist:
# Map distance to color index
color_idx = int((dist / max_dist) * len(DIJKSTRA_COLORS))
color_idx = min(color_idx, len(DIJKSTRA_COLORS) - 1)
grid.at(x, y).color = DIJKSTRA_COLORS[color_idx]
def move_enemies(dt):
"""Move enemies based on current mode"""
if mode == "CHASE":
# Enemies chase player
for enemy in enemies:
path = enemy.path_to(int(player.x), int(player.y))
if path and len(path) > 1: # Don't move onto player
# Move towards player
next_x, next_y = path[1]
# Smooth movement
dx = next_x - enemy.x
dy = next_y - enemy.y
enemy.x += dx * dt * animation_speed
enemy.y += dy * dt * animation_speed
elif mode == "FLEE":
# Enemies flee from player
for enemy in enemies:
# Compute opposite direction
dx = enemy.x - player.x
dy = enemy.y - player.y
# Find safe spot in that direction
target_x = int(enemy.x + dx * 2)
target_y = int(enemy.y + dy * 2)
# Clamp to grid
target_x = max(0, min(29, target_x))
target_y = max(0, min(19, target_y))
path = enemy.path_to(target_x, target_y)
if path and len(path) > 0:
next_x, next_y = path[0]
# Move away from player
dx = next_x - enemy.x
dy = next_y - enemy.y
enemy.x += dx * dt * animation_speed
enemy.y += dy * dt * animation_speed
def move_patrols(dt):
"""Move patrol entities along waypoints"""
for patrol in patrol_entities:
if patrol not in entity_waypoints:
continue
# Get current waypoint
waypoints = entity_waypoints[patrol]
waypoint_index = entity_waypoint_indices[patrol]
target_x, target_y = waypoints[waypoint_index]
# Check if reached waypoint
dist = abs(patrol.x - target_x) + abs(patrol.y - target_y)
if dist < 0.5:
# Move to next waypoint
entity_waypoint_indices[patrol] = (waypoint_index + 1) % len(waypoints)
waypoint_index = entity_waypoint_indices[patrol]
target_x, target_y = waypoints[waypoint_index]
# Path to waypoint
path = patrol.path_to(target_x, target_y)
if path and len(path) > 0:
next_x, next_y = path[0]
dx = next_x - patrol.x
dy = next_y - patrol.y
patrol.x += dx * dt * animation_speed * 0.5 # Slower patrol speed
patrol.y += dy * dt * animation_speed * 0.5
def update_entities(dt):
"""Update all entity movements"""
move_enemies(dt / 1000.0) # Convert to seconds
move_patrols(dt / 1000.0)
# Update Dijkstra visualization
if show_dijkstra and player:
visualize_dijkstra(int(player.x), int(player.y))
def handle_keypress(scene_name, keycode):
"""Handle keyboard input"""
global mode, show_dijkstra, player
# Mode switching
if keycode == 49: # '1'
mode = "CHASE"
mode_text.text = "Mode: CHASE - Enemies pursue player"
clear_colors()
elif keycode == 50: # '2'
mode = "FLEE"
mode_text.text = "Mode: FLEE - Enemies avoid player"
clear_colors()
elif keycode == 51: # '3'
mode = "PATROL"
mode_text.text = "Mode: PATROL - Entities follow waypoints"
clear_colors()
# Toggle Dijkstra visualization
elif keycode == 68 or keycode == 100: # 'D' or 'd'
show_dijkstra = not show_dijkstra
debug_text.text = f"Dijkstra Debug: {'ON' if show_dijkstra else 'OFF'}"
if not show_dijkstra:
clear_colors()
# Move player with arrow keys or WASD
elif keycode in [87, 119]: # W/w - Up
if player.y > 0:
path = player.path_to(int(player.x), int(player.y) - 1)
if path:
player.y -= 1
elif keycode in [83, 115]: # S/s - Down
if player.y < 19:
path = player.path_to(int(player.x), int(player.y) + 1)
if path:
player.y += 1
elif keycode in [65, 97]: # A/a - Left
if player.x > 0:
path = player.path_to(int(player.x) - 1, int(player.y))
if path:
player.x -= 1
elif keycode in [68, 100]: # D/d - Right
if player.x < 29:
path = player.path_to(int(player.x) + 1, int(player.y))
if path:
player.x += 1
# Reset
elif keycode == 82 or keycode == 114: # 'R' or 'r'
spawn_entities()
clear_colors()
# Quit
elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
print("\nExiting pathfinding showcase...")
sys.exit(0)
def clear_colors():
"""Reset floor colors"""
for y in range(20):
for x in range(30):
if grid.at(x, y).walkable:
grid.at(x, y).color = FLOOR_COLOR
# Create the showcase
print("Pathfinding Showcase Demo")
print("=========================")
print("Controls:")
print(" WASD - Move player")
print(" 1 - Chase mode (enemies pursue)")
print(" 2 - Flee mode (enemies avoid)")
print(" 3 - Patrol mode")
print(" D - Toggle Dijkstra visualization")
print(" R - Reset entities")
print(" Q/ESC - Quit")
# Create dungeon
create_dungeon()
spawn_entities()
# Set up UI
ui = mcrfpy.sceneUI("pathfinding_showcase")
ui.append(grid)
# Scale and position
grid.size = (750, 500) # 30*25, 20*25
grid.position = (25, 60)
# Add title
title = mcrfpy.Caption("Pathfinding Showcase", 300, 10)
title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title)
# Add mode text
mode_text = mcrfpy.Caption("Mode: CHASE - Enemies pursue player", 25, 580)
mode_text.fill_color = mcrfpy.Color(255, 255, 200)
ui.append(mode_text)
# Add debug text
debug_text = mcrfpy.Caption("Dijkstra Debug: OFF", 25, 600)
debug_text.fill_color = mcrfpy.Color(200, 200, 255)
ui.append(debug_text)
# Add legend
legend = mcrfpy.Caption("@ Player E Enemy $ Treasure P Patrol", 25, 620)
legend.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(legend)
# Set up input handling
mcrfpy.keypressScene(handle_keypress)
# Set up animation timer
mcrfpy.setTimer("entities", update_entities, 16) # 60 FPS
# Show scene
mcrfpy.setScene("pathfinding_showcase")
print("\nShowcase ready! Move with WASD and watch entities react.")

View File

@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""
Simple Text Input Widget for McRogueFace
Minimal implementation focusing on core functionality
"""
import mcrfpy
import sys
class TextInput:
"""Simple text input widget"""
def __init__(self, x, y, width, label=""):
self.x = x
self.y = y
self.width = width
self.label = label
self.text = ""
self.cursor_pos = 0
self.focused = False
# Create UI elements
self.frame = mcrfpy.Frame(self.x, self.y, self.width, 24)
self.frame.fill_color = (255, 255, 255, 255)
self.frame.outline_color = (128, 128, 128, 255)
self.frame.outline = 2
# Label
if self.label:
self.label_caption = mcrfpy.Caption(self.label, self.x, self.y - 20)
self.label_caption.fill_color = (255, 255, 255, 255)
# Text display
self.text_caption = mcrfpy.Caption("", self.x + 4, self.y + 4)
self.text_caption.fill_color = (0, 0, 0, 255)
# Cursor (a simple vertical line using a frame)
self.cursor = mcrfpy.Frame(self.x + 4, self.y + 4, 2, 16)
self.cursor.fill_color = (0, 0, 0, 255)
self.cursor.visible = False
# Click handler
self.frame.click = self._on_click
def _on_click(self, x, y, button):
"""Handle clicks"""
if button == 1: # Left click
# Request focus
global current_focus
if current_focus and current_focus != self:
current_focus.blur()
current_focus = self
self.focus()
def focus(self):
"""Give focus to this input"""
self.focused = True
self.frame.outline_color = (0, 120, 255, 255)
self.frame.outline = 3
self.cursor.visible = True
self._update_cursor()
def blur(self):
"""Remove focus"""
self.focused = False
self.frame.outline_color = (128, 128, 128, 255)
self.frame.outline = 2
self.cursor.visible = False
def handle_key(self, key):
"""Process keyboard input"""
if not self.focused:
return False
if key == "BackSpace":
if self.cursor_pos > 0:
self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:]
self.cursor_pos -= 1
elif key == "Delete":
if self.cursor_pos < len(self.text):
self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:]
elif key == "Left":
self.cursor_pos = max(0, self.cursor_pos - 1)
elif key == "Right":
self.cursor_pos = min(len(self.text), self.cursor_pos + 1)
elif key == "Home":
self.cursor_pos = 0
elif key == "End":
self.cursor_pos = len(self.text)
elif len(key) == 1 and key.isprintable():
self.text = self.text[:self.cursor_pos] + key + self.text[self.cursor_pos:]
self.cursor_pos += 1
else:
return False
self._update_display()
return True
def _update_display(self):
"""Update text display"""
self.text_caption.text = self.text
self._update_cursor()
def _update_cursor(self):
"""Update cursor position"""
if self.focused:
# Estimate character width (roughly 10 pixels per char)
self.cursor.x = self.x + 4 + (self.cursor_pos * 10)
def add_to_scene(self, scene):
"""Add all components to scene"""
scene.append(self.frame)
if hasattr(self, 'label_caption'):
scene.append(self.label_caption)
scene.append(self.text_caption)
scene.append(self.cursor)
# Global focus tracking
current_focus = None
text_inputs = []
def demo_test(timer_name):
"""Run automated demo after scene loads"""
print("\n=== Text Input Widget Demo ===")
# Test typing in first field
print("Testing first input field...")
text_inputs[0].focus()
for char in "Hello":
text_inputs[0].handle_key(char)
print(f"First field contains: '{text_inputs[0].text}'")
# Test second field
print("\nTesting second input field...")
text_inputs[1].focus()
for char in "World":
text_inputs[1].handle_key(char)
print(f"Second field contains: '{text_inputs[1].text}'")
# Test text operations
print("\nTesting cursor movement and deletion...")
text_inputs[1].handle_key("Home")
text_inputs[1].handle_key("Delete")
print(f"After delete at start: '{text_inputs[1].text}'")
text_inputs[1].handle_key("End")
text_inputs[1].handle_key("BackSpace")
print(f"After backspace at end: '{text_inputs[1].text}'")
print("\n=== Demo Complete! ===")
print("Text input widget is working successfully!")
print("Features demonstrated:")
print(" - Text entry")
print(" - Focus management (blue outline)")
print(" - Cursor positioning")
print(" - Delete/Backspace operations")
sys.exit(0)
def create_scene():
"""Create the demo scene"""
global text_inputs
mcrfpy.createScene("demo")
scene = mcrfpy.sceneUI("demo")
# Background
bg = mcrfpy.Frame(0, 0, 800, 600)
bg.fill_color = (40, 40, 40, 255)
scene.append(bg)
# Title
title = mcrfpy.Caption("Text Input Widget Demo", 10, 10)
title.fill_color = (255, 255, 255, 255)
scene.append(title)
# Create input fields
input1 = TextInput(50, 100, 300, "Name:")
input1.add_to_scene(scene)
text_inputs.append(input1)
input2 = TextInput(50, 160, 300, "Email:")
input2.add_to_scene(scene)
text_inputs.append(input2)
input3 = TextInput(50, 220, 400, "Comment:")
input3.add_to_scene(scene)
text_inputs.append(input3)
# Status text
status = mcrfpy.Caption("Click to focus, type to enter text", 50, 280)
status.fill_color = (200, 200, 200, 255)
scene.append(status)
# Keyboard handler
def handle_keys(scene_name, key):
global current_focus, text_inputs
# Tab to switch fields
if key == "Tab" and current_focus:
idx = text_inputs.index(current_focus)
next_idx = (idx + 1) % len(text_inputs)
text_inputs[next_idx]._on_click(0, 0, 1)
else:
# Pass to focused input
if current_focus:
current_focus.handle_key(key)
# Update status
texts = [inp.text for inp in text_inputs]
status.text = f"Values: {texts[0]} | {texts[1]} | {texts[2]}"
mcrfpy.keypressScene("demo", handle_keys)
mcrfpy.setScene("demo")
# Schedule test
mcrfpy.setTimer("test", demo_test, 500)
if __name__ == "__main__":
print("Starting simple text input demo...")
create_scene()

View File

@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""
McRogueFace Animation Sizzle Reel - Final Version
=================================================
Complete demonstration of all animation capabilities.
This version works properly with the game loop and avoids API issues.
WARNING: This demo causes a segmentation fault due to a bug in the
AnimationManager. When UI elements with active animations are removed
from the scene, the AnimationManager crashes when trying to update them.
Use sizzle_reel_final_fixed.py instead, which works around this issue
by hiding objects off-screen instead of removing them.
"""
import mcrfpy
# Configuration
DEMO_DURATION = 6.0 # Duration for each demo
# All available easing functions
EASING_FUNCTIONS = [
"linear", "easeIn", "easeOut", "easeInOut",
"easeInQuad", "easeOutQuad", "easeInOutQuad",
"easeInCubic", "easeOutCubic", "easeInOutCubic",
"easeInQuart", "easeOutQuart", "easeInOutQuart",
"easeInSine", "easeOutSine", "easeInOutSine",
"easeInExpo", "easeOutExpo", "easeInOutExpo",
"easeInCirc", "easeOutCirc", "easeInOutCirc",
"easeInElastic", "easeOutElastic", "easeInOutElastic",
"easeInBack", "easeOutBack", "easeInOutBack",
"easeInBounce", "easeOutBounce", "easeInOutBounce"
]
# Track demo state
current_demo = 0
subtitle = None
def create_scene():
"""Create the demo scene"""
mcrfpy.createScene("demo")
mcrfpy.setScene("demo")
ui = mcrfpy.sceneUI("demo")
# Title
title = mcrfpy.Caption("Animation Sizzle Reel", 500, 20)
title.fill_color = mcrfpy.Color(255, 255, 0)
title.outline = 2
title.font_size = 28
ui.append(title)
# Subtitle
global subtitle
subtitle = mcrfpy.Caption("Starting...", 450, 60)
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(subtitle)
return ui
def demo1_frame_animations():
"""Frame position, size, and color animations"""
ui = mcrfpy.sceneUI("demo")
subtitle.text = "Demo 1: Frame Animations"
# Create frame
f = mcrfpy.Frame(100, 150, 200, 100)
f.fill_color = mcrfpy.Color(50, 50, 150)
f.outline = 3
f.outline_color = mcrfpy.Color(255, 255, 255)
ui.append(f)
# Animate properties
mcrfpy.Animation("x", 600.0, 2.0, "easeInOutBack").start(f)
mcrfpy.Animation("y", 300.0, 2.0, "easeInOutElastic").start(f)
mcrfpy.Animation("w", 300.0, 2.5, "easeInOutCubic").start(f)
mcrfpy.Animation("h", 150.0, 2.5, "easeInOutCubic").start(f)
mcrfpy.Animation("fill_color", (255, 100, 50, 200), 3.0, "easeInOutSine").start(f)
mcrfpy.Animation("outline", 8.0, 3.0, "easeInOutQuad").start(f)
def demo2_caption_animations():
"""Caption movement and text effects"""
ui = mcrfpy.sceneUI("demo")
subtitle.text = "Demo 2: Caption Animations"
# Moving caption
c1 = mcrfpy.Caption("Bouncing Text!", 100, 200)
c1.fill_color = mcrfpy.Color(255, 255, 255)
c1.font_size = 28
ui.append(c1)
mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1)
# Color cycling
c2 = mcrfpy.Caption("Color Cycle", 400, 300)
c2.outline = 2
c2.font_size = 28
ui.append(c2)
mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear").start(c2)
# Typewriter effect
c3 = mcrfpy.Caption("", 100, 400)
c3.fill_color = mcrfpy.Color(0, 255, 255)
c3.font_size = 28
ui.append(c3)
mcrfpy.Animation("text", "Typewriter effect animation...", 3.0, "linear").start(c3)
def demo3_easing_showcase():
"""Show all 30 easing functions"""
ui = mcrfpy.sceneUI("demo")
subtitle.text = "Demo 3: All 30 Easing Functions"
# Create a small frame for each easing
for i, easing in enumerate(EASING_FUNCTIONS[:15]): # First 15
row = i // 5
col = i % 5
x = 100 + col * 200
y = 150 + row * 100
# Frame
f = mcrfpy.Frame(x, y, 20, 20)
f.fill_color = mcrfpy.Color(100, 150, 255)
ui.append(f)
# Label
label = mcrfpy.Caption(easing[:10], x, y - 20)
label.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(label)
# Animate with this easing
mcrfpy.Animation("x", float(x + 150), 3.0, easing).start(f)
def demo4_performance():
"""Many simultaneous animations"""
ui = mcrfpy.sceneUI("demo")
subtitle.text = "Demo 4: 50+ Simultaneous Animations"
for i in range(50):
x = 100 + (i % 10) * 100
y = 150 + (i // 10) * 100
f = mcrfpy.Frame(x, y, 30, 30)
f.fill_color = mcrfpy.Color((i*37)%256, (i*73)%256, (i*113)%256)
ui.append(f)
# Animate to random position
target_x = 150 + (i % 8) * 110
target_y = 200 + (i // 8) * 90
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
mcrfpy.Animation("x", float(target_x), 2.5, easing).start(f)
mcrfpy.Animation("y", float(target_y), 2.5, easing).start(f)
mcrfpy.Animation("opacity", 0.3 + (i%7)*0.1, 2.0, "easeInOutSine").start(f)
def clear_demo_objects():
"""Clear scene except title and subtitle"""
ui = mcrfpy.sceneUI("demo")
# Keep removing items after the first 2 (title and subtitle)
while len(ui) > 2:
# Remove the last item
ui.remove(len(ui)-1)
def next_demo(runtime):
"""Run the next demo"""
global current_demo
clear_demo_objects()
demos = [
demo1_frame_animations,
demo2_caption_animations,
demo3_easing_showcase,
demo4_performance
]
if current_demo < len(demos):
demos[current_demo]()
current_demo += 1
if current_demo < len(demos):
#mcrfpy.setTimer("next", next_demo, int(DEMO_DURATION * 1000))
pass
else:
subtitle.text = "Demo Complete!"
# Initialize
print("Starting Animation Sizzle Reel...")
create_scene()
mcrfpy.setTimer("start", next_demo, int(DEMO_DURATION * 1000))
next_demo(0)

Some files were not shown because too many files have changed in this diff Show More