Compare commits

...

11 Commits

Author SHA1 Message Date
John McCardle c5e7e8e298 Update test demos for new Python API and entity system
- Update all text input demos to use new Entity constructor signature
- Fix pathfinding showcase to work with new entity position handling
- Remove entity_waypoints tracking in favor of simplified movement
- Delete obsolete exhaustive_api_demo.py (superseded by newer demos)
- Adjust entity creation calls to match Entity((x, y), texture, sprite_index) pattern

All demos now properly demonstrate the updated API while maintaining their
original functionality for showcasing engine features.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 01:37:57 -04:00
John McCardle 6d29652ae7 Update animation demo suite with crash fixes and improvements
- Add warnings about AnimationManager segfault bug in sizzle_reel_final.py
- Create sizzle_reel_final_fixed.py that works around the crash by hiding objects instead of removing them
- Increase font sizes for better visibility in demos
- Extend demo durations for better showcase of animations
- Remove debug prints from animation_sizzle_reel_working.py
- Minor cleanup and improvements to all animation demos

These demos showcase the full animation system capabilities while documenting and working around known issues with object removal during active animations.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 01:36:46 -04:00
John McCardle a010e5fa96 Update game scripts for new Python API
- Convert entity position access from tuple to x/y properties
- Update caption size property to font_size
- Fix grid boundary checks to use grid_size instead of exceptions
- Clean up demo timer on menu exit to prevent callbacks

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 01:35:35 -04:00
John McCardle 9c8d6c4591 Fix click event z-order handling in PyScene
Changed click detection to properly respect z-index by:
- Sorting ui_elements in-place when needed (same as render order)
- Using reverse iterators to check highest z-index elements first
- This ensures top-most elements receive clicks before lower ones

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 01:34:29 -04:00
John McCardle dcd1b0ca33 Add roguelike tutorial implementation files
Implement Parts 0-2 of the classic roguelike tutorial adapted for McRogueFace:
- Part 0: Basic grid setup and tile rendering
- Part 1: Drawing '@' symbol and basic movement
- Part 1b: Variant with sprite-based player
- Part 2: Entity system and NPC implementation with three movement variants:
  - part_2.py: Standard implementation
  - part_2-naive.py: Naive movement approach
  - part_2-onemovequeued.py: Queued movement system

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

These examples demonstrate McRogueFace's capabilities for traditional roguelike development.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 01:33:40 -04:00
John McCardle 6813fb5129 Standardize Python API constructors and remove PyArgHelpers
- Remove PyArgHelpers.h and all macro-based argument parsing
- Convert all UI class constructors to use PyArg_ParseTupleAndKeywords
- Standardize constructor signatures across UICaption, UIEntity, UIFrame, UIGrid, and UISprite
- Replace PYARGHELPER_SINGLE/MULTI macros with explicit argument parsing
- Improve error messages and argument validation
- Maintain backward compatibility with existing Python code

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 01:32:22 -04:00
John McCardle 6f67fbb51e Fix animation callback crashes from iterator invalidation (#119)
Resolved segfaults caused by creating new animations from within
animation callbacks. The issue was iterator invalidation in
AnimationManager::update() when callbacks modified the active
animations vector.

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 00:35:00 -04:00
John McCardle eb88c7b3aa Add animation completion callbacks (#119)
Implement callbacks that fire when animations complete, enabling direct
causality between animation end and game state changes. This eliminates
race conditions from parallel timer workarounds.

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 22:55:39 -04:00
John McCardle 9fb428dd01 Update ROADMAP with GitHub issue numbers (#111-#125)
Added issue numbers from GitHub tracker to roadmap items:
- #111: Grid Click Events Broken in Headless
- #112: Object Splitting Bug (Python type preservation)
- #113: Batch Operations for Grid
- #114: CellView API
- #115: SpatialHash Implementation
- #116: Dirty Flag System
- #117: Memory Pool for Entities
- #118: Scene as Drawable
- #119: Animation Completion Callbacks
- #120: Animation Property Locking
- #121: Timer Object System
- #122: Parent-Child UI System
- #123: Grid Subgrid System
- #124: Grid Point Animation
- #125: GitHub Issues Automation

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 15:16:14 -04:00
John McCardle bde82028b5 Roadmap: Integrate July 12 transcript analysis - critical tutorial blockers
URGENT: RoguelikeDev event starts July 15 (3 days)

Critical Blockers Identified:
- Animation system blocking tutorial Part 2 (input queueing, collision)
- Grid clicking completely broken in headless mode
- Python API consistency issues found during tutorial writing
- Object splitting bug: derived classes lose type in collections

Added Sections:
- Detailed tutorial status with specific blockers
- Animation system critical issues breakdown
- Grid clicking discovery (all events commented out)
- Python API consistency crisis details
- Proposed architecture improvements (OOP overhaul)
- Claude Code quality concerns after 6-7 weeks
- Comprehensive 34-issue list from transcript analysis

Immediate Actions Required:
1. Fix animation input queueing TODAY
2. Fix grid clicking implementation TODAY
3. Create tutorial announcement if blockers fixed
4. Regenerate Parts 3-6 (machine drafts broken)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 14:42:43 -04:00
John McCardle 062e4dadc4 Fix animation segfaults with RAII weak_ptr implementation
Resolved two critical segmentation faults in AnimationManager:
1. Race condition when creating multiple animations in timer callbacks
2. Exit crash when animations outlive their target objects

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 10:21:48 -04:00
41 changed files with 3026 additions and 2326 deletions

935
ROADMAP.md Normal file
View File

@ -0,0 +1,935 @@
# McRogueFace - Development Roadmap
## 🚨 URGENT PRIORITIES - July 12, 2025 🚨
### CRITICAL: RoguelikeDev Tutorial Event starts July 15! (3 days)
#### 1. Tutorial Status & Blockers
- [x] **Part 0**: Complete (Starting McRogueFace)
- [x] **Part 1**: Complete (Setting up grid and tile sheet)
- [ ] **Part 2**: Draft exists but BLOCKED by animation issues - PRIORITY FIX!
- [ ] **Parts 3-6**: Machine-generated drafts need complete rework
- [ ] **Parts 7-15**: Need creation this weekend
**Key Blockers**:
- Need smooth character movement animation (Pokemon-style)
- Grid needs walkable grass center, non-walkable tree edges
- Input queueing during animations not working properly
#### 2. Animation System Critical Issues 🚨
**BLOCKER FOR TUTORIAL PART 2**:
- [ ] **Input Queue System**: Holding arrow keys doesn't queue movements
- Animation must complete before next input accepted
- Need "press and hold" that queues ONE additional move
- Goal: Pokemon-style smooth continuous movement
- [ ] **Collision Reservation**: When entity starts moving, should block destination
- Prevents overlapping movements
- Already claimed tiles should reject incoming entities
- [x] **Segfault Fix**: Refactored from bare pointers to weak references ✅
#### 3. Grid Clicking BROKEN in Headless Mode 🚨
**MAJOR DISCOVERY**: All click events commented out!
- [ ] **#111** - Grid Click Events Broken in Headless: All click events commented out
- [ ] **Grid Click Coordinates**: Need tile coords, not just mouse coords
- [ ] **Nested Grid Support**: Clicks must work on grids within frames
- [ ] **No Error Reporting**: System claimed complete but isn't
#### 4. Python API Consistency Crisis
**Tutorial Writing Reveals Major Issues**:
- [ ] **#101/#110** - Inconsistent Constructors: Each class has different requirements
- [ ] **#109** - Vector Class Broken: No [0], [1] indexing like tuples
- [ ] **#112** - Object Splitting Bug: Python derived classes lose type in collections
- Shared pointer extracted, Python reference discarded
- Retrieved objects are base class only
- No way to cast back to derived type
- [ ] **Need Systematic Generation**: All bindings should be consistent
- [x] **UIGrid TCOD Integration** (8 hours) ✅ COMPLETED!
- ✅ Add TCODMap* to UIGrid constructor with proper lifecycle
- ✅ Implement complete Dijkstra pathfinding system
- ✅ Create mcrfpy.libtcod submodule with Python bindings
- ✅ Fix critical PyArg bug preventing Color object assignments
- ✅ Implement FOV with perspective rendering
- [ ] **#113** - Add batch operations for NumPy-style access (deferred)
- [ ] **#114** - Create CellView for ergonomic .at((x,y)) access (deferred)
- [x] **UIEntity Pathfinding** (4 hours) ✅ COMPLETED!
- ✅ Implement Dijkstra maps for multiple targets in UIGrid
- ✅ Add path_to(target) method using A* to UIEntity
- ✅ Cache paths in UIEntity for performance
#### 3. Performance Critical Path
- [ ] **#115** - Implement SpatialHash for 10,000+ entities (2 hours)
- [ ] **#116** - Add dirty flag system to UIGrid (1 hour)
- [ ] **#113** - Batch update context managers (2 hours)
- [ ] **#117** - Memory pool for entities (2 hours)
#### 4. Bug Fixing Pipeline
- [ ] **#125** - Set up GitHub Issues automation
- [ ] Create test for each bug before fixing
- [ ] Track: Memory leaks, Segfaults, Python/C++ boundary errors
---
## 🏗️ PROPOSED ARCHITECTURE IMPROVEMENTS (From July 12 Analysis)
### Object-Oriented Design Overhaul
1. **Scene System Revolution**:
- [ ] **#118** - Make Scene derive from Drawable (scenes are drawn!)
- [ ] Give scenes position and visibility properties
- [ ] Scene selection by visibility (auto-hide old scene)
- [ ] Replace transition system with animations
2. **Animation System Enhancements**:
- [ ] **#119** - Add proper completion callbacks (object + animation params)
- [ ] **#120** - Prevent property conflicts (exclusive locking)
- [ ] Currently using timer sync workarounds
3. **Timer System Improvements**:
- [ ] **#121** - Replace string-dictionary system with objects
- [ ] Add start(), stop(), pause() methods
- [ ] Implement proper one-shot mode
- [ ] Pass timer object to callbacks (not just ms)
4. **Parent-Child UI Relationships**:
- [ ] **#122** - Add parent field to UI drawables (like entities have)
- [ ] Implement append/remove/extend with auto-parent updates
- [ ] Auto-remove from old parent when adding to new
### Performance Optimizations Needed
- [ ] **Grid Rendering**: Consider texture caching vs real-time
- [ ] **#123** - Subgrid System: Split large grids into 256x256 chunks
- [ ] **#116** - Dirty Flagging: Propagate from base class up
- [ ] **#124** - Animation Features: Tile color animation, sprite cycling
---
## ⚠️ CLAUDE CODE QUALITY CONCERNS (6-7 Weeks In)
### Issues Observed:
1. **Declining Quality**: High quantity but low quality results
2. **Not Following Requirements**: Ignoring specific implementation needs
3. **Bad Practices**:
- Creating parallel copies (animation_RAII.cpp, _fixed, _final versions)
- Should use Git, not file copies
- Claims functionality "done" when stubbed out
4. **File Management Problems**:
- Git operations reset timestamps
- Can't determine creation order of multiple versions
### Recommendations:
- Use Git for version control exclusively
- Fix things in place, not copies
- Acknowledge incomplete functionality
- Follow project's implementation style
---
## 🎯 STRATEGIC ARCHITECTURE VISION
### Three-Layer Grid Architecture (From Compass Research)
Following successful roguelike patterns (Caves of Qud, Cogmind, DCSS):
1. **Visual Layer** (UIGridPoint) - Sprites, colors, animations
2. **World State Layer** (TCODMap) - Walkability, transparency, physics
3. **Entity Perspective Layer** (UIGridPointState) - Per-entity FOV, knowledge
### Performance Architecture (Critical for 1000x1000 maps)
- **Spatial Hashing** for entity queries (not quadtrees!)
- **Batch Operations** with context managers (10-100x speedup)
- **Memory Pooling** for entities and components
- **Dirty Flag System** to avoid unnecessary updates
- **Zero-Copy NumPy Integration** via buffer protocol
### Key Insight from Research
"Minimizing Python/C++ boundary crossings matters more than individual function complexity"
- Batch everything possible
- Use context managers for logical operations
- Expose arrays, not individual cells
- Profile and optimize hot paths only
---
## Project Status: 🎉 ALPHA 0.1 RELEASE! 🎉
**Current State**: Documentation system complete, TCOD integration urgent
**Latest Update**: Tutorial Parts 0-6 complete with documentation (2025-07-11)
**Branch**: alpha_streamline_2
**Open Issues**: ~46 remaining + URGENT TCOD/Tutorial work
---
## 📋 TCOD Integration Implementation Details
### Phase 1: Core UIGrid Integration (Day 1 Morning)
```cpp
// UIGrid.h additions
class UIGrid : public UIDrawable {
private:
TCODMap* world_state; // Add TCOD map
std::unordered_map<int, UIGridPointState*> entity_perspectives;
bool batch_mode = false;
std::vector<CellUpdate> pending_updates;
```
### Phase 2: Python Bindings (Day 1 Afternoon)
```python
# New API surface
grid = mcrfpy.Grid(100, 100)
grid.compute_fov(player.x, player.y, radius=10) # Returns visible cells
grid.at((x, y)).walkable = False # Ergonomic access
with grid.batch_update(): # Context manager for performance
# All updates batched
```
### Phase 3: Entity Integration (Day 2 Morning)
```python
# UIEntity additions
entity.path_to(target_x, target_y) # A* pathfinding
entity.flee_from(threat) # Dijkstra map
entity.can_see(other_entity) # FOV check
```
### Critical Success Factors:
1. **Batch everything** - Never update single cells in loops
2. **Lazy evaluation** - Only compute FOV for entities that need it
3. **Sparse storage** - Don't store full grids per entity
4. **Profile early** - Find the 20% of code taking 80% of time
---
## Recent Achievements
### 2025-07-12: Animation System RAII Overhaul - Critical Segfault Fix! 🛡️
**Fixed two major crashes in AnimationManager**
- ✅ Race condition when creating animations in timer callbacks
- ✅ Exit crash when animations outlive their targets
- ✅ Implemented weak_ptr tracking for automatic cleanup
- ✅ Added complete() and hasValidTarget() methods
- ✅ No more use-after-free bugs - proper RAII design
- ✅ Extensively tested with stress tests and production demos
### 2025-07-10: Complete FOV, A* Pathfinding & GUI Text Widgets! 👁️🗺️⌨️
**Engine Feature Sprint - Major Capabilities Added**
- ✅ Complete FOV (Field of View) system with perspective rendering
- UIGrid.perspective property controls which entity's view to render
- Three-layer overlay system: unexplored (black), explored (dark), visible (normal)
- Per-entity visibility state tracking with UIGridPointState
- Perfect knowledge updates - only explored areas persist
- ✅ A* Pathfinding implementation
- Entity.path_to(x, y) method for direct pathfinding
- UIGrid compute_astar() and get_astar_path() methods
- Path caching in entities for performance
- Complete test suite comparing A* vs Dijkstra performance
- ✅ GUI Text Input Widget System
- Full-featured TextInputWidget class with cursor, selection, scrolling
- Improved widget with proper text rendering and multi-line support
- Example showcase demonstrating multiple input fields
- Foundation for in-game consoles, chat systems, and text entry
- ✅ Sizzle Reel Demos
- path_vision_sizzle_reel.py combines pathfinding with FOV
- Interactive visibility demos showing real-time FOV updates
- Performance demonstrations with multiple entities
### 2025-07-09: Dijkstra Pathfinding & Critical Bug Fix! 🗺️
**TCOD Integration Sprint - Major Progress**
- ✅ Complete Dijkstra pathfinding implementation in UIGrid
- compute_dijkstra(), get_dijkstra_distance(), get_dijkstra_path() methods
- Full TCODMap and TCODDijkstra integration with proper memory management
- Comprehensive test suite with both headless and interactive demos
- ✅ **CRITICAL FIX**: PyArg bug in UIGridPoint color setter
- Now supports both mcrfpy.Color objects and (r,g,b,a) tuples
- Eliminated mysterious "SystemError: new style getargs format" crashes
- Proper error handling and exception propagation
- ✅ mcrfpy.libtcod submodule with Python bindings
- dijkstra_compute(), dijkstra_get_distance(), dijkstra_get_path()
- line() function for corridor generation
- Foundation ready for FOV implementation
- ✅ Test consolidation: 6 broken demos → 2 clean, working versions
### 2025-07-08: PyArgHelpers Infrastructure Complete! 🔧
**Standardized Python API Argument Parsing**
- Unified position handling: (x, y) tuples or separate x, y args
- Consistent size parsing: (w, h) tuples or width, height args
- Grid-specific helpers for tile-based positioning
- Proper conflict detection between positional and keyword args
- All UI components migrated: Frame, Caption, Sprite, Grid, Entity
- Improved error messages: "Value must be a number (int or float)"
- Foundation for Phase 7 documentation efforts
### 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!**
### 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
### 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
### 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
---
## 🔧 CURRENT WORK: Alpha Streamline 2 - Major Architecture Improvements
### Recent Completions:
- ✅ **Phase 1-4 Complete** - Foundation, API Polish, Entity Lifecycle, Visibility/Performance
- ✅ **Phase 5 Complete** - Window/Scene Architecture fully implemented!
- Window singleton with properties (#34)
- OOP Scene support with lifecycle methods (#61)
- Window resize events (#1)
- Scene transitions with animations (#105)
- ✅ **Phase 6 Complete** - Rendering Revolution achieved!
- Grid background colors (#50) ✅
- RenderTexture overhaul (#6) ✅
- UIFrame clipping support ✅
- Viewport-based rendering (#8) ✅
### Active Development:
- **Branch**: alpha_streamline_2
- **Current Phase**: Phase 7 - Documentation & Distribution
- **Achievement**: PyArgHelpers infrastructure complete - standardized Python API
- **Strategic Vision**: See STRATEGIC_VISION.md for platform roadmap
- **Latest**: All UI components now use consistent argument parsing patterns!
### 🏗️ Architectural Dependencies Map
```
Foundation Layer:
├── #71 Base Class (_Drawable)
│ ├── #10 Visibility System (needs AABB from base)
│ ├── #87 visible property
│ └── #88 opacity property
├── #7 Safe Constructors (affects all classes)
│ └── Blocks any new class creation until resolved
└── #30 Entity/Grid Integration (lifecycle management)
└── Enables reliable entity management
Window/Scene Layer:
├── #34 Window Object
│ ├── #61 Scene Object (depends on Window)
│ ├── #14 SFML Exposure (helps implement Window)
│ └── Future: Multi-window support
Rendering Layer:
└── #6 RenderTexture Overhaul
├── Enables clipping
├── Off-screen rendering
└── Post-processing effects
```
## 🚀 Alpha Streamline 2 - Comprehensive Phase Plan
### Phase 1: Foundation Stabilization (1-2 weeks)
**Goal**: Safe, predictable base for all future work
```
1. #7 - Audit and fix unsafe constructors (CRITICAL - do first!)
- Find all manually implemented no-arg constructors
- Verify map compatibility requirements
- Make pointer-safe or remove
2. #71 - _Drawable base class implementation
- Common properties: x, y, w, h, visible, opacity
- Virtual methods: get_bounds(), render()
- Proper Python inheritance setup
3. #87 - visible property
- Add to base class
- Update all render methods to check
4. #88 - opacity property (depends on #87)
- 0.0-1.0 float range
- Apply in render methods
5. #89 - get_bounds() method
- Virtual method returning (x, y, w, h)
- Override in each UI class
6. #98 - move()/resize() convenience methods
- move(dx, dy) - relative movement
- resize(w, h) - absolute sizing
```
*Rationale*: Can't build on unsafe foundations. Base class enables all UI improvements.
### Phase 2: Constructor & API Polish (1 week)
**Goal**: Pythonic, intuitive API
```
1. #101 - Standardize (0,0) defaults for all positions
2. #38 - Frame children parameter: Frame(children=[...])
3. #42 - Click handler in __init__: Button(click=callback)
4. #90 - Grid size tuple: Grid(grid_size=(10, 10))
5. #19 - Sprite texture swapping: sprite.texture = new_texture
6. #52 - Grid skip out-of-bounds entities (performance)
```
*Rationale*: Quick wins that make the API more pleasant before bigger changes.
### Phase 3: Entity Lifecycle Management (1 week)
**Goal**: Bulletproof entity/grid relationships
```
1. #30 - Entity.die() and grid association
- Grid.entities.append(e) sets e.grid = self
- Grid.entities.remove(e) sets e.grid = None
- Entity.die() calls self.grid.remove(self)
- Entity can only be in 0 or 1 grid
2. #93 - Vector arithmetic methods
- add, subtract, multiply, divide
- distance, normalize, dot product
3. #94 - Color helper methods
- from_hex("#FF0000"), to_hex()
- lerp(other_color, t) for interpolation
4. #103 - Timer objects
timer = mcrfpy.Timer("my_timer", callback, 1000)
timer.pause()
timer.resume()
timer.cancel()
```
*Rationale*: Games need reliable entity management. Timer objects enable entity AI.
### Phase 4: Visibility & Performance (1-2 weeks)
**Goal**: Only render/process what's needed
```
1. #10 - [UNSCHEDULED] Full visibility system with AABB
- Postponed: UIDrawables can exist in multiple collections
- Cannot reliably determine screen position due to multiple render contexts
- Needs architectural solution for parent-child relationships
2. #52 - Grid culling (COMPLETED in Phase 2)
3. #39/40/41 - Name system for finding elements
- name="button1" property on all UIDrawables
- only_one=True for unique names
- scene.find("button1") returns element
- collection.find("enemy*") returns list
4. #104 - Basic profiling/metrics
- Frame time tracking
- Draw call counting
- Python vs C++ time split
```
*Rationale*: Performance is feature. Finding elements by name is huge QoL.
### Phase 5: Window/Scene Architecture ✅ COMPLETE! (2025-07-06)
**Goal**: Modern, flexible architecture
```
1. ✅ #34 - Window object (singleton first)
window = mcrfpy.Window.get()
window.resolution = (1920, 1080)
window.fullscreen = True
window.vsync = True
2. ✅ #1 - Window resize events
scene.on_resize(self, width, height) callback implemented
3. ✅ #61 - Scene object (OOP scenes)
class MenuScene(mcrfpy.Scene):
def on_keypress(self, key, state):
# handle input
def on_enter(self):
# setup UI
def on_exit(self):
# cleanup
def update(self, dt):
# frame update
4. ✅ #14 - SFML exposure research
- Completed comprehensive analysis
- Recommendation: Direct integration as mcrfpy.sfml
- SFML 3.0 migration deferred to late 2025
5. ✅ #105 - Scene transitions
mcrfpy.setScene("menu", "fade", 1.0)
# Supports: fade, slide_left, slide_right, slide_up, slide_down
```
*Result*: Entire window/scene system modernized with OOP design!
### Phase 6: Rendering Revolution (3-4 weeks) ✅ COMPLETE!
**Goal**: Professional rendering capabilities
```
1. ✅ #50 - Grid background colors [COMPLETED]
grid.background_color = mcrfpy.Color(50, 50, 50)
- Added background_color property with animation support
- Default dark gray background (8, 8, 8, 255)
2. ✅ #6 - RenderTexture overhaul [COMPLETED]
✅ Base infrastructure in UIDrawable
✅ UIFrame clip_children property
✅ Dirty flag optimization system
✅ Nested clipping support
✅ UIGrid already has appropriate RenderTexture implementation
❌ UICaption/UISprite clipping not needed (no children)
3. ✅ #8 - Viewport-based rendering [COMPLETED]
- Fixed game resolution (window.game_resolution)
- Three scaling modes: "center", "stretch", "fit"
- Window to game coordinate transformation
- Mouse input properly scaled with windowToGameCoords()
- Python API fully integrated
- Tests: test_viewport_simple.py, test_viewport_visual.py, test_viewport_scaling.py
4. #106 - Shader support [DEFERRED TO POST-PHASE 7]
sprite.shader = mcrfpy.Shader.load("glow.frag")
frame.shader_params = {"intensity": 0.5}
5. #107 - Particle system [DEFERRED TO POST-PHASE 7]
emitter = mcrfpy.ParticleEmitter()
emitter.texture = spark_texture
emitter.emission_rate = 100
emitter.lifetime = (0.5, 2.0)
```
**Phase 6 Achievement Summary**:
- Grid backgrounds (#50) ✅ - Customizable background colors with animation
- RenderTexture overhaul (#6) ✅ - UIFrame clipping with opt-in architecture
- Viewport rendering (#8) ✅ - Three scaling modes with coordinate transformation
- UIGrid already had optimal RenderTexture implementation for its use case
- UICaption/UISprite clipping unnecessary (no children to clip)
- Performance optimized with dirty flag system
- Backward compatibility preserved throughout
- Effects/Shader/Particle systems deferred for focused delivery
*Rationale*: This unlocks professional visual effects but is complex.
### Phase 7: Documentation & Distribution (1-2 weeks)
**Goal**: Ready for the world
```
1. ✅ #85 - Replace all "docstring" placeholders [COMPLETED 2025-07-08]
2. ✅ #86 - Add parameter documentation [COMPLETED 2025-07-08]
3. ✅ #108 - Generate .pyi type stubs for IDE support [COMPLETED 2025-07-08]
4. ❌ #70 - PyPI wheel preparation [CANCELLED - Architectural mismatch]
5. API reference generator tool
```
## 📋 Critical Path & Parallel Tracks
### 🔴 **Critical Path** (Must do in order)
**Safe Constructors (#7)** → **Base Class (#71)****Visibility (#10)****Window (#34)** → **Scene (#61)**
### 🟡 **Parallel Tracks** (Can be done alongside critical path)
**Track A: Entity Systems**
- Entity/Grid integration (#30)
- Timer objects (#103)
- Vector/Color helpers (#93, #94)
**Track B: API Polish**
- Constructor improvements (#101, #38, #42, #90)
- Sprite texture swap (#19)
- Name/search system (#39/40/41)
**Track C: Performance**
- Grid culling (#52)
- Visibility culling (part of #10)
- Profiling tools (#104)
### 💎 **Quick Wins to Sprinkle Throughout**
1. Color helpers (#94) - 1 hour
2. Vector methods (#93) - 1 hour
3. Grid backgrounds (#50) - 30 minutes
4. Default positions (#101) - 30 minutes
### 🎯 **Recommended Execution Order**
**Week 1-2**: Foundation (Critical constructors + base class)
**Week 3**: Entity lifecycle + API polish
**Week 4**: Visibility system + performance
**Week 5-6**: Window/Scene architecture
**Week 7-9**: Rendering revolution (or defer to gamma)
**Week 10**: Documentation + release prep
### 🆕 **New Issues to Create/Track**
1. [x] **Timer Objects** - Pythonic timer management (#103) - *Completed Phase 3*
2. [ ] **Event System Enhancement** - Mouse enter/leave, drag, right-click
3. [ ] **Resource Manager** - Centralized asset loading
4. [ ] **Serialization System** - Save/load game state
5. [x] **Scene Transitions** - Fade, slide, custom effects (#105) - *Completed Phase 5*
6. [x] **Profiling Tools** - Performance metrics (#104) - *Completed Phase 4*
7. [ ] **Particle System** - Visual effects framework (#107)
8. [ ] **Shader Support** - Custom rendering effects (#106)
---
## 📋 Phase 6 Implementation Strategy
### RenderTexture Overhaul (#6) - Technical Approach
**Current State**:
- UIGrid already uses RenderTexture for entity rendering
- Scene transitions use RenderTextures for smooth animations
- Direct rendering to window for Frame, Caption, Sprite
**Implementation Plan**:
1. **Base Infrastructure**:
- Add `sf::RenderTexture* target` to UIDrawable base
- Modify `render()` to check if target exists
- If target: render to texture, then draw texture to parent
- If no target: render directly (backward compatible)
2. **Clipping Support**:
- Frame enforces bounds on children via RenderTexture
- Children outside bounds are automatically clipped
- Nested frames create render texture hierarchy
3. **Performance Optimization**:
- Lazy RenderTexture creation (only when needed)
- Dirty flag system (only re-render when changed)
- Texture pooling for commonly used sizes
4. **Integration Points**:
- Scene transitions already working with RenderTextures
- UIGrid can be reference implementation
- Test with deeply nested UI structures
**Quick Wins Before Core Work**:
1. **Grid Background (#50)** - 30 min implementation
- Add `background_color` and `background_texture` properties
- Render before entities in UIGrid::render()
- Good warm-up before tackling RenderTexture
2. **Research Tasks**:
- Study UIGrid's current RenderTexture usage
- Profile scene transition performance
- Identify potential texture size limits
---
## 🚀 NEXT PHASE: Beta Features & Polish
### 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*
Issue #37 is **on hold** until we have a Windows build environment available. I actually suspect this is already fixed by the updates to the makefile, anyway.
- [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
- [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
---
## 🗂 ISSUE TRIAGE BY SYSTEM (78 Total Issues)
### 🎮 Core Engine Systems
#### Iterator/Collection System (2 issues)
- [x] **#73** - Entity index() method for removal - *Fixed*
- [x] **#69** ⚠️ **Alpha Blocker** - Sequence Protocol refactor - *Completed! (2025-07-05)*
#### Python/C++ Integration (7 issues)
- [x] **#76** - UIEntity derived type preservation in collections - *Multiple Integrations*
- [ ] **#71** - Drawable base class hierarchy - *Extensive Overhaul*
- [ ] **#70** - PyPI wheel distribution - *Extensive Overhaul*
- [~] **#32** - Executable behave like `python` command - *Extensive Overhaul* *(90% Complete: -h, -V, -c, -m, -i, script execution, sys.argv, --exec all implemented. Only stdin (-) support missing)*
- [ ] **#35** - TCOD as built-in module - *Extensive Overhaul*
- [~] **#14** - Expose SFML as built-in module - *Research Complete, Implementation Pending*
- [ ] **#46** - Subinterpreter threading tests - *Multiple Integrations*
#### UI/Rendering System (12 issues)
- [x] **#63** ⚠️ **Alpha Blocker** - Z-order for UIDrawables - *Multiple Integrations*
- [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*
- [x] **#9** - UIGrid RenderTexture resize handling - *Multiple Integrations*
- [ ] **#52** - UIGrid skip out-of-bounds entities - *Isolated Fix*
- [ ] **#50** - UIGrid background color field - *Isolated Fix*
- [ ] **#19** - Sprite get/set texture methods - *Multiple Integrations*
- [ ] **#17** - Move UISprite position into sf::Sprite - *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*
- [ ] **#16** - Grid strict mode for entity knowledge/visibility - *Extensive Overhaul*
- [ ] **#67** - Grid stitching for infinite worlds - *Extensive Overhaul*
- [ ] **#15** - UIGridPointState cleanup and standardization - *Multiple Integrations*
- [ ] **#20** - UIGrid get_grid_size standardization - *Multiple Integrations*
- [x] **#12** - GridPoint/GridPointState forbid direct init - *Isolated Fix*
#### Scene/Window Management (5 issues)
- [x] **#61** - Scene object encapsulating key callbacks - *Completed Phase 5*
- [x] **#34** - Window object for resolution/scaling - *Completed Phase 5*
- [ ] **#62** - Multiple windows support - *Extensive Overhaul*
- [ ] **#49** - Window resolution & viewport controls - *Multiple Integrations*
- [x] **#1** - Scene resize event handling - *Completed Phase 5*
### 🔧 Quality of Life Features
#### UI Enhancement Features (8 issues)
- [ ] **#39** - Name field on UIDrawables - *Multiple Integrations*
- [ ] **#40** - `only_one` arg for unique naming - *Multiple Integrations*
- [ ] **#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*
- [x] **#27** - UIEntityCollection.extend() method - *Fixed*
- [ ] **#28** - UICollectionIter for scene ui iteration - *Isolated Fix*
- [ ] **#26** - UIEntityCollectionIter implementation - *Isolated Fix*
### 🧹 Refactoring & Cleanup
#### Code Cleanup (7 issues)
- [x] **#3** ⚠️ **Alpha Blocker** - Remove `McRFPy_API::player_input` - *Completed*
- [x] **#2** ⚠️ **Alpha Blocker** - Review `registerPyAction` necessity - *Completed*
- [ ] **#7** - Remove unsafe no-argument constructors - *Multiple Integrations*
- [ ] **#21** - PyUIGrid dealloc cleanup - *Isolated Fix*
- [ ] **#75** - REPL thread separation from SFML window - *Multiple Integrations*
### 📚 Demo & Documentation
#### Documentation (2 issues)
- [x] **#47** ⚠️ **Alpha Blocker** - Alpha release README.md - *Isolated Fix*
- [ ] **#48** - Dependency compilation documentation - *Isolated Fix*
#### Demo Projects (6 issues)
- [ ] **#54** - Jupyter notebook integration demo - *Multiple Integrations*
- [ ] **#55** - Hunt the Wumpus AI demo - *Multiple Integrations*
- [ ] **#53** - Web interface input demo - *Multiple Integrations* *(New automation API could help)*
- [ ] **#45** - Accessibility mode demos - *Multiple Integrations* *(New automation API could help test)*
- [ ] **#36** - Dear ImGui integration tests - *Extensive Overhaul*
- [ ] **#65** - Python Explorer scene (replaces uitest) - *Extensive Overhaul*
---
## 🎮 STRATEGIC DIRECTION
### Engine Philosophy Maintained
- **C++ First**: Performance-critical code stays in C++
- **Python Close Behind**: Rich scripting without frame-rate impact
- **Game-Ready**: Each improvement should benefit actual game development
### Architecture Goals
1. **Clean Inheritance**: Drawable → UI components, proper type preservation
2. **Collection Consistency**: Uniform iteration, indexing, and search patterns
3. **Resource Management**: RAII everywhere, proper lifecycle handling
4. **Multi-Platform**: Windows/Linux feature parity maintained
---
## 📚 REFERENCES & CONTEXT
**Issue Dependencies** (Key Chains):
- Iterator System: Grid points → #73#69 (Alpha Blocker)
- UI Hierarchy: #71#63 (Alpha Blocker)
- Rendering: #6 (Alpha Blocker) → #8, #9#10
- Entity System: #30#16#67
- Window Management: #34#49, #61#62
**Commit References**:
- 167636c: Iterator improvements (UICollection/UIEntityCollection complete)
- Recent work: 7DRL 2025 completion, RPATH updates, console improvements
**Architecture Files**:
- Iterator patterns: src/UICollection.cpp, src/UIGrid.cpp
- Python integration: src/McRFPy_API.cpp, src/PyObjectUtils.h
- Game implementation: src/scripts/ (Crypt of Sokoban complete game)
---
## 🔮 FUTURE VISION: Pure Python Extension Architecture
### Concept: McRogueFace as a Traditional Python Package
**Status**: Unscheduled - Long-term vision
**Complexity**: Major architectural overhaul
Instead of being a C++ application that embeds Python, McRogueFace could be redesigned as a pure Python extension module that can be installed via `pip install mcrogueface`.
### Technical Approach
1. **Separate Core Engine from Python Embedding**
- Extract SFML rendering, audio, and input into C++ extension modules
- Remove embedded CPython interpreter
- Use Python's C API to expose functionality
2. **Module Structure**
```
mcrfpy/
├── __init__.py # Pure Python coordinator
├── _core.so # C++ rendering/game loop extension
├── _sfml.so # SFML bindings
├── _audio.so # Audio system bindings
└── engine.py # Python game engine logic
```
3. **Inverted Control Flow**
- Python drives the main loop instead of C++
- C++ extensions handle performance-critical operations
- Python manages game logic, scenes, and entity systems
### Benefits
- **Standard Python Packaging**: `pip install mcrogueface`
- **Virtual Environment Support**: Works with venv, conda, poetry
- **Better IDE Integration**: Standard Python development workflow
- **Easier Testing**: Use pytest, standard Python testing tools
- **Cross-Python Compatibility**: Support multiple Python versions
- **Modular Architecture**: Users can import only what they need
### Challenges
- **Major Refactoring**: Complete restructure of codebase
- **Performance Considerations**: Python-driven main loop overhead
- **Build Complexity**: Multiple extension modules to compile
- **Platform Support**: Need wheels for many platform/Python combinations
- **API Stability**: Would need careful design to maintain compatibility
### Implementation Phases (If Pursued)
1. **Proof of Concept**: Simple SFML binding as Python extension
2. **Core Extraction**: Separate rendering from Python embedding
3. **Module Design**: Define clean API boundaries
4. **Incremental Migration**: Move systems one at a time
5. **Compatibility Layer**: Support existing games during transition
### Example Usage (Future Vision)
```python
import mcrfpy
from mcrfpy import Scene, Frame, Sprite, Grid
# Create game directly in Python
game = mcrfpy.Game(width=1024, height=768)
# Define scenes using Python classes
class MainMenu(Scene):
def on_enter(self):
self.ui.append(Frame(100, 100, 200, 50))
self.ui.append(Sprite("logo.png", x=400, y=100))
def on_keypress(self, key, pressed):
if key == "ENTER" and pressed:
self.game.set_scene("game")
# Run the game
game.add_scene("menu", MainMenu())
game.run()
```
This architecture would make McRogueFace a first-class Python citizen, following standard Python packaging conventions while maintaining high performance through C++ extensions.
---
## 🚀 IMMEDIATE NEXT STEPS (Priority Order)
### TODAY (July 12) - CRITICAL PATH:
1. **FIX ANIMATION BLOCKERS** for Tutorial Part 2:
- Implement input queueing during animations
- Add destination square reservation
- Test Pokemon-style continuous movement
2. **FIX GRID CLICKING** (discovered broken in headless):
- Uncomment and implement click events
- Add tile coordinate conversion
- Enable nested grid support
3. **CREATE TUTORIAL ANNOUNCEMENT** if blockers fixed
### Weekend (July 13-14) - Tutorial Sprint:
1. **Regenerate Parts 3-6** (machine drafts are broken)
2. **Create Parts 7-10**: Interface, Items, Targeting, Save/Load
3. **Create Parts 11-15**: Dungeon levels, difficulty, equipment
4. **Post more frequently during event** (narrator emphasis)
### Architecture Decision Log:
- **DECIDED**: Use three-layer architecture (visual/world/perspective)
- **DECIDED**: Spatial hashing over quadtrees for entities
- **DECIDED**: Batch operations are mandatory, not optional
- **DECIDED**: TCOD integration as mcrfpy.libtcod submodule
- **DECIDED**: Tutorial must showcase McRogueFace strengths, not mimic TCOD
### Risk Mitigation:
- **If TCOD integration delays**: Use pure Python FOV for tutorial
- **If performance issues**: Focus on <100x100 maps for demos
- **If tutorial incomplete**: Ship with 4 solid parts + roadmap
- **If bugs block progress**: Document as "known issues" and continue
---
## 📋 COMPREHENSIVE ISSUES FROM TRANSCRIPT ANALYSIS
### Animation System (6 issues)
1. **Input Queue During Animation**: Queue one additional move during animation
2. **Destination Square Reservation**: Block target when movement begins
3. **Pokemon-Style Movement**: Smooth continuous movement with input handling
4. **#119** - Animation Callbacks: Add completion callbacks with parameters
5. **#120** - Property Conflict Prevention: Prevent multiple animations on same property
6. **Remove Bare Pointers**: Complete refactoring to weak references ✅
### Grid System (6 issues)
7. **#111** - Grid Click Implementation: Fix commented-out events in headless
8. **Tile Coordinate Conversion**: Convert mouse to tile coordinates
9. **Nested Grid Support**: Enable clicking on grids within grids
10. **#123** - Grid Rendering Performance: Implement 256x256 subgrid system
11. **#116** - Dirty Flagging: Add dirty flag propagation from base
12. **#124** - Grid Point Animation: Enable animating individual tiles
### Python API (6 issues)
13. **Regenerate Python Bindings**: Create consistent interface generation
14. **#109** - Vector Class Enhancement: Add [0], [1] indexing to vectors
15. **#112** - Fix Object Splitting: Preserve Python derived class types
16. **#101/#110** - Standardize Constructors: Make all constructors consistent
17. **Color Class Bindings**: Properly expose SFML Color class
18. **Font Class Bindings**: Properly expose SFML Font class
### Architecture (8 issues)
19. **#118** - Scene as Drawable: Refactor Scene to inherit from Drawable
20. **Scene Visibility System**: Implement exclusive visibility switching
21. **Replace Transition System**: Use animations not special transitions
22. **#122** - Parent-Child UI: Add parent field to UI drawables
23. **Collection Methods**: Implement append/remove/extend with parent updates
24. **#121** - Timer Object System: Replace string-dictionary timers
25. **One-Shot Timer Mode**: Implement proper one-shot functionality
26. **Button Mechanics**: Any entity type can trigger buttons
### Entity System (4 issues)
27. **Step-On Entities**: Implement trigger when stepped on
28. **Bump Interaction**: Add bump-to-interact behavior
29. **Type-Aware Interactions**: Entity interactions based on type
30. **Button Mechanics**: Any entity can trigger buttons
### Tutorial & Documentation (4 issues)
31. **Fix Part 2 Tutorial**: Unblock with animation fixes
32. **Regenerate Parts 3-6**: Replace machine-generated content
33. **API Documentation**: Document ergonomic improvements
34. **Tutorial Alignment**: Ensure parts match TCOD structure
---
*Last Updated: 2025-07-12 (CRITICAL TUTORIAL SPRINT)*
*Next Review: July 15 after event start*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

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

View File

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

View File

@ -16,7 +16,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
{
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
Resources::game = this;
window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine";
window_title = "McRogueFace Engine";
// Initialize rendering based on headless mode
if (headless) {
@ -91,6 +91,9 @@ void GameEngine::cleanup()
if (cleaned_up) return;
cleaned_up = true;
// Clear all animations first (RAII handles invalidation)
AnimationManager::getInstance().clear();
// Clear Python references before destroying C++ objects
// Clear all timers (they hold Python callables)
timers.clear();
@ -182,7 +185,7 @@ void GameEngine::setWindowScale(float multiplier)
void GameEngine::run()
{
std::cout << "GameEngine::run() starting main loop..." << std::endl;
//std::cout << "GameEngine::run() starting main loop..." << std::endl;
float fps = 0.0;
frameTime = 0.016f; // Initialize to ~60 FPS
clock.restart();
@ -259,7 +262,7 @@ void GameEngine::run()
int tenth_fps = (metrics.fps * 10) % 10;
if (!headless && window) {
window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
window->setTitle(window_title);
}
// In windowed mode, check if window was closed

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -65,26 +65,37 @@ namespace mcrfpydef {
//.tp_iter
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)\n\n"
.tp_doc = PyDoc_STR("Caption(pos=None, font=None, text='', **kwargs)\n\n"
"A text display UI element with customizable font and styling.\n\n"
"Args:\n"
" text (str): The text content to display. Default: ''\n"
" x (float): X position in pixels. Default: 0\n"
" y (float): Y position in pixels. Default: 0\n"
" font (Font): Font object for text rendering. Default: engine default font\n"
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
" font (Font, optional): Font object for text rendering. Default: engine default font\n"
" text (str, optional): The text content to display. Default: ''\n\n"
"Keyword Args:\n"
" fill_color (Color): Text fill color. Default: (255, 255, 255, 255)\n"
" outline_color (Color): Text outline color. Default: (0, 0, 0, 255)\n"
" outline (float): Text outline thickness. Default: 0\n"
" click (callable): Click event handler. Default: None\n\n"
" font_size (float): Font size in points. Default: 16\n"
" click (callable): Click event handler. Default: None\n"
" visible (bool): Visibility state. Default: True\n"
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
" z_index (int): Rendering order. Default: 0\n"
" name (str): Element name for finding. Default: None\n"
" x (float): X position override. Default: 0\n"
" y (float): Y position override. Default: 0\n\n"
"Attributes:\n"
" text (str): The displayed text content\n"
" x, y (float): Position in pixels\n"
" pos (Vector): Position as a Vector object\n"
" font (Font): Font used for rendering\n"
" font_size (float): Font size in points\n"
" fill_color, outline_color (Color): Text appearance\n"
" outline (float): Outline thickness\n"
" click (callable): Click event handler\n"
" visible (bool): Visibility state\n"
" opacity (float): Opacity value\n"
" z_index (int): Rendering order\n"
" name (str): Element name\n"
" w, h (float): Read-only computed size based on text and font"),
.tp_methods = UICaption_methods,
//.tp_members = PyUIFrame_members,

View File

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

View File

@ -88,7 +88,28 @@ namespace mcrfpydef {
.tp_itemsize = 0,
.tp_repr = (reprfunc)UIEntity::repr,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = "UIEntity objects",
.tp_doc = PyDoc_STR("Entity(grid_pos=None, texture=None, sprite_index=0, **kwargs)\n\n"
"A game entity that exists on a grid with sprite rendering.\n\n"
"Args:\n"
" grid_pos (tuple, optional): Grid position as (x, y) tuple. Default: (0, 0)\n"
" texture (Texture, optional): Texture object for sprite. Default: default texture\n"
" sprite_index (int, optional): Index into texture atlas. Default: 0\n\n"
"Keyword Args:\n"
" grid (Grid): Grid to attach entity to. Default: None\n"
" visible (bool): Visibility state. Default: True\n"
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
" name (str): Element name for finding. Default: None\n"
" x (float): X grid position override. Default: 0\n"
" y (float): Y grid position override. Default: 0\n\n"
"Attributes:\n"
" pos (tuple): Grid position as (x, y) tuple\n"
" x, y (float): Grid position coordinates\n"
" draw_pos (tuple): Pixel position for rendering\n"
" gridstate (GridPointState): Visibility state for grid points\n"
" sprite_index (int): Current sprite index\n"
" visible (bool): Visibility state\n"
" opacity (float): Opacity value\n"
" name (str): Element name"),
.tp_methods = UIEntity_all_methods,
.tp_getset = UIEntity::getsetters,
.tp_base = &mcrfpydef::PyDrawableType,

View File

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

View File

@ -86,27 +86,38 @@ namespace mcrfpydef {
//.tp_iter
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)\n\n"
.tp_doc = PyDoc_STR("Frame(pos=None, size=None, **kwargs)\n\n"
"A rectangular frame UI element that can contain other drawable elements.\n\n"
"Args:\n"
" x (float): X position in pixels. Default: 0\n"
" y (float): Y position in pixels. Default: 0\n"
" w (float): Width in pixels. Default: 0\n"
" h (float): Height in pixels. Default: 0\n"
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
" size (tuple, optional): Size as (width, height) tuple. Default: (0, 0)\n\n"
"Keyword Args:\n"
" fill_color (Color): Background fill color. Default: (0, 0, 0, 128)\n"
" outline_color (Color): Border outline color. Default: (255, 255, 255, 255)\n"
" outline (float): Border outline thickness. Default: 0\n"
" click (callable): Click event handler. Default: None\n"
" children (list): Initial list of child drawable elements. Default: None\n\n"
" children (list): Initial list of child drawable elements. Default: None\n"
" visible (bool): Visibility state. Default: True\n"
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
" z_index (int): Rendering order. Default: 0\n"
" name (str): Element name for finding. Default: None\n"
" x (float): X position override. Default: 0\n"
" y (float): Y position override. Default: 0\n"
" w (float): Width override. Default: 0\n"
" h (float): Height override. Default: 0\n"
" clip_children (bool): Whether to clip children to frame bounds. Default: False\n\n"
"Attributes:\n"
" x, y (float): Position in pixels\n"
" w, h (float): Size in pixels\n"
" pos (Vector): Position as a Vector object\n"
" fill_color, outline_color (Color): Visual appearance\n"
" outline (float): Border thickness\n"
" click (callable): Click event handler\n"
" children (list): Collection of child drawable elements\n"
" visible (bool): Visibility state\n"
" opacity (float): Opacity value\n"
" z_index (int): Rendering order\n"
" name (str): Element name\n"
" clip_children (bool): Whether to clip children to frame bounds"),
.tp_methods = UIFrame_methods,
//.tp_members = PyUIFrame_members,

View File

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

View File

@ -184,29 +184,49 @@ namespace mcrfpydef {
//.tp_iter
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)\n\n"
"A grid-based tilemap UI element for rendering tile-based levels and game worlds.\n\n"
.tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n"
"A grid-based UI element for tile-based rendering and entity management.\n\n"
"Args:\n"
" x (float): X position in pixels. Default: 0\n"
" y (float): Y position in pixels. Default: 0\n"
" grid_size (tuple): Grid dimensions as (width, height) in tiles. Default: (20, 20)\n"
" texture (Texture): Texture atlas containing tile sprites. Default: None\n"
" tile_width (int): Width of each tile in pixels. Default: 16\n"
" tile_height (int): Height of each tile in pixels. Default: 16\n"
" scale (float): Grid scaling factor. Default: 1.0\n"
" click (callable): Click event handler. Default: None\n\n"
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
" size (tuple, optional): Size as (width, height) tuple. Default: auto-calculated from grid_size\n"
" grid_size (tuple, optional): Grid dimensions as (grid_x, grid_y) tuple. Default: (2, 2)\n"
" texture (Texture, optional): Texture containing tile sprites. Default: default texture\n\n"
"Keyword Args:\n"
" fill_color (Color): Background fill color. Default: None\n"
" click (callable): Click event handler. Default: None\n"
" center_x (float): X coordinate of center point. Default: 0\n"
" center_y (float): Y coordinate of center point. Default: 0\n"
" zoom (float): Zoom level for rendering. Default: 1.0\n"
" perspective (int): Entity perspective index (-1 for omniscient). Default: -1\n"
" visible (bool): Visibility state. Default: True\n"
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
" z_index (int): Rendering order. Default: 0\n"
" name (str): Element name for finding. Default: None\n"
" x (float): X position override. Default: 0\n"
" y (float): Y position override. Default: 0\n"
" w (float): Width override. Default: auto-calculated\n"
" h (float): Height override. Default: auto-calculated\n"
" grid_x (int): Grid width override. Default: 2\n"
" grid_y (int): Grid height override. Default: 2\n\n"
"Attributes:\n"
" x, y (float): Position in pixels\n"
" w, h (float): Size in pixels\n"
" pos (Vector): Position as a Vector object\n"
" size (tuple): Size as (width, height) tuple\n"
" center (tuple): Center point as (x, y) tuple\n"
" center_x, center_y (float): Center point coordinates\n"
" zoom (float): Zoom level for rendering\n"
" grid_size (tuple): Grid dimensions (width, height) in tiles\n"
" tile_width, tile_height (int): Tile dimensions in pixels\n"
" grid_x, grid_y (int): Grid dimensions\n"
" texture (Texture): Tile texture atlas\n"
" scale (float): Scale multiplier\n"
" points (list): 2D array of GridPoint objects for tile data\n"
" entities (list): Collection of Entity objects in the grid\n"
" background_color (Color): Grid background color\n"
" fill_color (Color): Background color\n"
" entities (EntityCollection): Collection of entities in the grid\n"
" perspective (int): Entity perspective index\n"
" click (callable): Click event handler\n"
" visible (bool): Visibility state\n"
" z_index (int): Rendering order"),
" opacity (float): Opacity value\n"
" z_index (int): Rendering order\n"
" name (str): Element name"),
.tp_methods = UIGrid_all_methods,
//.tp_members = UIGrid::members,
.tp_getset = UIGrid::getsetters,

View File

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

View File

@ -92,23 +92,35 @@ namespace mcrfpydef {
//.tp_iter
//.tp_iternext
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)\n\n"
.tp_doc = PyDoc_STR("Sprite(pos=None, texture=None, sprite_index=0, **kwargs)\n\n"
"A sprite UI element that displays a texture or portion of a texture atlas.\n\n"
"Args:\n"
" x (float): X position in pixels. Default: 0\n"
" y (float): Y position in pixels. Default: 0\n"
" texture (Texture): Texture object to display. Default: None\n"
" sprite_index (int): Index into texture atlas (if applicable). Default: 0\n"
" scale (float): Sprite scaling factor. Default: 1.0\n"
" click (callable): Click event handler. Default: None\n\n"
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
" texture (Texture, optional): Texture object to display. Default: default texture\n"
" sprite_index (int, optional): Index into texture atlas. Default: 0\n\n"
"Keyword Args:\n"
" scale (float): Uniform scale factor. Default: 1.0\n"
" scale_x (float): Horizontal scale factor. Default: 1.0\n"
" scale_y (float): Vertical scale factor. Default: 1.0\n"
" click (callable): Click event handler. Default: None\n"
" visible (bool): Visibility state. Default: True\n"
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
" z_index (int): Rendering order. Default: 0\n"
" name (str): Element name for finding. Default: None\n"
" x (float): X position override. Default: 0\n"
" y (float): Y position override. Default: 0\n\n"
"Attributes:\n"
" x, y (float): Position in pixels\n"
" pos (Vector): Position as a Vector object\n"
" texture (Texture): The texture being displayed\n"
" sprite_index (int): Current sprite index in texture atlas\n"
" scale (float): Scale multiplier\n"
" scale (float): Uniform scale factor\n"
" scale_x, scale_y (float): Individual scale factors\n"
" click (callable): Click event handler\n"
" visible (bool): Visibility state\n"
" opacity (float): Opacity value\n"
" z_index (int): Rendering order\n"
" name (str): Element name\n"
" w, h (float): Read-only computed size based on texture and scale"),
.tp_methods = UISprite_methods,
//.tp_members = PyUIFrame_members,

View File

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

View File

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

View File

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

View File

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

View File

@ -258,8 +258,9 @@ def demo_grid_animations(ui):
except:
texture = None
grid = Grid(100, 150, grid_size=(20, 15), texture=texture,
tile_width=24, tile_height=24)
# Grid constructor: Grid(grid_x, grid_y, texture, position, size)
# Note: tile dimensions are determined by texture's grid_size
grid = Grid(20, 15, texture, (100, 150), (480, 360)) # 20x24, 15x24
grid.fill_color = Color(20, 20, 40)
ui.append(grid)
@ -282,7 +283,7 @@ def demo_grid_animations(ui):
# Create entities in the grid
if texture:
entity1 = Entity(5.0, 5.0, texture, sprite_index=8)
entity1 = Entity((5.0, 5.0), texture, 8) # position tuple, texture, sprite_index
entity1.scale = 1.5
grid.entities.append(entity1)
@ -291,7 +292,7 @@ def demo_grid_animations(ui):
entity_pos.start(entity1)
# Create patrolling entity
entity2 = Entity(10.0, 2.0, texture, sprite_index=12)
entity2 = Entity((10.0, 2.0), texture, 12) # position tuple, texture, sprite_index
grid.entities.append(entity2)
# Animate sprite changes

View File

@ -183,7 +183,7 @@ def clear_scene():
# Keep only the first two elements (title and subtitle)
while len(ui) > 2:
ui.remove(ui[2])
ui.remove(2)
def run_demo_sequence(runtime):
"""Run through all demos"""

View File

@ -268,8 +268,6 @@ def run_next_demo(runtime):
# Clean up timers from previous demo
for timer in ["opacity_0", "opacity_1", "opacity_2", "opacity_3",
"c_green", "c_blue", "c_white"]:
if not mcrfpy.getTimer(timer):
continue
try:
mcrfpy.delTimer(timer)
except:

File diff suppressed because it is too large Load Diff

View File

@ -48,6 +48,10 @@ mode = "CHASE"
show_dijkstra = False
animation_speed = 3.0
# Track waypoints separately since Entity doesn't have custom attributes
entity_waypoints = {} # entity -> [(x, y), ...]
entity_waypoint_indices = {} # entity -> current index
def create_dungeon():
"""Create a dungeon-like map"""
global grid
@ -126,37 +130,34 @@ def spawn_entities():
global player, enemies, treasures, patrol_entities
# Clear existing entities
grid.entities.clear()
#grid.entities.clear()
enemies = []
treasures = []
patrol_entities = []
# Spawn player in center room
player = mcrfpy.Entity(15, 11)
player.sprite_index = PLAYER
player = mcrfpy.Entity((15, 11), mcrfpy.default_texture, PLAYER)
grid.entities.append(player)
# Spawn enemies in corners
enemy_positions = [(4, 4), (24, 4), (4, 16), (24, 16)]
for x, y in enemy_positions:
enemy = mcrfpy.Entity(x, y)
enemy.sprite_index = ENEMY
enemy = mcrfpy.Entity((x, y), mcrfpy.default_texture, ENEMY)
grid.entities.append(enemy)
enemies.append(enemy)
# Spawn treasures
treasure_positions = [(6, 5), (24, 5), (15, 10)]
for x, y in treasure_positions:
treasure = mcrfpy.Entity(x, y)
treasure.sprite_index = TREASURE
treasure = mcrfpy.Entity((x, y), mcrfpy.default_texture, TREASURE)
grid.entities.append(treasure)
treasures.append(treasure)
# Spawn patrol entities
patrol = mcrfpy.Entity(10, 10)
patrol.sprite_index = PATROL
patrol.waypoints = [(10, 10), (19, 10), (19, 16), (10, 16)] # Square patrol
patrol.waypoint_index = 0
patrol = mcrfpy.Entity((10, 10), mcrfpy.default_texture, PATROL)
# Store waypoints separately since Entity doesn't support custom attributes
entity_waypoints[patrol] = [(10, 10), (19, 10), (19, 16), (10, 16)] # Square patrol
entity_waypoint_indices[patrol] = 0
grid.entities.append(patrol)
patrol_entities.append(patrol)
@ -222,18 +223,21 @@ def move_enemies(dt):
def move_patrols(dt):
"""Move patrol entities along waypoints"""
for patrol in patrol_entities:
if not hasattr(patrol, 'waypoints'):
if patrol not in entity_waypoints:
continue
# Get current waypoint
target_x, target_y = patrol.waypoints[patrol.waypoint_index]
waypoints = entity_waypoints[patrol]
waypoint_index = entity_waypoint_indices[patrol]
target_x, target_y = waypoints[waypoint_index]
# Check if reached waypoint
dist = abs(patrol.x - target_x) + abs(patrol.y - target_y)
if dist < 0.5:
# Move to next waypoint
patrol.waypoint_index = (patrol.waypoint_index + 1) % len(patrol.waypoints)
target_x, target_y = patrol.waypoints[patrol.waypoint_index]
entity_waypoint_indices[patrol] = (waypoint_index + 1) % len(waypoints)
waypoint_index = entity_waypoint_indices[patrol]
target_x, target_y = waypoints[waypoint_index]
# Path to waypoint
path = patrol.path_to(target_x, target_y)
@ -370,4 +374,4 @@ mcrfpy.setTimer("entities", update_entities, 16) # 60 FPS
# Show scene
mcrfpy.setScene("pathfinding_showcase")
print("\nShowcase ready! Move with WASD and watch entities react.")
print("\nShowcase ready! Move with WASD and watch entities react.")

View File

@ -28,11 +28,11 @@ class TextInput:
# Label
if self.label:
self.label_caption = mcrfpy.Caption(self.label, self.x, self.y - 20)
self.label_caption.color = (255, 255, 255, 255)
self.label_caption.fill_color = (255, 255, 255, 255)
# Text display
self.text_caption = mcrfpy.Caption("", self.x + 4, self.y + 4)
self.text_caption.color = (0, 0, 0, 255)
self.text_caption.fill_color = (0, 0, 0, 255)
# Cursor (a simple vertical line using a frame)
self.cursor = mcrfpy.Frame(self.x + 4, self.y + 4, 2, 16)
@ -176,7 +176,7 @@ def create_scene():
# Title
title = mcrfpy.Caption("Text Input Widget Demo", 10, 10)
title.color = (255, 255, 255, 255)
title.fill_color = (255, 255, 255, 255)
scene.append(title)
# Create input fields
@ -194,7 +194,7 @@ def create_scene():
# Status text
status = mcrfpy.Caption("Click to focus, type to enter text", 50, 280)
status.color = (200, 200, 200, 255)
status.fill_color = (200, 200, 200, 255)
scene.append(status)
# Keyboard handler

View File

@ -5,12 +5,19 @@ McRogueFace Animation Sizzle Reel - Final Version
Complete demonstration of all animation capabilities.
This version works properly with the game loop and avoids API issues.
WARNING: This demo causes a segmentation fault due to a bug in the
AnimationManager. When UI elements with active animations are removed
from the scene, the AnimationManager crashes when trying to update them.
Use sizzle_reel_final_fixed.py instead, which works around this issue
by hiding objects off-screen instead of removing them.
"""
import mcrfpy
# Configuration
DEMO_DURATION = 4.0 # Duration for each demo
DEMO_DURATION = 6.0 # Duration for each demo
# All available easing functions
EASING_FUNCTIONS = [
@ -41,6 +48,7 @@ def create_scene():
title = mcrfpy.Caption("Animation Sizzle Reel", 500, 20)
title.fill_color = mcrfpy.Color(255, 255, 0)
title.outline = 2
title.font_size = 28
ui.append(title)
# Subtitle
@ -79,18 +87,21 @@ def demo2_caption_animations():
# Moving caption
c1 = mcrfpy.Caption("Bouncing Text!", 100, 200)
c1.fill_color = mcrfpy.Color(255, 255, 255)
c1.font_size = 28
ui.append(c1)
mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1)
# Color cycling
c2 = mcrfpy.Caption("Color Cycle", 400, 300)
c2.outline = 2
c2.font_size = 28
ui.append(c2)
mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear").start(c2)
# Typewriter effect
c3 = mcrfpy.Caption("", 100, 400)
c3.fill_color = mcrfpy.Color(0, 255, 255)
c3.font_size = 28
ui.append(c3)
mcrfpy.Animation("text", "Typewriter effect animation...", 3.0, "linear").start(c3)
@ -147,7 +158,7 @@ def clear_demo_objects():
# Keep removing items after the first 2 (title and subtitle)
while len(ui) > 2:
# Remove the last item
ui.remove(ui[len(ui)-1])
ui.remove(len(ui)-1)
def next_demo(runtime):
"""Run the next demo"""
@ -167,11 +178,13 @@ def next_demo(runtime):
current_demo += 1
if current_demo < len(demos):
mcrfpy.setTimer("next", next_demo, int(DEMO_DURATION * 1000))
#mcrfpy.setTimer("next", next_demo, int(DEMO_DURATION * 1000))
pass
else:
subtitle.text = "Demo Complete!"
# Initialize
print("Starting Animation Sizzle Reel...")
create_scene()
mcrfpy.setTimer("start", next_demo, 500)
mcrfpy.setTimer("start", next_demo, int(DEMO_DURATION * 1000))
next_demo(0)

View File

@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""
McRogueFace Animation Sizzle Reel - Fixed Version
=================================================
This version works around the animation crash by:
1. Using shorter demo durations to ensure animations complete before clearing
2. Adding a delay before clearing to let animations finish
3. Not removing objects, just hiding them off-screen instead
"""
import mcrfpy
# Configuration
DEMO_DURATION = 3.5 # Slightly shorter to ensure animations complete
CLEAR_DELAY = 0.5 # Extra delay before clearing
# All available easing functions
EASING_FUNCTIONS = [
"linear", "easeIn", "easeOut", "easeInOut",
"easeInQuad", "easeOutQuad", "easeInOutQuad",
"easeInCubic", "easeOutCubic", "easeInOutCubic",
"easeInQuart", "easeOutQuart", "easeInOutQuart",
"easeInSine", "easeOutSine", "easeInOutSine",
"easeInExpo", "easeOutExpo", "easeInOutExpo",
"easeInCirc", "easeOutCirc", "easeInOutCirc",
"easeInElastic", "easeOutElastic", "easeInOutElastic",
"easeInBack", "easeOutBack", "easeInOutBack",
"easeInBounce", "easeOutBounce", "easeInOutBounce"
]
# Track demo state
current_demo = 0
subtitle = None
demo_objects = [] # Track objects to hide instead of remove
def create_scene():
"""Create the demo scene"""
mcrfpy.createScene("demo")
mcrfpy.setScene("demo")
ui = mcrfpy.sceneUI("demo")
# Title
title = mcrfpy.Caption("Animation Sizzle Reel", 500, 20)
title.fill_color = mcrfpy.Color(255, 255, 0)
title.outline = 2
ui.append(title)
# Subtitle
global subtitle
subtitle = mcrfpy.Caption("Starting...", 450, 60)
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(subtitle)
return ui
def hide_demo_objects():
"""Hide demo objects by moving them off-screen instead of removing"""
global demo_objects
# Move all demo objects far off-screen
for obj in demo_objects:
obj.x = -1000
obj.y = -1000
demo_objects = []
def demo1_frame_animations():
"""Frame position, size, and color animations"""
global demo_objects
ui = mcrfpy.sceneUI("demo")
subtitle.text = "Demo 1: Frame Animations"
# Create frame
f = mcrfpy.Frame(100, 150, 200, 100)
f.fill_color = mcrfpy.Color(50, 50, 150)
f.outline = 3
f.outline_color = mcrfpy.Color(255, 255, 255)
ui.append(f)
demo_objects.append(f)
# Animate properties with shorter durations
mcrfpy.Animation("x", 600.0, 2.0, "easeInOutBack").start(f)
mcrfpy.Animation("y", 300.0, 2.0, "easeInOutElastic").start(f)
mcrfpy.Animation("w", 300.0, 2.5, "easeInOutCubic").start(f)
mcrfpy.Animation("h", 150.0, 2.5, "easeInOutCubic").start(f)
mcrfpy.Animation("fill_color", (255, 100, 50, 200), 3.0, "easeInOutSine").start(f)
mcrfpy.Animation("outline", 8.0, 3.0, "easeInOutQuad").start(f)
def demo2_caption_animations():
"""Caption movement and text effects"""
global demo_objects
ui = mcrfpy.sceneUI("demo")
subtitle.text = "Demo 2: Caption Animations"
# Moving caption
c1 = mcrfpy.Caption("Bouncing Text!", 100, 200)
c1.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(c1)
demo_objects.append(c1)
mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1)
# Color cycling
c2 = mcrfpy.Caption("Color Cycle", 400, 300)
c2.outline = 2
ui.append(c2)
demo_objects.append(c2)
mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear").start(c2)
# Static text (no typewriter effect to avoid issues)
c3 = mcrfpy.Caption("Animation Demo", 100, 400)
c3.fill_color = mcrfpy.Color(0, 255, 255)
ui.append(c3)
demo_objects.append(c3)
def demo3_easing_showcase():
"""Show all 30 easing functions"""
global demo_objects
ui = mcrfpy.sceneUI("demo")
subtitle.text = "Demo 3: All 30 Easing Functions"
# Create a small frame for each easing
for i, easing in enumerate(EASING_FUNCTIONS[:15]): # First 15
row = i // 5
col = i % 5
x = 100 + col * 200
y = 150 + row * 100
# Frame
f = mcrfpy.Frame(x, y, 20, 20)
f.fill_color = mcrfpy.Color(100, 150, 255)
ui.append(f)
demo_objects.append(f)
# Label
label = mcrfpy.Caption(easing[:10], x, y - 20)
label.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(label)
demo_objects.append(label)
# Animate with this easing
mcrfpy.Animation("x", float(x + 150), 3.0, easing).start(f)
def demo4_performance():
"""Many simultaneous animations"""
global demo_objects
ui = mcrfpy.sceneUI("demo")
subtitle.text = "Demo 4: 50+ Simultaneous Animations"
for i in range(50):
x = 100 + (i % 10) * 80
y = 150 + (i // 10) * 80
f = mcrfpy.Frame(x, y, 30, 30)
f.fill_color = mcrfpy.Color((i*37)%256, (i*73)%256, (i*113)%256)
ui.append(f)
demo_objects.append(f)
# Animate to random position
target_x = 150 + (i % 8) * 90
target_y = 200 + (i // 8) * 70
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
mcrfpy.Animation("x", float(target_x), 2.5, easing).start(f)
mcrfpy.Animation("y", float(target_y), 2.5, easing).start(f)
def next_demo(runtime):
"""Run the next demo with proper cleanup"""
global current_demo
# First hide old objects
hide_demo_objects()
demos = [
demo1_frame_animations,
demo2_caption_animations,
demo3_easing_showcase,
demo4_performance
]
if current_demo < len(demos):
demos[current_demo]()
current_demo += 1
if current_demo < len(demos):
mcrfpy.setTimer("next", next_demo, int(DEMO_DURATION * 1000))
else:
subtitle.text = "Demo Complete!"
mcrfpy.setTimer("exit", lambda t: mcrfpy.exit(), 2000)
# Initialize
print("Starting Animation Sizzle Reel (Fixed)...")
create_scene()
mcrfpy.setTimer("start", next_demo, 500)

View File

@ -60,12 +60,12 @@ def create_demo():
scene.append(bg)
# Title
title = mcrfpy.Caption(10, 10, "Text Input Widget Demo - Auto Test", font_size=24)
title = mcrfpy.Caption(10, 10, "Text Input Widget Demo - Auto Test")
title.color = (255, 255, 255, 255)
scene.append(title)
# Instructions
instructions = mcrfpy.Caption(10, 50, "This will automatically test the text input system", font_size=14)
instructions = mcrfpy.Caption(10, 50, "This will automatically test the text input system")
instructions.color = (200, 200, 200, 255)
scene.append(instructions)
@ -109,7 +109,7 @@ def create_demo():
fields.append(comment_input)
# Result display
result_text = mcrfpy.Caption(50, 320, "Values will appear here as you type...", font_size=14)
result_text = mcrfpy.Caption(50, 320, "Values will appear here as you type...")
result_text.color = (150, 255, 150, 255)
scene.append(result_text)

View File

@ -79,8 +79,7 @@ class TextInput:
self.label_text = mcrfpy.Caption(
self.x - 5,
self.y - self.font_size - 5,
self.label,
font_size=self.font_size
self.label
)
self.label_text.color = (255, 255, 255, 255)
@ -88,8 +87,7 @@ class TextInput:
self.text_display = mcrfpy.Caption(
self.x + 4,
self.y + 4,
"",
font_size=self.font_size
""
)
self.text_display.color = (0, 0, 0, 255)
@ -260,12 +258,12 @@ def create_demo():
scene.append(bg)
# Title
title = mcrfpy.Caption(10, 10, "Text Input Widget System", font_size=24)
title = mcrfpy.Caption(10, 10, "Text Input Widget System")
title.color = (255, 255, 255, 255)
scene.append(title)
# Instructions
info = mcrfpy.Caption(10, 50, "Click to focus | Tab to switch fields | Type to enter text", font_size=14)
info = mcrfpy.Caption(10, 50, "Click to focus | Tab to switch fields | Type to enter text")
info.color = (200, 200, 200, 255)
scene.append(info)
@ -289,7 +287,7 @@ def create_demo():
comment_input.add_to_scene(scene)
# Status display
status = mcrfpy.Caption(50, 320, "Ready for input...", font_size=14)
status = mcrfpy.Caption(50, 320, "Ready for input...")
status.color = (150, 255, 150, 255)
scene.append(status)

View File

@ -95,8 +95,7 @@ class TextInput:
self.label_text = mcrfpy.Caption(
self.x - 5,
self.y - self.font_size - 5,
self.label,
font_size=self.font_size
self.label
)
self.label_text.color = (255, 255, 255, 255)
@ -104,8 +103,7 @@ class TextInput:
self.text_display = mcrfpy.Caption(
self.x + 4,
self.y + 4,
"",
font_size=self.font_size
""
)
self.text_display.color = (0, 0, 0, 255)
@ -227,12 +225,12 @@ def create_demo():
scene.append(bg)
# Title
title = mcrfpy.Caption(10, 10, "Text Input Widget Demo", font_size=24)
title = mcrfpy.Caption(10, 10, "Text Input Widget Demo")
title.color = (255, 255, 255, 255)
scene.append(title)
# Instructions
instructions = mcrfpy.Caption(10, 50, "Click to focus, Tab to switch fields, Type to enter text", font_size=14)
instructions = mcrfpy.Caption(10, 50, "Click to focus, Tab to switch fields, Type to enter text")
instructions.color = (200, 200, 200, 255)
scene.append(instructions)
@ -276,7 +274,7 @@ def create_demo():
fields.append(comment_input)
# Result display
result_text = mcrfpy.Caption(50, 320, "Type in the fields above...", font_size=14)
result_text = mcrfpy.Caption(50, 320, "Type in the fields above...")
result_text.color = (150, 255, 150, 255)
scene.append(result_text)