diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index b88733b..0000000 --- a/ROADMAP.md +++ /dev/null @@ -1,935 +0,0 @@ -# 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 deleted file mode 100644 index eb9ed94..0000000 --- a/roguelike_tutorial/part_0.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -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 deleted file mode 100644 index 4c19d6d..0000000 --- a/roguelike_tutorial/part_1.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -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 deleted file mode 100644 index 3894fc7..0000000 --- a/roguelike_tutorial/part_1b.py +++ /dev/null @@ -1,117 +0,0 @@ -""" -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 deleted file mode 100644 index 6959a4b..0000000 --- a/roguelike_tutorial/part_2-naive.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -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 deleted file mode 100644 index 126c433..0000000 --- a/roguelike_tutorial/part_2-onemovequeued.py +++ /dev/null @@ -1,241 +0,0 @@ -""" -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 deleted file mode 100644 index 66a11b0..0000000 --- a/roguelike_tutorial/part_2.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -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 deleted file mode 100644 index e785419..0000000 Binary files a/roguelike_tutorial/tutorial2.png and /dev/null differ diff --git a/roguelike_tutorial/tutorial_hero.png b/roguelike_tutorial/tutorial_hero.png deleted file mode 100644 index c202176..0000000 Binary files a/roguelike_tutorial/tutorial_hero.png and /dev/null differ diff --git a/src/Animation.cpp b/src/Animation.cpp index 20b8fad..7fa27ce 100644 --- a/src/Animation.cpp +++ b/src/Animation.cpp @@ -1,8 +1,6 @@ #include "Animation.h" #include "UIDrawable.h" #include "UIEntity.h" -#include "PyAnimation.h" -#include "McRFPy_API.h" #include #include #include @@ -11,100 +9,75 @@ #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, - PyObject* callback) + bool delta) : targetProperty(targetProperty) , targetValue(targetValue) , duration(duration) , easingFunc(easingFunc) , delta(delta) - , pythonCallback(callback) { - // Increase reference count for Python callback - if (pythonCallback) { - Py_INCREF(pythonCallback); - } } -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; +void Animation::start(UIDrawable* target) { + currentTarget = target; elapsed = 0.0f; - callbackTriggered = false; // Reset callback state - // Capture start value from target - std::visit([this, &target](const auto& targetVal) { + // Capture startValue from target based on targetProperty + if (!currentTarget) return; + + // Try to get the current value based on the expected type + std::visit([this](const auto& targetVal) { using T = std::decay_t; if constexpr (std::is_same_v) { float value; - if (target->getProperty(targetProperty, value)) { + if (currentTarget->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v) { int value; - if (target->getProperty(targetProperty, value)) { + if (currentTarget->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v>) { // For sprite animation, get current sprite index int value; - if (target->getProperty(targetProperty, value)) { + if (currentTarget->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v) { sf::Color value; - if (target->getProperty(targetProperty, value)) { + if (currentTarget->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v) { sf::Vector2f value; - if (target->getProperty(targetProperty, value)) { + if (currentTarget->getProperty(targetProperty, value)) { startValue = value; } } else if constexpr (std::is_same_v) { std::string value; - if (target->getProperty(targetProperty, value)) { + if (currentTarget->getProperty(targetProperty, value)) { startValue = value; } } }, targetValue); } -void Animation::startEntity(std::shared_ptr target) { - if (!target) return; - - entityTargetWeak = target; +void Animation::startEntity(UIEntity* target) { + currentEntityTarget = target; + currentTarget = nullptr; // Clear drawable target elapsed = 0.0f; - callbackTriggered = false; // Reset callback state // Capture the starting value from the entity std::visit([this, target](const auto& val) { @@ -126,49 +99,8 @@ void Animation::startEntity(std::shared_ptr 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) { - // 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()) { + if ((!currentTarget && !currentEntityTarget) || isComplete()) { return false; } @@ -182,18 +114,39 @@ bool Animation::update(float deltaTime) { // Get interpolated value AnimationValue currentValue = interpolate(easedT); - // 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(); - } + // 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); return !isComplete(); } @@ -301,77 +254,6 @@ 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 { @@ -620,50 +502,26 @@ AnimationManager& AnimationManager::getInstance() { } void AnimationManager::addAnimation(std::shared_ptr animation) { - if (animation && animation->hasValidTarget()) { - if (isUpdating) { - // Defer adding during update to avoid iterator invalidation - pendingAnimations.push_back(animation); - } else { - activeAnimations.push_back(animation); - } - } + activeAnimations.push_back(animation); } void AnimationManager::update(float deltaTime) { - // Set flag to defer new animations - isUpdating = true; - - // Remove completed or invalid animations + for (auto& anim : activeAnimations) { + anim->update(deltaTime); + } + cleanup(); +} + +void AnimationManager::cleanup() { activeAnimations.erase( std::remove_if(activeAnimations.begin(), activeAnimations.end(), - [deltaTime](std::shared_ptr& anim) { - return !anim || !anim->update(deltaTime); + [](const std::shared_ptr& anim) { + return anim->isComplete(); }), 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(bool completeAnimations) { - if (completeAnimations) { - // Complete all animations before clearing - for (auto& anim : activeAnimations) { - if (anim) { - anim->complete(); - } - } - } +void AnimationManager::clear() { activeAnimations.clear(); } \ No newline at end of file diff --git a/src/Animation.h b/src/Animation.h index 181bec4..6308f32 100644 --- a/src/Animation.h +++ b/src/Animation.h @@ -6,7 +6,6 @@ #include #include #include -#include "Python.h" // Forward declarations class UIDrawable; @@ -37,20 +36,13 @@ public: const AnimationValue& targetValue, float duration, EasingFunction easingFunc = EasingFunctions::linear, - bool delta = false, - PyObject* callback = nullptr); - - // Destructor - cleanup Python callback reference - ~Animation(); + bool delta = false); // Apply this animation to a drawable - void start(std::shared_ptr target); + void start(UIDrawable* target); // Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable) - void startEntity(std::shared_ptr target); - - // Complete the animation immediately (jump to final value) - void complete(); + void startEntity(UIEntity* target); // Update animation (called each frame) // Returns true if animation is still running, false if complete @@ -59,12 +51,6 @@ 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; } @@ -81,24 +67,11 @@ private: EasingFunction easingFunc; // Easing function to use bool delta; // If true, targetValue is relative to start - // 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 + UIDrawable* currentTarget = nullptr; // Current target being animated + UIEntity* currentEntityTarget = nullptr; // Current entity target (alternative to drawable) // Helper to interpolate between values AnimationValue interpolate(float t) const; - - // 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 @@ -161,12 +134,13 @@ public: // Update all animations void update(float deltaTime); - // Clear all animations (optionally completing them first) - void clear(bool completeAnimations = false); + // Remove completed animations + void cleanup(); + + // Clear all animations + void clear(); 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 dcba0e4..5b35d79 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 = "McRogueFace Engine"; + window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine"; // Initialize rendering based on headless mode if (headless) { @@ -91,9 +91,6 @@ 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(); @@ -185,7 +182,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(); @@ -262,7 +259,7 @@ void GameEngine::run() int tenth_fps = (metrics.fps * 10) % 10; if (!headless && window) { - window->setTitle(window_title); + window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS"); } // In windowed mode, check if window was closed diff --git a/src/PyAnimation.cpp b/src/PyAnimation.cpp index d45c6eb..720b8d9 100644 --- a/src/PyAnimation.cpp +++ b/src/PyAnimation.cpp @@ -18,31 +18,19 @@ 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", "callback", nullptr}; + static const char* keywords[] = {"property", "target", "duration", "easing", "delta", nullptr}; const char* property_name; PyObject* target_value; float duration; const char* easing_name = "linear"; int delta = 0; - PyObject* callback = nullptr; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast(keywords), - &property_name, &target_value, &duration, &easing_name, &delta, &callback)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|sp", const_cast(keywords), + &property_name, &target_value, &duration, &easing_name, &delta)) { 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; @@ -102,7 +90,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, callback); + self->data = std::make_shared(property_name, animValue, duration, easingFunc, delta != 0); return 0; } @@ -138,50 +126,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; - if (frame->data) { - self->data->start(frame->data); - AnimationManager::getInstance().addAnimation(self->data); - } + drawable = frame->data.get(); } else if (strcmp(type_name, "mcrfpy.Caption") == 0) { PyUICaptionObject* caption = (PyUICaptionObject*)target_obj; - if (caption->data) { - self->data->start(caption->data); - AnimationManager::getInstance().addAnimation(self->data); - } + drawable = caption->data.get(); } else if (strcmp(type_name, "mcrfpy.Sprite") == 0) { PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj; - if (sprite->data) { - self->data->start(sprite->data); - AnimationManager::getInstance().addAnimation(self->data); - } + drawable = sprite->data.get(); } else if (strcmp(type_name, "mcrfpy.Grid") == 0) { PyUIGridObject* grid = (PyUIGridObject*)target_obj; - if (grid->data) { - self->data->start(grid->data); - AnimationManager::getInstance().addAnimation(self->data); - } + drawable = grid->data.get(); } else if (strcmp(type_name, "mcrfpy.Entity") == 0) { // Special handling for Entity since it doesn't inherit from UIDrawable PyUIEntityObject* entity = (PyUIEntityObject*)target_obj; - if (entity->data) { - self->data->startEntity(entity->data); - AnimationManager::getInstance().addAnimation(self->data); - } + // Start the animation directly on the entity + self->data->startEntity(entity->data.get()); + + // Add to AnimationManager + AnimationManager::getInstance().addAnimation(self->data); + + Py_RETURN_NONE; } else { PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity"); return NULL; } + // Start the animation + self->data->start(drawable); + + // Add to AnimationManager + AnimationManager::getInstance().addAnimation(self->data); + Py_RETURN_NONE; } @@ -226,20 +214,6 @@ 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}, @@ -251,23 +225,10 @@ PyGetSetDef PyAnimation::getsetters[] = { PyMethodDef PyAnimation::methods[] = { {"start", (PyCFunction)start, METH_VARARGS, - "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."}, + "Start the animation on a target UIDrawable"}, {"update", (PyCFunction)update, METH_VARARGS, "Update the animation by deltaTime (returns True if still running)"}, {"get_current_value", (PyCFunction)get_current_value, METH_NOARGS, "Get the current interpolated value"}, - {"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 ccb4f36..9976cb2 100644 --- a/src/PyAnimation.h +++ b/src/PyAnimation.h @@ -28,8 +28,6 @@ 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 new file mode 100644 index 0000000..d827789 --- /dev/null +++ b/src/PyArgHelpers.h @@ -0,0 +1,410 @@ +#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 84b92a7..fb2a49e 100644 --- a/src/PyScene.cpp +++ b/src/PyScene.cpp @@ -31,18 +31,13 @@ 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)); - // 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; - } + // 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; }); - // 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; + // Check elements in z-order (top to bottom) + for (const auto& element : sorted_elements) { if (!element->visible) continue; if (auto target = element->click_at(sf::Vector2f(mousepos))) { diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 07cd586..1df752a 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -3,6 +3,7 @@ #include "PyColor.h" #include "PyVector.h" #include "PyFont.h" +#include "PyArgHelpers.h" // UIDrawable methods now in UIBase.h #include @@ -302,135 +303,183 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) { using namespace mcrfpydef; - // Define all parameters with defaults - PyObject* pos_obj = nullptr; + // 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; 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; - // 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; + // 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); + return -1; + } + Py_DECREF(remaining_args); } - - // 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); + // 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 tuple must contain numbers"); + PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); return -1; } - } else { - PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); + } + } 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; + } + } } } - // 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; - } - auto obj = (PyFontObject*)font; - pyfont = obj->data; - } - - // 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 { + 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 + { // 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 + } } } - - // 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)); + + // 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(""); } - - // 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"); + 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__"); return -1; } - self->data->text.setFillColor(color_obj->data); - Py_DECREF(color_obj); + self->data->text.setFillColor(PyColor::fromPy(fc)); + //Py_DECREF(fc); } else { - self->data->text.setFillColor(sf::Color(255, 255, 255, 255)); // Default: white + self->data->text.setFillColor(sf::Color(0,0,0,255)); } - - // 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"); + + 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__"); return -1; } - self->data->text.setOutlineColor(color_obj->data); - Py_DECREF(color_obj); + self->data->text.setOutlineColor(PyColor::fromPy(oc)); + //Py_DECREF(oc); } else { - self->data->text.setOutlineColor(sf::Color(0, 0, 0, 255)); // Default: black + self->data->text.setOutlineColor(sf::Color(128,128,128,255)); } - - // 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 + + // Process click handler if provided if (click_handler && click_handler != Py_None) { if (!PyCallable_Check(click_handler)) { PyErr_SetString(PyExc_TypeError, "click must be callable"); @@ -438,11 +487,10 @@ 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 95e3f1a..9e29a35 100644 --- a/src/UICaption.h +++ b/src/UICaption.h @@ -65,37 +65,26 @@ namespace mcrfpydef { //.tp_iter //.tp_iternext .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Caption(pos=None, font=None, text='', **kwargs)\n\n" + .tp_doc = PyDoc_STR("Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)\n\n" "A text display UI element with customizable font and styling.\n\n" "Args:\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" + " 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" " 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" - " 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" + " click (callable): Click event handler. Default: None\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 3d8397d..4143ed0 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -4,6 +4,7 @@ #include #include "PyObjectUtils.h" #include "PyVector.h" +#include "PyArgHelpers.h" // UIDrawable methods now in UIBase.h #include "UIEntityPyMethods.h" @@ -120,57 +121,81 @@ PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) } int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { - // Define all parameters with defaults - PyObject* grid_pos_obj = nullptr; - PyObject* texture = nullptr; + // 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; + PyObject* texture = nullptr; PyObject* grid_obj = nullptr; - int visible = 1; - float opacity = 1.0f; - const char* name = nullptr; - float x = 0.0f, y = 0.0f; - // 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 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); } - - // 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); + // 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); + } } else { - PyErr_SetString(PyExc_TypeError, "grid_pos tuple must contain numbers"); + PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)"); return -1; } - } else { - PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)"); - return -1; } } - // Handle texture argument + // check types for texture + // + // Set Texture - allow None or use default + // std::shared_ptr texture_ptr = nullptr; - 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; - } + if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){ + PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); + return -1; + } else if (texture != NULL && texture != Py_None) { auto pytexture = (PyTextureObject*)texture; texture_ptr = pytexture->data; } else { @@ -178,20 +203,25 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { texture_ptr = McRFPy_API::default_texture; } - // Handle grid argument - if (grid_obj && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { + // 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"))) { PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance"); return -1; } - // Create the entity + // Always use default constructor for lazy initialization self->data = std::make_shared(); // Store reference to Python object self->data->self = (PyObject*)self; Py_INCREF(self); - // Set texture and sprite index + // TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers if (texture_ptr) { self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0); } else { @@ -200,20 +230,12 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { } // Set position using grid coordinates - self->data->position = sf::Vector2f(x, y); + self->data->position = sf::Vector2f(grid_x, grid_y); - // 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) { + if (grid_obj != NULL) { PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj; self->data->grid = pygrid->data; - // Append entity to grid's entity list + // todone - on creation of Entity with Grid assignment, also append it to the 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 508f4e1..dfd155e 100644 --- a/src/UIEntity.h +++ b/src/UIEntity.h @@ -88,28 +88,7 @@ namespace mcrfpydef { .tp_itemsize = 0, .tp_repr = (reprfunc)UIEntity::repr, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, - .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_doc = "UIEntity objects", .tp_methods = UIEntity_all_methods, .tp_getset = UIEntity::getsetters, .tp_base = &mcrfpydef::PyDrawableType, diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index ada2b67..aeb03bb 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -6,6 +6,7 @@ #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) @@ -431,47 +432,67 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) // Initialize children first self->data->children = std::make_shared>>(); - // Define all parameters with defaults - PyObject* pos_obj = nullptr; - PyObject* size_obj = nullptr; + // 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; 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; - // 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 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); } - - // 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(); + // 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) { 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); @@ -479,87 +500,47 @@ 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; } } - } - // 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); + + // 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); + } } else { - PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers"); + PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)"); 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 - // Set the position and size - self->data->position = sf::Vector2f(x, y); - self->data->box.setPosition(self->data->position); + self->data->position = sf::Vector2f(x, y); // Set base class position + self->data->box.setPosition(self->data->position); // Sync box position self->data->box.setSize(sf::Vector2f(w, h)); self->data->box.setOutlineThickness(outline); - - // 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); - } + // 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; // Process children argument if provided if (children_arg && children_arg != Py_None) { diff --git a/src/UIFrame.h b/src/UIFrame.h index 16c8596..2478001 100644 --- a/src/UIFrame.h +++ b/src/UIFrame.h @@ -86,38 +86,27 @@ namespace mcrfpydef { //.tp_iter //.tp_iternext .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Frame(pos=None, size=None, **kwargs)\n\n" + .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" "A rectangular frame UI element that can contain other drawable elements.\n\n" "Args:\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" + " 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" " 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" - " 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" + " children (list): Initial list of child drawable elements. Default: None\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 bf8ade6..d6a109e 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -1,6 +1,7 @@ #include "UIGrid.h" #include "GameEngine.h" #include "McRFPy_API.h" +#include "PyArgHelpers.h" #include // UIDrawable methods now in UIBase.h @@ -517,49 +518,102 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point) int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - // 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; + // Default values + int grid_x = 0, grid_y = 0; float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f; - int grid_x = 2, grid_y = 2; // Default to 2x2 grid + PyObject* textureObj = nullptr; - // 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; + // 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; + } } - // 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); + // Try tuple-based parsing if we have a tuple as first argument + if (has_tuple_first_arg) { + int arg_idx = 0; + auto grid_size_result = PyArgHelpers::parseGridSize(args, kwds, &arg_idx); + + // If grid size parsing failed with an error, report it + if (!grid_size_result.valid) { + if (grid_size_result.error) { + PyErr_SetString(PyExc_TypeError, grid_size_result.error); + } else { + PyErr_SetString(PyExc_TypeError, "Invalid grid size tuple"); + } + return -1; + } + + // We got a valid grid size + grid_x = grid_size_result.grid_w; + grid_y = grid_size_result.grid_h; + + // Try to parse position and size + auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx); + if (pos_result.valid) { + x = pos_result.x; + y = pos_result.y; + } + + auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx); + if (size_result.valid) { + w = size_result.w; + h = size_result.h; } else { - PyErr_Clear(); + // 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) { 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); @@ -568,50 +622,36 @@ 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 tuple must contain numbers"); + PyErr_SetString(PyExc_TypeError, "pos must contain numbers"); return -1; } } else { - PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector"); + PyErr_SetString(PyExc_TypeError, "pos must be a tuple of two 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); + + // 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; + } } else { - PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers"); + PyErr_SetString(PyExc_TypeError, "size must be a tuple of two numbers"); return -1; } } else { - 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; + // Default size based on grid + w = grid_x * 16.0f; + h = grid_y * 16.0f; } } @@ -621,8 +661,12 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { return -1; } - // Handle texture argument + // At this point we have x, y, w, h values from either parsing method + + // Convert PyObject texture to shared_ptr 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"); @@ -635,51 +679,14 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { texture_ptr = McRFPy_API::default_texture; } - // If size wasn't specified, calculate based on grid dimensions and texture - if (!size_obj && texture_ptr) { + // 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) { 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 0581eeb..96f41ed 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -184,49 +184,29 @@ namespace mcrfpydef { //.tp_iter //.tp_iternext .tp_flags = Py_TPFLAGS_DEFAULT, - .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" + .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" "Args:\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" + " 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" "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" - " grid_x, grid_y (int): Grid dimensions\n" + " tile_width, tile_height (int): Tile dimensions in pixels\n" " texture (Texture): Tile texture atlas\n" - " fill_color (Color): Background color\n" - " entities (EntityCollection): Collection of entities in the grid\n" - " perspective (int): Entity perspective index\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" " 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"), + " z_index (int): Rendering order"), .tp_methods = UIGrid_all_methods, //.tp_members = UIGrid::members, .tp_getset = UIGrid::getsetters, diff --git a/src/UISprite.cpp b/src/UISprite.cpp index 8cad830..8daf639 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -1,6 +1,7 @@ #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) @@ -326,46 +327,57 @@ PyObject* UISprite::repr(PyUISpriteObject* self) int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) { - // Define all parameters with defaults - PyObject* pos_obj = nullptr; - PyObject* texture = nullptr; + // 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; - float scale = 1.0f; - float scale_x = 1.0f; - float scale_y = 1.0f; + PyObject* texture = 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; - // 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 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); } - - // 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(); + // 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) { 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); @@ -373,10 +385,12 @@ 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; @@ -386,11 +400,10 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) // Handle texture - allow None or use default std::shared_ptr texture_ptr = nullptr; - 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; - } + if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){ + PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None"); + return -1; + } else if (texture != NULL && texture != Py_None) { auto pytexture = (PyTextureObject*)texture; texture_ptr = pytexture->data; } else { @@ -403,27 +416,9 @@ 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); - } - // Handle click handler + // Process click handler if provided 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 6fdc0a2..5e18ade 100644 --- a/src/UISprite.h +++ b/src/UISprite.h @@ -92,35 +92,23 @@ namespace mcrfpydef { //.tp_iter //.tp_iternext .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Sprite(pos=None, texture=None, sprite_index=0, **kwargs)\n\n" + .tp_doc = PyDoc_STR("Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)\n\n" "A sprite UI element that displays a texture or portion of a texture atlas.\n\n" "Args:\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" + " 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" "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): Uniform scale factor\n" - " scale_x, scale_y (float): Individual scale factors\n" + " scale (float): Scale multiplier\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 6519630..6b8ff59 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.x == old_pos.x and e.draw_pos.y == old_pos.y: e.ev_exit(self) + if e.draw_pos == old_pos: e.ev_exit(self) for e in self.game.entities: if e is self: continue - if e.draw_pos.x == tx and e.draw_pos.y == ty: e.ev_enter(self) + if e.draw_pos == (tx, 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.x + dx), int(self.draw_pos.y + dy) + tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + 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.x == tx and e.draw_pos.y == ty: + if e.draw_pos == (tx, 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.x + dx), int(self.draw_pos.y + dy) + tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + 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.x, caster.draw_pos.y + fx, fy = caster.draw_pos 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.x == spawn[0] and e.draw_pos.y == spawn[1]: break + if e.draw_pos == spawn: 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.x + dx), int(self.draw_pos.y + dy) + tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy) # Is the boulder blocked the same direction as the bumper? If not, let's both move - old_pos = int(self.draw_pos.x), int(self.draw_pos.y) + old_pos = int(self.draw_pos[0]), int(self.draw_pos[1]) 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.x), int(self.draw_pos.y) + pos = int(self.draw_pos[0]), int(self.draw_pos[1]) 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.x), int(self.draw_pos.y) + old_pos = int(self.draw_pos[0]), int(self.draw_pos[1]) 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.x), int(self.draw_pos.y) + old_pos = int(self.draw_pos[0]), int(self.draw_pos[1]) 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.x, self.draw_pos.y - px, py = self.game.player.draw_pos.x, self.game.player.draw_pos.y + x, y = self.draw_pos + px, py = self.game.player.draw_pos 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 079516f..4b80785 100644 --- a/src/scripts/cos_tiles.py +++ b/src/scripts/cos_tiles.py @@ -22,13 +22,12 @@ 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] - if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max: - values[d] = True - else: + try: values[d] = grid.at((tx, ty)).walkable + except ValueError: + values[d] = True return TileInfo(values) @staticmethod @@ -71,10 +70,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 - x_max, y_max = grid.grid_size - if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max: + try: + return grid.at((tx, ty)).tilesprite == allowed_tile + except ValueError: 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 0a7b6e4..8bee8c9 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.font_size = 26 + self.level_caption.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.font_size = 16 + i.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.x != self.player.draw_pos.x + dx or e.draw_pos.y != self.player.draw_pos.y + dy: continue + if e.draw_pos != (self.player.draw_pos[0] + dx, self.player.draw_pos[1] + 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.x, old_pos.y) + target_boulder.do_move(*old_pos) 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.font_size = font_size + self.caption.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.font_size = 64 + drop_shadow.size = 64 components.append( drop_shadow ) title_txt = mcrfpy.Caption((158, 18), "Crypt Of Sokoban", font, fill_color=(255, 255, 255)) - title_txt.font_size = 64 + title_txt.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.font_size = 28 + self.toast.size = 28 self.toast.outline = 2 self.toast.outline_color = (255, 255, 255) self.toast_event = None @@ -626,7 +626,6 @@ 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 deleted file mode 100644 index 7cd019a..0000000 --- a/tests/demo_animation_callback_usage.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/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 15c2e7c..d3b1e20 100644 --- a/tests/demos/animation_sizzle_reel.py +++ b/tests/demos/animation_sizzle_reel.py @@ -258,9 +258,8 @@ def demo_grid_animations(ui): except: texture = None - # Grid constructor: Grid(grid_x, grid_y, texture, position, size) - # Note: tile dimensions are determined by texture's grid_size - grid = Grid(20, 15, texture, (100, 150), (480, 360)) # 20x24, 15x24 + grid = Grid(100, 150, grid_size=(20, 15), texture=texture, + tile_width=24, tile_height=24) grid.fill_color = Color(20, 20, 40) ui.append(grid) @@ -283,7 +282,7 @@ def demo_grid_animations(ui): # Create entities in the grid if texture: - entity1 = Entity((5.0, 5.0), texture, 8) # position tuple, texture, sprite_index + entity1 = Entity(5.0, 5.0, texture, sprite_index=8) entity1.scale = 1.5 grid.entities.append(entity1) @@ -292,7 +291,7 @@ def demo_grid_animations(ui): entity_pos.start(entity1) # Create patrolling entity - entity2 = Entity((10.0, 2.0), texture, 12) # position tuple, texture, sprite_index + entity2 = Entity(10.0, 2.0, texture, sprite_index=12) 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 b9c0e2e..e12f9bc 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(2) + ui.remove(ui[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 bb2f7af..d24cc1a 100644 --- a/tests/demos/animation_sizzle_reel_working.py +++ b/tests/demos/animation_sizzle_reel_working.py @@ -268,6 +268,8 @@ 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 new file mode 100644 index 0000000..76d36cc --- /dev/null +++ b/tests/demos/exhaustive_api_demo.py @@ -0,0 +1,1204 @@ +#!/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 31b9f37..d4e082f 100644 --- a/tests/demos/pathfinding_showcase.py +++ b/tests/demos/pathfinding_showcase.py @@ -48,10 +48,6 @@ 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 @@ -130,34 +126,37 @@ 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), mcrfpy.default_texture, PLAYER) + player = mcrfpy.Entity(15, 11) + player.sprite_index = PLAYER grid.entities.append(player) # Spawn enemies in corners enemy_positions = [(4, 4), (24, 4), (4, 16), (24, 16)] for x, y in enemy_positions: - enemy = mcrfpy.Entity((x, y), mcrfpy.default_texture, ENEMY) + enemy = mcrfpy.Entity(x, y) + enemy.sprite_index = ENEMY grid.entities.append(enemy) enemies.append(enemy) # Spawn treasures treasure_positions = [(6, 5), (24, 5), (15, 10)] for x, y in treasure_positions: - treasure = mcrfpy.Entity((x, y), mcrfpy.default_texture, TREASURE) + treasure = mcrfpy.Entity(x, y) + treasure.sprite_index = TREASURE grid.entities.append(treasure) treasures.append(treasure) # Spawn patrol entities - patrol = mcrfpy.Entity((10, 10), mcrfpy.default_texture, PATROL) - # Store waypoints separately since Entity doesn't support custom attributes - entity_waypoints[patrol] = [(10, 10), (19, 10), (19, 16), (10, 16)] # Square patrol - entity_waypoint_indices[patrol] = 0 + 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 grid.entities.append(patrol) patrol_entities.append(patrol) @@ -223,21 +222,18 @@ def move_enemies(dt): def move_patrols(dt): """Move patrol entities along waypoints""" for patrol in patrol_entities: - if patrol not in entity_waypoints: + if not hasattr(patrol, 'waypoints'): continue # Get current waypoint - waypoints = entity_waypoints[patrol] - waypoint_index = entity_waypoint_indices[patrol] - target_x, target_y = waypoints[waypoint_index] + target_x, target_y = patrol.waypoints[patrol.waypoint_index] # Check if reached waypoint dist = abs(patrol.x - target_x) + abs(patrol.y - target_y) if dist < 0.5: # Move to next waypoint - entity_waypoint_indices[patrol] = (waypoint_index + 1) % len(waypoints) - waypoint_index = entity_waypoint_indices[patrol] - target_x, target_y = waypoints[waypoint_index] + patrol.waypoint_index = (patrol.waypoint_index + 1) % len(patrol.waypoints) + target_x, target_y = patrol.waypoints[patrol.waypoint_index] # Path to waypoint path = patrol.path_to(target_x, target_y) @@ -374,4 +370,4 @@ mcrfpy.setTimer("entities", update_entities, 16) # 60 FPS # Show scene mcrfpy.setScene("pathfinding_showcase") -print("\nShowcase ready! Move with WASD and watch entities react.") +print("\nShowcase ready! Move with WASD and watch entities react.") \ No newline at end of file diff --git a/tests/demos/simple_text_input.py b/tests/demos/simple_text_input.py index ad11509..e775670 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.fill_color = (255, 255, 255, 255) + self.label_caption.color = (255, 255, 255, 255) # Text display self.text_caption = mcrfpy.Caption("", self.x + 4, self.y + 4) - self.text_caption.fill_color = (0, 0, 0, 255) + self.text_caption.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.fill_color = (255, 255, 255, 255) + title.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.fill_color = (200, 200, 200, 255) + status.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 94ac610..8251498 100644 --- a/tests/demos/sizzle_reel_final.py +++ b/tests/demos/sizzle_reel_final.py @@ -5,19 +5,12 @@ McRogueFace Animation Sizzle Reel - Final Version Complete demonstration of all animation capabilities. This version works properly with the game loop and avoids API issues. - -WARNING: This demo causes a segmentation fault due to a bug in the -AnimationManager. When UI elements with active animations are removed -from the scene, the AnimationManager crashes when trying to update them. - -Use sizzle_reel_final_fixed.py instead, which works around this issue -by hiding objects off-screen instead of removing them. """ import mcrfpy # Configuration -DEMO_DURATION = 6.0 # Duration for each demo +DEMO_DURATION = 4.0 # Duration for each demo # All available easing functions EASING_FUNCTIONS = [ @@ -48,7 +41,6 @@ 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 @@ -87,21 +79,18 @@ 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) @@ -158,7 +147,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(len(ui)-1) + ui.remove(ui[len(ui)-1]) def next_demo(runtime): """Run the next demo""" @@ -178,13 +167,11 @@ def next_demo(runtime): current_demo += 1 if current_demo < len(demos): - #mcrfpy.setTimer("next", next_demo, int(DEMO_DURATION * 1000)) - pass + mcrfpy.setTimer("next", next_demo, int(DEMO_DURATION * 1000)) else: subtitle.text = "Demo Complete!" # Initialize print("Starting Animation Sizzle Reel...") create_scene() -mcrfpy.setTimer("start", next_demo, int(DEMO_DURATION * 1000)) -next_demo(0) +mcrfpy.setTimer("start", next_demo, 500) \ No newline at end of file diff --git a/tests/demos/sizzle_reel_final_fixed.py b/tests/demos/sizzle_reel_final_fixed.py deleted file mode 100644 index 0ecf99a..0000000 --- a/tests/demos/sizzle_reel_final_fixed.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/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 51538bb..5e5de6a 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") + title = mcrfpy.Caption(10, 10, "Text Input Widget Demo - Auto Test", font_size=24) title.color = (255, 255, 255, 255) scene.append(title) # Instructions - instructions = mcrfpy.Caption(10, 50, "This will automatically test the text input system") + instructions = mcrfpy.Caption(10, 50, "This will automatically test the text input system", font_size=14) 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...") + result_text = mcrfpy.Caption(50, 320, "Values will appear here as you type...", font_size=14) 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 2bcf7d8..fa6fe81 100644 --- a/tests/demos/text_input_standalone.py +++ b/tests/demos/text_input_standalone.py @@ -79,7 +79,8 @@ class TextInput: self.label_text = mcrfpy.Caption( self.x - 5, self.y - self.font_size - 5, - self.label + self.label, + font_size=self.font_size ) self.label_text.color = (255, 255, 255, 255) @@ -87,7 +88,8 @@ 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) @@ -258,12 +260,12 @@ def create_demo(): scene.append(bg) # Title - title = mcrfpy.Caption(10, 10, "Text Input Widget System") + title = mcrfpy.Caption(10, 10, "Text Input Widget System", font_size=24) 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") + info = mcrfpy.Caption(10, 50, "Click to focus | Tab to switch fields | Type to enter text", font_size=14) info.color = (200, 200, 200, 255) scene.append(info) @@ -287,7 +289,7 @@ def create_demo(): comment_input.add_to_scene(scene) # Status display - status = mcrfpy.Caption(50, 320, "Ready for input...") + status = mcrfpy.Caption(50, 320, "Ready for input...", font_size=14) 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 adbd201..0986a7c 100644 --- a/tests/demos/text_input_widget.py +++ b/tests/demos/text_input_widget.py @@ -95,7 +95,8 @@ class TextInput: self.label_text = mcrfpy.Caption( self.x - 5, self.y - self.font_size - 5, - self.label + self.label, + font_size=self.font_size ) self.label_text.color = (255, 255, 255, 255) @@ -103,7 +104,8 @@ 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) @@ -225,12 +227,12 @@ def create_demo(): scene.append(bg) # Title - title = mcrfpy.Caption(10, 10, "Text Input Widget Demo") + title = mcrfpy.Caption(10, 10, "Text Input Widget Demo", font_size=24) 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") + instructions = mcrfpy.Caption(10, 50, "Click to focus, Tab to switch fields, Type to enter text", font_size=14) instructions.color = (200, 200, 200, 255) scene.append(instructions) @@ -274,7 +276,7 @@ def create_demo(): fields.append(comment_input) # Result display - result_text = mcrfpy.Caption(50, 320, "Type in the fields above...") + result_text = mcrfpy.Caption(50, 320, "Type in the fields above...", font_size=14) result_text.color = (150, 255, 150, 255) scene.append(result_text)