Compare commits

..

10 Commits

Author SHA1 Message Date
John McCardle 9bd1561bfc 🎉 ALPHA 0.1 ACHIEVED! Update ROADMAP to reflect alpha release
- Mark project as Alpha 0.1 complete
- Move RenderTexture (#6) to Beta (not essential for Alpha)
- All 6 original alpha blockers resolved:
  * Animation system (#59)
  * Z-order rendering (#63)
  * Python Sequence Protocol (#69)
  * New README (#47)
  * Removed deprecated methods (#2, #3)
- Ready for alpha release and merge to main!

The engine now has:
- Full Python scripting with game loop integration
- Complete UI system with animations
- Proper z-order rendering
- Python sequence protocol for collections
- Automation API for testing
- Headless mode support
- Cross-platform CMake build

🍾 Time to celebrate - McRogueFace Alpha 0.1 is ready!
2025-07-05 11:20:07 -04:00
John McCardle 43321487eb Update ROADMAP.md: Mark Issue #63 (z-order rendering) as complete
- Add z-order rendering to recent achievements
- Update alpha blocker count from 3 to 2
- Archive z-order test files
- Next priority: RenderTexture concept (#6) - last major alpha blocker
2025-07-05 10:36:09 -04:00
John McCardle 90c318104b Fix Issue #63: Implement z-order rendering with dirty flag optimization
- Add dirty flags to PyScene and UIFrame to track when sorting is needed
- Implement lazy sorting - only sort when z_index changes or elements are added/removed
- Make Frame children respect z_index (previously rendered in insertion order only)
- Update UIDrawable::set_int to notify when z_index changes
- Mark collections dirty on append, remove, setitem, and slice operations
- Remove per-frame vector copy in PyScene::render for better performance

Performance improvement: Static scenes now use O(1) check instead of O(n log n) sort every frame

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 10:34:06 -04:00
John McCardle 2a48138011 Update ROADMAP.md to reflect completion of Issue #69 (Sequence Protocol)
- Mark Issue #69 as complete in all sections
- Add achievement entry for Python Sequence Protocol implementation
- Update alpha blockers count: 3 remaining (was 4)
- Update total open issues: 62 (was 63)
- Next priorities: Z-order rendering (#63) or RenderTexture (#6)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 02:00:12 -04:00
John McCardle e4482e7189 Implement complete Python Sequence Protocol for collections (closes #69)
Major implementation of the full sequence protocol for both UICollection
and UIEntityCollection, making them behave like proper Python sequences.

Core Features Implemented:
- __setitem__ (collection[i] = value) with type validation
- __delitem__ (del collection[i]) with proper cleanup
- __contains__ (item in collection) by C++ pointer comparison
- __add__ (collection + other) returns Python list
- __iadd__ (collection += other) with full validation before modification
- Negative indexing support throughout
- Complete slice support (getting, setting, deletion)
- Extended slices with step \!= 1
- index() and count() methods
- Type safety enforced for all operations

UICollection specifics:
- Accepts Frame, Caption, Sprite, and Grid objects only
- Preserves z_index when replacing items
- Auto-assigns z_index on append (existing behavior maintained)

UIEntityCollection specifics:
- Accepts Entity objects only
- Manages grid references on add/remove/replace
- Uses std::list iteration with std::advance()

Also includes:
- Default value support for constructors:
  - Caption accepts None for font (uses default_font)
  - Grid accepts None for texture (uses default_texture)
  - Sprite accepts None for texture (uses default_texture)
  - Entity accepts None for texture (uses default_texture)

This completes Issue #69, removing it as an Alpha Blocker.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 01:58:03 -04:00
John McCardle 38d44777f5 Update ROADMAP.md to reflect completion of Issue #59 (Animation System)
- Mark Animation system as complete in all relevant sections
- Update alpha blockers count from 7 to 4
- Add animation system architectural decisions
- Update project status and next priorities

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 00:58:41 -04:00
John McCardle 70cf44f8f0 Implement comprehensive animation system (closes #59)
- Add Animation class with 30+ easing functions (linear, ease in/out, quad, cubic, elastic, bounce, etc.)
- Add property system to all UI classes for animation support:
  - UIFrame: position, size, colors (including individual r/g/b/a components)
  - UICaption: position, size, text, colors
  - UISprite: position, scale, sprite_number (with sequence support)
  - UIGrid: position, size, camera center, zoom
  - UIEntity: position, sprite properties
- Create AnimationManager singleton for frame-based updates
- Add Python bindings through PyAnimation wrapper
- Support for delta animations (relative values)
- Fix segfault when running scripts directly (mcrf_module initialization)
- Fix headless/windowed mode behavior to respect --headless flag
- Animations run purely in C++ without Python callbacks per frame

All UI properties are now animatable with smooth interpolation and professional easing curves.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 00:56:42 -04:00
John McCardle dd3c64784d Mark Issue #47 (Alpha README) as completed in ROADMAP
Documentation has been comprehensively updated for the Alpha release.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 06:59:29 -04:00
John McCardle 05bddae511 Update comprehensive documentation for Alpha release (Issue #47)
- Completely rewrote README.md to reflect 7DRL 2025 success and current features
- Updated GitHub Pages documentation site with:
  - Modern landing page highlighting Crypt of Sokoban
  - Comprehensive API reference (2700+ lines) with exhaustive examples
  - Updated getting-started guide with installation and first game tutorial
  - 8 detailed tutorials covering all major game systems
  - Quick reference cheat sheet for common operations
- Generated documentation screenshots showing UI elements
- Fixed deprecated API references and added new features
- Added automation API documentation
- Included Python 3.12 requirement and platform-specific instructions

Note: Text rendering in headless mode has limitations for screenshots

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-04 06:59:02 -04:00
John McCardle 0d26d51bc3 Compress ROADMAP.md and archive completed test files
- Condensed 'Today's Achievements' section for clarity
- Archived 9 completed test files from bug fixing session
- Updated task completion status for issues fixed today
- Identified 5 remaining Alpha blockers as next priority

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-03 23:05:30 -04:00
46 changed files with 4458 additions and 171 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,30 +1,85 @@
# McRogueFace - 2D Game Engine
# McRogueFace
An experimental prototype game engine built for my own use in 7DRL 2023.
A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML.
*Blame my wife for the name*
**Latest Release**: Successfully completed 7DRL 2025 with *"Crypt of Sokoban"* - a unique roguelike that blends Sokoban puzzle mechanics with dungeon crawling!
## Tenets:
## Features
* C++ first, Python close behind.
* Entity-Component system based on David Churchill's Memorial University COMP4300 course lectures available on Youtube.
* Graphics, particles and shaders provided by SFML.
* Pathfinding, noise generation, and other Roguelike goodness provided by TCOD.
- **Python-First Design**: Write your game logic in Python while leveraging C++ performance
- **Rich UI System**: Sprites, Grids, Frames, and Captions with full animation support
- **Entity-Component Architecture**: Flexible game object system with Python integration
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod
- **Automation API**: PyAutoGUI-compatible testing and demo recording
- **Interactive Development**: Python REPL integration for live game debugging
## Why?
## Quick Start
I did the r/RoguelikeDev TCOD tutorial in Python. I loved it, but I did not want to be limited to ASCII. I want to be able to draw pixels on top of my tiles (like lines or circles) and eventually incorporate even more polish.
```bash
# Clone and build
git clone https://github.com/jmcb/McRogueFace.git
cd McRogueFace
make
## To-do
# Run the example game
cd build
./mcrogueface
```
* ✅ Initial Commit
* ✅ Integrate scene, action, entity, component system from COMP4300 engine
* ✅ Windows / Visual Studio project
* ✅ Draw Sprites
* ✅ Play Sounds
* ✅ Draw UI, spawn entity from Python code
* ❌ Python AI for entities (NPCs on set paths, enemies towards player)
* ✅ Walking / Collision
* ❌ "Boards" (stairs / doors / walk off edge of screen)
* ❌ Cutscenes - interrupt normal controls, text scroll, character portraits
* ❌ Mouse integration - tooltips, zoom, click to select targets, cursors
## Example: Creating a Simple Scene
```python
import mcrfpy
# Create a new scene
mcrfpy.createScene("intro")
# Add a text caption
caption = mcrfpy.Caption(50, 50, "Welcome to McRogueFace!")
caption.font = mcrfpy.default_font
caption.font_color = (255, 255, 255)
# Add to scene
mcrfpy.sceneUI("intro").append(caption)
# Switch to the scene
mcrfpy.setScene("intro")
```
## Documentation
For comprehensive documentation, tutorials, and API reference, visit:
**[https://mcrogueface.github.io](https://mcrogueface.github.io)**
## Requirements
- C++17 compiler (GCC 7+ or Clang 5+)
- CMake 3.14+
- Python 3.12+
- SFML 2.5+
- Linux or Windows (macOS untested)
## Project Structure
```
McRogueFace/
├── src/ # C++ engine source
├── scripts/ # Python game scripts
├── assets/ # Sprites, fonts, audio
├── build/ # Build output directory
└── tests/ # Automated test suite
```
## Contributing
McRogueFace is under active development. Check the [ROADMAP.md](ROADMAP.md) for current priorities and open issues.
## License
This project is licensed under the MIT License - see LICENSE file for details.
## Acknowledgments
- Developed for 7-Day Roguelike Challenge 2025
- Built with [SFML](https://www.sfml-dev.org/), [libtcod](https://github.com/libtcod/libtcod), and Python
- Inspired by David Churchill's COMP4300 game engine lectures

View File

@ -1,38 +1,54 @@
# McRogueFace - Development Roadmap
## Project Status: Post-7DRL 2025 "Crypt of Sokoban"
## Project Status: 🎉 ALPHA 0.1 RELEASE! 🎉
**Current State**: Successful 7DRL completion with Python/C++ game engine
**Latest Update**: Fixed 12+ critical bugs in one day! (2025-01-03)
**Branch**: interpreter_mode (comprehensive test suite + major stability fixes)
**Open Issues**: ~48 remaining from original 64 (closed 14 + fixed 14 today)
**Current State**: Alpha release achieved! All critical blockers resolved!
**Latest Update**: Moved RenderTexture (#6) to Beta - Alpha is READY! (2025-07-05)
**Branch**: interpreter_mode (ready for alpha release merge)
**Open Issues**: ~46 remaining (non-blocking quality-of-life improvements)
---
## 🎉 TODAY'S ACHIEVEMENTS (2025-01-03)
## Recent Achievements
In a single productive session, we fixed 12+ critical bugs and implemented missing features:
### 2025-07-05: ALPHA 0.1 ACHIEVED! 🎊🍾
**All Alpha Blockers Resolved!**
- Z-order rendering with performance optimization (Issue #63)
- Python Sequence Protocol for collections (Issue #69)
- Comprehensive Animation System (Issue #59)
- Moved RenderTexture to Beta (not needed for Alpha)
- **McRogueFace is ready for Alpha release!**
### Critical Bug Fixes:
- **Grid Segfault** - Fixed crash when texture is None/null, added default 16x16 cell dimensions
- **Issue #78** - Fixed middle mouse click incorrectly sending 'C' keyboard event (SFML event union bug)
- **Issue #77** - Fixed error message copy/paste bug in Grid validation
- **Issue #74** - Added missing Grid.grid_y property (closes #74)
- **Entity Setters** - Fixed "new style getargs format" error with proper PyVector conversion
- **PyVector** - Implemented missing x/y property getters and setters
- **Sprite Texture** - Fixed setter returning -1 without setting exception
- **keypressScene** - Added validation to reject non-callable arguments
### 2025-07-05: Z-order Rendering Complete! 🎉
**Issue #63 Resolved**: Consistent z-order rendering with performance optimization
- Dirty flag pattern prevents unnecessary per-frame sorting
- Lazy sorting for both Scene elements and Frame children
- Frame children now respect z_index (fixed inconsistency)
- Automatic dirty marking on z_index changes and collection modifications
- Performance: O(1) check for static scenes vs O(n log n) every frame
### New Features Implemented:
- **Issue #73** - Entity.index() method for finding position in collection (closes #73)
- **Issue #27** - EntityCollection.extend() for adding multiple entities at once (closes #27)
- **Issue #33** - Sprite index validation against texture bounds (closes #33)
- **Issue #3** - Removed deprecated player_input and turn-based functions (closes #3)
- **Issue #2** - Removed entire registerPyAction/registerInputAction system (closes #2)
### 2025-07-05: Python Sequence Protocol Complete! 🎉
**Issue #69 Resolved**: Full sequence protocol implementation for collections
- Complete __setitem__, __delitem__, __contains__ support
- Slice operations with extended slice support (step != 1)
- Concatenation (+) and in-place concatenation (+=) with validation
- Negative indexing throughout, index() and count() methods
- Type safety: UICollection (Frame/Caption/Sprite/Grid), EntityCollection (Entity only)
- Default value support: None for texture/font parameters uses engine defaults
### Test-Driven Development:
Every fix was accompanied by a comprehensive test using the timer callback pattern.
All tests verify the fix and ensure no regressions.
### 2025-07-05: Animation System Complete! 🎉
**Issue #59 Resolved**: Comprehensive animation system with 30+ easing functions
- Property-based animations for all UI classes (Frame, Caption, Sprite, Grid, Entity)
- Individual color component animation (r/g/b/a)
- Sprite sequence animation and text typewriter effects
- Pure C++ execution without Python callbacks
- Delta animation support for relative values
### 2025-01-03: Major Stability Update
**Major Cleanup**: Removed deprecated registerPyAction system (-180 lines)
**Bug Fixes**: 12 critical issues including Grid segfault, Issue #78 (middle click), Entity setters
**New Features**: Entity.index() (#73), EntityCollection.extend() (#27), Sprite validation (#33)
**Test Coverage**: Comprehensive test suite with timer callback pattern established
---
@ -59,6 +75,8 @@ All tests verify the fix and ensure no regressions.
2. **Honor system for scripts** - Scripts must return control to C++ render loop
3. **Shared Python state** - All --exec scripts share the same interpreter
4. **No threading complexity** - Chose simplicity over parallelism (see THREADING_FOOTGUNS.md)
5. **Animation system in pure C++** - All interpolation happens in C++ for performance
6. **Property-based animation** - Unified interface for all UI element properties
#### Key Files Created:
- `src/McRFPy_Automation.h/cpp` - Complete automation API implementation
@ -90,44 +108,49 @@ Created comprehensive test suite with 13 tests covering all Python-exposed metho
- #41: UICollection.find(name) method
- #38: Frame 'children' constructor parameter
- #33: Sprite index validation
- #69: Partial Sequence Protocol (no slicing, 'in' operator)
---
## 🚧 IMMEDIATE PRIORITY: Critical Bugfixes & Iterator Completion
## 🚀 NEXT PHASE: Beta Features & Polish
### 🔥 Critical Bugfixes (Complete First)
- [ ] **CRITICAL: Grid Segfault** - Grid class crashes on instantiation (blocks ALL Grid functionality) - *High Priority*
- [ ] **#78** - Middle Mouse Click sends "C" keyboard event to scene event handler - *Confirmed Bug*
- [ ] **#77** - Fix error message copy/paste bug (`x value out of range (0, Grid.grid_y)`) - *Isolated Fix*
- [ ] **#74** - Add missing `Grid.grid_y` property referenced in error messages - *Isolated Fix*
### Alpha Complete! Moving to Beta Priorities:
1. ~~**#69** - Python Sequence Protocol for collections~~ - *Completed! (2025-07-05)*
2. ~~**#63** - Z-order rendering for UIDrawables~~ - *Completed! (2025-07-05)*
3. ~~**#59** - Animation system~~ - *Completed! (2025-07-05)*
4. **#6** - RenderTexture concept - *Extensive Overhaul*
5. ~~**#47** - New README.md for Alpha release~~ - *Completed*
- [x] **#78** - Middle Mouse Click sends "C" keyboard event - *Fixed*
- [x] **#77** - Fix error message copy/paste bug - *Fixed*
- [x] **#74** - Add missing `Grid.grid_y` property - *Fixed*
- [ ] **#37** - Fix Windows build module import from "scripts" directory - *Isolated Fix*
- [ ] **Entity Property Setters** - Fix "new style getargs format" error - *Multiple Fixes*
- [ ] **Sprite Texture Setter** - Fix "error return without exception set" - *Isolated Fix*
- [ ] **keypressScene() Validation** - Add proper error handling for non-callable arguments - *Isolated Fix*
- [x] **Entity Property Setters** - Fix "new style getargs format" error - *Fixed*
- [x] **Sprite Texture Setter** - Fix "error return without exception set" - *Fixed*
- [x] **keypressScene() Validation** - Add proper error handling - *Fixed*
### 🔄 Complete Iterator System
**Status**: Core iterators complete (#72 closed), Grid point iterators still pending
- [ ] **Grid Point Iterator Implementation** - Complete the remaining grid iteration work
- [ ] **#73** - Add `entity.index()` method for collection removal - *Isolated Fix*
- [ ] **#69** ⚠️ **Alpha Blocker** - Refactor all collections to use Python Sequence Protocol - *Extensive Overhaul*
- [x] **#73** - Add `entity.index()` method for collection removal - *Fixed*
- [x] **#69** ⚠️ **Alpha Blocker** - Refactor all collections to use Python Sequence Protocol - *Completed! (2025-07-05)*
**Dependencies**: Grid point iterators → #73 entity.index() → #69 Sequence Protocol overhaul
---
## 🎯 ALPHA 0.1 RELEASE BLOCKERS (6 Issues)
## ✅ ALPHA 0.1 RELEASE ACHIEVED! (All Blockers Complete)
### ⚠️ Must Complete Before Alpha Release
- [ ] **#69** - Collections use Python Sequence Protocol - *Extensive Overhaul*
- [ ] **#63** - Z-order rendering for UIDrawables - *Multiple Integrations*
- [ ] **#59** - Animation system for arbitrary UIDrawable fields - *Extensive Overhaul*
- [ ] **#6** - RenderTexture concept for all UIDrawables - *Extensive Overhaul*
- [ ] **#47** - New README.md for Alpha release - *Isolated Fix*
### ✅ All Alpha Requirements Complete!
- [x] **#69** - Collections use Python Sequence Protocol - *Completed! (2025-07-05)*
- [x] **#63** - Z-order rendering for UIDrawables - *Completed! (2025-07-05)*
- [x] **#59** - Animation system for arbitrary UIDrawable fields - *Completed! (2025-07-05)*
- [x] **#47** - New README.md for Alpha release - *Completed*
- [x] **#3** - Remove deprecated `McRFPy_API::player_input` - *Completed*
- [x] **#2** - Remove `registerPyAction` system - *Completed*
### 📋 Moved to Beta:
- [ ] **#6** - RenderTexture concept - *Moved to Beta (not needed for Alpha)*
---
## 🗂 ISSUE TRIAGE BY SYSTEM (78 Total Issues)
@ -135,8 +158,8 @@ Created comprehensive test suite with 13 tests covering all Python-exposed metho
### 🎮 Core Engine Systems
#### Iterator/Collection System (2 issues)
- [ ] **#73** - Entity index() method for removal - *Isolated Fix*
- [ ] **#69** ⚠️ **Alpha Blocker** - Sequence Protocol refactor - *Extensive Overhaul*
- [x] **#73** - Entity index() method for removal - *Fixed*
- [x] **#69** ⚠️ **Alpha Blocker** - Sequence Protocol refactor - *Completed! (2025-07-05)*
#### Python/C++ Integration (7 issues)
- [ ] **#76** - UIEntity derived type preservation in collections - *Multiple Integrations*
@ -149,7 +172,7 @@ Created comprehensive test suite with 13 tests covering all Python-exposed metho
#### UI/Rendering System (12 issues)
- [ ] **#63** ⚠️ **Alpha Blocker** - Z-order for UIDrawables - *Multiple Integrations*
- [ ] **#59** ⚠️ **Alpha Blocker** - Animation system - *Extensive Overhaul*
- [x] **#59** ⚠️ **Alpha Blocker** - Animation system - *Completed! (2025-07-05)*
- [ ] **#6** ⚠️ **Alpha Blocker** - RenderTexture for all UIDrawables - *Extensive Overhaul*
- [ ] **#10** - UIDrawable visibility/AABB system - *Extensive Overhaul*
- [ ] **#8** - UIGrid RenderTexture viewport sizing - *Multiple Integrations*
@ -158,7 +181,7 @@ Created comprehensive test suite with 13 tests covering all Python-exposed metho
- [ ] **#50** - UIGrid background color field - *Isolated Fix*
- [ ] **#19** - Sprite get/set texture methods - *Multiple Integrations*
- [ ] **#17** - Move UISprite position into sf::Sprite - *Isolated Fix*
- [ ] **#33** - Sprite index validation against texture range - *Isolated Fix*
- [x] **#33** - Sprite index validation against texture range - *Fixed*
#### Grid/Entity System (6 issues)
- [ ] **#30** - Entity/Grid association management (.die() method) - *Extensive Overhaul*
@ -183,7 +206,7 @@ Created comprehensive test suite with 13 tests covering all Python-exposed metho
- [ ] **#41** - `.find(name)` method for collections - *Multiple Integrations*
- [ ] **#38** - `children` arg for Frame initialization - *Isolated Fix*
- [ ] **#42** - Click callback arg for UIDrawable init - *Isolated Fix*
- [ ] **#27** - UIEntityCollection.extend() method - *Isolated Fix*
- [x] **#27** - UIEntityCollection.extend() method - *Fixed*
- [ ] **#28** - UICollectionIter for scene ui iteration - *Isolated Fix*
- [ ] **#26** - UIEntityCollectionIter implementation - *Isolated Fix*
@ -238,8 +261,8 @@ REMAINING IN PHASE 1:
1. Collections Sequence Protocol (#69) - Major refactor, alpha blocker
2. Z-order rendering (#63) - Essential UI improvement, alpha blocker
3. RenderTexture overhaul (#6) - Core rendering improvement, alpha blocker
4. Animation system (#59) - Major feature, alpha blocker
5. Documentation (#47, #48) - Complete alpha release docs
4. ✅ Animation system (#59) - COMPLETE! 30+ easing functions, all UI properties
5. ✅ Documentation (#47) - README.md complete, #48 dependency docs remaining
```
### Phase 3: Engine Architecture (6-8 weeks)
@ -303,7 +326,7 @@ REMAINING IN PHASE 1:
4. **Multi-Platform**: Windows/Linux feature parity maintained
### Success Metrics for Alpha 0.1
- [ ] All 7 Alpha Blocker issues resolved
- [ ] All Alpha Blocker issues resolved (5 of 7 complete: #69, #59, #47, #3, #2)
- [ ] Grid point iteration complete and tested
- [ ] Clean build on Windows and Linux
- [ ] Documentation sufficient for external developers
@ -331,9 +354,9 @@ REMAINING IN PHASE 1:
---
*Last Updated: 2025-07-03*
*Total Open Issues: 64* (from original 78)
*Alpha Blockers: 7*
*Current Work: Python interpreter mode features (--exec flag, automation API)*
*Next Session: Continue interpreter mode or switch to critical bugfixes*
*Last Updated: 2025-07-05*
*Total Open Issues: 62* (from original 78)
*Alpha Status: 🎉 COMPLETE! All blockers resolved!*
*Achievement Unlocked: Alpha 0.1 Release Ready*
*Next Phase: Beta features including RenderTexture (#6), advanced UI patterns, and platform polish*

527
src/Animation.cpp Normal file
View File

@ -0,0 +1,527 @@
#include "Animation.h"
#include "UIDrawable.h"
#include "UIEntity.h"
#include <cmath>
#include <algorithm>
#include <unordered_map>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// Animation implementation
Animation::Animation(const std::string& targetProperty,
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc,
bool delta)
: targetProperty(targetProperty)
, targetValue(targetValue)
, duration(duration)
, easingFunc(easingFunc)
, delta(delta)
{
}
void Animation::start(UIDrawable* target) {
currentTarget = target;
elapsed = 0.0f;
// 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) {
using T = std::decay_t<decltype(targetVal)>;
if constexpr (std::is_same_v<T, float>) {
float value;
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, int>) {
int value;
if (currentTarget->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)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, sf::Color>) {
sf::Color value;
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
sf::Vector2f value;
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, std::string>) {
std::string value;
if (currentTarget->getProperty(targetProperty, value)) {
startValue = value;
}
}
}, targetValue);
}
void Animation::startEntity(UIEntity* target) {
currentEntityTarget = target;
currentTarget = nullptr; // Clear drawable target
elapsed = 0.0f;
// Capture the starting value from the entity
std::visit([this, target](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
float value = 0.0f;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, int>) {
// For entities, we might need to handle sprite_number differently
if (targetProperty == "sprite_number") {
startValue = target->sprite.getSpriteIndex();
}
}
// Entities don't support other types yet
}, targetValue);
}
bool Animation::update(float deltaTime) {
if ((!currentTarget && !currentEntityTarget) || isComplete()) {
return false;
}
elapsed += deltaTime;
elapsed = std::min(elapsed, duration);
// Calculate easing value (0.0 to 1.0)
float t = duration > 0 ? elapsed / duration : 1.0f;
float easedT = easingFunc(t);
// 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);
return !isComplete();
}
AnimationValue Animation::getCurrentValue() const {
float t = duration > 0 ? elapsed / duration : 1.0f;
float easedT = easingFunc(t);
return interpolate(easedT);
}
AnimationValue Animation::interpolate(float t) const {
// Visit the variant to perform type-specific interpolation
return std::visit([this, t](const auto& target) -> AnimationValue {
using T = std::decay_t<decltype(target)>;
if constexpr (std::is_same_v<T, float>) {
// Interpolate float
const float* start = std::get_if<float>(&startValue);
if (!start) return target; // Type mismatch
if (delta) {
return *start + target * t;
} else {
return *start + (target - *start) * t;
}
}
else if constexpr (std::is_same_v<T, int>) {
// Interpolate integer
const int* start = std::get_if<int>(&startValue);
if (!start) return target;
float result;
if (delta) {
result = *start + target * t;
} else {
result = *start + (target - *start) * t;
}
return static_cast<int>(std::round(result));
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// For sprite animation, interpolate through the list
if (target.empty()) return target;
// Map t to an index in the vector
size_t index = static_cast<size_t>(t * (target.size() - 1));
index = std::min(index, target.size() - 1);
return static_cast<int>(target[index]);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
// Interpolate color
const sf::Color* start = std::get_if<sf::Color>(&startValue);
if (!start) return target;
sf::Color result;
if (delta) {
result.r = std::clamp(start->r + target.r * t, 0.0f, 255.0f);
result.g = std::clamp(start->g + target.g * t, 0.0f, 255.0f);
result.b = std::clamp(start->b + target.b * t, 0.0f, 255.0f);
result.a = std::clamp(start->a + target.a * t, 0.0f, 255.0f);
} else {
result.r = start->r + (target.r - start->r) * t;
result.g = start->g + (target.g - start->g) * t;
result.b = start->b + (target.b - start->b) * t;
result.a = start->a + (target.a - start->a) * t;
}
return result;
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
// Interpolate vector
const sf::Vector2f* start = std::get_if<sf::Vector2f>(&startValue);
if (!start) return target;
if (delta) {
return sf::Vector2f(start->x + target.x * t,
start->y + target.y * t);
} else {
return sf::Vector2f(start->x + (target.x - start->x) * t,
start->y + (target.y - start->y) * t);
}
}
else if constexpr (std::is_same_v<T, std::string>) {
// For text, show characters based on t
const std::string* start = std::get_if<std::string>(&startValue);
if (!start) return target;
// If delta mode, append characters from target
if (delta) {
size_t chars = static_cast<size_t>(target.length() * t);
return *start + target.substr(0, chars);
} else {
// Transition from start text to target text
if (t < 0.5f) {
// First half: remove characters from start
size_t chars = static_cast<size_t>(start->length() * (1.0f - t * 2.0f));
return start->substr(0, chars);
} else {
// Second half: add characters to target
size_t chars = static_cast<size_t>(target.length() * ((t - 0.5f) * 2.0f));
return target.substr(0, chars);
}
}
}
return target; // Fallback
}, targetValue);
}
// Easing functions implementation
namespace EasingFunctions {
float linear(float t) {
return t;
}
float easeIn(float t) {
return t * t;
}
float easeOut(float t) {
return t * (2.0f - t);
}
float easeInOut(float t) {
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
}
// Quadratic
float easeInQuad(float t) {
return t * t;
}
float easeOutQuad(float t) {
return t * (2.0f - t);
}
float easeInOutQuad(float t) {
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
}
// Cubic
float easeInCubic(float t) {
return t * t * t;
}
float easeOutCubic(float t) {
float t1 = t - 1.0f;
return t1 * t1 * t1 + 1.0f;
}
float easeInOutCubic(float t) {
return t < 0.5f ? 4.0f * t * t * t : (t - 1.0f) * (2.0f * t - 2.0f) * (2.0f * t - 2.0f) + 1.0f;
}
// Quartic
float easeInQuart(float t) {
return t * t * t * t;
}
float easeOutQuart(float t) {
float t1 = t - 1.0f;
return 1.0f - t1 * t1 * t1 * t1;
}
float easeInOutQuart(float t) {
return t < 0.5f ? 8.0f * t * t * t * t : 1.0f - 8.0f * (t - 1.0f) * (t - 1.0f) * (t - 1.0f) * (t - 1.0f);
}
// Sine
float easeInSine(float t) {
return 1.0f - std::cos(t * M_PI / 2.0f);
}
float easeOutSine(float t) {
return std::sin(t * M_PI / 2.0f);
}
float easeInOutSine(float t) {
return 0.5f * (1.0f - std::cos(M_PI * t));
}
// Exponential
float easeInExpo(float t) {
return t == 0.0f ? 0.0f : std::pow(2.0f, 10.0f * (t - 1.0f));
}
float easeOutExpo(float t) {
return t == 1.0f ? 1.0f : 1.0f - std::pow(2.0f, -10.0f * t);
}
float easeInOutExpo(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
if (t < 0.5f) {
return 0.5f * std::pow(2.0f, 20.0f * t - 10.0f);
} else {
return 1.0f - 0.5f * std::pow(2.0f, -20.0f * t + 10.0f);
}
}
// Circular
float easeInCirc(float t) {
return 1.0f - std::sqrt(1.0f - t * t);
}
float easeOutCirc(float t) {
float t1 = t - 1.0f;
return std::sqrt(1.0f - t1 * t1);
}
float easeInOutCirc(float t) {
if (t < 0.5f) {
return 0.5f * (1.0f - std::sqrt(1.0f - 4.0f * t * t));
} else {
return 0.5f * (std::sqrt(1.0f - (2.0f * t - 2.0f) * (2.0f * t - 2.0f)) + 1.0f);
}
}
// Elastic
float easeInElastic(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
float p = 0.3f;
float a = 1.0f;
float s = p / 4.0f;
float t1 = t - 1.0f;
return -(a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p));
}
float easeOutElastic(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
float p = 0.3f;
float a = 1.0f;
float s = p / 4.0f;
return a * std::pow(2.0f, -10.0f * t) * std::sin((t - s) * (2.0f * M_PI) / p) + 1.0f;
}
float easeInOutElastic(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
float p = 0.45f;
float a = 1.0f;
float s = p / 4.0f;
if (t < 0.5f) {
float t1 = 2.0f * t - 1.0f;
return -0.5f * (a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p));
} else {
float t1 = 2.0f * t - 1.0f;
return a * std::pow(2.0f, -10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p) * 0.5f + 1.0f;
}
}
// Back (overshooting)
float easeInBack(float t) {
const float s = 1.70158f;
return t * t * ((s + 1.0f) * t - s);
}
float easeOutBack(float t) {
const float s = 1.70158f;
float t1 = t - 1.0f;
return t1 * t1 * ((s + 1.0f) * t1 + s) + 1.0f;
}
float easeInOutBack(float t) {
const float s = 1.70158f * 1.525f;
if (t < 0.5f) {
return 0.5f * (4.0f * t * t * ((s + 1.0f) * 2.0f * t - s));
} else {
float t1 = 2.0f * t - 2.0f;
return 0.5f * (t1 * t1 * ((s + 1.0f) * t1 + s) + 2.0f);
}
}
// Bounce
float easeOutBounce(float t) {
if (t < 1.0f / 2.75f) {
return 7.5625f * t * t;
} else if (t < 2.0f / 2.75f) {
float t1 = t - 1.5f / 2.75f;
return 7.5625f * t1 * t1 + 0.75f;
} else if (t < 2.5f / 2.75f) {
float t1 = t - 2.25f / 2.75f;
return 7.5625f * t1 * t1 + 0.9375f;
} else {
float t1 = t - 2.625f / 2.75f;
return 7.5625f * t1 * t1 + 0.984375f;
}
}
float easeInBounce(float t) {
return 1.0f - easeOutBounce(1.0f - t);
}
float easeInOutBounce(float t) {
if (t < 0.5f) {
return 0.5f * easeInBounce(2.0f * t);
} else {
return 0.5f * easeOutBounce(2.0f * t - 1.0f) + 0.5f;
}
}
// Get easing function by name
EasingFunction getByName(const std::string& name) {
static std::unordered_map<std::string, EasingFunction> easingMap = {
{"linear", linear},
{"easeIn", easeIn},
{"easeOut", easeOut},
{"easeInOut", easeInOut},
{"easeInQuad", easeInQuad},
{"easeOutQuad", easeOutQuad},
{"easeInOutQuad", easeInOutQuad},
{"easeInCubic", easeInCubic},
{"easeOutCubic", easeOutCubic},
{"easeInOutCubic", easeInOutCubic},
{"easeInQuart", easeInQuart},
{"easeOutQuart", easeOutQuart},
{"easeInOutQuart", easeInOutQuart},
{"easeInSine", easeInSine},
{"easeOutSine", easeOutSine},
{"easeInOutSine", easeInOutSine},
{"easeInExpo", easeInExpo},
{"easeOutExpo", easeOutExpo},
{"easeInOutExpo", easeInOutExpo},
{"easeInCirc", easeInCirc},
{"easeOutCirc", easeOutCirc},
{"easeInOutCirc", easeInOutCirc},
{"easeInElastic", easeInElastic},
{"easeOutElastic", easeOutElastic},
{"easeInOutElastic", easeInOutElastic},
{"easeInBack", easeInBack},
{"easeOutBack", easeOutBack},
{"easeInOutBack", easeInOutBack},
{"easeInBounce", easeInBounce},
{"easeOutBounce", easeOutBounce},
{"easeInOutBounce", easeInOutBounce}
};
auto it = easingMap.find(name);
if (it != easingMap.end()) {
return it->second;
}
return linear; // Default to linear
}
} // namespace EasingFunctions
// AnimationManager implementation
AnimationManager& AnimationManager::getInstance() {
static AnimationManager instance;
return instance;
}
void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) {
activeAnimations.push_back(animation);
}
void AnimationManager::update(float deltaTime) {
for (auto& anim : activeAnimations) {
anim->update(deltaTime);
}
cleanup();
}
void AnimationManager::cleanup() {
activeAnimations.erase(
std::remove_if(activeAnimations.begin(), activeAnimations.end(),
[](const std::shared_ptr<Animation>& anim) {
return anim->isComplete();
}),
activeAnimations.end()
);
}
void AnimationManager::clear() {
activeAnimations.clear();
}

146
src/Animation.h Normal file
View File

@ -0,0 +1,146 @@
#pragma once
#include <string>
#include <functional>
#include <memory>
#include <variant>
#include <vector>
#include <SFML/Graphics.hpp>
// Forward declarations
class UIDrawable;
class UIEntity;
// Forward declare namespace
namespace EasingFunctions {
float linear(float t);
}
// Easing function type
typedef std::function<float(float)> EasingFunction;
// Animation target value can be various types
typedef std::variant<
float, // Single float value
int, // Single integer value
std::vector<int>, // List of integers (for sprite animation)
sf::Color, // Color animation
sf::Vector2f, // Vector animation
std::string // String animation (for text)
> AnimationValue;
class Animation {
public:
// Constructor
Animation(const std::string& targetProperty,
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc = EasingFunctions::linear,
bool delta = false);
// Apply this animation to a drawable
void start(UIDrawable* target);
// Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable)
void startEntity(UIEntity* target);
// Update animation (called each frame)
// Returns true if animation is still running, false if complete
bool update(float deltaTime);
// Get current interpolated value
AnimationValue getCurrentValue() const;
// Animation properties
std::string getTargetProperty() const { return targetProperty; }
float getDuration() const { return duration; }
float getElapsed() const { return elapsed; }
bool isComplete() const { return elapsed >= duration; }
bool isDelta() const { return delta; }
private:
std::string targetProperty; // Property name to animate (e.g., "x", "color.r", "sprite_number")
AnimationValue startValue; // Starting value (captured when animation starts)
AnimationValue targetValue; // Target value to animate to
float duration; // Animation duration in seconds
float elapsed = 0.0f; // Elapsed time
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)
// Helper to interpolate between values
AnimationValue interpolate(float t) const;
};
// Easing functions library
namespace EasingFunctions {
// Basic easing functions
float linear(float t);
float easeIn(float t);
float easeOut(float t);
float easeInOut(float t);
// Advanced easing functions
float easeInQuad(float t);
float easeOutQuad(float t);
float easeInOutQuad(float t);
float easeInCubic(float t);
float easeOutCubic(float t);
float easeInOutCubic(float t);
float easeInQuart(float t);
float easeOutQuart(float t);
float easeInOutQuart(float t);
float easeInSine(float t);
float easeOutSine(float t);
float easeInOutSine(float t);
float easeInExpo(float t);
float easeOutExpo(float t);
float easeInOutExpo(float t);
float easeInCirc(float t);
float easeOutCirc(float t);
float easeInOutCirc(float t);
float easeInElastic(float t);
float easeOutElastic(float t);
float easeInOutElastic(float t);
float easeInBack(float t);
float easeOutBack(float t);
float easeInOutBack(float t);
float easeInBounce(float t);
float easeOutBounce(float t);
float easeInOutBounce(float t);
// Get easing function by name
EasingFunction getByName(const std::string& name);
}
// Animation manager to handle active animations
class AnimationManager {
public:
static AnimationManager& getInstance();
// Add an animation to be managed
void addAnimation(std::shared_ptr<Animation> animation);
// Update all animations
void update(float deltaTime);
// Remove completed animations
void cleanup();
// Clear all animations
void clear();
private:
AnimationManager() = default;
std::vector<std::shared_ptr<Animation>> activeAnimations;
};

View File

@ -4,6 +4,7 @@
#include "PyScene.h"
#include "UITestScene.h"
#include "Resources.h"
#include "Animation.h"
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
{
@ -114,11 +115,18 @@ void GameEngine::run()
{
std::cout << "GameEngine::run() starting main loop..." << std::endl;
float fps = 0.0;
frameTime = 0.016f; // Initialize to ~60 FPS
clock.restart();
while (running)
{
currentScene()->update();
testTimers();
// Update animations (only if frameTime is valid)
if (frameTime > 0.0f && frameTime < 1.0f) {
AnimationManager::getInstance().update(frameTime);
}
if (!headless) {
sUserInput();
}

View File

@ -1,9 +1,11 @@
#include "McRFPy_API.h"
#include "McRFPy_Automation.h"
#include "platform.h"
#include "PyAnimation.h"
#include "GameEngine.h"
#include "UI.h"
#include "Resources.h"
#include "PyScene.h"
#include <filesystem>
#include <cstring>
@ -76,6 +78,9 @@ PyObject* PyInit_mcrfpy()
/*collections & iterators*/
&PyUICollectionType, &PyUICollectionIterType,
&PyUIEntityCollectionType, &PyUIEntityCollectionIterType,
/*animation*/
&PyAnimationType,
nullptr};
int i = 0;
auto t = pytypes[i];
@ -535,3 +540,15 @@ PyObject* McRFPy_API::_setScale(PyObject* self, PyObject* args) {
Py_INCREF(Py_None);
return Py_None;
}
void McRFPy_API::markSceneNeedsSort() {
// Mark the current scene as needing a z_index sort
auto scene = game->currentScene();
if (scene && scene->ui_elements) {
// Cast to PyScene to access ui_elements_need_sort
PyScene* pyscene = dynamic_cast<PyScene*>(scene);
if (pyscene) {
pyscene->ui_elements_need_sort = true;
}
}
}

View File

@ -70,4 +70,7 @@ public:
static void executeScript(std::string);
static void executePyString(std::string);
// Helper to mark scenes as needing z_index resort
static void markSceneNeedsSort();
};

234
src/PyAnimation.cpp Normal file
View File

@ -0,0 +1,234 @@
#include "PyAnimation.h"
#include "McRFPy_API.h"
#include "UIDrawable.h"
#include "UIFrame.h"
#include "UICaption.h"
#include "UISprite.h"
#include "UIGrid.h"
#include "UIEntity.h"
#include "UI.h" // For the PyTypeObject definitions
#include <cstring>
PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds) {
PyAnimationObject* self = (PyAnimationObject*)type->tp_alloc(type, 0);
if (self != NULL) {
// Will be initialized in init
}
return (PyObject*)self;
}
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", nullptr};
const char* property_name;
PyObject* target_value;
float duration;
const char* easing_name = "linear";
int delta = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|sp", const_cast<char**>(keywords),
&property_name, &target_value, &duration, &easing_name, &delta)) {
return -1;
}
// Convert Python target value to AnimationValue
AnimationValue animValue;
if (PyFloat_Check(target_value)) {
animValue = static_cast<float>(PyFloat_AsDouble(target_value));
}
else if (PyLong_Check(target_value)) {
animValue = static_cast<int>(PyLong_AsLong(target_value));
}
else if (PyList_Check(target_value)) {
// List of integers for sprite animation
std::vector<int> indices;
Py_ssize_t size = PyList_Size(target_value);
for (Py_ssize_t i = 0; i < size; i++) {
PyObject* item = PyList_GetItem(target_value, i);
if (PyLong_Check(item)) {
indices.push_back(PyLong_AsLong(item));
} else {
PyErr_SetString(PyExc_TypeError, "Sprite animation list must contain only integers");
return -1;
}
}
animValue = indices;
}
else if (PyTuple_Check(target_value)) {
Py_ssize_t size = PyTuple_Size(target_value);
if (size == 2) {
// Vector2f
float x = PyFloat_AsDouble(PyTuple_GetItem(target_value, 0));
float y = PyFloat_AsDouble(PyTuple_GetItem(target_value, 1));
animValue = sf::Vector2f(x, y);
}
else if (size == 3 || size == 4) {
// Color (RGB or RGBA)
int r = PyLong_AsLong(PyTuple_GetItem(target_value, 0));
int g = PyLong_AsLong(PyTuple_GetItem(target_value, 1));
int b = PyLong_AsLong(PyTuple_GetItem(target_value, 2));
int a = size == 4 ? PyLong_AsLong(PyTuple_GetItem(target_value, 3)) : 255;
animValue = sf::Color(r, g, b, a);
}
else {
PyErr_SetString(PyExc_ValueError, "Tuple must have 2 elements (vector) or 3-4 elements (color)");
return -1;
}
}
else if (PyUnicode_Check(target_value)) {
// String for text animation
const char* str = PyUnicode_AsUTF8(target_value);
animValue = std::string(str);
}
else {
PyErr_SetString(PyExc_TypeError, "Target value must be float, int, list, tuple, or string");
return -1;
}
// Get easing function
EasingFunction easingFunc = EasingFunctions::getByName(easing_name);
// Create the Animation
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0);
return 0;
}
void PyAnimation::dealloc(PyAnimationObject* self) {
self->data.reset();
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyObject* PyAnimation::get_property(PyAnimationObject* self, void* closure) {
return PyUnicode_FromString(self->data->getTargetProperty().c_str());
}
PyObject* PyAnimation::get_duration(PyAnimationObject* self, void* closure) {
return PyFloat_FromDouble(self->data->getDuration());
}
PyObject* PyAnimation::get_elapsed(PyAnimationObject* self, void* closure) {
return PyFloat_FromDouble(self->data->getElapsed());
}
PyObject* PyAnimation::get_is_complete(PyAnimationObject* self, void* closure) {
return PyBool_FromLong(self->data->isComplete());
}
PyObject* PyAnimation::get_is_delta(PyAnimationObject* self, void* closure) {
return PyBool_FromLong(self->data->isDelta());
}
PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
PyObject* target_obj;
if (!PyArg_ParseTuple(args, "O", &target_obj)) {
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();
}
else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
drawable = caption->data.get();
}
else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
drawable = sprite->data.get();
}
else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
drawable = grid->data.get();
}
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;
}
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;
}
PyObject* PyAnimation::update(PyAnimationObject* self, PyObject* args) {
float deltaTime;
if (!PyArg_ParseTuple(args, "f", &deltaTime)) {
return NULL;
}
bool still_running = self->data->update(deltaTime);
return PyBool_FromLong(still_running);
}
PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args) {
AnimationValue value = self->data->getCurrentValue();
// Convert AnimationValue back to Python
return std::visit([](const auto& val) -> PyObject* {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
return PyFloat_FromDouble(val);
}
else if constexpr (std::is_same_v<T, int>) {
return PyLong_FromLong(val);
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// This shouldn't happen as we interpolate to int
return PyLong_FromLong(0);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
return Py_BuildValue("(iiii)", val.r, val.g, val.b, val.a);
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
return Py_BuildValue("(ff)", val.x, val.y);
}
else if constexpr (std::is_same_v<T, std::string>) {
return PyUnicode_FromString(val.c_str());
}
Py_RETURN_NONE;
}, value);
}
PyGetSetDef PyAnimation::getsetters[] = {
{"property", (getter)get_property, NULL, "Target property name", NULL},
{"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL},
{"elapsed", (getter)get_elapsed, NULL, "Elapsed time in seconds", NULL},
{"is_complete", (getter)get_is_complete, NULL, "Whether animation is complete", NULL},
{"is_delta", (getter)get_is_delta, NULL, "Whether animation uses delta mode", NULL},
{NULL}
};
PyMethodDef PyAnimation::methods[] = {
{"start", (PyCFunction)start, METH_VARARGS,
"Start the animation on a target UIDrawable"},
{"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"},
{NULL}
};

50
src/PyAnimation.h Normal file
View File

@ -0,0 +1,50 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "structmember.h"
#include "Animation.h"
#include <memory>
typedef struct {
PyObject_HEAD
std::shared_ptr<Animation> data;
} PyAnimationObject;
class PyAnimation {
public:
static PyObject* create(PyTypeObject* type, PyObject* args, PyObject* kwds);
static int init(PyAnimationObject* self, PyObject* args, PyObject* kwds);
static void dealloc(PyAnimationObject* self);
// Properties
static PyObject* get_property(PyAnimationObject* self, void* closure);
static PyObject* get_duration(PyAnimationObject* self, void* closure);
static PyObject* get_elapsed(PyAnimationObject* self, void* closure);
static PyObject* get_is_complete(PyAnimationObject* self, void* closure);
static PyObject* get_is_delta(PyAnimationObject* self, void* closure);
// Methods
static PyObject* start(PyAnimationObject* self, PyObject* args);
static PyObject* update(PyAnimationObject* self, PyObject* args);
static PyObject* get_current_value(PyAnimationObject* self, PyObject* args);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
};
namespace mcrfpydef {
static PyTypeObject PyAnimationType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Animation",
.tp_basicsize = sizeof(PyAnimationObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyAnimation::dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Animation object for animating UI properties"),
.tp_methods = PyAnimation::methods,
.tp_getset = PyAnimation::getsetters,
.tp_init = (initproc)PyAnimation::init,
.tp_new = PyAnimation::create,
};
}

View File

@ -2,6 +2,7 @@
#include "ActionCode.h"
#include "Resources.h"
#include "PyCallable.h"
#include <algorithm>
PyScene::PyScene(GameEngine* g) : Scene(g)
{
@ -66,8 +67,17 @@ void PyScene::render()
{
game->getRenderTarget().clear();
auto vec = *ui_elements;
for (auto e: vec)
// Only sort if z_index values have changed
if (ui_elements_need_sort) {
std::sort(ui_elements->begin(), ui_elements->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
ui_elements_need_sort = false;
}
// Render in sorted order (no need to copy anymore)
for (auto e: *ui_elements)
{
if (e)
e->render();

View File

@ -14,4 +14,7 @@ public:
void render() override final;
void do_mouse_input(std::string, std::string);
// Dirty flag for z_index sorting optimization
bool ui_elements_need_sort = true;
};

View File

@ -3,6 +3,7 @@
#include "PyColor.h"
#include "PyVector.h"
#include "PyFont.h"
#include <algorithm>
UIDrawable* UICaption::click_at(sf::Vector2f point)
{
@ -198,6 +199,7 @@ PyGetSetDef UICaption::getsetters[] = {
{"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL},
{"size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Text size (integer) in points", (void*)5},
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UICAPTION},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION},
{NULL}
};
@ -234,7 +236,7 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
//if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOf",
// const_cast<char**>(keywords), &x, &y, &text, &font, &fill_color, &outline_color, &outline))
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|zOOOf",
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Oz|OOOf",
const_cast<char**>(keywords), &pos, &text, &font, &fill_color, &outline_color, &outline))
{
return -1;
@ -250,10 +252,10 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
// check types for font, fill_color, outline_color
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl;
if (font != NULL && !PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font")/*(PyObject*)&PyFontType)*/)){
PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance");
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)
} else if (font != NULL && font != Py_None)
{
auto font_obj = (PyFontObject*)font;
self->data->text.setFont(font_obj->data->font);
@ -261,8 +263,16 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
Py_INCREF(font);
} else
{
// default font
//self->data->text.setFont(Resources::game->getFont());
// 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
}
}
}
self->data->text.setString((std::string)text);
@ -294,3 +304,172 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
return 0;
}
// Property system implementation for animations
bool UICaption::setProperty(const std::string& name, float value) {
if (name == "x") {
text.setPosition(sf::Vector2f(value, text.getPosition().y));
return true;
}
else if (name == "y") {
text.setPosition(sf::Vector2f(text.getPosition().x, value));
return true;
}
else if (name == "size") {
text.setCharacterSize(static_cast<unsigned int>(value));
return true;
}
else if (name == "outline") {
text.setOutlineThickness(value);
return true;
}
else if (name == "fill_color.r") {
auto color = text.getFillColor();
color.r = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setFillColor(color);
return true;
}
else if (name == "fill_color.g") {
auto color = text.getFillColor();
color.g = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setFillColor(color);
return true;
}
else if (name == "fill_color.b") {
auto color = text.getFillColor();
color.b = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setFillColor(color);
return true;
}
else if (name == "fill_color.a") {
auto color = text.getFillColor();
color.a = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setFillColor(color);
return true;
}
else if (name == "outline_color.r") {
auto color = text.getOutlineColor();
color.r = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setOutlineColor(color);
return true;
}
else if (name == "outline_color.g") {
auto color = text.getOutlineColor();
color.g = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setOutlineColor(color);
return true;
}
else if (name == "outline_color.b") {
auto color = text.getOutlineColor();
color.b = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setOutlineColor(color);
return true;
}
else if (name == "outline_color.a") {
auto color = text.getOutlineColor();
color.a = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
text.setOutlineColor(color);
return true;
}
else if (name == "z_index") {
z_index = static_cast<int>(value);
return true;
}
return false;
}
bool UICaption::setProperty(const std::string& name, const sf::Color& value) {
if (name == "fill_color") {
text.setFillColor(value);
return true;
}
else if (name == "outline_color") {
text.setOutlineColor(value);
return true;
}
return false;
}
bool UICaption::setProperty(const std::string& name, const std::string& value) {
if (name == "text") {
text.setString(value);
return true;
}
return false;
}
bool UICaption::getProperty(const std::string& name, float& value) const {
if (name == "x") {
value = text.getPosition().x;
return true;
}
else if (name == "y") {
value = text.getPosition().y;
return true;
}
else if (name == "size") {
value = static_cast<float>(text.getCharacterSize());
return true;
}
else if (name == "outline") {
value = text.getOutlineThickness();
return true;
}
else if (name == "fill_color.r") {
value = text.getFillColor().r;
return true;
}
else if (name == "fill_color.g") {
value = text.getFillColor().g;
return true;
}
else if (name == "fill_color.b") {
value = text.getFillColor().b;
return true;
}
else if (name == "fill_color.a") {
value = text.getFillColor().a;
return true;
}
else if (name == "outline_color.r") {
value = text.getOutlineColor().r;
return true;
}
else if (name == "outline_color.g") {
value = text.getOutlineColor().g;
return true;
}
else if (name == "outline_color.b") {
value = text.getOutlineColor().b;
return true;
}
else if (name == "outline_color.a") {
value = text.getOutlineColor().a;
return true;
}
else if (name == "z_index") {
value = static_cast<float>(z_index);
return true;
}
return false;
}
bool UICaption::getProperty(const std::string& name, sf::Color& value) const {
if (name == "fill_color") {
value = text.getFillColor();
return true;
}
else if (name == "outline_color") {
value = text.getOutlineColor();
return true;
}
return false;
}
bool UICaption::getProperty(const std::string& name, std::string& value) const {
if (name == "text") {
value = text.getString();
return true;
}
return false;
}

View File

@ -10,6 +10,15 @@ public:
void render(sf::Vector2f, sf::RenderTarget&) override final;
PyObjectsEnum derived_type() override final;
virtual UIDrawable* click_at(sf::Vector2f point) override final;
// Property system for animations
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, const sf::Color& value) override;
bool setProperty(const std::string& name, const std::string& value) override;
bool getProperty(const std::string& name, float& value) const override;
bool getProperty(const std::string& name, sf::Color& value) const override;
bool getProperty(const std::string& name, std::string& value) const override;
static PyObject* get_float_member(PyUICaptionObject* self, void* closure);
static int set_float_member(PyUICaptionObject* self, PyObject* value, void* closure);

View File

@ -6,6 +6,8 @@
#include "UIGrid.h"
#include "McRFPy_API.h"
#include "PyObjectUtils.h"
#include <climits>
#include <algorithm>
using namespace mcrfpydef;
@ -148,15 +150,394 @@ PyObject* UICollection::getitem(PyUICollectionObject* self, Py_ssize_t index) {
}
int UICollection::setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject* value) {
auto vec = self->data.get();
if (!vec) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return -1;
}
// Handle negative indexing
while (index < 0) index += self->data->size();
// Bounds check
if (index >= self->data->size()) {
PyErr_SetString(PyExc_IndexError, "UICollection assignment index out of range");
return -1;
}
// Handle deletion
if (value == NULL) {
self->data->erase(self->data->begin() + index);
return 0;
}
// Type checking - must be a UIDrawable subclass
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyErr_SetString(PyExc_TypeError, "UICollection can only contain Frame, Caption, Sprite, and Grid objects");
return -1;
}
// Get the C++ object from the Python object
std::shared_ptr<UIDrawable> new_drawable = nullptr;
int old_z_index = (*vec)[index]->z_index; // Preserve the z_index
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
PyUIFrameObject* frame = (PyUIFrameObject*)value;
new_drawable = frame->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
PyUICaptionObject* caption = (PyUICaptionObject*)value;
new_drawable = caption->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
PyUISpriteObject* sprite = (PyUISpriteObject*)value;
new_drawable = sprite->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyUIGridObject* grid = (PyUIGridObject*)value;
new_drawable = grid->data;
}
if (!new_drawable) {
PyErr_SetString(PyExc_RuntimeError, "Failed to extract C++ object from Python object");
return -1;
}
// Preserve the z_index of the replaced element
new_drawable->z_index = old_z_index;
// Replace the element
(*vec)[index] = new_drawable;
// Mark scene as needing resort after replacing element
McRFPy_API::markSceneNeedsSort();
return 0;
}
int UICollection::contains(PyUICollectionObject* self, PyObject* value) {
auto vec = self->data.get();
if (!vec) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return -1;
}
// Type checking - must be a UIDrawable subclass
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
// Not a valid type, so it can't be in the collection
return 0;
}
// Get the C++ object from the Python object
std::shared_ptr<UIDrawable> search_drawable = nullptr;
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
PyUIFrameObject* frame = (PyUIFrameObject*)value;
search_drawable = frame->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
PyUICaptionObject* caption = (PyUICaptionObject*)value;
search_drawable = caption->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
PyUISpriteObject* sprite = (PyUISpriteObject*)value;
search_drawable = sprite->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyUIGridObject* grid = (PyUIGridObject*)value;
search_drawable = grid->data;
}
if (!search_drawable) {
return 0;
}
// Search for the object by comparing C++ pointers
for (const auto& drawable : *vec) {
if (drawable.get() == search_drawable.get()) {
return 1; // Found
}
}
return 0; // Not found
}
PyObject* UICollection::concat(PyUICollectionObject* self, PyObject* other) {
// Create a new Python list containing elements from both collections
if (!PySequence_Check(other)) {
PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to UICollection");
return NULL;
}
Py_ssize_t self_len = self->data->size();
Py_ssize_t other_len = PySequence_Length(other);
if (other_len == -1) {
return NULL; // Error already set
}
PyObject* result_list = PyList_New(self_len + other_len);
if (!result_list) {
return NULL;
}
// Add all elements from self
for (Py_ssize_t i = 0; i < self_len; i++) {
PyObject* item = convertDrawableToPython((*self->data)[i]);
if (!item) {
Py_DECREF(result_list);
return NULL;
}
PyList_SET_ITEM(result_list, i, item); // Steals reference
}
// Add all elements from other
for (Py_ssize_t i = 0; i < other_len; i++) {
PyObject* item = PySequence_GetItem(other, i);
if (!item) {
Py_DECREF(result_list);
return NULL;
}
PyList_SET_ITEM(result_list, self_len + i, item); // Steals reference
}
return result_list;
}
PyObject* UICollection::inplace_concat(PyUICollectionObject* self, PyObject* other) {
if (!PySequence_Check(other)) {
PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to UICollection");
return NULL;
}
// First, validate ALL items in the sequence before modifying anything
Py_ssize_t other_len = PySequence_Length(other);
if (other_len == -1) {
return NULL; // Error already set
}
// Validate all items first
for (Py_ssize_t i = 0; i < other_len; i++) {
PyObject* item = PySequence_GetItem(other, i);
if (!item) {
return NULL;
}
// Type check
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
Py_DECREF(item);
PyErr_Format(PyExc_TypeError,
"UICollection can only contain Frame, Caption, Sprite, and Grid objects; "
"got %s at index %zd", Py_TYPE(item)->tp_name, i);
return NULL;
}
Py_DECREF(item);
}
// All items validated, now we can safely add them
for (Py_ssize_t i = 0; i < other_len; i++) {
PyObject* item = PySequence_GetItem(other, i);
if (!item) {
return NULL; // Shouldn't happen, but be safe
}
// Use the existing append method which handles z_index assignment
PyObject* result = append(self, item);
Py_DECREF(item);
if (!result) {
return NULL; // append() failed
}
Py_DECREF(result); // append returns Py_None
}
Py_INCREF(self);
return (PyObject*)self;
}
PyObject* UICollection::subscript(PyUICollectionObject* self, PyObject* key) {
if (PyLong_Check(key)) {
// Single index - delegate to sq_item
Py_ssize_t index = PyLong_AsSsize_t(key);
if (index == -1 && PyErr_Occurred()) {
return NULL;
}
return getitem(self, index);
} else if (PySlice_Check(key)) {
// Handle slice
Py_ssize_t start, stop, step, slicelength;
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
return NULL;
}
PyObject* result_list = PyList_New(slicelength);
if (!result_list) {
return NULL;
}
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
PyObject* item = convertDrawableToPython((*self->data)[cur]);
if (!item) {
Py_DECREF(result_list);
return NULL;
}
PyList_SET_ITEM(result_list, i, item); // Steals reference
}
return result_list;
} else {
PyErr_Format(PyExc_TypeError, "UICollection indices must be integers or slices, not %.200s",
Py_TYPE(key)->tp_name);
return NULL;
}
}
int UICollection::ass_subscript(PyUICollectionObject* self, PyObject* key, PyObject* value) {
if (PyLong_Check(key)) {
// Single index - delegate to sq_ass_item
Py_ssize_t index = PyLong_AsSsize_t(key);
if (index == -1 && PyErr_Occurred()) {
return -1;
}
return setitem(self, index, value);
} else if (PySlice_Check(key)) {
// Handle slice assignment/deletion
Py_ssize_t start, stop, step, slicelength;
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
return -1;
}
if (value == NULL) {
// Deletion
if (step != 1) {
// For non-contiguous slices, delete from highest to lowest to maintain indices
std::vector<Py_ssize_t> indices;
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
indices.push_back(cur);
}
// Sort in descending order and delete
std::sort(indices.begin(), indices.end(), std::greater<Py_ssize_t>());
for (Py_ssize_t idx : indices) {
self->data->erase(self->data->begin() + idx);
}
} else {
// Contiguous slice - can delete in one go
self->data->erase(self->data->begin() + start, self->data->begin() + stop);
}
// Mark scene as needing resort after slice deletion
McRFPy_API::markSceneNeedsSort();
return 0;
} else {
// Assignment
if (!PySequence_Check(value)) {
PyErr_SetString(PyExc_TypeError, "can only assign sequence to slice");
return -1;
}
Py_ssize_t value_len = PySequence_Length(value);
if (value_len == -1) {
return -1;
}
// Validate all items first
std::vector<std::shared_ptr<UIDrawable>> new_items;
for (Py_ssize_t i = 0; i < value_len; i++) {
PyObject* item = PySequence_GetItem(value, i);
if (!item) {
return -1;
}
// Type check and extract C++ object
std::shared_ptr<UIDrawable> drawable = nullptr;
if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
drawable = ((PyUIFrameObject*)item)->data;
} else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
drawable = ((PyUICaptionObject*)item)->data;
} else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
drawable = ((PyUISpriteObject*)item)->data;
} else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
drawable = ((PyUIGridObject*)item)->data;
} else {
Py_DECREF(item);
PyErr_Format(PyExc_TypeError,
"UICollection can only contain Frame, Caption, Sprite, and Grid objects; "
"got %s at index %zd", Py_TYPE(item)->tp_name, i);
return -1;
}
Py_DECREF(item);
new_items.push_back(drawable);
}
// Now perform the assignment
if (step == 1) {
// Contiguous slice
if (slicelength != value_len) {
// Need to resize
auto it_start = self->data->begin() + start;
auto it_stop = self->data->begin() + stop;
self->data->erase(it_start, it_stop);
self->data->insert(self->data->begin() + start, new_items.begin(), new_items.end());
} else {
// Same size, just replace
for (Py_ssize_t i = 0; i < slicelength; i++) {
// Preserve z_index
new_items[i]->z_index = (*self->data)[start + i]->z_index;
(*self->data)[start + i] = new_items[i];
}
}
} else {
// Extended slice
if (slicelength != value_len) {
PyErr_Format(PyExc_ValueError,
"attempt to assign sequence of size %zd to extended slice of size %zd",
value_len, slicelength);
return -1;
}
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
// Preserve z_index
new_items[i]->z_index = (*self->data)[cur]->z_index;
(*self->data)[cur] = new_items[i];
}
}
// Mark scene as needing resort after slice assignment
McRFPy_API::markSceneNeedsSort();
return 0;
}
} else {
PyErr_Format(PyExc_TypeError, "UICollection indices must be integers or slices, not %.200s",
Py_TYPE(key)->tp_name);
return -1;
}
}
PyMappingMethods UICollection::mpmethods = {
.mp_length = (lenfunc)UICollection::len,
.mp_subscript = (binaryfunc)UICollection::subscript,
.mp_ass_subscript = (objobjargproc)UICollection::ass_subscript
};
PySequenceMethods UICollection::sqmethods = {
.sq_length = (lenfunc)UICollection::len,
.sq_concat = (binaryfunc)UICollection::concat,
.sq_repeat = NULL,
.sq_item = (ssizeargfunc)UICollection::getitem,
//.sq_item_by_index = PyUICollection_getitem
//.sq_slice - return a subset of the iterable
//.sq_ass_item - called when `o[x] = y` is executed (x is any object type)
//.sq_ass_slice - cool; no thanks, for now
//.sq_contains - called when `x in o` is executed
//.sq_ass_item_by_index - called when `o[x] = y` is executed (x is explictly an integer)
.was_sq_slice = NULL,
.sq_ass_item = (ssizeobjargproc)UICollection::setitem,
.was_sq_ass_slice = NULL,
.sq_contains = (objobjproc)UICollection::contains,
.sq_inplace_concat = (binaryfunc)UICollection::inplace_concat,
.sq_inplace_repeat = NULL
};
/* Idiomatic way to fetch complete types from the API rather than referencing their PyTypeObject struct
@ -173,6 +554,12 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
// if not UIDrawable subclass, reject it
// self->data->push_back( c++ object inside o );
// Ensure module is initialized
if (!McRFPy_API::mcrf_module) {
PyErr_SetString(PyExc_RuntimeError, "mcrfpy module not initialized");
return NULL;
}
// this would be a great use case for .tp_base
if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
@ -184,26 +571,45 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
return NULL;
}
// Calculate z_index for the new element
int new_z_index = 0;
if (!self->data->empty()) {
// Get the z_index of the last element and add 10
int last_z = self->data->back()->z_index;
if (last_z <= INT_MAX - 10) {
new_z_index = last_z + 10;
} else {
new_z_index = INT_MAX;
}
}
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")))
{
PyUIFrameObject* frame = (PyUIFrameObject*)o;
frame->data->z_index = new_z_index;
self->data->push_back(frame->data);
}
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")))
{
PyUICaptionObject* caption = (PyUICaptionObject*)o;
caption->data->z_index = new_z_index;
self->data->push_back(caption->data);
}
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")))
{
PyUISpriteObject* sprite = (PyUISpriteObject*)o;
sprite->data->z_index = new_z_index;
self->data->push_back(sprite->data);
}
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")))
{
PyUIGridObject* grid = (PyUIGridObject*)o;
grid->data->z_index = new_z_index;
self->data->push_back(grid->data);
}
// Mark scene as needing resort after adding element
McRFPy_API::markSceneNeedsSort();
Py_INCREF(Py_None);
return Py_None;
@ -217,27 +623,121 @@ PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o)
return NULL;
}
long index = PyLong_AsLong(o);
// Handle negative indexing
while (index < 0) index += self->data->size();
if (index >= self->data->size())
{
PyErr_SetString(PyExc_ValueError, "Index out of range");
return NULL;
}
else if (index < 0)
{
PyErr_SetString(PyExc_NotImplementedError, "reverse indexing is not implemented.");
return NULL;
}
// release the shared pointer at self->data[index];
self->data->erase(self->data->begin() + index);
// Mark scene as needing resort after removing element
McRFPy_API::markSceneNeedsSort();
Py_INCREF(Py_None);
return Py_None;
}
PyObject* UICollection::index_method(PyUICollectionObject* self, PyObject* value) {
auto vec = self->data.get();
if (!vec) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return NULL;
}
// Type checking - must be a UIDrawable subclass
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyErr_SetString(PyExc_TypeError, "UICollection.index requires a Frame, Caption, Sprite, or Grid object");
return NULL;
}
// Get the C++ object from the Python object
std::shared_ptr<UIDrawable> search_drawable = nullptr;
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
search_drawable = ((PyUIFrameObject*)value)->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
search_drawable = ((PyUICaptionObject*)value)->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
search_drawable = ((PyUISpriteObject*)value)->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
search_drawable = ((PyUIGridObject*)value)->data;
}
if (!search_drawable) {
PyErr_SetString(PyExc_RuntimeError, "Failed to extract C++ object from Python object");
return NULL;
}
// Search for the object
for (size_t i = 0; i < vec->size(); i++) {
if ((*vec)[i].get() == search_drawable.get()) {
return PyLong_FromSsize_t(i);
}
}
PyErr_SetString(PyExc_ValueError, "value not in UICollection");
return NULL;
}
PyObject* UICollection::count(PyUICollectionObject* self, PyObject* value) {
auto vec = self->data.get();
if (!vec) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return NULL;
}
// Type checking - must be a UIDrawable subclass
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
// Not a valid type, so count is 0
return PyLong_FromLong(0);
}
// Get the C++ object from the Python object
std::shared_ptr<UIDrawable> search_drawable = nullptr;
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
search_drawable = ((PyUIFrameObject*)value)->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
search_drawable = ((PyUICaptionObject*)value)->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
search_drawable = ((PyUISpriteObject*)value)->data;
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
search_drawable = ((PyUIGridObject*)value)->data;
}
if (!search_drawable) {
return PyLong_FromLong(0);
}
// Count occurrences
Py_ssize_t count = 0;
for (const auto& drawable : *vec) {
if (drawable.get() == search_drawable.get()) {
count++;
}
}
return PyLong_FromSsize_t(count);
}
PyMethodDef UICollection::methods[] = {
{"append", (PyCFunction)UICollection::append, METH_O},
//{"extend", (PyCFunction)PyUICollection_extend, METH_O}, // TODO
{"remove", (PyCFunction)UICollection::remove, METH_O},
{"index", (PyCFunction)UICollection::index_method, METH_O},
{"count", (PyCFunction)UICollection::count, METH_O},
{NULL, NULL, 0, NULL}
};

View File

@ -19,9 +19,18 @@ class UICollection
public:
static Py_ssize_t len(PyUICollectionObject* self);
static PyObject* getitem(PyUICollectionObject* self, Py_ssize_t index);
static int setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject* value);
static int contains(PyUICollectionObject* self, PyObject* value);
static PyObject* concat(PyUICollectionObject* self, PyObject* other);
static PyObject* inplace_concat(PyUICollectionObject* self, PyObject* other);
static PySequenceMethods sqmethods;
static PyMappingMethods mpmethods;
static PyObject* subscript(PyUICollectionObject* self, PyObject* key);
static int ass_subscript(PyUICollectionObject* self, PyObject* key, PyObject* value);
static PyObject* append(PyUICollectionObject* self, PyObject* o);
static PyObject* remove(PyUICollectionObject* self, PyObject* o);
static PyObject* index_method(PyUICollectionObject* self, PyObject* value);
static PyObject* count(PyUICollectionObject* self, PyObject* value);
static PyMethodDef methods[];
static PyObject* repr(PyUICollectionObject* self);
static int init(PyUICollectionObject* self, PyObject* args, PyObject* kwds);
@ -71,6 +80,7 @@ namespace mcrfpydef {
},
.tp_repr = (reprfunc)UICollection::repr,
.tp_as_sequence = &UICollection::sqmethods,
.tp_as_mapping = &UICollection::mpmethods,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Iterable, indexable collection of UI objects"),
.tp_iter = (getiterfunc)UICollection::iter,

View File

@ -4,6 +4,7 @@
#include "UISprite.h"
#include "UIGrid.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
UIDrawable::UIDrawable() { click_callable = NULL; }
@ -80,3 +81,85 @@ void UIDrawable::click_register(PyObject* callable)
{
click_callable = std::make_unique<PyClickCallable>(callable);
}
PyObject* UIDrawable::get_int(PyObject* self, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
drawable = ((PyUIFrameObject*)self)->data.get();
break;
case PyObjectsEnum::UICAPTION:
drawable = ((PyUICaptionObject*)self)->data.get();
break;
case PyObjectsEnum::UISPRITE:
drawable = ((PyUISpriteObject*)self)->data.get();
break;
case PyObjectsEnum::UIGRID:
drawable = ((PyUIGridObject*)self)->data.get();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return NULL;
}
return PyLong_FromLong(drawable->z_index);
}
int UIDrawable::set_int(PyObject* self, PyObject* value, void* closure) {
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(closure));
UIDrawable* drawable = nullptr;
switch (objtype) {
case PyObjectsEnum::UIFRAME:
drawable = ((PyUIFrameObject*)self)->data.get();
break;
case PyObjectsEnum::UICAPTION:
drawable = ((PyUICaptionObject*)self)->data.get();
break;
case PyObjectsEnum::UISPRITE:
drawable = ((PyUISpriteObject*)self)->data.get();
break;
case PyObjectsEnum::UIGRID:
drawable = ((PyUIGridObject*)self)->data.get();
break;
default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return -1;
}
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "z_index must be an integer");
return -1;
}
long z = PyLong_AsLong(value);
if (z == -1 && PyErr_Occurred()) {
return -1;
}
// Clamp to int range
if (z < INT_MIN) z = INT_MIN;
if (z > INT_MAX) z = INT_MAX;
int old_z_index = drawable->z_index;
drawable->z_index = static_cast<int>(z);
// Notify of z_index change
if (old_z_index != drawable->z_index) {
drawable->notifyZIndexChanged();
}
return 0;
}
void UIDrawable::notifyZIndexChanged() {
// Mark the current scene as needing sort
// This works for elements in the scene's ui_elements collection
McRFPy_API::markSceneNeedsSort();
// TODO: In the future, we could add parent tracking to handle Frame children
// For now, Frame children will need manual sorting or collection modification
// to trigger a resort
}

View File

@ -42,6 +42,27 @@ public:
static PyObject* get_click(PyObject* self, void* closure);
static int set_click(PyObject* self, PyObject* value, void* closure);
static PyObject* get_int(PyObject* self, void* closure);
static int set_int(PyObject* self, PyObject* value, void* closure);
// Z-order for rendering (lower values rendered first, higher values on top)
int z_index = 0;
// Notification for z_index changes
void notifyZIndexChanged();
// Animation support
virtual bool setProperty(const std::string& name, float value) { return false; }
virtual bool setProperty(const std::string& name, int value) { return false; }
virtual bool setProperty(const std::string& name, const sf::Color& value) { return false; }
virtual bool setProperty(const std::string& name, const sf::Vector2f& value) { return false; }
virtual bool setProperty(const std::string& name, const std::string& value) { return false; }
virtual bool getProperty(const std::string& name, float& value) const { return false; }
virtual bool getProperty(const std::string& name, int& value) const { return false; }
virtual bool getProperty(const std::string& name, sf::Color& value) const { return false; }
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; }
};
typedef struct {

View File

@ -75,7 +75,7 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
//if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffOi|O",
// const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &grid))
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOi|O",
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OiO",
const_cast<char**>(keywords), &pos, &texture, &sprite_index, &grid))
{
return -1;
@ -90,33 +90,37 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
// check types for texture
//
// Set Texture
// Set Texture - allow None or use default
//
if (texture != NULL && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance");
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) // this section needs to go; texture isn't optional and isn't managed by the UI objects anymore
{
self->texture = texture;
Py_INCREF(texture);
} else
{
// default tex?
}*/
} else if (texture != NULL && texture != Py_None) {
auto pytexture = (PyTextureObject*)texture;
texture_ptr = pytexture->data;
} else {
// Use default texture when None or not provided
texture_ptr = McRFPy_API::default_texture;
}
if (!texture_ptr) {
PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
return -1;
}
if (grid != NULL && !PyObject_IsInstance(grid, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
return -1;
}
auto pytexture = (PyTextureObject*)texture;
if (grid == NULL)
self->data = std::make_shared<UIEntity>();
else
self->data = std::make_shared<UIEntity>(*((PyUIGridObject*)grid)->data);
// TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers
self->data->sprite = UISprite(pytexture->data, sprite_index, sf::Vector2f(0,0), 1.0);
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
self->data->position = pos_result->data;
if (grid != NULL) {
PyUIGridObject* pygrid = (PyUIGridObject*)grid;
@ -261,3 +265,51 @@ PyObject* UIEntity::repr(PyUIEntityObject* self) {
std::string repr_str = ss.str();
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
}
// Property system implementation for animations
bool UIEntity::setProperty(const std::string& name, float value) {
if (name == "x") {
position.x = value;
collision_pos.x = static_cast<int>(value);
// Update sprite position based on grid position
// Note: This is a simplified version - actual grid-to-pixel conversion depends on grid properties
sprite.setPosition(sf::Vector2f(position.x, position.y));
return true;
}
else if (name == "y") {
position.y = value;
collision_pos.y = static_cast<int>(value);
// Update sprite position based on grid position
sprite.setPosition(sf::Vector2f(position.x, position.y));
return true;
}
else if (name == "sprite_scale") {
sprite.setScale(sf::Vector2f(value, value));
return true;
}
return false;
}
bool UIEntity::setProperty(const std::string& name, int value) {
if (name == "sprite_number") {
sprite.setSpriteIndex(value);
return true;
}
return false;
}
bool UIEntity::getProperty(const std::string& name, float& value) const {
if (name == "x") {
value = position.x;
return true;
}
else if (name == "y") {
value = position.y;
return true;
}
else if (name == "sprite_scale") {
value = sprite.getScale().x; // Assuming uniform scale
return true;
}
return false;
}

View File

@ -46,6 +46,11 @@ public:
UIEntity();
UIEntity(UIGrid&);
// Property system for animations
bool setProperty(const std::string& name, float value);
bool setProperty(const std::string& name, int value);
bool getProperty(const std::string& name, float& value) const;
static PyObject* at(PyUIEntityObject* self, PyObject* o);
static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds);

View File

@ -51,6 +51,15 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
target.draw(box);
box.move(-offset);
// Sort children by z_index if needed
if (children_need_sort && !children->empty()) {
std::sort(children->begin(), children->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
children_need_sort = false;
}
for (auto drawable : *children) {
drawable->render(offset + box.getPosition(), target);
}
@ -215,6 +224,7 @@ PyGetSetDef UIFrame::getsetters[] = {
{"outline_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Outline color of the rectangle", (void*)1},
{"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL},
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIFRAME},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIFRAME},
{NULL}
};
@ -264,3 +274,152 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
if (err_val) return err_val;
return 0;
}
// Animation property system implementation
bool UIFrame::setProperty(const std::string& name, float value) {
if (name == "x") {
box.setPosition(sf::Vector2f(value, box.getPosition().y));
return true;
} else if (name == "y") {
box.setPosition(sf::Vector2f(box.getPosition().x, value));
return true;
} else if (name == "w") {
box.setSize(sf::Vector2f(value, box.getSize().y));
return true;
} else if (name == "h") {
box.setSize(sf::Vector2f(box.getSize().x, value));
return true;
} else if (name == "outline") {
box.setOutlineThickness(value);
return true;
} else if (name == "fill_color.r") {
auto color = box.getFillColor();
color.r = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
return true;
} else if (name == "fill_color.g") {
auto color = box.getFillColor();
color.g = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
return true;
} else if (name == "fill_color.b") {
auto color = box.getFillColor();
color.b = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
return true;
} else if (name == "fill_color.a") {
auto color = box.getFillColor();
color.a = std::clamp(static_cast<int>(value), 0, 255);
box.setFillColor(color);
return true;
} else if (name == "outline_color.r") {
auto color = box.getOutlineColor();
color.r = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
return true;
} else if (name == "outline_color.g") {
auto color = box.getOutlineColor();
color.g = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
return true;
} else if (name == "outline_color.b") {
auto color = box.getOutlineColor();
color.b = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
return true;
} else if (name == "outline_color.a") {
auto color = box.getOutlineColor();
color.a = std::clamp(static_cast<int>(value), 0, 255);
box.setOutlineColor(color);
return true;
}
return false;
}
bool UIFrame::setProperty(const std::string& name, const sf::Color& value) {
if (name == "fill_color") {
box.setFillColor(value);
return true;
} else if (name == "outline_color") {
box.setOutlineColor(value);
return true;
}
return false;
}
bool UIFrame::setProperty(const std::string& name, const sf::Vector2f& value) {
if (name == "position") {
box.setPosition(value);
return true;
} else if (name == "size") {
box.setSize(value);
return true;
}
return false;
}
bool UIFrame::getProperty(const std::string& name, float& value) const {
if (name == "x") {
value = box.getPosition().x;
return true;
} else if (name == "y") {
value = box.getPosition().y;
return true;
} else if (name == "w") {
value = box.getSize().x;
return true;
} else if (name == "h") {
value = box.getSize().y;
return true;
} else if (name == "outline") {
value = box.getOutlineThickness();
return true;
} else if (name == "fill_color.r") {
value = box.getFillColor().r;
return true;
} else if (name == "fill_color.g") {
value = box.getFillColor().g;
return true;
} else if (name == "fill_color.b") {
value = box.getFillColor().b;
return true;
} else if (name == "fill_color.a") {
value = box.getFillColor().a;
return true;
} else if (name == "outline_color.r") {
value = box.getOutlineColor().r;
return true;
} else if (name == "outline_color.g") {
value = box.getOutlineColor().g;
return true;
} else if (name == "outline_color.b") {
value = box.getOutlineColor().b;
return true;
} else if (name == "outline_color.a") {
value = box.getOutlineColor().a;
return true;
}
return false;
}
bool UIFrame::getProperty(const std::string& name, sf::Color& value) const {
if (name == "fill_color") {
value = box.getFillColor();
return true;
} else if (name == "outline_color") {
value = box.getOutlineColor();
return true;
}
return false;
}
bool UIFrame::getProperty(const std::string& name, sf::Vector2f& value) const {
if (name == "position") {
value = box.getPosition();
return true;
} else if (name == "size") {
value = box.getSize();
return true;
}
return false;
}

View File

@ -28,6 +28,7 @@ public:
sf::RectangleShape box;
float outline;
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children;
bool children_need_sort = true; // Dirty flag for z_index sorting optimization
void render(sf::Vector2f, sf::RenderTarget&) override final;
void move(sf::Vector2f);
PyObjectsEnum derived_type() override final;
@ -42,6 +43,15 @@ public:
static PyGetSetDef getsetters[];
static PyObject* repr(PyUIFrameObject* self);
static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds);
// Animation property system
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, const sf::Color& value) override;
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
bool getProperty(const std::string& name, float& value) const override;
bool getProperty(const std::string& name, sf::Color& value) const override;
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
};
namespace mcrfpydef {

View File

@ -1,6 +1,7 @@
#include "UIGrid.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include <algorithm>
UIGrid::UIGrid() {}
@ -218,27 +219,66 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
int grid_x, grid_y;
PyObject* textureObj;
PyObject* textureObj = Py_None;
//float box_x, box_y, box_w, box_h;
PyObject* pos, *size;
PyObject* pos = NULL;
PyObject* size = NULL;
//if (!PyArg_ParseTuple(args, "iiOffff", &grid_x, &grid_y, &textureObj, &box_x, &box_y, &box_w, &box_h)) {
if (!PyArg_ParseTuple(args, "iiOOO", &grid_x, &grid_y, &textureObj, &pos, &size)) {
if (!PyArg_ParseTuple(args, "ii|OOO", &grid_x, &grid_y, &textureObj, &pos, &size)) {
return -1; // If parsing fails, return an error
}
PyVectorObject* pos_result = PyVector::from_arg(pos);
if (!pos_result)
{
PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
return -1;
// Default position and size if not provided
PyVectorObject* pos_result = NULL;
PyVectorObject* size_result = NULL;
if (pos) {
pos_result = PyVector::from_arg(pos);
if (!pos_result)
{
PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
return -1;
}
} else {
// Default position (0, 0)
PyObject* vector_class = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (vector_class) {
PyObject* pos_obj = PyObject_CallFunction(vector_class, "ff", 0.0f, 0.0f);
Py_DECREF(vector_class);
if (pos_obj) {
pos_result = (PyVectorObject*)pos_obj;
}
}
if (!pos_result) {
PyErr_SetString(PyExc_RuntimeError, "Failed to create default position vector");
return -1;
}
}
PyVectorObject* size_result = PyVector::from_arg(size);
if (!size_result)
{
PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
return -1;
if (size) {
size_result = PyVector::from_arg(size);
if (!size_result)
{
PyErr_SetString(PyExc_TypeError, "size must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
return -1;
}
} else {
// Default size based on grid dimensions
float default_w = grid_x * 16.0f; // Assuming 16 pixel tiles
float default_h = grid_y * 16.0f;
PyObject* vector_class = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (vector_class) {
PyObject* size_obj = PyObject_CallFunction(vector_class, "ff", default_w, default_h);
Py_DECREF(vector_class);
if (size_obj) {
size_result = (PyVectorObject*)size_obj;
}
}
if (!size_result) {
PyErr_SetString(PyExc_RuntimeError, "Failed to create default size vector");
return -1;
}
}
// Convert PyObject texture to IndexTexture*
@ -246,7 +286,7 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
std::shared_ptr<PyTexture> texture_ptr = nullptr;
// Allow None for texture
// Allow None for texture - use default texture in that case
if (textureObj != Py_None) {
//if (!PyObject_IsInstance(textureObj, (PyObject*)&PyTextureType)) {
if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
@ -255,6 +295,9 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
}
PyTextureObject* pyTexture = reinterpret_cast<PyTextureObject*>(textureObj);
texture_ptr = pyTexture->data;
} else {
// Use default texture when None is provided
texture_ptr = McRFPy_API::default_texture;
}
// Initialize UIGrid - texture_ptr will be nullptr if texture was None
@ -458,6 +501,7 @@ PyGetSetDef UIGrid::getsetters[] = {
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIGRID},
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID},
{NULL} /* Sentinel */
};
@ -581,15 +625,196 @@ return NULL;
}
int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value) {
auto list = self->data.get();
if (!list) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return -1;
}
// Handle negative indexing
while (index < 0) index += list->size();
// Bounds check
if (index >= list->size()) {
PyErr_SetString(PyExc_IndexError, "EntityCollection assignment index out of range");
return -1;
}
// Get iterator to the target position
auto it = list->begin();
std::advance(it, index);
// Handle deletion
if (value == NULL) {
// Clear grid reference from the entity being removed
(*it)->grid = nullptr;
list->erase(it);
return 0;
}
// Type checking - must be an Entity
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
PyErr_SetString(PyExc_TypeError, "EntityCollection can only contain Entity objects");
return -1;
}
// Get the C++ object from the Python object
PyUIEntityObject* entity = (PyUIEntityObject*)value;
if (!entity->data) {
PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object");
return -1;
}
// Clear grid reference from the old entity
(*it)->grid = nullptr;
// Replace the element and set grid reference
*it = entity->data;
entity->data->grid = self->grid;
return 0;
}
int UIEntityCollection::contains(PyUIEntityCollectionObject* self, PyObject* value) {
auto list = self->data.get();
if (!list) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return -1;
}
// Type checking - must be an Entity
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
// Not an Entity, so it can't be in the collection
return 0;
}
// Get the C++ object from the Python object
PyUIEntityObject* entity = (PyUIEntityObject*)value;
if (!entity->data) {
return 0;
}
// Search for the object by comparing C++ pointers
for (const auto& ent : *list) {
if (ent.get() == entity->data.get()) {
return 1; // Found
}
}
return 0; // Not found
}
PyObject* UIEntityCollection::concat(PyUIEntityCollectionObject* self, PyObject* other) {
// Create a new Python list containing elements from both collections
if (!PySequence_Check(other)) {
PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to EntityCollection");
return NULL;
}
Py_ssize_t self_len = self->data->size();
Py_ssize_t other_len = PySequence_Length(other);
if (other_len == -1) {
return NULL; // Error already set
}
PyObject* result_list = PyList_New(self_len + other_len);
if (!result_list) {
return NULL;
}
// Add all elements from self
Py_ssize_t idx = 0;
for (const auto& entity : *self->data) {
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
auto obj = (PyUIEntityObject*)type->tp_alloc(type, 0);
if (obj) {
obj->data = entity;
PyList_SET_ITEM(result_list, idx, (PyObject*)obj); // Steals reference
} else {
Py_DECREF(result_list);
Py_DECREF(type);
return NULL;
}
Py_DECREF(type);
idx++;
}
// Add all elements from other
for (Py_ssize_t i = 0; i < other_len; i++) {
PyObject* item = PySequence_GetItem(other, i);
if (!item) {
Py_DECREF(result_list);
return NULL;
}
PyList_SET_ITEM(result_list, self_len + i, item); // Steals reference
}
return result_list;
}
PyObject* UIEntityCollection::inplace_concat(PyUIEntityCollectionObject* self, PyObject* other) {
if (!PySequence_Check(other)) {
PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to EntityCollection");
return NULL;
}
// First, validate ALL items in the sequence before modifying anything
Py_ssize_t other_len = PySequence_Length(other);
if (other_len == -1) {
return NULL; // Error already set
}
// Validate all items first
for (Py_ssize_t i = 0; i < other_len; i++) {
PyObject* item = PySequence_GetItem(other, i);
if (!item) {
return NULL;
}
// Type check
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
Py_DECREF(item);
PyErr_Format(PyExc_TypeError,
"EntityCollection can only contain Entity objects; "
"got %s at index %zd", Py_TYPE(item)->tp_name, i);
return NULL;
}
Py_DECREF(item);
}
// All items validated, now we can safely add them
for (Py_ssize_t i = 0; i < other_len; i++) {
PyObject* item = PySequence_GetItem(other, i);
if (!item) {
return NULL; // Shouldn't happen, but be safe
}
// Use the existing append method which handles grid references
PyObject* result = append(self, item);
Py_DECREF(item);
if (!result) {
return NULL; // append() failed
}
Py_DECREF(result); // append returns Py_None
}
Py_INCREF(self);
return (PyObject*)self;
}
PySequenceMethods UIEntityCollection::sqmethods = {
.sq_length = (lenfunc)UIEntityCollection::len,
.sq_concat = (binaryfunc)UIEntityCollection::concat,
.sq_repeat = NULL,
.sq_item = (ssizeargfunc)UIEntityCollection::getitem,
//.sq_item_by_index = UIEntityCollection::getitem
//.sq_slice - return a subset of the iterable
//.sq_ass_item - called when `o[x] = y` is executed (x is any object type)
//.sq_ass_slice - cool; no thanks, for now
//.sq_contains - called when `x in o` is executed
//.sq_ass_item_by_index - called when `o[x] = y` is executed (x is explictly an integer)
.was_sq_slice = NULL,
.sq_ass_item = (ssizeobjargproc)UIEntityCollection::setitem,
.was_sq_ass_slice = NULL,
.sq_contains = (objobjproc)UIEntityCollection::contains,
.sq_inplace_concat = (binaryfunc)UIEntityCollection::inplace_concat,
.sq_inplace_repeat = NULL
};
PyObject* UIEntityCollection::append(PyUIEntityCollectionObject* self, PyObject* o)
@ -616,23 +841,29 @@ PyObject* UIEntityCollection::remove(PyUIEntityCollectionObject* self, PyObject*
{
if (!PyLong_Check(o))
{
PyErr_SetString(PyExc_TypeError, "UICollection.remove requires an integer index to remove");
PyErr_SetString(PyExc_TypeError, "EntityCollection.remove requires an integer index to remove");
return NULL;
}
long index = PyLong_AsLong(o);
// Handle negative indexing
while (index < 0) index += self->data->size();
if (index >= self->data->size())
{
PyErr_SetString(PyExc_ValueError, "Index out of range");
return NULL;
}
else if (index < 0)
{
PyErr_SetString(PyExc_NotImplementedError, "reverse indexing is not implemented.");
return NULL;
}
// Get iterator to the entity to remove
auto it = self->data->begin();
std::advance(it, index);
// Clear grid reference before removing
(*it)->grid = nullptr;
// release the shared pointer at correct part of the list
self->data->erase(std::next(self->data->begin(), index));
self->data->erase(it);
Py_INCREF(Py_None);
return Py_None;
}
@ -675,10 +906,275 @@ PyObject* UIEntityCollection::extend(PyUIEntityCollectionObject* self, PyObject*
return Py_None;
}
PyObject* UIEntityCollection::index_method(PyUIEntityCollectionObject* self, PyObject* value) {
auto list = self->data.get();
if (!list) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return NULL;
}
// Type checking - must be an Entity
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
PyErr_SetString(PyExc_TypeError, "EntityCollection.index requires an Entity object");
return NULL;
}
// Get the C++ object from the Python object
PyUIEntityObject* entity = (PyUIEntityObject*)value;
if (!entity->data) {
PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object");
return NULL;
}
// Search for the object
Py_ssize_t idx = 0;
for (const auto& ent : *list) {
if (ent.get() == entity->data.get()) {
return PyLong_FromSsize_t(idx);
}
idx++;
}
PyErr_SetString(PyExc_ValueError, "Entity not in EntityCollection");
return NULL;
}
PyObject* UIEntityCollection::count(PyUIEntityCollectionObject* self, PyObject* value) {
auto list = self->data.get();
if (!list) {
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
return NULL;
}
// Type checking - must be an Entity
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
// Not an Entity, so count is 0
return PyLong_FromLong(0);
}
// Get the C++ object from the Python object
PyUIEntityObject* entity = (PyUIEntityObject*)value;
if (!entity->data) {
return PyLong_FromLong(0);
}
// Count occurrences
Py_ssize_t count = 0;
for (const auto& ent : *list) {
if (ent.get() == entity->data.get()) {
count++;
}
}
return PyLong_FromSsize_t(count);
}
PyObject* UIEntityCollection::subscript(PyUIEntityCollectionObject* self, PyObject* key) {
if (PyLong_Check(key)) {
// Single index - delegate to sq_item
Py_ssize_t index = PyLong_AsSsize_t(key);
if (index == -1 && PyErr_Occurred()) {
return NULL;
}
return getitem(self, index);
} else if (PySlice_Check(key)) {
// Handle slice
Py_ssize_t start, stop, step, slicelength;
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
return NULL;
}
PyObject* result_list = PyList_New(slicelength);
if (!result_list) {
return NULL;
}
// Iterate through the list with slice parameters
auto it = self->data->begin();
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
auto cur_it = it;
std::advance(cur_it, cur);
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
auto obj = (PyUIEntityObject*)type->tp_alloc(type, 0);
if (obj) {
obj->data = *cur_it;
PyList_SET_ITEM(result_list, i, (PyObject*)obj); // Steals reference
} else {
Py_DECREF(result_list);
Py_DECREF(type);
return NULL;
}
Py_DECREF(type);
}
return result_list;
} else {
PyErr_Format(PyExc_TypeError, "EntityCollection indices must be integers or slices, not %.200s",
Py_TYPE(key)->tp_name);
return NULL;
}
}
int UIEntityCollection::ass_subscript(PyUIEntityCollectionObject* self, PyObject* key, PyObject* value) {
if (PyLong_Check(key)) {
// Single index - delegate to sq_ass_item
Py_ssize_t index = PyLong_AsSsize_t(key);
if (index == -1 && PyErr_Occurred()) {
return -1;
}
return setitem(self, index, value);
} else if (PySlice_Check(key)) {
// Handle slice assignment/deletion
Py_ssize_t start, stop, step, slicelength;
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
return -1;
}
if (value == NULL) {
// Deletion
if (step != 1) {
// For non-contiguous slices, delete from highest to lowest to maintain indices
std::vector<Py_ssize_t> indices;
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
indices.push_back(cur);
}
// Sort in descending order
std::sort(indices.begin(), indices.end(), std::greater<Py_ssize_t>());
// Delete each index
for (Py_ssize_t idx : indices) {
auto it = self->data->begin();
std::advance(it, idx);
(*it)->grid = nullptr; // Clear grid reference
self->data->erase(it);
}
} else {
// Contiguous slice - delete range
auto it_start = self->data->begin();
auto it_stop = self->data->begin();
std::advance(it_start, start);
std::advance(it_stop, stop);
// Clear grid references
for (auto it = it_start; it != it_stop; ++it) {
(*it)->grid = nullptr;
}
self->data->erase(it_start, it_stop);
}
return 0;
} else {
// Assignment
if (!PySequence_Check(value)) {
PyErr_SetString(PyExc_TypeError, "can only assign sequence to slice");
return -1;
}
Py_ssize_t value_len = PySequence_Length(value);
if (value_len == -1) {
return -1;
}
// Validate all items first
std::vector<std::shared_ptr<UIEntity>> new_items;
for (Py_ssize_t i = 0; i < value_len; i++) {
PyObject* item = PySequence_GetItem(value, i);
if (!item) {
return -1;
}
// Type check
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) {
Py_DECREF(item);
PyErr_Format(PyExc_TypeError,
"EntityCollection can only contain Entity objects; "
"got %s at index %zd", Py_TYPE(item)->tp_name, i);
return -1;
}
PyUIEntityObject* entity = (PyUIEntityObject*)item;
Py_DECREF(item);
new_items.push_back(entity->data);
}
// Now perform the assignment
if (step == 1) {
// Contiguous slice
if (slicelength != value_len) {
// Need to resize - remove old items and insert new ones
auto it_start = self->data->begin();
auto it_stop = self->data->begin();
std::advance(it_start, start);
std::advance(it_stop, stop);
// Clear grid references from old items
for (auto it = it_start; it != it_stop; ++it) {
(*it)->grid = nullptr;
}
// Erase old range
it_start = self->data->erase(it_start, it_stop);
// Insert new items
for (const auto& entity : new_items) {
entity->grid = self->grid;
it_start = self->data->insert(it_start, entity);
++it_start;
}
} else {
// Same size, just replace
auto it = self->data->begin();
std::advance(it, start);
for (const auto& entity : new_items) {
(*it)->grid = nullptr; // Clear old grid ref
*it = entity;
entity->grid = self->grid; // Set new grid ref
++it;
}
}
} else {
// Extended slice
if (slicelength != value_len) {
PyErr_Format(PyExc_ValueError,
"attempt to assign sequence of size %zd to extended slice of size %zd",
value_len, slicelength);
return -1;
}
auto list_it = self->data->begin();
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
auto cur_it = list_it;
std::advance(cur_it, cur);
(*cur_it)->grid = nullptr; // Clear old grid ref
*cur_it = new_items[i];
new_items[i]->grid = self->grid; // Set new grid ref
}
}
return 0;
}
} else {
PyErr_Format(PyExc_TypeError, "EntityCollection indices must be integers or slices, not %.200s",
Py_TYPE(key)->tp_name);
return -1;
}
}
PyMappingMethods UIEntityCollection::mpmethods = {
.mp_length = (lenfunc)UIEntityCollection::len,
.mp_subscript = (binaryfunc)UIEntityCollection::subscript,
.mp_ass_subscript = (objobjargproc)UIEntityCollection::ass_subscript
};
PyMethodDef UIEntityCollection::methods[] = {
{"append", (PyCFunction)UIEntityCollection::append, METH_O},
{"extend", (PyCFunction)UIEntityCollection::extend, METH_O},
{"remove", (PyCFunction)UIEntityCollection::remove, METH_O},
{"index", (PyCFunction)UIEntityCollection::index_method, METH_O},
{"count", (PyCFunction)UIEntityCollection::count, METH_O},
{NULL, NULL, 0, NULL}
};
@ -723,3 +1219,115 @@ PyObject* UIEntityCollection::iter(PyUIEntityCollectionObject* self)
Py_DECREF(iterType);
return (PyObject*)iterObj;
}
// Property system implementation for animations
bool UIGrid::setProperty(const std::string& name, float value) {
if (name == "x") {
box.setPosition(sf::Vector2f(value, box.getPosition().y));
output.setPosition(box.getPosition());
return true;
}
else if (name == "y") {
box.setPosition(sf::Vector2f(box.getPosition().x, value));
output.setPosition(box.getPosition());
return true;
}
else if (name == "w" || name == "width") {
box.setSize(sf::Vector2f(value, box.getSize().y));
output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y));
return true;
}
else if (name == "h" || name == "height") {
box.setSize(sf::Vector2f(box.getSize().x, value));
output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y));
return true;
}
else if (name == "center_x") {
center_x = value;
return true;
}
else if (name == "center_y") {
center_y = value;
return true;
}
else if (name == "zoom") {
zoom = value;
return true;
}
else if (name == "z_index") {
z_index = static_cast<int>(value);
return true;
}
return false;
}
bool UIGrid::setProperty(const std::string& name, const sf::Vector2f& value) {
if (name == "position") {
box.setPosition(value);
output.setPosition(box.getPosition());
return true;
}
else if (name == "size") {
box.setSize(value);
output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y));
return true;
}
else if (name == "center") {
center_x = value.x;
center_y = value.y;
return true;
}
return false;
}
bool UIGrid::getProperty(const std::string& name, float& value) const {
if (name == "x") {
value = box.getPosition().x;
return true;
}
else if (name == "y") {
value = box.getPosition().y;
return true;
}
else if (name == "w" || name == "width") {
value = box.getSize().x;
return true;
}
else if (name == "h" || name == "height") {
value = box.getSize().y;
return true;
}
else if (name == "center_x") {
value = center_x;
return true;
}
else if (name == "center_y") {
value = center_y;
return true;
}
else if (name == "zoom") {
value = zoom;
return true;
}
else if (name == "z_index") {
value = static_cast<float>(z_index);
return true;
}
return false;
}
bool UIGrid::getProperty(const std::string& name, sf::Vector2f& value) const {
if (name == "position") {
value = box.getPosition();
return true;
}
else if (name == "size") {
value = box.getSize();
return true;
}
else if (name == "center") {
value = sf::Vector2f(center_x, center_y);
return true;
}
return false;
}

View File

@ -45,6 +45,12 @@ public:
sf::RenderTexture renderTexture;
std::vector<UIGridPoint> points;
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> entities;
// Property system for animations
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
bool getProperty(const std::string& name, float& value) const override;
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
static int init(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* get_grid_size(PyUIGridObject* self, void* closure);
@ -76,15 +82,24 @@ typedef struct {
class UIEntityCollection {
public:
static PySequenceMethods sqmethods;
static PyMappingMethods mpmethods;
static PyObject* append(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* extend(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* remove(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* index_method(PyUIEntityCollectionObject* self, PyObject* value);
static PyObject* count(PyUIEntityCollectionObject* self, PyObject* value);
static PyMethodDef methods[];
static PyObject* repr(PyUIEntityCollectionObject* self);
static int init(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds);
static PyObject* iter(PyUIEntityCollectionObject* self);
static Py_ssize_t len(PyUIEntityCollectionObject* self);
static PyObject* getitem(PyUIEntityCollectionObject* self, Py_ssize_t index);
static int setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value);
static int contains(PyUIEntityCollectionObject* self, PyObject* value);
static PyObject* concat(PyUIEntityCollectionObject* self, PyObject* other);
static PyObject* inplace_concat(PyUIEntityCollectionObject* self, PyObject* other);
static PyObject* subscript(PyUIEntityCollectionObject* self, PyObject* key);
static int ass_subscript(PyUIEntityCollectionObject* self, PyObject* key, PyObject* value);
};
typedef struct {
@ -174,6 +189,7 @@ namespace mcrfpydef {
},
.tp_repr = (reprfunc)UIEntityCollection::repr,
.tp_as_sequence = &UIEntityCollection::sqmethods,
.tp_as_mapping = &UIEntityCollection::mpmethods,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Iterable, indexable collection of Entities"),
.tp_iter = (getiterfunc)UIEntityCollection::iter,

View File

@ -58,7 +58,7 @@ void UISprite::setSpriteIndex(int _sprite_index)
sprite = ptex->sprite(sprite_index, sprite.getPosition(), sprite.getScale());
}
sf::Vector2f UISprite::getScale()
sf::Vector2f UISprite::getScale() const
{
return sprite.getScale();
}
@ -202,6 +202,7 @@ PyGetSetDef UISprite::getsetters[] = {
{"sprite_number", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Which sprite on the texture is shown", NULL},
{"texture", (getter)UISprite::get_texture, (setter)UISprite::set_texture, "Texture object", NULL},
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UISPRITE},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UISPRITE},
{NULL}
};
@ -224,8 +225,8 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
//std::cout << "Init called\n";
static const char* keywords[] = { "x", "y", "texture", "sprite_index", "scale", nullptr };
float x = 0.0f, y = 0.0f, scale = 1.0f;
int sprite_index;
PyObject* texture;
int sprite_index = 0;
PyObject* texture = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOif",
const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &scale))
@ -233,15 +234,107 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
return -1;
}
// check types for texture
//if (texture != NULL && !PyObject_IsInstance(texture, (PyObject*)&PyTextureType)){
if (texture != NULL && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance");
// 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) {
auto pytexture = (PyTextureObject*)texture;
texture_ptr = pytexture->data;
} else {
// Use default texture when None or not provided
texture_ptr = McRFPy_API::default_texture;
}
if (!texture_ptr) {
PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
return -1;
}
auto pytexture = (PyTextureObject*)texture;
self->data = std::make_shared<UISprite>(pytexture->data, sprite_index, sf::Vector2f(x, y), scale);
self->data = std::make_shared<UISprite>(texture_ptr, sprite_index, sf::Vector2f(x, y), scale);
self->data->setPosition(sf::Vector2f(x, y));
return 0;
}
// Property system implementation for animations
bool UISprite::setProperty(const std::string& name, float value) {
if (name == "x") {
sprite.setPosition(sf::Vector2f(value, sprite.getPosition().y));
return true;
}
else if (name == "y") {
sprite.setPosition(sf::Vector2f(sprite.getPosition().x, value));
return true;
}
else if (name == "scale") {
sprite.setScale(sf::Vector2f(value, value));
return true;
}
else if (name == "scale_x") {
sprite.setScale(sf::Vector2f(value, sprite.getScale().y));
return true;
}
else if (name == "scale_y") {
sprite.setScale(sf::Vector2f(sprite.getScale().x, value));
return true;
}
else if (name == "z_index") {
z_index = static_cast<int>(value);
return true;
}
return false;
}
bool UISprite::setProperty(const std::string& name, int value) {
if (name == "sprite_number") {
setSpriteIndex(value);
return true;
}
else if (name == "z_index") {
z_index = value;
return true;
}
return false;
}
bool UISprite::getProperty(const std::string& name, float& value) const {
if (name == "x") {
value = sprite.getPosition().x;
return true;
}
else if (name == "y") {
value = sprite.getPosition().y;
return true;
}
else if (name == "scale") {
value = sprite.getScale().x; // Assuming uniform scale
return true;
}
else if (name == "scale_x") {
value = sprite.getScale().x;
return true;
}
else if (name == "scale_y") {
value = sprite.getScale().y;
return true;
}
else if (name == "z_index") {
value = static_cast<float>(z_index);
return true;
}
return false;
}
bool UISprite::getProperty(const std::string& name, int& value) const {
if (name == "sprite_number") {
value = sprite_index;
return true;
}
else if (name == "z_index") {
value = z_index;
return true;
}
return false;
}

View File

@ -33,7 +33,7 @@ public:
void setPosition(sf::Vector2f);
sf::Vector2f getPosition();
void setScale(sf::Vector2f);
sf::Vector2f getScale();
sf::Vector2f getScale() const;
void setSpriteIndex(int);
int getSpriteIndex();
@ -41,6 +41,12 @@ public:
std::shared_ptr<PyTexture> getTexture();
PyObjectsEnum derived_type() override final;
// Property system for animations
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, int value) override;
bool getProperty(const std::string& name, float& value) const override;
bool getProperty(const std::string& name, int& value) const override;
static PyObject* get_float_member(PyUISpriteObject* self, void* closure);

View File

@ -3,6 +3,8 @@
#include "CommandLineParser.h"
#include "McRogueFaceConfig.h"
#include "McRFPy_API.h"
#include "PyFont.h"
#include "PyTexture.h"
#include <Python.h>
#include <iostream>
#include <filesystem>
@ -44,14 +46,27 @@ int run_game_engine(const McRogueFaceConfig& config)
int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv[])
{
// Create a headless game engine for automation API support
McRogueFaceConfig engine_config = config;
engine_config.headless = true; // Force headless mode for Python interpreter
GameEngine* engine = new GameEngine(engine_config);
// Create a game engine with the requested configuration
GameEngine* engine = new GameEngine(config);
// Initialize Python with configuration
McRFPy_API::init_python_with_config(config, argc, argv);
// Import mcrfpy module and store reference
McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy");
if (!McRFPy_API::mcrf_module) {
PyErr_Print();
std::cerr << "Failed to import mcrfpy module" << std::endl;
} else {
// Set up default_font and default_texture if not already done
if (!McRFPy_API::default_font) {
McRFPy_API::default_font = std::make_shared<PyFont>("assets/JetbrainsMono.ttf");
McRFPy_API::default_texture = std::make_shared<PyTexture>("assets/kenney_tinydungeon.png", 16, 16);
}
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject());
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject());
}
// Handle different Python modes
if (!config.python_command.empty()) {
// Execute command from -c
@ -161,6 +176,9 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
PyRun_InteractiveLoop(stdin, "<stdin>");
}
// Run the game engine after script execution
engine->run();
Py_Finalize();
delete engine;
return result;

165
tests/animation_demo.py Normal file
View File

@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""Animation System Demo - Shows all animation capabilities"""
import mcrfpy
import math
# Create main scene
mcrfpy.createScene("animation_demo")
ui = mcrfpy.sceneUI("animation_demo")
mcrfpy.setScene("animation_demo")
# Title
title = mcrfpy.Caption((400, 30), "McRogueFace Animation System Demo", mcrfpy.default_font)
title.size = 24
title.fill_color = (255, 255, 255)
# Note: centered property doesn't exist for Caption
ui.append(title)
# 1. Position Animation Demo
pos_frame = mcrfpy.Frame(50, 100, 80, 80)
pos_frame.fill_color = (255, 100, 100)
pos_frame.outline = 2
ui.append(pos_frame)
pos_label = mcrfpy.Caption((50, 80), "Position Animation", mcrfpy.default_font)
pos_label.fill_color = (200, 200, 200)
ui.append(pos_label)
# 2. Size Animation Demo
size_frame = mcrfpy.Frame(200, 100, 50, 50)
size_frame.fill_color = (100, 255, 100)
size_frame.outline = 2
ui.append(size_frame)
size_label = mcrfpy.Caption((200, 80), "Size Animation", mcrfpy.default_font)
size_label.fill_color = (200, 200, 200)
ui.append(size_label)
# 3. Color Animation Demo
color_frame = mcrfpy.Frame(350, 100, 80, 80)
color_frame.fill_color = (255, 0, 0)
ui.append(color_frame)
color_label = mcrfpy.Caption((350, 80), "Color Animation", mcrfpy.default_font)
color_label.fill_color = (200, 200, 200)
ui.append(color_label)
# 4. Easing Functions Demo
easing_y = 250
easing_frames = []
easings = ["linear", "easeIn", "easeOut", "easeInOut", "easeInElastic", "easeOutBounce"]
for i, easing in enumerate(easings):
x = 50 + i * 120
frame = mcrfpy.Frame(x, easing_y, 20, 20)
frame.fill_color = (100, 150, 255)
ui.append(frame)
easing_frames.append((frame, easing))
label = mcrfpy.Caption((x, easing_y - 20), easing, mcrfpy.default_font)
label.size = 12
label.fill_color = (200, 200, 200)
ui.append(label)
# 5. Complex Animation Demo
complex_frame = mcrfpy.Frame(300, 350, 100, 100)
complex_frame.fill_color = (128, 128, 255)
complex_frame.outline = 3
ui.append(complex_frame)
complex_label = mcrfpy.Caption((300, 330), "Complex Multi-Property", mcrfpy.default_font)
complex_label.fill_color = (200, 200, 200)
ui.append(complex_label)
# Start animations
def start_animations(runtime):
# 1. Position animation - back and forth
x_anim = mcrfpy.Animation("x", 500.0, 3.0, "easeInOut")
x_anim.start(pos_frame)
# 2. Size animation - pulsing
w_anim = mcrfpy.Animation("w", 150.0, 2.0, "easeInOut")
h_anim = mcrfpy.Animation("h", 150.0, 2.0, "easeInOut")
w_anim.start(size_frame)
h_anim.start(size_frame)
# 3. Color animation - rainbow cycle
color_anim = mcrfpy.Animation("fill_color", (0, 255, 255, 255), 2.0, "linear")
color_anim.start(color_frame)
# 4. Easing demos - all move up with different easings
for frame, easing in easing_frames:
y_anim = mcrfpy.Animation("y", 150.0, 2.0, easing)
y_anim.start(frame)
# 5. Complex animation - multiple properties
cx_anim = mcrfpy.Animation("x", 500.0, 4.0, "easeInOut")
cy_anim = mcrfpy.Animation("y", 400.0, 4.0, "easeOut")
cw_anim = mcrfpy.Animation("w", 150.0, 4.0, "easeInElastic")
ch_anim = mcrfpy.Animation("h", 150.0, 4.0, "easeInElastic")
outline_anim = mcrfpy.Animation("outline", 10.0, 4.0, "linear")
cx_anim.start(complex_frame)
cy_anim.start(complex_frame)
cw_anim.start(complex_frame)
ch_anim.start(complex_frame)
outline_anim.start(complex_frame)
# Individual color component animations
r_anim = mcrfpy.Animation("fill_color.r", 255.0, 4.0, "easeInOut")
g_anim = mcrfpy.Animation("fill_color.g", 100.0, 4.0, "easeInOut")
b_anim = mcrfpy.Animation("fill_color.b", 50.0, 4.0, "easeInOut")
r_anim.start(complex_frame)
g_anim.start(complex_frame)
b_anim.start(complex_frame)
print("All animations started!")
# Reverse some animations
def reverse_animations(runtime):
# Position back
x_anim = mcrfpy.Animation("x", 50.0, 3.0, "easeInOut")
x_anim.start(pos_frame)
# Size back
w_anim = mcrfpy.Animation("w", 50.0, 2.0, "easeInOut")
h_anim = mcrfpy.Animation("h", 50.0, 2.0, "easeInOut")
w_anim.start(size_frame)
h_anim.start(size_frame)
# Color cycle continues
color_anim = mcrfpy.Animation("fill_color", (255, 0, 255, 255), 2.0, "linear")
color_anim.start(color_frame)
# Easing frames back down
for frame, easing in easing_frames:
y_anim = mcrfpy.Animation("y", 250.0, 2.0, easing)
y_anim.start(frame)
# Continue color cycle
def cycle_colors(runtime):
color_anim = mcrfpy.Animation("fill_color", (255, 255, 0, 255), 2.0, "linear")
color_anim.start(color_frame)
# Info text
info = mcrfpy.Caption((400, 550), "Watch as different properties animate with various easing functions!", mcrfpy.default_font)
info.fill_color = (255, 255, 200)
# Note: centered property doesn't exist for Caption
ui.append(info)
# Schedule animations
mcrfpy.setTimer("start", start_animations, 500)
mcrfpy.setTimer("reverse", reverse_animations, 4000)
mcrfpy.setTimer("cycle", cycle_colors, 2500)
# Exit handler
def on_key(key):
if key == "Escape":
mcrfpy.exit()
mcrfpy.keypressScene(on_key)
print("Animation demo started! Press Escape to exit.")

View File

@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Generate caption documentation screenshot with proper font"""
import mcrfpy
from mcrfpy import automation
import sys
def capture_caption(runtime):
"""Capture caption example after render loop starts"""
# Take screenshot
automation.screenshot("mcrogueface.github.io/images/ui_caption_example.png")
print("Caption screenshot saved!")
# Exit after capturing
sys.exit(0)
# Create scene
mcrfpy.createScene("captions")
# Title
title = mcrfpy.Caption(400, 30, "Caption Examples")
title.font = mcrfpy.default_font
title.font_size = 28
title.font_color = (255, 255, 255)
# Different sizes
size_label = mcrfpy.Caption(100, 100, "Different Sizes:")
size_label.font = mcrfpy.default_font
size_label.font_color = (200, 200, 200)
large = mcrfpy.Caption(300, 100, "Large Text (24pt)")
large.font = mcrfpy.default_font
large.font_size = 24
large.font_color = (255, 255, 255)
medium = mcrfpy.Caption(300, 140, "Medium Text (18pt)")
medium.font = mcrfpy.default_font
medium.font_size = 18
medium.font_color = (255, 255, 255)
small = mcrfpy.Caption(300, 170, "Small Text (14pt)")
small.font = mcrfpy.default_font
small.font_size = 14
small.font_color = (255, 255, 255)
# Different colors
color_label = mcrfpy.Caption(100, 230, "Different Colors:")
color_label.font = mcrfpy.default_font
color_label.font_color = (200, 200, 200)
white_text = mcrfpy.Caption(300, 230, "White Text")
white_text.font = mcrfpy.default_font
white_text.font_color = (255, 255, 255)
green_text = mcrfpy.Caption(300, 260, "Green Text")
green_text.font = mcrfpy.default_font
green_text.font_color = (100, 255, 100)
red_text = mcrfpy.Caption(300, 290, "Red Text")
red_text.font = mcrfpy.default_font
red_text.font_color = (255, 100, 100)
blue_text = mcrfpy.Caption(300, 320, "Blue Text")
blue_text.font = mcrfpy.default_font
blue_text.font_color = (100, 150, 255)
# Caption with background
bg_label = mcrfpy.Caption(100, 380, "With Background:")
bg_label.font = mcrfpy.default_font
bg_label.font_color = (200, 200, 200)
# Frame background
frame = mcrfpy.Frame(280, 370, 250, 50)
frame.bgcolor = (64, 64, 128)
frame.outline = 2
framed_text = mcrfpy.Caption(405, 395, "Caption on Frame")
framed_text.font = mcrfpy.default_font
framed_text.font_size = 18
framed_text.font_color = (255, 255, 255)
framed_text.centered = True
# Centered text example
center_label = mcrfpy.Caption(100, 460, "Centered Text:")
center_label.font = mcrfpy.default_font
center_label.font_color = (200, 200, 200)
centered = mcrfpy.Caption(400, 460, "This text is centered")
centered.font = mcrfpy.default_font
centered.font_size = 20
centered.font_color = (255, 255, 100)
centered.centered = True
# Multi-line example
multi_label = mcrfpy.Caption(100, 520, "Multi-line:")
multi_label.font = mcrfpy.default_font
multi_label.font_color = (200, 200, 200)
multiline = mcrfpy.Caption(300, 520, "Line 1: McRogueFace\nLine 2: Game Engine\nLine 3: Python API")
multiline.font = mcrfpy.default_font
multiline.font_size = 14
multiline.font_color = (255, 255, 255)
# Add all to scene
ui = mcrfpy.sceneUI("captions")
ui.append(title)
ui.append(size_label)
ui.append(large)
ui.append(medium)
ui.append(small)
ui.append(color_label)
ui.append(white_text)
ui.append(green_text)
ui.append(red_text)
ui.append(blue_text)
ui.append(bg_label)
ui.append(frame)
ui.append(framed_text)
ui.append(center_label)
ui.append(centered)
ui.append(multi_label)
ui.append(multiline)
# Switch to scene
mcrfpy.setScene("captions")
# Set timer to capture after rendering starts
mcrfpy.setTimer("capture", capture_caption, 100)

View File

@ -0,0 +1,451 @@
#!/usr/bin/env python3
"""Generate documentation screenshots for McRogueFace UI elements"""
import mcrfpy
from mcrfpy import automation
import sys
import os
# Crypt of Sokoban color scheme
FRAME_COLOR = mcrfpy.Color(64, 64, 128)
SHADOW_COLOR = mcrfpy.Color(64, 64, 86)
BOX_COLOR = mcrfpy.Color(96, 96, 160)
WHITE = mcrfpy.Color(255, 255, 255)
BLACK = mcrfpy.Color(0, 0, 0)
GREEN = mcrfpy.Color(0, 255, 0)
RED = mcrfpy.Color(255, 0, 0)
# Create texture for sprites
sprite_texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Output directory - create it during setup
output_dir = "mcrogueface.github.io/images"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
def create_caption(x, y, text, font_size=16, text_color=WHITE, outline_color=BLACK):
"""Helper function to create captions with common settings"""
caption = mcrfpy.Caption(mcrfpy.Vector(x, y), text=text)
caption.size = font_size
caption.fill_color = text_color
caption.outline_color = outline_color
return caption
def create_caption_example():
"""Create a scene showing Caption UI element examples"""
mcrfpy.createScene("caption_example")
ui = mcrfpy.sceneUI("caption_example")
# Background frame
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
# Title caption
title = create_caption(200, 50, "Caption Examples", 32)
ui.append(title)
# Different sized captions
caption1 = create_caption(100, 150, "Large Caption (24pt)", 24)
ui.append(caption1)
caption2 = create_caption(100, 200, "Medium Caption (18pt)", 18, GREEN)
ui.append(caption2)
caption3 = create_caption(100, 240, "Small Caption (14pt)", 14, RED)
ui.append(caption3)
# Caption with background
caption_bg = mcrfpy.Frame(100, 300, 300, 50, fill_color=BOX_COLOR)
ui.append(caption_bg)
caption4 = create_caption(110, 315, "Caption with Background", 16)
ui.append(caption4)
def create_sprite_example():
"""Create a scene showing Sprite UI element examples"""
mcrfpy.createScene("sprite_example")
ui = mcrfpy.sceneUI("sprite_example")
# Background frame
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
# Title
title = create_caption(250, 50, "Sprite Examples", 32)
ui.append(title)
# Create a grid background for sprites
sprite_bg = mcrfpy.Frame(100, 150, 600, 300, fill_color=BOX_COLOR)
ui.append(sprite_bg)
# Player sprite (84)
player_label = create_caption(150, 180, "Player", 14)
ui.append(player_label)
player_sprite = mcrfpy.Sprite(150, 200, sprite_texture, 84, 3.0)
ui.append(player_sprite)
# Enemy sprites
enemy_label = create_caption(250, 180, "Enemies", 14)
ui.append(enemy_label)
enemy1 = mcrfpy.Sprite(250, 200, sprite_texture, 123, 3.0) # Basic enemy
ui.append(enemy1)
enemy2 = mcrfpy.Sprite(300, 200, sprite_texture, 107, 3.0) # Different enemy
ui.append(enemy2)
# Boulder sprite (66)
boulder_label = create_caption(400, 180, "Boulder", 14)
ui.append(boulder_label)
boulder_sprite = mcrfpy.Sprite(400, 200, sprite_texture, 66, 3.0)
ui.append(boulder_sprite)
# Exit sprites
exit_label = create_caption(500, 180, "Exit States", 14)
ui.append(exit_label)
exit_locked = mcrfpy.Sprite(500, 200, sprite_texture, 45, 3.0) # Locked
ui.append(exit_locked)
exit_open = mcrfpy.Sprite(550, 200, sprite_texture, 21, 3.0) # Open
ui.append(exit_open)
# Item sprites
item_label = create_caption(150, 300, "Items", 14)
ui.append(item_label)
treasure = mcrfpy.Sprite(150, 320, sprite_texture, 89, 3.0) # Treasure
ui.append(treasure)
sword = mcrfpy.Sprite(200, 320, sprite_texture, 222, 3.0) # Sword
ui.append(sword)
potion = mcrfpy.Sprite(250, 320, sprite_texture, 175, 3.0) # Potion
ui.append(potion)
# Button sprite
button_label = create_caption(350, 300, "Button", 14)
ui.append(button_label)
button = mcrfpy.Sprite(350, 320, sprite_texture, 250, 3.0)
ui.append(button)
def create_frame_example():
"""Create a scene showing Frame UI element examples"""
mcrfpy.createScene("frame_example")
ui = mcrfpy.sceneUI("frame_example")
# Background
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=SHADOW_COLOR)
ui.append(bg)
# Title
title = create_caption(250, 30, "Frame Examples", 32)
ui.append(title)
# Basic frame
frame1 = mcrfpy.Frame(50, 100, 200, 150, fill_color=FRAME_COLOR)
ui.append(frame1)
label1 = create_caption(60, 110, "Basic Frame", 16)
ui.append(label1)
# Frame with outline
frame2 = mcrfpy.Frame(300, 100, 200, 150, fill_color=BOX_COLOR,
outline_color=WHITE, outline=2.0)
ui.append(frame2)
label2 = create_caption(310, 110, "Frame with Outline", 16)
ui.append(label2)
# Nested frames
frame3 = mcrfpy.Frame(550, 100, 200, 150, fill_color=FRAME_COLOR,
outline_color=WHITE, outline=1)
ui.append(frame3)
inner_frame = mcrfpy.Frame(570, 130, 160, 90, fill_color=BOX_COLOR)
ui.append(inner_frame)
label3 = create_caption(560, 110, "Nested Frames", 16)
ui.append(label3)
# Complex layout with frames
main_frame = mcrfpy.Frame(50, 300, 700, 250, fill_color=FRAME_COLOR,
outline_color=WHITE, outline=2)
ui.append(main_frame)
# Add some UI elements inside
ui_label = create_caption(60, 310, "Complex UI Layout", 18)
ui.append(ui_label)
# Status panel
status_frame = mcrfpy.Frame(70, 350, 150, 180, fill_color=BOX_COLOR)
ui.append(status_frame)
status_label = create_caption(80, 360, "Status", 14)
ui.append(status_label)
# Inventory panel
inv_frame = mcrfpy.Frame(240, 350, 300, 180, fill_color=BOX_COLOR)
ui.append(inv_frame)
inv_label = create_caption(250, 360, "Inventory", 14)
ui.append(inv_label)
# Actions panel
action_frame = mcrfpy.Frame(560, 350, 170, 180, fill_color=BOX_COLOR)
ui.append(action_frame)
action_label = create_caption(570, 360, "Actions", 14)
ui.append(action_label)
def create_grid_example():
"""Create a scene showing Grid UI element examples"""
mcrfpy.createScene("grid_example")
ui = mcrfpy.sceneUI("grid_example")
# Background
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
# Title
title = create_caption(250, 30, "Grid Example", 32)
ui.append(title)
# Create a grid showing a small dungeon
grid = mcrfpy.Grid(20, 15, sprite_texture,
mcrfpy.Vector(100, 100), mcrfpy.Vector(320, 240))
# Set up dungeon tiles
# Floor tiles (index 48)
# Wall tiles (index 3)
for x in range(20):
for y in range(15):
if x == 0 or x == 19 or y == 0 or y == 14:
# Walls around edge
grid.at((x, y)).tilesprite = 3
grid.at((x, y)).walkable = False
else:
# Floor
grid.at((x, y)).tilesprite = 48
grid.at((x, y)).walkable = True
# Add some internal walls
for x in range(5, 15):
grid.at((x, 7)).tilesprite = 3
grid.at((x, 7)).walkable = False
for y in range(3, 8):
grid.at((10, y)).tilesprite = 3
grid.at((10, y)).walkable = False
# Add a door
grid.at((10, 7)).tilesprite = 131 # Door tile
grid.at((10, 7)).walkable = True
# Add to UI
ui.append(grid)
# Label
grid_label = create_caption(100, 480, "20x15 Grid with 2x scale - Simple Dungeon Layout", 16)
ui.append(grid_label)
def create_entity_example():
"""Create a scene showing Entity examples in a Grid"""
mcrfpy.createScene("entity_example")
ui = mcrfpy.sceneUI("entity_example")
# Background
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
# Title
title = create_caption(200, 30, "Entity Collection Example", 32)
ui.append(title)
# Create a grid for the entities
grid = mcrfpy.Grid(15, 10, sprite_texture,
mcrfpy.Vector(150, 100), mcrfpy.Vector(360, 240))
# Set all tiles to floor
for x in range(15):
for y in range(10):
grid.at((x, y)).tilesprite = 48
grid.at((x, y)).walkable = True
# Add walls
for x in range(15):
grid.at((x, 0)).tilesprite = 3
grid.at((x, 0)).walkable = False
grid.at((x, 9)).tilesprite = 3
grid.at((x, 9)).walkable = False
for y in range(10):
grid.at((0, y)).tilesprite = 3
grid.at((0, y)).walkable = False
grid.at((14, y)).tilesprite = 3
grid.at((14, y)).walkable = False
ui.append(grid)
# Add entities to the grid
# Player entity
player = mcrfpy.Entity(mcrfpy.Vector(3, 3), sprite_texture, 84, grid)
grid.entities.append(player)
# Enemy entities
enemy1 = mcrfpy.Entity(mcrfpy.Vector(7, 4), sprite_texture, 123, grid)
grid.entities.append(enemy1)
enemy2 = mcrfpy.Entity(mcrfpy.Vector(10, 6), sprite_texture, 107, grid)
grid.entities.append(enemy2)
# Boulder
boulder = mcrfpy.Entity(mcrfpy.Vector(5, 5), sprite_texture, 66, grid)
grid.entities.append(boulder)
# Treasure
treasure = mcrfpy.Entity(mcrfpy.Vector(12, 2), sprite_texture, 89, grid)
grid.entities.append(treasure)
# Exit (locked)
exit_door = mcrfpy.Entity(mcrfpy.Vector(12, 8), sprite_texture, 45, grid)
grid.entities.append(exit_door)
# Button
button = mcrfpy.Entity(mcrfpy.Vector(3, 7), sprite_texture, 250, grid)
grid.entities.append(button)
# Items
sword = mcrfpy.Entity(mcrfpy.Vector(8, 2), sprite_texture, 222, grid)
grid.entities.append(sword)
potion = mcrfpy.Entity(mcrfpy.Vector(6, 8), sprite_texture, 175, grid)
grid.entities.append(potion)
# Label
entity_label = create_caption(150, 500, "Grid with Entity Collection - Game Objects", 16)
ui.append(entity_label)
def create_combined_example():
"""Create a scene showing all UI elements combined"""
mcrfpy.createScene("combined_example")
ui = mcrfpy.sceneUI("combined_example")
# Background
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=SHADOW_COLOR)
ui.append(bg)
# Title
title = create_caption(200, 20, "McRogueFace UI Elements", 28)
ui.append(title)
# Main game area frame
game_frame = mcrfpy.Frame(20, 70, 500, 400, fill_color=FRAME_COLOR,
outline_color=WHITE, outline=2)
ui.append(game_frame)
# Grid inside game frame
grid = mcrfpy.Grid(12, 10, sprite_texture,
mcrfpy.Vector(30, 80), mcrfpy.Vector(480, 400))
for x in range(12):
for y in range(10):
if x == 0 or x == 11 or y == 0 or y == 9:
grid.at((x, y)).tilesprite = 3
grid.at((x, y)).walkable = False
else:
grid.at((x, y)).tilesprite = 48
grid.at((x, y)).walkable = True
# Add some entities
player = mcrfpy.Entity(mcrfpy.Vector(2, 2), sprite_texture, 84, grid)
grid.entities.append(player)
enemy = mcrfpy.Entity(mcrfpy.Vector(8, 6), sprite_texture, 123, grid)
grid.entities.append(enemy)
boulder = mcrfpy.Entity(mcrfpy.Vector(5, 4), sprite_texture, 66, grid)
grid.entities.append(boulder)
ui.append(grid)
# Status panel
status_frame = mcrfpy.Frame(540, 70, 240, 200, fill_color=BOX_COLOR,
outline_color=WHITE, outline=1)
ui.append(status_frame)
status_title = create_caption(550, 80, "Status", 20)
ui.append(status_title)
hp_label = create_caption(550, 120, "HP: 10/10", 16, GREEN)
ui.append(hp_label)
level_label = create_caption(550, 150, "Level: 1", 16)
ui.append(level_label)
# Inventory panel
inv_frame = mcrfpy.Frame(540, 290, 240, 180, fill_color=BOX_COLOR,
outline_color=WHITE, outline=1)
ui.append(inv_frame)
inv_title = create_caption(550, 300, "Inventory", 20)
ui.append(inv_title)
# Add some item sprites
item1 = mcrfpy.Sprite(560, 340, sprite_texture, 222, 2.0)
ui.append(item1)
item2 = mcrfpy.Sprite(610, 340, sprite_texture, 175, 2.0)
ui.append(item2)
# Message log
log_frame = mcrfpy.Frame(20, 490, 760, 90, fill_color=BOX_COLOR,
outline_color=WHITE, outline=1)
ui.append(log_frame)
log_msg = create_caption(30, 500, "Welcome to McRogueFace!", 14)
ui.append(log_msg)
# Set up all the scenes
print("Creating UI example scenes...")
create_caption_example()
create_sprite_example()
create_frame_example()
create_grid_example()
create_entity_example()
create_combined_example()
# Screenshot state
current_screenshot = 0
screenshots = [
("caption_example", "ui_caption_example.png"),
("sprite_example", "ui_sprite_example.png"),
("frame_example", "ui_frame_example.png"),
("grid_example", "ui_grid_example.png"),
("entity_example", "ui_entity_example.png"),
("combined_example", "ui_combined_example.png")
]
def take_screenshots(runtime):
"""Timer callback to take screenshots sequentially"""
global current_screenshot
if current_screenshot >= len(screenshots):
print("\nAll screenshots captured successfully!")
print(f"Screenshots saved to: {output_dir}/")
mcrfpy.exit()
return
scene_name, filename = screenshots[current_screenshot]
# Switch to the scene
mcrfpy.setScene(scene_name)
# Take screenshot after a short delay to ensure rendering
def capture():
global current_screenshot
full_path = f"{output_dir}/{filename}"
result = automation.screenshot(full_path)
print(f"Screenshot {current_screenshot + 1}/{len(screenshots)}: {filename} - {'Success' if result else 'Failed'}")
current_screenshot += 1
# Schedule next screenshot
mcrfpy.setTimer("next_screenshot", take_screenshots, 200)
# Give scene time to render
mcrfpy.setTimer("capture", lambda r: capture(), 100)
# Start with the first scene
mcrfpy.setScene("caption_example")
# Start the screenshot process
print(f"\nStarting screenshot capture of {len(screenshots)} scenes...")
mcrfpy.setTimer("start", take_screenshots, 500)
# Safety timeout
def safety_exit(runtime):
print("\nERROR: Safety timeout reached! Exiting...")
mcrfpy.exit()
mcrfpy.setTimer("safety", safety_exit, 30000)
print("Setup complete. Game loop starting...")

View File

@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""Generate documentation screenshots for McRogueFace UI elements - Simple version"""
import mcrfpy
from mcrfpy import automation
import sys
import os
# Crypt of Sokoban color scheme
FRAME_COLOR = mcrfpy.Color(64, 64, 128)
SHADOW_COLOR = mcrfpy.Color(64, 64, 86)
BOX_COLOR = mcrfpy.Color(96, 96, 160)
WHITE = mcrfpy.Color(255, 255, 255)
BLACK = mcrfpy.Color(0, 0, 0)
GREEN = mcrfpy.Color(0, 255, 0)
RED = mcrfpy.Color(255, 0, 0)
# Create texture for sprites
sprite_texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Output directory
output_dir = "mcrogueface.github.io/images"
if not os.path.exists(output_dir):
os.makedirs(output_dir)
def create_caption(x, y, text, font_size=16, text_color=WHITE, outline_color=BLACK):
"""Helper function to create captions with common settings"""
caption = mcrfpy.Caption(mcrfpy.Vector(x, y), text=text)
caption.size = font_size
caption.fill_color = text_color
caption.outline_color = outline_color
return caption
# Screenshot counter
screenshot_count = 0
total_screenshots = 4
def screenshot_and_continue(runtime):
"""Take a screenshot and move to the next scene"""
global screenshot_count
if screenshot_count == 0:
# Caption example
print("Creating Caption example...")
mcrfpy.createScene("caption_example")
ui = mcrfpy.sceneUI("caption_example")
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
title = create_caption(200, 50, "Caption Examples", 32)
ui.append(title)
caption1 = create_caption(100, 150, "Large Caption (24pt)", 24)
ui.append(caption1)
caption2 = create_caption(100, 200, "Medium Caption (18pt)", 18, GREEN)
ui.append(caption2)
caption3 = create_caption(100, 240, "Small Caption (14pt)", 14, RED)
ui.append(caption3)
caption_bg = mcrfpy.Frame(100, 300, 300, 50, fill_color=BOX_COLOR)
ui.append(caption_bg)
caption4 = create_caption(110, 315, "Caption with Background", 16)
ui.append(caption4)
mcrfpy.setScene("caption_example")
mcrfpy.setTimer("next1", lambda r: capture_screenshot("ui_caption_example.png"), 200)
elif screenshot_count == 1:
# Sprite example
print("Creating Sprite example...")
mcrfpy.createScene("sprite_example")
ui = mcrfpy.sceneUI("sprite_example")
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
title = create_caption(250, 50, "Sprite Examples", 32)
ui.append(title)
sprite_bg = mcrfpy.Frame(100, 150, 600, 300, fill_color=BOX_COLOR)
ui.append(sprite_bg)
player_label = create_caption(150, 180, "Player", 14)
ui.append(player_label)
player_sprite = mcrfpy.Sprite(150, 200, sprite_texture, 84, 3.0)
ui.append(player_sprite)
enemy_label = create_caption(250, 180, "Enemies", 14)
ui.append(enemy_label)
enemy1 = mcrfpy.Sprite(250, 200, sprite_texture, 123, 3.0)
ui.append(enemy1)
enemy2 = mcrfpy.Sprite(300, 200, sprite_texture, 107, 3.0)
ui.append(enemy2)
boulder_label = create_caption(400, 180, "Boulder", 14)
ui.append(boulder_label)
boulder_sprite = mcrfpy.Sprite(400, 200, sprite_texture, 66, 3.0)
ui.append(boulder_sprite)
exit_label = create_caption(500, 180, "Exit States", 14)
ui.append(exit_label)
exit_locked = mcrfpy.Sprite(500, 200, sprite_texture, 45, 3.0)
ui.append(exit_locked)
exit_open = mcrfpy.Sprite(550, 200, sprite_texture, 21, 3.0)
ui.append(exit_open)
mcrfpy.setScene("sprite_example")
mcrfpy.setTimer("next2", lambda r: capture_screenshot("ui_sprite_example.png"), 200)
elif screenshot_count == 2:
# Frame example
print("Creating Frame example...")
mcrfpy.createScene("frame_example")
ui = mcrfpy.sceneUI("frame_example")
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=SHADOW_COLOR)
ui.append(bg)
title = create_caption(250, 30, "Frame Examples", 32)
ui.append(title)
frame1 = mcrfpy.Frame(50, 100, 200, 150, fill_color=FRAME_COLOR)
ui.append(frame1)
label1 = create_caption(60, 110, "Basic Frame", 16)
ui.append(label1)
frame2 = mcrfpy.Frame(300, 100, 200, 150, fill_color=BOX_COLOR,
outline_color=WHITE, outline=2.0)
ui.append(frame2)
label2 = create_caption(310, 110, "Frame with Outline", 16)
ui.append(label2)
frame3 = mcrfpy.Frame(550, 100, 200, 150, fill_color=FRAME_COLOR,
outline_color=WHITE, outline=1)
ui.append(frame3)
inner_frame = mcrfpy.Frame(570, 130, 160, 90, fill_color=BOX_COLOR)
ui.append(inner_frame)
label3 = create_caption(560, 110, "Nested Frames", 16)
ui.append(label3)
mcrfpy.setScene("frame_example")
mcrfpy.setTimer("next3", lambda r: capture_screenshot("ui_frame_example.png"), 200)
elif screenshot_count == 3:
# Grid example
print("Creating Grid example...")
mcrfpy.createScene("grid_example")
ui = mcrfpy.sceneUI("grid_example")
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
ui.append(bg)
title = create_caption(250, 30, "Grid Example", 32)
ui.append(title)
grid = mcrfpy.Grid(20, 15, sprite_texture,
mcrfpy.Vector(100, 100), mcrfpy.Vector(320, 240))
# Set up dungeon tiles
for x in range(20):
for y in range(15):
if x == 0 or x == 19 or y == 0 or y == 14:
# Walls
grid.at((x, y)).tilesprite = 3
grid.at((x, y)).walkable = False
else:
# Floor
grid.at((x, y)).tilesprite = 48
grid.at((x, y)).walkable = True
# Add some internal walls
for x in range(5, 15):
grid.at((x, 7)).tilesprite = 3
grid.at((x, 7)).walkable = False
for y in range(3, 8):
grid.at((10, y)).tilesprite = 3
grid.at((10, y)).walkable = False
# Add a door
grid.at((10, 7)).tilesprite = 131
grid.at((10, 7)).walkable = True
ui.append(grid)
grid_label = create_caption(100, 480, "20x15 Grid - Simple Dungeon Layout", 16)
ui.append(grid_label)
mcrfpy.setScene("grid_example")
mcrfpy.setTimer("next4", lambda r: capture_screenshot("ui_grid_example.png"), 200)
else:
print("\nAll screenshots captured successfully!")
print(f"Screenshots saved to: {output_dir}/")
mcrfpy.exit()
return
def capture_screenshot(filename):
"""Capture a screenshot"""
global screenshot_count
full_path = f"{output_dir}/{filename}"
result = automation.screenshot(full_path)
print(f"Screenshot {screenshot_count + 1}/{total_screenshots}: {filename} - {'Success' if result else 'Failed'}")
screenshot_count += 1
# Schedule next scene
mcrfpy.setTimer("continue", screenshot_and_continue, 300)
# Start the process
print("Starting screenshot generation...")
mcrfpy.setTimer("start", screenshot_and_continue, 500)
# Safety timeout
mcrfpy.setTimer("safety", lambda r: mcrfpy.exit(), 30000)
print("Setup complete. Game loop starting...")

View File

@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Generate entity documentation screenshot with proper font loading"""
import mcrfpy
from mcrfpy import automation
import sys
def capture_entity(runtime):
"""Capture entity example after render loop starts"""
# Take screenshot
automation.screenshot("mcrogueface.github.io/images/ui_entity_example.png")
print("Entity screenshot saved!")
# Exit after capturing
sys.exit(0)
# Create scene
mcrfpy.createScene("entities")
# Use the default font which is already loaded
# Instead of: font = mcrfpy.Font("assets/JetbrainsMono.ttf")
# We use: mcrfpy.default_font (which is already loaded by the engine)
# Title
title = mcrfpy.Caption((400, 30), "Entity Example - Roguelike Characters", font=mcrfpy.default_font)
#title.font = mcrfpy.default_font
#title.font_size = 24
title.size=24
#title.font_color = (255, 255, 255)
#title.text_color = (255,255,255)
# Create a grid background
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Create grid with entities - using 2x scale (32x32 pixel tiles)
#grid = mcrfpy.Grid((100, 100), (20, 15), texture, 16, 16) # I can never get the args right for this thing
t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
grid = mcrfpy.Grid(20, 15, t, (10, 10), (1014, 758))
grid.zoom = 2.0
#grid.texture = texture
# Define tile types
FLOOR = 58 # Stone floor
WALL = 11 # Stone wall
# Fill with floor
for x in range(20):
for y in range(15):
grid.at((x, y)).tilesprite = WALL
# Add walls around edges
for x in range(20):
grid.at((x, 0)).tilesprite = WALL
grid.at((x, 14)).tilesprite = WALL
for y in range(15):
grid.at((0, y)).tilesprite = WALL
grid.at((19, y)).tilesprite = WALL
# Create entities
# Player at center
player = mcrfpy.Entity((10, 7), t, 84)
#player.texture = texture
#player.sprite_index = 84 # Player sprite
# Enemies
rat1 = mcrfpy.Entity((5, 5), t, 123)
#rat1.texture = texture
#rat1.sprite_index = 123 # Rat
rat2 = mcrfpy.Entity((15, 5), t, 123)
#rat2.texture = texture
#rat2.sprite_index = 123 # Rat
big_rat = mcrfpy.Entity((7, 10), t, 130)
#big_rat.texture = texture
#big_rat.sprite_index = 130 # Big rat
cyclops = mcrfpy.Entity((13, 10), t, 109)
#cyclops.texture = texture
#cyclops.sprite_index = 109 # Cyclops
# Items
chest = mcrfpy.Entity((3, 3), t, 89)
#chest.texture = texture
#chest.sprite_index = 89 # Chest
boulder = mcrfpy.Entity((10, 5), t, 66)
#boulder.texture = texture
#boulder.sprite_index = 66 # Boulder
key = mcrfpy.Entity((17, 12), t, 384)
#key.texture = texture
#key.sprite_index = 384 # Key
# Add all entities to grid
grid.entities.append(player)
grid.entities.append(rat1)
grid.entities.append(rat2)
grid.entities.append(big_rat)
grid.entities.append(cyclops)
grid.entities.append(chest)
grid.entities.append(boulder)
grid.entities.append(key)
# Labels
entity_label = mcrfpy.Caption((100, 580), "Entities move independently on the grid. Grid scale: 2x (32x32 pixels)")
#entity_label.font = mcrfpy.default_font
#entity_label.font_color = (255, 255, 255)
info = mcrfpy.Caption((100, 600), "Player (center), Enemies (rats, cyclops), Items (chest, boulder, key)")
#info.font = mcrfpy.default_font
#info.font_size = 14
#info.font_color = (200, 200, 200)
# Legend frame
legend_frame = mcrfpy.Frame(50, 50, 200, 150)
#legend_frame.bgcolor = (64, 64, 128)
#legend_frame.outline = 2
legend_title = mcrfpy.Caption((150, 60), "Entity Types")
#legend_title.font = mcrfpy.default_font
#legend_title.font_color = (255, 255, 255)
#legend_title.centered = True
#legend_text = mcrfpy.Caption((60, 90), "Player: @\nRat: r\nBig Rat: R\nCyclops: C\nChest: $\nBoulder: O\nKey: k")
#legend_text.font = mcrfpy.default_font
#legend_text.font_size = 12
#legend_text.font_color = (255, 255, 255)
# Add all to scene
ui = mcrfpy.sceneUI("entities")
ui.append(grid)
ui.append(title)
ui.append(entity_label)
ui.append(info)
ui.append(legend_frame)
ui.append(legend_title)
#ui.append(legend_text)
# Switch to scene
mcrfpy.setScene("entities")
# Set timer to capture after rendering starts
mcrfpy.setTimer("capture", capture_entity, 100)

View File

@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""Generate grid documentation screenshot for McRogueFace"""
import mcrfpy
from mcrfpy import automation
import sys
def capture_grid(runtime):
"""Capture grid example after render loop starts"""
# Take screenshot
automation.screenshot("mcrogueface.github.io/images/ui_grid_example.png")
print("Grid screenshot saved!")
# Exit after capturing
sys.exit(0)
# Create scene
mcrfpy.createScene("grid")
# Load texture
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Title
title = mcrfpy.Caption(400, 30, "Grid Example - Dungeon View")
title.font = mcrfpy.default_font
title.font_size = 24
title.font_color = (255, 255, 255)
# Create main grid (20x15 tiles, each 32x32 pixels)
grid = mcrfpy.Grid(100, 100, 20, 15, texture, 32, 32)
grid.texture = texture
# Define tile types from Crypt of Sokoban
FLOOR = 58 # Stone floor
WALL = 11 # Stone wall
DOOR = 28 # Closed door
CHEST = 89 # Treasure chest
BUTTON = 250 # Floor button
EXIT = 45 # Locked exit
BOULDER = 66 # Boulder
# Create a simple dungeon room layout
# Fill with walls first
for x in range(20):
for y in range(15):
grid.set_tile(x, y, WALL)
# Carve out room
for x in range(2, 18):
for y in range(2, 13):
grid.set_tile(x, y, FLOOR)
# Add door
grid.set_tile(10, 2, DOOR)
# Add some features
grid.set_tile(5, 5, CHEST)
grid.set_tile(15, 10, BUTTON)
grid.set_tile(10, 12, EXIT)
grid.set_tile(8, 8, BOULDER)
grid.set_tile(12, 8, BOULDER)
# Create some entities on the grid
# Player entity
player = mcrfpy.Entity(5, 7)
player.texture = texture
player.sprite_index = 84 # Player sprite
# Enemy entities
rat1 = mcrfpy.Entity(12, 5)
rat1.texture = texture
rat1.sprite_index = 123 # Rat
rat2 = mcrfpy.Entity(14, 9)
rat2.texture = texture
rat2.sprite_index = 123 # Rat
cyclops = mcrfpy.Entity(10, 10)
cyclops.texture = texture
cyclops.sprite_index = 109 # Cyclops
# Add entities to grid
grid.entities.append(player)
grid.entities.append(rat1)
grid.entities.append(rat2)
grid.entities.append(cyclops)
# Create a smaller grid showing tile palette
palette_label = mcrfpy.Caption(100, 600, "Tile Types:")
palette_label.font = mcrfpy.default_font
palette_label.font_color = (255, 255, 255)
palette = mcrfpy.Grid(250, 580, 7, 1, texture, 32, 32)
palette.texture = texture
palette.set_tile(0, 0, FLOOR)
palette.set_tile(1, 0, WALL)
palette.set_tile(2, 0, DOOR)
palette.set_tile(3, 0, CHEST)
palette.set_tile(4, 0, BUTTON)
palette.set_tile(5, 0, EXIT)
palette.set_tile(6, 0, BOULDER)
# Labels for palette
labels = ["Floor", "Wall", "Door", "Chest", "Button", "Exit", "Boulder"]
for i, label in enumerate(labels):
l = mcrfpy.Caption(250 + i * 32, 615, label)
l.font = mcrfpy.default_font
l.font_size = 10
l.font_color = (255, 255, 255)
mcrfpy.sceneUI("grid").append(l)
# Add info caption
info = mcrfpy.Caption(100, 680, "Grid supports tiles and entities. Entities can move independently of the tile grid.")
info.font = mcrfpy.default_font
info.font_size = 14
info.font_color = (200, 200, 200)
# Add all elements to scene
ui = mcrfpy.sceneUI("grid")
ui.append(title)
ui.append(grid)
ui.append(palette_label)
ui.append(palette)
ui.append(info)
# Switch to scene
mcrfpy.setScene("grid")
# Set timer to capture after rendering starts
mcrfpy.setTimer("capture", capture_grid, 100)

View File

@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""Generate sprite documentation screenshots for McRogueFace"""
import mcrfpy
from mcrfpy import automation
import sys
def capture_sprites(runtime):
"""Capture sprite examples after render loop starts"""
# Take screenshot
automation.screenshot("mcrogueface.github.io/images/ui_sprite_example.png")
print("Sprite screenshot saved!")
# Exit after capturing
sys.exit(0)
# Create scene
mcrfpy.createScene("sprites")
# Load texture
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
# Title
title = mcrfpy.Caption(400, 30, "Sprite Examples")
title.font = mcrfpy.default_font
title.font_size = 24
title.font_color = (255, 255, 255)
# Create a frame background
frame = mcrfpy.Frame(50, 80, 700, 500)
frame.bgcolor = (64, 64, 128)
frame.outline = 2
# Player sprite
player_label = mcrfpy.Caption(100, 120, "Player")
player_label.font = mcrfpy.default_font
player_label.font_color = (255, 255, 255)
player = mcrfpy.Sprite(120, 150)
player.texture = texture
player.sprite_index = 84 # Player sprite
player.scale = (3.0, 3.0)
# Enemy sprites
enemy_label = mcrfpy.Caption(250, 120, "Enemies")
enemy_label.font = mcrfpy.default_font
enemy_label.font_color = (255, 255, 255)
rat = mcrfpy.Sprite(250, 150)
rat.texture = texture
rat.sprite_index = 123 # Rat
rat.scale = (3.0, 3.0)
big_rat = mcrfpy.Sprite(320, 150)
big_rat.texture = texture
big_rat.sprite_index = 130 # Big rat
big_rat.scale = (3.0, 3.0)
cyclops = mcrfpy.Sprite(390, 150)
cyclops.texture = texture
cyclops.sprite_index = 109 # Cyclops
cyclops.scale = (3.0, 3.0)
# Items row
items_label = mcrfpy.Caption(100, 250, "Items")
items_label.font = mcrfpy.default_font
items_label.font_color = (255, 255, 255)
# Boulder
boulder = mcrfpy.Sprite(100, 280)
boulder.texture = texture
boulder.sprite_index = 66 # Boulder
boulder.scale = (3.0, 3.0)
# Chest
chest = mcrfpy.Sprite(170, 280)
chest.texture = texture
chest.sprite_index = 89 # Closed chest
chest.scale = (3.0, 3.0)
# Key
key = mcrfpy.Sprite(240, 280)
key.texture = texture
key.sprite_index = 384 # Key
key.scale = (3.0, 3.0)
# Button
button = mcrfpy.Sprite(310, 280)
button.texture = texture
button.sprite_index = 250 # Button
button.scale = (3.0, 3.0)
# UI elements row
ui_label = mcrfpy.Caption(100, 380, "UI Elements")
ui_label.font = mcrfpy.default_font
ui_label.font_color = (255, 255, 255)
# Hearts
heart_full = mcrfpy.Sprite(100, 410)
heart_full.texture = texture
heart_full.sprite_index = 210 # Full heart
heart_full.scale = (3.0, 3.0)
heart_half = mcrfpy.Sprite(170, 410)
heart_half.texture = texture
heart_half.sprite_index = 209 # Half heart
heart_half.scale = (3.0, 3.0)
heart_empty = mcrfpy.Sprite(240, 410)
heart_empty.texture = texture
heart_empty.sprite_index = 208 # Empty heart
heart_empty.scale = (3.0, 3.0)
# Armor
armor = mcrfpy.Sprite(340, 410)
armor.texture = texture
armor.sprite_index = 211 # Armor
armor.scale = (3.0, 3.0)
# Scale demonstration
scale_label = mcrfpy.Caption(500, 120, "Scale Demo")
scale_label.font = mcrfpy.default_font
scale_label.font_color = (255, 255, 255)
# Same sprite at different scales
for i, scale in enumerate([1.0, 2.0, 3.0, 4.0]):
s = mcrfpy.Sprite(500 + i * 60, 150)
s.texture = texture
s.sprite_index = 84 # Player
s.scale = (scale, scale)
mcrfpy.sceneUI("sprites").append(s)
# Add all elements to scene
ui = mcrfpy.sceneUI("sprites")
ui.append(frame)
ui.append(title)
ui.append(player_label)
ui.append(player)
ui.append(enemy_label)
ui.append(rat)
ui.append(big_rat)
ui.append(cyclops)
ui.append(items_label)
ui.append(boulder)
ui.append(chest)
ui.append(key)
ui.append(button)
ui.append(ui_label)
ui.append(heart_full)
ui.append(heart_half)
ui.append(heart_empty)
ui.append(armor)
ui.append(scale_label)
# Switch to scene
mcrfpy.setScene("sprites")
# Set timer to capture after rendering starts
mcrfpy.setTimer("capture", capture_sprites, 100)

View File

@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Simple screenshot test to verify automation API"""
import mcrfpy
from mcrfpy import automation
import sys
import time
def take_screenshot(runtime):
"""Take screenshot after render starts"""
print(f"Timer callback fired at runtime: {runtime}")
# Try different paths
paths = [
"test_screenshot.png",
"./test_screenshot.png",
"mcrogueface.github.io/images/test_screenshot.png"
]
for path in paths:
try:
print(f"Trying to save to: {path}")
automation.screenshot(path)
print(f"Success: {path}")
except Exception as e:
print(f"Failed {path}: {e}")
sys.exit(0)
# Create minimal scene
mcrfpy.createScene("test")
# Add a visible element
caption = mcrfpy.Caption(100, 100, "Screenshot Test")
caption.font = mcrfpy.default_font
caption.font_color = (255, 255, 255)
caption.font_size = 24
mcrfpy.sceneUI("test").append(caption)
mcrfpy.setScene("test")
# Use timer to ensure rendering has started
print("Setting timer...")
mcrfpy.setTimer("screenshot", take_screenshot, 500) # Wait 0.5 seconds
print("Timer set, entering game loop...")

View File

@ -33,34 +33,34 @@ def test_Entity():
# Test entity properties
try:
print(f" Entity1 pos: {entity1.pos}")
print(f" Entity1 draw_pos: {entity1.draw_pos}")
print(f" Entity1 sprite_number: {entity1.sprite_number}")
print(f" Entity1 pos: {entity1.pos}")
print(f" Entity1 draw_pos: {entity1.draw_pos}")
print(f" Entity1 sprite_number: {entity1.sprite_number}")
# Modify properties
entity1.pos = mcrfpy.Vector(3, 3)
entity1.sprite_number = 5
print(" Entity properties modified")
print(" Entity properties modified")
except Exception as e:
print(f" Entity property access failed: {e}")
print(f"X Entity property access failed: {e}")
# Test gridstate access
try:
gridstate = entity2.gridstate
print(f" Entity gridstate accessible")
print(" Entity gridstate accessible")
# Test at() method
point_state = entity2.at(0, 0)
print(f" Entity at() method works")
point_state = entity2.at()#.at(0, 0)
print(" Entity at() method works")
except Exception as e:
print(f" Entity gridstate/at() failed: {e}")
print(f"X Entity gridstate/at() failed: {e}")
# Test index() method (Issue #73)
print("\nTesting index() method (Issue #73)...")
try:
# Try to find entity2's index
index = entity2.index()
print(f" index() method works: entity2 is at index {index}")
print(f":) index() method works: entity2 is at index {index}")
# Verify by checking collection
if entities[index] == entity2:
@ -70,7 +70,7 @@ def test_Entity():
# Remove using index
entities.remove(index)
print(f" Removed entity using index, now {len(entities)} entities")
print(f":) Removed entity using index, now {len(entities)} entities")
except AttributeError:
print("✗ index() method not implemented (Issue #73)")
# Try manual removal as workaround
@ -78,21 +78,21 @@ def test_Entity():
for i in range(len(entities)):
if entities[i] == entity2:
entities.remove(i)
print(f" Manual removal workaround succeeded")
print(":) Manual removal workaround succeeded")
break
except:
print("✗ Manual removal also failed")
except Exception as e:
print(f" index() method error: {e}")
print(f":) index() method error: {e}")
# Test EntityCollection iteration
try:
positions = []
for entity in entities:
positions.append(entity.pos)
print(f" Entity iteration works: {len(positions)} entities")
print(f":) Entity iteration works: {len(positions)} entities")
except Exception as e:
print(f" Entity iteration failed: {e}")
print(f"X Entity iteration failed: {e}")
# Test EntityCollection extend (Issue #27)
try:
@ -101,11 +101,11 @@ def test_Entity():
mcrfpy.Entity(mcrfpy.Vector(9, 9), mcrfpy.default_texture, 4, grid)
]
entities.extend(new_entities)
print(f" extend() method works: now {len(entities)} entities")
print(f":) extend() method works: now {len(entities)} entities")
except AttributeError:
print("✗ extend() method not implemented (Issue #27)")
except Exception as e:
print(f" extend() method error: {e}")
print(f"X extend() method error: {e}")
# Skip screenshot in headless mode
print("PASS")
@ -113,4 +113,4 @@ def test_Entity():
# Run test immediately in headless mode
print("Running test immediately...")
test_Entity()
print("Test completed.")
print("Test completed.")