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