Compare commits
11 Commits
98fc49a978
...
c5e7e8e298
Author | SHA1 | Date |
---|---|---|
|
c5e7e8e298 | |
|
6d29652ae7 | |
|
a010e5fa96 | |
|
9c8d6c4591 | |
|
dcd1b0ca33 | |
|
6813fb5129 | |
|
6f67fbb51e | |
|
eb88c7b3aa | |
|
9fb428dd01 | |
|
bde82028b5 | |
|
062e4dadc4 |
|
@ -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*
|
|
@ -0,0 +1,80 @@
|
||||||
|
"""
|
||||||
|
McRogueFace Tutorial - Part 0: Introduction to Scene, Texture, and Grid
|
||||||
|
|
||||||
|
This tutorial introduces the basic building blocks:
|
||||||
|
- Scene: A container for UI elements and game state
|
||||||
|
- Texture: Loading image assets for use in the game
|
||||||
|
- Grid: A tilemap component for rendering tile-based worlds
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Create and activate a new scene
|
||||||
|
mcrfpy.createScene("tutorial")
|
||||||
|
mcrfpy.setScene("tutorial")
|
||||||
|
|
||||||
|
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||||
|
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||||
|
|
||||||
|
# Create a grid of tiles
|
||||||
|
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||||
|
|
||||||
|
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||||
|
|
||||||
|
# calculating the size in pixels to fit the entire grid on-screen
|
||||||
|
zoom = 2.0
|
||||||
|
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||||
|
|
||||||
|
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||||
|
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=grid_position,
|
||||||
|
grid_size=(grid_width, grid_height),
|
||||||
|
texture=texture,
|
||||||
|
size=grid_size, # height and width on screen
|
||||||
|
)
|
||||||
|
|
||||||
|
grid.zoom = zoom
|
||||||
|
grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile
|
||||||
|
|
||||||
|
# Define tile types
|
||||||
|
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||||
|
WALL_TILES = [3, 7, 11]
|
||||||
|
|
||||||
|
# Fill the grid with a simple pattern
|
||||||
|
for y in range(grid_height):
|
||||||
|
for x in range(grid_width):
|
||||||
|
# Create walls around the edges
|
||||||
|
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||||
|
tile_index = random.choice(WALL_TILES)
|
||||||
|
else:
|
||||||
|
# Fill interior with floor tiles
|
||||||
|
tile_index = random.choice(FLOOR_TILES)
|
||||||
|
|
||||||
|
# Set the tile at this position
|
||||||
|
point = grid.at(x, y)
|
||||||
|
if point:
|
||||||
|
point.tilesprite = tile_index
|
||||||
|
|
||||||
|
# Add the grid to the scene
|
||||||
|
mcrfpy.sceneUI("tutorial").append(grid)
|
||||||
|
|
||||||
|
# Add a title caption
|
||||||
|
title = mcrfpy.Caption((320, 10),
|
||||||
|
text="McRogueFace Tutorial - Part 0",
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(title)
|
||||||
|
|
||||||
|
# Add instructions
|
||||||
|
instructions = mcrfpy.Caption((280, 750),
|
||||||
|
text="Scene + Texture + Grid = Tilemap!",
|
||||||
|
)
|
||||||
|
instructions.font_size=18
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||||
|
|
||||||
|
print("Tutorial Part 0 loaded!")
|
||||||
|
print(f"Created a {grid.grid_size[0]}x{grid.grid_size[1]} grid")
|
||||||
|
print(f"Grid positioned at ({grid.x}, {grid.y})")
|
|
@ -0,0 +1,116 @@
|
||||||
|
"""
|
||||||
|
McRogueFace Tutorial - Part 1: Entities and Keyboard Input
|
||||||
|
|
||||||
|
This tutorial builds on Part 0 by adding:
|
||||||
|
- Entity: A game object that can be placed in a grid
|
||||||
|
- Keyboard handling: Responding to key presses to move the entity
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Create and activate a new scene
|
||||||
|
mcrfpy.createScene("tutorial")
|
||||||
|
mcrfpy.setScene("tutorial")
|
||||||
|
|
||||||
|
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||||
|
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||||
|
|
||||||
|
# Load the hero sprite texture (32x32 sprite sheet)
|
||||||
|
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||||
|
|
||||||
|
# Create a grid of tiles
|
||||||
|
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||||
|
|
||||||
|
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||||
|
|
||||||
|
# calculating the size in pixels to fit the entire grid on-screen
|
||||||
|
zoom = 2.0
|
||||||
|
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||||
|
|
||||||
|
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||||
|
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=grid_position,
|
||||||
|
grid_size=(grid_width, grid_height),
|
||||||
|
texture=texture,
|
||||||
|
size=grid_size, # height and width on screen
|
||||||
|
)
|
||||||
|
|
||||||
|
grid.zoom = zoom
|
||||||
|
grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile
|
||||||
|
|
||||||
|
# Define tile types
|
||||||
|
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||||
|
WALL_TILES = [3, 7, 11]
|
||||||
|
|
||||||
|
# Fill the grid with a simple pattern
|
||||||
|
for y in range(grid_height):
|
||||||
|
for x in range(grid_width):
|
||||||
|
# Create walls around the edges
|
||||||
|
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||||
|
tile_index = random.choice(WALL_TILES)
|
||||||
|
else:
|
||||||
|
# Fill interior with floor tiles
|
||||||
|
tile_index = random.choice(FLOOR_TILES)
|
||||||
|
|
||||||
|
# Set the tile at this position
|
||||||
|
point = grid.at(x, y)
|
||||||
|
if point:
|
||||||
|
point.tilesprite = tile_index
|
||||||
|
|
||||||
|
# Add the grid to the scene
|
||||||
|
mcrfpy.sceneUI("tutorial").append(grid)
|
||||||
|
|
||||||
|
# Create a player entity at position (4, 4)
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
(4, 4), # Entity positions are tile coordinates
|
||||||
|
texture=hero_texture,
|
||||||
|
sprite_index=0 # Use the first sprite in the texture
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add the player entity to the grid
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Define keyboard handler
|
||||||
|
def handle_keys(key, state):
|
||||||
|
"""Handle keyboard input to move the player"""
|
||||||
|
if state == "start": # Only respond to key press, not release
|
||||||
|
# Get current player position in grid coordinates
|
||||||
|
px, py = player.x, player.y
|
||||||
|
|
||||||
|
# Calculate new position based on key press
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
py -= 1
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
py += 1
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
px -= 1
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
px += 1
|
||||||
|
|
||||||
|
# Update player position (no collision checking yet)
|
||||||
|
player.x = px
|
||||||
|
player.y = py
|
||||||
|
|
||||||
|
# Register the keyboard handler
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
# Add a title caption
|
||||||
|
title = mcrfpy.Caption((320, 10),
|
||||||
|
text="McRogueFace Tutorial - Part 1",
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(title)
|
||||||
|
|
||||||
|
# Add instructions
|
||||||
|
instructions = mcrfpy.Caption((200, 750),
|
||||||
|
text="Use WASD or Arrow Keys to move the hero!",
|
||||||
|
)
|
||||||
|
instructions.font_size=18
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||||
|
|
||||||
|
print("Tutorial Part 1 loaded!")
|
||||||
|
print(f"Player entity created at grid position (4, 4)")
|
||||||
|
print("Use WASD or Arrow keys to move!")
|
|
@ -0,0 +1,117 @@
|
||||||
|
"""
|
||||||
|
McRogueFace Tutorial - Part 1: Entities and Keyboard Input
|
||||||
|
|
||||||
|
This tutorial builds on Part 0 by adding:
|
||||||
|
- Entity: A game object that can be placed in a grid
|
||||||
|
- Keyboard handling: Responding to key presses to move the entity
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Create and activate a new scene
|
||||||
|
mcrfpy.createScene("tutorial")
|
||||||
|
mcrfpy.setScene("tutorial")
|
||||||
|
|
||||||
|
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||||
|
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||||
|
|
||||||
|
# Load the hero sprite texture (32x32 sprite sheet)
|
||||||
|
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||||
|
|
||||||
|
# Create a grid of tiles
|
||||||
|
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||||
|
|
||||||
|
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||||
|
|
||||||
|
# calculating the size in pixels to fit the entire grid on-screen
|
||||||
|
zoom = 2.0
|
||||||
|
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||||
|
|
||||||
|
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||||
|
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=grid_position,
|
||||||
|
grid_size=(grid_width, grid_height),
|
||||||
|
texture=texture,
|
||||||
|
size=grid_size, # height and width on screen
|
||||||
|
)
|
||||||
|
|
||||||
|
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
||||||
|
|
||||||
|
# Define tile types
|
||||||
|
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||||
|
WALL_TILES = [3, 7, 11]
|
||||||
|
|
||||||
|
# Fill the grid with a simple pattern
|
||||||
|
for y in range(grid_height):
|
||||||
|
for x in range(grid_width):
|
||||||
|
# Create walls around the edges
|
||||||
|
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||||
|
tile_index = random.choice(WALL_TILES)
|
||||||
|
else:
|
||||||
|
# Fill interior with floor tiles
|
||||||
|
tile_index = random.choice(FLOOR_TILES)
|
||||||
|
|
||||||
|
# Set the tile at this position
|
||||||
|
point = grid.at(x, y)
|
||||||
|
if point:
|
||||||
|
point.tilesprite = tile_index
|
||||||
|
|
||||||
|
# Add the grid to the scene
|
||||||
|
mcrfpy.sceneUI("tutorial").append(grid)
|
||||||
|
|
||||||
|
# Create a player entity at position (4, 4)
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
(4, 4), # Entity positions are tile coordinates
|
||||||
|
texture=hero_texture,
|
||||||
|
sprite_index=0 # Use the first sprite in the texture
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add the player entity to the grid
|
||||||
|
grid.entities.append(player)
|
||||||
|
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||||
|
|
||||||
|
# Define keyboard handler
|
||||||
|
def handle_keys(key, state):
|
||||||
|
"""Handle keyboard input to move the player"""
|
||||||
|
if state == "start": # Only respond to key press, not release
|
||||||
|
# Get current player position in grid coordinates
|
||||||
|
px, py = player.x, player.y
|
||||||
|
|
||||||
|
# Calculate new position based on key press
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
py -= 1
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
py += 1
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
px -= 1
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
px += 1
|
||||||
|
|
||||||
|
# Update player position (no collision checking yet)
|
||||||
|
player.x = px
|
||||||
|
player.y = py
|
||||||
|
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||||
|
|
||||||
|
# Register the keyboard handler
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
# Add a title caption
|
||||||
|
title = mcrfpy.Caption((320, 10),
|
||||||
|
text="McRogueFace Tutorial - Part 1",
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(title)
|
||||||
|
|
||||||
|
# Add instructions
|
||||||
|
instructions = mcrfpy.Caption((200, 750),
|
||||||
|
text="Use WASD or Arrow Keys to move the hero!",
|
||||||
|
)
|
||||||
|
instructions.font_size=18
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||||
|
|
||||||
|
print("Tutorial Part 1 loaded!")
|
||||||
|
print(f"Player entity created at grid position (4, 4)")
|
||||||
|
print("Use WASD or Arrow keys to move!")
|
|
@ -0,0 +1,149 @@
|
||||||
|
"""
|
||||||
|
McRogueFace Tutorial - Part 2: Animated Movement
|
||||||
|
|
||||||
|
This tutorial builds on Part 1 by adding:
|
||||||
|
- Animation system for smooth movement
|
||||||
|
- Movement that takes 0.5 seconds per tile
|
||||||
|
- Input blocking during movement animation
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Create and activate a new scene
|
||||||
|
mcrfpy.createScene("tutorial")
|
||||||
|
mcrfpy.setScene("tutorial")
|
||||||
|
|
||||||
|
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||||
|
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||||
|
|
||||||
|
# Load the hero sprite texture (32x32 sprite sheet)
|
||||||
|
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||||
|
|
||||||
|
# Create a grid of tiles
|
||||||
|
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||||
|
|
||||||
|
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||||
|
|
||||||
|
# calculating the size in pixels to fit the entire grid on-screen
|
||||||
|
zoom = 2.0
|
||||||
|
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||||
|
|
||||||
|
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||||
|
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=grid_position,
|
||||||
|
grid_size=(grid_width, grid_height),
|
||||||
|
texture=texture,
|
||||||
|
size=grid_size, # height and width on screen
|
||||||
|
)
|
||||||
|
|
||||||
|
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
||||||
|
|
||||||
|
# Define tile types
|
||||||
|
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||||
|
WALL_TILES = [3, 7, 11]
|
||||||
|
|
||||||
|
# Fill the grid with a simple pattern
|
||||||
|
for y in range(grid_height):
|
||||||
|
for x in range(grid_width):
|
||||||
|
# Create walls around the edges
|
||||||
|
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||||
|
tile_index = random.choice(WALL_TILES)
|
||||||
|
else:
|
||||||
|
# Fill interior with floor tiles
|
||||||
|
tile_index = random.choice(FLOOR_TILES)
|
||||||
|
|
||||||
|
# Set the tile at this position
|
||||||
|
point = grid.at(x, y)
|
||||||
|
if point:
|
||||||
|
point.tilesprite = tile_index
|
||||||
|
|
||||||
|
# Add the grid to the scene
|
||||||
|
mcrfpy.sceneUI("tutorial").append(grid)
|
||||||
|
|
||||||
|
# Create a player entity at position (4, 4)
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
(4, 4), # Entity positions are tile coordinates
|
||||||
|
texture=hero_texture,
|
||||||
|
sprite_index=0 # Use the first sprite in the texture
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add the player entity to the grid
|
||||||
|
grid.entities.append(player)
|
||||||
|
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||||
|
|
||||||
|
# Movement state tracking
|
||||||
|
is_moving = False
|
||||||
|
move_animations = [] # Track active animations
|
||||||
|
|
||||||
|
# Animation completion callback
|
||||||
|
def movement_complete(runtime):
|
||||||
|
"""Called when movement animation completes"""
|
||||||
|
global is_moving
|
||||||
|
is_moving = False
|
||||||
|
# Ensure grid is centered on final position
|
||||||
|
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||||
|
|
||||||
|
motion_speed = 0.30 # seconds per tile
|
||||||
|
# Define keyboard handler
|
||||||
|
def handle_keys(key, state):
|
||||||
|
"""Handle keyboard input to move the player"""
|
||||||
|
global is_moving, move_animations
|
||||||
|
|
||||||
|
if state == "start" and not is_moving: # Only respond to key press when not moving
|
||||||
|
# Get current player position in grid coordinates
|
||||||
|
px, py = player.x, player.y
|
||||||
|
new_x, new_y = px, py
|
||||||
|
|
||||||
|
# Calculate new position based on key press
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
new_y -= 1
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
new_y += 1
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
new_x -= 1
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
new_x += 1
|
||||||
|
|
||||||
|
# If position changed, start movement animation
|
||||||
|
if new_x != px or new_y != py:
|
||||||
|
is_moving = True
|
||||||
|
|
||||||
|
# Create animations for player position
|
||||||
|
anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad")
|
||||||
|
anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
||||||
|
anim_x.start(player)
|
||||||
|
anim_y.start(player)
|
||||||
|
|
||||||
|
# Animate grid center to follow player
|
||||||
|
center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
||||||
|
center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
||||||
|
center_x.start(grid)
|
||||||
|
center_y.start(grid)
|
||||||
|
|
||||||
|
# Set a timer to mark movement as complete
|
||||||
|
mcrfpy.setTimer("move_complete", movement_complete, 500)
|
||||||
|
|
||||||
|
# Register the keyboard handler
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
# Add a title caption
|
||||||
|
title = mcrfpy.Caption((320, 10),
|
||||||
|
text="McRogueFace Tutorial - Part 2",
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(title)
|
||||||
|
|
||||||
|
# Add instructions
|
||||||
|
instructions = mcrfpy.Caption((150, 750),
|
||||||
|
text="Smooth movement! Each step takes 0.5 seconds.",
|
||||||
|
)
|
||||||
|
instructions.font_size=18
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||||
|
|
||||||
|
print("Tutorial Part 2 loaded!")
|
||||||
|
print(f"Player entity created at grid position (4, 4)")
|
||||||
|
print("Movement is now animated over 0.5 seconds per tile!")
|
||||||
|
print("Use WASD or Arrow keys to move!")
|
|
@ -0,0 +1,241 @@
|
||||||
|
"""
|
||||||
|
McRogueFace Tutorial - Part 2: Enhanced with Single Move Queue
|
||||||
|
|
||||||
|
This tutorial builds on Part 2 by adding:
|
||||||
|
- Single queued move system for responsive input
|
||||||
|
- Debug display showing position and queue status
|
||||||
|
- Smooth continuous movement when keys are held
|
||||||
|
- Animation callbacks to prevent race conditions
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Create and activate a new scene
|
||||||
|
mcrfpy.createScene("tutorial")
|
||||||
|
mcrfpy.setScene("tutorial")
|
||||||
|
|
||||||
|
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||||
|
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||||
|
|
||||||
|
# Load the hero sprite texture (32x32 sprite sheet)
|
||||||
|
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||||
|
|
||||||
|
# Create a grid of tiles
|
||||||
|
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||||
|
|
||||||
|
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||||
|
|
||||||
|
# calculating the size in pixels to fit the entire grid on-screen
|
||||||
|
zoom = 2.0
|
||||||
|
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||||
|
|
||||||
|
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||||
|
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=grid_position,
|
||||||
|
grid_size=(grid_width, grid_height),
|
||||||
|
texture=texture,
|
||||||
|
size=grid_size, # height and width on screen
|
||||||
|
)
|
||||||
|
|
||||||
|
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
||||||
|
|
||||||
|
# Define tile types
|
||||||
|
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||||
|
WALL_TILES = [3, 7, 11]
|
||||||
|
|
||||||
|
# Fill the grid with a simple pattern
|
||||||
|
for y in range(grid_height):
|
||||||
|
for x in range(grid_width):
|
||||||
|
# Create walls around the edges
|
||||||
|
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||||
|
tile_index = random.choice(WALL_TILES)
|
||||||
|
else:
|
||||||
|
# Fill interior with floor tiles
|
||||||
|
tile_index = random.choice(FLOOR_TILES)
|
||||||
|
|
||||||
|
# Set the tile at this position
|
||||||
|
point = grid.at(x, y)
|
||||||
|
if point:
|
||||||
|
point.tilesprite = tile_index
|
||||||
|
|
||||||
|
# Add the grid to the scene
|
||||||
|
mcrfpy.sceneUI("tutorial").append(grid)
|
||||||
|
|
||||||
|
# Create a player entity at position (4, 4)
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
(4, 4), # Entity positions are tile coordinates
|
||||||
|
texture=hero_texture,
|
||||||
|
sprite_index=0 # Use the first sprite in the texture
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add the player entity to the grid
|
||||||
|
grid.entities.append(player)
|
||||||
|
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||||
|
|
||||||
|
# Movement state tracking
|
||||||
|
is_moving = False
|
||||||
|
move_queue = [] # List to store queued moves (max 1 item)
|
||||||
|
#last_position = (4, 4) # Track last position
|
||||||
|
current_destination = None # Track where we're currently moving to
|
||||||
|
current_move = None # Track current move direction
|
||||||
|
|
||||||
|
# Store animation references
|
||||||
|
player_anim_x = None
|
||||||
|
player_anim_y = None
|
||||||
|
grid_anim_x = None
|
||||||
|
grid_anim_y = None
|
||||||
|
|
||||||
|
# Debug display caption
|
||||||
|
debug_caption = mcrfpy.Caption((10, 40),
|
||||||
|
text="Last: (4, 4) | Queue: 0 | Dest: None",
|
||||||
|
)
|
||||||
|
debug_caption.font_size = 16
|
||||||
|
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
||||||
|
|
||||||
|
# Additional debug caption for movement state
|
||||||
|
move_debug_caption = mcrfpy.Caption((10, 60),
|
||||||
|
text="Moving: False | Current: None | Queued: None",
|
||||||
|
)
|
||||||
|
move_debug_caption.font_size = 16
|
||||||
|
move_debug_caption.fill_color = mcrfpy.Color(255, 200, 0, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(move_debug_caption)
|
||||||
|
|
||||||
|
def key_to_direction(key):
|
||||||
|
"""Convert key to direction string"""
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
return "Up"
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
return "Down"
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
return "Left"
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
return "Right"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_debug_display():
|
||||||
|
"""Update the debug caption with current state"""
|
||||||
|
queue_count = len(move_queue)
|
||||||
|
dest_text = f"({current_destination[0]}, {current_destination[1]})" if current_destination else "None"
|
||||||
|
debug_caption.text = f"Last: ({player.x}, {player.y}) | Queue: {queue_count} | Dest: {dest_text}"
|
||||||
|
|
||||||
|
# Update movement state debug
|
||||||
|
current_dir = key_to_direction(current_move) if current_move else "None"
|
||||||
|
queued_dir = key_to_direction(move_queue[0]) if move_queue else "None"
|
||||||
|
move_debug_caption.text = f"Moving: {is_moving} | Current: {current_dir} | Queued: {queued_dir}"
|
||||||
|
|
||||||
|
# Animation completion callback
|
||||||
|
def movement_complete(anim, target):
|
||||||
|
"""Called when movement animation completes"""
|
||||||
|
global is_moving, move_queue, current_destination, current_move
|
||||||
|
global player_anim_x, player_anim_y
|
||||||
|
print(f"In callback for animation: {anim=} {target=}")
|
||||||
|
# Clear movement state
|
||||||
|
is_moving = False
|
||||||
|
current_move = None
|
||||||
|
current_destination = None
|
||||||
|
# Clear animation references
|
||||||
|
player_anim_x = None
|
||||||
|
player_anim_y = None
|
||||||
|
|
||||||
|
# Update last position to where we actually are now
|
||||||
|
#last_position = (int(player.x), int(player.y))
|
||||||
|
|
||||||
|
# Ensure grid is centered on final position
|
||||||
|
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||||
|
|
||||||
|
# Check if there's a queued move
|
||||||
|
if move_queue:
|
||||||
|
# Pop the next move from the queue
|
||||||
|
next_move = move_queue.pop(0)
|
||||||
|
print(f"Processing queued move: {next_move}")
|
||||||
|
# Process it like a fresh input
|
||||||
|
process_move(next_move)
|
||||||
|
|
||||||
|
update_debug_display()
|
||||||
|
|
||||||
|
motion_speed = 0.30 # seconds per tile
|
||||||
|
|
||||||
|
def process_move(key):
|
||||||
|
"""Process a move based on the key"""
|
||||||
|
global is_moving, current_move, current_destination, move_queue
|
||||||
|
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
||||||
|
|
||||||
|
# If already moving, just update the queue
|
||||||
|
if is_moving:
|
||||||
|
print(f"process_move processing {key=} as a queued move (is_moving = True)")
|
||||||
|
# Clear queue and add new move (only keep 1 queued move)
|
||||||
|
move_queue.clear()
|
||||||
|
move_queue.append(key)
|
||||||
|
update_debug_display()
|
||||||
|
return
|
||||||
|
print(f"process_move processing {key=} as a new, immediate animation (is_moving = False)")
|
||||||
|
# Calculate new position from current position
|
||||||
|
px, py = int(player.x), int(player.y)
|
||||||
|
new_x, new_y = px, py
|
||||||
|
|
||||||
|
# Calculate new position based on key press (only one tile movement)
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
new_y -= 1
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
new_y += 1
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
new_x -= 1
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
new_x += 1
|
||||||
|
|
||||||
|
# Start the move if position changed
|
||||||
|
if new_x != px or new_y != py:
|
||||||
|
is_moving = True
|
||||||
|
current_move = key
|
||||||
|
current_destination = (new_x, new_y)
|
||||||
|
# only animate a single axis, same callback from either
|
||||||
|
if new_x != px:
|
||||||
|
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
||||||
|
player_anim_x.start(player)
|
||||||
|
elif new_y != py:
|
||||||
|
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad", callback=movement_complete)
|
||||||
|
player_anim_y.start(player)
|
||||||
|
|
||||||
|
# Animate grid center to follow player
|
||||||
|
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
||||||
|
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
||||||
|
grid_anim_x.start(grid)
|
||||||
|
grid_anim_y.start(grid)
|
||||||
|
|
||||||
|
update_debug_display()
|
||||||
|
|
||||||
|
# Define keyboard handler
|
||||||
|
def handle_keys(key, state):
|
||||||
|
"""Handle keyboard input to move the player"""
|
||||||
|
if state == "start":
|
||||||
|
# Only process movement keys
|
||||||
|
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
||||||
|
print(f"handle_keys producing actual input: {key=}")
|
||||||
|
process_move(key)
|
||||||
|
|
||||||
|
|
||||||
|
# Register the keyboard handler
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
# Add a title caption
|
||||||
|
title = mcrfpy.Caption((320, 10),
|
||||||
|
text="McRogueFace Tutorial - Part 2 Enhanced",
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(title)
|
||||||
|
|
||||||
|
# Add instructions
|
||||||
|
instructions = mcrfpy.Caption((150, 750),
|
||||||
|
text="One-move queue system with animation callbacks!",
|
||||||
|
)
|
||||||
|
instructions.font_size=18
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||||
|
|
||||||
|
print("Tutorial Part 2 Enhanced loaded!")
|
||||||
|
print(f"Player entity created at grid position (4, 4)")
|
||||||
|
print("Movement now uses animation callbacks to prevent race conditions!")
|
||||||
|
print("Use WASD or Arrow keys to move!")
|
|
@ -0,0 +1,149 @@
|
||||||
|
"""
|
||||||
|
McRogueFace Tutorial - Part 2: Animated Movement
|
||||||
|
|
||||||
|
This tutorial builds on Part 1 by adding:
|
||||||
|
- Animation system for smooth movement
|
||||||
|
- Movement that takes 0.5 seconds per tile
|
||||||
|
- Input blocking during movement animation
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Create and activate a new scene
|
||||||
|
mcrfpy.createScene("tutorial")
|
||||||
|
mcrfpy.setScene("tutorial")
|
||||||
|
|
||||||
|
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||||
|
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||||
|
|
||||||
|
# Load the hero sprite texture (32x32 sprite sheet)
|
||||||
|
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||||
|
|
||||||
|
# Create a grid of tiles
|
||||||
|
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||||
|
|
||||||
|
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||||
|
|
||||||
|
# calculating the size in pixels to fit the entire grid on-screen
|
||||||
|
zoom = 2.0
|
||||||
|
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||||
|
|
||||||
|
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||||
|
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=grid_position,
|
||||||
|
grid_size=(grid_width, grid_height),
|
||||||
|
texture=texture,
|
||||||
|
size=grid_size, # height and width on screen
|
||||||
|
)
|
||||||
|
|
||||||
|
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
||||||
|
|
||||||
|
# Define tile types
|
||||||
|
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||||
|
WALL_TILES = [3, 7, 11]
|
||||||
|
|
||||||
|
# Fill the grid with a simple pattern
|
||||||
|
for y in range(grid_height):
|
||||||
|
for x in range(grid_width):
|
||||||
|
# Create walls around the edges
|
||||||
|
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||||
|
tile_index = random.choice(WALL_TILES)
|
||||||
|
else:
|
||||||
|
# Fill interior with floor tiles
|
||||||
|
tile_index = random.choice(FLOOR_TILES)
|
||||||
|
|
||||||
|
# Set the tile at this position
|
||||||
|
point = grid.at(x, y)
|
||||||
|
if point:
|
||||||
|
point.tilesprite = tile_index
|
||||||
|
|
||||||
|
# Add the grid to the scene
|
||||||
|
mcrfpy.sceneUI("tutorial").append(grid)
|
||||||
|
|
||||||
|
# Create a player entity at position (4, 4)
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
(4, 4), # Entity positions are tile coordinates
|
||||||
|
texture=hero_texture,
|
||||||
|
sprite_index=0 # Use the first sprite in the texture
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add the player entity to the grid
|
||||||
|
grid.entities.append(player)
|
||||||
|
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||||
|
|
||||||
|
# Movement state tracking
|
||||||
|
is_moving = False
|
||||||
|
move_animations = [] # Track active animations
|
||||||
|
|
||||||
|
# Animation completion callback
|
||||||
|
def movement_complete(runtime):
|
||||||
|
"""Called when movement animation completes"""
|
||||||
|
global is_moving
|
||||||
|
is_moving = False
|
||||||
|
# Ensure grid is centered on final position
|
||||||
|
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||||
|
|
||||||
|
motion_speed = 0.30 # seconds per tile
|
||||||
|
# Define keyboard handler
|
||||||
|
def handle_keys(key, state):
|
||||||
|
"""Handle keyboard input to move the player"""
|
||||||
|
global is_moving, move_animations
|
||||||
|
|
||||||
|
if state == "start" and not is_moving: # Only respond to key press when not moving
|
||||||
|
# Get current player position in grid coordinates
|
||||||
|
px, py = player.x, player.y
|
||||||
|
new_x, new_y = px, py
|
||||||
|
|
||||||
|
# Calculate new position based on key press
|
||||||
|
if key == "W" or key == "Up":
|
||||||
|
new_y -= 1
|
||||||
|
elif key == "S" or key == "Down":
|
||||||
|
new_y += 1
|
||||||
|
elif key == "A" or key == "Left":
|
||||||
|
new_x -= 1
|
||||||
|
elif key == "D" or key == "Right":
|
||||||
|
new_x += 1
|
||||||
|
|
||||||
|
# If position changed, start movement animation
|
||||||
|
if new_x != px or new_y != py:
|
||||||
|
is_moving = True
|
||||||
|
|
||||||
|
# Create animations for player position
|
||||||
|
anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad")
|
||||||
|
anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
||||||
|
anim_x.start(player)
|
||||||
|
anim_y.start(player)
|
||||||
|
|
||||||
|
# Animate grid center to follow player
|
||||||
|
center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
||||||
|
center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
||||||
|
center_x.start(grid)
|
||||||
|
center_y.start(grid)
|
||||||
|
|
||||||
|
# Set a timer to mark movement as complete
|
||||||
|
mcrfpy.setTimer("move_complete", movement_complete, 500)
|
||||||
|
|
||||||
|
# Register the keyboard handler
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
# Add a title caption
|
||||||
|
title = mcrfpy.Caption((320, 10),
|
||||||
|
text="McRogueFace Tutorial - Part 2",
|
||||||
|
)
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(title)
|
||||||
|
|
||||||
|
# Add instructions
|
||||||
|
instructions = mcrfpy.Caption((150, 750),
|
||||||
|
"Smooth movement! Each step takes 0.5 seconds.",
|
||||||
|
)
|
||||||
|
instructions.font_size=18
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||||
|
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||||
|
|
||||||
|
print("Tutorial Part 2 loaded!")
|
||||||
|
print(f"Player entity created at grid position (4, 4)")
|
||||||
|
print("Movement is now animated over 0.5 seconds per tile!")
|
||||||
|
print("Use WASD or Arrow keys to move!")
|
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -1,6 +1,8 @@
|
||||||
#include "Animation.h"
|
#include "Animation.h"
|
||||||
#include "UIDrawable.h"
|
#include "UIDrawable.h"
|
||||||
#include "UIEntity.h"
|
#include "UIEntity.h"
|
||||||
|
#include "PyAnimation.h"
|
||||||
|
#include "McRFPy_API.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
@ -9,75 +11,100 @@
|
||||||
#define M_PI 3.14159265358979323846
|
#define M_PI 3.14159265358979323846
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Forward declaration of PyAnimation type
|
||||||
|
namespace mcrfpydef {
|
||||||
|
extern PyTypeObject PyAnimationType;
|
||||||
|
}
|
||||||
|
|
||||||
// Animation implementation
|
// Animation implementation
|
||||||
Animation::Animation(const std::string& targetProperty,
|
Animation::Animation(const std::string& targetProperty,
|
||||||
const AnimationValue& targetValue,
|
const AnimationValue& targetValue,
|
||||||
float duration,
|
float duration,
|
||||||
EasingFunction easingFunc,
|
EasingFunction easingFunc,
|
||||||
bool delta)
|
bool delta,
|
||||||
|
PyObject* callback)
|
||||||
: targetProperty(targetProperty)
|
: targetProperty(targetProperty)
|
||||||
, targetValue(targetValue)
|
, targetValue(targetValue)
|
||||||
, duration(duration)
|
, duration(duration)
|
||||||
, easingFunc(easingFunc)
|
, easingFunc(easingFunc)
|
||||||
, delta(delta)
|
, delta(delta)
|
||||||
|
, pythonCallback(callback)
|
||||||
{
|
{
|
||||||
|
// Increase reference count for Python callback
|
||||||
|
if (pythonCallback) {
|
||||||
|
Py_INCREF(pythonCallback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Animation::start(UIDrawable* target) {
|
Animation::~Animation() {
|
||||||
currentTarget = target;
|
// 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;
|
elapsed = 0.0f;
|
||||||
|
callbackTriggered = false; // Reset callback state
|
||||||
|
|
||||||
// Capture startValue from target based on targetProperty
|
// Capture start value from target
|
||||||
if (!currentTarget) return;
|
std::visit([this, &target](const auto& targetVal) {
|
||||||
|
|
||||||
// Try to get the current value based on the expected type
|
|
||||||
std::visit([this](const auto& targetVal) {
|
|
||||||
using T = std::decay_t<decltype(targetVal)>;
|
using T = std::decay_t<decltype(targetVal)>;
|
||||||
|
|
||||||
if constexpr (std::is_same_v<T, float>) {
|
if constexpr (std::is_same_v<T, float>) {
|
||||||
float value;
|
float value;
|
||||||
if (currentTarget->getProperty(targetProperty, value)) {
|
if (target->getProperty(targetProperty, value)) {
|
||||||
startValue = value;
|
startValue = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if constexpr (std::is_same_v<T, int>) {
|
else if constexpr (std::is_same_v<T, int>) {
|
||||||
int value;
|
int value;
|
||||||
if (currentTarget->getProperty(targetProperty, value)) {
|
if (target->getProperty(targetProperty, value)) {
|
||||||
startValue = value;
|
startValue = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if constexpr (std::is_same_v<T, std::vector<int>>) {
|
else if constexpr (std::is_same_v<T, std::vector<int>>) {
|
||||||
// For sprite animation, get current sprite index
|
// For sprite animation, get current sprite index
|
||||||
int value;
|
int value;
|
||||||
if (currentTarget->getProperty(targetProperty, value)) {
|
if (target->getProperty(targetProperty, value)) {
|
||||||
startValue = value;
|
startValue = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
else if constexpr (std::is_same_v<T, sf::Color>) {
|
||||||
sf::Color value;
|
sf::Color value;
|
||||||
if (currentTarget->getProperty(targetProperty, value)) {
|
if (target->getProperty(targetProperty, value)) {
|
||||||
startValue = value;
|
startValue = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
||||||
sf::Vector2f value;
|
sf::Vector2f value;
|
||||||
if (currentTarget->getProperty(targetProperty, value)) {
|
if (target->getProperty(targetProperty, value)) {
|
||||||
startValue = value;
|
startValue = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if constexpr (std::is_same_v<T, std::string>) {
|
else if constexpr (std::is_same_v<T, std::string>) {
|
||||||
std::string value;
|
std::string value;
|
||||||
if (currentTarget->getProperty(targetProperty, value)) {
|
if (target->getProperty(targetProperty, value)) {
|
||||||
startValue = value;
|
startValue = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, targetValue);
|
}, targetValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Animation::startEntity(UIEntity* target) {
|
void Animation::startEntity(std::shared_ptr<UIEntity> target) {
|
||||||
currentEntityTarget = target;
|
if (!target) return;
|
||||||
currentTarget = nullptr; // Clear drawable target
|
|
||||||
|
entityTargetWeak = target;
|
||||||
elapsed = 0.0f;
|
elapsed = 0.0f;
|
||||||
|
callbackTriggered = false; // Reset callback state
|
||||||
|
|
||||||
// Capture the starting value from the entity
|
// Capture the starting value from the entity
|
||||||
std::visit([this, target](const auto& val) {
|
std::visit([this, target](const auto& val) {
|
||||||
|
@ -99,8 +126,49 @@ void Animation::startEntity(UIEntity* target) {
|
||||||
}, targetValue);
|
}, 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) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,39 +182,18 @@ bool Animation::update(float deltaTime) {
|
||||||
// Get interpolated value
|
// Get interpolated value
|
||||||
AnimationValue currentValue = interpolate(easedT);
|
AnimationValue currentValue = interpolate(easedT);
|
||||||
|
|
||||||
// Apply currentValue to target (either drawable or entity)
|
// Apply to whichever target is valid
|
||||||
std::visit([this](const auto& value) {
|
if (target) {
|
||||||
using T = std::decay_t<decltype(value)>;
|
applyValue(target.get(), currentValue);
|
||||||
|
} else if (entity) {
|
||||||
|
applyValue(entity.get(), currentValue);
|
||||||
|
}
|
||||||
|
|
||||||
if (currentTarget) {
|
// Trigger callback when animation completes
|
||||||
// Handle UIDrawable targets
|
// Check pythonCallback again in case it was cleared during update
|
||||||
if constexpr (std::is_same_v<T, float>) {
|
if (isComplete() && !callbackTriggered && pythonCallback) {
|
||||||
currentTarget->setProperty(targetProperty, value);
|
triggerCallback();
|
||||||
}
|
}
|
||||||
else if constexpr (std::is_same_v<T, int>) {
|
|
||||||
currentTarget->setProperty(targetProperty, value);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
|
||||||
currentTarget->setProperty(targetProperty, value);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
|
||||||
currentTarget->setProperty(targetProperty, value);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, std::string>) {
|
|
||||||
currentTarget->setProperty(targetProperty, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (currentEntityTarget) {
|
|
||||||
// Handle UIEntity targets
|
|
||||||
if constexpr (std::is_same_v<T, float>) {
|
|
||||||
currentEntityTarget->setProperty(targetProperty, value);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, int>) {
|
|
||||||
currentEntityTarget->setProperty(targetProperty, value);
|
|
||||||
}
|
|
||||||
// Entities don't support other types yet
|
|
||||||
}
|
|
||||||
}, currentValue);
|
|
||||||
|
|
||||||
return !isComplete();
|
return !isComplete();
|
||||||
}
|
}
|
||||||
|
@ -254,6 +301,77 @@ AnimationValue Animation::interpolate(float t) const {
|
||||||
}, targetValue);
|
}, 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
|
// Easing functions implementation
|
||||||
namespace EasingFunctions {
|
namespace EasingFunctions {
|
||||||
|
|
||||||
|
@ -502,26 +620,50 @@ AnimationManager& AnimationManager::getInstance() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) {
|
void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) {
|
||||||
|
if (animation && animation->hasValidTarget()) {
|
||||||
|
if (isUpdating) {
|
||||||
|
// Defer adding during update to avoid iterator invalidation
|
||||||
|
pendingAnimations.push_back(animation);
|
||||||
|
} else {
|
||||||
activeAnimations.push_back(animation);
|
activeAnimations.push_back(animation);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AnimationManager::update(float deltaTime) {
|
void AnimationManager::update(float deltaTime) {
|
||||||
for (auto& anim : activeAnimations) {
|
// Set flag to defer new animations
|
||||||
anim->update(deltaTime);
|
isUpdating = true;
|
||||||
}
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AnimationManager::cleanup() {
|
// Remove completed or invalid animations
|
||||||
activeAnimations.erase(
|
activeAnimations.erase(
|
||||||
std::remove_if(activeAnimations.begin(), activeAnimations.end(),
|
std::remove_if(activeAnimations.begin(), activeAnimations.end(),
|
||||||
[](const std::shared_ptr<Animation>& anim) {
|
[deltaTime](std::shared_ptr<Animation>& anim) {
|
||||||
return anim->isComplete();
|
return !anim || !anim->update(deltaTime);
|
||||||
}),
|
}),
|
||||||
activeAnimations.end()
|
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();
|
activeAnimations.clear();
|
||||||
}
|
}
|
|
@ -6,6 +6,7 @@
|
||||||
#include <variant>
|
#include <variant>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <SFML/Graphics.hpp>
|
#include <SFML/Graphics.hpp>
|
||||||
|
#include "Python.h"
|
||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
class UIDrawable;
|
class UIDrawable;
|
||||||
|
@ -36,13 +37,20 @@ public:
|
||||||
const AnimationValue& targetValue,
|
const AnimationValue& targetValue,
|
||||||
float duration,
|
float duration,
|
||||||
EasingFunction easingFunc = EasingFunctions::linear,
|
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
|
// 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)
|
// 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)
|
// Update animation (called each frame)
|
||||||
// Returns true if animation is still running, false if complete
|
// Returns true if animation is still running, false if complete
|
||||||
|
@ -51,6 +59,12 @@ public:
|
||||||
// Get current interpolated value
|
// Get current interpolated value
|
||||||
AnimationValue getCurrentValue() const;
|
AnimationValue getCurrentValue() const;
|
||||||
|
|
||||||
|
// Check if animation has valid target
|
||||||
|
bool hasValidTarget() const;
|
||||||
|
|
||||||
|
// Clear the callback (called when PyAnimation is deallocated)
|
||||||
|
void clearCallback();
|
||||||
|
|
||||||
// Animation properties
|
// Animation properties
|
||||||
std::string getTargetProperty() const { return targetProperty; }
|
std::string getTargetProperty() const { return targetProperty; }
|
||||||
float getDuration() const { return duration; }
|
float getDuration() const { return duration; }
|
||||||
|
@ -67,11 +81,24 @@ private:
|
||||||
EasingFunction easingFunc; // Easing function to use
|
EasingFunction easingFunc; // Easing function to use
|
||||||
bool delta; // If true, targetValue is relative to start
|
bool delta; // If true, targetValue is relative to start
|
||||||
|
|
||||||
UIDrawable* currentTarget = nullptr; // Current target being animated
|
// RAII: Use weak_ptr for safe target tracking
|
||||||
UIEntity* currentEntityTarget = nullptr; // Current entity target (alternative to drawable)
|
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
|
// Helper to interpolate between values
|
||||||
AnimationValue interpolate(float t) const;
|
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
|
// Easing functions library
|
||||||
|
@ -134,13 +161,12 @@ public:
|
||||||
// Update all animations
|
// Update all animations
|
||||||
void update(float deltaTime);
|
void update(float deltaTime);
|
||||||
|
|
||||||
// Remove completed animations
|
// Clear all animations (optionally completing them first)
|
||||||
void cleanup();
|
void clear(bool completeAnimations = false);
|
||||||
|
|
||||||
// Clear all animations
|
|
||||||
void clear();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
AnimationManager() = default;
|
AnimationManager() = default;
|
||||||
std::vector<std::shared_ptr<Animation>> activeAnimations;
|
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
|
||||||
};
|
};
|
|
@ -16,7 +16,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
||||||
{
|
{
|
||||||
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
|
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
|
||||||
Resources::game = this;
|
Resources::game = this;
|
||||||
window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine";
|
window_title = "McRogueFace Engine";
|
||||||
|
|
||||||
// Initialize rendering based on headless mode
|
// Initialize rendering based on headless mode
|
||||||
if (headless) {
|
if (headless) {
|
||||||
|
@ -91,6 +91,9 @@ void GameEngine::cleanup()
|
||||||
if (cleaned_up) return;
|
if (cleaned_up) return;
|
||||||
cleaned_up = true;
|
cleaned_up = true;
|
||||||
|
|
||||||
|
// Clear all animations first (RAII handles invalidation)
|
||||||
|
AnimationManager::getInstance().clear();
|
||||||
|
|
||||||
// Clear Python references before destroying C++ objects
|
// Clear Python references before destroying C++ objects
|
||||||
// Clear all timers (they hold Python callables)
|
// Clear all timers (they hold Python callables)
|
||||||
timers.clear();
|
timers.clear();
|
||||||
|
@ -182,7 +185,7 @@ void GameEngine::setWindowScale(float multiplier)
|
||||||
|
|
||||||
void GameEngine::run()
|
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;
|
float fps = 0.0;
|
||||||
frameTime = 0.016f; // Initialize to ~60 FPS
|
frameTime = 0.016f; // Initialize to ~60 FPS
|
||||||
clock.restart();
|
clock.restart();
|
||||||
|
@ -259,7 +262,7 @@ void GameEngine::run()
|
||||||
int tenth_fps = (metrics.fps * 10) % 10;
|
int tenth_fps = (metrics.fps * 10) % 10;
|
||||||
|
|
||||||
if (!headless && window) {
|
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
|
// In windowed mode, check if window was closed
|
||||||
|
|
|
@ -18,19 +18,31 @@ PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds
|
||||||
}
|
}
|
||||||
|
|
||||||
int PyAnimation::init(PyAnimationObject* self, 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;
|
const char* property_name;
|
||||||
PyObject* target_value;
|
PyObject* target_value;
|
||||||
float duration;
|
float duration;
|
||||||
const char* easing_name = "linear";
|
const char* easing_name = "linear";
|
||||||
int delta = 0;
|
int delta = 0;
|
||||||
|
PyObject* callback = nullptr;
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|sp", const_cast<char**>(keywords),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast<char**>(keywords),
|
||||||
&property_name, &target_value, &duration, &easing_name, &delta)) {
|
&property_name, &target_value, &duration, &easing_name, &delta, &callback)) {
|
||||||
return -1;
|
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
|
// Convert Python target value to AnimationValue
|
||||||
AnimationValue animValue;
|
AnimationValue animValue;
|
||||||
|
|
||||||
|
@ -90,7 +102,7 @@ int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
|
||||||
EasingFunction easingFunc = EasingFunctions::getByName(easing_name);
|
EasingFunction easingFunc = EasingFunctions::getByName(easing_name);
|
||||||
|
|
||||||
// Create the Animation
|
// 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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -126,50 +138,50 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the UIDrawable from the Python object
|
|
||||||
UIDrawable* drawable = nullptr;
|
|
||||||
|
|
||||||
// Check type by comparing type names
|
// Check type by comparing type names
|
||||||
const char* type_name = Py_TYPE(target_obj)->tp_name;
|
const char* type_name = Py_TYPE(target_obj)->tp_name;
|
||||||
|
|
||||||
if (strcmp(type_name, "mcrfpy.Frame") == 0) {
|
if (strcmp(type_name, "mcrfpy.Frame") == 0) {
|
||||||
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj;
|
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) {
|
else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
|
||||||
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
|
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) {
|
else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
|
||||||
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
|
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) {
|
else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
|
||||||
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
|
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) {
|
else if (strcmp(type_name, "mcrfpy.Entity") == 0) {
|
||||||
// Special handling for Entity since it doesn't inherit from UIDrawable
|
// Special handling for Entity since it doesn't inherit from UIDrawable
|
||||||
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
|
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
|
||||||
// Start the animation directly on the entity
|
if (entity->data) {
|
||||||
self->data->startEntity(entity->data.get());
|
self->data->startEntity(entity->data);
|
||||||
|
|
||||||
// Add to AnimationManager
|
|
||||||
AnimationManager::getInstance().addAnimation(self->data);
|
AnimationManager::getInstance().addAnimation(self->data);
|
||||||
|
}
|
||||||
Py_RETURN_NONE;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity");
|
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the animation
|
|
||||||
self->data->start(drawable);
|
|
||||||
|
|
||||||
// Add to AnimationManager
|
|
||||||
AnimationManager::getInstance().addAnimation(self->data);
|
|
||||||
|
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,6 +226,20 @@ PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args
|
||||||
}, value);
|
}, 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[] = {
|
PyGetSetDef PyAnimation::getsetters[] = {
|
||||||
{"property", (getter)get_property, NULL, "Target property name", NULL},
|
{"property", (getter)get_property, NULL, "Target property name", NULL},
|
||||||
{"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL},
|
{"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL},
|
||||||
|
@ -225,10 +251,23 @@ PyGetSetDef PyAnimation::getsetters[] = {
|
||||||
|
|
||||||
PyMethodDef PyAnimation::methods[] = {
|
PyMethodDef PyAnimation::methods[] = {
|
||||||
{"start", (PyCFunction)start, METH_VARARGS,
|
{"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", (PyCFunction)update, METH_VARARGS,
|
||||||
"Update the animation by deltaTime (returns True if still running)"},
|
"Update the animation by deltaTime (returns True if still running)"},
|
||||||
{"get_current_value", (PyCFunction)get_current_value, METH_NOARGS,
|
{"get_current_value", (PyCFunction)get_current_value, METH_NOARGS,
|
||||||
"Get the current interpolated value"},
|
"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}
|
{NULL}
|
||||||
};
|
};
|
|
@ -28,6 +28,8 @@ public:
|
||||||
static PyObject* start(PyAnimationObject* self, PyObject* args);
|
static PyObject* start(PyAnimationObject* self, PyObject* args);
|
||||||
static PyObject* update(PyAnimationObject* self, PyObject* args);
|
static PyObject* update(PyAnimationObject* self, PyObject* args);
|
||||||
static PyObject* get_current_value(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 PyGetSetDef getsetters[];
|
||||||
static PyMethodDef methods[];
|
static PyMethodDef methods[];
|
||||||
|
|
|
@ -1,410 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include "Python.h"
|
|
||||||
#include "PyVector.h"
|
|
||||||
#include "PyColor.h"
|
|
||||||
#include <SFML/Graphics.hpp>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
// Unified argument parsing helpers for Python API consistency
|
|
||||||
namespace PyArgHelpers {
|
|
||||||
|
|
||||||
// Position in pixels (float)
|
|
||||||
struct PositionResult {
|
|
||||||
float x, y;
|
|
||||||
bool valid;
|
|
||||||
const char* error;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Size in pixels (float)
|
|
||||||
struct SizeResult {
|
|
||||||
float w, h;
|
|
||||||
bool valid;
|
|
||||||
const char* error;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Grid position in tiles (float - for animation)
|
|
||||||
struct GridPositionResult {
|
|
||||||
float grid_x, grid_y;
|
|
||||||
bool valid;
|
|
||||||
const char* error;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Grid size in tiles (int - can't have fractional tiles)
|
|
||||||
struct GridSizeResult {
|
|
||||||
int grid_w, grid_h;
|
|
||||||
bool valid;
|
|
||||||
const char* error;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Color parsing
|
|
||||||
struct ColorResult {
|
|
||||||
sf::Color color;
|
|
||||||
bool valid;
|
|
||||||
const char* error;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper to check if a keyword conflicts with positional args
|
|
||||||
static bool hasConflict(PyObject* kwds, const char* key, bool has_positional) {
|
|
||||||
if (!kwds || !has_positional) return false;
|
|
||||||
PyObject* value = PyDict_GetItemString(kwds, key);
|
|
||||||
return value != nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse position with conflict detection
|
|
||||||
static PositionResult parsePosition(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
|
|
||||||
PositionResult result = {0.0f, 0.0f, false, nullptr};
|
|
||||||
int start_idx = next_arg ? *next_arg : 0;
|
|
||||||
bool has_positional = false;
|
|
||||||
|
|
||||||
// Check for positional tuple argument first
|
|
||||||
if (args && PyTuple_Size(args) > start_idx) {
|
|
||||||
PyObject* first = PyTuple_GetItem(args, start_idx);
|
|
||||||
|
|
||||||
// Is it a tuple/Vector?
|
|
||||||
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
|
|
||||||
// Extract from tuple
|
|
||||||
PyObject* x_obj = PyTuple_GetItem(first, 0);
|
|
||||||
PyObject* y_obj = PyTuple_GetItem(first, 1);
|
|
||||||
|
|
||||||
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
|
|
||||||
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
|
|
||||||
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
|
|
||||||
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
|
|
||||||
result.valid = true;
|
|
||||||
has_positional = true;
|
|
||||||
if (next_arg) (*next_arg)++;
|
|
||||||
}
|
|
||||||
} else if (PyObject_TypeCheck(first, (PyTypeObject*)PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
|
||||||
// It's a Vector object
|
|
||||||
PyVectorObject* vec = (PyVectorObject*)first;
|
|
||||||
result.x = vec->data.x;
|
|
||||||
result.y = vec->data.y;
|
|
||||||
result.valid = true;
|
|
||||||
has_positional = true;
|
|
||||||
if (next_arg) (*next_arg)++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for keyword conflicts
|
|
||||||
if (has_positional) {
|
|
||||||
if (hasConflict(kwds, "pos", true) || hasConflict(kwds, "x", true) || hasConflict(kwds, "y", true)) {
|
|
||||||
result.valid = false;
|
|
||||||
result.error = "position specified both positionally and by keyword";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no positional, try keywords
|
|
||||||
if (!has_positional && kwds) {
|
|
||||||
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
|
|
||||||
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
|
|
||||||
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
|
|
||||||
|
|
||||||
// Check for conflicts between pos and x/y
|
|
||||||
if (pos_obj && (x_obj || y_obj)) {
|
|
||||||
result.valid = false;
|
|
||||||
result.error = "pos and x/y cannot both be specified";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pos_obj) {
|
|
||||||
// Parse pos keyword
|
|
||||||
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
|
||||||
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
|
||||||
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
|
||||||
|
|
||||||
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
|
||||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
|
||||||
result.x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
|
||||||
result.y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
|
||||||
result.valid = true;
|
|
||||||
}
|
|
||||||
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
|
||||||
PyVectorObject* vec = (PyVectorObject*)pos_obj;
|
|
||||||
result.x = vec->data.x;
|
|
||||||
result.y = vec->data.y;
|
|
||||||
result.valid = true;
|
|
||||||
}
|
|
||||||
} else if (x_obj && y_obj) {
|
|
||||||
// Parse x, y keywords
|
|
||||||
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
|
|
||||||
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
|
|
||||||
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
|
|
||||||
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
|
|
||||||
result.valid = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse size with conflict detection
|
|
||||||
static SizeResult parseSize(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
|
|
||||||
SizeResult result = {0.0f, 0.0f, false, nullptr};
|
|
||||||
int start_idx = next_arg ? *next_arg : 0;
|
|
||||||
bool has_positional = false;
|
|
||||||
|
|
||||||
// Check for positional tuple argument
|
|
||||||
if (args && PyTuple_Size(args) > start_idx) {
|
|
||||||
PyObject* first = PyTuple_GetItem(args, start_idx);
|
|
||||||
|
|
||||||
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
|
|
||||||
PyObject* w_obj = PyTuple_GetItem(first, 0);
|
|
||||||
PyObject* h_obj = PyTuple_GetItem(first, 1);
|
|
||||||
|
|
||||||
if ((PyFloat_Check(w_obj) || PyLong_Check(w_obj)) &&
|
|
||||||
(PyFloat_Check(h_obj) || PyLong_Check(h_obj))) {
|
|
||||||
result.w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : PyLong_AsLong(w_obj);
|
|
||||||
result.h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : PyLong_AsLong(h_obj);
|
|
||||||
result.valid = true;
|
|
||||||
has_positional = true;
|
|
||||||
if (next_arg) (*next_arg)++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for keyword conflicts
|
|
||||||
if (has_positional) {
|
|
||||||
if (hasConflict(kwds, "size", true) || hasConflict(kwds, "w", true) || hasConflict(kwds, "h", true)) {
|
|
||||||
result.valid = false;
|
|
||||||
result.error = "size specified both positionally and by keyword";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no positional, try keywords
|
|
||||||
if (!has_positional && kwds) {
|
|
||||||
PyObject* size_obj = PyDict_GetItemString(kwds, "size");
|
|
||||||
PyObject* w_obj = PyDict_GetItemString(kwds, "w");
|
|
||||||
PyObject* h_obj = PyDict_GetItemString(kwds, "h");
|
|
||||||
|
|
||||||
// Check for conflicts between size and w/h
|
|
||||||
if (size_obj && (w_obj || h_obj)) {
|
|
||||||
result.valid = false;
|
|
||||||
result.error = "size and w/h cannot both be specified";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size_obj) {
|
|
||||||
// Parse size keyword
|
|
||||||
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
|
||||||
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
|
||||||
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
|
||||||
|
|
||||||
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
|
|
||||||
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
|
||||||
result.w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
|
|
||||||
result.h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
|
||||||
result.valid = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (w_obj && h_obj) {
|
|
||||||
// Parse w, h keywords
|
|
||||||
if ((PyFloat_Check(w_obj) || PyLong_Check(w_obj)) &&
|
|
||||||
(PyFloat_Check(h_obj) || PyLong_Check(h_obj))) {
|
|
||||||
result.w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : PyLong_AsLong(w_obj);
|
|
||||||
result.h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : PyLong_AsLong(h_obj);
|
|
||||||
result.valid = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse grid position (float for smooth animation)
|
|
||||||
static GridPositionResult parseGridPosition(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
|
|
||||||
GridPositionResult result = {0.0f, 0.0f, false, nullptr};
|
|
||||||
int start_idx = next_arg ? *next_arg : 0;
|
|
||||||
bool has_positional = false;
|
|
||||||
|
|
||||||
// Check for positional tuple argument
|
|
||||||
if (args && PyTuple_Size(args) > start_idx) {
|
|
||||||
PyObject* first = PyTuple_GetItem(args, start_idx);
|
|
||||||
|
|
||||||
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
|
|
||||||
PyObject* x_obj = PyTuple_GetItem(first, 0);
|
|
||||||
PyObject* y_obj = PyTuple_GetItem(first, 1);
|
|
||||||
|
|
||||||
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
|
|
||||||
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
|
|
||||||
result.grid_x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
|
|
||||||
result.grid_y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
|
|
||||||
result.valid = true;
|
|
||||||
has_positional = true;
|
|
||||||
if (next_arg) (*next_arg)++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for keyword conflicts
|
|
||||||
if (has_positional) {
|
|
||||||
if (hasConflict(kwds, "grid_pos", true) || hasConflict(kwds, "grid_x", true) || hasConflict(kwds, "grid_y", true)) {
|
|
||||||
result.valid = false;
|
|
||||||
result.error = "grid position specified both positionally and by keyword";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no positional, try keywords
|
|
||||||
if (!has_positional && kwds) {
|
|
||||||
PyObject* grid_pos_obj = PyDict_GetItemString(kwds, "grid_pos");
|
|
||||||
PyObject* grid_x_obj = PyDict_GetItemString(kwds, "grid_x");
|
|
||||||
PyObject* grid_y_obj = PyDict_GetItemString(kwds, "grid_y");
|
|
||||||
|
|
||||||
// Check for conflicts between grid_pos and grid_x/grid_y
|
|
||||||
if (grid_pos_obj && (grid_x_obj || grid_y_obj)) {
|
|
||||||
result.valid = false;
|
|
||||||
result.error = "grid_pos and grid_x/grid_y cannot both be specified";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (grid_pos_obj) {
|
|
||||||
// Parse grid_pos keyword
|
|
||||||
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
|
|
||||||
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
|
|
||||||
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
|
|
||||||
|
|
||||||
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
|
||||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
|
||||||
result.grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
|
||||||
result.grid_y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
|
||||||
result.valid = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (grid_x_obj && grid_y_obj) {
|
|
||||||
// Parse grid_x, grid_y keywords
|
|
||||||
if ((PyFloat_Check(grid_x_obj) || PyLong_Check(grid_x_obj)) &&
|
|
||||||
(PyFloat_Check(grid_y_obj) || PyLong_Check(grid_y_obj))) {
|
|
||||||
result.grid_x = PyFloat_Check(grid_x_obj) ? PyFloat_AsDouble(grid_x_obj) : PyLong_AsLong(grid_x_obj);
|
|
||||||
result.grid_y = PyFloat_Check(grid_y_obj) ? PyFloat_AsDouble(grid_y_obj) : PyLong_AsLong(grid_y_obj);
|
|
||||||
result.valid = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse grid size (int - no fractional tiles)
|
|
||||||
static GridSizeResult parseGridSize(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
|
|
||||||
GridSizeResult result = {0, 0, false, nullptr};
|
|
||||||
int start_idx = next_arg ? *next_arg : 0;
|
|
||||||
bool has_positional = false;
|
|
||||||
|
|
||||||
// Check for positional tuple argument
|
|
||||||
if (args && PyTuple_Size(args) > start_idx) {
|
|
||||||
PyObject* first = PyTuple_GetItem(args, start_idx);
|
|
||||||
|
|
||||||
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
|
|
||||||
PyObject* w_obj = PyTuple_GetItem(first, 0);
|
|
||||||
PyObject* h_obj = PyTuple_GetItem(first, 1);
|
|
||||||
|
|
||||||
if (PyLong_Check(w_obj) && PyLong_Check(h_obj)) {
|
|
||||||
result.grid_w = PyLong_AsLong(w_obj);
|
|
||||||
result.grid_h = PyLong_AsLong(h_obj);
|
|
||||||
result.valid = true;
|
|
||||||
has_positional = true;
|
|
||||||
if (next_arg) (*next_arg)++;
|
|
||||||
} else {
|
|
||||||
result.valid = false;
|
|
||||||
result.error = "grid size must be specified with integers";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for keyword conflicts
|
|
||||||
if (has_positional) {
|
|
||||||
if (hasConflict(kwds, "grid_size", true) || hasConflict(kwds, "grid_w", true) || hasConflict(kwds, "grid_h", true)) {
|
|
||||||
result.valid = false;
|
|
||||||
result.error = "grid size specified both positionally and by keyword";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no positional, try keywords
|
|
||||||
if (!has_positional && kwds) {
|
|
||||||
PyObject* grid_size_obj = PyDict_GetItemString(kwds, "grid_size");
|
|
||||||
PyObject* grid_w_obj = PyDict_GetItemString(kwds, "grid_w");
|
|
||||||
PyObject* grid_h_obj = PyDict_GetItemString(kwds, "grid_h");
|
|
||||||
|
|
||||||
// Check for conflicts between grid_size and grid_w/grid_h
|
|
||||||
if (grid_size_obj && (grid_w_obj || grid_h_obj)) {
|
|
||||||
result.valid = false;
|
|
||||||
result.error = "grid_size and grid_w/grid_h cannot both be specified";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (grid_size_obj) {
|
|
||||||
// Parse grid_size keyword
|
|
||||||
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
|
|
||||||
PyObject* w_val = PyTuple_GetItem(grid_size_obj, 0);
|
|
||||||
PyObject* h_val = PyTuple_GetItem(grid_size_obj, 1);
|
|
||||||
|
|
||||||
if (PyLong_Check(w_val) && PyLong_Check(h_val)) {
|
|
||||||
result.grid_w = PyLong_AsLong(w_val);
|
|
||||||
result.grid_h = PyLong_AsLong(h_val);
|
|
||||||
result.valid = true;
|
|
||||||
} else {
|
|
||||||
result.valid = false;
|
|
||||||
result.error = "grid size must be specified with integers";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (grid_w_obj && grid_h_obj) {
|
|
||||||
// Parse grid_w, grid_h keywords
|
|
||||||
if (PyLong_Check(grid_w_obj) && PyLong_Check(grid_h_obj)) {
|
|
||||||
result.grid_w = PyLong_AsLong(grid_w_obj);
|
|
||||||
result.grid_h = PyLong_AsLong(grid_h_obj);
|
|
||||||
result.valid = true;
|
|
||||||
} else {
|
|
||||||
result.valid = false;
|
|
||||||
result.error = "grid size must be specified with integers";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse color using existing PyColor infrastructure
|
|
||||||
static ColorResult parseColor(PyObject* obj, const char* param_name = nullptr) {
|
|
||||||
ColorResult result = {sf::Color::White, false, nullptr};
|
|
||||||
|
|
||||||
if (!obj) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use existing PyColor::from_arg which handles tuple/Color conversion
|
|
||||||
auto py_color = PyColor::from_arg(obj);
|
|
||||||
if (py_color) {
|
|
||||||
result.color = py_color->data;
|
|
||||||
result.valid = true;
|
|
||||||
} else {
|
|
||||||
result.valid = false;
|
|
||||||
std::string error_msg = param_name
|
|
||||||
? std::string(param_name) + " must be a color tuple (r,g,b) or (r,g,b,a)"
|
|
||||||
: "Invalid color format - expected tuple (r,g,b) or (r,g,b,a)";
|
|
||||||
result.error = error_msg.c_str();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to validate a texture object
|
|
||||||
static bool isValidTexture(PyObject* obj) {
|
|
||||||
if (!obj) return false;
|
|
||||||
PyObject* texture_type = PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Texture");
|
|
||||||
bool is_texture = PyObject_IsInstance(obj, texture_type);
|
|
||||||
Py_DECREF(texture_type);
|
|
||||||
return is_texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to validate a click handler
|
|
||||||
static bool isValidClickHandler(PyObject* obj) {
|
|
||||||
return obj && PyCallable_Check(obj);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -31,13 +31,18 @@ void PyScene::do_mouse_input(std::string button, std::string type)
|
||||||
// Convert window coordinates to game coordinates using the viewport
|
// Convert window coordinates to game coordinates using the viewport
|
||||||
auto mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos));
|
auto mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos));
|
||||||
|
|
||||||
// Create a sorted copy by z-index (highest first)
|
// Only sort if z_index values have changed
|
||||||
std::vector<std::shared_ptr<UIDrawable>> sorted_elements(*ui_elements);
|
if (ui_elements_need_sort) {
|
||||||
std::sort(sorted_elements.begin(), sorted_elements.end(),
|
// Sort in ascending order (same as render)
|
||||||
[](const auto& a, const auto& b) { return a->z_index > b->z_index; });
|
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)
|
// Check elements in reverse z-order (highest z_index first, top to bottom)
|
||||||
for (const auto& element : sorted_elements) {
|
// 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 (!element->visible) continue;
|
||||||
|
|
||||||
if (auto target = element->click_at(sf::Vector2f(mousepos))) {
|
if (auto target = element->click_at(sf::Vector2f(mousepos))) {
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
#include "PyColor.h"
|
#include "PyColor.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PyFont.h"
|
#include "PyFont.h"
|
||||||
#include "PyArgHelpers.h"
|
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
@ -303,67 +302,47 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
||||||
{
|
{
|
||||||
using namespace mcrfpydef;
|
using namespace mcrfpydef;
|
||||||
|
|
||||||
// Try parsing with PyArgHelpers
|
// Define all parameters with defaults
|
||||||
int arg_idx = 0;
|
PyObject* pos_obj = nullptr;
|
||||||
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
|
|
||||||
|
|
||||||
// Default values
|
|
||||||
float x = 0.0f, y = 0.0f, outline = 0.0f;
|
|
||||||
char* text = nullptr;
|
|
||||||
PyObject* font = nullptr;
|
PyObject* font = nullptr;
|
||||||
|
const char* text = "";
|
||||||
PyObject* fill_color = nullptr;
|
PyObject* fill_color = nullptr;
|
||||||
PyObject* outline_color = nullptr;
|
PyObject* outline_color = nullptr;
|
||||||
|
float outline = 0.0f;
|
||||||
|
float font_size = 16.0f;
|
||||||
PyObject* click_handler = 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;
|
||||||
|
|
||||||
// Case 1: Got position from helpers (tuple format)
|
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||||
if (pos_result.valid) {
|
static const char* kwlist[] = {
|
||||||
x = pos_result.x;
|
"pos", "font", "text", // Positional args (as per spec)
|
||||||
y = pos_result.y;
|
// Keyword-only args
|
||||||
|
"fill_color", "outline_color", "outline", "font_size", "click",
|
||||||
// Parse remaining arguments
|
"visible", "opacity", "z_index", "name", "x", "y",
|
||||||
static const char* remaining_keywords[] = {
|
nullptr
|
||||||
"text", "font", "fill_color", "outline_color", "outline", "click", nullptr
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create new tuple with remaining args
|
// Parse arguments with | for optional positional args
|
||||||
Py_ssize_t total_args = PyTuple_Size(args);
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizff", const_cast<char**>(kwlist),
|
||||||
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
|
&pos_obj, &font, &text, // Positional
|
||||||
|
&fill_color, &outline_color, &outline, &font_size, &click_handler,
|
||||||
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|zOOOfO",
|
&visible, &opacity, &z_index, &name, &x, &y)) {
|
||||||
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);
|
|
||||||
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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle pos keyword override
|
// Handle position argument (can be tuple, Vector, or use x/y keywords)
|
||||||
if (pos_obj && pos_obj != Py_None) {
|
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) {
|
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
||||||
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
||||||
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
||||||
|
@ -371,115 +350,87 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
||||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_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);
|
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 {
|
} else {
|
||||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
|
||||||
return -1;
|
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 {
|
} else {
|
||||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||||
return -1;
|
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;
|
||||||
|
}
|
||||||
|
auto obj = (PyFontObject*)font;
|
||||||
|
pyfont = obj->data;
|
||||||
}
|
}
|
||||||
|
|
||||||
self->data->position = sf::Vector2f(x, y); // Set base class position
|
// Create the caption
|
||||||
self->data->text.setPosition(self->data->position); // Sync text position
|
self->data = std::make_shared<UICaption>();
|
||||||
// check types for font, fill_color, outline_color
|
self->data->position = sf::Vector2f(x, y);
|
||||||
|
self->data->text.setPosition(self->data->position);
|
||||||
|
self->data->text.setOutlineThickness(outline);
|
||||||
|
|
||||||
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl;
|
// Set the font
|
||||||
if (font != NULL && font != Py_None && !PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font")/*(PyObject*)&PyFontType)*/)){
|
if (pyfont) {
|
||||||
PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance or None");
|
self->data->text.setFont(pyfont->font);
|
||||||
return -1;
|
} else {
|
||||||
} 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
|
|
||||||
{
|
|
||||||
// Use default font when None or not provided
|
// Use default font when None or not provided
|
||||||
if (McRFPy_API::default_font) {
|
if (McRFPy_API::default_font) {
|
||||||
self->data->text.setFont(McRFPy_API::default_font->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
|
// Set character size
|
||||||
if (text && text != NULL) {
|
self->data->text.setCharacterSize(static_cast<unsigned int>(font_size));
|
||||||
self->data->text.setString((std::string)text);
|
|
||||||
} else {
|
// Set text
|
||||||
self->data->text.setString("");
|
if (text && strlen(text) > 0) {
|
||||||
|
self->data->text.setString(std::string(text));
|
||||||
}
|
}
|
||||||
self->data->text.setOutlineThickness(outline);
|
|
||||||
if (fill_color) {
|
// Handle fill_color
|
||||||
auto fc = PyColor::from_arg(fill_color);
|
if (fill_color && fill_color != Py_None) {
|
||||||
if (!fc) {
|
PyColorObject* color_obj = PyColor::from_arg(fill_color);
|
||||||
PyErr_SetString(PyExc_TypeError, "fill_color must be mcrfpy.Color or arguments to mcrfpy.Color.__init__");
|
if (!color_obj) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
self->data->text.setFillColor(PyColor::fromPy(fc));
|
self->data->text.setFillColor(color_obj->data);
|
||||||
//Py_DECREF(fc);
|
Py_DECREF(color_obj);
|
||||||
} else {
|
} 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) {
|
// Handle outline_color
|
||||||
auto oc = PyColor::from_arg(outline_color);
|
if (outline_color && outline_color != Py_None) {
|
||||||
if (!oc) {
|
PyColorObject* color_obj = PyColor::from_arg(outline_color);
|
||||||
PyErr_SetString(PyExc_TypeError, "outline_color must be mcrfpy.Color or arguments to mcrfpy.Color.__init__");
|
if (!color_obj) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or color tuple");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
self->data->text.setOutlineColor(PyColor::fromPy(oc));
|
self->data->text.setOutlineColor(color_obj->data);
|
||||||
//Py_DECREF(oc);
|
Py_DECREF(color_obj);
|
||||||
} else {
|
} 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 (click_handler && click_handler != Py_None) {
|
||||||
if (!PyCallable_Check(click_handler)) {
|
if (!PyCallable_Check(click_handler)) {
|
||||||
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
||||||
|
@ -491,6 +442,7 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Property system implementation for animations
|
// Property system implementation for animations
|
||||||
bool UICaption::setProperty(const std::string& name, float value) {
|
bool UICaption::setProperty(const std::string& name, float value) {
|
||||||
if (name == "x") {
|
if (name == "x") {
|
||||||
|
|
|
@ -65,26 +65,37 @@ namespace mcrfpydef {
|
||||||
//.tp_iter
|
//.tp_iter
|
||||||
//.tp_iternext
|
//.tp_iternext
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.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"
|
"A text display UI element with customizable font and styling.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" text (str): The text content to display. Default: ''\n"
|
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||||
" x (float): X position in pixels. Default: 0\n"
|
" font (Font, optional): Font object for text rendering. Default: engine default font\n"
|
||||||
" y (float): Y position in pixels. Default: 0\n"
|
" text (str, optional): The text content to display. Default: ''\n\n"
|
||||||
" font (Font): Font object for text rendering. Default: engine default font\n"
|
"Keyword Args:\n"
|
||||||
" fill_color (Color): Text fill color. Default: (255, 255, 255, 255)\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_color (Color): Text outline color. Default: (0, 0, 0, 255)\n"
|
||||||
" outline (float): Text outline thickness. Default: 0\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"
|
"Attributes:\n"
|
||||||
" text (str): The displayed text content\n"
|
" text (str): The displayed text content\n"
|
||||||
" x, y (float): Position in pixels\n"
|
" x, y (float): Position in pixels\n"
|
||||||
|
" pos (Vector): Position as a Vector object\n"
|
||||||
" font (Font): Font used for rendering\n"
|
" font (Font): Font used for rendering\n"
|
||||||
|
" font_size (float): Font size in points\n"
|
||||||
" fill_color, outline_color (Color): Text appearance\n"
|
" fill_color, outline_color (Color): Text appearance\n"
|
||||||
" outline (float): Outline thickness\n"
|
" outline (float): Outline thickness\n"
|
||||||
" click (callable): Click event handler\n"
|
" click (callable): Click event handler\n"
|
||||||
" visible (bool): Visibility state\n"
|
" visible (bool): Visibility state\n"
|
||||||
|
" opacity (float): Opacity value\n"
|
||||||
" z_index (int): Rendering order\n"
|
" z_index (int): Rendering order\n"
|
||||||
|
" name (str): Element name\n"
|
||||||
" w, h (float): Read-only computed size based on text and font"),
|
" w, h (float): Read-only computed size based on text and font"),
|
||||||
.tp_methods = UICaption_methods,
|
.tp_methods = UICaption_methods,
|
||||||
//.tp_members = PyUIFrame_members,
|
//.tp_members = PyUIFrame_members,
|
||||||
|
|
114
src/UIEntity.cpp
114
src/UIEntity.cpp
|
@ -4,7 +4,6 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include "PyObjectUtils.h"
|
#include "PyObjectUtils.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PyArgHelpers.h"
|
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
#include "UIEntityPyMethods.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) {
|
int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||||
// Try parsing with PyArgHelpers for grid position
|
// Define all parameters with defaults
|
||||||
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;
|
|
||||||
PyObject* texture = nullptr;
|
|
||||||
PyObject* grid_obj = nullptr;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
// 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;
|
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;
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOiOO",
|
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||||
const_cast<char**>(keywords),
|
static const char* kwlist[] = {
|
||||||
&grid_x, &grid_y, &texture, &sprite_index,
|
"grid_pos", "texture", "sprite_index", // Positional args (as per spec)
|
||||||
&grid_obj, &grid_pos_obj)) {
|
// 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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle grid_pos keyword override
|
// Handle grid position argument (can be tuple or use x/y keywords)
|
||||||
if (grid_pos_obj && grid_pos_obj != Py_None) {
|
if (grid_pos_obj) {
|
||||||
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
|
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
|
||||||
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
|
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
|
||||||
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
|
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
|
||||||
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||||
grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
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);
|
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "grid_pos tuple must contain numbers");
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)");
|
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// check types for texture
|
// Handle texture argument
|
||||||
//
|
|
||||||
// Set Texture - allow None or use default
|
|
||||||
//
|
|
||||||
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
||||||
if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
|
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");
|
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||||
return -1;
|
return -1;
|
||||||
} else if (texture != NULL && texture != Py_None) {
|
}
|
||||||
auto pytexture = (PyTextureObject*)texture;
|
auto pytexture = (PyTextureObject*)texture;
|
||||||
texture_ptr = pytexture->data;
|
texture_ptr = pytexture->data;
|
||||||
} else {
|
} else {
|
||||||
|
@ -203,25 +178,20 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||||
texture_ptr = McRFPy_API::default_texture;
|
texture_ptr = McRFPy_API::default_texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow creation without texture for testing purposes
|
// Handle grid argument
|
||||||
// if (!texture_ptr) {
|
if (grid_obj && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||||
// 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"))) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
|
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always use default constructor for lazy initialization
|
// Create the entity
|
||||||
self->data = std::make_shared<UIEntity>();
|
self->data = std::make_shared<UIEntity>();
|
||||||
|
|
||||||
// Store reference to Python object
|
// Store reference to Python object
|
||||||
self->data->self = (PyObject*)self;
|
self->data->self = (PyObject*)self;
|
||||||
Py_INCREF(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) {
|
if (texture_ptr) {
|
||||||
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
|
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
|
||||||
} else {
|
} else {
|
||||||
|
@ -230,12 +200,20 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set position using grid coordinates
|
// 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;
|
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj;
|
||||||
self->data->grid = pygrid->data;
|
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);
|
pygrid->data->entities->push_back(self->data);
|
||||||
|
|
||||||
// Don't initialize gridstate here - lazy initialization to support large numbers of entities
|
// Don't initialize gridstate here - lazy initialization to support large numbers of entities
|
||||||
|
|
|
@ -88,7 +88,28 @@ namespace mcrfpydef {
|
||||||
.tp_itemsize = 0,
|
.tp_itemsize = 0,
|
||||||
.tp_repr = (reprfunc)UIEntity::repr,
|
.tp_repr = (reprfunc)UIEntity::repr,
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
.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_methods = UIEntity_all_methods,
|
||||||
.tp_getset = UIEntity::getsetters,
|
.tp_getset = UIEntity::getsetters,
|
||||||
.tp_base = &mcrfpydef::PyDrawableType,
|
.tp_base = &mcrfpydef::PyDrawableType,
|
||||||
|
|
167
src/UIFrame.cpp
167
src/UIFrame.cpp
|
@ -6,7 +6,6 @@
|
||||||
#include "UISprite.h"
|
#include "UISprite.h"
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PyArgHelpers.h"
|
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
|
|
||||||
UIDrawable* UIFrame::click_at(sf::Vector2f point)
|
UIDrawable* UIFrame::click_at(sf::Vector2f point)
|
||||||
|
@ -432,67 +431,47 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
||||||
// Initialize children first
|
// Initialize children first
|
||||||
self->data->children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
|
self->data->children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
|
||||||
|
|
||||||
// Try parsing with PyArgHelpers
|
// Define all parameters with defaults
|
||||||
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;
|
|
||||||
PyObject* fill_color = nullptr;
|
|
||||||
PyObject* outline_color = nullptr;
|
|
||||||
PyObject* children_arg = nullptr;
|
|
||||||
PyObject* click_handler = nullptr;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
// 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* pos_obj = nullptr;
|
||||||
PyObject* size_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;
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffffOOfOOOO",
|
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||||
const_cast<char**>(keywords),
|
static const char* kwlist[] = {
|
||||||
&x, &y, &w, &h, &fill_color, &outline_color,
|
"pos", "size", // Positional args (as per spec)
|
||||||
&outline, &children_arg, &click_handler,
|
// Keyword-only args
|
||||||
&pos_obj, &size_obj)) {
|
"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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle pos keyword override
|
// Handle position argument (can be tuple, Vector, or use x/y keywords)
|
||||||
if (pos_obj && pos_obj != Py_None) {
|
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) {
|
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
||||||
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
||||||
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
||||||
|
@ -500,20 +479,20 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
||||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_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);
|
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 {
|
} else {
|
||||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// If no pos_obj but x/y keywords were provided, they're already in x, y variables
|
||||||
|
|
||||||
// Handle size keyword override
|
// Handle size argument (can be tuple or use w/h keywords)
|
||||||
if (size_obj && size_obj != Py_None) {
|
if (size_obj) {
|
||||||
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||||
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
||||||
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
||||||
|
@ -521,26 +500,66 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
||||||
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
||||||
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_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);
|
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers");
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
|
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
|
||||||
return -1;
|
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
|
// Set the position and size
|
||||||
self->data->box.setPosition(self->data->position); // Sync box position
|
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.setSize(sf::Vector2f(w, h));
|
||||||
self->data->box.setOutlineThickness(outline);
|
self->data->box.setOutlineThickness(outline);
|
||||||
// getsetter abuse because I haven't standardized Color object parsing (TODO)
|
|
||||||
int err_val = 0;
|
// Handle fill_color
|
||||||
if (fill_color && fill_color != Py_None) err_val = UIFrame::set_color_member(self, fill_color, (void*)0);
|
if (fill_color && fill_color != Py_None) {
|
||||||
else self->data->box.setFillColor(sf::Color(0,0,0,255));
|
PyColorObject* color_obj = PyColor::from_arg(fill_color);
|
||||||
if (err_val) return err_val;
|
if (!color_obj) {
|
||||||
if (outline_color && outline_color != Py_None) err_val = UIFrame::set_color_member(self, outline_color, (void*)1);
|
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple");
|
||||||
else self->data->box.setOutlineColor(sf::Color(128,128,128,255));
|
return -1;
|
||||||
if (err_val) return err_val;
|
}
|
||||||
|
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
|
// Process children argument if provided
|
||||||
if (children_arg && children_arg != Py_None) {
|
if (children_arg && children_arg != Py_None) {
|
||||||
|
|
|
@ -86,27 +86,38 @@ namespace mcrfpydef {
|
||||||
//.tp_iter
|
//.tp_iter
|
||||||
//.tp_iternext
|
//.tp_iternext
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.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"
|
"A rectangular frame UI element that can contain other drawable elements.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" x (float): X position in pixels. Default: 0\n"
|
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||||
" y (float): Y position in pixels. Default: 0\n"
|
" size (tuple, optional): Size as (width, height) tuple. Default: (0, 0)\n\n"
|
||||||
" w (float): Width in pixels. Default: 0\n"
|
"Keyword Args:\n"
|
||||||
" h (float): Height in pixels. Default: 0\n"
|
|
||||||
" fill_color (Color): Background fill color. Default: (0, 0, 0, 128)\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_color (Color): Border outline color. Default: (255, 255, 255, 255)\n"
|
||||||
" outline (float): Border outline thickness. Default: 0\n"
|
" outline (float): Border outline thickness. Default: 0\n"
|
||||||
" click (callable): Click event handler. Default: None\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"
|
"Attributes:\n"
|
||||||
" x, y (float): Position in pixels\n"
|
" x, y (float): Position in pixels\n"
|
||||||
" w, h (float): Size 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"
|
" fill_color, outline_color (Color): Visual appearance\n"
|
||||||
" outline (float): Border thickness\n"
|
" outline (float): Border thickness\n"
|
||||||
" click (callable): Click event handler\n"
|
" click (callable): Click event handler\n"
|
||||||
" children (list): Collection of child drawable elements\n"
|
" children (list): Collection of child drawable elements\n"
|
||||||
" visible (bool): Visibility state\n"
|
" visible (bool): Visibility state\n"
|
||||||
|
" opacity (float): Opacity value\n"
|
||||||
" z_index (int): Rendering order\n"
|
" z_index (int): Rendering order\n"
|
||||||
|
" name (str): Element name\n"
|
||||||
" clip_children (bool): Whether to clip children to frame bounds"),
|
" clip_children (bool): Whether to clip children to frame bounds"),
|
||||||
.tp_methods = UIFrame_methods,
|
.tp_methods = UIFrame_methods,
|
||||||
//.tp_members = PyUIFrame_members,
|
//.tp_members = PyUIFrame_members,
|
||||||
|
|
203
src/UIGrid.cpp
203
src/UIGrid.cpp
|
@ -1,7 +1,6 @@
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PyArgHelpers.h"
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
// UIDrawable methods now in UIBase.h
|
// 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) {
|
int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
// Default values
|
// Define all parameters with defaults
|
||||||
int grid_x = 0, grid_y = 0;
|
|
||||||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
|
|
||||||
PyObject* textureObj = nullptr;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
} 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* pos_obj = nullptr;
|
||||||
PyObject* size_obj = nullptr;
|
PyObject* size_obj = nullptr;
|
||||||
PyObject* grid_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
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiOOOO",
|
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||||
const_cast<char**>(keywords),
|
static const char* kwlist[] = {
|
||||||
&grid_x, &grid_y, &textureObj,
|
"pos", "size", "grid_size", "texture", // Positional args (as per spec)
|
||||||
&pos_obj, &size_obj, &grid_size_obj)) {
|
// Keyword-only args
|
||||||
|
"fill_color", "click", "center_x", "center_y", "zoom", "perspective",
|
||||||
|
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "grid_x", "grid_y",
|
||||||
|
nullptr
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse arguments with | for optional positional args
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffiifizffffii", const_cast<char**>(kwlist),
|
||||||
|
&pos_obj, &size_obj, &grid_size_obj, &textureObj, // Positional
|
||||||
|
&fill_color, &click_handler, ¢er_x, ¢er_y, &zoom, &perspective,
|
||||||
|
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_x, &grid_y)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle grid_size override
|
// Handle position argument (can be tuple, Vector, or use x/y keywords)
|
||||||
if (grid_size_obj && grid_size_obj != Py_None) {
|
if (pos_obj) {
|
||||||
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
|
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||||
PyObject* x_obj = PyTuple_GetItem(grid_size_obj, 0);
|
if (vec) {
|
||||||
PyObject* y_obj = PyTuple_GetItem(grid_size_obj, 1);
|
x = vec->data.x;
|
||||||
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
|
y = vec->data.y;
|
||||||
grid_x = PyLong_AsLong(x_obj);
|
Py_DECREF(vec);
|
||||||
grid_y = PyLong_AsLong(y_obj);
|
|
||||||
} else {
|
} else {
|
||||||
PyErr_SetString(PyExc_TypeError, "grid_size must contain integers");
|
PyErr_Clear();
|
||||||
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) {
|
|
||||||
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
||||||
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
||||||
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
||||||
|
@ -622,17 +568,18 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_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);
|
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||||
} else {
|
} else {
|
||||||
PyErr_SetString(PyExc_TypeError, "pos must contain numbers");
|
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle size
|
// Handle size argument (can be tuple or use w/h keywords)
|
||||||
if (size_obj && size_obj != Py_None) {
|
if (size_obj) {
|
||||||
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||||
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
||||||
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
||||||
|
@ -641,17 +588,30 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_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);
|
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
||||||
} else {
|
} else {
|
||||||
PyErr_SetString(PyExc_TypeError, "size must contain numbers");
|
PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple of two numbers");
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Default size based on grid
|
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple (grid_x, grid_y)");
|
||||||
w = grid_x * 16.0f;
|
return -1;
|
||||||
h = grid_y * 16.0f;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -661,12 +621,8 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point we have x, y, w, h values from either parsing method
|
// Handle texture argument
|
||||||
|
|
||||||
// Convert PyObject texture to shared_ptr<PyTexture>
|
|
||||||
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
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 (textureObj && textureObj != Py_None) {
|
||||||
if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
|
if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
|
||||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
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;
|
texture_ptr = McRFPy_API::default_texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust size based on texture if available and size not explicitly set
|
// If size wasn't specified, calculate based on grid dimensions and texture
|
||||||
if (texture_ptr && w == grid_x * 16.0f && h == grid_y * 16.0f) {
|
if (!size_obj && texture_ptr) {
|
||||||
w = grid_x * texture_ptr->sprite_width;
|
w = grid_x * texture_ptr->sprite_width;
|
||||||
h = grid_y * texture_ptr->sprite_height;
|
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,
|
self->data = std::make_shared<UIGrid>(grid_x, grid_y, texture_ptr,
|
||||||
sf::Vector2f(x, y), sf::Vector2f(w, h));
|
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
|
return 0; // Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
52
src/UIGrid.h
52
src/UIGrid.h
|
@ -184,29 +184,49 @@ namespace mcrfpydef {
|
||||||
//.tp_iter
|
//.tp_iter
|
||||||
//.tp_iternext
|
//.tp_iternext
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.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"
|
.tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n"
|
||||||
"A grid-based tilemap UI element for rendering tile-based levels and game worlds.\n\n"
|
"A grid-based UI element for tile-based rendering and entity management.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" x (float): X position in pixels. Default: 0\n"
|
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||||
" y (float): Y position in pixels. Default: 0\n"
|
" size (tuple, optional): Size as (width, height) tuple. Default: auto-calculated from grid_size\n"
|
||||||
" grid_size (tuple): Grid dimensions as (width, height) in tiles. Default: (20, 20)\n"
|
" grid_size (tuple, optional): Grid dimensions as (grid_x, grid_y) tuple. Default: (2, 2)\n"
|
||||||
" texture (Texture): Texture atlas containing tile sprites. Default: None\n"
|
" texture (Texture, optional): Texture containing tile sprites. Default: default texture\n\n"
|
||||||
" tile_width (int): Width of each tile in pixels. Default: 16\n"
|
"Keyword Args:\n"
|
||||||
" tile_height (int): Height of each tile in pixels. Default: 16\n"
|
" fill_color (Color): Background fill color. Default: None\n"
|
||||||
" scale (float): Grid scaling factor. Default: 1.0\n"
|
" click (callable): Click event handler. Default: None\n"
|
||||||
" click (callable): Click event handler. Default: None\n\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"
|
"Attributes:\n"
|
||||||
" x, y (float): Position in pixels\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"
|
" 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"
|
" texture (Texture): Tile texture atlas\n"
|
||||||
" scale (float): Scale multiplier\n"
|
" fill_color (Color): Background color\n"
|
||||||
" points (list): 2D array of GridPoint objects for tile data\n"
|
" entities (EntityCollection): Collection of entities in the grid\n"
|
||||||
" entities (list): Collection of Entity objects in the grid\n"
|
" perspective (int): Entity perspective index\n"
|
||||||
" background_color (Color): Grid background color\n"
|
|
||||||
" click (callable): Click event handler\n"
|
" click (callable): Click event handler\n"
|
||||||
" visible (bool): Visibility state\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_methods = UIGrid_all_methods,
|
||||||
//.tp_members = UIGrid::members,
|
//.tp_members = UIGrid::members,
|
||||||
.tp_getset = UIGrid::getsetters,
|
.tp_getset = UIGrid::getsetters,
|
||||||
|
|
115
src/UISprite.cpp
115
src/UISprite.cpp
|
@ -1,7 +1,6 @@
|
||||||
#include "UISprite.h"
|
#include "UISprite.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PyArgHelpers.h"
|
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
|
|
||||||
UIDrawable* UISprite::click_at(sf::Vector2f point)
|
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)
|
int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
||||||
{
|
{
|
||||||
// Try parsing with PyArgHelpers
|
// Define all parameters with defaults
|
||||||
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;
|
|
||||||
PyObject* texture = nullptr;
|
|
||||||
PyObject* click_handler = nullptr;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
// 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;
|
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;
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOifOO",
|
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||||
const_cast<char**>(keywords),
|
static const char* kwlist[] = {
|
||||||
&x, &y, &texture, &sprite_index, &scale,
|
"pos", "texture", "sprite_index", // Positional args (as per spec)
|
||||||
&click_handler, &pos_obj)) {
|
// 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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle pos keyword override
|
// Handle position argument (can be tuple, Vector, or use x/y keywords)
|
||||||
if (pos_obj && pos_obj != Py_None) {
|
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) {
|
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
||||||
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
||||||
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
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))) {
|
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_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);
|
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 {
|
} else {
|
||||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -400,10 +386,11 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
||||||
|
|
||||||
// Handle texture - allow None or use default
|
// Handle texture - allow None or use default
|
||||||
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
||||||
if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
|
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");
|
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||||
return -1;
|
return -1;
|
||||||
} else if (texture != NULL && texture != Py_None) {
|
}
|
||||||
auto pytexture = (PyTextureObject*)texture;
|
auto pytexture = (PyTextureObject*)texture;
|
||||||
texture_ptr = pytexture->data;
|
texture_ptr = pytexture->data;
|
||||||
} else {
|
} else {
|
||||||
|
@ -416,9 +403,27 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create the sprite
|
||||||
self->data = std::make_shared<UISprite>(texture_ptr, sprite_index, sf::Vector2f(x, y), scale);
|
self->data = std::make_shared<UISprite>(texture_ptr, sprite_index, sf::Vector2f(x, y), scale);
|
||||||
|
|
||||||
// Process click handler if provided
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle click handler
|
||||||
if (click_handler && click_handler != Py_None) {
|
if (click_handler && click_handler != Py_None) {
|
||||||
if (!PyCallable_Check(click_handler)) {
|
if (!PyCallable_Check(click_handler)) {
|
||||||
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
||||||
|
|
|
@ -92,23 +92,35 @@ namespace mcrfpydef {
|
||||||
//.tp_iter
|
//.tp_iter
|
||||||
//.tp_iternext
|
//.tp_iternext
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.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"
|
"A sprite UI element that displays a texture or portion of a texture atlas.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" x (float): X position in pixels. Default: 0\n"
|
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||||
" y (float): Y position in pixels. Default: 0\n"
|
" texture (Texture, optional): Texture object to display. Default: default texture\n"
|
||||||
" texture (Texture): Texture object to display. Default: None\n"
|
" sprite_index (int, optional): Index into texture atlas. Default: 0\n\n"
|
||||||
" sprite_index (int): Index into texture atlas (if applicable). Default: 0\n"
|
"Keyword Args:\n"
|
||||||
" scale (float): Sprite scaling factor. Default: 1.0\n"
|
" scale (float): Uniform scale factor. Default: 1.0\n"
|
||||||
" click (callable): Click event handler. Default: None\n\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"
|
"Attributes:\n"
|
||||||
" x, y (float): Position in pixels\n"
|
" x, y (float): Position in pixels\n"
|
||||||
|
" pos (Vector): Position as a Vector object\n"
|
||||||
" texture (Texture): The texture being displayed\n"
|
" texture (Texture): The texture being displayed\n"
|
||||||
" sprite_index (int): Current sprite index in texture atlas\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"
|
" click (callable): Click event handler\n"
|
||||||
" visible (bool): Visibility state\n"
|
" visible (bool): Visibility state\n"
|
||||||
|
" opacity (float): Opacity value\n"
|
||||||
" z_index (int): Rendering order\n"
|
" z_index (int): Rendering order\n"
|
||||||
|
" name (str): Element name\n"
|
||||||
" w, h (float): Read-only computed size based on texture and scale"),
|
" w, h (float): Read-only computed size based on texture and scale"),
|
||||||
.tp_methods = UISprite_methods,
|
.tp_methods = UISprite_methods,
|
||||||
//.tp_members = PyUIFrame_members,
|
//.tp_members = PyUIFrame_members,
|
||||||
|
|
|
@ -67,10 +67,10 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
|
||||||
self.draw_pos = (tx, ty)
|
self.draw_pos = (tx, ty)
|
||||||
for e in self.game.entities:
|
for e in self.game.entities:
|
||||||
if e is self: continue
|
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:
|
for e in self.game.entities:
|
||||||
if e is self: continue
|
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):
|
def act(self):
|
||||||
pass
|
pass
|
||||||
|
@ -83,12 +83,12 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
|
||||||
|
|
||||||
def try_move(self, dx, dy, test=False):
|
def try_move(self, dx, dy, test=False):
|
||||||
x_max, y_max = self.grid.grid_size
|
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):
|
#for e in iterable_entities(self.grid):
|
||||||
|
|
||||||
# sorting entities to test against the boulder instead of the button when they overlap.
|
# 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):
|
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}")
|
#print(f"bumping {e}")
|
||||||
return e.bump(self, dx, dy)
|
return e.bump(self, dx, dy)
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _relative_move(self, dx, dy):
|
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.draw_pos = (tx, ty)
|
||||||
self.do_move(tx, ty)
|
self.do_move(tx, ty)
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@ class Equippable:
|
||||||
if self.zap_cooldown_remaining != 0:
|
if self.zap_cooldown_remaining != 0:
|
||||||
print("zap is cooling down.")
|
print("zap is cooling down.")
|
||||||
return False
|
return False
|
||||||
fx, fy = caster.draw_pos
|
fx, fy = caster.draw_pos.x, caster.draw_pos.y
|
||||||
x, y = int(fx), int (fy)
|
x, y = int(fx), int (fy)
|
||||||
dist = lambda tx, ty: abs(int(tx) - x) + abs(int(ty) - y)
|
dist = lambda tx, ty: abs(int(tx) - x) + abs(int(ty) - y)
|
||||||
targets = []
|
targets = []
|
||||||
|
@ -293,7 +293,7 @@ class PlayerEntity(COSEntity):
|
||||||
## TODO - find other entities to avoid spawning on top of
|
## TODO - find other entities to avoid spawning on top of
|
||||||
for spawn in spawn_points:
|
for spawn in spawn_points:
|
||||||
for e in avoid or []:
|
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:
|
else:
|
||||||
break
|
break
|
||||||
self.draw_pos = spawn
|
self.draw_pos = spawn
|
||||||
|
@ -314,9 +314,9 @@ class BoulderEntity(COSEntity):
|
||||||
elif type(other) == EnemyEntity:
|
elif type(other) == EnemyEntity:
|
||||||
if not other.can_push: return False
|
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.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
|
# 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 self.try_move(dx, dy, test=test):
|
||||||
if not test:
|
if not test:
|
||||||
other.do_move(*old_pos)
|
other.do_move(*old_pos)
|
||||||
|
@ -342,7 +342,7 @@ class ButtonEntity(COSEntity):
|
||||||
# self.exit.unlock()
|
# self.exit.unlock()
|
||||||
# TODO: unlock, and then lock again, when player steps on/off
|
# TODO: unlock, and then lock again, when player steps on/off
|
||||||
if not test:
|
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)
|
other.do_move(*pos)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -393,7 +393,7 @@ class EnemyEntity(COSEntity):
|
||||||
def bump(self, other, dx, dy, test=False):
|
def bump(self, other, dx, dy, test=False):
|
||||||
if self.hp == 0:
|
if self.hp == 0:
|
||||||
if not test:
|
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)
|
other.do_move(*old_pos)
|
||||||
return True
|
return True
|
||||||
if type(other) == PlayerEntity:
|
if type(other) == PlayerEntity:
|
||||||
|
@ -415,7 +415,7 @@ class EnemyEntity(COSEntity):
|
||||||
print("Ouch, my entire body!!")
|
print("Ouch, my entire body!!")
|
||||||
self._entity.sprite_number = self.base_sprite + 246
|
self._entity.sprite_number = self.base_sprite + 246
|
||||||
self.hp = 0
|
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:
|
if not test:
|
||||||
other.do_move(*old_pos)
|
other.do_move(*old_pos)
|
||||||
return True
|
return True
|
||||||
|
@ -423,8 +423,8 @@ class EnemyEntity(COSEntity):
|
||||||
def act(self):
|
def act(self):
|
||||||
if self.hp > 0:
|
if self.hp > 0:
|
||||||
# if player nearby: attack
|
# if player nearby: attack
|
||||||
x, y = self.draw_pos
|
x, y = self.draw_pos.x, self.draw_pos.y
|
||||||
px, py = self.game.player.draw_pos
|
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)):
|
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):
|
if int(x + d[0]) == int(px) and int(y + d[1]) == int(py):
|
||||||
self.try_move(*d)
|
self.try_move(*d)
|
||||||
|
|
|
@ -22,12 +22,13 @@ class TileInfo:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_grid(grid, xy:tuple):
|
def from_grid(grid, xy:tuple):
|
||||||
values = {}
|
values = {}
|
||||||
|
x_max, y_max = grid.grid_size
|
||||||
for d in deltas:
|
for d in deltas:
|
||||||
tx, ty = d[0] + xy[0], d[1] + xy[1]
|
tx, ty = d[0] + xy[0], d[1] + xy[1]
|
||||||
try:
|
if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max:
|
||||||
values[d] = grid.at((tx, ty)).walkable
|
|
||||||
except ValueError:
|
|
||||||
values[d] = True
|
values[d] = True
|
||||||
|
else:
|
||||||
|
values[d] = grid.at((tx, ty)).walkable
|
||||||
return TileInfo(values)
|
return TileInfo(values)
|
||||||
|
|
||||||
@staticmethod
|
@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]
|
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 ''}")
|
#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
|
if (tx, ty) in unverified_tiles and cardinal in "nsew": return pass_unverified
|
||||||
try:
|
x_max, y_max = grid.grid_size
|
||||||
return grid.at((tx, ty)).tilesprite == allowed_tile
|
if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max:
|
||||||
except ValueError:
|
|
||||||
return False
|
return False
|
||||||
|
return grid.at((tx, ty)).tilesprite == allowed_tile
|
||||||
|
|
||||||
import random
|
import random
|
||||||
tile_of_last_resort = 431
|
tile_of_last_resort = 431
|
||||||
|
|
|
@ -87,7 +87,7 @@ class Crypt:
|
||||||
|
|
||||||
# Side Bar (inventory, level info) config
|
# Side Bar (inventory, level info) config
|
||||||
self.level_caption = mcrfpy.Caption((5,5), "Level: 1", font, fill_color=(255, 255, 255))
|
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 = 3
|
||||||
self.level_caption.outline_color = (0, 0, 0)
|
self.level_caption.outline_color = (0, 0, 0)
|
||||||
self.sidebar.children.append(self.level_caption)
|
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)
|
mcrfpy.Caption((25, 130 + 95 * i), "x", font, fill_color=(255, 255, 255)) for i in range(5)
|
||||||
]
|
]
|
||||||
for i in self.inv_captions:
|
for i in self.inv_captions:
|
||||||
i.size = 16
|
i.font_size = 16
|
||||||
self.sidebar.children.append(i)
|
self.sidebar.children.append(i)
|
||||||
|
|
||||||
liminal_void = mcrfpy.Grid(1, 1, t, (0, 0), (16, 16))
|
liminal_void = mcrfpy.Grid(1, 1, t, (0, 0), (16, 16))
|
||||||
|
@ -382,7 +382,7 @@ class Crypt:
|
||||||
def pull_boulder_search(self):
|
def pull_boulder_search(self):
|
||||||
for dx, dy in ( (0, -1), (-1, 0), (1, 0), (0, 1) ):
|
for dx, dy in ( (0, -1), (-1, 0), (1, 0), (0, 1) ):
|
||||||
for e in self.entities:
|
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:
|
if type(e) == ce.BoulderEntity:
|
||||||
self.pull_boulder_move((dx, dy), e)
|
self.pull_boulder_move((dx, dy), e)
|
||||||
return self.enemy_turn()
|
return self.enemy_turn()
|
||||||
|
@ -395,7 +395,7 @@ class Crypt:
|
||||||
if self.player.try_move(-p[0], -p[1], test=True):
|
if self.player.try_move(-p[0], -p[1], test=True):
|
||||||
old_pos = self.player.draw_pos
|
old_pos = self.player.draw_pos
|
||||||
self.player.try_move(-p[0], -p[1])
|
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):
|
def swap_level(self, new_level, spawn_point):
|
||||||
self.level = new_level
|
self.level = new_level
|
||||||
|
@ -451,7 +451,7 @@ class SweetButton:
|
||||||
|
|
||||||
# main button caption
|
# main button caption
|
||||||
self.caption = mcrfpy.Caption((40, 3), caption, font, fill_color=font_color)
|
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_color=font_outline_color
|
||||||
self.caption.outline=font_outline_width
|
self.caption.outline=font_outline_width
|
||||||
self.main_button.children.append(self.caption)
|
self.main_button.children.append(self.caption)
|
||||||
|
@ -548,20 +548,20 @@ class MainMenu:
|
||||||
# title text
|
# title text
|
||||||
drop_shadow = mcrfpy.Caption((150, 10), "Crypt Of Sokoban", font, fill_color=(96, 96, 96), outline_color=(192, 0, 0))
|
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.outline = 3
|
||||||
drop_shadow.size = 64
|
drop_shadow.font_size = 64
|
||||||
components.append(
|
components.append(
|
||||||
drop_shadow
|
drop_shadow
|
||||||
)
|
)
|
||||||
|
|
||||||
title_txt = mcrfpy.Caption((158, 18), "Crypt Of Sokoban", font, fill_color=(255, 255, 255))
|
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(
|
components.append(
|
||||||
title_txt
|
title_txt
|
||||||
)
|
)
|
||||||
|
|
||||||
# toast: text over the demo grid that fades out on a timer
|
# 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 = 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 = 2
|
||||||
self.toast.outline_color = (255, 255, 255)
|
self.toast.outline_color = (255, 255, 255)
|
||||||
self.toast_event = None
|
self.toast_event = None
|
||||||
|
@ -626,6 +626,7 @@ class MainMenu:
|
||||||
def play(self, sweet_btn, args):
|
def play(self, sweet_btn, args):
|
||||||
#if args[3] == "start": return # DRAMATIC on release action!
|
#if args[3] == "start": return # DRAMATIC on release action!
|
||||||
if args[3] == "end": return
|
if args[3] == "end": return
|
||||||
|
mcrfpy.delTimer("demo_motion") # Clean up the demo timer
|
||||||
self.crypt = Crypt()
|
self.crypt = Crypt()
|
||||||
#mcrfpy.setScene("play")
|
#mcrfpy.setScene("play")
|
||||||
self.crypt.start()
|
self.crypt.start()
|
||||||
|
|
|
@ -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()
|
|
@ -258,8 +258,9 @@ def demo_grid_animations(ui):
|
||||||
except:
|
except:
|
||||||
texture = None
|
texture = None
|
||||||
|
|
||||||
grid = Grid(100, 150, grid_size=(20, 15), texture=texture,
|
# Grid constructor: Grid(grid_x, grid_y, texture, position, size)
|
||||||
tile_width=24, tile_height=24)
|
# 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)
|
grid.fill_color = Color(20, 20, 40)
|
||||||
ui.append(grid)
|
ui.append(grid)
|
||||||
|
|
||||||
|
@ -282,7 +283,7 @@ def demo_grid_animations(ui):
|
||||||
|
|
||||||
# Create entities in the grid
|
# Create entities in the grid
|
||||||
if texture:
|
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
|
entity1.scale = 1.5
|
||||||
grid.entities.append(entity1)
|
grid.entities.append(entity1)
|
||||||
|
|
||||||
|
@ -291,7 +292,7 @@ def demo_grid_animations(ui):
|
||||||
entity_pos.start(entity1)
|
entity_pos.start(entity1)
|
||||||
|
|
||||||
# Create patrolling entity
|
# 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)
|
grid.entities.append(entity2)
|
||||||
|
|
||||||
# Animate sprite changes
|
# Animate sprite changes
|
||||||
|
|
|
@ -183,7 +183,7 @@ def clear_scene():
|
||||||
|
|
||||||
# Keep only the first two elements (title and subtitle)
|
# Keep only the first two elements (title and subtitle)
|
||||||
while len(ui) > 2:
|
while len(ui) > 2:
|
||||||
ui.remove(ui[2])
|
ui.remove(2)
|
||||||
|
|
||||||
def run_demo_sequence(runtime):
|
def run_demo_sequence(runtime):
|
||||||
"""Run through all demos"""
|
"""Run through all demos"""
|
||||||
|
|
|
@ -268,8 +268,6 @@ def run_next_demo(runtime):
|
||||||
# Clean up timers from previous demo
|
# Clean up timers from previous demo
|
||||||
for timer in ["opacity_0", "opacity_1", "opacity_2", "opacity_3",
|
for timer in ["opacity_0", "opacity_1", "opacity_2", "opacity_3",
|
||||||
"c_green", "c_blue", "c_white"]:
|
"c_green", "c_blue", "c_white"]:
|
||||||
if not mcrfpy.getTimer(timer):
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
mcrfpy.delTimer(timer)
|
mcrfpy.delTimer(timer)
|
||||||
except:
|
except:
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -48,6 +48,10 @@ mode = "CHASE"
|
||||||
show_dijkstra = False
|
show_dijkstra = False
|
||||||
animation_speed = 3.0
|
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():
|
def create_dungeon():
|
||||||
"""Create a dungeon-like map"""
|
"""Create a dungeon-like map"""
|
||||||
global grid
|
global grid
|
||||||
|
@ -126,37 +130,34 @@ def spawn_entities():
|
||||||
global player, enemies, treasures, patrol_entities
|
global player, enemies, treasures, patrol_entities
|
||||||
|
|
||||||
# Clear existing entities
|
# Clear existing entities
|
||||||
grid.entities.clear()
|
#grid.entities.clear()
|
||||||
enemies = []
|
enemies = []
|
||||||
treasures = []
|
treasures = []
|
||||||
patrol_entities = []
|
patrol_entities = []
|
||||||
|
|
||||||
# Spawn player in center room
|
# Spawn player in center room
|
||||||
player = mcrfpy.Entity(15, 11)
|
player = mcrfpy.Entity((15, 11), mcrfpy.default_texture, PLAYER)
|
||||||
player.sprite_index = PLAYER
|
|
||||||
grid.entities.append(player)
|
grid.entities.append(player)
|
||||||
|
|
||||||
# Spawn enemies in corners
|
# Spawn enemies in corners
|
||||||
enemy_positions = [(4, 4), (24, 4), (4, 16), (24, 16)]
|
enemy_positions = [(4, 4), (24, 4), (4, 16), (24, 16)]
|
||||||
for x, y in enemy_positions:
|
for x, y in enemy_positions:
|
||||||
enemy = mcrfpy.Entity(x, y)
|
enemy = mcrfpy.Entity((x, y), mcrfpy.default_texture, ENEMY)
|
||||||
enemy.sprite_index = ENEMY
|
|
||||||
grid.entities.append(enemy)
|
grid.entities.append(enemy)
|
||||||
enemies.append(enemy)
|
enemies.append(enemy)
|
||||||
|
|
||||||
# Spawn treasures
|
# Spawn treasures
|
||||||
treasure_positions = [(6, 5), (24, 5), (15, 10)]
|
treasure_positions = [(6, 5), (24, 5), (15, 10)]
|
||||||
for x, y in treasure_positions:
|
for x, y in treasure_positions:
|
||||||
treasure = mcrfpy.Entity(x, y)
|
treasure = mcrfpy.Entity((x, y), mcrfpy.default_texture, TREASURE)
|
||||||
treasure.sprite_index = TREASURE
|
|
||||||
grid.entities.append(treasure)
|
grid.entities.append(treasure)
|
||||||
treasures.append(treasure)
|
treasures.append(treasure)
|
||||||
|
|
||||||
# Spawn patrol entities
|
# Spawn patrol entities
|
||||||
patrol = mcrfpy.Entity(10, 10)
|
patrol = mcrfpy.Entity((10, 10), mcrfpy.default_texture, PATROL)
|
||||||
patrol.sprite_index = PATROL
|
# Store waypoints separately since Entity doesn't support custom attributes
|
||||||
patrol.waypoints = [(10, 10), (19, 10), (19, 16), (10, 16)] # Square patrol
|
entity_waypoints[patrol] = [(10, 10), (19, 10), (19, 16), (10, 16)] # Square patrol
|
||||||
patrol.waypoint_index = 0
|
entity_waypoint_indices[patrol] = 0
|
||||||
grid.entities.append(patrol)
|
grid.entities.append(patrol)
|
||||||
patrol_entities.append(patrol)
|
patrol_entities.append(patrol)
|
||||||
|
|
||||||
|
@ -222,18 +223,21 @@ def move_enemies(dt):
|
||||||
def move_patrols(dt):
|
def move_patrols(dt):
|
||||||
"""Move patrol entities along waypoints"""
|
"""Move patrol entities along waypoints"""
|
||||||
for patrol in patrol_entities:
|
for patrol in patrol_entities:
|
||||||
if not hasattr(patrol, 'waypoints'):
|
if patrol not in entity_waypoints:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get current waypoint
|
# 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
|
# Check if reached waypoint
|
||||||
dist = abs(patrol.x - target_x) + abs(patrol.y - target_y)
|
dist = abs(patrol.x - target_x) + abs(patrol.y - target_y)
|
||||||
if dist < 0.5:
|
if dist < 0.5:
|
||||||
# Move to next waypoint
|
# Move to next waypoint
|
||||||
patrol.waypoint_index = (patrol.waypoint_index + 1) % len(patrol.waypoints)
|
entity_waypoint_indices[patrol] = (waypoint_index + 1) % len(waypoints)
|
||||||
target_x, target_y = patrol.waypoints[patrol.waypoint_index]
|
waypoint_index = entity_waypoint_indices[patrol]
|
||||||
|
target_x, target_y = waypoints[waypoint_index]
|
||||||
|
|
||||||
# Path to waypoint
|
# Path to waypoint
|
||||||
path = patrol.path_to(target_x, target_y)
|
path = patrol.path_to(target_x, target_y)
|
||||||
|
|
|
@ -28,11 +28,11 @@ class TextInput:
|
||||||
# Label
|
# Label
|
||||||
if self.label:
|
if self.label:
|
||||||
self.label_caption = mcrfpy.Caption(self.label, self.x, self.y - 20)
|
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
|
# Text display
|
||||||
self.text_caption = mcrfpy.Caption("", self.x + 4, self.y + 4)
|
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)
|
# Cursor (a simple vertical line using a frame)
|
||||||
self.cursor = mcrfpy.Frame(self.x + 4, self.y + 4, 2, 16)
|
self.cursor = mcrfpy.Frame(self.x + 4, self.y + 4, 2, 16)
|
||||||
|
@ -176,7 +176,7 @@ def create_scene():
|
||||||
|
|
||||||
# Title
|
# Title
|
||||||
title = mcrfpy.Caption("Text Input Widget Demo", 10, 10)
|
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)
|
scene.append(title)
|
||||||
|
|
||||||
# Create input fields
|
# Create input fields
|
||||||
|
@ -194,7 +194,7 @@ def create_scene():
|
||||||
|
|
||||||
# Status text
|
# Status text
|
||||||
status = mcrfpy.Caption("Click to focus, type to enter text", 50, 280)
|
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)
|
scene.append(status)
|
||||||
|
|
||||||
# Keyboard handler
|
# Keyboard handler
|
||||||
|
|
|
@ -5,12 +5,19 @@ McRogueFace Animation Sizzle Reel - Final Version
|
||||||
|
|
||||||
Complete demonstration of all animation capabilities.
|
Complete demonstration of all animation capabilities.
|
||||||
This version works properly with the game loop and avoids API issues.
|
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
|
import mcrfpy
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
DEMO_DURATION = 4.0 # Duration for each demo
|
DEMO_DURATION = 6.0 # Duration for each demo
|
||||||
|
|
||||||
# All available easing functions
|
# All available easing functions
|
||||||
EASING_FUNCTIONS = [
|
EASING_FUNCTIONS = [
|
||||||
|
@ -41,6 +48,7 @@ def create_scene():
|
||||||
title = mcrfpy.Caption("Animation Sizzle Reel", 500, 20)
|
title = mcrfpy.Caption("Animation Sizzle Reel", 500, 20)
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 0)
|
title.fill_color = mcrfpy.Color(255, 255, 0)
|
||||||
title.outline = 2
|
title.outline = 2
|
||||||
|
title.font_size = 28
|
||||||
ui.append(title)
|
ui.append(title)
|
||||||
|
|
||||||
# Subtitle
|
# Subtitle
|
||||||
|
@ -79,18 +87,21 @@ def demo2_caption_animations():
|
||||||
# Moving caption
|
# Moving caption
|
||||||
c1 = mcrfpy.Caption("Bouncing Text!", 100, 200)
|
c1 = mcrfpy.Caption("Bouncing Text!", 100, 200)
|
||||||
c1.fill_color = mcrfpy.Color(255, 255, 255)
|
c1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
c1.font_size = 28
|
||||||
ui.append(c1)
|
ui.append(c1)
|
||||||
mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1)
|
mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1)
|
||||||
|
|
||||||
# Color cycling
|
# Color cycling
|
||||||
c2 = mcrfpy.Caption("Color Cycle", 400, 300)
|
c2 = mcrfpy.Caption("Color Cycle", 400, 300)
|
||||||
c2.outline = 2
|
c2.outline = 2
|
||||||
|
c2.font_size = 28
|
||||||
ui.append(c2)
|
ui.append(c2)
|
||||||
mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear").start(c2)
|
mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear").start(c2)
|
||||||
|
|
||||||
# Typewriter effect
|
# Typewriter effect
|
||||||
c3 = mcrfpy.Caption("", 100, 400)
|
c3 = mcrfpy.Caption("", 100, 400)
|
||||||
c3.fill_color = mcrfpy.Color(0, 255, 255)
|
c3.fill_color = mcrfpy.Color(0, 255, 255)
|
||||||
|
c3.font_size = 28
|
||||||
ui.append(c3)
|
ui.append(c3)
|
||||||
mcrfpy.Animation("text", "Typewriter effect animation...", 3.0, "linear").start(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)
|
# Keep removing items after the first 2 (title and subtitle)
|
||||||
while len(ui) > 2:
|
while len(ui) > 2:
|
||||||
# Remove the last item
|
# Remove the last item
|
||||||
ui.remove(ui[len(ui)-1])
|
ui.remove(len(ui)-1)
|
||||||
|
|
||||||
def next_demo(runtime):
|
def next_demo(runtime):
|
||||||
"""Run the next demo"""
|
"""Run the next demo"""
|
||||||
|
@ -167,11 +178,13 @@ def next_demo(runtime):
|
||||||
current_demo += 1
|
current_demo += 1
|
||||||
|
|
||||||
if current_demo < len(demos):
|
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:
|
else:
|
||||||
subtitle.text = "Demo Complete!"
|
subtitle.text = "Demo Complete!"
|
||||||
|
|
||||||
# Initialize
|
# Initialize
|
||||||
print("Starting Animation Sizzle Reel...")
|
print("Starting Animation Sizzle Reel...")
|
||||||
create_scene()
|
create_scene()
|
||||||
mcrfpy.setTimer("start", next_demo, 500)
|
mcrfpy.setTimer("start", next_demo, int(DEMO_DURATION * 1000))
|
||||||
|
next_demo(0)
|
||||||
|
|
|
@ -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)
|
|
@ -60,12 +60,12 @@ def create_demo():
|
||||||
scene.append(bg)
|
scene.append(bg)
|
||||||
|
|
||||||
# Title
|
# 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)
|
title.color = (255, 255, 255, 255)
|
||||||
scene.append(title)
|
scene.append(title)
|
||||||
|
|
||||||
# Instructions
|
# 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)
|
instructions.color = (200, 200, 200, 255)
|
||||||
scene.append(instructions)
|
scene.append(instructions)
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ def create_demo():
|
||||||
fields.append(comment_input)
|
fields.append(comment_input)
|
||||||
|
|
||||||
# Result display
|
# 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)
|
result_text.color = (150, 255, 150, 255)
|
||||||
scene.append(result_text)
|
scene.append(result_text)
|
||||||
|
|
||||||
|
|
|
@ -79,8 +79,7 @@ class TextInput:
|
||||||
self.label_text = mcrfpy.Caption(
|
self.label_text = mcrfpy.Caption(
|
||||||
self.x - 5,
|
self.x - 5,
|
||||||
self.y - self.font_size - 5,
|
self.y - self.font_size - 5,
|
||||||
self.label,
|
self.label
|
||||||
font_size=self.font_size
|
|
||||||
)
|
)
|
||||||
self.label_text.color = (255, 255, 255, 255)
|
self.label_text.color = (255, 255, 255, 255)
|
||||||
|
|
||||||
|
@ -88,8 +87,7 @@ class TextInput:
|
||||||
self.text_display = mcrfpy.Caption(
|
self.text_display = mcrfpy.Caption(
|
||||||
self.x + 4,
|
self.x + 4,
|
||||||
self.y + 4,
|
self.y + 4,
|
||||||
"",
|
""
|
||||||
font_size=self.font_size
|
|
||||||
)
|
)
|
||||||
self.text_display.color = (0, 0, 0, 255)
|
self.text_display.color = (0, 0, 0, 255)
|
||||||
|
|
||||||
|
@ -260,12 +258,12 @@ def create_demo():
|
||||||
scene.append(bg)
|
scene.append(bg)
|
||||||
|
|
||||||
# Title
|
# 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)
|
title.color = (255, 255, 255, 255)
|
||||||
scene.append(title)
|
scene.append(title)
|
||||||
|
|
||||||
# Instructions
|
# 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)
|
info.color = (200, 200, 200, 255)
|
||||||
scene.append(info)
|
scene.append(info)
|
||||||
|
|
||||||
|
@ -289,7 +287,7 @@ def create_demo():
|
||||||
comment_input.add_to_scene(scene)
|
comment_input.add_to_scene(scene)
|
||||||
|
|
||||||
# Status display
|
# 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)
|
status.color = (150, 255, 150, 255)
|
||||||
scene.append(status)
|
scene.append(status)
|
||||||
|
|
||||||
|
|
|
@ -95,8 +95,7 @@ class TextInput:
|
||||||
self.label_text = mcrfpy.Caption(
|
self.label_text = mcrfpy.Caption(
|
||||||
self.x - 5,
|
self.x - 5,
|
||||||
self.y - self.font_size - 5,
|
self.y - self.font_size - 5,
|
||||||
self.label,
|
self.label
|
||||||
font_size=self.font_size
|
|
||||||
)
|
)
|
||||||
self.label_text.color = (255, 255, 255, 255)
|
self.label_text.color = (255, 255, 255, 255)
|
||||||
|
|
||||||
|
@ -104,8 +103,7 @@ class TextInput:
|
||||||
self.text_display = mcrfpy.Caption(
|
self.text_display = mcrfpy.Caption(
|
||||||
self.x + 4,
|
self.x + 4,
|
||||||
self.y + 4,
|
self.y + 4,
|
||||||
"",
|
""
|
||||||
font_size=self.font_size
|
|
||||||
)
|
)
|
||||||
self.text_display.color = (0, 0, 0, 255)
|
self.text_display.color = (0, 0, 0, 255)
|
||||||
|
|
||||||
|
@ -227,12 +225,12 @@ def create_demo():
|
||||||
scene.append(bg)
|
scene.append(bg)
|
||||||
|
|
||||||
# Title
|
# 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)
|
title.color = (255, 255, 255, 255)
|
||||||
scene.append(title)
|
scene.append(title)
|
||||||
|
|
||||||
# Instructions
|
# 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)
|
instructions.color = (200, 200, 200, 255)
|
||||||
scene.append(instructions)
|
scene.append(instructions)
|
||||||
|
|
||||||
|
@ -276,7 +274,7 @@ def create_demo():
|
||||||
fields.append(comment_input)
|
fields.append(comment_input)
|
||||||
|
|
||||||
# Result display
|
# 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)
|
result_text.color = (150, 255, 150, 255)
|
||||||
scene.append(result_text)
|
scene.append(result_text)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue