diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..b88733b --- /dev/null +++ b/ROADMAP.md @@ -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 entity_perspectives; + bool batch_mode = false; + std::vector 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* diff --git a/roguelike_tutorial/part_0.py b/roguelike_tutorial/part_0.py new file mode 100644 index 0000000..eb9ed94 --- /dev/null +++ b/roguelike_tutorial/part_0.py @@ -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})") diff --git a/roguelike_tutorial/part_1.py b/roguelike_tutorial/part_1.py new file mode 100644 index 0000000..4c19d6d --- /dev/null +++ b/roguelike_tutorial/part_1.py @@ -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!") diff --git a/roguelike_tutorial/part_1b.py b/roguelike_tutorial/part_1b.py new file mode 100644 index 0000000..3894fc7 --- /dev/null +++ b/roguelike_tutorial/part_1b.py @@ -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!") diff --git a/roguelike_tutorial/part_2-naive.py b/roguelike_tutorial/part_2-naive.py new file mode 100644 index 0000000..6959a4b --- /dev/null +++ b/roguelike_tutorial/part_2-naive.py @@ -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!") diff --git a/roguelike_tutorial/part_2-onemovequeued.py b/roguelike_tutorial/part_2-onemovequeued.py new file mode 100644 index 0000000..126c433 --- /dev/null +++ b/roguelike_tutorial/part_2-onemovequeued.py @@ -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!") diff --git a/roguelike_tutorial/part_2.py b/roguelike_tutorial/part_2.py new file mode 100644 index 0000000..66a11b0 --- /dev/null +++ b/roguelike_tutorial/part_2.py @@ -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!") diff --git a/roguelike_tutorial/tutorial2.png b/roguelike_tutorial/tutorial2.png new file mode 100644 index 0000000..e785419 Binary files /dev/null and b/roguelike_tutorial/tutorial2.png differ diff --git a/roguelike_tutorial/tutorial_hero.png b/roguelike_tutorial/tutorial_hero.png new file mode 100644 index 0000000..c202176 Binary files /dev/null and b/roguelike_tutorial/tutorial_hero.png differ diff --git a/src/Animation.cpp b/src/Animation.cpp index 7fa27ce..20b8fad 100644 --- a/src/Animation.cpp +++ b/src/Animation.cpp @@ -1,6 +1,8 @@ #include "Animation.h" #include "UIDrawable.h" #include "UIEntity.h" +#include "PyAnimation.h" +#include "McRFPy_API.h" #include #include #include @@ -9,75 +11,100 @@ #define M_PI 3.14159265358979323846 #endif +// Forward declaration of PyAnimation type +namespace mcrfpydef { + extern PyTypeObject PyAnimationType; +} + // Animation implementation Animation::Animation(const std::string& targetProperty, const AnimationValue& targetValue, float duration, EasingFunction easingFunc, - bool delta) + bool delta, + PyObject* callback) : targetProperty(targetProperty) , targetValue(targetValue) , duration(duration) , easingFunc(easingFunc) , delta(delta) + , pythonCallback(callback) { + // Increase reference count for Python callback + if (pythonCallback) { + Py_INCREF(pythonCallback); + } } -void Animation::start(UIDrawable* target) { - currentTarget = target; +Animation::~Animation() { + // Decrease reference count for Python callback if we still own it + PyObject* callback = pythonCallback; + if (callback) { + pythonCallback = nullptr; + + PyGILState_STATE gstate = PyGILState_Ensure(); + Py_DECREF(callback); + PyGILState_Release(gstate); + } +} + +void Animation::start(std::shared_ptr target) { + if (!target) return; + + targetWeak = target; elapsed = 0.0f; + callbackTriggered = false; // Reset callback state - // Capture startValue from target based on targetProperty - if (!currentTarget) return; - - // Try to get the current value based on the expected type - std::visit([this](const auto& targetVal) { + // Capture start value from target + std::visit([this, &target](const auto& targetVal) { using T = std::decay_t; if constexpr (std::is_same_v) { float value; - if (currentTarget->getProperty(targetProperty, value)) { + if (target->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v) { int value; - if (currentTarget->getProperty(targetProperty, value)) { + if (target->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v>) { // For sprite animation, get current sprite index int value; - if (currentTarget->getProperty(targetProperty, value)) { + if (target->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v) { sf::Color value; - if (currentTarget->getProperty(targetProperty, value)) { + if (target->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v) { sf::Vector2f value; - if (currentTarget->getProperty(targetProperty, value)) { + if (target->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v) { std::string value; - if (currentTarget->getProperty(targetProperty, value)) { + if (target->getProperty(targetProperty, value)) { startValue = value; } } }, targetValue); } -void Animation::startEntity(UIEntity* target) { - currentEntityTarget = target; - currentTarget = nullptr; // Clear drawable target +void Animation::startEntity(std::shared_ptr target) { + if (!target) return; + + entityTargetWeak = target; elapsed = 0.0f; + callbackTriggered = false; // Reset callback state // Capture the starting value from the entity std::visit([this, target](const auto& val) { @@ -99,8 +126,49 @@ void Animation::startEntity(UIEntity* target) { }, targetValue); } +bool Animation::hasValidTarget() const { + return !targetWeak.expired() || !entityTargetWeak.expired(); +} + +void Animation::clearCallback() { + // Safely clear the callback when PyAnimation is being destroyed + PyObject* callback = pythonCallback; + if (callback) { + pythonCallback = nullptr; + callbackTriggered = true; // Prevent future triggering + + PyGILState_STATE gstate = PyGILState_Ensure(); + Py_DECREF(callback); + PyGILState_Release(gstate); + } +} + +void Animation::complete() { + // Jump to end of animation + elapsed = duration; + + // Apply final value + if (auto target = targetWeak.lock()) { + AnimationValue finalValue = interpolate(1.0f); + applyValue(target.get(), finalValue); + } + else if (auto entity = entityTargetWeak.lock()) { + AnimationValue finalValue = interpolate(1.0f); + applyValue(entity.get(), finalValue); + } +} + bool Animation::update(float deltaTime) { - if ((!currentTarget && !currentEntityTarget) || isComplete()) { + // Try to lock weak_ptr to get shared_ptr + std::shared_ptr target = targetWeak.lock(); + std::shared_ptr entity = entityTargetWeak.lock(); + + // If both are null, target was destroyed + if (!target && !entity) { + return false; // Remove this animation + } + + if (isComplete()) { return false; } @@ -114,39 +182,18 @@ bool Animation::update(float deltaTime) { // Get interpolated value AnimationValue currentValue = interpolate(easedT); - // Apply currentValue to target (either drawable or entity) - std::visit([this](const auto& value) { - using T = std::decay_t; - - if (currentTarget) { - // Handle UIDrawable targets - if constexpr (std::is_same_v) { - currentTarget->setProperty(targetProperty, value); - } - else if constexpr (std::is_same_v) { - currentTarget->setProperty(targetProperty, value); - } - else if constexpr (std::is_same_v) { - currentTarget->setProperty(targetProperty, value); - } - else if constexpr (std::is_same_v) { - currentTarget->setProperty(targetProperty, value); - } - else if constexpr (std::is_same_v) { - currentTarget->setProperty(targetProperty, value); - } - } - else if (currentEntityTarget) { - // Handle UIEntity targets - if constexpr (std::is_same_v) { - currentEntityTarget->setProperty(targetProperty, value); - } - else if constexpr (std::is_same_v) { - currentEntityTarget->setProperty(targetProperty, value); - } - // Entities don't support other types yet - } - }, currentValue); + // Apply to whichever target is valid + if (target) { + applyValue(target.get(), currentValue); + } else if (entity) { + applyValue(entity.get(), currentValue); + } + + // Trigger callback when animation completes + // Check pythonCallback again in case it was cleared during update + if (isComplete() && !callbackTriggered && pythonCallback) { + triggerCallback(); + } return !isComplete(); } @@ -254,6 +301,77 @@ AnimationValue Animation::interpolate(float t) const { }, targetValue); } +void Animation::applyValue(UIDrawable* target, const AnimationValue& value) { + if (!target) return; + + std::visit([this, target](const auto& val) { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + target->setProperty(targetProperty, val); + } + else if constexpr (std::is_same_v) { + target->setProperty(targetProperty, val); + } + else if constexpr (std::is_same_v) { + target->setProperty(targetProperty, val); + } + else if constexpr (std::is_same_v) { + target->setProperty(targetProperty, val); + } + else if constexpr (std::is_same_v) { + 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; + + if constexpr (std::is_same_v) { + entity->setProperty(targetProperty, val); + } + else if constexpr (std::is_same_v) { + entity->setProperty(targetProperty, val); + } + // Entities don't support other types yet + }, value); +} + +void Animation::triggerCallback() { + if (!pythonCallback) return; + + // Ensure we only trigger once + if (callbackTriggered) return; + callbackTriggered = true; + + PyGILState_STATE gstate = PyGILState_Ensure(); + + // TODO: In future, create PyAnimation wrapper for this animation + // For now, pass None for both parameters + PyObject* args = PyTuple_New(2); + Py_INCREF(Py_None); + Py_INCREF(Py_None); + PyTuple_SetItem(args, 0, Py_None); // animation parameter + PyTuple_SetItem(args, 1, Py_None); // target parameter + + PyObject* result = PyObject_CallObject(pythonCallback, args); + Py_DECREF(args); + + if (!result) { + // Print error but don't crash + PyErr_Print(); + PyErr_Clear(); // Clear the error state + } else { + Py_DECREF(result); + } + + PyGILState_Release(gstate); +} + // Easing functions implementation namespace EasingFunctions { @@ -502,26 +620,50 @@ AnimationManager& AnimationManager::getInstance() { } void AnimationManager::addAnimation(std::shared_ptr animation) { - activeAnimations.push_back(animation); + if (animation && animation->hasValidTarget()) { + if (isUpdating) { + // Defer adding during update to avoid iterator invalidation + pendingAnimations.push_back(animation); + } else { + activeAnimations.push_back(animation); + } + } } void AnimationManager::update(float deltaTime) { - for (auto& anim : activeAnimations) { - anim->update(deltaTime); - } - cleanup(); -} - -void AnimationManager::cleanup() { + // Set flag to defer new animations + isUpdating = true; + + // Remove completed or invalid animations activeAnimations.erase( std::remove_if(activeAnimations.begin(), activeAnimations.end(), - [](const std::shared_ptr& anim) { - return anim->isComplete(); + [deltaTime](std::shared_ptr& anim) { + return !anim || !anim->update(deltaTime); }), activeAnimations.end() ); + + // Clear update flag + isUpdating = false; + + // Add any animations that were created during update + if (!pendingAnimations.empty()) { + activeAnimations.insert(activeAnimations.end(), + pendingAnimations.begin(), + pendingAnimations.end()); + pendingAnimations.clear(); + } } -void AnimationManager::clear() { + +void AnimationManager::clear(bool completeAnimations) { + if (completeAnimations) { + // Complete all animations before clearing + for (auto& anim : activeAnimations) { + if (anim) { + anim->complete(); + } + } + } activeAnimations.clear(); } \ No newline at end of file diff --git a/src/Animation.h b/src/Animation.h index 6308f32..181bec4 100644 --- a/src/Animation.h +++ b/src/Animation.h @@ -6,6 +6,7 @@ #include #include #include +#include "Python.h" // Forward declarations class UIDrawable; @@ -36,13 +37,20 @@ public: const AnimationValue& targetValue, float duration, EasingFunction easingFunc = EasingFunctions::linear, - bool delta = false); + bool delta = false, + PyObject* callback = nullptr); + + // Destructor - cleanup Python callback reference + ~Animation(); // Apply this animation to a drawable - void start(UIDrawable* target); + void start(std::shared_ptr target); // Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable) - void startEntity(UIEntity* target); + void startEntity(std::shared_ptr target); + + // Complete the animation immediately (jump to final value) + void complete(); // Update animation (called each frame) // Returns true if animation is still running, false if complete @@ -51,6 +59,12 @@ public: // Get current interpolated value AnimationValue getCurrentValue() const; + // Check if animation has valid target + bool hasValidTarget() const; + + // Clear the callback (called when PyAnimation is deallocated) + void clearCallback(); + // Animation properties std::string getTargetProperty() const { return targetProperty; } float getDuration() const { return duration; } @@ -67,11 +81,24 @@ private: EasingFunction easingFunc; // Easing function to use bool delta; // If true, targetValue is relative to start - UIDrawable* currentTarget = nullptr; // Current target being animated - UIEntity* currentEntityTarget = nullptr; // Current entity target (alternative to drawable) + // RAII: Use weak_ptr for safe target tracking + std::weak_ptr targetWeak; + std::weak_ptr entityTargetWeak; + + // Callback support + PyObject* pythonCallback = nullptr; // Python callback function (we own a reference) + bool callbackTriggered = false; // Ensure callback only fires once + PyObject* pyAnimationWrapper = nullptr; // Weak reference to PyAnimation if created from Python // Helper to interpolate between values AnimationValue interpolate(float t) const; + + // Helper to apply value to target + void applyValue(UIDrawable* target, const AnimationValue& value); + void applyValue(UIEntity* entity, const AnimationValue& value); + + // Trigger callback when animation completes + void triggerCallback(); }; // Easing functions library @@ -134,13 +161,12 @@ public: // Update all animations void update(float deltaTime); - // Remove completed animations - void cleanup(); - - // Clear all animations - void clear(); + // Clear all animations (optionally completing them first) + void clear(bool completeAnimations = false); private: AnimationManager() = default; std::vector> activeAnimations; + std::vector> pendingAnimations; // Animations to add after update + bool isUpdating = false; // Flag to track if we're in update loop }; \ No newline at end of file diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 5b35d79..dcba0e4 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -16,7 +16,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) { Resources::font.loadFromFile("./assets/JetbrainsMono.ttf"); Resources::game = this; - window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine"; + window_title = "McRogueFace Engine"; // Initialize rendering based on headless mode if (headless) { @@ -91,6 +91,9 @@ void GameEngine::cleanup() if (cleaned_up) return; cleaned_up = true; + // Clear all animations first (RAII handles invalidation) + AnimationManager::getInstance().clear(); + // Clear Python references before destroying C++ objects // Clear all timers (they hold Python callables) timers.clear(); @@ -182,7 +185,7 @@ void GameEngine::setWindowScale(float multiplier) void GameEngine::run() { - std::cout << "GameEngine::run() starting main loop..." << std::endl; + //std::cout << "GameEngine::run() starting main loop..." << std::endl; float fps = 0.0; frameTime = 0.016f; // Initialize to ~60 FPS clock.restart(); @@ -259,7 +262,7 @@ void GameEngine::run() int tenth_fps = (metrics.fps * 10) % 10; if (!headless && window) { - window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS"); + window->setTitle(window_title); } // In windowed mode, check if window was closed diff --git a/src/PyAnimation.cpp b/src/PyAnimation.cpp index 720b8d9..d45c6eb 100644 --- a/src/PyAnimation.cpp +++ b/src/PyAnimation.cpp @@ -18,19 +18,31 @@ PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds } int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) { - static const char* keywords[] = {"property", "target", "duration", "easing", "delta", nullptr}; + static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr}; const char* property_name; PyObject* target_value; float duration; const char* easing_name = "linear"; int delta = 0; + PyObject* callback = nullptr; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|sp", const_cast(keywords), - &property_name, &target_value, &duration, &easing_name, &delta)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast(keywords), + &property_name, &target_value, &duration, &easing_name, &delta, &callback)) { return -1; } + // Validate callback is callable if provided + if (callback && callback != Py_None && !PyCallable_Check(callback)) { + PyErr_SetString(PyExc_TypeError, "callback must be callable"); + return -1; + } + + // Convert None to nullptr for C++ + if (callback == Py_None) { + callback = nullptr; + } + // Convert Python target value to AnimationValue AnimationValue animValue; @@ -90,7 +102,7 @@ int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) { EasingFunction easingFunc = EasingFunctions::getByName(easing_name); // Create the Animation - self->data = std::make_shared(property_name, animValue, duration, easingFunc, delta != 0); + self->data = std::make_shared(property_name, animValue, duration, easingFunc, delta != 0, callback); return 0; } @@ -126,50 +138,50 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) { return NULL; } - // Get the UIDrawable from the Python object - UIDrawable* drawable = nullptr; - // Check type by comparing type names const char* type_name = Py_TYPE(target_obj)->tp_name; if (strcmp(type_name, "mcrfpy.Frame") == 0) { PyUIFrameObject* frame = (PyUIFrameObject*)target_obj; - drawable = frame->data.get(); + if (frame->data) { + self->data->start(frame->data); + AnimationManager::getInstance().addAnimation(self->data); + } } else if (strcmp(type_name, "mcrfpy.Caption") == 0) { PyUICaptionObject* caption = (PyUICaptionObject*)target_obj; - drawable = caption->data.get(); + if (caption->data) { + self->data->start(caption->data); + AnimationManager::getInstance().addAnimation(self->data); + } } else if (strcmp(type_name, "mcrfpy.Sprite") == 0) { PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj; - drawable = sprite->data.get(); + if (sprite->data) { + self->data->start(sprite->data); + AnimationManager::getInstance().addAnimation(self->data); + } } else if (strcmp(type_name, "mcrfpy.Grid") == 0) { PyUIGridObject* grid = (PyUIGridObject*)target_obj; - drawable = grid->data.get(); + if (grid->data) { + self->data->start(grid->data); + AnimationManager::getInstance().addAnimation(self->data); + } } else if (strcmp(type_name, "mcrfpy.Entity") == 0) { // Special handling for Entity since it doesn't inherit from UIDrawable PyUIEntityObject* entity = (PyUIEntityObject*)target_obj; - // Start the animation directly on the entity - self->data->startEntity(entity->data.get()); - - // Add to AnimationManager - AnimationManager::getInstance().addAnimation(self->data); - - Py_RETURN_NONE; + if (entity->data) { + self->data->startEntity(entity->data); + AnimationManager::getInstance().addAnimation(self->data); + } } else { PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity"); return NULL; } - // Start the animation - self->data->start(drawable); - - // Add to AnimationManager - AnimationManager::getInstance().addAnimation(self->data); - Py_RETURN_NONE; } @@ -214,6 +226,20 @@ PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args }, value); } +PyObject* PyAnimation::complete(PyAnimationObject* self, PyObject* args) { + if (self->data) { + self->data->complete(); + } + Py_RETURN_NONE; +} + +PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args) { + if (self->data && self->data->hasValidTarget()) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; +} + PyGetSetDef PyAnimation::getsetters[] = { {"property", (getter)get_property, NULL, "Target property name", NULL}, {"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL}, @@ -225,10 +251,23 @@ PyGetSetDef PyAnimation::getsetters[] = { PyMethodDef PyAnimation::methods[] = { {"start", (PyCFunction)start, METH_VARARGS, - "Start the animation on a target UIDrawable"}, + "start(target) -> None\n\n" + "Start the animation on a target UI element.\n\n" + "Args:\n" + " target: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)\n\n" + "Note:\n" + " The animation will automatically stop if the target is destroyed."}, {"update", (PyCFunction)update, METH_VARARGS, "Update the animation by deltaTime (returns True if still running)"}, {"get_current_value", (PyCFunction)get_current_value, METH_NOARGS, "Get the current interpolated value"}, + {"complete", (PyCFunction)complete, METH_NOARGS, + "complete() -> None\n\n" + "Complete the animation immediately by jumping to the final value."}, + {"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS, + "hasValidTarget() -> bool\n\n" + "Check if the animation still has a valid target.\n\n" + "Returns:\n" + " True if the target still exists, False if it was destroyed."}, {NULL} }; \ No newline at end of file diff --git a/src/PyAnimation.h b/src/PyAnimation.h index 9976cb2..ccb4f36 100644 --- a/src/PyAnimation.h +++ b/src/PyAnimation.h @@ -28,6 +28,8 @@ public: static PyObject* start(PyAnimationObject* self, PyObject* args); static PyObject* update(PyAnimationObject* self, PyObject* args); static PyObject* get_current_value(PyAnimationObject* self, PyObject* args); + static PyObject* complete(PyAnimationObject* self, PyObject* args); + static PyObject* has_valid_target(PyAnimationObject* self, PyObject* args); static PyGetSetDef getsetters[]; static PyMethodDef methods[]; diff --git a/src/PyArgHelpers.h b/src/PyArgHelpers.h deleted file mode 100644 index d827789..0000000 --- a/src/PyArgHelpers.h +++ /dev/null @@ -1,410 +0,0 @@ -#pragma once -#include "Python.h" -#include "PyVector.h" -#include "PyColor.h" -#include -#include - -// 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); - } -} \ No newline at end of file diff --git a/src/PyScene.cpp b/src/PyScene.cpp index fb2a49e..84b92a7 100644 --- a/src/PyScene.cpp +++ b/src/PyScene.cpp @@ -31,13 +31,18 @@ void PyScene::do_mouse_input(std::string button, std::string type) // Convert window coordinates to game coordinates using the viewport auto mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos)); - // Create a sorted copy by z-index (highest first) - std::vector> sorted_elements(*ui_elements); - std::sort(sorted_elements.begin(), sorted_elements.end(), - [](const auto& a, const auto& b) { return a->z_index > b->z_index; }); + // Only sort if z_index values have changed + if (ui_elements_need_sort) { + // Sort in ascending order (same as render) + std::sort(ui_elements->begin(), ui_elements->end(), + [](const auto& a, const auto& b) { return a->z_index < b->z_index; }); + ui_elements_need_sort = false; + } - // Check elements in z-order (top to bottom) - for (const auto& element : sorted_elements) { + // Check elements in reverse z-order (highest z_index first, top to bottom) + // Use reverse iterators to go from end to beginning + for (auto it = ui_elements->rbegin(); it != ui_elements->rend(); ++it) { + const auto& element = *it; if (!element->visible) continue; if (auto target = element->click_at(sf::Vector2f(mousepos))) { diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 1df752a..07cd586 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -3,7 +3,6 @@ #include "PyColor.h" #include "PyVector.h" #include "PyFont.h" -#include "PyArgHelpers.h" // UIDrawable methods now in UIBase.h #include @@ -303,183 +302,135 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) { using namespace mcrfpydef; - // Try parsing with PyArgHelpers - int arg_idx = 0; - auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx); - - // Default values - float x = 0.0f, y = 0.0f, outline = 0.0f; - char* text = nullptr; + // Define all parameters with defaults + PyObject* pos_obj = nullptr; PyObject* font = nullptr; + const char* text = ""; PyObject* fill_color = nullptr; PyObject* outline_color = nullptr; + float outline = 0.0f; + float font_size = 16.0f; PyObject* click_handler = nullptr; + int visible = 1; + float opacity = 1.0f; + int z_index = 0; + const char* name = nullptr; + float x = 0.0f, y = 0.0f; - // Case 1: Got position from helpers (tuple format) - if (pos_result.valid) { - x = pos_result.x; - y = pos_result.y; - - // Parse remaining arguments - static const char* remaining_keywords[] = { - "text", "font", "fill_color", "outline_color", "outline", "click", nullptr - }; - - // Create new tuple with remaining args - Py_ssize_t total_args = PyTuple_Size(args); - PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args); - - if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|zOOOfO", - const_cast(remaining_keywords), - &text, &font, &fill_color, &outline_color, - &outline, &click_handler)) { - Py_DECREF(remaining_args); - if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error); + // Keywords list matches the new spec: positional args first, then all keyword args + static const char* kwlist[] = { + "pos", "font", "text", // Positional args (as per spec) + // Keyword-only args + "fill_color", "outline_color", "outline", "font_size", "click", + "visible", "opacity", "z_index", "name", "x", "y", + nullptr + }; + + // Parse arguments with | for optional positional args + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizff", const_cast(kwlist), + &pos_obj, &font, &text, // Positional + &fill_color, &outline_color, &outline, &font_size, &click_handler, + &visible, &opacity, &z_index, &name, &x, &y)) { + return -1; + } + + // Handle position argument (can be tuple, Vector, or use x/y keywords) + if (pos_obj) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (vec) { + x = vec->data.x; + y = vec->data.y; + Py_DECREF(vec); + } else { + PyErr_Clear(); + if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { + PyObject* x_val = PyTuple_GetItem(pos_obj, 0); + PyObject* y_val = PyTuple_GetItem(pos_obj, 1); + if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) && + (PyFloat_Check(y_val) || PyLong_Check(y_val))) { + x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); + y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); + } else { + PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers"); + return -1; + } + } else { + PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); + return -1; + } + } + } + + // Handle font argument + std::shared_ptr pyfont = nullptr; + if (font && font != Py_None) { + if (!PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font"))) { + PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance"); return -1; } - Py_DECREF(remaining_args); - } - // Case 2: Traditional format - else { - PyErr_Clear(); // Clear any errors from helpers - - // First check if this is the old (text, x, y, ...) format - PyObject* first_arg = args && PyTuple_Size(args) > 0 ? PyTuple_GetItem(args, 0) : nullptr; - bool text_first = first_arg && PyUnicode_Check(first_arg); - - if (text_first) { - // Pattern: (text, x, y, ...) - static const char* text_first_keywords[] = { - "text", "x", "y", "font", "fill_color", "outline_color", - "outline", "click", "pos", nullptr - }; - PyObject* pos_obj = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zffOOOfOO", - const_cast(text_first_keywords), - &text, &x, &y, &font, &fill_color, &outline_color, - &outline, &click_handler, &pos_obj)) { - return -1; - } - - // Handle pos keyword override - if (pos_obj && pos_obj != Py_None) { - if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { - PyObject* x_val = PyTuple_GetItem(pos_obj, 0); - PyObject* y_val = PyTuple_GetItem(pos_obj, 1); - if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) && - (PyFloat_Check(y_val) || PyLong_Check(y_val))) { - x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); - y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); - } - } else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString( - PyImport_ImportModule("mcrfpy"), "Vector"))) { - PyVectorObject* vec = (PyVectorObject*)pos_obj; - x = vec->data.x; - y = vec->data.y; - } else { - PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); - return -1; - } - } - } else { - // Pattern: (x, y, text, ...) - static const char* xy_keywords[] = { - "x", "y", "text", "font", "fill_color", "outline_color", - "outline", "click", "pos", nullptr - }; - PyObject* pos_obj = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOfOO", - const_cast(xy_keywords), - &x, &y, &text, &font, &fill_color, &outline_color, - &outline, &click_handler, &pos_obj)) { - return -1; - } - - // Handle pos keyword override - if (pos_obj && pos_obj != Py_None) { - if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { - PyObject* x_val = PyTuple_GetItem(pos_obj, 0); - PyObject* y_val = PyTuple_GetItem(pos_obj, 1); - if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) && - (PyFloat_Check(y_val) || PyLong_Check(y_val))) { - x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); - y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); - } - } else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString( - PyImport_ImportModule("mcrfpy"), "Vector"))) { - PyVectorObject* vec = (PyVectorObject*)pos_obj; - x = vec->data.x; - y = vec->data.y; - } else { - PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); - return -1; - } - } - } + auto obj = (PyFontObject*)font; + pyfont = obj->data; } - self->data->position = sf::Vector2f(x, y); // Set base class position - self->data->text.setPosition(self->data->position); // Sync text position - // check types for font, fill_color, outline_color - - //std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl; - if (font != NULL && font != Py_None && !PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font")/*(PyObject*)&PyFontType)*/)){ - PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance or None"); - return -1; - } else if (font != NULL && font != Py_None) - { - auto font_obj = (PyFontObject*)font; - self->data->text.setFont(font_obj->data->font); - self->font = font; - Py_INCREF(font); - } else - { + // Create the caption + self->data = std::make_shared(); + self->data->position = sf::Vector2f(x, y); + self->data->text.setPosition(self->data->position); + self->data->text.setOutlineThickness(outline); + + // Set the font + if (pyfont) { + self->data->text.setFont(pyfont->font); + } else { // Use default font when None or not provided if (McRFPy_API::default_font) { self->data->text.setFont(McRFPy_API::default_font->font); - // Store reference to default font - PyObject* default_font_obj = PyObject_GetAttrString(McRFPy_API::mcrf_module, "default_font"); - if (default_font_obj) { - self->font = default_font_obj; - // Don't need to DECREF since we're storing it - } } } - - // Handle text - default to empty string if not provided - if (text && text != NULL) { - self->data->text.setString((std::string)text); - } else { - self->data->text.setString(""); + + // Set character size + self->data->text.setCharacterSize(static_cast(font_size)); + + // Set text + if (text && strlen(text) > 0) { + self->data->text.setString(std::string(text)); } - self->data->text.setOutlineThickness(outline); - if (fill_color) { - auto fc = PyColor::from_arg(fill_color); - if (!fc) { - PyErr_SetString(PyExc_TypeError, "fill_color must be mcrfpy.Color or arguments to mcrfpy.Color.__init__"); + + // Handle fill_color + if (fill_color && fill_color != Py_None) { + PyColorObject* color_obj = PyColor::from_arg(fill_color); + if (!color_obj) { + PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple"); return -1; } - self->data->text.setFillColor(PyColor::fromPy(fc)); - //Py_DECREF(fc); + self->data->text.setFillColor(color_obj->data); + Py_DECREF(color_obj); } else { - self->data->text.setFillColor(sf::Color(0,0,0,255)); + self->data->text.setFillColor(sf::Color(255, 255, 255, 255)); // Default: white } - - if (outline_color) { - auto oc = PyColor::from_arg(outline_color); - if (!oc) { - PyErr_SetString(PyExc_TypeError, "outline_color must be mcrfpy.Color or arguments to mcrfpy.Color.__init__"); + + // Handle outline_color + if (outline_color && outline_color != Py_None) { + PyColorObject* color_obj = PyColor::from_arg(outline_color); + if (!color_obj) { + PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or color tuple"); return -1; } - self->data->text.setOutlineColor(PyColor::fromPy(oc)); - //Py_DECREF(oc); + self->data->text.setOutlineColor(color_obj->data); + Py_DECREF(color_obj); } else { - self->data->text.setOutlineColor(sf::Color(128,128,128,255)); + self->data->text.setOutlineColor(sf::Color(0, 0, 0, 255)); // Default: black } - - // Process click handler if provided + + // Set other properties + self->data->visible = visible; + self->data->opacity = opacity; + self->data->z_index = z_index; + if (name) { + self->data->name = std::string(name); + } + + // Handle click handler if (click_handler && click_handler != Py_None) { if (!PyCallable_Check(click_handler)) { PyErr_SetString(PyExc_TypeError, "click must be callable"); @@ -487,10 +438,11 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) } self->data->click_register(click_handler); } - + return 0; } + // Property system implementation for animations bool UICaption::setProperty(const std::string& name, float value) { if (name == "x") { diff --git a/src/UICaption.h b/src/UICaption.h index 9e29a35..95e3f1a 100644 --- a/src/UICaption.h +++ b/src/UICaption.h @@ -65,26 +65,37 @@ namespace mcrfpydef { //.tp_iter //.tp_iternext .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)\n\n" + .tp_doc = PyDoc_STR("Caption(pos=None, font=None, text='', **kwargs)\n\n" "A text display UI element with customizable font and styling.\n\n" "Args:\n" - " text (str): The text content to display. Default: ''\n" - " x (float): X position in pixels. Default: 0\n" - " y (float): Y position in pixels. Default: 0\n" - " font (Font): Font object for text rendering. Default: engine default font\n" + " pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n" + " font (Font, optional): Font object for text rendering. Default: engine default font\n" + " text (str, optional): The text content to display. Default: ''\n\n" + "Keyword Args:\n" " fill_color (Color): Text fill color. Default: (255, 255, 255, 255)\n" " outline_color (Color): Text outline color. Default: (0, 0, 0, 255)\n" " outline (float): Text outline thickness. Default: 0\n" - " click (callable): Click event handler. Default: None\n\n" + " font_size (float): Font size in points. Default: 16\n" + " click (callable): Click event handler. Default: None\n" + " visible (bool): Visibility state. Default: True\n" + " opacity (float): Opacity (0.0-1.0). Default: 1.0\n" + " z_index (int): Rendering order. Default: 0\n" + " name (str): Element name for finding. Default: None\n" + " x (float): X position override. Default: 0\n" + " y (float): Y position override. Default: 0\n\n" "Attributes:\n" " text (str): The displayed text content\n" " x, y (float): Position in pixels\n" + " pos (Vector): Position as a Vector object\n" " font (Font): Font used for rendering\n" + " font_size (float): Font size in points\n" " fill_color, outline_color (Color): Text appearance\n" " outline (float): Outline thickness\n" " click (callable): Click event handler\n" " visible (bool): Visibility state\n" + " opacity (float): Opacity value\n" " z_index (int): Rendering order\n" + " name (str): Element name\n" " w, h (float): Read-only computed size based on text and font"), .tp_methods = UICaption_methods, //.tp_members = PyUIFrame_members, diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 4143ed0..3d8397d 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -4,7 +4,6 @@ #include #include "PyObjectUtils.h" #include "PyVector.h" -#include "PyArgHelpers.h" // UIDrawable methods now in UIBase.h #include "UIEntityPyMethods.h" @@ -121,81 +120,57 @@ PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) } int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { - // Try parsing with PyArgHelpers for grid position - int arg_idx = 0; - auto grid_pos_result = PyArgHelpers::parseGridPosition(args, kwds, &arg_idx); - - // Default values - float grid_x = 0.0f, grid_y = 0.0f; - int sprite_index = 0; + // Define all parameters with defaults + PyObject* grid_pos_obj = nullptr; PyObject* texture = nullptr; + int sprite_index = 0; PyObject* grid_obj = nullptr; + int visible = 1; + float opacity = 1.0f; + const char* name = nullptr; + float x = 0.0f, y = 0.0f; - // Case 1: Got grid position from helpers (tuple format) - if (grid_pos_result.valid) { - grid_x = grid_pos_result.grid_x; - grid_y = grid_pos_result.grid_y; - - // Parse remaining arguments - static const char* remaining_keywords[] = { - "texture", "sprite_index", "grid", nullptr - }; - - // Create new tuple with remaining args - Py_ssize_t total_args = PyTuple_Size(args); - PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args); - - if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OiO", - const_cast(remaining_keywords), - &texture, &sprite_index, &grid_obj)) { - Py_DECREF(remaining_args); - if (grid_pos_result.error) PyErr_SetString(PyExc_TypeError, grid_pos_result.error); - return -1; - } - Py_DECREF(remaining_args); + // Keywords list matches the new spec: positional args first, then all keyword args + static const char* kwlist[] = { + "grid_pos", "texture", "sprite_index", // Positional args (as per spec) + // Keyword-only args + "grid", "visible", "opacity", "name", "x", "y", + nullptr + }; + + // Parse arguments with | for optional positional args + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOiOifzff", const_cast(kwlist), + &grid_pos_obj, &texture, &sprite_index, // Positional + &grid_obj, &visible, &opacity, &name, &x, &y)) { + return -1; } - // Case 2: Traditional format - else { - PyErr_Clear(); // Clear any errors from helpers - - static const char* keywords[] = { - "grid_x", "grid_y", "texture", "sprite_index", "grid", "grid_pos", nullptr - }; - PyObject* grid_pos_obj = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOiOO", - const_cast(keywords), - &grid_x, &grid_y, &texture, &sprite_index, - &grid_obj, &grid_pos_obj)) { - return -1; - } - - // Handle grid_pos keyword override - if (grid_pos_obj && grid_pos_obj != Py_None) { - if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) { - PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0); - PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1); - if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) && - (PyFloat_Check(y_val) || PyLong_Check(y_val))) { - grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); - grid_y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); - } + + // Handle grid position argument (can be tuple or use x/y keywords) + if (grid_pos_obj) { + if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) { + PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0); + PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1); + if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) && + (PyFloat_Check(y_val) || PyLong_Check(y_val))) { + x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); + y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); } else { - PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)"); + PyErr_SetString(PyExc_TypeError, "grid_pos tuple must contain numbers"); return -1; } + } else { + PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)"); + return -1; } } - // check types for texture - // - // Set Texture - allow None or use default - // + // Handle texture argument std::shared_ptr texture_ptr = nullptr; - if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){ - PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); - return -1; - } else if (texture != NULL && texture != Py_None) { + if (texture && texture != Py_None) { + if (!PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) { + PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); + return -1; + } auto pytexture = (PyTextureObject*)texture; texture_ptr = pytexture->data; } else { @@ -203,25 +178,20 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { texture_ptr = McRFPy_API::default_texture; } - // Allow creation without texture for testing purposes - // if (!texture_ptr) { - // PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available"); - // return -1; - // } - - if (grid_obj != NULL && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { + // Handle grid argument + if (grid_obj && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance"); return -1; } - // Always use default constructor for lazy initialization + // Create the entity self->data = std::make_shared(); // Store reference to Python object self->data->self = (PyObject*)self; Py_INCREF(self); - // TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers + // Set texture and sprite index if (texture_ptr) { self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0); } else { @@ -230,12 +200,20 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { } // Set position using grid coordinates - self->data->position = sf::Vector2f(grid_x, grid_y); + self->data->position = sf::Vector2f(x, y); - if (grid_obj != NULL) { + // Set other properties (delegate to sprite) + self->data->sprite.visible = visible; + self->data->sprite.opacity = opacity; + if (name) { + self->data->sprite.name = std::string(name); + } + + // Handle grid attachment + if (grid_obj) { PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj; self->data->grid = pygrid->data; - // todone - on creation of Entity with Grid assignment, also append it to the entity list + // Append entity to grid's entity list pygrid->data->entities->push_back(self->data); // Don't initialize gridstate here - lazy initialization to support large numbers of entities diff --git a/src/UIEntity.h b/src/UIEntity.h index dfd155e..508f4e1 100644 --- a/src/UIEntity.h +++ b/src/UIEntity.h @@ -88,7 +88,28 @@ namespace mcrfpydef { .tp_itemsize = 0, .tp_repr = (reprfunc)UIEntity::repr, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, - .tp_doc = "UIEntity objects", + .tp_doc = PyDoc_STR("Entity(grid_pos=None, texture=None, sprite_index=0, **kwargs)\n\n" + "A game entity that exists on a grid with sprite rendering.\n\n" + "Args:\n" + " grid_pos (tuple, optional): Grid position as (x, y) tuple. Default: (0, 0)\n" + " texture (Texture, optional): Texture object for sprite. Default: default texture\n" + " sprite_index (int, optional): Index into texture atlas. Default: 0\n\n" + "Keyword Args:\n" + " grid (Grid): Grid to attach entity to. Default: None\n" + " visible (bool): Visibility state. Default: True\n" + " opacity (float): Opacity (0.0-1.0). Default: 1.0\n" + " name (str): Element name for finding. Default: None\n" + " x (float): X grid position override. Default: 0\n" + " y (float): Y grid position override. Default: 0\n\n" + "Attributes:\n" + " pos (tuple): Grid position as (x, y) tuple\n" + " x, y (float): Grid position coordinates\n" + " draw_pos (tuple): Pixel position for rendering\n" + " gridstate (GridPointState): Visibility state for grid points\n" + " sprite_index (int): Current sprite index\n" + " visible (bool): Visibility state\n" + " opacity (float): Opacity value\n" + " name (str): Element name"), .tp_methods = UIEntity_all_methods, .tp_getset = UIEntity::getsetters, .tp_base = &mcrfpydef::PyDrawableType, diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index aeb03bb..ada2b67 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -6,7 +6,6 @@ #include "UISprite.h" #include "UIGrid.h" #include "McRFPy_API.h" -#include "PyArgHelpers.h" // UIDrawable methods now in UIBase.h UIDrawable* UIFrame::click_at(sf::Vector2f point) @@ -432,67 +431,47 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) // Initialize children first self->data->children = std::make_shared>>(); - // Try parsing with PyArgHelpers - int arg_idx = 0; - auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx); - auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx); - - // Default values - float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f, outline = 0.0f; + // Define all parameters with defaults + PyObject* pos_obj = nullptr; + PyObject* size_obj = nullptr; PyObject* fill_color = nullptr; PyObject* outline_color = nullptr; + float outline = 0.0f; PyObject* children_arg = nullptr; PyObject* click_handler = nullptr; + int visible = 1; + float opacity = 1.0f; + int z_index = 0; + const char* name = nullptr; + float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f; + int clip_children = 0; - // Case 1: Got position and size from helpers (tuple format) - if (pos_result.valid && size_result.valid) { - x = pos_result.x; - y = pos_result.y; - w = size_result.w; - h = size_result.h; - - // Parse remaining arguments - static const char* remaining_keywords[] = { - "fill_color", "outline_color", "outline", "children", "click", nullptr - }; - - // Create new tuple with remaining args - Py_ssize_t total_args = PyTuple_Size(args); - PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args); - - if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OOfOO", - const_cast(remaining_keywords), - &fill_color, &outline_color, &outline, - &children_arg, &click_handler)) { - Py_DECREF(remaining_args); - if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error); - else if (size_result.error) PyErr_SetString(PyExc_TypeError, size_result.error); - return -1; - } - Py_DECREF(remaining_args); + // Keywords list matches the new spec: positional args first, then all keyword args + static const char* kwlist[] = { + "pos", "size", // Positional args (as per spec) + // Keyword-only args + "fill_color", "outline_color", "outline", "children", "click", + "visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children", + nullptr + }; + + // Parse arguments with | for optional positional args + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffi", const_cast(kwlist), + &pos_obj, &size_obj, // Positional + &fill_color, &outline_color, &outline, &children_arg, &click_handler, + &visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children)) { + return -1; } - // Case 2: Traditional format (x, y, w, h, ...) - else { - PyErr_Clear(); // Clear any errors from helpers - - static const char* keywords[] = { - "x", "y", "w", "h", "fill_color", "outline_color", "outline", - "children", "click", "pos", "size", nullptr - }; - - PyObject* pos_obj = nullptr; - PyObject* size_obj = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffffOOfOOOO", - const_cast(keywords), - &x, &y, &w, &h, &fill_color, &outline_color, - &outline, &children_arg, &click_handler, - &pos_obj, &size_obj)) { - return -1; - } - - // Handle pos keyword override - if (pos_obj && pos_obj != Py_None) { + + // Handle position argument (can be tuple, Vector, or use x/y keywords) + if (pos_obj) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (vec) { + x = vec->data.x; + y = vec->data.y; + Py_DECREF(vec); + } else { + PyErr_Clear(); if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { PyObject* x_val = PyTuple_GetItem(pos_obj, 0); PyObject* y_val = PyTuple_GetItem(pos_obj, 1); @@ -500,47 +479,87 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) (PyFloat_Check(y_val) || PyLong_Check(y_val))) { x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); + } else { + PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers"); + return -1; } - } else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString( - PyImport_ImportModule("mcrfpy"), "Vector"))) { - PyVectorObject* vec = (PyVectorObject*)pos_obj; - x = vec->data.x; - y = vec->data.y; } else { PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); return -1; } } - - // Handle size keyword override - if (size_obj && size_obj != Py_None) { - if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) { - PyObject* w_val = PyTuple_GetItem(size_obj, 0); - PyObject* h_val = PyTuple_GetItem(size_obj, 1); - if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) && - (PyFloat_Check(h_val) || PyLong_Check(h_val))) { - w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val); - h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val); - } + } + // If no pos_obj but x/y keywords were provided, they're already in x, y variables + + // Handle size argument (can be tuple or use w/h keywords) + if (size_obj) { + if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) { + PyObject* w_val = PyTuple_GetItem(size_obj, 0); + PyObject* h_val = PyTuple_GetItem(size_obj, 1); + if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) && + (PyFloat_Check(h_val) || PyLong_Check(h_val))) { + w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val); + h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val); } else { - PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)"); + PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers"); return -1; } + } else { + PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)"); + return -1; } } + // If no size_obj but w/h keywords were provided, they're already in w, h variables - self->data->position = sf::Vector2f(x, y); // Set base class position - self->data->box.setPosition(self->data->position); // Sync box position + // Set the position and size + self->data->position = sf::Vector2f(x, y); + self->data->box.setPosition(self->data->position); self->data->box.setSize(sf::Vector2f(w, h)); self->data->box.setOutlineThickness(outline); - // getsetter abuse because I haven't standardized Color object parsing (TODO) - int err_val = 0; - if (fill_color && fill_color != Py_None) err_val = UIFrame::set_color_member(self, fill_color, (void*)0); - else self->data->box.setFillColor(sf::Color(0,0,0,255)); - if (err_val) return err_val; - if (outline_color && outline_color != Py_None) err_val = UIFrame::set_color_member(self, outline_color, (void*)1); - else self->data->box.setOutlineColor(sf::Color(128,128,128,255)); - if (err_val) return err_val; + + // Handle fill_color + if (fill_color && fill_color != Py_None) { + PyColorObject* color_obj = PyColor::from_arg(fill_color); + if (!color_obj) { + PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple"); + return -1; + } + self->data->box.setFillColor(color_obj->data); + Py_DECREF(color_obj); + } else { + self->data->box.setFillColor(sf::Color(0, 0, 0, 128)); // Default: semi-transparent black + } + + // Handle outline_color + if (outline_color && outline_color != Py_None) { + PyColorObject* color_obj = PyColor::from_arg(outline_color); + if (!color_obj) { + PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or color tuple"); + return -1; + } + self->data->box.setOutlineColor(color_obj->data); + Py_DECREF(color_obj); + } else { + self->data->box.setOutlineColor(sf::Color(255, 255, 255, 255)); // Default: white + } + + // Set other properties + self->data->visible = visible; + self->data->opacity = opacity; + self->data->z_index = z_index; + self->data->clip_children = clip_children; + if (name) { + self->data->name = std::string(name); + } + + // Handle click handler + if (click_handler && click_handler != Py_None) { + if (!PyCallable_Check(click_handler)) { + PyErr_SetString(PyExc_TypeError, "click must be callable"); + return -1; + } + self->data->click_register(click_handler); + } // Process children argument if provided if (children_arg && children_arg != Py_None) { diff --git a/src/UIFrame.h b/src/UIFrame.h index 2478001..16c8596 100644 --- a/src/UIFrame.h +++ b/src/UIFrame.h @@ -86,27 +86,38 @@ namespace mcrfpydef { //.tp_iter //.tp_iternext .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)\n\n" + .tp_doc = PyDoc_STR("Frame(pos=None, size=None, **kwargs)\n\n" "A rectangular frame UI element that can contain other drawable elements.\n\n" "Args:\n" - " x (float): X position in pixels. Default: 0\n" - " y (float): Y position in pixels. Default: 0\n" - " w (float): Width in pixels. Default: 0\n" - " h (float): Height in pixels. Default: 0\n" + " pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n" + " size (tuple, optional): Size as (width, height) tuple. Default: (0, 0)\n\n" + "Keyword Args:\n" " fill_color (Color): Background fill color. Default: (0, 0, 0, 128)\n" " outline_color (Color): Border outline color. Default: (255, 255, 255, 255)\n" " outline (float): Border outline thickness. Default: 0\n" " click (callable): Click event handler. Default: None\n" - " children (list): Initial list of child drawable elements. Default: None\n\n" + " children (list): Initial list of child drawable elements. Default: None\n" + " visible (bool): Visibility state. Default: True\n" + " opacity (float): Opacity (0.0-1.0). Default: 1.0\n" + " z_index (int): Rendering order. Default: 0\n" + " name (str): Element name for finding. Default: None\n" + " x (float): X position override. Default: 0\n" + " y (float): Y position override. Default: 0\n" + " w (float): Width override. Default: 0\n" + " h (float): Height override. Default: 0\n" + " clip_children (bool): Whether to clip children to frame bounds. Default: False\n\n" "Attributes:\n" " x, y (float): Position in pixels\n" " w, h (float): Size in pixels\n" + " pos (Vector): Position as a Vector object\n" " fill_color, outline_color (Color): Visual appearance\n" " outline (float): Border thickness\n" " click (callable): Click event handler\n" " children (list): Collection of child drawable elements\n" " visible (bool): Visibility state\n" + " opacity (float): Opacity value\n" " z_index (int): Rendering order\n" + " name (str): Element name\n" " clip_children (bool): Whether to clip children to frame bounds"), .tp_methods = UIFrame_methods, //.tp_members = PyUIFrame_members, diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index d6a109e..bf8ade6 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -1,7 +1,6 @@ #include "UIGrid.h" #include "GameEngine.h" #include "McRFPy_API.h" -#include "PyArgHelpers.h" #include // UIDrawable methods now in UIBase.h @@ -518,102 +517,49 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point) int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - // Default values - int grid_x = 0, grid_y = 0; - float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f; + // Define all parameters with defaults + PyObject* pos_obj = nullptr; + PyObject* size_obj = nullptr; + PyObject* grid_size_obj = nullptr; PyObject* textureObj = nullptr; + PyObject* fill_color = nullptr; + PyObject* click_handler = nullptr; + float center_x = 0.0f, center_y = 0.0f; + float zoom = 1.0f; + int perspective = -1; // perspective is a difficult __init__ arg; needs an entity in collection to work + int visible = 1; + float opacity = 1.0f; + int z_index = 0; + const char* name = nullptr; + float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f; + int grid_x = 2, grid_y = 2; // Default to 2x2 grid - // Check if first argument is a tuple (for tuple-based initialization) - bool has_tuple_first_arg = false; - if (args && PyTuple_Size(args) > 0) { - PyObject* first_arg = PyTuple_GetItem(args, 0); - if (PyTuple_Check(first_arg)) { - has_tuple_first_arg = true; - } + // Keywords list matches the new spec: positional args first, then all keyword args + static const char* kwlist[] = { + "pos", "size", "grid_size", "texture", // Positional args (as per spec) + // Keyword-only args + "fill_color", "click", "center_x", "center_y", "zoom", "perspective", + "visible", "opacity", "z_index", "name", "x", "y", "w", "h", "grid_x", "grid_y", + nullptr + }; + + // Parse arguments with | for optional positional args + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffiifizffffii", const_cast(kwlist), + &pos_obj, &size_obj, &grid_size_obj, &textureObj, // Positional + &fill_color, &click_handler, ¢er_x, ¢er_y, &zoom, &perspective, + &visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_x, &grid_y)) { + return -1; } - // Try tuple-based parsing if we have a tuple as first argument - if (has_tuple_first_arg) { - int arg_idx = 0; - auto grid_size_result = PyArgHelpers::parseGridSize(args, kwds, &arg_idx); - - // If grid size parsing failed with an error, report it - if (!grid_size_result.valid) { - if (grid_size_result.error) { - PyErr_SetString(PyExc_TypeError, grid_size_result.error); - } else { - PyErr_SetString(PyExc_TypeError, "Invalid grid size tuple"); - } - return -1; - } - - // We got a valid grid size - grid_x = grid_size_result.grid_w; - grid_y = grid_size_result.grid_h; - - // Try to parse position and size - auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx); - if (pos_result.valid) { - x = pos_result.x; - y = pos_result.y; - } - - auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx); - if (size_result.valid) { - w = size_result.w; - h = size_result.h; + // Handle position argument (can be tuple, Vector, or use x/y keywords) + if (pos_obj) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (vec) { + x = vec->data.x; + y = vec->data.y; + Py_DECREF(vec); } else { - // Default size based on grid dimensions - w = grid_x * 16.0f; - h = grid_y * 16.0f; - } - - // Parse remaining arguments (texture) - static const char* remaining_keywords[] = { "texture", nullptr }; - Py_ssize_t total_args = PyTuple_Size(args); - PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args); - - PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|O", - const_cast(remaining_keywords), - &textureObj); - Py_DECREF(remaining_args); - } - // Traditional format parsing - else { - static const char* keywords[] = { - "grid_x", "grid_y", "texture", "pos", "size", "grid_size", nullptr - }; - PyObject* pos_obj = nullptr; - PyObject* size_obj = nullptr; - PyObject* grid_size_obj = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiOOOO", - const_cast(keywords), - &grid_x, &grid_y, &textureObj, - &pos_obj, &size_obj, &grid_size_obj)) { - return -1; - } - - // Handle grid_size override - if (grid_size_obj && grid_size_obj != Py_None) { - if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) { - PyObject* x_obj = PyTuple_GetItem(grid_size_obj, 0); - PyObject* y_obj = PyTuple_GetItem(grid_size_obj, 1); - if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { - grid_x = PyLong_AsLong(x_obj); - grid_y = PyLong_AsLong(y_obj); - } else { - PyErr_SetString(PyExc_TypeError, "grid_size must contain integers"); - return -1; - } - } else { - PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple of two integers"); - return -1; - } - } - - // Handle position - if (pos_obj && pos_obj != Py_None) { + PyErr_Clear(); if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { PyObject* x_val = PyTuple_GetItem(pos_obj, 0); PyObject* y_val = PyTuple_GetItem(pos_obj, 1); @@ -622,36 +568,50 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); } else { - PyErr_SetString(PyExc_TypeError, "pos must contain numbers"); + PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers"); return -1; } } else { - PyErr_SetString(PyExc_TypeError, "pos must be a tuple of two numbers"); + PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); return -1; } } - - // Handle size - if (size_obj && size_obj != Py_None) { - if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) { - PyObject* w_val = PyTuple_GetItem(size_obj, 0); - PyObject* h_val = PyTuple_GetItem(size_obj, 1); - if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) && - (PyFloat_Check(h_val) || PyLong_Check(h_val))) { - w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val); - h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val); - } else { - PyErr_SetString(PyExc_TypeError, "size must contain numbers"); - return -1; - } + } + + // Handle size argument (can be tuple or use w/h keywords) + if (size_obj) { + if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) { + PyObject* w_val = PyTuple_GetItem(size_obj, 0); + PyObject* h_val = PyTuple_GetItem(size_obj, 1); + if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) && + (PyFloat_Check(h_val) || PyLong_Check(h_val))) { + w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val); + h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val); } else { - PyErr_SetString(PyExc_TypeError, "size must be a tuple of two numbers"); + PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers"); return -1; } } else { - // Default size based on grid - w = grid_x * 16.0f; - h = grid_y * 16.0f; + PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)"); + return -1; + } + } + + // Handle grid_size argument (can be tuple or use grid_x/grid_y keywords) + if (grid_size_obj) { + if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) { + PyObject* gx_val = PyTuple_GetItem(grid_size_obj, 0); + PyObject* gy_val = PyTuple_GetItem(grid_size_obj, 1); + if (PyLong_Check(gx_val) && PyLong_Check(gy_val)) { + grid_x = PyLong_AsLong(gx_val); + grid_y = PyLong_AsLong(gy_val); + } else { + PyErr_SetString(PyExc_TypeError, "grid_size tuple must contain integers"); + return -1; + } + } else { + PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple (grid_x, grid_y)"); + return -1; } } @@ -661,12 +621,8 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { return -1; } - // At this point we have x, y, w, h values from either parsing method - - // Convert PyObject texture to shared_ptr + // Handle texture argument std::shared_ptr texture_ptr = nullptr; - - // Allow None or NULL for texture - use default texture in that case if (textureObj && textureObj != Py_None) { if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) { PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); @@ -679,14 +635,51 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { texture_ptr = McRFPy_API::default_texture; } - // Adjust size based on texture if available and size not explicitly set - if (texture_ptr && w == grid_x * 16.0f && h == grid_y * 16.0f) { + // If size wasn't specified, calculate based on grid dimensions and texture + if (!size_obj && texture_ptr) { w = grid_x * texture_ptr->sprite_width; h = grid_y * texture_ptr->sprite_height; + } else if (!size_obj) { + w = grid_x * 16.0f; // Default tile size + h = grid_y * 16.0f; } + // Create the grid self->data = std::make_shared(grid_x, grid_y, texture_ptr, sf::Vector2f(x, y), sf::Vector2f(w, h)); + + // Set additional properties + self->data->center_x = center_x; + self->data->center_y = center_y; + self->data->zoom = zoom; + self->data->perspective = perspective; + self->data->visible = visible; + self->data->opacity = opacity; + self->data->z_index = z_index; + if (name) { + self->data->name = std::string(name); + } + + // Handle fill_color + if (fill_color && fill_color != Py_None) { + PyColorObject* color_obj = PyColor::from_arg(fill_color); + if (!color_obj) { + PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple"); + return -1; + } + self->data->box.setFillColor(color_obj->data); + Py_DECREF(color_obj); + } + + // Handle click handler + if (click_handler && click_handler != Py_None) { + if (!PyCallable_Check(click_handler)) { + PyErr_SetString(PyExc_TypeError, "click must be callable"); + return -1; + } + self->data->click_register(click_handler); + } + return 0; // Success } diff --git a/src/UIGrid.h b/src/UIGrid.h index 96f41ed..0581eeb 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -184,29 +184,49 @@ namespace mcrfpydef { //.tp_iter //.tp_iternext .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)\n\n" - "A grid-based tilemap UI element for rendering tile-based levels and game worlds.\n\n" + .tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n" + "A grid-based UI element for tile-based rendering and entity management.\n\n" "Args:\n" - " x (float): X position in pixels. Default: 0\n" - " y (float): Y position in pixels. Default: 0\n" - " grid_size (tuple): Grid dimensions as (width, height) in tiles. Default: (20, 20)\n" - " texture (Texture): Texture atlas containing tile sprites. Default: None\n" - " tile_width (int): Width of each tile in pixels. Default: 16\n" - " tile_height (int): Height of each tile in pixels. Default: 16\n" - " scale (float): Grid scaling factor. Default: 1.0\n" - " click (callable): Click event handler. Default: None\n\n" + " pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n" + " size (tuple, optional): Size as (width, height) tuple. Default: auto-calculated from grid_size\n" + " grid_size (tuple, optional): Grid dimensions as (grid_x, grid_y) tuple. Default: (2, 2)\n" + " texture (Texture, optional): Texture containing tile sprites. Default: default texture\n\n" + "Keyword Args:\n" + " fill_color (Color): Background fill color. Default: None\n" + " click (callable): Click event handler. Default: None\n" + " center_x (float): X coordinate of center point. Default: 0\n" + " center_y (float): Y coordinate of center point. Default: 0\n" + " zoom (float): Zoom level for rendering. Default: 1.0\n" + " perspective (int): Entity perspective index (-1 for omniscient). Default: -1\n" + " visible (bool): Visibility state. Default: True\n" + " opacity (float): Opacity (0.0-1.0). Default: 1.0\n" + " z_index (int): Rendering order. Default: 0\n" + " name (str): Element name for finding. Default: None\n" + " x (float): X position override. Default: 0\n" + " y (float): Y position override. Default: 0\n" + " w (float): Width override. Default: auto-calculated\n" + " h (float): Height override. Default: auto-calculated\n" + " grid_x (int): Grid width override. Default: 2\n" + " grid_y (int): Grid height override. Default: 2\n\n" "Attributes:\n" " x, y (float): Position in pixels\n" + " w, h (float): Size in pixels\n" + " pos (Vector): Position as a Vector object\n" + " size (tuple): Size as (width, height) tuple\n" + " center (tuple): Center point as (x, y) tuple\n" + " center_x, center_y (float): Center point coordinates\n" + " zoom (float): Zoom level for rendering\n" " grid_size (tuple): Grid dimensions (width, height) in tiles\n" - " tile_width, tile_height (int): Tile dimensions in pixels\n" + " grid_x, grid_y (int): Grid dimensions\n" " texture (Texture): Tile texture atlas\n" - " scale (float): Scale multiplier\n" - " points (list): 2D array of GridPoint objects for tile data\n" - " entities (list): Collection of Entity objects in the grid\n" - " background_color (Color): Grid background color\n" + " fill_color (Color): Background color\n" + " entities (EntityCollection): Collection of entities in the grid\n" + " perspective (int): Entity perspective index\n" " click (callable): Click event handler\n" " visible (bool): Visibility state\n" - " z_index (int): Rendering order"), + " opacity (float): Opacity value\n" + " z_index (int): Rendering order\n" + " name (str): Element name"), .tp_methods = UIGrid_all_methods, //.tp_members = UIGrid::members, .tp_getset = UIGrid::getsetters, diff --git a/src/UISprite.cpp b/src/UISprite.cpp index 8daf639..8cad830 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -1,7 +1,6 @@ #include "UISprite.h" #include "GameEngine.h" #include "PyVector.h" -#include "PyArgHelpers.h" // UIDrawable methods now in UIBase.h UIDrawable* UISprite::click_at(sf::Vector2f point) @@ -327,57 +326,46 @@ PyObject* UISprite::repr(PyUISpriteObject* self) int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) { - // Try parsing with PyArgHelpers - int arg_idx = 0; - auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx); - - // Default values - float x = 0.0f, y = 0.0f, scale = 1.0f; - int sprite_index = 0; + // Define all parameters with defaults + PyObject* pos_obj = nullptr; PyObject* texture = nullptr; + int sprite_index = 0; + float scale = 1.0f; + float scale_x = 1.0f; + float scale_y = 1.0f; PyObject* click_handler = nullptr; + int visible = 1; + float opacity = 1.0f; + int z_index = 0; + const char* name = nullptr; + float x = 0.0f, y = 0.0f; - // Case 1: Got position from helpers (tuple format) - if (pos_result.valid) { - x = pos_result.x; - y = pos_result.y; - - // Parse remaining arguments - static const char* remaining_keywords[] = { - "texture", "sprite_index", "scale", "click", nullptr - }; - - // Create new tuple with remaining args - Py_ssize_t total_args = PyTuple_Size(args); - PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args); - - if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OifO", - const_cast(remaining_keywords), - &texture, &sprite_index, &scale, &click_handler)) { - Py_DECREF(remaining_args); - if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error); - return -1; - } - Py_DECREF(remaining_args); + // Keywords list matches the new spec: positional args first, then all keyword args + static const char* kwlist[] = { + "pos", "texture", "sprite_index", // Positional args (as per spec) + // Keyword-only args + "scale", "scale_x", "scale_y", "click", + "visible", "opacity", "z_index", "name", "x", "y", + nullptr + }; + + // Parse arguments with | for optional positional args + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizff", const_cast(kwlist), + &pos_obj, &texture, &sprite_index, // Positional + &scale, &scale_x, &scale_y, &click_handler, + &visible, &opacity, &z_index, &name, &x, &y)) { + return -1; } - // Case 2: Traditional format - else { - PyErr_Clear(); // Clear any errors from helpers - - static const char* keywords[] = { - "x", "y", "texture", "sprite_index", "scale", "click", "pos", nullptr - }; - PyObject* pos_obj = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOifOO", - const_cast(keywords), - &x, &y, &texture, &sprite_index, &scale, - &click_handler, &pos_obj)) { - return -1; - } - - // Handle pos keyword override - if (pos_obj && pos_obj != Py_None) { + + // Handle position argument (can be tuple, Vector, or use x/y keywords) + if (pos_obj) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (vec) { + x = vec->data.x; + y = vec->data.y; + Py_DECREF(vec); + } else { + PyErr_Clear(); if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { PyObject* x_val = PyTuple_GetItem(pos_obj, 0); PyObject* y_val = PyTuple_GetItem(pos_obj, 1); @@ -385,12 +373,10 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) (PyFloat_Check(y_val) || PyLong_Check(y_val))) { x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val); y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val); + } else { + PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers"); + return -1; } - } else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString( - PyImport_ImportModule("mcrfpy"), "Vector"))) { - PyVectorObject* vec = (PyVectorObject*)pos_obj; - x = vec->data.x; - y = vec->data.y; } else { PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); return -1; @@ -400,10 +386,11 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) // Handle texture - allow None or use default std::shared_ptr texture_ptr = nullptr; - if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){ - PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); - return -1; - } else if (texture != NULL && texture != Py_None) { + if (texture && texture != Py_None) { + if (!PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) { + PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); + return -1; + } auto pytexture = (PyTextureObject*)texture; texture_ptr = pytexture->data; } else { @@ -416,9 +403,27 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) return -1; } + // Create the sprite self->data = std::make_shared(texture_ptr, sprite_index, sf::Vector2f(x, y), scale); + + // Set scale properties + if (scale_x != 1.0f || scale_y != 1.0f) { + // If scale_x or scale_y were explicitly set, use them + self->data->setScale(sf::Vector2f(scale_x, scale_y)); + } else if (scale != 1.0f) { + // Otherwise use uniform scale + self->data->setScale(sf::Vector2f(scale, scale)); + } + + // Set other properties + self->data->visible = visible; + self->data->opacity = opacity; + self->data->z_index = z_index; + if (name) { + self->data->name = std::string(name); + } - // Process click handler if provided + // Handle click handler if (click_handler && click_handler != Py_None) { if (!PyCallable_Check(click_handler)) { PyErr_SetString(PyExc_TypeError, "click must be callable"); diff --git a/src/UISprite.h b/src/UISprite.h index 5e18ade..6fdc0a2 100644 --- a/src/UISprite.h +++ b/src/UISprite.h @@ -92,23 +92,35 @@ namespace mcrfpydef { //.tp_iter //.tp_iternext .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)\n\n" + .tp_doc = PyDoc_STR("Sprite(pos=None, texture=None, sprite_index=0, **kwargs)\n\n" "A sprite UI element that displays a texture or portion of a texture atlas.\n\n" "Args:\n" - " x (float): X position in pixels. Default: 0\n" - " y (float): Y position in pixels. Default: 0\n" - " texture (Texture): Texture object to display. Default: None\n" - " sprite_index (int): Index into texture atlas (if applicable). Default: 0\n" - " scale (float): Sprite scaling factor. Default: 1.0\n" - " click (callable): Click event handler. Default: None\n\n" + " pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n" + " texture (Texture, optional): Texture object to display. Default: default texture\n" + " sprite_index (int, optional): Index into texture atlas. Default: 0\n\n" + "Keyword Args:\n" + " scale (float): Uniform scale factor. Default: 1.0\n" + " scale_x (float): Horizontal scale factor. Default: 1.0\n" + " scale_y (float): Vertical scale factor. Default: 1.0\n" + " click (callable): Click event handler. Default: None\n" + " visible (bool): Visibility state. Default: True\n" + " opacity (float): Opacity (0.0-1.0). Default: 1.0\n" + " z_index (int): Rendering order. Default: 0\n" + " name (str): Element name for finding. Default: None\n" + " x (float): X position override. Default: 0\n" + " y (float): Y position override. Default: 0\n\n" "Attributes:\n" " x, y (float): Position in pixels\n" + " pos (Vector): Position as a Vector object\n" " texture (Texture): The texture being displayed\n" " sprite_index (int): Current sprite index in texture atlas\n" - " scale (float): Scale multiplier\n" + " scale (float): Uniform scale factor\n" + " scale_x, scale_y (float): Individual scale factors\n" " click (callable): Click event handler\n" " visible (bool): Visibility state\n" + " opacity (float): Opacity value\n" " z_index (int): Rendering order\n" + " name (str): Element name\n" " w, h (float): Read-only computed size based on texture and scale"), .tp_methods = UISprite_methods, //.tp_members = PyUIFrame_members, diff --git a/src/scripts/cos_entities.py b/src/scripts/cos_entities.py index 6b8ff59..6519630 100644 --- a/src/scripts/cos_entities.py +++ b/src/scripts/cos_entities.py @@ -67,10 +67,10 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu self.draw_pos = (tx, ty) for e in self.game.entities: if e is self: continue - if e.draw_pos == old_pos: e.ev_exit(self) + if e.draw_pos.x == old_pos.x and e.draw_pos.y == old_pos.y: e.ev_exit(self) for e in self.game.entities: if e is self: continue - if e.draw_pos == (tx, ty): e.ev_enter(self) + if e.draw_pos.x == tx and e.draw_pos.y == ty: e.ev_enter(self) def act(self): pass @@ -83,12 +83,12 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu def try_move(self, dx, dy, test=False): x_max, y_max = self.grid.grid_size - tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) + tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy) #for e in iterable_entities(self.grid): # sorting entities to test against the boulder instead of the button when they overlap. for e in sorted(self.game.entities, key = lambda i: i.draw_order, reverse = True): - if e.draw_pos == (tx, ty): + if e.draw_pos.x == tx and e.draw_pos.y == ty: #print(f"bumping {e}") return e.bump(self, dx, dy) @@ -106,7 +106,7 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu return False def _relative_move(self, dx, dy): - tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) + tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy) #self.draw_pos = (tx, ty) self.do_move(tx, ty) @@ -181,7 +181,7 @@ class Equippable: if self.zap_cooldown_remaining != 0: print("zap is cooling down.") return False - fx, fy = caster.draw_pos + fx, fy = caster.draw_pos.x, caster.draw_pos.y x, y = int(fx), int (fy) dist = lambda tx, ty: abs(int(tx) - x) + abs(int(ty) - y) targets = [] @@ -293,7 +293,7 @@ class PlayerEntity(COSEntity): ## TODO - find other entities to avoid spawning on top of for spawn in spawn_points: for e in avoid or []: - if e.draw_pos == spawn: break + if e.draw_pos.x == spawn[0] and e.draw_pos.y == spawn[1]: break else: break self.draw_pos = spawn @@ -314,9 +314,9 @@ class BoulderEntity(COSEntity): elif type(other) == EnemyEntity: if not other.can_push: return False #tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy) - tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) + tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy) # Is the boulder blocked the same direction as the bumper? If not, let's both move - old_pos = int(self.draw_pos[0]), int(self.draw_pos[1]) + old_pos = int(self.draw_pos.x), int(self.draw_pos.y) if self.try_move(dx, dy, test=test): if not test: other.do_move(*old_pos) @@ -342,7 +342,7 @@ class ButtonEntity(COSEntity): # self.exit.unlock() # TODO: unlock, and then lock again, when player steps on/off if not test: - pos = int(self.draw_pos[0]), int(self.draw_pos[1]) + pos = int(self.draw_pos.x), int(self.draw_pos.y) other.do_move(*pos) return True @@ -393,7 +393,7 @@ class EnemyEntity(COSEntity): def bump(self, other, dx, dy, test=False): if self.hp == 0: if not test: - old_pos = int(self.draw_pos[0]), int(self.draw_pos[1]) + old_pos = int(self.draw_pos.x), int(self.draw_pos.y) other.do_move(*old_pos) return True if type(other) == PlayerEntity: @@ -415,7 +415,7 @@ class EnemyEntity(COSEntity): print("Ouch, my entire body!!") self._entity.sprite_number = self.base_sprite + 246 self.hp = 0 - old_pos = int(self.draw_pos[0]), int(self.draw_pos[1]) + old_pos = int(self.draw_pos.x), int(self.draw_pos.y) if not test: other.do_move(*old_pos) return True @@ -423,8 +423,8 @@ class EnemyEntity(COSEntity): def act(self): if self.hp > 0: # if player nearby: attack - x, y = self.draw_pos - px, py = self.game.player.draw_pos + x, y = self.draw_pos.x, self.draw_pos.y + px, py = self.game.player.draw_pos.x, self.game.player.draw_pos.y for d in ((1, 0), (0, 1), (-1, 0), (1, 0)): if int(x + d[0]) == int(px) and int(y + d[1]) == int(py): self.try_move(*d) diff --git a/src/scripts/cos_tiles.py b/src/scripts/cos_tiles.py index 4b80785..079516f 100644 --- a/src/scripts/cos_tiles.py +++ b/src/scripts/cos_tiles.py @@ -22,12 +22,13 @@ class TileInfo: @staticmethod def from_grid(grid, xy:tuple): values = {} + x_max, y_max = grid.grid_size for d in deltas: tx, ty = d[0] + xy[0], d[1] + xy[1] - try: - values[d] = grid.at((tx, ty)).walkable - except ValueError: + if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max: values[d] = True + else: + values[d] = grid.at((tx, ty)).walkable return TileInfo(values) @staticmethod @@ -70,10 +71,10 @@ def special_rule_verify(rule, grid, xy, unverified_tiles, pass_unverified=False) tx, ty = xy[0] + dxy[0], xy[1] + dxy[1] #print(f"Special rule: {cardinal} {allowed_tile} {type(allowed_tile)} -> ({tx}, {ty}) [{grid.at((tx, ty)).tilesprite}]{'*' if (tx, ty) in unverified_tiles else ''}") if (tx, ty) in unverified_tiles and cardinal in "nsew": return pass_unverified - try: - return grid.at((tx, ty)).tilesprite == allowed_tile - except ValueError: + x_max, y_max = grid.grid_size + if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max: return False + return grid.at((tx, ty)).tilesprite == allowed_tile import random tile_of_last_resort = 431 diff --git a/src/scripts/game.py b/src/scripts/game.py index 8bee8c9..0a7b6e4 100644 --- a/src/scripts/game.py +++ b/src/scripts/game.py @@ -87,7 +87,7 @@ class Crypt: # Side Bar (inventory, level info) config self.level_caption = mcrfpy.Caption((5,5), "Level: 1", font, fill_color=(255, 255, 255)) - self.level_caption.size = 26 + self.level_caption.font_size = 26 self.level_caption.outline = 3 self.level_caption.outline_color = (0, 0, 0) self.sidebar.children.append(self.level_caption) @@ -103,7 +103,7 @@ class Crypt: mcrfpy.Caption((25, 130 + 95 * i), "x", font, fill_color=(255, 255, 255)) for i in range(5) ] for i in self.inv_captions: - i.size = 16 + i.font_size = 16 self.sidebar.children.append(i) liminal_void = mcrfpy.Grid(1, 1, t, (0, 0), (16, 16)) @@ -382,7 +382,7 @@ class Crypt: def pull_boulder_search(self): for dx, dy in ( (0, -1), (-1, 0), (1, 0), (0, 1) ): for e in self.entities: - if e.draw_pos != (self.player.draw_pos[0] + dx, self.player.draw_pos[1] + dy): continue + if e.draw_pos.x != self.player.draw_pos.x + dx or e.draw_pos.y != self.player.draw_pos.y + dy: continue if type(e) == ce.BoulderEntity: self.pull_boulder_move((dx, dy), e) return self.enemy_turn() @@ -395,7 +395,7 @@ class Crypt: if self.player.try_move(-p[0], -p[1], test=True): old_pos = self.player.draw_pos self.player.try_move(-p[0], -p[1]) - target_boulder.do_move(*old_pos) + target_boulder.do_move(old_pos.x, old_pos.y) def swap_level(self, new_level, spawn_point): self.level = new_level @@ -451,7 +451,7 @@ class SweetButton: # main button caption self.caption = mcrfpy.Caption((40, 3), caption, font, fill_color=font_color) - self.caption.size = font_size + self.caption.font_size = font_size self.caption.outline_color=font_outline_color self.caption.outline=font_outline_width self.main_button.children.append(self.caption) @@ -548,20 +548,20 @@ class MainMenu: # title text drop_shadow = mcrfpy.Caption((150, 10), "Crypt Of Sokoban", font, fill_color=(96, 96, 96), outline_color=(192, 0, 0)) drop_shadow.outline = 3 - drop_shadow.size = 64 + drop_shadow.font_size = 64 components.append( drop_shadow ) title_txt = mcrfpy.Caption((158, 18), "Crypt Of Sokoban", font, fill_color=(255, 255, 255)) - title_txt.size = 64 + title_txt.font_size = 64 components.append( title_txt ) # toast: text over the demo grid that fades out on a timer self.toast = mcrfpy.Caption((150, 400), "", font, fill_color=(0, 0, 0)) - self.toast.size = 28 + self.toast.font_size = 28 self.toast.outline = 2 self.toast.outline_color = (255, 255, 255) self.toast_event = None @@ -626,6 +626,7 @@ class MainMenu: def play(self, sweet_btn, args): #if args[3] == "start": return # DRAMATIC on release action! if args[3] == "end": return + mcrfpy.delTimer("demo_motion") # Clean up the demo timer self.crypt = Crypt() #mcrfpy.setScene("play") self.crypt.start() diff --git a/tests/demo_animation_callback_usage.py b/tests/demo_animation_callback_usage.py new file mode 100644 index 0000000..7cd019a --- /dev/null +++ b/tests/demo_animation_callback_usage.py @@ -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() \ No newline at end of file diff --git a/tests/demos/animation_sizzle_reel.py b/tests/demos/animation_sizzle_reel.py index d3b1e20..15c2e7c 100644 --- a/tests/demos/animation_sizzle_reel.py +++ b/tests/demos/animation_sizzle_reel.py @@ -258,8 +258,9 @@ def demo_grid_animations(ui): except: texture = None - grid = Grid(100, 150, grid_size=(20, 15), texture=texture, - tile_width=24, tile_height=24) + # Grid constructor: Grid(grid_x, grid_y, texture, position, size) + # Note: tile dimensions are determined by texture's grid_size + grid = Grid(20, 15, texture, (100, 150), (480, 360)) # 20x24, 15x24 grid.fill_color = Color(20, 20, 40) ui.append(grid) @@ -282,7 +283,7 @@ def demo_grid_animations(ui): # Create entities in the grid if texture: - entity1 = Entity(5.0, 5.0, texture, sprite_index=8) + entity1 = Entity((5.0, 5.0), texture, 8) # position tuple, texture, sprite_index entity1.scale = 1.5 grid.entities.append(entity1) @@ -291,7 +292,7 @@ def demo_grid_animations(ui): entity_pos.start(entity1) # Create patrolling entity - entity2 = Entity(10.0, 2.0, texture, sprite_index=12) + entity2 = Entity((10.0, 2.0), texture, 12) # position tuple, texture, sprite_index grid.entities.append(entity2) # Animate sprite changes diff --git a/tests/demos/animation_sizzle_reel_fixed.py b/tests/demos/animation_sizzle_reel_fixed.py index e12f9bc..b9c0e2e 100644 --- a/tests/demos/animation_sizzle_reel_fixed.py +++ b/tests/demos/animation_sizzle_reel_fixed.py @@ -183,7 +183,7 @@ def clear_scene(): # Keep only the first two elements (title and subtitle) while len(ui) > 2: - ui.remove(ui[2]) + ui.remove(2) def run_demo_sequence(runtime): """Run through all demos""" diff --git a/tests/demos/animation_sizzle_reel_working.py b/tests/demos/animation_sizzle_reel_working.py index d24cc1a..bb2f7af 100644 --- a/tests/demos/animation_sizzle_reel_working.py +++ b/tests/demos/animation_sizzle_reel_working.py @@ -268,8 +268,6 @@ def run_next_demo(runtime): # Clean up timers from previous demo for timer in ["opacity_0", "opacity_1", "opacity_2", "opacity_3", "c_green", "c_blue", "c_white"]: - if not mcrfpy.getTimer(timer): - continue try: mcrfpy.delTimer(timer) except: diff --git a/tests/demos/exhaustive_api_demo.py b/tests/demos/exhaustive_api_demo.py deleted file mode 100644 index 76d36cc..0000000 --- a/tests/demos/exhaustive_api_demo.py +++ /dev/null @@ -1,1204 +0,0 @@ -#!/usr/bin/env python3 -""" -McRogueFace Exhaustive API Demonstration -======================================== - -This script demonstrates EVERY constructor variant and EVERY method -for EVERY UI object type in McRogueFace. It serves as both a test -suite and a comprehensive API reference with working examples. - -The script is organized by UI object type, showing: -1. All constructor variants (empty, partial args, full args) -2. All properties (get and set) -3. All methods with different parameter combinations -4. Special behaviors and edge cases - -Author: Claude -Purpose: Complete API demonstration and validation -""" - -import mcrfpy -from mcrfpy import Color, Vector, Font, Texture, Frame, Caption, Sprite, Grid, Entity -import sys - -# Test configuration -VERBOSE = True # Print detailed information about each test - -def print_section(title): - """Print a section header""" - print("\n" + "="*60) - print(f" {title}") - print("="*60) - -def print_test(test_name, success=True): - """Print test result""" - status = "โœ“ PASS" if success else "โœ— FAIL" - print(f" {status} - {test_name}") - -def test_color_api(): - """Test all Color constructors and methods""" - print_section("COLOR API TESTS") - - # Constructor variants - print("\n Constructors:") - - # Empty constructor (defaults to white) - c1 = Color() - print_test(f"Color() = ({c1.r}, {c1.g}, {c1.b}, {c1.a})") - - # Single value (grayscale) - c2 = Color(128) - print_test(f"Color(128) = ({c2.r}, {c2.g}, {c2.b}, {c2.a})") - - # RGB only (alpha defaults to 255) - c3 = Color(255, 128, 0) - print_test(f"Color(255, 128, 0) = ({c3.r}, {c3.g}, {c3.b}, {c3.a})") - - # Full RGBA - c4 = Color(100, 150, 200, 128) - print_test(f"Color(100, 150, 200, 128) = ({c4.r}, {c4.g}, {c4.b}, {c4.a})") - - # From hex string - c5 = Color.from_hex("#FF8800") - print_test(f"Color.from_hex('#FF8800') = ({c5.r}, {c5.g}, {c5.b}, {c5.a})") - - c6 = Color.from_hex("#FF8800AA") - print_test(f"Color.from_hex('#FF8800AA') = ({c6.r}, {c6.g}, {c6.b}, {c6.a})") - - # Methods - print("\n Methods:") - - # to_hex - hex_str = c4.to_hex() - print_test(f"Color(100, 150, 200, 128).to_hex() = '{hex_str}'") - - # lerp (linear interpolation) - c_start = Color(0, 0, 0) - c_end = Color(255, 255, 255) - c_mid = c_start.lerp(c_end, 0.5) - print_test(f"Black.lerp(White, 0.5) = ({c_mid.r}, {c_mid.g}, {c_mid.b}, {c_mid.a})") - - # Property access - print("\n Properties:") - c = Color(10, 20, 30, 40) - print_test(f"Initial: r={c.r}, g={c.g}, b={c.b}, a={c.a}") - - c.r = 200 - c.g = 150 - c.b = 100 - c.a = 255 - print_test(f"After modification: r={c.r}, g={c.g}, b={c.b}, a={c.a}") - - return True - -def test_vector_api(): - """Test all Vector constructors and methods""" - print_section("VECTOR API TESTS") - - # Constructor variants - print("\n Constructors:") - - # Empty constructor - v1 = Vector() - print_test(f"Vector() = ({v1.x}, {v1.y})") - - # Single value (both x and y) - v2 = Vector(5.0) - print_test(f"Vector(5.0) = ({v2.x}, {v2.y})") - - # Full x, y - v3 = Vector(10.5, 20.3) - print_test(f"Vector(10.5, 20.3) = ({v3.x}, {v3.y})") - - # Methods - print("\n Methods:") - - # magnitude - v = Vector(3, 4) - mag = v.magnitude() - print_test(f"Vector(3, 4).magnitude() = {mag}") - - # normalize - v_norm = v.normalize() - print_test(f"Vector(3, 4).normalize() = ({v_norm.x:.3f}, {v_norm.y:.3f})") - - # dot product - v_a = Vector(2, 3) - v_b = Vector(4, 5) - dot = v_a.dot(v_b) - print_test(f"Vector(2, 3).dot(Vector(4, 5)) = {dot}") - - # distance_to - dist = v_a.distance_to(v_b) - print_test(f"Vector(2, 3).distance_to(Vector(4, 5)) = {dist:.3f}") - - # Operators - print("\n Operators:") - - # Addition - v_sum = v_a + v_b - print_test(f"Vector(2, 3) + Vector(4, 5) = ({v_sum.x}, {v_sum.y})") - - # Subtraction - v_diff = v_b - v_a - print_test(f"Vector(4, 5) - Vector(2, 3) = ({v_diff.x}, {v_diff.y})") - - # Multiplication (scalar) - v_mult = v_a * 2.5 - print_test(f"Vector(2, 3) * 2.5 = ({v_mult.x}, {v_mult.y})") - - # Division (scalar) - v_div = v_b / 2.0 - print_test(f"Vector(4, 5) / 2.0 = ({v_div.x}, {v_div.y})") - - # Comparison - v_eq1 = Vector(1, 2) - v_eq2 = Vector(1, 2) - v_neq = Vector(3, 4) - print_test(f"Vector(1, 2) == Vector(1, 2) = {v_eq1 == v_eq2}") - print_test(f"Vector(1, 2) != Vector(3, 4) = {v_eq1 != v_neq}") - - return True - -def test_frame_api(): - """Test all Frame constructors and methods""" - print_section("FRAME API TESTS") - - # Create a test scene - mcrfpy.createScene("api_test") - mcrfpy.setScene("api_test") - ui = mcrfpy.sceneUI("api_test") - - # Constructor variants - print("\n Constructors:") - - # Empty constructor - f1 = Frame() - print_test(f"Frame() - pos=({f1.x}, {f1.y}), size=({f1.w}, {f1.h})") - ui.append(f1) - - # Position only - f2 = Frame(100, 50) - print_test(f"Frame(100, 50) - pos=({f2.x}, {f2.y}), size=({f2.w}, {f2.h})") - ui.append(f2) - - # Position and size - f3 = Frame(200, 100, 150, 75) - print_test(f"Frame(200, 100, 150, 75) - pos=({f3.x}, {f3.y}), size=({f3.w}, {f3.h})") - ui.append(f3) - - # Full constructor - f4 = Frame(300, 200, 200, 100, - fill_color=Color(100, 100, 200), - outline_color=Color(255, 255, 0), - outline=3) - print_test("Frame with all parameters") - ui.append(f4) - - # With click handler - def on_click(x, y, button): - print(f" Frame clicked at ({x}, {y}) with button {button}") - - f5 = Frame(500, 300, 100, 100, click=on_click) - print_test("Frame with click handler") - ui.append(f5) - - # Properties - print("\n Properties:") - - # Position and size - f = Frame(10, 20, 30, 40) - print_test(f"Initial: x={f.x}, y={f.y}, w={f.w}, h={f.h}") - - f.x = 50 - f.y = 60 - f.w = 70 - f.h = 80 - print_test(f"Modified: x={f.x}, y={f.y}, w={f.w}, h={f.h}") - - # Colors - f.fill_color = Color(255, 0, 0, 128) - f.outline_color = Color(0, 255, 0) - f.outline = 5.0 - print_test(f"Colors set, outline={f.outline}") - - # Visibility and opacity - f.visible = False - f.opacity = 0.5 - print_test(f"visible={f.visible}, opacity={f.opacity}") - f.visible = True # Reset - - # Z-index - f.z_index = 10 - print_test(f"z_index={f.z_index}") - - # Children collection - child1 = Frame(5, 5, 20, 20) - child2 = Frame(30, 5, 20, 20) - f.children.append(child1) - f.children.append(child2) - print_test(f"children.count = {len(f.children)}") - - # Clip children - f.clip_children = True - print_test(f"clip_children={f.clip_children}") - - # Methods - print("\n Methods:") - - # get_bounds - bounds = f.get_bounds() - print_test(f"get_bounds() = {bounds}") - - # move - old_pos = (f.x, f.y) - f.move(10, 15) - new_pos = (f.x, f.y) - print_test(f"move(10, 15): {old_pos} -> {new_pos}") - - # resize - old_size = (f.w, f.h) - f.resize(100, 120) - new_size = (f.w, f.h) - print_test(f"resize(100, 120): {old_size} -> {new_size}") - - # Position tuple property - f.pos = (150, 175) - print_test(f"pos property: ({f.x}, {f.y})") - - # Children collection methods - print("\n Children Collection:") - - # Clear and test - f.children.extend([Frame(0, 0, 10, 10) for _ in range(3)]) - print_test(f"extend() - count = {len(f.children)}") - - # Index access - first_child = f.children[0] - print_test(f"children[0] = Frame at ({first_child.x}, {first_child.y})") - - # Remove - f.children.remove(first_child) - print_test(f"remove() - count = {len(f.children)}") - - # Iteration - count = 0 - for child in f.children: - count += 1 - print_test(f"iteration - counted {count} children") - - return True - -def test_caption_api(): - """Test all Caption constructors and methods""" - print_section("CAPTION API TESTS") - - ui = mcrfpy.sceneUI("api_test") - - # Constructor variants - print("\n Constructors:") - - # Empty constructor - c1 = Caption() - print_test(f"Caption() - text='{c1.text}', pos=({c1.x}, {c1.y})") - ui.append(c1) - - # Text only - c2 = Caption("Hello World") - print_test(f"Caption('Hello World') - pos=({c2.x}, {c2.y})") - ui.append(c2) - - # Text and position - c3 = Caption("Positioned Text", 100, 50) - print_test(f"Caption('Positioned Text', 100, 50)") - ui.append(c3) - - # With font (would need Font object) - # font = Font("assets/fonts/arial.ttf", 16) - # c4 = Caption("Custom Font", 200, 100, font) - - # Full constructor - c5 = Caption("Styled Text", 300, 150, - fill_color=Color(255, 255, 0), - outline_color=Color(255, 0, 0), - outline=2) - print_test("Caption with all style parameters") - ui.append(c5) - - # With click handler - def caption_click(x, y, button): - print(f" Caption clicked at ({x}, {y})") - - c6 = Caption("Clickable", 400, 200, click=caption_click) - print_test("Caption with click handler") - ui.append(c6) - - # Properties - print("\n Properties:") - - c = Caption("Test Caption", 10, 20) - - # Text - c.text = "Modified Text" - print_test(f"text = '{c.text}'") - - # Position - c.x = 50 - c.y = 60 - print_test(f"position = ({c.x}, {c.y})") - - # Colors and style - c.fill_color = Color(0, 255, 255) - c.outline_color = Color(255, 0, 255) - c.outline = 3.0 - print_test("Colors and outline set") - - # Size (read-only, computed from text) - print_test(f"size (computed) = ({c.w}, {c.h})") - - # Common properties - c.visible = True - c.opacity = 0.8 - c.z_index = 5 - print_test(f"visible={c.visible}, opacity={c.opacity}, z_index={c.z_index}") - - # Methods - print("\n Methods:") - - # get_bounds - bounds = c.get_bounds() - print_test(f"get_bounds() = {bounds}") - - # move - c.move(25, 30) - print_test(f"move(25, 30) - new pos = ({c.x}, {c.y})") - - # Special text behaviors - print("\n Text Behaviors:") - - # Empty text - c.text = "" - print_test(f"Empty text - size = ({c.w}, {c.h})") - - # Multiline text - c.text = "Line 1\nLine 2\nLine 3" - print_test(f"Multiline text - size = ({c.w}, {c.h})") - - # Very long text - c.text = "A" * 100 - print_test(f"Long text (100 chars) - size = ({c.w}, {c.h})") - - return True - -def test_sprite_api(): - """Test all Sprite constructors and methods""" - print_section("SPRITE API TESTS") - - ui = mcrfpy.sceneUI("api_test") - - # Try to load a texture for testing - texture = None - try: - texture = Texture("assets/sprites/player.png", grid_size=(32, 32)) - print_test("Texture loaded successfully") - except: - print_test("Texture load failed - using None", False) - - # Constructor variants - print("\n Constructors:") - - # Empty constructor - s1 = Sprite() - print_test(f"Sprite() - pos=({s1.x}, {s1.y}), sprite_index={s1.sprite_index}") - ui.append(s1) - - # Position only - s2 = Sprite(100, 50) - print_test(f"Sprite(100, 50)") - ui.append(s2) - - # Position and texture - s3 = Sprite(200, 100, texture) - print_test(f"Sprite(200, 100, texture)") - ui.append(s3) - - # Full constructor - s4 = Sprite(300, 150, texture, sprite_index=5, scale=2.0) - print_test(f"Sprite with texture, index=5, scale=2.0") - ui.append(s4) - - # With click handler - def sprite_click(x, y, button): - print(f" Sprite clicked!") - - s5 = Sprite(400, 200, texture, click=sprite_click) - print_test("Sprite with click handler") - ui.append(s5) - - # Properties - print("\n Properties:") - - s = Sprite(10, 20, texture) - - # Position - s.x = 50 - s.y = 60 - print_test(f"position = ({s.x}, {s.y})") - - # Position tuple - s.pos = (75, 85) - print_test(f"pos tuple = ({s.x}, {s.y})") - - # Sprite index - s.sprite_index = 10 - print_test(f"sprite_index = {s.sprite_index}") - - # Scale - s.scale = 1.5 - print_test(f"scale = {s.scale}") - - # Size (computed from texture and scale) - print_test(f"size (computed) = ({s.w}, {s.h})") - - # Texture - s.texture = texture # Can reassign texture - print_test("Texture reassigned") - - # Common properties - s.visible = True - s.opacity = 0.9 - s.z_index = 3 - print_test(f"visible={s.visible}, opacity={s.opacity}, z_index={s.z_index}") - - # Methods - print("\n Methods:") - - # get_bounds - bounds = s.get_bounds() - print_test(f"get_bounds() = {bounds}") - - # move - old_pos = (s.x, s.y) - s.move(15, 20) - new_pos = (s.x, s.y) - print_test(f"move(15, 20): {old_pos} -> {new_pos}") - - # Sprite animation test - print("\n Sprite Animation:") - - # Test different sprite indices - for i in range(5): - s.sprite_index = i - print_test(f"Set sprite_index to {i}") - - return True - -def test_grid_api(): - """Test all Grid constructors and methods""" - print_section("GRID API TESTS") - - ui = mcrfpy.sceneUI("api_test") - - # Load texture for grid - texture = None - try: - texture = Texture("assets/sprites/tiles.png", grid_size=(16, 16)) - print_test("Tile texture loaded") - except: - print_test("Tile texture load failed", False) - - # Constructor variants - print("\n Constructors:") - - # Empty constructor - g1 = Grid() - print_test(f"Grid() - pos=({g1.x}, {g1.y}), grid_size={g1.grid_size}") - ui.append(g1) - - # Position only - g2 = Grid(100, 50) - print_test(f"Grid(100, 50)") - ui.append(g2) - - # Position and grid size - g3 = Grid(200, 100, grid_size=(30, 20)) - print_test(f"Grid with size (30, 20)") - ui.append(g3) - - # With texture - g4 = Grid(300, 150, grid_size=(25, 15), texture=texture) - print_test("Grid with texture") - ui.append(g4) - - # Full constructor - g5 = Grid(400, 200, grid_size=(20, 10), texture=texture, - tile_width=24, tile_height=24, scale=1.5) - print_test("Grid with all parameters") - ui.append(g5) - - # With click handler - def grid_click(x, y, button): - print(f" Grid clicked at ({x}, {y})") - - g6 = Grid(500, 250, click=grid_click) - print_test("Grid with click handler") - ui.append(g6) - - # Properties - print("\n Properties:") - - g = Grid(10, 20, grid_size=(40, 30)) - - # Position - g.x = 50 - g.y = 60 - print_test(f"position = ({g.x}, {g.y})") - - # Grid dimensions - print_test(f"grid_size = {g.grid_size}") - print_test(f"grid_x = {g.grid_x}, grid_y = {g.grid_y}") - - # Tile dimensions - g.tile_width = 20 - g.tile_height = 20 - print_test(f"tile size = ({g.tile_width}, {g.tile_height})") - - # Scale - g.scale = 2.0 - print_test(f"scale = {g.scale}") - - # Texture - g.texture = texture - print_test("Texture assigned") - - # Fill color - g.fill_color = Color(30, 30, 50) - print_test("Fill color set") - - # Camera properties - g.center = (20.0, 15.0) - print_test(f"center (camera) = {g.center}") - - g.zoom = 1.5 - print_test(f"zoom = {g.zoom}") - - # Common properties - g.visible = True - g.opacity = 0.95 - g.z_index = 1 - print_test(f"visible={g.visible}, opacity={g.opacity}, z_index={g.z_index}") - - # Grid point access - print("\n Grid Points:") - - # Access grid point - point = g.at(5, 5) - print_test(f"at(5, 5) returned GridPoint") - - # Modify grid point - point.tilesprite = 10 - point.tile_overlay = 2 - point.walkable = False - point.transparent = True - point.color = Color(255, 0, 0, 128) - print_test("GridPoint properties modified") - - # Check modifications - print_test(f" tilesprite = {point.tilesprite}") - print_test(f" walkable = {point.walkable}") - print_test(f" transparent = {point.transparent}") - - # Entity collection - print("\n Entity Collection:") - - # Create entities - if texture: - e1 = Entity(10.5, 10.5, texture, sprite_index=5) - e2 = Entity(15.0, 12.0, texture, sprite_index=8) - - g.entities.append(e1) - g.entities.append(e2) - print_test(f"Added 2 entities, count = {len(g.entities)}") - - # Access entities - first = g.entities[0] - print_test(f"entities[0] at ({first.x}, {first.y})") - - # Iterate entities - count = 0 - for entity in g.entities: - count += 1 - print_test(f"Iterated {count} entities") - - # Methods - print("\n Methods:") - - # get_bounds - bounds = g.get_bounds() - print_test(f"get_bounds() = {bounds}") - - # move - g.move(20, 25) - print_test(f"move(20, 25) - new pos = ({g.x}, {g.y})") - - # Points array access - print("\n Points Array:") - - # The points property is a 2D array - all_points = g.points - print_test(f"points array dimensions: {len(all_points)}x{len(all_points[0]) if all_points else 0}") - - # Modify multiple points - for y in range(5): - for x in range(5): - pt = g.at(x, y) - pt.tilesprite = x + y * 5 - pt.color = Color(x * 50, y * 50, 100) - print_test("Modified 5x5 area of grid") - - return True - -def test_entity_api(): - """Test all Entity constructors and methods""" - print_section("ENTITY API TESTS") - - # Entities need to be in a grid - ui = mcrfpy.sceneUI("api_test") - - # Create grid and texture - texture = None - try: - texture = Texture("assets/sprites/entities.png", grid_size=(32, 32)) - print_test("Entity texture loaded") - except: - print_test("Entity texture load failed", False) - - grid = Grid(50, 50, grid_size=(30, 30), texture=texture) - ui.append(grid) - - # Constructor variants - print("\n Constructors:") - - # Empty constructor - e1 = Entity() - print_test(f"Entity() - pos=({e1.x}, {e1.y}), sprite_index={e1.sprite_index}") - grid.entities.append(e1) - - # Position only - e2 = Entity(5.5, 3.5) - print_test(f"Entity(5.5, 3.5)") - grid.entities.append(e2) - - # Position and texture - e3 = Entity(10.0, 8.0, texture) - print_test("Entity with texture") - grid.entities.append(e3) - - # Full constructor - e4 = Entity(15.5, 12.5, texture, sprite_index=7, scale=1.5) - print_test("Entity with all parameters") - grid.entities.append(e4) - - # Properties - print("\n Properties:") - - e = Entity(20.0, 15.0, texture, sprite_index=3) - grid.entities.append(e) - - # Position (float coordinates in grid space) - e.x = 22.5 - e.y = 16.5 - print_test(f"position = ({e.x}, {e.y})") - - # Position tuple - e.position = (24.0, 18.0) - print_test(f"position tuple = {e.position}") - - # Sprite index - e.sprite_index = 12 - print_test(f"sprite_index = {e.sprite_index}") - - # Scale - e.scale = 2.0 - print_test(f"scale = {e.scale}") - - # Methods - print("\n Methods:") - - # index() - get position in entity collection - idx = e.index() - print_test(f"index() in collection = {idx}") - - # Gridstate (visibility per grid cell) - print("\n Grid State:") - - # Access gridstate - if len(e.gridstate) > 0: - state = e.gridstate[0] - print_test(f"gridstate[0] - visible={state.visible}, discovered={state.discovered}") - - # Modify visibility - state.visible = True - state.discovered = True - print_test("Modified gridstate visibility") - - # at() method - check if entity occupies a grid point - # This would need a GridPointState object - # occupied = e.at(some_gridpoint_state) - - # die() method - remove from grid - print("\n Entity Lifecycle:") - - # Create temporary entity - temp_entity = Entity(25.0, 25.0, texture) - grid.entities.append(temp_entity) - count_before = len(grid.entities) - - # Remove it - temp_entity.die() - count_after = len(grid.entities) - print_test(f"die() - entity count: {count_before} -> {count_after}") - - # Entity movement - print("\n Entity Movement:") - - # Test fractional positions (entities can be between grid cells) - e.position = (10.0, 10.0) - print_test(f"Integer position: {e.position}") - - e.position = (10.5, 10.5) - print_test(f"Center of cell: {e.position}") - - e.position = (10.25, 10.75) - print_test(f"Fractional position: {e.position}") - - return True - -def test_collections(): - """Test UICollection and EntityCollection behaviors""" - print_section("COLLECTION API TESTS") - - ui = mcrfpy.sceneUI("api_test") - - # Test UICollection (scene UI and frame children) - print("\n UICollection (Scene UI):") - - # Clear scene - while len(ui) > 0: - ui.remove(ui[0]) - print_test(f"Cleared - length = {len(ui)}") - - # append - f1 = Frame(10, 10, 50, 50) - ui.append(f1) - print_test(f"append() - length = {len(ui)}") - - # extend - frames = [Frame(x * 60, 10, 50, 50) for x in range(1, 4)] - ui.extend(frames) - print_test(f"extend() with 3 items - length = {len(ui)}") - - # index access - item = ui[0] - print_test(f"ui[0] = Frame at ({item.x}, {item.y})") - - # slice access - slice_items = ui[1:3] - print_test(f"ui[1:3] returned {len(slice_items)} items") - - # index() method - idx = ui.index(f1) - print_test(f"index(frame) = {idx}") - - # count() method - cnt = ui.count(f1) - print_test(f"count(frame) = {cnt}") - - # in operator - contains = f1 in ui - print_test(f"frame in ui = {contains}") - - # iteration - count = 0 - for item in ui: - count += 1 - print_test(f"Iteration counted {count} items") - - # remove - ui.remove(f1) - print_test(f"remove() - length = {len(ui)}") - - # Test Frame.children collection - print("\n UICollection (Frame Children):") - - parent = Frame(100, 100, 300, 200) - ui.append(parent) - - # Add children - child1 = Caption("Child 1", 10, 10) - child2 = Caption("Child 2", 10, 30) - child3 = Frame(10, 50, 50, 50) - - parent.children.append(child1) - parent.children.append(child2) - parent.children.append(child3) - print_test(f"Added 3 children - count = {len(parent.children)}") - - # Mixed types in collection - has_caption = any(isinstance(child, Caption) for child in parent.children) - has_frame = any(isinstance(child, Frame) for child in parent.children) - print_test(f"Mixed types: has Caption = {has_caption}, has Frame = {has_frame}") - - # Test EntityCollection - print("\n EntityCollection (Grid Entities):") - - texture = None - try: - texture = Texture("assets/sprites/entities.png", grid_size=(32, 32)) - except: - pass - - grid = Grid(400, 100, grid_size=(20, 20), texture=texture) - ui.append(grid) - - # Add entities - entities = [] - for i in range(5): - e = Entity(float(i * 2), float(i * 2), texture, sprite_index=i) - grid.entities.append(e) - entities.append(e) - - print_test(f"Added 5 entities - count = {len(grid.entities)}") - - # Access and iteration - first_entity = grid.entities[0] - print_test(f"entities[0] at ({first_entity.x}, {first_entity.y})") - - # Remove entity - grid.entities.remove(first_entity) - print_test(f"Removed entity - count = {len(grid.entities)}") - - return True - -def test_animation_api(): - """Test Animation class API""" - print_section("ANIMATION API TESTS") - - ui = mcrfpy.sceneUI("api_test") - - # Import Animation - from mcrfpy import Animation - - print("\n Animation Constructors:") - - # Basic animation - anim1 = Animation("x", 100.0, 2.0) - print_test("Animation('x', 100.0, 2.0)") - - # With easing - anim2 = Animation("y", 200.0, 3.0, "easeInOut") - print_test("Animation with easing='easeInOut'") - - # Delta mode - anim3 = Animation("w", 50.0, 1.5, "linear", delta=True) - print_test("Animation with delta=True") - - # Color animation - anim4 = Animation("fill_color", Color(255, 0, 0), 2.0) - print_test("Animation with Color target") - - # Vector animation - anim5 = Animation("position", (10.0, 20.0), 2.5, "easeOutBounce") - print_test("Animation with position tuple") - - # Sprite sequence - anim6 = Animation("sprite_index", [0, 1, 2, 3, 2, 1], 2.0) - print_test("Animation with sprite sequence") - - # Properties - print("\n Animation Properties:") - - # Check properties - print_test(f"property = '{anim1.property}'") - print_test(f"duration = {anim1.duration}") - print_test(f"elapsed = {anim1.elapsed}") - print_test(f"is_complete = {anim1.is_complete}") - print_test(f"is_delta = {anim3.is_delta}") - - # Methods - print("\n Animation Methods:") - - # Create test frame - frame = Frame(50, 50, 100, 100) - frame.fill_color = Color(100, 100, 100) - ui.append(frame) - - # Start animation - anim1.start(frame) - print_test("start() called on frame") - - # Get current value (before update) - current = anim1.get_current_value() - print_test(f"get_current_value() = {current}") - - # Manual update (usually automatic) - anim1.update(0.5) # 0.5 seconds - print_test("update(0.5) called") - - # Check elapsed time - print_test(f"elapsed after update = {anim1.elapsed}") - - # All easing functions - print("\n Available Easing Functions:") - easings = [ - "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" - ] - - # Test creating animation with each easing - for easing in easings[:10]: # Test first 10 - try: - test_anim = Animation("x", 100.0, 1.0, easing) - print_test(f"Easing '{easing}' โœ“") - except: - print_test(f"Easing '{easing}' failed", False) - - return True - -def test_scene_api(): - """Test scene-related API functions""" - print_section("SCENE API TESTS") - - print("\n Scene Management:") - - # Create scene - mcrfpy.createScene("test_scene_1") - print_test("createScene('test_scene_1')") - - mcrfpy.createScene("test_scene_2") - print_test("createScene('test_scene_2')") - - # Set active scene - mcrfpy.setScene("test_scene_1") - print_test("setScene('test_scene_1')") - - # Get scene UI - ui1 = mcrfpy.sceneUI("test_scene_1") - print_test(f"sceneUI('test_scene_1') - collection size = {len(ui1)}") - - ui2 = mcrfpy.sceneUI("test_scene_2") - print_test(f"sceneUI('test_scene_2') - collection size = {len(ui2)}") - - # Add content to scenes - ui1.append(Frame(10, 10, 100, 100)) - ui1.append(Caption("Scene 1", 10, 120)) - print_test(f"Added content to scene 1 - size = {len(ui1)}") - - ui2.append(Frame(20, 20, 150, 150)) - ui2.append(Caption("Scene 2", 20, 180)) - print_test(f"Added content to scene 2 - size = {len(ui2)}") - - # Scene transitions - print("\n Scene Transitions:") - - # Note: Actual transition types would need to be tested visually - # TransitionType enum: None, Fade, SlideLeft, SlideRight, SlideUp, SlideDown - - # Keypress handling - print("\n Input Handling:") - - def test_keypress(scene_name, keycode): - print(f" Key pressed in {scene_name}: {keycode}") - - mcrfpy.keypressScene("test_scene_1", test_keypress) - print_test("keypressScene() handler registered") - - return True - -def test_audio_api(): - """Test audio-related API functions""" - print_section("AUDIO API TESTS") - - print("\n Sound Functions:") - - # Create sound buffer - try: - mcrfpy.createSoundBuffer("test_sound", "assets/audio/click.wav") - print_test("createSoundBuffer('test_sound', 'click.wav')") - - # Play sound - mcrfpy.playSound("test_sound") - print_test("playSound('test_sound')") - - # Set volume - mcrfpy.setVolume("test_sound", 0.5) - print_test("setVolume('test_sound', 0.5)") - - except Exception as e: - print_test(f"Audio functions failed: {e}", False) - - return True - -def test_timer_api(): - """Test timer API functions""" - print_section("TIMER API TESTS") - - print("\n Timer Functions:") - - # Timer callback - def timer_callback(runtime): - print(f" Timer fired at runtime: {runtime}") - - # Set timer - mcrfpy.setTimer("test_timer", timer_callback, 1000) # 1 second - print_test("setTimer('test_timer', callback, 1000)") - - # Delete timer - mcrfpy.delTimer("test_timer") - print_test("delTimer('test_timer')") - - # Multiple timers - mcrfpy.setTimer("timer1", lambda r: print(f" Timer 1: {r}"), 500) - mcrfpy.setTimer("timer2", lambda r: print(f" Timer 2: {r}"), 750) - mcrfpy.setTimer("timer3", lambda r: print(f" Timer 3: {r}"), 1000) - print_test("Set 3 timers with different intervals") - - # Clean up - mcrfpy.delTimer("timer1") - mcrfpy.delTimer("timer2") - mcrfpy.delTimer("timer3") - print_test("Cleaned up all timers") - - return True - -def test_edge_cases(): - """Test edge cases and error conditions""" - print_section("EDGE CASES AND ERROR HANDLING") - - ui = mcrfpy.sceneUI("api_test") - - print("\n Boundary Values:") - - # Negative positions - f = Frame(-100, -50, 50, 50) - print_test(f"Negative position: ({f.x}, {f.y})") - - # Zero size - f2 = Frame(0, 0, 0, 0) - print_test(f"Zero size: ({f2.w}, {f2.h})") - - # Very large values - f3 = Frame(10000, 10000, 5000, 5000) - print_test(f"Large values: pos=({f3.x}, {f3.y}), size=({f3.w}, {f3.h})") - - # Opacity bounds - f.opacity = -0.5 - print_test(f"Opacity below 0: {f.opacity}") - - f.opacity = 2.0 - print_test(f"Opacity above 1: {f.opacity}") - - # Color component bounds - c = Color(300, -50, 1000, 128) - print_test(f"Color out of bounds: ({c.r}, {c.g}, {c.b}, {c.a})") - - print("\n Empty Collections:") - - # Empty children - frame = Frame(0, 0, 100, 100) - print_test(f"Empty children collection: {len(frame.children)}") - - # Access empty collection - try: - item = frame.children[0] - print_test("Accessing empty collection[0]", False) - except IndexError: - print_test("Accessing empty collection[0] raises IndexError") - - print("\n Invalid Operations:") - - # Grid without texture - g = Grid(0, 0, grid_size=(10, 10)) - point = g.at(5, 5) - point.tilesprite = 10 # No texture to reference - print_test("Set tilesprite without texture") - - # Entity without grid - e = Entity(5.0, 5.0) - # e.die() would fail if not in a grid - print_test("Created entity without grid") - - return True - -def run_all_tests(): - """Run all API tests""" - print("\n" + "="*60) - print(" McRogueFace Exhaustive API Test Suite") - print(" Testing every constructor and method...") - print("="*60) - - # Run each test category - test_functions = [ - test_color_api, - test_vector_api, - test_frame_api, - test_caption_api, - test_sprite_api, - test_grid_api, - test_entity_api, - test_collections, - test_animation_api, - test_scene_api, - test_audio_api, - test_timer_api, - test_edge_cases - ] - - passed = 0 - failed = 0 - - for test_func in test_functions: - try: - if test_func(): - passed += 1 - else: - failed += 1 - except Exception as e: - print(f"\n ERROR in {test_func.__name__}: {e}") - failed += 1 - - # Summary - print("\n" + "="*60) - print(f" TEST SUMMARY: {passed} passed, {failed} failed") - print("="*60) - - # Visual test scene - print("\n Visual elements are displayed in the 'api_test' scene.") - print(" The test is complete. Press ESC to exit.") - -def handle_exit(scene_name, keycode): - """Handle ESC key to exit""" - if keycode == 256: # ESC - print("\nExiting API test suite...") - sys.exit(0) - -# Set up exit handler -mcrfpy.keypressScene("api_test", handle_exit) - -# Run after short delay to ensure scene is ready -def start_tests(runtime): - run_all_tests() - -mcrfpy.setTimer("start_tests", start_tests, 100) - -print("Starting McRogueFace Exhaustive API Demo...") -print("This will test EVERY constructor and method.") -print("Press ESC to exit at any time.") \ No newline at end of file diff --git a/tests/demos/pathfinding_showcase.py b/tests/demos/pathfinding_showcase.py index d4e082f..31b9f37 100644 --- a/tests/demos/pathfinding_showcase.py +++ b/tests/demos/pathfinding_showcase.py @@ -48,6 +48,10 @@ mode = "CHASE" show_dijkstra = False animation_speed = 3.0 +# Track waypoints separately since Entity doesn't have custom attributes +entity_waypoints = {} # entity -> [(x, y), ...] +entity_waypoint_indices = {} # entity -> current index + def create_dungeon(): """Create a dungeon-like map""" global grid @@ -126,37 +130,34 @@ def spawn_entities(): global player, enemies, treasures, patrol_entities # Clear existing entities - grid.entities.clear() + #grid.entities.clear() enemies = [] treasures = [] patrol_entities = [] # Spawn player in center room - player = mcrfpy.Entity(15, 11) - player.sprite_index = PLAYER + player = mcrfpy.Entity((15, 11), mcrfpy.default_texture, PLAYER) grid.entities.append(player) # Spawn enemies in corners enemy_positions = [(4, 4), (24, 4), (4, 16), (24, 16)] for x, y in enemy_positions: - enemy = mcrfpy.Entity(x, y) - enemy.sprite_index = ENEMY + enemy = mcrfpy.Entity((x, y), mcrfpy.default_texture, ENEMY) grid.entities.append(enemy) enemies.append(enemy) # Spawn treasures treasure_positions = [(6, 5), (24, 5), (15, 10)] for x, y in treasure_positions: - treasure = mcrfpy.Entity(x, y) - treasure.sprite_index = TREASURE + treasure = mcrfpy.Entity((x, y), mcrfpy.default_texture, TREASURE) grid.entities.append(treasure) treasures.append(treasure) # Spawn patrol entities - patrol = mcrfpy.Entity(10, 10) - patrol.sprite_index = PATROL - patrol.waypoints = [(10, 10), (19, 10), (19, 16), (10, 16)] # Square patrol - patrol.waypoint_index = 0 + patrol = mcrfpy.Entity((10, 10), mcrfpy.default_texture, PATROL) + # Store waypoints separately since Entity doesn't support custom attributes + entity_waypoints[patrol] = [(10, 10), (19, 10), (19, 16), (10, 16)] # Square patrol + entity_waypoint_indices[patrol] = 0 grid.entities.append(patrol) patrol_entities.append(patrol) @@ -222,18 +223,21 @@ def move_enemies(dt): def move_patrols(dt): """Move patrol entities along waypoints""" for patrol in patrol_entities: - if not hasattr(patrol, 'waypoints'): + if patrol not in entity_waypoints: continue # Get current waypoint - target_x, target_y = patrol.waypoints[patrol.waypoint_index] + waypoints = entity_waypoints[patrol] + waypoint_index = entity_waypoint_indices[patrol] + target_x, target_y = waypoints[waypoint_index] # Check if reached waypoint dist = abs(patrol.x - target_x) + abs(patrol.y - target_y) if dist < 0.5: # Move to next waypoint - patrol.waypoint_index = (patrol.waypoint_index + 1) % len(patrol.waypoints) - target_x, target_y = patrol.waypoints[patrol.waypoint_index] + entity_waypoint_indices[patrol] = (waypoint_index + 1) % len(waypoints) + waypoint_index = entity_waypoint_indices[patrol] + target_x, target_y = waypoints[waypoint_index] # Path to waypoint path = patrol.path_to(target_x, target_y) @@ -370,4 +374,4 @@ mcrfpy.setTimer("entities", update_entities, 16) # 60 FPS # Show scene mcrfpy.setScene("pathfinding_showcase") -print("\nShowcase ready! Move with WASD and watch entities react.") \ No newline at end of file +print("\nShowcase ready! Move with WASD and watch entities react.") diff --git a/tests/demos/simple_text_input.py b/tests/demos/simple_text_input.py index e775670..ad11509 100644 --- a/tests/demos/simple_text_input.py +++ b/tests/demos/simple_text_input.py @@ -28,11 +28,11 @@ class TextInput: # Label if self.label: self.label_caption = mcrfpy.Caption(self.label, self.x, self.y - 20) - self.label_caption.color = (255, 255, 255, 255) + self.label_caption.fill_color = (255, 255, 255, 255) # Text display self.text_caption = mcrfpy.Caption("", self.x + 4, self.y + 4) - self.text_caption.color = (0, 0, 0, 255) + self.text_caption.fill_color = (0, 0, 0, 255) # Cursor (a simple vertical line using a frame) self.cursor = mcrfpy.Frame(self.x + 4, self.y + 4, 2, 16) @@ -176,7 +176,7 @@ def create_scene(): # Title title = mcrfpy.Caption("Text Input Widget Demo", 10, 10) - title.color = (255, 255, 255, 255) + title.fill_color = (255, 255, 255, 255) scene.append(title) # Create input fields @@ -194,7 +194,7 @@ def create_scene(): # Status text status = mcrfpy.Caption("Click to focus, type to enter text", 50, 280) - status.color = (200, 200, 200, 255) + status.fill_color = (200, 200, 200, 255) scene.append(status) # Keyboard handler diff --git a/tests/demos/sizzle_reel_final.py b/tests/demos/sizzle_reel_final.py index 8251498..94ac610 100644 --- a/tests/demos/sizzle_reel_final.py +++ b/tests/demos/sizzle_reel_final.py @@ -5,12 +5,19 @@ McRogueFace Animation Sizzle Reel - Final Version Complete demonstration of all animation capabilities. This version works properly with the game loop and avoids API issues. + +WARNING: This demo causes a segmentation fault due to a bug in the +AnimationManager. When UI elements with active animations are removed +from the scene, the AnimationManager crashes when trying to update them. + +Use sizzle_reel_final_fixed.py instead, which works around this issue +by hiding objects off-screen instead of removing them. """ import mcrfpy # Configuration -DEMO_DURATION = 4.0 # Duration for each demo +DEMO_DURATION = 6.0 # Duration for each demo # All available easing functions EASING_FUNCTIONS = [ @@ -41,6 +48,7 @@ def create_scene(): title = mcrfpy.Caption("Animation Sizzle Reel", 500, 20) title.fill_color = mcrfpy.Color(255, 255, 0) title.outline = 2 + title.font_size = 28 ui.append(title) # Subtitle @@ -79,18 +87,21 @@ def demo2_caption_animations(): # Moving caption c1 = mcrfpy.Caption("Bouncing Text!", 100, 200) c1.fill_color = mcrfpy.Color(255, 255, 255) + c1.font_size = 28 ui.append(c1) mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1) # Color cycling c2 = mcrfpy.Caption("Color Cycle", 400, 300) c2.outline = 2 + c2.font_size = 28 ui.append(c2) mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear").start(c2) # Typewriter effect c3 = mcrfpy.Caption("", 100, 400) c3.fill_color = mcrfpy.Color(0, 255, 255) + c3.font_size = 28 ui.append(c3) mcrfpy.Animation("text", "Typewriter effect animation...", 3.0, "linear").start(c3) @@ -147,7 +158,7 @@ def clear_demo_objects(): # Keep removing items after the first 2 (title and subtitle) while len(ui) > 2: # Remove the last item - ui.remove(ui[len(ui)-1]) + ui.remove(len(ui)-1) def next_demo(runtime): """Run the next demo""" @@ -167,11 +178,13 @@ def next_demo(runtime): current_demo += 1 if current_demo < len(demos): - mcrfpy.setTimer("next", next_demo, int(DEMO_DURATION * 1000)) + #mcrfpy.setTimer("next", next_demo, int(DEMO_DURATION * 1000)) + pass else: subtitle.text = "Demo Complete!" # Initialize print("Starting Animation Sizzle Reel...") create_scene() -mcrfpy.setTimer("start", next_demo, 500) \ No newline at end of file +mcrfpy.setTimer("start", next_demo, int(DEMO_DURATION * 1000)) +next_demo(0) diff --git a/tests/demos/sizzle_reel_final_fixed.py b/tests/demos/sizzle_reel_final_fixed.py new file mode 100644 index 0000000..0ecf99a --- /dev/null +++ b/tests/demos/sizzle_reel_final_fixed.py @@ -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) \ No newline at end of file diff --git a/tests/demos/text_input_demo.py b/tests/demos/text_input_demo.py index 5e5de6a..51538bb 100644 --- a/tests/demos/text_input_demo.py +++ b/tests/demos/text_input_demo.py @@ -60,12 +60,12 @@ def create_demo(): scene.append(bg) # Title - title = mcrfpy.Caption(10, 10, "Text Input Widget Demo - Auto Test", font_size=24) + title = mcrfpy.Caption(10, 10, "Text Input Widget Demo - Auto Test") title.color = (255, 255, 255, 255) scene.append(title) # Instructions - instructions = mcrfpy.Caption(10, 50, "This will automatically test the text input system", font_size=14) + instructions = mcrfpy.Caption(10, 50, "This will automatically test the text input system") instructions.color = (200, 200, 200, 255) scene.append(instructions) @@ -109,7 +109,7 @@ def create_demo(): fields.append(comment_input) # Result display - result_text = mcrfpy.Caption(50, 320, "Values will appear here as you type...", font_size=14) + result_text = mcrfpy.Caption(50, 320, "Values will appear here as you type...") result_text.color = (150, 255, 150, 255) scene.append(result_text) diff --git a/tests/demos/text_input_standalone.py b/tests/demos/text_input_standalone.py index fa6fe81..2bcf7d8 100644 --- a/tests/demos/text_input_standalone.py +++ b/tests/demos/text_input_standalone.py @@ -79,8 +79,7 @@ class TextInput: self.label_text = mcrfpy.Caption( self.x - 5, self.y - self.font_size - 5, - self.label, - font_size=self.font_size + self.label ) self.label_text.color = (255, 255, 255, 255) @@ -88,8 +87,7 @@ class TextInput: self.text_display = mcrfpy.Caption( self.x + 4, self.y + 4, - "", - font_size=self.font_size + "" ) self.text_display.color = (0, 0, 0, 255) @@ -260,12 +258,12 @@ def create_demo(): scene.append(bg) # Title - title = mcrfpy.Caption(10, 10, "Text Input Widget System", font_size=24) + title = mcrfpy.Caption(10, 10, "Text Input Widget System") title.color = (255, 255, 255, 255) scene.append(title) # Instructions - info = mcrfpy.Caption(10, 50, "Click to focus | Tab to switch fields | Type to enter text", font_size=14) + info = mcrfpy.Caption(10, 50, "Click to focus | Tab to switch fields | Type to enter text") info.color = (200, 200, 200, 255) scene.append(info) @@ -289,7 +287,7 @@ def create_demo(): comment_input.add_to_scene(scene) # Status display - status = mcrfpy.Caption(50, 320, "Ready for input...", font_size=14) + status = mcrfpy.Caption(50, 320, "Ready for input...") status.color = (150, 255, 150, 255) scene.append(status) diff --git a/tests/demos/text_input_widget.py b/tests/demos/text_input_widget.py index 0986a7c..adbd201 100644 --- a/tests/demos/text_input_widget.py +++ b/tests/demos/text_input_widget.py @@ -95,8 +95,7 @@ class TextInput: self.label_text = mcrfpy.Caption( self.x - 5, self.y - self.font_size - 5, - self.label, - font_size=self.font_size + self.label ) self.label_text.color = (255, 255, 255, 255) @@ -104,8 +103,7 @@ class TextInput: self.text_display = mcrfpy.Caption( self.x + 4, self.y + 4, - "", - font_size=self.font_size + "" ) self.text_display.color = (0, 0, 0, 255) @@ -227,12 +225,12 @@ def create_demo(): scene.append(bg) # Title - title = mcrfpy.Caption(10, 10, "Text Input Widget Demo", font_size=24) + title = mcrfpy.Caption(10, 10, "Text Input Widget Demo") title.color = (255, 255, 255, 255) scene.append(title) # Instructions - instructions = mcrfpy.Caption(10, 50, "Click to focus, Tab to switch fields, Type to enter text", font_size=14) + instructions = mcrfpy.Caption(10, 50, "Click to focus, Tab to switch fields, Type to enter text") instructions.color = (200, 200, 200, 255) scene.append(instructions) @@ -276,7 +274,7 @@ def create_demo(): fields.append(comment_input) # Result display - result_text = mcrfpy.Caption(50, 320, "Type in the fields above...", font_size=14) + result_text = mcrfpy.Caption(50, 320, "Type in the fields above...") result_text.color = (150, 255, 150, 255) scene.append(result_text)