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 commitc5e7e8e298
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 commit6d29652ae7
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 commita010e5fa96
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. commit9c8d6c4591
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 commitdcd1b0ca33
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 commit6813fb5129
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. commit6f67fbb51e
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. commiteb88c7b3aa
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 commit9fb428dd01
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. commit062e4dadc4
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. commit98fc49a978
Directory structure cleanup and organization overhaul
This commit is contained in:
parent
1a143982e1
commit
f4343e1e82
|
@ -30,3 +30,4 @@ scripts/
|
|||
test_*
|
||||
|
||||
tcod_reference
|
||||
.archive
|
||||
|
|
File diff suppressed because it is too large
Load Diff
67
README.md
67
README.md
|
@ -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.
|
||||
|
|
|
@ -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 ..
|
157
css_colors.txt
157
css_colors.txt
|
@ -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
|
|
@ -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
|
@ -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."""
|
||||
...
|
|
@ -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
|
|
@ -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,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})")
|
|
@ -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!")
|
|
@ -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!")
|
|
@ -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!")
|
|
@ -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!")
|
|
@ -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 |
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
};
|
|
@ -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[];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))) {
|
||||
|
|
|
@ -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}
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
};
|
127
src/Timer.cpp
127
src/Timer.cpp
|
@ -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);
|
||||
}
|
47
src/Timer.h
47
src/Timer.h
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
154
src/UIEntity.cpp
154
src/UIEntity.cpp
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
197
src/UIFrame.cpp
197
src/UIFrame.cpp
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
255
src/UIGrid.cpp
255
src/UIGrid.cpp
|
@ -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, ¢er_x, ¢er_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;
|
||||
|
|
70
src/UIGrid.h
70
src/UIGrid.h
|
@ -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,
|
||||
|
|
171
src/UISprite.cpp
171
src/UISprite.cpp
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env python3
|
||||
# Count format string characters
|
||||
|
||||
fmt = "|OOOOfOOifizfffi"
|
||||
print(f"Format string: {fmt}")
|
||||
|
||||
# Remove the | prefix
|
||||
fmt_chars = fmt[1:]
|
||||
print(f"Format chars after |: {fmt_chars}")
|
||||
print(f"Length: {len(fmt_chars)}")
|
||||
|
||||
# Count each type
|
||||
o_count = fmt_chars.count('O')
|
||||
f_count = fmt_chars.count('f')
|
||||
i_count = fmt_chars.count('i')
|
||||
z_count = fmt_chars.count('z')
|
||||
s_count = fmt_chars.count('s')
|
||||
|
||||
print(f"\nCounts:")
|
||||
print(f"O (objects): {o_count}")
|
||||
print(f"f (floats): {f_count}")
|
||||
print(f"i (ints): {i_count}")
|
||||
print(f"z (strings): {z_count}")
|
||||
print(f"s (strings): {s_count}")
|
||||
print(f"Total: {o_count + f_count + i_count + z_count + s_count}")
|
||||
|
||||
# List out each position
|
||||
print("\nPosition by position:")
|
||||
for i, c in enumerate(fmt_chars):
|
||||
print(f"{i+1}: {c}")
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demonstration of animation callbacks solving race conditions.
|
||||
Shows how callbacks enable direct causality for game state changes.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Game state
|
||||
player_moving = False
|
||||
move_queue = []
|
||||
|
||||
def movement_complete(anim, target):
|
||||
"""Called when player movement animation completes"""
|
||||
global player_moving, move_queue
|
||||
|
||||
print("Movement animation completed!")
|
||||
player_moving = False
|
||||
|
||||
# Process next move if queued
|
||||
if move_queue:
|
||||
next_pos = move_queue.pop(0)
|
||||
move_player_to(next_pos)
|
||||
else:
|
||||
print("Player is now idle and ready for input")
|
||||
|
||||
def move_player_to(new_pos):
|
||||
"""Move player with animation and proper state management"""
|
||||
global player_moving
|
||||
|
||||
if player_moving:
|
||||
print(f"Queueing move to {new_pos}")
|
||||
move_queue.append(new_pos)
|
||||
return
|
||||
|
||||
player_moving = True
|
||||
print(f"Moving player to {new_pos}")
|
||||
|
||||
# Get player entity (placeholder for demo)
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
player = ui[0] # Assume first element is player
|
||||
|
||||
# Animate movement with callback
|
||||
x, y = new_pos
|
||||
anim_x = mcrfpy.Animation("x", float(x), 0.5, "easeInOutQuad", callback=movement_complete)
|
||||
anim_y = mcrfpy.Animation("y", float(y), 0.5, "easeInOutQuad")
|
||||
|
||||
anim_x.start(player)
|
||||
anim_y.start(player)
|
||||
|
||||
def setup_demo():
|
||||
"""Set up the demo scene"""
|
||||
# Create scene
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
# Create player sprite
|
||||
player = mcrfpy.Frame((100, 100), (32, 32), fill_color=(0, 255, 0))
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
ui.append(player)
|
||||
|
||||
print("Demo: Animation callbacks for movement queue")
|
||||
print("=" * 40)
|
||||
|
||||
# Simulate rapid movement commands
|
||||
mcrfpy.setTimer("move1", lambda r: move_player_to((200, 100)), 100)
|
||||
mcrfpy.setTimer("move2", lambda r: move_player_to((200, 200)), 200) # Will be queued
|
||||
mcrfpy.setTimer("move3", lambda r: move_player_to((100, 200)), 300) # Will be queued
|
||||
|
||||
# Exit after demo
|
||||
mcrfpy.setTimer("exit", lambda r: exit_demo(), 3000)
|
||||
|
||||
def exit_demo():
|
||||
"""Exit the demo"""
|
||||
print("\nDemo completed successfully!")
|
||||
print("Callbacks ensure proper movement sequencing without race conditions")
|
||||
import sys
|
||||
sys.exit(0)
|
||||
|
||||
# Run the demo
|
||||
setup_demo()
|
|
@ -0,0 +1,146 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Animation Demo - Safe Version
|
||||
=========================================
|
||||
|
||||
A safer, simpler version that demonstrates animations without crashes.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Configuration
|
||||
DEMO_DURATION = 4.0
|
||||
|
||||
# Track state
|
||||
current_demo = 0
|
||||
subtitle = None
|
||||
demo_items = []
|
||||
|
||||
def create_scene():
|
||||
"""Create the demo scene"""
|
||||
mcrfpy.createScene("demo")
|
||||
mcrfpy.setScene("demo")
|
||||
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Animation Demo", 500, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
title.outline = 2
|
||||
ui.append(title)
|
||||
|
||||
# Subtitle
|
||||
global subtitle
|
||||
subtitle = mcrfpy.Caption("Starting...", 450, 60)
|
||||
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(subtitle)
|
||||
|
||||
def clear_demo_items():
|
||||
"""Clear demo items from scene"""
|
||||
global demo_items
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
|
||||
# Remove demo items by tracking what we added
|
||||
for item in demo_items:
|
||||
try:
|
||||
# Find index of item
|
||||
for i in range(len(ui)):
|
||||
if i >= 2: # Skip title and subtitle
|
||||
ui.remove(i)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
demo_items = []
|
||||
|
||||
def demo1_basic():
|
||||
"""Basic frame animations"""
|
||||
global demo_items
|
||||
clear_demo_items()
|
||||
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 1: Basic Frame Animations"
|
||||
|
||||
# Create frame
|
||||
f = mcrfpy.Frame(100, 150, 200, 100)
|
||||
f.fill_color = mcrfpy.Color(50, 50, 150)
|
||||
f.outline = 3
|
||||
ui.append(f)
|
||||
demo_items.append(f)
|
||||
|
||||
# Simple animations
|
||||
mcrfpy.Animation("x", 600.0, 2.0, "easeInOut").start(f)
|
||||
mcrfpy.Animation("w", 300.0, 2.0, "easeInOut").start(f)
|
||||
mcrfpy.Animation("fill_color", (255, 100, 50, 200), 3.0, "linear").start(f)
|
||||
|
||||
def demo2_caption():
|
||||
"""Caption animations"""
|
||||
global demo_items
|
||||
clear_demo_items()
|
||||
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 2: Caption Animations"
|
||||
|
||||
# Moving caption
|
||||
c1 = mcrfpy.Caption("Moving Text!", 100, 200)
|
||||
c1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(c1)
|
||||
demo_items.append(c1)
|
||||
|
||||
mcrfpy.Animation("x", 700.0, 3.0, "easeOutBounce").start(c1)
|
||||
|
||||
# Typewriter
|
||||
c2 = mcrfpy.Caption("", 100, 300)
|
||||
c2.fill_color = mcrfpy.Color(0, 255, 255)
|
||||
ui.append(c2)
|
||||
demo_items.append(c2)
|
||||
|
||||
mcrfpy.Animation("text", "Typewriter effect...", 3.0, "linear").start(c2)
|
||||
|
||||
def demo3_multiple():
|
||||
"""Multiple animations"""
|
||||
global demo_items
|
||||
clear_demo_items()
|
||||
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 3: Multiple Animations"
|
||||
|
||||
# Create several frames
|
||||
for i in range(5):
|
||||
f = mcrfpy.Frame(100 + i * 120, 200, 80, 80)
|
||||
f.fill_color = mcrfpy.Color(50 + i * 40, 100, 200 - i * 30)
|
||||
ui.append(f)
|
||||
demo_items.append(f)
|
||||
|
||||
# Animate each differently
|
||||
target_y = 350 + i * 20
|
||||
mcrfpy.Animation("y", float(target_y), 2.0, "easeInOut").start(f)
|
||||
mcrfpy.Animation("opacity", 0.5, 3.0, "easeInOut").start(f)
|
||||
|
||||
def run_next_demo(runtime):
|
||||
"""Run the next demo"""
|
||||
global current_demo
|
||||
|
||||
demos = [demo1_basic, demo2_caption, demo3_multiple]
|
||||
|
||||
if current_demo < len(demos):
|
||||
demos[current_demo]()
|
||||
current_demo += 1
|
||||
|
||||
if current_demo < len(demos):
|
||||
mcrfpy.setTimer("next", run_next_demo, int(DEMO_DURATION * 1000))
|
||||
else:
|
||||
subtitle.text = "Demo Complete!"
|
||||
# Exit after a delay
|
||||
def exit_program(rt):
|
||||
print("Demo finished successfully!")
|
||||
sys.exit(0)
|
||||
mcrfpy.setTimer("exit", exit_program, 2000)
|
||||
|
||||
# Initialize
|
||||
print("Starting Safe Animation Demo...")
|
||||
create_scene()
|
||||
|
||||
# Start demos
|
||||
mcrfpy.setTimer("start", run_next_demo, 500)
|
|
@ -0,0 +1,616 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Animation Sizzle Reel
|
||||
=================================
|
||||
|
||||
This script demonstrates EVERY animation type on EVERY UI object type.
|
||||
It showcases all 30 easing functions, all animatable properties, and
|
||||
special animation modes (delta, sprite sequences, text effects).
|
||||
|
||||
The script creates a comprehensive visual demonstration of the animation
|
||||
system's capabilities, cycling through different objects and effects.
|
||||
|
||||
Author: Claude
|
||||
Purpose: Complete animation system demonstration
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
from mcrfpy import Color, Frame, Caption, Sprite, Grid, Entity, Texture, Animation
|
||||
import sys
|
||||
import math
|
||||
|
||||
# Configuration
|
||||
SCENE_WIDTH = 1280
|
||||
SCENE_HEIGHT = 720
|
||||
DEMO_DURATION = 5.0 # Duration for each demo section
|
||||
|
||||
# All available easing functions
|
||||
EASING_FUNCTIONS = [
|
||||
"linear", "easeIn", "easeOut", "easeInOut",
|
||||
"easeInQuad", "easeOutQuad", "easeInOutQuad",
|
||||
"easeInCubic", "easeOutCubic", "easeInOutCubic",
|
||||
"easeInQuart", "easeOutQuart", "easeInOutQuart",
|
||||
"easeInSine", "easeOutSine", "easeInOutSine",
|
||||
"easeInExpo", "easeOutExpo", "easeInOutExpo",
|
||||
"easeInCirc", "easeOutCirc", "easeInOutCirc",
|
||||
"easeInElastic", "easeOutElastic", "easeInOutElastic",
|
||||
"easeInBack", "easeOutBack", "easeInOutBack",
|
||||
"easeInBounce", "easeOutBounce", "easeInOutBounce"
|
||||
]
|
||||
|
||||
# Track current demo state
|
||||
current_demo = 0
|
||||
demo_start_time = 0
|
||||
demos = []
|
||||
|
||||
# Handle ESC key to exit
|
||||
def handle_keypress(scene_name, keycode):
|
||||
if keycode == 256: # ESC key
|
||||
print("Exiting animation sizzle reel...")
|
||||
sys.exit(0)
|
||||
|
||||
def create_demo_scene():
|
||||
"""Create the main demo scene with title"""
|
||||
mcrfpy.createScene("sizzle_reel")
|
||||
mcrfpy.setScene("sizzle_reel")
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
|
||||
# Title caption
|
||||
title = Caption("McRogueFace Animation Sizzle Reel",
|
||||
SCENE_WIDTH/2 - 200, 20)
|
||||
title.fill_color = Color(255, 255, 0)
|
||||
title.outline = 2
|
||||
title.outline_color = Color(0, 0, 0)
|
||||
ui.append(title)
|
||||
|
||||
# Subtitle showing current demo
|
||||
global subtitle
|
||||
subtitle = Caption("Initializing...",
|
||||
SCENE_WIDTH/2 - 150, 60)
|
||||
subtitle.fill_color = Color(200, 200, 200)
|
||||
ui.append(subtitle)
|
||||
|
||||
return ui
|
||||
|
||||
def demo_frame_basic_animations(ui):
|
||||
"""Demo 1: Basic frame animations - position, size, colors"""
|
||||
subtitle.text = "Demo 1: Frame Basic Animations (Position, Size, Colors)"
|
||||
|
||||
# Create test frame
|
||||
frame = Frame(100, 150, 200, 100)
|
||||
frame.fill_color = Color(50, 50, 150)
|
||||
frame.outline = 3
|
||||
frame.outline_color = Color(255, 255, 255)
|
||||
ui.append(frame)
|
||||
|
||||
# Position animations with different easings
|
||||
x_anim = Animation("x", 800.0, 2.0, "easeInOutBack")
|
||||
y_anim = Animation("y", 400.0, 2.0, "easeInOutElastic")
|
||||
x_anim.start(frame)
|
||||
y_anim.start(frame)
|
||||
|
||||
# Size animations
|
||||
w_anim = Animation("w", 400.0, 3.0, "easeInOutCubic")
|
||||
h_anim = Animation("h", 200.0, 3.0, "easeInOutCubic")
|
||||
w_anim.start(frame)
|
||||
h_anim.start(frame)
|
||||
|
||||
# Color animations - use tuples instead of Color objects
|
||||
fill_anim = Animation("fill_color", (255, 100, 50, 200), 4.0, "easeInOutSine")
|
||||
outline_anim = Animation("outline_color", (0, 255, 255, 255), 4.0, "easeOutBounce")
|
||||
fill_anim.start(frame)
|
||||
outline_anim.start(frame)
|
||||
|
||||
# Outline thickness animation
|
||||
thickness_anim = Animation("outline", 10.0, 4.5, "easeInOutQuad")
|
||||
thickness_anim.start(frame)
|
||||
|
||||
return frame
|
||||
|
||||
def demo_frame_opacity_zindex(ui):
|
||||
"""Demo 2: Frame opacity and z-index animations"""
|
||||
subtitle.text = "Demo 2: Frame Opacity & Z-Index Animations"
|
||||
|
||||
frames = []
|
||||
colors = [
|
||||
Color(255, 0, 0, 200),
|
||||
Color(0, 255, 0, 200),
|
||||
Color(0, 0, 255, 200),
|
||||
Color(255, 255, 0, 200)
|
||||
]
|
||||
|
||||
# Create overlapping frames
|
||||
for i in range(4):
|
||||
frame = Frame(200 + i*80, 200 + i*40, 200, 150)
|
||||
frame.fill_color = colors[i]
|
||||
frame.outline = 2
|
||||
frame.z_index = i
|
||||
ui.append(frame)
|
||||
frames.append(frame)
|
||||
|
||||
# Animate opacity in waves
|
||||
opacity_anim = Animation("opacity", 0.3, 2.0, "easeInOutSine")
|
||||
opacity_anim.start(frame)
|
||||
|
||||
# Reverse opacity animation
|
||||
opacity_back = Animation("opacity", 1.0, 2.0, "easeInOutSine", delta=False)
|
||||
mcrfpy.setTimer(f"opacity_back_{i}", lambda t, f=frame, a=opacity_back: a.start(f), 2000)
|
||||
|
||||
# Z-index shuffle animation
|
||||
z_anim = Animation("z_index", (i + 2) % 4, 3.0, "linear")
|
||||
z_anim.start(frame)
|
||||
|
||||
return frames
|
||||
|
||||
def demo_caption_animations(ui):
|
||||
"""Demo 3: Caption text animations and effects"""
|
||||
subtitle.text = "Demo 3: Caption Animations (Text, Color, Position)"
|
||||
|
||||
# Basic caption with position animation
|
||||
caption1 = Caption("Moving Text!", 100, 200)
|
||||
caption1.fill_color = Color(255, 255, 255)
|
||||
caption1.outline = 1
|
||||
ui.append(caption1)
|
||||
|
||||
# Animate across screen with bounce
|
||||
x_anim = Animation("x", 900.0, 3.0, "easeOutBounce")
|
||||
x_anim.start(caption1)
|
||||
|
||||
# Color cycling caption
|
||||
caption2 = Caption("Rainbow Colors", 400, 300)
|
||||
caption2.outline = 2
|
||||
ui.append(caption2)
|
||||
|
||||
# Cycle through colors - use tuples
|
||||
color_anim1 = Animation("fill_color", (255, 0, 0, 255), 1.0, "linear")
|
||||
color_anim2 = Animation("fill_color", (0, 255, 0, 255), 1.0, "linear")
|
||||
color_anim3 = Animation("fill_color", (0, 0, 255, 255), 1.0, "linear")
|
||||
color_anim4 = Animation("fill_color", (255, 255, 255, 255), 1.0, "linear")
|
||||
|
||||
color_anim1.start(caption2)
|
||||
mcrfpy.setTimer("color2", lambda t: color_anim2.start(caption2), 1000)
|
||||
mcrfpy.setTimer("color3", lambda t: color_anim3.start(caption2), 2000)
|
||||
mcrfpy.setTimer("color4", lambda t: color_anim4.start(caption2), 3000)
|
||||
|
||||
# Typewriter effect caption
|
||||
caption3 = Caption("", 100, 400)
|
||||
caption3.fill_color = Color(0, 255, 255)
|
||||
ui.append(caption3)
|
||||
|
||||
typewriter = Animation("text", "This text appears one character at a time...", 3.0, "linear")
|
||||
typewriter.start(caption3)
|
||||
|
||||
# Size animation caption
|
||||
caption4 = Caption("Growing Text", 400, 500)
|
||||
caption4.fill_color = Color(255, 200, 0)
|
||||
ui.append(caption4)
|
||||
|
||||
# Note: size animation would require font size property support
|
||||
# For now, animate position to simulate growth
|
||||
scale_sim = Animation("y", 480.0, 2.0, "easeInOutElastic")
|
||||
scale_sim.start(caption4)
|
||||
|
||||
return [caption1, caption2, caption3, caption4]
|
||||
|
||||
def demo_sprite_animations(ui):
|
||||
"""Demo 4: Sprite animations including sprite sequences"""
|
||||
subtitle.text = "Demo 4: Sprite Animations (Position, Scale, Sprite Sequences)"
|
||||
|
||||
# Load a test texture (you'll need to adjust path)
|
||||
try:
|
||||
texture = Texture("assets/sprites/player.png", grid_size=(32, 32))
|
||||
except:
|
||||
# Fallback if texture not found
|
||||
texture = None
|
||||
|
||||
if texture:
|
||||
# Basic sprite with position animation
|
||||
sprite1 = Sprite(100, 200, texture, sprite_index=0)
|
||||
sprite1.scale = 2.0
|
||||
ui.append(sprite1)
|
||||
|
||||
# Circular motion using sin/cos animations
|
||||
# We'll use delta mode to create circular motion
|
||||
x_circle = Animation("x", 300.0, 4.0, "easeInOutSine")
|
||||
y_circle = Animation("y", 300.0, 4.0, "easeInOutCubic")
|
||||
x_circle.start(sprite1)
|
||||
y_circle.start(sprite1)
|
||||
|
||||
# Sprite sequence animation (walking cycle)
|
||||
sprite2 = Sprite(500, 300, texture, sprite_index=0)
|
||||
sprite2.scale = 3.0
|
||||
ui.append(sprite2)
|
||||
|
||||
# Animate through sprite indices for animation
|
||||
walk_cycle = Animation("sprite_index", [0, 1, 2, 3, 2, 1], 2.0, "linear")
|
||||
walk_cycle.start(sprite2)
|
||||
|
||||
# Scale pulsing sprite
|
||||
sprite3 = Sprite(800, 400, texture, sprite_index=4)
|
||||
ui.append(sprite3)
|
||||
|
||||
# Note: scale animation would need to be supported
|
||||
# For now use position to simulate
|
||||
pulse_y = Animation("y", 380.0, 0.5, "easeInOutSine")
|
||||
pulse_y.start(sprite3)
|
||||
|
||||
# Z-index animation for layering
|
||||
sprite3_z = Animation("z_index", 10, 2.0, "linear")
|
||||
sprite3_z.start(sprite3)
|
||||
|
||||
return [sprite1, sprite2, sprite3]
|
||||
else:
|
||||
# Create placeholder caption if no texture
|
||||
no_texture = Caption("(Sprite demo requires texture file)", 400, 350)
|
||||
no_texture.fill_color = Color(255, 100, 100)
|
||||
ui.append(no_texture)
|
||||
return [no_texture]
|
||||
|
||||
def demo_grid_animations(ui):
|
||||
"""Demo 5: Grid animations (position, camera, zoom)"""
|
||||
subtitle.text = "Demo 5: Grid Animations (Position, Camera Effects)"
|
||||
|
||||
# Create a grid
|
||||
try:
|
||||
texture = Texture("assets/sprites/tiles.png", grid_size=(16, 16))
|
||||
except:
|
||||
texture = None
|
||||
|
||||
# Grid constructor: Grid(grid_x, grid_y, texture, position, size)
|
||||
# Note: tile dimensions are determined by texture's grid_size
|
||||
grid = Grid(20, 15, texture, (100, 150), (480, 360)) # 20x24, 15x24
|
||||
grid.fill_color = Color(20, 20, 40)
|
||||
ui.append(grid)
|
||||
|
||||
# Fill with some test pattern
|
||||
for y in range(15):
|
||||
for x in range(20):
|
||||
point = grid.at(x, y)
|
||||
point.tilesprite = (x + y) % 4
|
||||
point.walkable = ((x + y) % 3) != 0
|
||||
if not point.walkable:
|
||||
point.color = Color(100, 50, 50, 128)
|
||||
|
||||
# Animate grid position
|
||||
grid_x = Animation("x", 400.0, 3.0, "easeInOutBack")
|
||||
grid_x.start(grid)
|
||||
|
||||
# Camera pan animation (if supported)
|
||||
# center_x = Animation("center", (10.0, 7.5), 4.0, "easeInOutCubic")
|
||||
# center_x.start(grid)
|
||||
|
||||
# Create entities in the grid
|
||||
if texture:
|
||||
entity1 = Entity((5.0, 5.0), texture, 8) # position tuple, texture, sprite_index
|
||||
entity1.scale = 1.5
|
||||
grid.entities.append(entity1)
|
||||
|
||||
# Animate entity movement
|
||||
entity_pos = Animation("position", (15.0, 10.0), 3.0, "easeInOutQuad")
|
||||
entity_pos.start(entity1)
|
||||
|
||||
# Create patrolling entity
|
||||
entity2 = Entity((10.0, 2.0), texture, 12) # position tuple, texture, sprite_index
|
||||
grid.entities.append(entity2)
|
||||
|
||||
# Animate sprite changes
|
||||
entity2_sprite = Animation("sprite_index", [12, 13, 14, 15, 14, 13], 2.0, "linear")
|
||||
entity2_sprite.start(entity2)
|
||||
|
||||
return grid
|
||||
|
||||
def demo_complex_combinations(ui):
|
||||
"""Demo 6: Complex multi-property animations"""
|
||||
subtitle.text = "Demo 6: Complex Multi-Property Animations"
|
||||
|
||||
# Create a complex UI composition
|
||||
main_frame = Frame(200, 200, 400, 300)
|
||||
main_frame.fill_color = Color(30, 30, 60, 200)
|
||||
main_frame.outline = 2
|
||||
ui.append(main_frame)
|
||||
|
||||
# Child elements
|
||||
title = Caption("Multi-Animation Demo", 20, 20)
|
||||
title.fill_color = Color(255, 255, 255)
|
||||
main_frame.children.append(title)
|
||||
|
||||
# Animate everything at once
|
||||
# Frame animations
|
||||
frame_x = Animation("x", 600.0, 3.0, "easeInOutElastic")
|
||||
frame_w = Animation("w", 300.0, 2.5, "easeOutBack")
|
||||
frame_fill = Animation("fill_color", (60, 30, 90, 220), 4.0, "easeInOutSine")
|
||||
frame_outline = Animation("outline", 8.0, 3.0, "easeInOutQuad")
|
||||
|
||||
frame_x.start(main_frame)
|
||||
frame_w.start(main_frame)
|
||||
frame_fill.start(main_frame)
|
||||
frame_outline.start(main_frame)
|
||||
|
||||
# Title animations
|
||||
title_color = Animation("fill_color", (255, 200, 0, 255), 2.0, "easeOutBounce")
|
||||
title_color.start(title)
|
||||
|
||||
# Add animated sub-frames
|
||||
for i in range(3):
|
||||
sub_frame = Frame(50 + i * 100, 100, 80, 80)
|
||||
sub_frame.fill_color = Color(100 + i*50, 50, 200 - i*50, 180)
|
||||
main_frame.children.append(sub_frame)
|
||||
|
||||
# Rotate positions using delta animations
|
||||
sub_y = Animation("y", 50.0, 2.0, "easeInOutSine", delta=True)
|
||||
sub_y.start(sub_frame)
|
||||
|
||||
return main_frame
|
||||
|
||||
def demo_easing_showcase(ui):
|
||||
"""Demo 7: Showcase all 30 easing functions"""
|
||||
subtitle.text = "Demo 7: All 30 Easing Functions Showcase"
|
||||
|
||||
# Create small frames for each easing function
|
||||
frames_per_row = 6
|
||||
frame_size = 180
|
||||
spacing = 10
|
||||
|
||||
for i, easing in enumerate(EASING_FUNCTIONS[:12]): # First 12 easings
|
||||
row = i // frames_per_row
|
||||
col = i % frames_per_row
|
||||
|
||||
x = 50 + col * (frame_size + spacing)
|
||||
y = 150 + row * (60 + spacing)
|
||||
|
||||
# Create indicator frame
|
||||
frame = Frame(x, y, 20, 20)
|
||||
frame.fill_color = Color(100, 200, 255)
|
||||
frame.outline = 1
|
||||
ui.append(frame)
|
||||
|
||||
# Label
|
||||
label = Caption(easing, x, y - 20)
|
||||
label.fill_color = Color(200, 200, 200)
|
||||
ui.append(label)
|
||||
|
||||
# Animate using this easing
|
||||
move_anim = Animation("x", x + frame_size - 20, 3.0, easing)
|
||||
move_anim.start(frame)
|
||||
|
||||
# Continue with remaining easings after a delay
|
||||
def show_more_easings(runtime):
|
||||
for j, easing in enumerate(EASING_FUNCTIONS[12:24]): # Next 12
|
||||
row = j // frames_per_row + 2
|
||||
col = j % frames_per_row
|
||||
|
||||
x = 50 + col * (frame_size + spacing)
|
||||
y = 150 + row * (60 + spacing)
|
||||
|
||||
frame2 = Frame(x, y, 20, 20)
|
||||
frame2.fill_color = Color(255, 150, 100)
|
||||
frame2.outline = 1
|
||||
ui.append(frame2)
|
||||
|
||||
label2 = Caption(easing, x, y - 20)
|
||||
label2.fill_color = Color(200, 200, 200)
|
||||
ui.append(label2)
|
||||
|
||||
move_anim2 = Animation("x", x + frame_size - 20, 3.0, easing)
|
||||
move_anim2.start(frame2)
|
||||
|
||||
mcrfpy.setTimer("more_easings", show_more_easings, 1000)
|
||||
|
||||
# Show final easings
|
||||
def show_final_easings(runtime):
|
||||
for k, easing in enumerate(EASING_FUNCTIONS[24:]): # Last 6
|
||||
row = k // frames_per_row + 4
|
||||
col = k % frames_per_row
|
||||
|
||||
x = 50 + col * (frame_size + spacing)
|
||||
y = 150 + row * (60 + spacing)
|
||||
|
||||
frame3 = Frame(x, y, 20, 20)
|
||||
frame3.fill_color = Color(150, 255, 150)
|
||||
frame3.outline = 1
|
||||
ui.append(frame3)
|
||||
|
||||
label3 = Caption(easing, x, y - 20)
|
||||
label3.fill_color = Color(200, 200, 200)
|
||||
ui.append(label3)
|
||||
|
||||
move_anim3 = Animation("x", x + frame_size - 20, 3.0, easing)
|
||||
move_anim3.start(frame3)
|
||||
|
||||
mcrfpy.setTimer("final_easings", show_final_easings, 2000)
|
||||
|
||||
def demo_delta_animations(ui):
|
||||
"""Demo 8: Delta mode animations (relative movements)"""
|
||||
subtitle.text = "Demo 8: Delta Mode Animations (Relative Movements)"
|
||||
|
||||
# Create objects that will move relative to their position
|
||||
frames = []
|
||||
start_positions = [(100, 200), (300, 200), (500, 200), (700, 200)]
|
||||
colors = [Color(255, 100, 100), Color(100, 255, 100),
|
||||
Color(100, 100, 255), Color(255, 255, 100)]
|
||||
|
||||
for i, (x, y) in enumerate(start_positions):
|
||||
frame = Frame(x, y, 80, 80)
|
||||
frame.fill_color = colors[i]
|
||||
frame.outline = 2
|
||||
ui.append(frame)
|
||||
frames.append(frame)
|
||||
|
||||
# Delta animations - move relative to current position
|
||||
# Each frame moves by different amounts
|
||||
dx = (i + 1) * 50
|
||||
dy = math.sin(i) * 100
|
||||
|
||||
x_delta = Animation("x", dx, 2.0, "easeInOutBack", delta=True)
|
||||
y_delta = Animation("y", dy, 2.0, "easeInOutElastic", delta=True)
|
||||
|
||||
x_delta.start(frame)
|
||||
y_delta.start(frame)
|
||||
|
||||
# Create caption showing delta mode
|
||||
delta_label = Caption("Delta mode: Relative animations from current position", 200, 400)
|
||||
delta_label.fill_color = Color(255, 255, 255)
|
||||
ui.append(delta_label)
|
||||
|
||||
# Animate the label with delta mode text append
|
||||
text_delta = Animation("text", " - ANIMATED!", 2.0, "linear", delta=True)
|
||||
text_delta.start(delta_label)
|
||||
|
||||
return frames
|
||||
|
||||
def demo_color_component_animations(ui):
|
||||
"""Demo 9: Individual color channel animations"""
|
||||
subtitle.text = "Demo 9: Color Component Animations (R, G, B, A channels)"
|
||||
|
||||
# Create frames to demonstrate individual color channel animations
|
||||
base_frame = Frame(300, 200, 600, 300)
|
||||
base_frame.fill_color = Color(128, 128, 128, 255)
|
||||
base_frame.outline = 3
|
||||
ui.append(base_frame)
|
||||
|
||||
# Labels for each channel
|
||||
labels = ["Red", "Green", "Blue", "Alpha"]
|
||||
positions = [(50, 50), (200, 50), (350, 50), (500, 50)]
|
||||
|
||||
for i, (label_text, (x, y)) in enumerate(zip(labels, positions)):
|
||||
# Create label
|
||||
label = Caption(label_text, x, y - 30)
|
||||
label.fill_color = Color(255, 255, 255)
|
||||
base_frame.children.append(label)
|
||||
|
||||
# Create demo frame for this channel
|
||||
demo_frame = Frame(x, y, 100, 100)
|
||||
demo_frame.fill_color = Color(100, 100, 100, 200)
|
||||
demo_frame.outline = 2
|
||||
base_frame.children.append(demo_frame)
|
||||
|
||||
# Animate individual color channel
|
||||
if i == 0: # Red
|
||||
r_anim = Animation("fill_color.r", 255, 3.0, "easeInOutSine")
|
||||
r_anim.start(demo_frame)
|
||||
elif i == 1: # Green
|
||||
g_anim = Animation("fill_color.g", 255, 3.0, "easeInOutSine")
|
||||
g_anim.start(demo_frame)
|
||||
elif i == 2: # Blue
|
||||
b_anim = Animation("fill_color.b", 255, 3.0, "easeInOutSine")
|
||||
b_anim.start(demo_frame)
|
||||
else: # Alpha
|
||||
a_anim = Animation("fill_color.a", 50, 3.0, "easeInOutSine")
|
||||
a_anim.start(demo_frame)
|
||||
|
||||
# Animate main frame outline color components in sequence
|
||||
outline_r = Animation("outline_color.r", 255, 1.0, "linear")
|
||||
outline_g = Animation("outline_color.g", 255, 1.0, "linear")
|
||||
outline_b = Animation("outline_color.b", 0, 1.0, "linear")
|
||||
|
||||
outline_r.start(base_frame)
|
||||
mcrfpy.setTimer("outline_g", lambda t: outline_g.start(base_frame), 1000)
|
||||
mcrfpy.setTimer("outline_b", lambda t: outline_b.start(base_frame), 2000)
|
||||
|
||||
return base_frame
|
||||
|
||||
def demo_performance_stress_test(ui):
|
||||
"""Demo 10: Performance test with many simultaneous animations"""
|
||||
subtitle.text = "Demo 10: Performance Stress Test (100+ Simultaneous Animations)"
|
||||
|
||||
# Create many small objects with different animations
|
||||
num_objects = 100
|
||||
|
||||
for i in range(num_objects):
|
||||
# Random starting position
|
||||
x = 100 + (i % 20) * 50
|
||||
y = 150 + (i // 20) * 50
|
||||
|
||||
# Create small frame
|
||||
size = 20 + (i % 3) * 10
|
||||
frame = Frame(x, y, size, size)
|
||||
|
||||
# Random color
|
||||
r = (i * 37) % 256
|
||||
g = (i * 73) % 256
|
||||
b = (i * 113) % 256
|
||||
frame.fill_color = Color(r, g, b, 200)
|
||||
frame.outline = 1
|
||||
ui.append(frame)
|
||||
|
||||
# Random animation properties
|
||||
target_x = 100 + (i % 15) * 70
|
||||
target_y = 150 + (i // 15) * 70
|
||||
duration = 2.0 + (i % 30) * 0.1
|
||||
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
|
||||
|
||||
# Start multiple animations per object
|
||||
x_anim = Animation("x", target_x, duration, easing)
|
||||
y_anim = Animation("y", target_y, duration, easing)
|
||||
opacity_anim = Animation("opacity", 0.3 + (i % 7) * 0.1, duration, "easeInOutSine")
|
||||
|
||||
x_anim.start(frame)
|
||||
y_anim.start(frame)
|
||||
opacity_anim.start(frame)
|
||||
|
||||
# Performance counter
|
||||
perf_caption = Caption(f"Animating {num_objects * 3} properties simultaneously", 400, 600)
|
||||
perf_caption.fill_color = Color(255, 255, 0)
|
||||
ui.append(perf_caption)
|
||||
|
||||
def next_demo(runtime):
|
||||
"""Cycle to the next demo"""
|
||||
global current_demo, demo_start_time
|
||||
|
||||
# Clear the UI except title and subtitle
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
|
||||
# Keep only the first two elements (title and subtitle)
|
||||
while len(ui) > 2:
|
||||
# Remove from the end to avoid index issues
|
||||
ui.remove(len(ui) - 1)
|
||||
|
||||
# Run the next demo
|
||||
if current_demo < len(demos):
|
||||
demos[current_demo](ui)
|
||||
current_demo += 1
|
||||
|
||||
# Schedule next demo
|
||||
if current_demo < len(demos):
|
||||
mcrfpy.setTimer("next_demo", next_demo, int(DEMO_DURATION * 1000))
|
||||
else:
|
||||
# All demos complete
|
||||
subtitle.text = "Animation Showcase Complete! Press ESC to exit."
|
||||
complete = Caption("All animation types demonstrated!", 400, 350)
|
||||
complete.fill_color = Color(0, 255, 0)
|
||||
complete.outline = 2
|
||||
ui.append(complete)
|
||||
|
||||
def run_sizzle_reel(runtime):
|
||||
"""Main entry point - start the demo sequence"""
|
||||
global demos
|
||||
|
||||
# List of all demo functions
|
||||
demos = [
|
||||
demo_frame_basic_animations,
|
||||
demo_frame_opacity_zindex,
|
||||
demo_caption_animations,
|
||||
demo_sprite_animations,
|
||||
demo_grid_animations,
|
||||
demo_complex_combinations,
|
||||
demo_easing_showcase,
|
||||
demo_delta_animations,
|
||||
demo_color_component_animations,
|
||||
demo_performance_stress_test
|
||||
]
|
||||
|
||||
# Start the first demo
|
||||
next_demo(runtime)
|
||||
|
||||
# Initialize scene
|
||||
ui = create_demo_scene()
|
||||
|
||||
|
||||
# Start the sizzle reel after a short delay
|
||||
mcrfpy.setTimer("start_sizzle", run_sizzle_reel, 500)
|
||||
|
||||
print("Starting McRogueFace Animation Sizzle Reel...")
|
||||
print("This will demonstrate ALL animation types on ALL objects.")
|
||||
print("Press ESC at any time to exit.")
|
|
@ -0,0 +1,227 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Animation Sizzle Reel (Fixed)
|
||||
=========================================
|
||||
|
||||
This script demonstrates EVERY animation type on EVERY UI object type.
|
||||
Fixed version that works properly with the game loop.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Configuration
|
||||
SCENE_WIDTH = 1280
|
||||
SCENE_HEIGHT = 720
|
||||
DEMO_DURATION = 5.0 # Duration for each demo section
|
||||
|
||||
# All available easing functions
|
||||
EASING_FUNCTIONS = [
|
||||
"linear", "easeIn", "easeOut", "easeInOut",
|
||||
"easeInQuad", "easeOutQuad", "easeInOutQuad",
|
||||
"easeInCubic", "easeOutCubic", "easeInOutCubic",
|
||||
"easeInQuart", "easeOutQuart", "easeInOutQuart",
|
||||
"easeInSine", "easeOutSine", "easeInOutSine",
|
||||
"easeInExpo", "easeOutExpo", "easeInOutExpo",
|
||||
"easeInCirc", "easeOutCirc", "easeInOutCirc",
|
||||
"easeInElastic", "easeOutElastic", "easeInOutElastic",
|
||||
"easeInBack", "easeOutBack", "easeInOutBack",
|
||||
"easeInBounce", "easeOutBounce", "easeInOutBounce"
|
||||
]
|
||||
|
||||
# Track current demo state
|
||||
current_demo = 0
|
||||
subtitle = None
|
||||
|
||||
def create_demo_scene():
|
||||
"""Create the main demo scene with title"""
|
||||
mcrfpy.createScene("sizzle_reel")
|
||||
mcrfpy.setScene("sizzle_reel")
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
|
||||
# Title caption
|
||||
title = mcrfpy.Caption("McRogueFace Animation Sizzle Reel",
|
||||
SCENE_WIDTH/2 - 200, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
title.outline = 2
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
ui.append(title)
|
||||
|
||||
# Subtitle showing current demo
|
||||
global subtitle
|
||||
subtitle = mcrfpy.Caption("Initializing...",
|
||||
SCENE_WIDTH/2 - 150, 60)
|
||||
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(subtitle)
|
||||
|
||||
return ui
|
||||
|
||||
def demo_frame_basic_animations():
|
||||
"""Demo 1: Basic frame animations - position, size, colors"""
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 1: Frame Basic Animations (Position, Size, Colors)"
|
||||
|
||||
# Create test frame
|
||||
frame = mcrfpy.Frame(100, 150, 200, 100)
|
||||
frame.fill_color = mcrfpy.Color(50, 50, 150)
|
||||
frame.outline = 3
|
||||
frame.outline_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(frame)
|
||||
|
||||
# Position animations with different easings
|
||||
x_anim = mcrfpy.Animation("x", 800.0, 2.0, "easeInOutBack")
|
||||
y_anim = mcrfpy.Animation("y", 400.0, 2.0, "easeInOutElastic")
|
||||
x_anim.start(frame)
|
||||
y_anim.start(frame)
|
||||
|
||||
# Size animations
|
||||
w_anim = mcrfpy.Animation("w", 400.0, 3.0, "easeInOutCubic")
|
||||
h_anim = mcrfpy.Animation("h", 200.0, 3.0, "easeInOutCubic")
|
||||
w_anim.start(frame)
|
||||
h_anim.start(frame)
|
||||
|
||||
# Color animations
|
||||
fill_anim = mcrfpy.Animation("fill_color", mcrfpy.Color(255, 100, 50, 200), 4.0, "easeInOutSine")
|
||||
outline_anim = mcrfpy.Animation("outline_color", mcrfpy.Color(0, 255, 255), 4.0, "easeOutBounce")
|
||||
fill_anim.start(frame)
|
||||
outline_anim.start(frame)
|
||||
|
||||
# Outline thickness animation
|
||||
thickness_anim = mcrfpy.Animation("outline", 10.0, 4.5, "easeInOutQuad")
|
||||
thickness_anim.start(frame)
|
||||
|
||||
def demo_caption_animations():
|
||||
"""Demo 2: Caption text animations and effects"""
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 2: Caption Animations (Text, Color, Position)"
|
||||
|
||||
# Basic caption with position animation
|
||||
caption1 = mcrfpy.Caption("Moving Text!", 100, 200)
|
||||
caption1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
caption1.outline = 1
|
||||
ui.append(caption1)
|
||||
|
||||
# Animate across screen with bounce
|
||||
x_anim = mcrfpy.Animation("x", 900.0, 3.0, "easeOutBounce")
|
||||
x_anim.start(caption1)
|
||||
|
||||
# Color cycling caption
|
||||
caption2 = mcrfpy.Caption("Rainbow Colors", 400, 300)
|
||||
caption2.outline = 2
|
||||
ui.append(caption2)
|
||||
|
||||
# Cycle through colors
|
||||
color_anim1 = mcrfpy.Animation("fill_color", mcrfpy.Color(255, 0, 0), 1.0, "linear")
|
||||
color_anim1.start(caption2)
|
||||
|
||||
# Typewriter effect caption
|
||||
caption3 = mcrfpy.Caption("", 100, 400)
|
||||
caption3.fill_color = mcrfpy.Color(0, 255, 255)
|
||||
ui.append(caption3)
|
||||
|
||||
typewriter = mcrfpy.Animation("text", "This text appears one character at a time...", 3.0, "linear")
|
||||
typewriter.start(caption3)
|
||||
|
||||
def demo_sprite_animations():
|
||||
"""Demo 3: Sprite animations (if texture available)"""
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 3: Sprite Animations"
|
||||
|
||||
# Create placeholder caption since texture might not exist
|
||||
no_texture = mcrfpy.Caption("(Sprite demo - textures may not be loaded)", 400, 350)
|
||||
no_texture.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
ui.append(no_texture)
|
||||
|
||||
def demo_performance_stress_test():
|
||||
"""Demo 4: Performance test with many simultaneous animations"""
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 4: Performance Test (50+ Simultaneous Animations)"
|
||||
|
||||
# Create many small objects with different animations
|
||||
num_objects = 50
|
||||
|
||||
for i in range(num_objects):
|
||||
# Random starting position
|
||||
x = 100 + (i % 10) * 100
|
||||
y = 150 + (i // 10) * 80
|
||||
|
||||
# Create small frame
|
||||
size = 20 + (i % 3) * 10
|
||||
frame = mcrfpy.Frame(x, y, size, size)
|
||||
|
||||
# Random color
|
||||
r = (i * 37) % 256
|
||||
g = (i * 73) % 256
|
||||
b = (i * 113) % 256
|
||||
frame.fill_color = mcrfpy.Color(r, g, b, 200)
|
||||
frame.outline = 1
|
||||
ui.append(frame)
|
||||
|
||||
# Random animation properties
|
||||
target_x = 100 + (i % 8) * 120
|
||||
target_y = 150 + (i // 8) * 100
|
||||
duration = 2.0 + (i % 30) * 0.1
|
||||
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
|
||||
|
||||
# Start multiple animations per object
|
||||
x_anim = mcrfpy.Animation("x", float(target_x), duration, easing)
|
||||
y_anim = mcrfpy.Animation("y", float(target_y), duration, easing)
|
||||
opacity_anim = mcrfpy.Animation("opacity", 0.3 + (i % 7) * 0.1, duration, "easeInOutSine")
|
||||
|
||||
x_anim.start(frame)
|
||||
y_anim.start(frame)
|
||||
opacity_anim.start(frame)
|
||||
|
||||
# Performance counter
|
||||
perf_caption = mcrfpy.Caption(f"Animating {num_objects * 3} properties simultaneously", 400, 600)
|
||||
perf_caption.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
ui.append(perf_caption)
|
||||
|
||||
def clear_scene():
|
||||
"""Clear the scene except title and subtitle"""
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
|
||||
# Keep only the first two elements (title and subtitle)
|
||||
while len(ui) > 2:
|
||||
ui.remove(2)
|
||||
|
||||
def run_demo_sequence(runtime):
|
||||
"""Run through all demos"""
|
||||
global current_demo
|
||||
|
||||
# Clear previous demo
|
||||
clear_scene()
|
||||
|
||||
# Demo list
|
||||
demos = [
|
||||
demo_frame_basic_animations,
|
||||
demo_caption_animations,
|
||||
demo_sprite_animations,
|
||||
demo_performance_stress_test
|
||||
]
|
||||
|
||||
if current_demo < len(demos):
|
||||
# Run current demo
|
||||
demos[current_demo]()
|
||||
current_demo += 1
|
||||
|
||||
# Schedule next demo
|
||||
if current_demo < len(demos):
|
||||
mcrfpy.setTimer("next_demo", run_demo_sequence, int(DEMO_DURATION * 1000))
|
||||
else:
|
||||
# All demos complete
|
||||
subtitle.text = "Animation Showcase Complete!"
|
||||
complete = mcrfpy.Caption("All animation types demonstrated!", 400, 350)
|
||||
complete.fill_color = mcrfpy.Color(0, 255, 0)
|
||||
complete.outline = 2
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
ui.append(complete)
|
||||
|
||||
# Initialize scene
|
||||
print("Starting McRogueFace Animation Sizzle Reel...")
|
||||
print("This will demonstrate animation types on various objects.")
|
||||
|
||||
ui = create_demo_scene()
|
||||
|
||||
# Start the demo sequence after a short delay
|
||||
mcrfpy.setTimer("start_demos", run_demo_sequence, 500)
|
|
@ -0,0 +1,307 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Animation Sizzle Reel v2
|
||||
====================================
|
||||
|
||||
Fixed version with proper API usage for animations and collections.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Configuration
|
||||
SCENE_WIDTH = 1280
|
||||
SCENE_HEIGHT = 720
|
||||
DEMO_DURATION = 5.0 # Duration for each demo section
|
||||
|
||||
# All available easing functions
|
||||
EASING_FUNCTIONS = [
|
||||
"linear", "easeIn", "easeOut", "easeInOut",
|
||||
"easeInQuad", "easeOutQuad", "easeInOutQuad",
|
||||
"easeInCubic", "easeOutCubic", "easeInOutCubic",
|
||||
"easeInQuart", "easeOutQuart", "easeInOutQuart",
|
||||
"easeInSine", "easeOutSine", "easeInOutSine",
|
||||
"easeInExpo", "easeOutExpo", "easeInOutExpo",
|
||||
"easeInCirc", "easeOutCirc", "easeInOutCirc",
|
||||
"easeInElastic", "easeOutElastic", "easeInOutElastic",
|
||||
"easeInBack", "easeOutBack", "easeInOutBack",
|
||||
"easeInBounce", "easeOutBounce", "easeInOutBounce"
|
||||
]
|
||||
|
||||
# Track current demo state
|
||||
current_demo = 0
|
||||
subtitle = None
|
||||
demo_objects = [] # Track objects from current demo
|
||||
|
||||
def create_demo_scene():
|
||||
"""Create the main demo scene with title"""
|
||||
mcrfpy.createScene("sizzle_reel")
|
||||
mcrfpy.setScene("sizzle_reel")
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
|
||||
# Title caption
|
||||
title = mcrfpy.Caption("McRogueFace Animation Sizzle Reel",
|
||||
SCENE_WIDTH/2 - 200, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
title.outline = 2
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
ui.append(title)
|
||||
|
||||
# Subtitle showing current demo
|
||||
global subtitle
|
||||
subtitle = mcrfpy.Caption("Initializing...",
|
||||
SCENE_WIDTH/2 - 150, 60)
|
||||
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(subtitle)
|
||||
|
||||
return ui
|
||||
|
||||
def demo_frame_basic_animations():
|
||||
"""Demo 1: Basic frame animations - position, size, colors"""
|
||||
global demo_objects
|
||||
demo_objects = []
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 1: Frame Basic Animations (Position, Size, Colors)"
|
||||
|
||||
# Create test frame
|
||||
frame = mcrfpy.Frame(100, 150, 200, 100)
|
||||
frame.fill_color = mcrfpy.Color(50, 50, 150)
|
||||
frame.outline = 3
|
||||
frame.outline_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(frame)
|
||||
demo_objects.append(frame)
|
||||
|
||||
# Position animations with different easings
|
||||
x_anim = mcrfpy.Animation("x", 800.0, 2.0, "easeInOutBack")
|
||||
y_anim = mcrfpy.Animation("y", 400.0, 2.0, "easeInOutElastic")
|
||||
x_anim.start(frame)
|
||||
y_anim.start(frame)
|
||||
|
||||
# Size animations
|
||||
w_anim = mcrfpy.Animation("w", 400.0, 3.0, "easeInOutCubic")
|
||||
h_anim = mcrfpy.Animation("h", 200.0, 3.0, "easeInOutCubic")
|
||||
w_anim.start(frame)
|
||||
h_anim.start(frame)
|
||||
|
||||
# Color animations - use tuples instead of Color objects
|
||||
fill_anim = mcrfpy.Animation("fill_color", (255, 100, 50, 200), 4.0, "easeInOutSine")
|
||||
outline_anim = mcrfpy.Animation("outline_color", (0, 255, 255, 255), 4.0, "easeOutBounce")
|
||||
fill_anim.start(frame)
|
||||
outline_anim.start(frame)
|
||||
|
||||
# Outline thickness animation
|
||||
thickness_anim = mcrfpy.Animation("outline", 10.0, 4.5, "easeInOutQuad")
|
||||
thickness_anim.start(frame)
|
||||
|
||||
def demo_caption_animations():
|
||||
"""Demo 2: Caption text animations and effects"""
|
||||
global demo_objects
|
||||
demo_objects = []
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 2: Caption Animations (Text, Color, Position)"
|
||||
|
||||
# Basic caption with position animation
|
||||
caption1 = mcrfpy.Caption("Moving Text!", 100, 200)
|
||||
caption1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
caption1.outline = 1
|
||||
ui.append(caption1)
|
||||
demo_objects.append(caption1)
|
||||
|
||||
# Animate across screen with bounce
|
||||
x_anim = mcrfpy.Animation("x", 900.0, 3.0, "easeOutBounce")
|
||||
x_anim.start(caption1)
|
||||
|
||||
# Color cycling caption
|
||||
caption2 = mcrfpy.Caption("Rainbow Colors", 400, 300)
|
||||
caption2.outline = 2
|
||||
ui.append(caption2)
|
||||
demo_objects.append(caption2)
|
||||
|
||||
# Cycle through colors using tuples
|
||||
color_anim1 = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear")
|
||||
color_anim1.start(caption2)
|
||||
|
||||
# Schedule color changes
|
||||
def change_to_green(rt):
|
||||
color_anim2 = mcrfpy.Animation("fill_color", (0, 255, 0, 255), 1.0, "linear")
|
||||
color_anim2.start(caption2)
|
||||
|
||||
def change_to_blue(rt):
|
||||
color_anim3 = mcrfpy.Animation("fill_color", (0, 0, 255, 255), 1.0, "linear")
|
||||
color_anim3.start(caption2)
|
||||
|
||||
def change_to_white(rt):
|
||||
color_anim4 = mcrfpy.Animation("fill_color", (255, 255, 255, 255), 1.0, "linear")
|
||||
color_anim4.start(caption2)
|
||||
|
||||
mcrfpy.setTimer("color2", change_to_green, 1000)
|
||||
mcrfpy.setTimer("color3", change_to_blue, 2000)
|
||||
mcrfpy.setTimer("color4", change_to_white, 3000)
|
||||
|
||||
# Typewriter effect caption
|
||||
caption3 = mcrfpy.Caption("", 100, 400)
|
||||
caption3.fill_color = mcrfpy.Color(0, 255, 255)
|
||||
ui.append(caption3)
|
||||
demo_objects.append(caption3)
|
||||
|
||||
typewriter = mcrfpy.Animation("text", "This text appears one character at a time...", 3.0, "linear")
|
||||
typewriter.start(caption3)
|
||||
|
||||
def demo_easing_showcase():
|
||||
"""Demo 3: Showcase different easing functions"""
|
||||
global demo_objects
|
||||
demo_objects = []
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 3: Easing Functions Showcase"
|
||||
|
||||
# Create small frames for each easing function
|
||||
frames_per_row = 6
|
||||
frame_width = 180
|
||||
spacing = 10
|
||||
|
||||
# Show first 12 easings
|
||||
for i, easing in enumerate(EASING_FUNCTIONS[:12]):
|
||||
row = i // frames_per_row
|
||||
col = i % frames_per_row
|
||||
|
||||
x = 50 + col * (frame_width + spacing)
|
||||
y = 150 + row * (80 + spacing)
|
||||
|
||||
# Create indicator frame
|
||||
frame = mcrfpy.Frame(x, y, 20, 20)
|
||||
frame.fill_color = mcrfpy.Color(100, 200, 255)
|
||||
frame.outline = 1
|
||||
ui.append(frame)
|
||||
demo_objects.append(frame)
|
||||
|
||||
# Label
|
||||
label = mcrfpy.Caption(easing[:8], x, y - 20) # Truncate long names
|
||||
label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(label)
|
||||
demo_objects.append(label)
|
||||
|
||||
# Animate using this easing
|
||||
move_anim = mcrfpy.Animation("x", float(x + frame_width - 20), 3.0, easing)
|
||||
move_anim.start(frame)
|
||||
|
||||
def demo_performance_stress_test():
|
||||
"""Demo 4: Performance test with many simultaneous animations"""
|
||||
global demo_objects
|
||||
demo_objects = []
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 4: Performance Test (50+ Simultaneous Animations)"
|
||||
|
||||
# Create many small objects with different animations
|
||||
num_objects = 50
|
||||
|
||||
for i in range(num_objects):
|
||||
# Starting position
|
||||
x = 100 + (i % 10) * 100
|
||||
y = 150 + (i // 10) * 80
|
||||
|
||||
# Create small frame
|
||||
size = 20 + (i % 3) * 10
|
||||
frame = mcrfpy.Frame(x, y, size, size)
|
||||
|
||||
# Random color
|
||||
r = (i * 37) % 256
|
||||
g = (i * 73) % 256
|
||||
b = (i * 113) % 256
|
||||
frame.fill_color = mcrfpy.Color(r, g, b, 200)
|
||||
frame.outline = 1
|
||||
ui.append(frame)
|
||||
demo_objects.append(frame)
|
||||
|
||||
# Random animation properties
|
||||
target_x = 100 + (i % 8) * 120
|
||||
target_y = 150 + (i // 8) * 100
|
||||
duration = 2.0 + (i % 30) * 0.1
|
||||
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
|
||||
|
||||
# Start multiple animations per object
|
||||
x_anim = mcrfpy.Animation("x", float(target_x), duration, easing)
|
||||
y_anim = mcrfpy.Animation("y", float(target_y), duration, easing)
|
||||
opacity_anim = mcrfpy.Animation("opacity", 0.3 + (i % 7) * 0.1, duration, "easeInOutSine")
|
||||
|
||||
x_anim.start(frame)
|
||||
y_anim.start(frame)
|
||||
opacity_anim.start(frame)
|
||||
|
||||
# Performance counter
|
||||
perf_caption = mcrfpy.Caption(f"Animating {num_objects * 3} properties simultaneously", 350, 600)
|
||||
perf_caption.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
ui.append(perf_caption)
|
||||
demo_objects.append(perf_caption)
|
||||
|
||||
def clear_scene():
|
||||
"""Clear the scene except title and subtitle"""
|
||||
global demo_objects
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
|
||||
# Remove all demo objects
|
||||
for obj in demo_objects:
|
||||
try:
|
||||
# Find index of object
|
||||
for i in range(len(ui)):
|
||||
if ui[i] is obj:
|
||||
ui.remove(ui[i])
|
||||
break
|
||||
except:
|
||||
pass # Object might already be removed
|
||||
|
||||
demo_objects = []
|
||||
|
||||
# Clean up any timers
|
||||
for timer_name in ["color2", "color3", "color4"]:
|
||||
try:
|
||||
mcrfpy.delTimer(timer_name)
|
||||
except:
|
||||
pass
|
||||
|
||||
def run_demo_sequence(runtime):
|
||||
"""Run through all demos"""
|
||||
global current_demo
|
||||
|
||||
# Clear previous demo
|
||||
clear_scene()
|
||||
|
||||
# Demo list
|
||||
demos = [
|
||||
demo_frame_basic_animations,
|
||||
demo_caption_animations,
|
||||
demo_easing_showcase,
|
||||
demo_performance_stress_test
|
||||
]
|
||||
|
||||
if current_demo < len(demos):
|
||||
# Run current demo
|
||||
demos[current_demo]()
|
||||
current_demo += 1
|
||||
|
||||
# Schedule next demo
|
||||
if current_demo < len(demos):
|
||||
mcrfpy.setTimer("next_demo", run_demo_sequence, int(DEMO_DURATION * 1000))
|
||||
else:
|
||||
# Final demo completed
|
||||
def show_complete(rt):
|
||||
subtitle.text = "Animation Showcase Complete!"
|
||||
complete = mcrfpy.Caption("All animation types demonstrated!", 400, 350)
|
||||
complete.fill_color = mcrfpy.Color(0, 255, 0)
|
||||
complete.outline = 2
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
ui.append(complete)
|
||||
|
||||
mcrfpy.setTimer("complete", show_complete, 3000)
|
||||
|
||||
# Initialize scene
|
||||
print("Starting McRogueFace Animation Sizzle Reel v2...")
|
||||
print("This will demonstrate animation types on various objects.")
|
||||
|
||||
ui = create_demo_scene()
|
||||
|
||||
# Start the demo sequence after a short delay
|
||||
mcrfpy.setTimer("start_demos", run_demo_sequence, 500)
|
|
@ -0,0 +1,316 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Animation Sizzle Reel - Working Version
|
||||
===================================================
|
||||
|
||||
Complete demonstration of all animation capabilities.
|
||||
Fixed to work properly with the API.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
import math
|
||||
|
||||
# Configuration
|
||||
DEMO_DURATION = 7.0 # Duration for each demo
|
||||
|
||||
# All available easing functions
|
||||
EASING_FUNCTIONS = [
|
||||
"linear", "easeIn", "easeOut", "easeInOut",
|
||||
"easeInQuad", "easeOutQuad", "easeInOutQuad",
|
||||
"easeInCubic", "easeOutCubic", "easeInOutCubic",
|
||||
"easeInQuart", "easeOutQuart", "easeInOutQuart",
|
||||
"easeInSine", "easeOutSine", "easeInOutSine",
|
||||
"easeInExpo", "easeOutExpo", "easeInOutExpo",
|
||||
"easeInCirc", "easeOutCirc", "easeInOutCirc",
|
||||
"easeInElastic", "easeOutElastic", "easeInOutElastic",
|
||||
"easeInBack", "easeOutBack", "easeInOutBack",
|
||||
"easeInBounce", "easeOutBounce", "easeInOutBounce"
|
||||
]
|
||||
|
||||
# Track state
|
||||
current_demo = 0
|
||||
subtitle = None
|
||||
demo_objects = []
|
||||
|
||||
def create_scene():
|
||||
"""Create the demo scene with title"""
|
||||
mcrfpy.createScene("sizzle")
|
||||
mcrfpy.setScene("sizzle")
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("McRogueFace Animation Sizzle Reel", 340, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
title.outline = 2
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
ui.append(title)
|
||||
|
||||
# Subtitle
|
||||
global subtitle
|
||||
subtitle = mcrfpy.Caption("Initializing...", 400, 60)
|
||||
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(subtitle)
|
||||
|
||||
def clear_demo():
|
||||
"""Clear demo objects"""
|
||||
global demo_objects
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Remove items starting from the end
|
||||
# Skip first 2 (title and subtitle)
|
||||
while len(ui) > 2:
|
||||
ui.remove(len(ui) - 1)
|
||||
|
||||
demo_objects = []
|
||||
|
||||
def demo1_frame_basics():
|
||||
"""Demo 1: Basic frame animations"""
|
||||
clear_demo()
|
||||
print("demo1")
|
||||
subtitle.text = "Demo 1: Frame Animations (Position, Size, Color)"
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Create frame
|
||||
frame = mcrfpy.Frame(100, 150, 200, 100)
|
||||
frame.fill_color = mcrfpy.Color(50, 50, 150)
|
||||
frame.outline = 3
|
||||
frame.outline_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(frame)
|
||||
|
||||
# Animate properties
|
||||
mcrfpy.Animation("x", 700.0, 2.5, "easeInOutBack").start(frame)
|
||||
mcrfpy.Animation("y", 350.0, 2.5, "easeInOutElastic").start(frame)
|
||||
mcrfpy.Animation("w", 350.0, 3.0, "easeInOutCubic").start(frame)
|
||||
mcrfpy.Animation("h", 180.0, 3.0, "easeInOutCubic").start(frame)
|
||||
mcrfpy.Animation("fill_color", (255, 100, 50, 200), 4.0, "easeInOutSine").start(frame)
|
||||
mcrfpy.Animation("outline_color", (0, 255, 255, 255), 4.0, "easeOutBounce").start(frame)
|
||||
mcrfpy.Animation("outline", 8.0, 4.0, "easeInOutQuad").start(frame)
|
||||
|
||||
def demo2_opacity_zindex():
|
||||
"""Demo 2: Opacity and z-index animations"""
|
||||
clear_demo()
|
||||
print("demo2")
|
||||
subtitle.text = "Demo 2: Opacity & Z-Index Animations"
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Create overlapping frames
|
||||
colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)]
|
||||
|
||||
for i in range(4):
|
||||
frame = mcrfpy.Frame(200 + i*80, 200 + i*40, 200, 150)
|
||||
frame.fill_color = mcrfpy.Color(colors[i][0], colors[i][1], colors[i][2], 200)
|
||||
frame.outline = 2
|
||||
frame.z_index = i
|
||||
ui.append(frame)
|
||||
|
||||
# Animate opacity
|
||||
mcrfpy.Animation("opacity", 0.3, 2.0, "easeInOutSine").start(frame)
|
||||
|
||||
# Schedule opacity return
|
||||
def return_opacity(rt):
|
||||
for i in range(4):
|
||||
mcrfpy.Animation("opacity", 1.0, 2.0, "easeInOutSine").start(ui[i])
|
||||
mcrfpy.setTimer(f"opacity_{i}", return_opacity, 2100)
|
||||
|
||||
def demo3_captions():
|
||||
"""Demo 3: Caption animations"""
|
||||
clear_demo()
|
||||
print("demo3")
|
||||
subtitle.text = "Demo 3: Caption Animations"
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Moving caption
|
||||
c1 = mcrfpy.Caption("Bouncing Text!", 100, 200)
|
||||
c1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
c1.outline = 1
|
||||
ui.append(c1)
|
||||
mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1)
|
||||
|
||||
# Color cycling caption
|
||||
c2 = mcrfpy.Caption("Color Cycle", 400, 300)
|
||||
c2.outline = 2
|
||||
ui.append(c2)
|
||||
|
||||
# Animate through colors
|
||||
def cycle_colors():
|
||||
anim = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 0.5, "linear")
|
||||
anim.start(c2)
|
||||
|
||||
def to_green(rt):
|
||||
mcrfpy.Animation("fill_color", (0, 255, 0, 255), 0.5, "linear").start(c2)
|
||||
def to_blue(rt):
|
||||
mcrfpy.Animation("fill_color", (0, 0, 255, 255), 0.5, "linear").start(c2)
|
||||
def to_white(rt):
|
||||
mcrfpy.Animation("fill_color", (255, 255, 255, 255), 0.5, "linear").start(c2)
|
||||
|
||||
mcrfpy.setTimer("c_green", to_green, 600)
|
||||
mcrfpy.setTimer("c_blue", to_blue, 1200)
|
||||
mcrfpy.setTimer("c_white", to_white, 1800)
|
||||
|
||||
cycle_colors()
|
||||
|
||||
# Typewriter effect
|
||||
c3 = mcrfpy.Caption("", 100, 400)
|
||||
c3.fill_color = mcrfpy.Color(0, 255, 255)
|
||||
ui.append(c3)
|
||||
mcrfpy.Animation("text", "This text appears one character at a time...", 3.0, "linear").start(c3)
|
||||
|
||||
def demo4_easing_showcase():
|
||||
"""Demo 4: Showcase easing functions"""
|
||||
clear_demo()
|
||||
print("demo4")
|
||||
subtitle.text = "Demo 4: 30 Easing Functions"
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Show first 15 easings
|
||||
for i in range(15):
|
||||
row = i // 5
|
||||
col = i % 5
|
||||
x = 80 + col * 180
|
||||
y = 150 + row * 120
|
||||
|
||||
# Create frame
|
||||
f = mcrfpy.Frame(x, y, 20, 20)
|
||||
f.fill_color = mcrfpy.Color(100, 150, 255)
|
||||
f.outline = 1
|
||||
ui.append(f)
|
||||
|
||||
# Label
|
||||
label = mcrfpy.Caption(EASING_FUNCTIONS[i][:10], x, y - 20)
|
||||
label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(label)
|
||||
|
||||
# Animate with this easing
|
||||
mcrfpy.Animation("x", float(x + 140), 3.0, EASING_FUNCTIONS[i]).start(f)
|
||||
|
||||
def demo5_performance():
|
||||
"""Demo 5: Many simultaneous animations"""
|
||||
clear_demo()
|
||||
print("demo5")
|
||||
subtitle.text = "Demo 5: 50+ Simultaneous Animations"
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Create many animated objects
|
||||
for i in range(50):
|
||||
print(f"{i}...",end='',flush=True)
|
||||
x = 100 + (i % 10) * 90
|
||||
y = 120 + (i // 10) * 80
|
||||
|
||||
f = mcrfpy.Frame(x, y, 25, 25)
|
||||
r = (i * 37) % 256
|
||||
g = (i * 73) % 256
|
||||
b = (i * 113) % 256
|
||||
f.fill_color = (r, g, b, 200) #mcrfpy.Color(r, g, b, 200)
|
||||
f.outline = 1
|
||||
ui.append(f)
|
||||
|
||||
# Random animations
|
||||
target_x = 150 + (i % 8) * 100
|
||||
target_y = 150 + (i // 8) * 85
|
||||
duration = 2.0 + (i % 30) * 0.1
|
||||
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
|
||||
|
||||
mcrfpy.Animation("x", float(target_x), duration, easing).start(f)
|
||||
mcrfpy.Animation("y", float(target_y), duration, easing).start(f)
|
||||
mcrfpy.Animation("opacity", 0.3 + (i % 7) * 0.1, 2.5, "easeInOutSine").start(f)
|
||||
|
||||
def demo6_delta_mode():
|
||||
"""Demo 6: Delta mode animations"""
|
||||
clear_demo()
|
||||
print("demo6")
|
||||
subtitle.text = "Demo 6: Delta Mode (Relative Movement)"
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Create frames that move relative to position
|
||||
positions = [(100, 300), (300, 300), (500, 300), (700, 300)]
|
||||
colors = [(255, 100, 100), (100, 255, 100), (100, 100, 255), (255, 255, 100)]
|
||||
|
||||
for i, ((x, y), color) in enumerate(zip(positions, colors)):
|
||||
f = mcrfpy.Frame(x, y, 60, 60)
|
||||
f.fill_color = mcrfpy.Color(color[0], color[1], color[2])
|
||||
f.outline = 2
|
||||
ui.append(f)
|
||||
|
||||
# Delta animations - move by amount, not to position
|
||||
dx = (i + 1) * 30
|
||||
dy = math.sin(i * 0.5) * 50
|
||||
|
||||
mcrfpy.Animation("x", float(dx), 2.0, "easeInOutBack", delta=True).start(f)
|
||||
mcrfpy.Animation("y", float(dy), 2.0, "easeInOutElastic", delta=True).start(f)
|
||||
|
||||
# Caption explaining delta mode
|
||||
info = mcrfpy.Caption("Delta mode: animations move BY amount, not TO position", 200, 450)
|
||||
info.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(info)
|
||||
|
||||
def run_next_demo(runtime):
|
||||
"""Run the next demo in sequence"""
|
||||
global current_demo
|
||||
|
||||
demos = [
|
||||
demo1_frame_basics,
|
||||
demo2_opacity_zindex,
|
||||
demo3_captions,
|
||||
demo4_easing_showcase,
|
||||
demo5_performance,
|
||||
demo6_delta_mode
|
||||
]
|
||||
|
||||
if current_demo < len(demos):
|
||||
# Clean up timers from previous demo
|
||||
for timer in ["opacity_0", "opacity_1", "opacity_2", "opacity_3",
|
||||
"c_green", "c_blue", "c_white"]:
|
||||
try:
|
||||
mcrfpy.delTimer(timer)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Run next demo
|
||||
print(f"Run next: {current_demo}")
|
||||
demos[current_demo]()
|
||||
current_demo += 1
|
||||
|
||||
# Schedule next demo
|
||||
if current_demo < len(demos):
|
||||
#mcrfpy.setTimer("next_demo", run_next_demo, int(DEMO_DURATION * 1000))
|
||||
pass
|
||||
else:
|
||||
current_demo = 0
|
||||
# All done
|
||||
#subtitle.text = "Animation Showcase Complete!"
|
||||
#complete = mcrfpy.Caption("All animations demonstrated successfully!", 350, 350)
|
||||
#complete.fill_color = mcrfpy.Color(0, 255, 0)
|
||||
#complete.outline = 2
|
||||
#ui = mcrfpy.sceneUI("sizzle")
|
||||
#ui.append(complete)
|
||||
#
|
||||
## Exit after delay
|
||||
#def exit_program(rt):
|
||||
# print("\nSizzle reel completed successfully!")
|
||||
# sys.exit(0)
|
||||
#mcrfpy.setTimer("exit", exit_program, 3000)
|
||||
|
||||
# Handle ESC key
|
||||
def handle_keypress(scene_name, keycode):
|
||||
if keycode == 256: # ESC
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
|
||||
# Initialize
|
||||
print("Starting McRogueFace Animation Sizzle Reel...")
|
||||
print("This demonstrates all animation capabilities.")
|
||||
print("Press ESC to exit at any time.")
|
||||
|
||||
create_scene()
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
# Start the show
|
||||
mcrfpy.setTimer("start", run_next_demo, int(DEMO_DURATION * 1000))
|
|
@ -0,0 +1,207 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace API Demo - Final Version
|
||||
====================================
|
||||
|
||||
Complete API demonstration with proper error handling.
|
||||
Tests all constructors and methods systematically.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def print_section(title):
|
||||
"""Print a section header"""
|
||||
print("\n" + "="*60)
|
||||
print(f" {title}")
|
||||
print("="*60)
|
||||
|
||||
def print_test(name, success=True):
|
||||
"""Print test result"""
|
||||
status = "✓" if success else "✗"
|
||||
print(f" {status} {name}")
|
||||
|
||||
def test_colors():
|
||||
"""Test Color API"""
|
||||
print_section("COLOR TESTS")
|
||||
|
||||
try:
|
||||
# Basic constructors
|
||||
c1 = mcrfpy.Color(255, 0, 0) # RGB
|
||||
print_test(f"Color(255,0,0) = ({c1.r},{c1.g},{c1.b},{c1.a})")
|
||||
|
||||
c2 = mcrfpy.Color(100, 150, 200, 128) # RGBA
|
||||
print_test(f"Color(100,150,200,128) = ({c2.r},{c2.g},{c2.b},{c2.a})")
|
||||
|
||||
# Property modification
|
||||
c1.r = 128
|
||||
c1.g = 128
|
||||
c1.b = 128
|
||||
c1.a = 200
|
||||
print_test(f"Modified color = ({c1.r},{c1.g},{c1.b},{c1.a})")
|
||||
|
||||
except Exception as e:
|
||||
print_test(f"Color test failed: {e}", False)
|
||||
|
||||
def test_frames():
|
||||
"""Test Frame API"""
|
||||
print_section("FRAME TESTS")
|
||||
|
||||
# Create scene
|
||||
mcrfpy.createScene("test")
|
||||
mcrfpy.setScene("test")
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
|
||||
try:
|
||||
# Constructors
|
||||
f1 = mcrfpy.Frame()
|
||||
print_test(f"Frame() at ({f1.x},{f1.y}) size ({f1.w},{f1.h})")
|
||||
|
||||
f2 = mcrfpy.Frame(100, 50)
|
||||
print_test(f"Frame(100,50) at ({f2.x},{f2.y})")
|
||||
|
||||
f3 = mcrfpy.Frame(200, 100, 150, 75)
|
||||
print_test(f"Frame(200,100,150,75) size ({f3.w},{f3.h})")
|
||||
|
||||
# Properties
|
||||
f3.fill_color = mcrfpy.Color(100, 100, 200)
|
||||
f3.outline = 3
|
||||
f3.outline_color = mcrfpy.Color(255, 255, 0)
|
||||
f3.opacity = 0.8
|
||||
f3.visible = True
|
||||
f3.z_index = 5
|
||||
print_test(f"Frame properties set")
|
||||
|
||||
# Add to scene
|
||||
ui.append(f3)
|
||||
print_test(f"Frame added to scene")
|
||||
|
||||
# Children
|
||||
child = mcrfpy.Frame(10, 10, 50, 50)
|
||||
f3.children.append(child)
|
||||
print_test(f"Child added, count = {len(f3.children)}")
|
||||
|
||||
except Exception as e:
|
||||
print_test(f"Frame test failed: {e}", False)
|
||||
|
||||
def test_captions():
|
||||
"""Test Caption API"""
|
||||
print_section("CAPTION TESTS")
|
||||
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
|
||||
try:
|
||||
# Constructors
|
||||
c1 = mcrfpy.Caption()
|
||||
print_test(f"Caption() text='{c1.text}'")
|
||||
|
||||
c2 = mcrfpy.Caption("Hello World")
|
||||
print_test(f"Caption('Hello World') at ({c2.x},{c2.y})")
|
||||
|
||||
c3 = mcrfpy.Caption("Test", 300, 200)
|
||||
print_test(f"Caption with position at ({c3.x},{c3.y})")
|
||||
|
||||
# Properties
|
||||
c3.text = "Modified"
|
||||
c3.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
c3.outline = 2
|
||||
c3.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
print_test(f"Caption text='{c3.text}'")
|
||||
|
||||
ui.append(c3)
|
||||
print_test("Caption added to scene")
|
||||
|
||||
except Exception as e:
|
||||
print_test(f"Caption test failed: {e}", False)
|
||||
|
||||
def test_animations():
|
||||
"""Test Animation API"""
|
||||
print_section("ANIMATION TESTS")
|
||||
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
|
||||
try:
|
||||
# Create target
|
||||
frame = mcrfpy.Frame(50, 50, 100, 100)
|
||||
frame.fill_color = mcrfpy.Color(100, 100, 100)
|
||||
ui.append(frame)
|
||||
|
||||
# Basic animations
|
||||
a1 = mcrfpy.Animation("x", 300.0, 2.0)
|
||||
print_test("Animation created (position)")
|
||||
|
||||
a2 = mcrfpy.Animation("opacity", 0.5, 1.5, "easeInOut")
|
||||
print_test("Animation with easing")
|
||||
|
||||
a3 = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 2.0)
|
||||
print_test("Color animation (tuple)")
|
||||
|
||||
# Start animations
|
||||
a1.start(frame)
|
||||
a2.start(frame)
|
||||
a3.start(frame)
|
||||
print_test("Animations started")
|
||||
|
||||
# Check properties
|
||||
print_test(f"Duration = {a1.duration}")
|
||||
print_test(f"Elapsed = {a1.elapsed}")
|
||||
print_test(f"Complete = {a1.is_complete}")
|
||||
|
||||
except Exception as e:
|
||||
print_test(f"Animation test failed: {e}", False)
|
||||
|
||||
def test_collections():
|
||||
"""Test collection operations"""
|
||||
print_section("COLLECTION TESTS")
|
||||
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
|
||||
try:
|
||||
# Clear scene
|
||||
while len(ui) > 0:
|
||||
ui.remove(ui[len(ui)-1])
|
||||
print_test(f"Scene cleared, length = {len(ui)}")
|
||||
|
||||
# Add items
|
||||
for i in range(5):
|
||||
f = mcrfpy.Frame(i*100, 50, 80, 80)
|
||||
ui.append(f)
|
||||
print_test(f"Added 5 frames, length = {len(ui)}")
|
||||
|
||||
# Access
|
||||
first = ui[0]
|
||||
print_test(f"Accessed ui[0] at ({first.x},{first.y})")
|
||||
|
||||
# Iteration
|
||||
count = sum(1 for _ in ui)
|
||||
print_test(f"Iteration count = {count}")
|
||||
|
||||
except Exception as e:
|
||||
print_test(f"Collection test failed: {e}", False)
|
||||
|
||||
def run_tests():
|
||||
"""Run all tests"""
|
||||
print("\n" + "="*60)
|
||||
print(" McRogueFace API Test Suite")
|
||||
print("="*60)
|
||||
|
||||
test_colors()
|
||||
test_frames()
|
||||
test_captions()
|
||||
test_animations()
|
||||
test_collections()
|
||||
|
||||
print("\n" + "="*60)
|
||||
print(" Tests Complete")
|
||||
print("="*60)
|
||||
|
||||
# Exit after delay
|
||||
def exit_program(runtime):
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
|
||||
mcrfpy.setTimer("exit", exit_program, 3000)
|
||||
|
||||
# Run tests
|
||||
print("Starting API tests...")
|
||||
run_tests()
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Debug the astar_vs_dijkstra demo issue"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Same setup as the demo
|
||||
start_pos = (5, 10)
|
||||
end_pos = (25, 10)
|
||||
|
||||
print("Debugging A* vs Dijkstra demo...")
|
||||
print(f"Start: {start_pos}, End: {end_pos}")
|
||||
|
||||
# Create scene and grid
|
||||
mcrfpy.createScene("debug")
|
||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||
|
||||
# Initialize all as floor
|
||||
print("\nInitializing 30x20 grid...")
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
grid.at(x, y).walkable = True
|
||||
|
||||
# Test path before obstacles
|
||||
print("\nTest 1: Path with no obstacles")
|
||||
path1 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
|
||||
print(f" Path: {path1[:5]}...{path1[-3:] if len(path1) > 5 else ''}")
|
||||
print(f" Length: {len(path1)}")
|
||||
|
||||
# Add obstacles from the demo
|
||||
obstacles = [
|
||||
# Vertical wall with gaps
|
||||
[(15, y) for y in range(3, 17) if y not in [8, 12]],
|
||||
# Horizontal walls
|
||||
[(x, 5) for x in range(10, 20)],
|
||||
[(x, 15) for x in range(10, 20)],
|
||||
# Maze-like structure
|
||||
[(x, 10) for x in range(20, 25)],
|
||||
[(25, y) for y in range(5, 15)],
|
||||
]
|
||||
|
||||
print("\nAdding obstacles...")
|
||||
wall_count = 0
|
||||
for obstacle_group in obstacles:
|
||||
for x, y in obstacle_group:
|
||||
grid.at(x, y).walkable = False
|
||||
wall_count += 1
|
||||
if wall_count <= 5:
|
||||
print(f" Wall at ({x}, {y})")
|
||||
|
||||
print(f" Total walls added: {wall_count}")
|
||||
|
||||
# Check specific cells
|
||||
print(f"\nChecking key positions:")
|
||||
print(f" Start ({start_pos[0]}, {start_pos[1]}): walkable={grid.at(start_pos[0], start_pos[1]).walkable}")
|
||||
print(f" End ({end_pos[0]}, {end_pos[1]}): walkable={grid.at(end_pos[0], end_pos[1]).walkable}")
|
||||
|
||||
# Check if path is blocked
|
||||
print(f"\nChecking horizontal line at y=10:")
|
||||
blocked_x = []
|
||||
for x in range(30):
|
||||
if not grid.at(x, 10).walkable:
|
||||
blocked_x.append(x)
|
||||
|
||||
print(f" Blocked x positions: {blocked_x}")
|
||||
|
||||
# Test path with obstacles
|
||||
print("\nTest 2: Path with obstacles")
|
||||
path2 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
|
||||
print(f" Path: {path2}")
|
||||
print(f" Length: {len(path2)}")
|
||||
|
||||
# Check if there's any path at all
|
||||
if not path2:
|
||||
print("\n No path found! Checking why...")
|
||||
|
||||
# Check if we can reach the vertical wall gap
|
||||
print("\n Testing path to wall gap at (15, 8):")
|
||||
path_to_gap = grid.compute_astar_path(start_pos[0], start_pos[1], 15, 8)
|
||||
print(f" Path to gap: {path_to_gap}")
|
||||
|
||||
# Check from gap to end
|
||||
print("\n Testing path from gap (15, 8) to end:")
|
||||
path_from_gap = grid.compute_astar_path(15, 8, end_pos[0], end_pos[1])
|
||||
print(f" Path from gap: {path_from_gap}")
|
||||
|
||||
# Check walls more carefully
|
||||
print("\nDetailed wall analysis:")
|
||||
print(" Walls at x=25 (blocking end?):")
|
||||
for y in range(5, 15):
|
||||
print(f" ({25}, {y}): walkable={grid.at(25, y).walkable}")
|
||||
|
||||
def timer_cb(dt):
|
||||
sys.exit(0)
|
||||
|
||||
ui = mcrfpy.sceneUI("debug")
|
||||
ui.append(grid)
|
||||
mcrfpy.setScene("debug")
|
||||
mcrfpy.setTimer("exit", timer_cb, 100)
|
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Working Dijkstra Demo with Clear Visual Feedback
|
||||
================================================
|
||||
|
||||
This demo shows pathfinding with high-contrast colors.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# High contrast colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown for walls
|
||||
FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray for floors
|
||||
PATH_COLOR = mcrfpy.Color(0, 255, 0) # Pure green for paths
|
||||
START_COLOR = mcrfpy.Color(255, 0, 0) # Red for start
|
||||
END_COLOR = mcrfpy.Color(0, 0, 255) # Blue for end
|
||||
|
||||
print("Dijkstra Demo - High Contrast")
|
||||
print("==============================")
|
||||
|
||||
# Create scene
|
||||
mcrfpy.createScene("dijkstra_demo")
|
||||
|
||||
# Create grid with exact layout from user
|
||||
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Map layout
|
||||
map_layout = [
|
||||
"..............", # Row 0
|
||||
"..W.....WWWW..", # Row 1
|
||||
"..W.W...W.EW..", # Row 2
|
||||
"..W.....W..W..", # Row 3
|
||||
"..W...E.WWWW..", # Row 4
|
||||
"E.W...........", # Row 5
|
||||
"..W...........", # Row 6
|
||||
"..W...........", # Row 7
|
||||
"..W.WWW.......", # Row 8
|
||||
"..............", # Row 9
|
||||
]
|
||||
|
||||
# Create the map
|
||||
entity_positions = []
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
|
||||
if char == 'W':
|
||||
cell.walkable = False
|
||||
cell.color = WALL_COLOR
|
||||
else:
|
||||
cell.walkable = True
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
if char == 'E':
|
||||
entity_positions.append((x, y))
|
||||
|
||||
print(f"Map created: {grid.grid_x}x{grid.grid_y}")
|
||||
print(f"Entity positions: {entity_positions}")
|
||||
|
||||
# Create entities
|
||||
entities = []
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
entity = mcrfpy.Entity(x, y)
|
||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||
grid.entities.append(entity)
|
||||
entities.append(entity)
|
||||
print(f"Entity {i+1} at ({x}, {y})")
|
||||
|
||||
# Highlight a path immediately
|
||||
if len(entities) >= 2:
|
||||
e1, e2 = entities[0], entities[1]
|
||||
print(f"\nCalculating path from Entity 1 ({e1.x}, {e1.y}) to Entity 2 ({e2.x}, {e2.y})...")
|
||||
|
||||
path = e1.path_to(int(e2.x), int(e2.y))
|
||||
print(f"Path found: {path}")
|
||||
print(f"Path length: {len(path)} steps")
|
||||
|
||||
if path:
|
||||
print("\nHighlighting path in bright green...")
|
||||
# Color start and end specially
|
||||
grid.at(int(e1.x), int(e1.y)).color = START_COLOR
|
||||
grid.at(int(e2.x), int(e2.y)).color = END_COLOR
|
||||
|
||||
# Color the path
|
||||
for i, (x, y) in enumerate(path):
|
||||
if i > 0 and i < len(path) - 1: # Skip start and end
|
||||
grid.at(x, y).color = PATH_COLOR
|
||||
print(f" Colored ({x}, {y}) green")
|
||||
|
||||
# Keypress handler
|
||||
def handle_keypress(scene_name, keycode):
|
||||
if keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
elif keycode == 32: # Space
|
||||
print("\nRefreshing path colors...")
|
||||
# Re-color the path to ensure it's visible
|
||||
if len(entities) >= 2 and path:
|
||||
for x, y in path[1:-1]:
|
||||
grid.at(x, y).color = PATH_COLOR
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("dijkstra_demo")
|
||||
ui.append(grid)
|
||||
|
||||
# Scale grid
|
||||
grid.size = (560, 400) # 14*40, 10*40
|
||||
grid.position = (120, 100)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Dijkstra Pathfinding - High Contrast", 200, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add legend
|
||||
legend1 = mcrfpy.Caption("Red=Start, Blue=End, Green=Path", 120, 520)
|
||||
legend1.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(legend1)
|
||||
|
||||
legend2 = mcrfpy.Caption("Press Q to quit, SPACE to refresh", 120, 540)
|
||||
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend2)
|
||||
|
||||
# Entity info
|
||||
info = mcrfpy.Caption(f"Path: Entity 1 to 2 = {len(path) if 'path' in locals() else 0} steps", 120, 60)
|
||||
info.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
ui.append(info)
|
||||
|
||||
# Set up input
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
mcrfpy.setScene("dijkstra_demo")
|
||||
|
||||
print("\nDemo ready! The path should be clearly visible in bright green.")
|
||||
print("Red = Start, Blue = End, Green = Path")
|
||||
print("Press SPACE to refresh colors if needed.")
|
|
@ -0,0 +1,306 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Exhaustive API Demo (Fixed)
|
||||
=======================================
|
||||
|
||||
Fixed version that properly exits after tests complete.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Test configuration
|
||||
VERBOSE = True # Print detailed information about each test
|
||||
|
||||
def print_section(title):
|
||||
"""Print a section header"""
|
||||
print("\n" + "="*60)
|
||||
print(f" {title}")
|
||||
print("="*60)
|
||||
|
||||
def print_test(test_name, success=True):
|
||||
"""Print test result"""
|
||||
status = "✓ PASS" if success else "✗ FAIL"
|
||||
print(f" {status} - {test_name}")
|
||||
|
||||
def test_color_api():
|
||||
"""Test all Color constructors and methods"""
|
||||
print_section("COLOR API TESTS")
|
||||
|
||||
# Constructor variants
|
||||
print("\n Constructors:")
|
||||
|
||||
# Empty constructor (defaults to white)
|
||||
c1 = mcrfpy.Color()
|
||||
print_test(f"Color() = ({c1.r}, {c1.g}, {c1.b}, {c1.a})")
|
||||
|
||||
# Single value (grayscale)
|
||||
c2 = mcrfpy.Color(128)
|
||||
print_test(f"Color(128) = ({c2.r}, {c2.g}, {c2.b}, {c2.a})")
|
||||
|
||||
# RGB only (alpha defaults to 255)
|
||||
c3 = mcrfpy.Color(255, 128, 0)
|
||||
print_test(f"Color(255, 128, 0) = ({c3.r}, {c3.g}, {c3.b}, {c3.a})")
|
||||
|
||||
# Full RGBA
|
||||
c4 = mcrfpy.Color(100, 150, 200, 128)
|
||||
print_test(f"Color(100, 150, 200, 128) = ({c4.r}, {c4.g}, {c4.b}, {c4.a})")
|
||||
|
||||
# Property access
|
||||
print("\n Properties:")
|
||||
c = mcrfpy.Color(10, 20, 30, 40)
|
||||
print_test(f"Initial: r={c.r}, g={c.g}, b={c.b}, a={c.a}")
|
||||
|
||||
c.r = 200
|
||||
c.g = 150
|
||||
c.b = 100
|
||||
c.a = 255
|
||||
print_test(f"After modification: r={c.r}, g={c.g}, b={c.b}, a={c.a}")
|
||||
|
||||
return True
|
||||
|
||||
def test_frame_api():
|
||||
"""Test all Frame constructors and methods"""
|
||||
print_section("FRAME API TESTS")
|
||||
|
||||
# Create a test scene
|
||||
mcrfpy.createScene("api_test")
|
||||
mcrfpy.setScene("api_test")
|
||||
ui = mcrfpy.sceneUI("api_test")
|
||||
|
||||
# Constructor variants
|
||||
print("\n Constructors:")
|
||||
|
||||
# Empty constructor
|
||||
f1 = mcrfpy.Frame()
|
||||
print_test(f"Frame() - pos=({f1.x}, {f1.y}), size=({f1.w}, {f1.h})")
|
||||
ui.append(f1)
|
||||
|
||||
# Position only
|
||||
f2 = mcrfpy.Frame(100, 50)
|
||||
print_test(f"Frame(100, 50) - pos=({f2.x}, {f2.y}), size=({f2.w}, {f2.h})")
|
||||
ui.append(f2)
|
||||
|
||||
# Position and size
|
||||
f3 = mcrfpy.Frame(200, 100, 150, 75)
|
||||
print_test(f"Frame(200, 100, 150, 75) - pos=({f3.x}, {f3.y}), size=({f3.w}, {f3.h})")
|
||||
ui.append(f3)
|
||||
|
||||
# Full constructor
|
||||
f4 = mcrfpy.Frame(300, 200, 200, 100,
|
||||
fill_color=mcrfpy.Color(100, 100, 200),
|
||||
outline_color=mcrfpy.Color(255, 255, 0),
|
||||
outline=3)
|
||||
print_test("Frame with all parameters")
|
||||
ui.append(f4)
|
||||
|
||||
# Properties
|
||||
print("\n Properties:")
|
||||
|
||||
# Position and size
|
||||
f = mcrfpy.Frame(10, 20, 30, 40)
|
||||
print_test(f"Initial: x={f.x}, y={f.y}, w={f.w}, h={f.h}")
|
||||
|
||||
f.x = 50
|
||||
f.y = 60
|
||||
f.w = 70
|
||||
f.h = 80
|
||||
print_test(f"Modified: x={f.x}, y={f.y}, w={f.w}, h={f.h}")
|
||||
|
||||
# Colors
|
||||
f.fill_color = mcrfpy.Color(255, 0, 0, 128)
|
||||
f.outline_color = mcrfpy.Color(0, 255, 0)
|
||||
f.outline = 5.0
|
||||
print_test(f"Colors set, outline={f.outline}")
|
||||
|
||||
# Visibility and opacity
|
||||
f.visible = False
|
||||
f.opacity = 0.5
|
||||
print_test(f"visible={f.visible}, opacity={f.opacity}")
|
||||
f.visible = True # Reset
|
||||
|
||||
# Z-index
|
||||
f.z_index = 10
|
||||
print_test(f"z_index={f.z_index}")
|
||||
|
||||
# Children collection
|
||||
child1 = mcrfpy.Frame(5, 5, 20, 20)
|
||||
child2 = mcrfpy.Frame(30, 5, 20, 20)
|
||||
f.children.append(child1)
|
||||
f.children.append(child2)
|
||||
print_test(f"children.count = {len(f.children)}")
|
||||
|
||||
return True
|
||||
|
||||
def test_caption_api():
|
||||
"""Test all Caption constructors and methods"""
|
||||
print_section("CAPTION API TESTS")
|
||||
|
||||
ui = mcrfpy.sceneUI("api_test")
|
||||
|
||||
# Constructor variants
|
||||
print("\n Constructors:")
|
||||
|
||||
# Empty constructor
|
||||
c1 = mcrfpy.Caption()
|
||||
print_test(f"Caption() - text='{c1.text}', pos=({c1.x}, {c1.y})")
|
||||
ui.append(c1)
|
||||
|
||||
# Text only
|
||||
c2 = mcrfpy.Caption("Hello World")
|
||||
print_test(f"Caption('Hello World') - pos=({c2.x}, {c2.y})")
|
||||
ui.append(c2)
|
||||
|
||||
# Text and position
|
||||
c3 = mcrfpy.Caption("Positioned Text", 100, 50)
|
||||
print_test(f"Caption('Positioned Text', 100, 50)")
|
||||
ui.append(c3)
|
||||
|
||||
# Full constructor
|
||||
c5 = mcrfpy.Caption("Styled Text", 300, 150,
|
||||
fill_color=mcrfpy.Color(255, 255, 0),
|
||||
outline_color=mcrfpy.Color(255, 0, 0),
|
||||
outline=2)
|
||||
print_test("Caption with all style parameters")
|
||||
ui.append(c5)
|
||||
|
||||
# Properties
|
||||
print("\n Properties:")
|
||||
|
||||
c = mcrfpy.Caption("Test Caption", 10, 20)
|
||||
|
||||
# Text
|
||||
c.text = "Modified Text"
|
||||
print_test(f"text = '{c.text}'")
|
||||
|
||||
# Position
|
||||
c.x = 50
|
||||
c.y = 60
|
||||
print_test(f"position = ({c.x}, {c.y})")
|
||||
|
||||
# Colors and style
|
||||
c.fill_color = mcrfpy.Color(0, 255, 255)
|
||||
c.outline_color = mcrfpy.Color(255, 0, 255)
|
||||
c.outline = 3.0
|
||||
print_test("Colors and outline set")
|
||||
|
||||
# Size (read-only, computed from text)
|
||||
print_test(f"size (computed) = ({c.w}, {c.h})")
|
||||
|
||||
return True
|
||||
|
||||
def test_animation_api():
|
||||
"""Test Animation class API"""
|
||||
print_section("ANIMATION API TESTS")
|
||||
|
||||
ui = mcrfpy.sceneUI("api_test")
|
||||
|
||||
print("\n Animation Constructors:")
|
||||
|
||||
# Basic animation
|
||||
anim1 = mcrfpy.Animation("x", 100.0, 2.0)
|
||||
print_test("Animation('x', 100.0, 2.0)")
|
||||
|
||||
# With easing
|
||||
anim2 = mcrfpy.Animation("y", 200.0, 3.0, "easeInOut")
|
||||
print_test("Animation with easing='easeInOut'")
|
||||
|
||||
# Delta mode
|
||||
anim3 = mcrfpy.Animation("w", 50.0, 1.5, "linear", delta=True)
|
||||
print_test("Animation with delta=True")
|
||||
|
||||
# Color animation (as tuple)
|
||||
anim4 = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 2.0)
|
||||
print_test("Animation with Color tuple target")
|
||||
|
||||
# Vector animation
|
||||
anim5 = mcrfpy.Animation("position", (10.0, 20.0), 2.5, "easeOutBounce")
|
||||
print_test("Animation with position tuple")
|
||||
|
||||
# Sprite sequence
|
||||
anim6 = mcrfpy.Animation("sprite_index", [0, 1, 2, 3, 2, 1], 2.0)
|
||||
print_test("Animation with sprite sequence")
|
||||
|
||||
# Properties
|
||||
print("\n Animation Properties:")
|
||||
|
||||
# Check properties
|
||||
print_test(f"property = '{anim1.property}'")
|
||||
print_test(f"duration = {anim1.duration}")
|
||||
print_test(f"elapsed = {anim1.elapsed}")
|
||||
print_test(f"is_complete = {anim1.is_complete}")
|
||||
print_test(f"is_delta = {anim3.is_delta}")
|
||||
|
||||
# Methods
|
||||
print("\n Animation Methods:")
|
||||
|
||||
# Create test frame
|
||||
frame = mcrfpy.Frame(50, 50, 100, 100)
|
||||
frame.fill_color = mcrfpy.Color(100, 100, 100)
|
||||
ui.append(frame)
|
||||
|
||||
# Start animation
|
||||
anim1.start(frame)
|
||||
print_test("start() called on frame")
|
||||
|
||||
# Test some easing functions
|
||||
print("\n Sample Easing Functions:")
|
||||
easings = ["linear", "easeIn", "easeOut", "easeInOut", "easeInBounce", "easeOutElastic"]
|
||||
|
||||
for easing in easings:
|
||||
try:
|
||||
test_anim = mcrfpy.Animation("x", 100.0, 1.0, easing)
|
||||
print_test(f"Easing '{easing}' ✓")
|
||||
except:
|
||||
print_test(f"Easing '{easing}' failed", False)
|
||||
|
||||
return True
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all API tests"""
|
||||
print("\n" + "="*60)
|
||||
print(" McRogueFace Exhaustive API Test Suite (Fixed)")
|
||||
print(" Testing constructors and methods...")
|
||||
print("="*60)
|
||||
|
||||
# Run each test category
|
||||
test_functions = [
|
||||
test_color_api,
|
||||
test_frame_api,
|
||||
test_caption_api,
|
||||
test_animation_api
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for test_func in test_functions:
|
||||
try:
|
||||
if test_func():
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"\n ERROR in {test_func.__name__}: {e}")
|
||||
failed += 1
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*60)
|
||||
print(f" TEST SUMMARY: {passed} passed, {failed} failed")
|
||||
print("="*60)
|
||||
|
||||
print("\n Visual elements are displayed in the 'api_test' scene.")
|
||||
print(" The test is complete.")
|
||||
|
||||
# Exit after a short delay to allow output to be seen
|
||||
def exit_test(runtime):
|
||||
print("\nExiting API test suite...")
|
||||
sys.exit(0)
|
||||
|
||||
mcrfpy.setTimer("exit", exit_test, 2000)
|
||||
|
||||
# Run the tests immediately
|
||||
print("Starting McRogueFace Exhaustive API Demo (Fixed)...")
|
||||
print("This will test constructors and methods.")
|
||||
|
||||
run_all_tests()
|
|
@ -0,0 +1,391 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Path & Vision Sizzle Reel
|
||||
=========================
|
||||
|
||||
A choreographed demo showing:
|
||||
- Smooth entity movement along paths
|
||||
- Camera following with grid center animation
|
||||
- Field of view updates as entities move
|
||||
- Dramatic perspective transitions with zoom effects
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 30, 30)
|
||||
FLOOR_COLOR = mcrfpy.Color(80, 80, 100)
|
||||
PATH_COLOR = mcrfpy.Color(120, 120, 180)
|
||||
DARK_FLOOR = mcrfpy.Color(40, 40, 50)
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
player = None
|
||||
enemy = None
|
||||
sequence_step = 0
|
||||
player_path = []
|
||||
enemy_path = []
|
||||
player_path_index = 0
|
||||
enemy_path_index = 0
|
||||
|
||||
def create_scene():
|
||||
"""Create the demo environment"""
|
||||
global grid, player, enemy
|
||||
|
||||
mcrfpy.createScene("path_vision_demo")
|
||||
|
||||
# Create larger grid for more dramatic movement
|
||||
grid = mcrfpy.Grid(grid_x=40, grid_y=25)
|
||||
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||
|
||||
# Map layout - interconnected rooms with corridors
|
||||
map_layout = [
|
||||
"########################################", # 0
|
||||
"#......##########......################", # 1
|
||||
"#......##########......################", # 2
|
||||
"#......##########......################", # 3
|
||||
"#......#.........#.....################", # 4
|
||||
"#......#.........#.....################", # 5
|
||||
"####.###.........####.#################", # 6
|
||||
"####.....................##############", # 7
|
||||
"####.....................##############", # 8
|
||||
"####.###.........####.#################", # 9
|
||||
"#......#.........#.....################", # 10
|
||||
"#......#.........#.....################", # 11
|
||||
"#......#.........#.....################", # 12
|
||||
"#......###.....###.....################", # 13
|
||||
"#......###.....###.....################", # 14
|
||||
"#......###.....###.....#########......#", # 15
|
||||
"#......###.....###.....#########......#", # 16
|
||||
"#......###.....###.....#########......#", # 17
|
||||
"#####.############.#############......#", # 18
|
||||
"#####...........................#.....#", # 19
|
||||
"#####...........................#.....#", # 20
|
||||
"#####.############.#############......#", # 21
|
||||
"#......###########.##########.........#", # 22
|
||||
"#......###########.##########.........#", # 23
|
||||
"########################################", # 24
|
||||
]
|
||||
|
||||
# Build the map
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
if char == '#':
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.color = WALL_COLOR
|
||||
else:
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
# Create player in top-left room
|
||||
player = mcrfpy.Entity(3, 3, grid=grid)
|
||||
player.sprite_index = 64 # @
|
||||
|
||||
# Create enemy in bottom-right area
|
||||
enemy = mcrfpy.Entity(35, 20, grid=grid)
|
||||
enemy.sprite_index = 69 # E
|
||||
|
||||
# Initial visibility
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
|
||||
# Set initial perspective to player
|
||||
grid.perspective = 0
|
||||
|
||||
def setup_paths():
|
||||
"""Define the paths for entities"""
|
||||
global player_path, enemy_path
|
||||
|
||||
# Player path: Top-left room → corridor → middle room
|
||||
player_waypoints = [
|
||||
(3, 3), # Start
|
||||
(3, 8), # Move down
|
||||
(7, 8), # Enter corridor
|
||||
(16, 8), # Through corridor
|
||||
(16, 12), # Enter middle room
|
||||
(12, 12), # Move in room
|
||||
(12, 16), # Move down
|
||||
(16, 16), # Move right
|
||||
(16, 19), # Exit room
|
||||
(25, 19), # Move right
|
||||
(30, 19), # Continue
|
||||
(35, 19), # Near enemy start
|
||||
]
|
||||
|
||||
# Enemy path: Bottom-right → around → approach player area
|
||||
enemy_waypoints = [
|
||||
(35, 20), # Start
|
||||
(30, 20), # Move left
|
||||
(25, 20), # Continue
|
||||
(20, 20), # Continue
|
||||
(16, 20), # Corridor junction
|
||||
(16, 16), # Move up (might see player)
|
||||
(16, 12), # Continue up
|
||||
(16, 8), # Top corridor
|
||||
(10, 8), # Move left
|
||||
(7, 8), # Continue
|
||||
(3, 8), # Player's area
|
||||
(3, 12), # Move down
|
||||
]
|
||||
|
||||
# Calculate full paths using pathfinding
|
||||
player_path = []
|
||||
for i in range(len(player_waypoints) - 1):
|
||||
x1, y1 = player_waypoints[i]
|
||||
x2, y2 = player_waypoints[i + 1]
|
||||
|
||||
# Use grid's A* pathfinding
|
||||
segment = grid.compute_astar_path(x1, y1, x2, y2)
|
||||
if segment:
|
||||
# Add segment (avoiding duplicates)
|
||||
if not player_path or segment[0] != player_path[-1]:
|
||||
player_path.extend(segment)
|
||||
else:
|
||||
player_path.extend(segment[1:])
|
||||
|
||||
enemy_path = []
|
||||
for i in range(len(enemy_waypoints) - 1):
|
||||
x1, y1 = enemy_waypoints[i]
|
||||
x2, y2 = enemy_waypoints[i + 1]
|
||||
|
||||
segment = grid.compute_astar_path(x1, y1, x2, y2)
|
||||
if segment:
|
||||
if not enemy_path or segment[0] != enemy_path[-1]:
|
||||
enemy_path.extend(segment)
|
||||
else:
|
||||
enemy_path.extend(segment[1:])
|
||||
|
||||
print(f"Player path: {len(player_path)} steps")
|
||||
print(f"Enemy path: {len(enemy_path)} steps")
|
||||
|
||||
def setup_ui():
|
||||
"""Create UI elements"""
|
||||
ui = mcrfpy.sceneUI("path_vision_demo")
|
||||
ui.append(grid)
|
||||
|
||||
# Position and size grid
|
||||
grid.position = (50, 80)
|
||||
grid.size = (700, 500) # Adjust based on zoom
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Path & Vision Sizzle Reel", 300, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Status
|
||||
global status_text, perspective_text
|
||||
status_text = mcrfpy.Caption("Starting demo...", 50, 50)
|
||||
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(status_text)
|
||||
|
||||
perspective_text = mcrfpy.Caption("Perspective: Player", 550, 50)
|
||||
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
ui.append(perspective_text)
|
||||
|
||||
# Controls
|
||||
controls = mcrfpy.Caption("Space: Pause/Resume | R: Restart | Q: Quit", 250, 600)
|
||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(controls)
|
||||
|
||||
# Animation control
|
||||
paused = False
|
||||
move_timer = 0
|
||||
zoom_transition = False
|
||||
|
||||
def move_entity_smooth(entity, target_x, target_y, duration=0.3):
|
||||
"""Smoothly animate entity to position"""
|
||||
# Create position animation
|
||||
anim_x = mcrfpy.Animation("x", float(target_x), duration, "easeInOut")
|
||||
anim_y = mcrfpy.Animation("y", float(target_y), duration, "easeInOut")
|
||||
|
||||
anim_x.start(entity)
|
||||
anim_y.start(entity)
|
||||
|
||||
def update_camera_smooth(center_x, center_y, duration=0.3):
|
||||
"""Smoothly move camera center"""
|
||||
# Convert grid coords to pixel coords (assuming 16x16 tiles)
|
||||
pixel_x = center_x * 16
|
||||
pixel_y = center_y * 16
|
||||
|
||||
anim = mcrfpy.Animation("center", (pixel_x, pixel_y), duration, "easeOut")
|
||||
anim.start(grid)
|
||||
|
||||
def start_perspective_transition():
|
||||
"""Begin the dramatic perspective shift"""
|
||||
global zoom_transition, sequence_step
|
||||
zoom_transition = True
|
||||
sequence_step = 100 # Special sequence number
|
||||
|
||||
status_text.text = "Perspective shift: Zooming out..."
|
||||
|
||||
# Zoom out with elastic easing
|
||||
zoom_out = mcrfpy.Animation("zoom", 0.5, 2.0, "easeInExpo")
|
||||
zoom_out.start(grid)
|
||||
|
||||
# Schedule the perspective switch
|
||||
mcrfpy.setTimer("switch_perspective", switch_perspective, 2100)
|
||||
|
||||
def switch_perspective(dt):
|
||||
"""Switch perspective at the peak of zoom"""
|
||||
global sequence_step
|
||||
|
||||
# Switch to enemy perspective
|
||||
grid.perspective = 1
|
||||
perspective_text.text = "Perspective: Enemy"
|
||||
perspective_text.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
|
||||
status_text.text = "Perspective shift: Following enemy..."
|
||||
|
||||
# Update camera to enemy position
|
||||
update_camera_smooth(enemy.x, enemy.y, 0.1)
|
||||
|
||||
# Zoom back in
|
||||
zoom_in = mcrfpy.Animation("zoom", 1.2, 2.0, "easeOutExpo")
|
||||
zoom_in.start(grid)
|
||||
|
||||
# Resume sequence
|
||||
mcrfpy.setTimer("resume_enemy", resume_enemy_sequence, 2100)
|
||||
|
||||
# Cancel this timer
|
||||
mcrfpy.delTimer("switch_perspective")
|
||||
|
||||
def resume_enemy_sequence(dt):
|
||||
"""Resume following enemy after perspective shift"""
|
||||
global sequence_step, zoom_transition
|
||||
zoom_transition = False
|
||||
sequence_step = 101 # Continue with enemy movement
|
||||
mcrfpy.delTimer("resume_enemy")
|
||||
|
||||
def sequence_tick(dt):
|
||||
"""Main sequence controller"""
|
||||
global sequence_step, player_path_index, enemy_path_index, move_timer
|
||||
|
||||
if paused or zoom_transition:
|
||||
return
|
||||
|
||||
move_timer += dt
|
||||
if move_timer < 400: # Move every 400ms
|
||||
return
|
||||
move_timer = 0
|
||||
|
||||
if sequence_step < 50:
|
||||
# Phase 1: Follow player movement
|
||||
if player_path_index < len(player_path):
|
||||
x, y = player_path[player_path_index]
|
||||
move_entity_smooth(player, x, y)
|
||||
player.update_visibility()
|
||||
|
||||
# Camera follows player
|
||||
if grid.perspective == 0:
|
||||
update_camera_smooth(player.x, player.y)
|
||||
|
||||
player_path_index += 1
|
||||
status_text.text = f"Player moving... Step {player_path_index}/{len(player_path)}"
|
||||
|
||||
# Start enemy movement after player has moved a bit
|
||||
if player_path_index == 10:
|
||||
sequence_step = 1 # Enable enemy movement
|
||||
else:
|
||||
# Player reached destination, start perspective transition
|
||||
start_perspective_transition()
|
||||
|
||||
if sequence_step >= 1 and sequence_step < 50:
|
||||
# Phase 2: Enemy movement (concurrent with player)
|
||||
if enemy_path_index < len(enemy_path):
|
||||
x, y = enemy_path[enemy_path_index]
|
||||
move_entity_smooth(enemy, x, y)
|
||||
enemy.update_visibility()
|
||||
|
||||
# Check if enemy is visible to player
|
||||
if grid.perspective == 0:
|
||||
enemy_cell_idx = int(enemy.y) * grid.grid_x + int(enemy.x)
|
||||
if enemy_cell_idx < len(player.gridstate) and player.gridstate[enemy_cell_idx].visible:
|
||||
status_text.text = "Enemy spotted!"
|
||||
|
||||
enemy_path_index += 1
|
||||
|
||||
elif sequence_step == 101:
|
||||
# Phase 3: Continue following enemy after perspective shift
|
||||
if enemy_path_index < len(enemy_path):
|
||||
x, y = enemy_path[enemy_path_index]
|
||||
move_entity_smooth(enemy, x, y)
|
||||
enemy.update_visibility()
|
||||
|
||||
# Camera follows enemy
|
||||
update_camera_smooth(enemy.x, enemy.y)
|
||||
|
||||
enemy_path_index += 1
|
||||
status_text.text = f"Following enemy... Step {enemy_path_index}/{len(enemy_path)}"
|
||||
else:
|
||||
status_text.text = "Demo complete! Press R to restart"
|
||||
sequence_step = 200 # Done
|
||||
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input"""
|
||||
global paused, sequence_step, player_path_index, enemy_path_index, move_timer
|
||||
key = key.lower()
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if key == "q":
|
||||
print("Exiting sizzle reel...")
|
||||
sys.exit(0)
|
||||
elif key == "space":
|
||||
paused = not paused
|
||||
status_text.text = "PAUSED" if paused else "Running..."
|
||||
elif key == "r":
|
||||
# Reset everything
|
||||
player.x, player.y = 3, 3
|
||||
enemy.x, enemy.y = 35, 20
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
grid.perspective = 0
|
||||
perspective_text.text = "Perspective: Player"
|
||||
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
sequence_step = 0
|
||||
player_path_index = 0
|
||||
enemy_path_index = 0
|
||||
move_timer = 0
|
||||
update_camera_smooth(player.x, player.y, 0.5)
|
||||
|
||||
# Reset zoom
|
||||
zoom_reset = mcrfpy.Animation("zoom", 1.2, 0.5, "easeOut")
|
||||
zoom_reset.start(grid)
|
||||
|
||||
status_text.text = "Demo restarted!"
|
||||
|
||||
# Initialize everything
|
||||
print("Path & Vision Sizzle Reel")
|
||||
print("=========================")
|
||||
print("Demonstrating:")
|
||||
print("- Smooth entity movement along calculated paths")
|
||||
print("- Camera following with animated grid centering")
|
||||
print("- Field of view updates as entities move")
|
||||
print("- Dramatic perspective transitions with zoom effects")
|
||||
print()
|
||||
|
||||
create_scene()
|
||||
setup_paths()
|
||||
setup_ui()
|
||||
|
||||
# Set scene and input
|
||||
mcrfpy.setScene("path_vision_demo")
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Initial camera setup
|
||||
grid.zoom = 1.2
|
||||
update_camera_smooth(player.x, player.y, 0.1)
|
||||
|
||||
# Start the sequence
|
||||
mcrfpy.setTimer("sequence", sequence_tick, 50) # Tick every 50ms
|
||||
|
||||
print("Demo started!")
|
||||
print("- Player (@) will navigate through rooms")
|
||||
print("- Enemy (E) will move on a different path")
|
||||
print("- Watch for the dramatic perspective shift!")
|
||||
print()
|
||||
print("Controls: Space=Pause, R=Restart, Q=Quit")
|
|
@ -0,0 +1,377 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pathfinding Showcase Demo
|
||||
=========================
|
||||
|
||||
Demonstrates various pathfinding scenarios with multiple entities.
|
||||
|
||||
Features:
|
||||
- Multiple entities pathfinding simultaneously
|
||||
- Chase mode: entities pursue targets
|
||||
- Flee mode: entities avoid threats
|
||||
- Patrol mode: entities follow waypoints
|
||||
- Visual debugging: show Dijkstra distance field
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
import random
|
||||
|
||||
# Colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 40, 40)
|
||||
FLOOR_COLOR = mcrfpy.Color(220, 220, 240)
|
||||
PATH_COLOR = mcrfpy.Color(180, 250, 180)
|
||||
THREAT_COLOR = mcrfpy.Color(255, 100, 100)
|
||||
GOAL_COLOR = mcrfpy.Color(100, 255, 100)
|
||||
DIJKSTRA_COLORS = [
|
||||
mcrfpy.Color(50, 50, 100), # Far
|
||||
mcrfpy.Color(70, 70, 150),
|
||||
mcrfpy.Color(90, 90, 200),
|
||||
mcrfpy.Color(110, 110, 250),
|
||||
mcrfpy.Color(150, 150, 255),
|
||||
mcrfpy.Color(200, 200, 255), # Near
|
||||
]
|
||||
|
||||
# Entity types
|
||||
PLAYER = 64 # @
|
||||
ENEMY = 69 # E
|
||||
TREASURE = 36 # $
|
||||
PATROL = 80 # P
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
player = None
|
||||
enemies = []
|
||||
treasures = []
|
||||
patrol_entities = []
|
||||
mode = "CHASE"
|
||||
show_dijkstra = False
|
||||
animation_speed = 3.0
|
||||
|
||||
# Track waypoints separately since Entity doesn't have custom attributes
|
||||
entity_waypoints = {} # entity -> [(x, y), ...]
|
||||
entity_waypoint_indices = {} # entity -> current index
|
||||
|
||||
def create_dungeon():
|
||||
"""Create a dungeon-like map"""
|
||||
global grid
|
||||
|
||||
mcrfpy.createScene("pathfinding_showcase")
|
||||
|
||||
# Create larger grid for showcase
|
||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Initialize all as floor
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
grid.at(x, y).walkable = True
|
||||
grid.at(x, y).transparent = True
|
||||
grid.at(x, y).color = FLOOR_COLOR
|
||||
|
||||
# Create rooms and corridors
|
||||
rooms = [
|
||||
(2, 2, 8, 6), # Top-left room
|
||||
(20, 2, 8, 6), # Top-right room
|
||||
(11, 8, 8, 6), # Center room
|
||||
(2, 14, 8, 5), # Bottom-left room
|
||||
(20, 14, 8, 5), # Bottom-right room
|
||||
]
|
||||
|
||||
# Create room walls
|
||||
for rx, ry, rw, rh in rooms:
|
||||
# Top and bottom walls
|
||||
for x in range(rx, rx + rw):
|
||||
if 0 <= x < 30:
|
||||
grid.at(x, ry).walkable = False
|
||||
grid.at(x, ry).color = WALL_COLOR
|
||||
grid.at(x, ry + rh - 1).walkable = False
|
||||
grid.at(x, ry + rh - 1).color = WALL_COLOR
|
||||
|
||||
# Left and right walls
|
||||
for y in range(ry, ry + rh):
|
||||
if 0 <= y < 20:
|
||||
grid.at(rx, y).walkable = False
|
||||
grid.at(rx, y).color = WALL_COLOR
|
||||
grid.at(rx + rw - 1, y).walkable = False
|
||||
grid.at(rx + rw - 1, y).color = WALL_COLOR
|
||||
|
||||
# Create doorways
|
||||
doorways = [
|
||||
(6, 2), (24, 2), # Top room doors
|
||||
(6, 7), (24, 7), # Top room doors bottom
|
||||
(15, 8), (15, 13), # Center room doors
|
||||
(6, 14), (24, 14), # Bottom room doors
|
||||
(11, 11), (18, 11), # Center room side doors
|
||||
]
|
||||
|
||||
for x, y in doorways:
|
||||
if 0 <= x < 30 and 0 <= y < 20:
|
||||
grid.at(x, y).walkable = True
|
||||
grid.at(x, y).color = FLOOR_COLOR
|
||||
|
||||
# Add some corridors
|
||||
# Horizontal corridors
|
||||
for x in range(10, 20):
|
||||
grid.at(x, 5).walkable = True
|
||||
grid.at(x, 5).color = FLOOR_COLOR
|
||||
grid.at(x, 16).walkable = True
|
||||
grid.at(x, 16).color = FLOOR_COLOR
|
||||
|
||||
# Vertical corridors
|
||||
for y in range(5, 17):
|
||||
grid.at(10, y).walkable = True
|
||||
grid.at(10, y).color = FLOOR_COLOR
|
||||
grid.at(19, y).walkable = True
|
||||
grid.at(19, y).color = FLOOR_COLOR
|
||||
|
||||
def spawn_entities():
|
||||
"""Spawn various entity types"""
|
||||
global player, enemies, treasures, patrol_entities
|
||||
|
||||
# Clear existing entities
|
||||
#grid.entities.clear()
|
||||
enemies = []
|
||||
treasures = []
|
||||
patrol_entities = []
|
||||
|
||||
# Spawn player in center room
|
||||
player = mcrfpy.Entity((15, 11), mcrfpy.default_texture, PLAYER)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Spawn enemies in corners
|
||||
enemy_positions = [(4, 4), (24, 4), (4, 16), (24, 16)]
|
||||
for x, y in enemy_positions:
|
||||
enemy = mcrfpy.Entity((x, y), mcrfpy.default_texture, ENEMY)
|
||||
grid.entities.append(enemy)
|
||||
enemies.append(enemy)
|
||||
|
||||
# Spawn treasures
|
||||
treasure_positions = [(6, 5), (24, 5), (15, 10)]
|
||||
for x, y in treasure_positions:
|
||||
treasure = mcrfpy.Entity((x, y), mcrfpy.default_texture, TREASURE)
|
||||
grid.entities.append(treasure)
|
||||
treasures.append(treasure)
|
||||
|
||||
# Spawn patrol entities
|
||||
patrol = mcrfpy.Entity((10, 10), mcrfpy.default_texture, PATROL)
|
||||
# Store waypoints separately since Entity doesn't support custom attributes
|
||||
entity_waypoints[patrol] = [(10, 10), (19, 10), (19, 16), (10, 16)] # Square patrol
|
||||
entity_waypoint_indices[patrol] = 0
|
||||
grid.entities.append(patrol)
|
||||
patrol_entities.append(patrol)
|
||||
|
||||
def visualize_dijkstra(target_x, target_y):
|
||||
"""Visualize Dijkstra distance field"""
|
||||
if not show_dijkstra:
|
||||
return
|
||||
|
||||
# Compute Dijkstra from target
|
||||
grid.compute_dijkstra(target_x, target_y)
|
||||
|
||||
# Color tiles based on distance
|
||||
max_dist = 30.0
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
if grid.at(x, y).walkable:
|
||||
dist = grid.get_dijkstra_distance(x, y)
|
||||
if dist is not None and dist < max_dist:
|
||||
# Map distance to color index
|
||||
color_idx = int((dist / max_dist) * len(DIJKSTRA_COLORS))
|
||||
color_idx = min(color_idx, len(DIJKSTRA_COLORS) - 1)
|
||||
grid.at(x, y).color = DIJKSTRA_COLORS[color_idx]
|
||||
|
||||
def move_enemies(dt):
|
||||
"""Move enemies based on current mode"""
|
||||
if mode == "CHASE":
|
||||
# Enemies chase player
|
||||
for enemy in enemies:
|
||||
path = enemy.path_to(int(player.x), int(player.y))
|
||||
if path and len(path) > 1: # Don't move onto player
|
||||
# Move towards player
|
||||
next_x, next_y = path[1]
|
||||
# Smooth movement
|
||||
dx = next_x - enemy.x
|
||||
dy = next_y - enemy.y
|
||||
enemy.x += dx * dt * animation_speed
|
||||
enemy.y += dy * dt * animation_speed
|
||||
|
||||
elif mode == "FLEE":
|
||||
# Enemies flee from player
|
||||
for enemy in enemies:
|
||||
# Compute opposite direction
|
||||
dx = enemy.x - player.x
|
||||
dy = enemy.y - player.y
|
||||
|
||||
# Find safe spot in that direction
|
||||
target_x = int(enemy.x + dx * 2)
|
||||
target_y = int(enemy.y + dy * 2)
|
||||
|
||||
# Clamp to grid
|
||||
target_x = max(0, min(29, target_x))
|
||||
target_y = max(0, min(19, target_y))
|
||||
|
||||
path = enemy.path_to(target_x, target_y)
|
||||
if path and len(path) > 0:
|
||||
next_x, next_y = path[0]
|
||||
# Move away from player
|
||||
dx = next_x - enemy.x
|
||||
dy = next_y - enemy.y
|
||||
enemy.x += dx * dt * animation_speed
|
||||
enemy.y += dy * dt * animation_speed
|
||||
|
||||
def move_patrols(dt):
|
||||
"""Move patrol entities along waypoints"""
|
||||
for patrol in patrol_entities:
|
||||
if patrol not in entity_waypoints:
|
||||
continue
|
||||
|
||||
# Get current waypoint
|
||||
waypoints = entity_waypoints[patrol]
|
||||
waypoint_index = entity_waypoint_indices[patrol]
|
||||
target_x, target_y = waypoints[waypoint_index]
|
||||
|
||||
# Check if reached waypoint
|
||||
dist = abs(patrol.x - target_x) + abs(patrol.y - target_y)
|
||||
if dist < 0.5:
|
||||
# Move to next waypoint
|
||||
entity_waypoint_indices[patrol] = (waypoint_index + 1) % len(waypoints)
|
||||
waypoint_index = entity_waypoint_indices[patrol]
|
||||
target_x, target_y = waypoints[waypoint_index]
|
||||
|
||||
# Path to waypoint
|
||||
path = patrol.path_to(target_x, target_y)
|
||||
if path and len(path) > 0:
|
||||
next_x, next_y = path[0]
|
||||
dx = next_x - patrol.x
|
||||
dy = next_y - patrol.y
|
||||
patrol.x += dx * dt * animation_speed * 0.5 # Slower patrol speed
|
||||
patrol.y += dy * dt * animation_speed * 0.5
|
||||
|
||||
def update_entities(dt):
|
||||
"""Update all entity movements"""
|
||||
move_enemies(dt / 1000.0) # Convert to seconds
|
||||
move_patrols(dt / 1000.0)
|
||||
|
||||
# Update Dijkstra visualization
|
||||
if show_dijkstra and player:
|
||||
visualize_dijkstra(int(player.x), int(player.y))
|
||||
|
||||
def handle_keypress(scene_name, keycode):
|
||||
"""Handle keyboard input"""
|
||||
global mode, show_dijkstra, player
|
||||
|
||||
# Mode switching
|
||||
if keycode == 49: # '1'
|
||||
mode = "CHASE"
|
||||
mode_text.text = "Mode: CHASE - Enemies pursue player"
|
||||
clear_colors()
|
||||
elif keycode == 50: # '2'
|
||||
mode = "FLEE"
|
||||
mode_text.text = "Mode: FLEE - Enemies avoid player"
|
||||
clear_colors()
|
||||
elif keycode == 51: # '3'
|
||||
mode = "PATROL"
|
||||
mode_text.text = "Mode: PATROL - Entities follow waypoints"
|
||||
clear_colors()
|
||||
|
||||
# Toggle Dijkstra visualization
|
||||
elif keycode == 68 or keycode == 100: # 'D' or 'd'
|
||||
show_dijkstra = not show_dijkstra
|
||||
debug_text.text = f"Dijkstra Debug: {'ON' if show_dijkstra else 'OFF'}"
|
||||
if not show_dijkstra:
|
||||
clear_colors()
|
||||
|
||||
# Move player with arrow keys or WASD
|
||||
elif keycode in [87, 119]: # W/w - Up
|
||||
if player.y > 0:
|
||||
path = player.path_to(int(player.x), int(player.y) - 1)
|
||||
if path:
|
||||
player.y -= 1
|
||||
elif keycode in [83, 115]: # S/s - Down
|
||||
if player.y < 19:
|
||||
path = player.path_to(int(player.x), int(player.y) + 1)
|
||||
if path:
|
||||
player.y += 1
|
||||
elif keycode in [65, 97]: # A/a - Left
|
||||
if player.x > 0:
|
||||
path = player.path_to(int(player.x) - 1, int(player.y))
|
||||
if path:
|
||||
player.x -= 1
|
||||
elif keycode in [68, 100]: # D/d - Right
|
||||
if player.x < 29:
|
||||
path = player.path_to(int(player.x) + 1, int(player.y))
|
||||
if path:
|
||||
player.x += 1
|
||||
|
||||
# Reset
|
||||
elif keycode == 82 or keycode == 114: # 'R' or 'r'
|
||||
spawn_entities()
|
||||
clear_colors()
|
||||
|
||||
# Quit
|
||||
elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
|
||||
print("\nExiting pathfinding showcase...")
|
||||
sys.exit(0)
|
||||
|
||||
def clear_colors():
|
||||
"""Reset floor colors"""
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
if grid.at(x, y).walkable:
|
||||
grid.at(x, y).color = FLOOR_COLOR
|
||||
|
||||
# Create the showcase
|
||||
print("Pathfinding Showcase Demo")
|
||||
print("=========================")
|
||||
print("Controls:")
|
||||
print(" WASD - Move player")
|
||||
print(" 1 - Chase mode (enemies pursue)")
|
||||
print(" 2 - Flee mode (enemies avoid)")
|
||||
print(" 3 - Patrol mode")
|
||||
print(" D - Toggle Dijkstra visualization")
|
||||
print(" R - Reset entities")
|
||||
print(" Q/ESC - Quit")
|
||||
|
||||
# Create dungeon
|
||||
create_dungeon()
|
||||
spawn_entities()
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("pathfinding_showcase")
|
||||
ui.append(grid)
|
||||
|
||||
# Scale and position
|
||||
grid.size = (750, 500) # 30*25, 20*25
|
||||
grid.position = (25, 60)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Pathfinding Showcase", 300, 10)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add mode text
|
||||
mode_text = mcrfpy.Caption("Mode: CHASE - Enemies pursue player", 25, 580)
|
||||
mode_text.fill_color = mcrfpy.Color(255, 255, 200)
|
||||
ui.append(mode_text)
|
||||
|
||||
# Add debug text
|
||||
debug_text = mcrfpy.Caption("Dijkstra Debug: OFF", 25, 600)
|
||||
debug_text.fill_color = mcrfpy.Color(200, 200, 255)
|
||||
ui.append(debug_text)
|
||||
|
||||
# Add legend
|
||||
legend = mcrfpy.Caption("@ Player E Enemy $ Treasure P Patrol", 25, 620)
|
||||
legend.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend)
|
||||
|
||||
# Set up input handling
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
# Set up animation timer
|
||||
mcrfpy.setTimer("entities", update_entities, 16) # 60 FPS
|
||||
|
||||
# Show scene
|
||||
mcrfpy.setScene("pathfinding_showcase")
|
||||
|
||||
print("\nShowcase ready! Move with WASD and watch entities react.")
|
|
@ -0,0 +1,226 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple Text Input Widget for McRogueFace
|
||||
Minimal implementation focusing on core functionality
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
|
||||
class TextInput:
|
||||
"""Simple text input widget"""
|
||||
def __init__(self, x, y, width, label=""):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.label = label
|
||||
self.text = ""
|
||||
self.cursor_pos = 0
|
||||
self.focused = False
|
||||
|
||||
# Create UI elements
|
||||
self.frame = mcrfpy.Frame(self.x, self.y, self.width, 24)
|
||||
self.frame.fill_color = (255, 255, 255, 255)
|
||||
self.frame.outline_color = (128, 128, 128, 255)
|
||||
self.frame.outline = 2
|
||||
|
||||
# Label
|
||||
if self.label:
|
||||
self.label_caption = mcrfpy.Caption(self.label, self.x, self.y - 20)
|
||||
self.label_caption.fill_color = (255, 255, 255, 255)
|
||||
|
||||
# Text display
|
||||
self.text_caption = mcrfpy.Caption("", self.x + 4, self.y + 4)
|
||||
self.text_caption.fill_color = (0, 0, 0, 255)
|
||||
|
||||
# Cursor (a simple vertical line using a frame)
|
||||
self.cursor = mcrfpy.Frame(self.x + 4, self.y + 4, 2, 16)
|
||||
self.cursor.fill_color = (0, 0, 0, 255)
|
||||
self.cursor.visible = False
|
||||
|
||||
# Click handler
|
||||
self.frame.click = self._on_click
|
||||
|
||||
def _on_click(self, x, y, button):
|
||||
"""Handle clicks"""
|
||||
if button == 1: # Left click
|
||||
# Request focus
|
||||
global current_focus
|
||||
if current_focus and current_focus != self:
|
||||
current_focus.blur()
|
||||
current_focus = self
|
||||
self.focus()
|
||||
|
||||
def focus(self):
|
||||
"""Give focus to this input"""
|
||||
self.focused = True
|
||||
self.frame.outline_color = (0, 120, 255, 255)
|
||||
self.frame.outline = 3
|
||||
self.cursor.visible = True
|
||||
self._update_cursor()
|
||||
|
||||
def blur(self):
|
||||
"""Remove focus"""
|
||||
self.focused = False
|
||||
self.frame.outline_color = (128, 128, 128, 255)
|
||||
self.frame.outline = 2
|
||||
self.cursor.visible = False
|
||||
|
||||
def handle_key(self, key):
|
||||
"""Process keyboard input"""
|
||||
if not self.focused:
|
||||
return False
|
||||
|
||||
if key == "BackSpace":
|
||||
if self.cursor_pos > 0:
|
||||
self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:]
|
||||
self.cursor_pos -= 1
|
||||
elif key == "Delete":
|
||||
if self.cursor_pos < len(self.text):
|
||||
self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:]
|
||||
elif key == "Left":
|
||||
self.cursor_pos = max(0, self.cursor_pos - 1)
|
||||
elif key == "Right":
|
||||
self.cursor_pos = min(len(self.text), self.cursor_pos + 1)
|
||||
elif key == "Home":
|
||||
self.cursor_pos = 0
|
||||
elif key == "End":
|
||||
self.cursor_pos = len(self.text)
|
||||
elif len(key) == 1 and key.isprintable():
|
||||
self.text = self.text[:self.cursor_pos] + key + self.text[self.cursor_pos:]
|
||||
self.cursor_pos += 1
|
||||
else:
|
||||
return False
|
||||
|
||||
self._update_display()
|
||||
return True
|
||||
|
||||
def _update_display(self):
|
||||
"""Update text display"""
|
||||
self.text_caption.text = self.text
|
||||
self._update_cursor()
|
||||
|
||||
def _update_cursor(self):
|
||||
"""Update cursor position"""
|
||||
if self.focused:
|
||||
# Estimate character width (roughly 10 pixels per char)
|
||||
self.cursor.x = self.x + 4 + (self.cursor_pos * 10)
|
||||
|
||||
def add_to_scene(self, scene):
|
||||
"""Add all components to scene"""
|
||||
scene.append(self.frame)
|
||||
if hasattr(self, 'label_caption'):
|
||||
scene.append(self.label_caption)
|
||||
scene.append(self.text_caption)
|
||||
scene.append(self.cursor)
|
||||
|
||||
|
||||
# Global focus tracking
|
||||
current_focus = None
|
||||
text_inputs = []
|
||||
|
||||
|
||||
def demo_test(timer_name):
|
||||
"""Run automated demo after scene loads"""
|
||||
print("\n=== Text Input Widget Demo ===")
|
||||
|
||||
# Test typing in first field
|
||||
print("Testing first input field...")
|
||||
text_inputs[0].focus()
|
||||
for char in "Hello":
|
||||
text_inputs[0].handle_key(char)
|
||||
|
||||
print(f"First field contains: '{text_inputs[0].text}'")
|
||||
|
||||
# Test second field
|
||||
print("\nTesting second input field...")
|
||||
text_inputs[1].focus()
|
||||
for char in "World":
|
||||
text_inputs[1].handle_key(char)
|
||||
|
||||
print(f"Second field contains: '{text_inputs[1].text}'")
|
||||
|
||||
# Test text operations
|
||||
print("\nTesting cursor movement and deletion...")
|
||||
text_inputs[1].handle_key("Home")
|
||||
text_inputs[1].handle_key("Delete")
|
||||
print(f"After delete at start: '{text_inputs[1].text}'")
|
||||
|
||||
text_inputs[1].handle_key("End")
|
||||
text_inputs[1].handle_key("BackSpace")
|
||||
print(f"After backspace at end: '{text_inputs[1].text}'")
|
||||
|
||||
print("\n=== Demo Complete! ===")
|
||||
print("Text input widget is working successfully!")
|
||||
print("Features demonstrated:")
|
||||
print(" - Text entry")
|
||||
print(" - Focus management (blue outline)")
|
||||
print(" - Cursor positioning")
|
||||
print(" - Delete/Backspace operations")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def create_scene():
|
||||
"""Create the demo scene"""
|
||||
global text_inputs
|
||||
|
||||
mcrfpy.createScene("demo")
|
||||
scene = mcrfpy.sceneUI("demo")
|
||||
|
||||
# Background
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600)
|
||||
bg.fill_color = (40, 40, 40, 255)
|
||||
scene.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Text Input Widget Demo", 10, 10)
|
||||
title.fill_color = (255, 255, 255, 255)
|
||||
scene.append(title)
|
||||
|
||||
# Create input fields
|
||||
input1 = TextInput(50, 100, 300, "Name:")
|
||||
input1.add_to_scene(scene)
|
||||
text_inputs.append(input1)
|
||||
|
||||
input2 = TextInput(50, 160, 300, "Email:")
|
||||
input2.add_to_scene(scene)
|
||||
text_inputs.append(input2)
|
||||
|
||||
input3 = TextInput(50, 220, 400, "Comment:")
|
||||
input3.add_to_scene(scene)
|
||||
text_inputs.append(input3)
|
||||
|
||||
# Status text
|
||||
status = mcrfpy.Caption("Click to focus, type to enter text", 50, 280)
|
||||
status.fill_color = (200, 200, 200, 255)
|
||||
scene.append(status)
|
||||
|
||||
# Keyboard handler
|
||||
def handle_keys(scene_name, key):
|
||||
global current_focus, text_inputs
|
||||
|
||||
# Tab to switch fields
|
||||
if key == "Tab" and current_focus:
|
||||
idx = text_inputs.index(current_focus)
|
||||
next_idx = (idx + 1) % len(text_inputs)
|
||||
text_inputs[next_idx]._on_click(0, 0, 1)
|
||||
else:
|
||||
# Pass to focused input
|
||||
if current_focus:
|
||||
current_focus.handle_key(key)
|
||||
# Update status
|
||||
texts = [inp.text for inp in text_inputs]
|
||||
status.text = f"Values: {texts[0]} | {texts[1]} | {texts[2]}"
|
||||
|
||||
mcrfpy.keypressScene("demo", handle_keys)
|
||||
mcrfpy.setScene("demo")
|
||||
|
||||
# Schedule test
|
||||
mcrfpy.setTimer("test", demo_test, 500)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting simple text input demo...")
|
||||
create_scene()
|
|
@ -0,0 +1,190 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Animation Sizzle Reel - Final Version
|
||||
=================================================
|
||||
|
||||
Complete demonstration of all animation capabilities.
|
||||
This version works properly with the game loop and avoids API issues.
|
||||
|
||||
WARNING: This demo causes a segmentation fault due to a bug in the
|
||||
AnimationManager. When UI elements with active animations are removed
|
||||
from the scene, the AnimationManager crashes when trying to update them.
|
||||
|
||||
Use sizzle_reel_final_fixed.py instead, which works around this issue
|
||||
by hiding objects off-screen instead of removing them.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Configuration
|
||||
DEMO_DURATION = 6.0 # Duration for each demo
|
||||
|
||||
# All available easing functions
|
||||
EASING_FUNCTIONS = [
|
||||
"linear", "easeIn", "easeOut", "easeInOut",
|
||||
"easeInQuad", "easeOutQuad", "easeInOutQuad",
|
||||
"easeInCubic", "easeOutCubic", "easeInOutCubic",
|
||||
"easeInQuart", "easeOutQuart", "easeInOutQuart",
|
||||
"easeInSine", "easeOutSine", "easeInOutSine",
|
||||
"easeInExpo", "easeOutExpo", "easeInOutExpo",
|
||||
"easeInCirc", "easeOutCirc", "easeInOutCirc",
|
||||
"easeInElastic", "easeOutElastic", "easeInOutElastic",
|
||||
"easeInBack", "easeOutBack", "easeInOutBack",
|
||||
"easeInBounce", "easeOutBounce", "easeInOutBounce"
|
||||
]
|
||||
|
||||
# Track demo state
|
||||
current_demo = 0
|
||||
subtitle = None
|
||||
|
||||
def create_scene():
|
||||
"""Create the demo scene"""
|
||||
mcrfpy.createScene("demo")
|
||||
mcrfpy.setScene("demo")
|
||||
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Animation Sizzle Reel", 500, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
title.outline = 2
|
||||
title.font_size = 28
|
||||
ui.append(title)
|
||||
|
||||
# Subtitle
|
||||
global subtitle
|
||||
subtitle = mcrfpy.Caption("Starting...", 450, 60)
|
||||
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(subtitle)
|
||||
|
||||
return ui
|
||||
|
||||
def demo1_frame_animations():
|
||||
"""Frame position, size, and color animations"""
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 1: Frame Animations"
|
||||
|
||||
# Create frame
|
||||
f = mcrfpy.Frame(100, 150, 200, 100)
|
||||
f.fill_color = mcrfpy.Color(50, 50, 150)
|
||||
f.outline = 3
|
||||
f.outline_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(f)
|
||||
|
||||
# Animate properties
|
||||
mcrfpy.Animation("x", 600.0, 2.0, "easeInOutBack").start(f)
|
||||
mcrfpy.Animation("y", 300.0, 2.0, "easeInOutElastic").start(f)
|
||||
mcrfpy.Animation("w", 300.0, 2.5, "easeInOutCubic").start(f)
|
||||
mcrfpy.Animation("h", 150.0, 2.5, "easeInOutCubic").start(f)
|
||||
mcrfpy.Animation("fill_color", (255, 100, 50, 200), 3.0, "easeInOutSine").start(f)
|
||||
mcrfpy.Animation("outline", 8.0, 3.0, "easeInOutQuad").start(f)
|
||||
|
||||
def demo2_caption_animations():
|
||||
"""Caption movement and text effects"""
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 2: Caption Animations"
|
||||
|
||||
# Moving caption
|
||||
c1 = mcrfpy.Caption("Bouncing Text!", 100, 200)
|
||||
c1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
c1.font_size = 28
|
||||
ui.append(c1)
|
||||
mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1)
|
||||
|
||||
# Color cycling
|
||||
c2 = mcrfpy.Caption("Color Cycle", 400, 300)
|
||||
c2.outline = 2
|
||||
c2.font_size = 28
|
||||
ui.append(c2)
|
||||
mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear").start(c2)
|
||||
|
||||
# Typewriter effect
|
||||
c3 = mcrfpy.Caption("", 100, 400)
|
||||
c3.fill_color = mcrfpy.Color(0, 255, 255)
|
||||
c3.font_size = 28
|
||||
ui.append(c3)
|
||||
mcrfpy.Animation("text", "Typewriter effect animation...", 3.0, "linear").start(c3)
|
||||
|
||||
def demo3_easing_showcase():
|
||||
"""Show all 30 easing functions"""
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 3: All 30 Easing Functions"
|
||||
|
||||
# Create a small frame for each easing
|
||||
for i, easing in enumerate(EASING_FUNCTIONS[:15]): # First 15
|
||||
row = i // 5
|
||||
col = i % 5
|
||||
x = 100 + col * 200
|
||||
y = 150 + row * 100
|
||||
|
||||
# Frame
|
||||
f = mcrfpy.Frame(x, y, 20, 20)
|
||||
f.fill_color = mcrfpy.Color(100, 150, 255)
|
||||
ui.append(f)
|
||||
|
||||
# Label
|
||||
label = mcrfpy.Caption(easing[:10], x, y - 20)
|
||||
label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(label)
|
||||
|
||||
# Animate with this easing
|
||||
mcrfpy.Animation("x", float(x + 150), 3.0, easing).start(f)
|
||||
|
||||
def demo4_performance():
|
||||
"""Many simultaneous animations"""
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 4: 50+ Simultaneous Animations"
|
||||
|
||||
for i in range(50):
|
||||
x = 100 + (i % 10) * 100
|
||||
y = 150 + (i // 10) * 100
|
||||
|
||||
f = mcrfpy.Frame(x, y, 30, 30)
|
||||
f.fill_color = mcrfpy.Color((i*37)%256, (i*73)%256, (i*113)%256)
|
||||
ui.append(f)
|
||||
|
||||
# Animate to random position
|
||||
target_x = 150 + (i % 8) * 110
|
||||
target_y = 200 + (i // 8) * 90
|
||||
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
|
||||
|
||||
mcrfpy.Animation("x", float(target_x), 2.5, easing).start(f)
|
||||
mcrfpy.Animation("y", float(target_y), 2.5, easing).start(f)
|
||||
mcrfpy.Animation("opacity", 0.3 + (i%7)*0.1, 2.0, "easeInOutSine").start(f)
|
||||
|
||||
def clear_demo_objects():
|
||||
"""Clear scene except title and subtitle"""
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
# Keep removing items after the first 2 (title and subtitle)
|
||||
while len(ui) > 2:
|
||||
# Remove the last item
|
||||
ui.remove(len(ui)-1)
|
||||
|
||||
def next_demo(runtime):
|
||||
"""Run the next demo"""
|
||||
global current_demo
|
||||
|
||||
clear_demo_objects()
|
||||
|
||||
demos = [
|
||||
demo1_frame_animations,
|
||||
demo2_caption_animations,
|
||||
demo3_easing_showcase,
|
||||
demo4_performance
|
||||
]
|
||||
|
||||
if current_demo < len(demos):
|
||||
demos[current_demo]()
|
||||
current_demo += 1
|
||||
|
||||
if current_demo < len(demos):
|
||||
#mcrfpy.setTimer("next", next_demo, int(DEMO_DURATION * 1000))
|
||||
pass
|
||||
else:
|
||||
subtitle.text = "Demo Complete!"
|
||||
|
||||
# Initialize
|
||||
print("Starting Animation Sizzle Reel...")
|
||||
create_scene()
|
||||
mcrfpy.setTimer("start", next_demo, int(DEMO_DURATION * 1000))
|
||||
next_demo(0)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue