Compare commits
12 Commits
master
...
alpha_pres
| Author | SHA1 | Date |
|---|---|---|
|
|
c5e7e8e298 | |
|
|
6d29652ae7 | |
|
|
a010e5fa96 | |
|
|
9c8d6c4591 | |
|
|
dcd1b0ca33 | |
|
|
6813fb5129 | |
|
|
6f67fbb51e | |
|
|
eb88c7b3aa | |
|
|
9fb428dd01 | |
|
|
bde82028b5 | |
|
|
062e4dadc4 | |
|
|
98fc49a978 |
|
|
@ -30,3 +30,4 @@ scripts/
|
|||
test_*
|
||||
|
||||
tcod_reference
|
||||
.archive
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
67
README.md
67
README.md
|
|
@ -3,19 +3,27 @@
|
|||
|
||||
A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML.
|
||||
|
||||
* Core roguelike logic from libtcod: field of view, pathfinding
|
||||
* Animate sprites with multiple frames. Smooth transitions for positions, sizes, zoom, and camera
|
||||
* Simple GUI element system allows keyboard and mouse input, composition
|
||||
* No compilation or installation necessary. The runtime is a full Python environment; "Zip And Ship"
|
||||
|
||||
![ Image ]()
|
||||
|
||||
**Pre-Alpha Release Demo**: my 7DRL 2025 entry *"Crypt of Sokoban"* - a prototype with buttons, boulders, enemies, and items.
|
||||
|
||||
## Tenets
|
||||
|
||||
- **Python & C++ Hand-in-Hand**: Create your game without ever recompiling. Your Python commands create C++ objects, and animations can occur without calling Python at all.
|
||||
- **Simple Yet Flexible UI System**: Sprites, Grids, Frames, and Captions with full animation support
|
||||
- **Entity-Component Architecture**: Implement your game objects with Python integration
|
||||
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod (demos still under construction)
|
||||
- **Automation API**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration
|
||||
- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Download**:
|
||||
|
||||
- The entire McRogueFace visual framework:
|
||||
- **Sprite**: an image file or one sprite from a shared sprite sheet
|
||||
- **Caption**: load a font, display text
|
||||
- **Frame**: A rectangle; put other things on it to move or manage GUIs as modules
|
||||
- **Grid**: A 2D array of tiles with zoom + position control
|
||||
- **Entity**: Lives on a Grid, displays a sprite, and can have a perspective or move along a path
|
||||
- **Animation**: Change any property on any of the above over time
|
||||
|
||||
```bash
|
||||
# Clone and build
|
||||
git clone <wherever you found this repo>
|
||||
|
|
@ -49,28 +57,59 @@ mcrfpy.setScene("intro")
|
|||
|
||||
## Documentation
|
||||
|
||||
### 📚 Full Documentation Site
|
||||
|
||||
For comprehensive documentation, tutorials, and API reference, visit:
|
||||
**[https://mcrogueface.github.io](https://mcrogueface.github.io)**
|
||||
|
||||
## Requirements
|
||||
The documentation site includes:
|
||||
|
||||
- **[Quickstart Guide](https://mcrogueface.github.io/quickstart/)** - Get running in 5 minutes
|
||||
- **[McRogueFace Does The Entire Roguelike Tutorial](https://mcrogueface.github.io/tutorials/)** - Step-by-step game building
|
||||
- **[Complete API Reference](https://mcrogueface.github.io/api/)** - Every function documented
|
||||
- **[Cookbook](https://mcrogueface.github.io/cookbook/)** - Ready-to-use code recipes
|
||||
- **[C++ Extension Guide](https://mcrogueface.github.io/extending-cpp/)** - For C++ developers: Add engine features
|
||||
|
||||
## Build Requirements
|
||||
|
||||
- C++17 compiler (GCC 7+ or Clang 5+)
|
||||
- CMake 3.14+
|
||||
- Python 3.12+
|
||||
- SFML 2.5+
|
||||
- SFML 2.6
|
||||
- Linux or Windows (macOS untested)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
McRogueFace/
|
||||
├── src/ # C++ engine source
|
||||
├── scripts/ # Python game scripts
|
||||
├── assets/ # Sprites, fonts, audio
|
||||
├── build/ # Build output directory
|
||||
├── build/ # Build output directory: zip + ship
|
||||
│ ├─ (*)assets/ # (copied location of assets)
|
||||
│ ├─ (*)scripts/ # (copied location of src/scripts)
|
||||
│ └─ lib/ # SFML, TCOD libraries, Python + standard library / modules
|
||||
├── deps/ # Python, SFML, and libtcod imports can be tossed in here to build
|
||||
│ └─ platform/ # windows, linux subdirectories for OS-specific cpython config
|
||||
├── docs/ # generated HTML, markdown docs
|
||||
│ └─ stubs/ # .pyi files for editor integration
|
||||
├── modules/ # git submodules, to build all of McRogueFace's dependencies from source
|
||||
├── src/ # C++ engine source
|
||||
│ └─ scripts/ # Python game scripts (copied during build)
|
||||
└── tests/ # Automated test suite
|
||||
└── tools/ # For the McRogueFace ecosystem: docs generation
|
||||
```
|
||||
|
||||
If you are building McRogueFace to implement game logic or scene configuration in C++, you'll have to compile the project.
|
||||
|
||||
If you are writing a game in Python using McRogueFace, you only need to rename and zip/distribute the `build` directory.
|
||||
|
||||
## Philosophy
|
||||
|
||||
- **C++ every frame, Python every tick**: All rendering data is handled in C++. Structure your UI and program animations in Python, and they are rendered without Python. All game logic can be written in Python.
|
||||
- **No Compiling Required; Zip And Ship**: Implement your game objects with Python, zip up McRogueFace with your "game.py" to ship
|
||||
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod
|
||||
- **Hands-Off Testing**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration
|
||||
- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter
|
||||
|
||||
## Contributing
|
||||
|
||||
PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,935 @@
|
|||
# McRogueFace - Development Roadmap
|
||||
|
||||
## 🚨 URGENT PRIORITIES - July 12, 2025 🚨
|
||||
|
||||
### CRITICAL: RoguelikeDev Tutorial Event starts July 15! (3 days)
|
||||
|
||||
#### 1. Tutorial Status & Blockers
|
||||
- [x] **Part 0**: Complete (Starting McRogueFace)
|
||||
- [x] **Part 1**: Complete (Setting up grid and tile sheet)
|
||||
- [ ] **Part 2**: Draft exists but BLOCKED by animation issues - PRIORITY FIX!
|
||||
- [ ] **Parts 3-6**: Machine-generated drafts need complete rework
|
||||
- [ ] **Parts 7-15**: Need creation this weekend
|
||||
|
||||
**Key Blockers**:
|
||||
- Need smooth character movement animation (Pokemon-style)
|
||||
- Grid needs walkable grass center, non-walkable tree edges
|
||||
- Input queueing during animations not working properly
|
||||
|
||||
#### 2. Animation System Critical Issues 🚨
|
||||
**BLOCKER FOR TUTORIAL PART 2**:
|
||||
- [ ] **Input Queue System**: Holding arrow keys doesn't queue movements
|
||||
- Animation must complete before next input accepted
|
||||
- Need "press and hold" that queues ONE additional move
|
||||
- Goal: Pokemon-style smooth continuous movement
|
||||
- [ ] **Collision Reservation**: When entity starts moving, should block destination
|
||||
- Prevents overlapping movements
|
||||
- Already claimed tiles should reject incoming entities
|
||||
- [x] **Segfault Fix**: Refactored from bare pointers to weak references ✅
|
||||
|
||||
#### 3. Grid Clicking BROKEN in Headless Mode 🚨
|
||||
**MAJOR DISCOVERY**: All click events commented out!
|
||||
- [ ] **#111** - Grid Click Events Broken in Headless: All click events commented out
|
||||
- [ ] **Grid Click Coordinates**: Need tile coords, not just mouse coords
|
||||
- [ ] **Nested Grid Support**: Clicks must work on grids within frames
|
||||
- [ ] **No Error Reporting**: System claimed complete but isn't
|
||||
|
||||
#### 4. Python API Consistency Crisis
|
||||
**Tutorial Writing Reveals Major Issues**:
|
||||
- [ ] **#101/#110** - Inconsistent Constructors: Each class has different requirements
|
||||
- [ ] **#109** - Vector Class Broken: No [0], [1] indexing like tuples
|
||||
- [ ] **#112** - Object Splitting Bug: Python derived classes lose type in collections
|
||||
- Shared pointer extracted, Python reference discarded
|
||||
- Retrieved objects are base class only
|
||||
- No way to cast back to derived type
|
||||
- [ ] **Need Systematic Generation**: All bindings should be consistent
|
||||
- [x] **UIGrid TCOD Integration** (8 hours) ✅ COMPLETED!
|
||||
- ✅ Add TCODMap* to UIGrid constructor with proper lifecycle
|
||||
- ✅ Implement complete Dijkstra pathfinding system
|
||||
- ✅ Create mcrfpy.libtcod submodule with Python bindings
|
||||
- ✅ Fix critical PyArg bug preventing Color object assignments
|
||||
- ✅ Implement FOV with perspective rendering
|
||||
- [ ] **#113** - Add batch operations for NumPy-style access (deferred)
|
||||
- [ ] **#114** - Create CellView for ergonomic .at((x,y)) access (deferred)
|
||||
- [x] **UIEntity Pathfinding** (4 hours) ✅ COMPLETED!
|
||||
- ✅ Implement Dijkstra maps for multiple targets in UIGrid
|
||||
- ✅ Add path_to(target) method using A* to UIEntity
|
||||
- ✅ Cache paths in UIEntity for performance
|
||||
|
||||
#### 3. Performance Critical Path
|
||||
- [ ] **#115** - Implement SpatialHash for 10,000+ entities (2 hours)
|
||||
- [ ] **#116** - Add dirty flag system to UIGrid (1 hour)
|
||||
- [ ] **#113** - Batch update context managers (2 hours)
|
||||
- [ ] **#117** - Memory pool for entities (2 hours)
|
||||
|
||||
#### 4. Bug Fixing Pipeline
|
||||
- [ ] **#125** - Set up GitHub Issues automation
|
||||
- [ ] Create test for each bug before fixing
|
||||
- [ ] Track: Memory leaks, Segfaults, Python/C++ boundary errors
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ PROPOSED ARCHITECTURE IMPROVEMENTS (From July 12 Analysis)
|
||||
|
||||
### Object-Oriented Design Overhaul
|
||||
1. **Scene System Revolution**:
|
||||
- [ ] **#118** - Make Scene derive from Drawable (scenes are drawn!)
|
||||
- [ ] Give scenes position and visibility properties
|
||||
- [ ] Scene selection by visibility (auto-hide old scene)
|
||||
- [ ] Replace transition system with animations
|
||||
|
||||
2. **Animation System Enhancements**:
|
||||
- [ ] **#119** - Add proper completion callbacks (object + animation params)
|
||||
- [ ] **#120** - Prevent property conflicts (exclusive locking)
|
||||
- [ ] Currently using timer sync workarounds
|
||||
|
||||
3. **Timer System Improvements**:
|
||||
- [ ] **#121** - Replace string-dictionary system with objects
|
||||
- [ ] Add start(), stop(), pause() methods
|
||||
- [ ] Implement proper one-shot mode
|
||||
- [ ] Pass timer object to callbacks (not just ms)
|
||||
|
||||
4. **Parent-Child UI Relationships**:
|
||||
- [ ] **#122** - Add parent field to UI drawables (like entities have)
|
||||
- [ ] Implement append/remove/extend with auto-parent updates
|
||||
- [ ] Auto-remove from old parent when adding to new
|
||||
|
||||
### Performance Optimizations Needed
|
||||
- [ ] **Grid Rendering**: Consider texture caching vs real-time
|
||||
- [ ] **#123** - Subgrid System: Split large grids into 256x256 chunks
|
||||
- [ ] **#116** - Dirty Flagging: Propagate from base class up
|
||||
- [ ] **#124** - Animation Features: Tile color animation, sprite cycling
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ CLAUDE CODE QUALITY CONCERNS (6-7 Weeks In)
|
||||
|
||||
### Issues Observed:
|
||||
1. **Declining Quality**: High quantity but low quality results
|
||||
2. **Not Following Requirements**: Ignoring specific implementation needs
|
||||
3. **Bad Practices**:
|
||||
- Creating parallel copies (animation_RAII.cpp, _fixed, _final versions)
|
||||
- Should use Git, not file copies
|
||||
- Claims functionality "done" when stubbed out
|
||||
4. **File Management Problems**:
|
||||
- Git operations reset timestamps
|
||||
- Can't determine creation order of multiple versions
|
||||
|
||||
### Recommendations:
|
||||
- Use Git for version control exclusively
|
||||
- Fix things in place, not copies
|
||||
- Acknowledge incomplete functionality
|
||||
- Follow project's implementation style
|
||||
|
||||
---
|
||||
|
||||
## 🎯 STRATEGIC ARCHITECTURE VISION
|
||||
|
||||
### Three-Layer Grid Architecture (From Compass Research)
|
||||
Following successful roguelike patterns (Caves of Qud, Cogmind, DCSS):
|
||||
|
||||
1. **Visual Layer** (UIGridPoint) - Sprites, colors, animations
|
||||
2. **World State Layer** (TCODMap) - Walkability, transparency, physics
|
||||
3. **Entity Perspective Layer** (UIGridPointState) - Per-entity FOV, knowledge
|
||||
|
||||
### Performance Architecture (Critical for 1000x1000 maps)
|
||||
- **Spatial Hashing** for entity queries (not quadtrees!)
|
||||
- **Batch Operations** with context managers (10-100x speedup)
|
||||
- **Memory Pooling** for entities and components
|
||||
- **Dirty Flag System** to avoid unnecessary updates
|
||||
- **Zero-Copy NumPy Integration** via buffer protocol
|
||||
|
||||
### Key Insight from Research
|
||||
"Minimizing Python/C++ boundary crossings matters more than individual function complexity"
|
||||
- Batch everything possible
|
||||
- Use context managers for logical operations
|
||||
- Expose arrays, not individual cells
|
||||
- Profile and optimize hot paths only
|
||||
|
||||
---
|
||||
|
||||
## Project Status: 🎉 ALPHA 0.1 RELEASE! 🎉
|
||||
|
||||
**Current State**: Documentation system complete, TCOD integration urgent
|
||||
**Latest Update**: Tutorial Parts 0-6 complete with documentation (2025-07-11)
|
||||
**Branch**: alpha_streamline_2
|
||||
**Open Issues**: ~46 remaining + URGENT TCOD/Tutorial work
|
||||
|
||||
---
|
||||
|
||||
## 📋 TCOD Integration Implementation Details
|
||||
|
||||
### Phase 1: Core UIGrid Integration (Day 1 Morning)
|
||||
```cpp
|
||||
// UIGrid.h additions
|
||||
class UIGrid : public UIDrawable {
|
||||
private:
|
||||
TCODMap* world_state; // Add TCOD map
|
||||
std::unordered_map<int, UIGridPointState*> entity_perspectives;
|
||||
bool batch_mode = false;
|
||||
std::vector<CellUpdate> pending_updates;
|
||||
```
|
||||
|
||||
### Phase 2: Python Bindings (Day 1 Afternoon)
|
||||
```python
|
||||
# New API surface
|
||||
grid = mcrfpy.Grid(100, 100)
|
||||
grid.compute_fov(player.x, player.y, radius=10) # Returns visible cells
|
||||
grid.at((x, y)).walkable = False # Ergonomic access
|
||||
with grid.batch_update(): # Context manager for performance
|
||||
# All updates batched
|
||||
```
|
||||
|
||||
### Phase 3: Entity Integration (Day 2 Morning)
|
||||
```python
|
||||
# UIEntity additions
|
||||
entity.path_to(target_x, target_y) # A* pathfinding
|
||||
entity.flee_from(threat) # Dijkstra map
|
||||
entity.can_see(other_entity) # FOV check
|
||||
```
|
||||
|
||||
### Critical Success Factors:
|
||||
1. **Batch everything** - Never update single cells in loops
|
||||
2. **Lazy evaluation** - Only compute FOV for entities that need it
|
||||
3. **Sparse storage** - Don't store full grids per entity
|
||||
4. **Profile early** - Find the 20% of code taking 80% of time
|
||||
|
||||
---
|
||||
|
||||
## Recent Achievements
|
||||
|
||||
### 2025-07-12: Animation System RAII Overhaul - Critical Segfault Fix! 🛡️
|
||||
**Fixed two major crashes in AnimationManager**
|
||||
- ✅ Race condition when creating animations in timer callbacks
|
||||
- ✅ Exit crash when animations outlive their targets
|
||||
- ✅ Implemented weak_ptr tracking for automatic cleanup
|
||||
- ✅ Added complete() and hasValidTarget() methods
|
||||
- ✅ No more use-after-free bugs - proper RAII design
|
||||
- ✅ Extensively tested with stress tests and production demos
|
||||
|
||||
### 2025-07-10: Complete FOV, A* Pathfinding & GUI Text Widgets! 👁️🗺️⌨️
|
||||
**Engine Feature Sprint - Major Capabilities Added**
|
||||
- ✅ Complete FOV (Field of View) system with perspective rendering
|
||||
- UIGrid.perspective property controls which entity's view to render
|
||||
- Three-layer overlay system: unexplored (black), explored (dark), visible (normal)
|
||||
- Per-entity visibility state tracking with UIGridPointState
|
||||
- Perfect knowledge updates - only explored areas persist
|
||||
- ✅ A* Pathfinding implementation
|
||||
- Entity.path_to(x, y) method for direct pathfinding
|
||||
- UIGrid compute_astar() and get_astar_path() methods
|
||||
- Path caching in entities for performance
|
||||
- Complete test suite comparing A* vs Dijkstra performance
|
||||
- ✅ GUI Text Input Widget System
|
||||
- Full-featured TextInputWidget class with cursor, selection, scrolling
|
||||
- Improved widget with proper text rendering and multi-line support
|
||||
- Example showcase demonstrating multiple input fields
|
||||
- Foundation for in-game consoles, chat systems, and text entry
|
||||
- ✅ Sizzle Reel Demos
|
||||
- path_vision_sizzle_reel.py combines pathfinding with FOV
|
||||
- Interactive visibility demos showing real-time FOV updates
|
||||
- Performance demonstrations with multiple entities
|
||||
|
||||
### 2025-07-09: Dijkstra Pathfinding & Critical Bug Fix! 🗺️
|
||||
**TCOD Integration Sprint - Major Progress**
|
||||
- ✅ Complete Dijkstra pathfinding implementation in UIGrid
|
||||
- compute_dijkstra(), get_dijkstra_distance(), get_dijkstra_path() methods
|
||||
- Full TCODMap and TCODDijkstra integration with proper memory management
|
||||
- Comprehensive test suite with both headless and interactive demos
|
||||
- ✅ **CRITICAL FIX**: PyArg bug in UIGridPoint color setter
|
||||
- Now supports both mcrfpy.Color objects and (r,g,b,a) tuples
|
||||
- Eliminated mysterious "SystemError: new style getargs format" crashes
|
||||
- Proper error handling and exception propagation
|
||||
- ✅ mcrfpy.libtcod submodule with Python bindings
|
||||
- dijkstra_compute(), dijkstra_get_distance(), dijkstra_get_path()
|
||||
- line() function for corridor generation
|
||||
- Foundation ready for FOV implementation
|
||||
- ✅ Test consolidation: 6 broken demos → 2 clean, working versions
|
||||
|
||||
### 2025-07-08: PyArgHelpers Infrastructure Complete! 🔧
|
||||
**Standardized Python API Argument Parsing**
|
||||
- Unified position handling: (x, y) tuples or separate x, y args
|
||||
- Consistent size parsing: (w, h) tuples or width, height args
|
||||
- Grid-specific helpers for tile-based positioning
|
||||
- Proper conflict detection between positional and keyword args
|
||||
- All UI components migrated: Frame, Caption, Sprite, Grid, Entity
|
||||
- Improved error messages: "Value must be a number (int or float)"
|
||||
- Foundation for Phase 7 documentation efforts
|
||||
|
||||
### 2025-07-05: ALPHA 0.1 ACHIEVED! 🎊🍾
|
||||
**All Alpha Blockers Resolved!**
|
||||
- Z-order rendering with performance optimization (Issue #63)
|
||||
- Python Sequence Protocol for collections (Issue #69)
|
||||
- Comprehensive Animation System (Issue #59)
|
||||
- Moved RenderTexture to Beta (not needed for Alpha)
|
||||
- **McRogueFace is ready for Alpha release!**
|
||||
|
||||
### 2025-07-05: Z-order Rendering Complete! 🎉
|
||||
**Issue #63 Resolved**: Consistent z-order rendering with performance optimization
|
||||
- Dirty flag pattern prevents unnecessary per-frame sorting
|
||||
- Lazy sorting for both Scene elements and Frame children
|
||||
- Frame children now respect z_index (fixed inconsistency)
|
||||
- Automatic dirty marking on z_index changes and collection modifications
|
||||
- Performance: O(1) check for static scenes vs O(n log n) every frame
|
||||
|
||||
### 2025-07-05: Python Sequence Protocol Complete! 🎉
|
||||
**Issue #69 Resolved**: Full sequence protocol implementation for collections
|
||||
- Complete __setitem__, __delitem__, __contains__ support
|
||||
- Slice operations with extended slice support (step != 1)
|
||||
- Concatenation (+) and in-place concatenation (+=) with validation
|
||||
- Negative indexing throughout, index() and count() methods
|
||||
- Type safety: UICollection (Frame/Caption/Sprite/Grid), EntityCollection (Entity only)
|
||||
- Default value support: None for texture/font parameters uses engine defaults
|
||||
|
||||
### 2025-07-05: Animation System Complete! 🎉
|
||||
**Issue #59 Resolved**: Comprehensive animation system with 30+ easing functions
|
||||
- Property-based animations for all UI classes (Frame, Caption, Sprite, Grid, Entity)
|
||||
- Individual color component animation (r/g/b/a)
|
||||
- Sprite sequence animation and text typewriter effects
|
||||
- Pure C++ execution without Python callbacks
|
||||
- Delta animation support for relative values
|
||||
|
||||
### 2025-01-03: Major Stability Update
|
||||
**Major Cleanup**: Removed deprecated registerPyAction system (-180 lines)
|
||||
**Bug Fixes**: 12 critical issues including Grid segfault, Issue #78 (middle click), Entity setters
|
||||
**New Features**: Entity.index() (#73), EntityCollection.extend() (#27), Sprite validation (#33)
|
||||
**Test Coverage**: Comprehensive test suite with timer callback pattern established
|
||||
|
||||
---
|
||||
|
||||
## 🔧 CURRENT WORK: Alpha Streamline 2 - Major Architecture Improvements
|
||||
|
||||
### Recent Completions:
|
||||
- ✅ **Phase 1-4 Complete** - Foundation, API Polish, Entity Lifecycle, Visibility/Performance
|
||||
- ✅ **Phase 5 Complete** - Window/Scene Architecture fully implemented!
|
||||
- Window singleton with properties (#34)
|
||||
- OOP Scene support with lifecycle methods (#61)
|
||||
- Window resize events (#1)
|
||||
- Scene transitions with animations (#105)
|
||||
- ✅ **Phase 6 Complete** - Rendering Revolution achieved!
|
||||
- Grid background colors (#50) ✅
|
||||
- RenderTexture overhaul (#6) ✅
|
||||
- UIFrame clipping support ✅
|
||||
- Viewport-based rendering (#8) ✅
|
||||
|
||||
### Active Development:
|
||||
- **Branch**: alpha_streamline_2
|
||||
- **Current Phase**: Phase 7 - Documentation & Distribution
|
||||
- **Achievement**: PyArgHelpers infrastructure complete - standardized Python API
|
||||
- **Strategic Vision**: See STRATEGIC_VISION.md for platform roadmap
|
||||
- **Latest**: All UI components now use consistent argument parsing patterns!
|
||||
|
||||
### 🏗️ Architectural Dependencies Map
|
||||
|
||||
```
|
||||
Foundation Layer:
|
||||
├── #71 Base Class (_Drawable)
|
||||
│ ├── #10 Visibility System (needs AABB from base)
|
||||
│ ├── #87 visible property
|
||||
│ └── #88 opacity property
|
||||
│
|
||||
├── #7 Safe Constructors (affects all classes)
|
||||
│ └── Blocks any new class creation until resolved
|
||||
│
|
||||
└── #30 Entity/Grid Integration (lifecycle management)
|
||||
└── Enables reliable entity management
|
||||
|
||||
Window/Scene Layer:
|
||||
├── #34 Window Object
|
||||
│ ├── #61 Scene Object (depends on Window)
|
||||
│ ├── #14 SFML Exposure (helps implement Window)
|
||||
│ └── Future: Multi-window support
|
||||
|
||||
Rendering Layer:
|
||||
└── #6 RenderTexture Overhaul
|
||||
├── Enables clipping
|
||||
├── Off-screen rendering
|
||||
└── Post-processing effects
|
||||
```
|
||||
|
||||
## 🚀 Alpha Streamline 2 - Comprehensive Phase Plan
|
||||
|
||||
### Phase 1: Foundation Stabilization (1-2 weeks)
|
||||
**Goal**: Safe, predictable base for all future work
|
||||
```
|
||||
1. #7 - Audit and fix unsafe constructors (CRITICAL - do first!)
|
||||
- Find all manually implemented no-arg constructors
|
||||
- Verify map compatibility requirements
|
||||
- Make pointer-safe or remove
|
||||
|
||||
2. #71 - _Drawable base class implementation
|
||||
- Common properties: x, y, w, h, visible, opacity
|
||||
- Virtual methods: get_bounds(), render()
|
||||
- Proper Python inheritance setup
|
||||
|
||||
3. #87 - visible property
|
||||
- Add to base class
|
||||
- Update all render methods to check
|
||||
|
||||
4. #88 - opacity property (depends on #87)
|
||||
- 0.0-1.0 float range
|
||||
- Apply in render methods
|
||||
|
||||
5. #89 - get_bounds() method
|
||||
- Virtual method returning (x, y, w, h)
|
||||
- Override in each UI class
|
||||
|
||||
6. #98 - move()/resize() convenience methods
|
||||
- move(dx, dy) - relative movement
|
||||
- resize(w, h) - absolute sizing
|
||||
```
|
||||
*Rationale*: Can't build on unsafe foundations. Base class enables all UI improvements.
|
||||
|
||||
### Phase 2: Constructor & API Polish (1 week)
|
||||
**Goal**: Pythonic, intuitive API
|
||||
```
|
||||
1. #101 - Standardize (0,0) defaults for all positions
|
||||
2. #38 - Frame children parameter: Frame(children=[...])
|
||||
3. #42 - Click handler in __init__: Button(click=callback)
|
||||
4. #90 - Grid size tuple: Grid(grid_size=(10, 10))
|
||||
5. #19 - Sprite texture swapping: sprite.texture = new_texture
|
||||
6. #52 - Grid skip out-of-bounds entities (performance)
|
||||
```
|
||||
*Rationale*: Quick wins that make the API more pleasant before bigger changes.
|
||||
|
||||
### Phase 3: Entity Lifecycle Management (1 week)
|
||||
**Goal**: Bulletproof entity/grid relationships
|
||||
```
|
||||
1. #30 - Entity.die() and grid association
|
||||
- Grid.entities.append(e) sets e.grid = self
|
||||
- Grid.entities.remove(e) sets e.grid = None
|
||||
- Entity.die() calls self.grid.remove(self)
|
||||
- Entity can only be in 0 or 1 grid
|
||||
|
||||
2. #93 - Vector arithmetic methods
|
||||
- add, subtract, multiply, divide
|
||||
- distance, normalize, dot product
|
||||
|
||||
3. #94 - Color helper methods
|
||||
- from_hex("#FF0000"), to_hex()
|
||||
- lerp(other_color, t) for interpolation
|
||||
|
||||
4. #103 - Timer objects
|
||||
timer = mcrfpy.Timer("my_timer", callback, 1000)
|
||||
timer.pause()
|
||||
timer.resume()
|
||||
timer.cancel()
|
||||
```
|
||||
*Rationale*: Games need reliable entity management. Timer objects enable entity AI.
|
||||
|
||||
### Phase 4: Visibility & Performance (1-2 weeks)
|
||||
**Goal**: Only render/process what's needed
|
||||
```
|
||||
1. #10 - [UNSCHEDULED] Full visibility system with AABB
|
||||
- Postponed: UIDrawables can exist in multiple collections
|
||||
- Cannot reliably determine screen position due to multiple render contexts
|
||||
- Needs architectural solution for parent-child relationships
|
||||
|
||||
2. #52 - Grid culling (COMPLETED in Phase 2)
|
||||
|
||||
3. #39/40/41 - Name system for finding elements
|
||||
- name="button1" property on all UIDrawables
|
||||
- only_one=True for unique names
|
||||
- scene.find("button1") returns element
|
||||
- collection.find("enemy*") returns list
|
||||
|
||||
4. #104 - Basic profiling/metrics
|
||||
- Frame time tracking
|
||||
- Draw call counting
|
||||
- Python vs C++ time split
|
||||
```
|
||||
*Rationale*: Performance is feature. Finding elements by name is huge QoL.
|
||||
|
||||
### Phase 5: Window/Scene Architecture ✅ COMPLETE! (2025-07-06)
|
||||
**Goal**: Modern, flexible architecture
|
||||
```
|
||||
1. ✅ #34 - Window object (singleton first)
|
||||
window = mcrfpy.Window.get()
|
||||
window.resolution = (1920, 1080)
|
||||
window.fullscreen = True
|
||||
window.vsync = True
|
||||
|
||||
2. ✅ #1 - Window resize events
|
||||
scene.on_resize(self, width, height) callback implemented
|
||||
|
||||
3. ✅ #61 - Scene object (OOP scenes)
|
||||
class MenuScene(mcrfpy.Scene):
|
||||
def on_keypress(self, key, state):
|
||||
# handle input
|
||||
def on_enter(self):
|
||||
# setup UI
|
||||
def on_exit(self):
|
||||
# cleanup
|
||||
def update(self, dt):
|
||||
# frame update
|
||||
|
||||
4. ✅ #14 - SFML exposure research
|
||||
- Completed comprehensive analysis
|
||||
- Recommendation: Direct integration as mcrfpy.sfml
|
||||
- SFML 3.0 migration deferred to late 2025
|
||||
|
||||
5. ✅ #105 - Scene transitions
|
||||
mcrfpy.setScene("menu", "fade", 1.0)
|
||||
# Supports: fade, slide_left, slide_right, slide_up, slide_down
|
||||
```
|
||||
*Result*: Entire window/scene system modernized with OOP design!
|
||||
|
||||
### Phase 6: Rendering Revolution (3-4 weeks) ✅ COMPLETE!
|
||||
**Goal**: Professional rendering capabilities
|
||||
```
|
||||
1. ✅ #50 - Grid background colors [COMPLETED]
|
||||
grid.background_color = mcrfpy.Color(50, 50, 50)
|
||||
- Added background_color property with animation support
|
||||
- Default dark gray background (8, 8, 8, 255)
|
||||
|
||||
2. ✅ #6 - RenderTexture overhaul [COMPLETED]
|
||||
✅ Base infrastructure in UIDrawable
|
||||
✅ UIFrame clip_children property
|
||||
✅ Dirty flag optimization system
|
||||
✅ Nested clipping support
|
||||
✅ UIGrid already has appropriate RenderTexture implementation
|
||||
❌ UICaption/UISprite clipping not needed (no children)
|
||||
|
||||
3. ✅ #8 - Viewport-based rendering [COMPLETED]
|
||||
- Fixed game resolution (window.game_resolution)
|
||||
- Three scaling modes: "center", "stretch", "fit"
|
||||
- Window to game coordinate transformation
|
||||
- Mouse input properly scaled with windowToGameCoords()
|
||||
- Python API fully integrated
|
||||
- Tests: test_viewport_simple.py, test_viewport_visual.py, test_viewport_scaling.py
|
||||
|
||||
4. #106 - Shader support [DEFERRED TO POST-PHASE 7]
|
||||
sprite.shader = mcrfpy.Shader.load("glow.frag")
|
||||
frame.shader_params = {"intensity": 0.5}
|
||||
|
||||
5. #107 - Particle system [DEFERRED TO POST-PHASE 7]
|
||||
emitter = mcrfpy.ParticleEmitter()
|
||||
emitter.texture = spark_texture
|
||||
emitter.emission_rate = 100
|
||||
emitter.lifetime = (0.5, 2.0)
|
||||
```
|
||||
|
||||
**Phase 6 Achievement Summary**:
|
||||
- Grid backgrounds (#50) ✅ - Customizable background colors with animation
|
||||
- RenderTexture overhaul (#6) ✅ - UIFrame clipping with opt-in architecture
|
||||
- Viewport rendering (#8) ✅ - Three scaling modes with coordinate transformation
|
||||
- UIGrid already had optimal RenderTexture implementation for its use case
|
||||
- UICaption/UISprite clipping unnecessary (no children to clip)
|
||||
- Performance optimized with dirty flag system
|
||||
- Backward compatibility preserved throughout
|
||||
- Effects/Shader/Particle systems deferred for focused delivery
|
||||
|
||||
*Rationale*: This unlocks professional visual effects but is complex.
|
||||
|
||||
### Phase 7: Documentation & Distribution (1-2 weeks)
|
||||
**Goal**: Ready for the world
|
||||
```
|
||||
1. ✅ #85 - Replace all "docstring" placeholders [COMPLETED 2025-07-08]
|
||||
2. ✅ #86 - Add parameter documentation [COMPLETED 2025-07-08]
|
||||
3. ✅ #108 - Generate .pyi type stubs for IDE support [COMPLETED 2025-07-08]
|
||||
4. ❌ #70 - PyPI wheel preparation [CANCELLED - Architectural mismatch]
|
||||
5. API reference generator tool
|
||||
```
|
||||
|
||||
## 📋 Critical Path & Parallel Tracks
|
||||
|
||||
### 🔴 **Critical Path** (Must do in order)
|
||||
**Safe Constructors (#7)** → **Base Class (#71)** → **Visibility (#10)** → **Window (#34)** → **Scene (#61)**
|
||||
|
||||
### 🟡 **Parallel Tracks** (Can be done alongside critical path)
|
||||
|
||||
**Track A: Entity Systems**
|
||||
- Entity/Grid integration (#30)
|
||||
- Timer objects (#103)
|
||||
- Vector/Color helpers (#93, #94)
|
||||
|
||||
**Track B: API Polish**
|
||||
- Constructor improvements (#101, #38, #42, #90)
|
||||
- Sprite texture swap (#19)
|
||||
- Name/search system (#39/40/41)
|
||||
|
||||
**Track C: Performance**
|
||||
- Grid culling (#52)
|
||||
- Visibility culling (part of #10)
|
||||
- Profiling tools (#104)
|
||||
|
||||
### 💎 **Quick Wins to Sprinkle Throughout**
|
||||
1. Color helpers (#94) - 1 hour
|
||||
2. Vector methods (#93) - 1 hour
|
||||
3. Grid backgrounds (#50) - 30 minutes
|
||||
4. Default positions (#101) - 30 minutes
|
||||
|
||||
### 🎯 **Recommended Execution Order**
|
||||
|
||||
**Week 1-2**: Foundation (Critical constructors + base class)
|
||||
**Week 3**: Entity lifecycle + API polish
|
||||
**Week 4**: Visibility system + performance
|
||||
**Week 5-6**: Window/Scene architecture
|
||||
**Week 7-9**: Rendering revolution (or defer to gamma)
|
||||
**Week 10**: Documentation + release prep
|
||||
|
||||
### 🆕 **New Issues to Create/Track**
|
||||
|
||||
1. [x] **Timer Objects** - Pythonic timer management (#103) - *Completed Phase 3*
|
||||
2. [ ] **Event System Enhancement** - Mouse enter/leave, drag, right-click
|
||||
3. [ ] **Resource Manager** - Centralized asset loading
|
||||
4. [ ] **Serialization System** - Save/load game state
|
||||
5. [x] **Scene Transitions** - Fade, slide, custom effects (#105) - *Completed Phase 5*
|
||||
6. [x] **Profiling Tools** - Performance metrics (#104) - *Completed Phase 4*
|
||||
7. [ ] **Particle System** - Visual effects framework (#107)
|
||||
8. [ ] **Shader Support** - Custom rendering effects (#106)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Phase 6 Implementation Strategy
|
||||
|
||||
### RenderTexture Overhaul (#6) - Technical Approach
|
||||
|
||||
**Current State**:
|
||||
- UIGrid already uses RenderTexture for entity rendering
|
||||
- Scene transitions use RenderTextures for smooth animations
|
||||
- Direct rendering to window for Frame, Caption, Sprite
|
||||
|
||||
**Implementation Plan**:
|
||||
1. **Base Infrastructure**:
|
||||
- Add `sf::RenderTexture* target` to UIDrawable base
|
||||
- Modify `render()` to check if target exists
|
||||
- If target: render to texture, then draw texture to parent
|
||||
- If no target: render directly (backward compatible)
|
||||
|
||||
2. **Clipping Support**:
|
||||
- Frame enforces bounds on children via RenderTexture
|
||||
- Children outside bounds are automatically clipped
|
||||
- Nested frames create render texture hierarchy
|
||||
|
||||
3. **Performance Optimization**:
|
||||
- Lazy RenderTexture creation (only when needed)
|
||||
- Dirty flag system (only re-render when changed)
|
||||
- Texture pooling for commonly used sizes
|
||||
|
||||
4. **Integration Points**:
|
||||
- Scene transitions already working with RenderTextures
|
||||
- UIGrid can be reference implementation
|
||||
- Test with deeply nested UI structures
|
||||
|
||||
**Quick Wins Before Core Work**:
|
||||
1. **Grid Background (#50)** - 30 min implementation
|
||||
- Add `background_color` and `background_texture` properties
|
||||
- Render before entities in UIGrid::render()
|
||||
- Good warm-up before tackling RenderTexture
|
||||
|
||||
2. **Research Tasks**:
|
||||
- Study UIGrid's current RenderTexture usage
|
||||
- Profile scene transition performance
|
||||
- Identify potential texture size limits
|
||||
|
||||
---
|
||||
|
||||
## 🚀 NEXT PHASE: Beta Features & Polish
|
||||
|
||||
### Alpha Complete! Moving to Beta Priorities:
|
||||
1. ~~**#69** - Python Sequence Protocol for collections~~ - *Completed! (2025-07-05)*
|
||||
2. ~~**#63** - Z-order rendering for UIDrawables~~ - *Completed! (2025-07-05)*
|
||||
3. ~~**#59** - Animation system~~ - *Completed! (2025-07-05)*
|
||||
4. **#6** - RenderTexture concept - *Extensive Overhaul*
|
||||
5. ~~**#47** - New README.md for Alpha release~~ - *Completed*
|
||||
- [x] **#78** - Middle Mouse Click sends "C" keyboard event - *Fixed*
|
||||
- [x] **#77** - Fix error message copy/paste bug - *Fixed*
|
||||
- [x] **#74** - Add missing `Grid.grid_y` property - *Fixed*
|
||||
- [ ] **#37** - Fix Windows build module import from "scripts" directory - *Isolated Fix*
|
||||
Issue #37 is **on hold** until we have a Windows build environment available. I actually suspect this is already fixed by the updates to the makefile, anyway.
|
||||
- [x] **Entity Property Setters** - Fix "new style getargs format" error - *Fixed*
|
||||
- [x] **Sprite Texture Setter** - Fix "error return without exception set" - *Fixed*
|
||||
- [x] **keypressScene() Validation** - Add proper error handling - *Fixed*
|
||||
|
||||
### 🔄 Complete Iterator System
|
||||
**Status**: Core iterators complete (#72 closed), Grid point iterators still pending
|
||||
|
||||
- [ ] **Grid Point Iterator Implementation** - Complete the remaining grid iteration work
|
||||
- [x] **#73** - Add `entity.index()` method for collection removal - *Fixed*
|
||||
- [x] **#69** ⚠️ **Alpha Blocker** - Refactor all collections to use Python Sequence Protocol - *Completed! (2025-07-05)*
|
||||
|
||||
**Dependencies**: Grid point iterators → #73 entity.index() → #69 Sequence Protocol overhaul
|
||||
|
||||
---
|
||||
|
||||
## 🗂 ISSUE TRIAGE BY SYSTEM (78 Total Issues)
|
||||
|
||||
### 🎮 Core Engine Systems
|
||||
|
||||
#### Iterator/Collection System (2 issues)
|
||||
- [x] **#73** - Entity index() method for removal - *Fixed*
|
||||
- [x] **#69** ⚠️ **Alpha Blocker** - Sequence Protocol refactor - *Completed! (2025-07-05)*
|
||||
|
||||
#### Python/C++ Integration (7 issues)
|
||||
- [x] **#76** - UIEntity derived type preservation in collections - *Multiple Integrations*
|
||||
- [ ] **#71** - Drawable base class hierarchy - *Extensive Overhaul*
|
||||
- [ ] **#70** - PyPI wheel distribution - *Extensive Overhaul*
|
||||
- [~] **#32** - Executable behave like `python` command - *Extensive Overhaul* *(90% Complete: -h, -V, -c, -m, -i, script execution, sys.argv, --exec all implemented. Only stdin (-) support missing)*
|
||||
- [ ] **#35** - TCOD as built-in module - *Extensive Overhaul*
|
||||
- [~] **#14** - Expose SFML as built-in module - *Research Complete, Implementation Pending*
|
||||
- [ ] **#46** - Subinterpreter threading tests - *Multiple Integrations*
|
||||
|
||||
#### UI/Rendering System (12 issues)
|
||||
- [x] **#63** ⚠️ **Alpha Blocker** - Z-order for UIDrawables - *Multiple Integrations*
|
||||
- [x] **#59** ⚠️ **Alpha Blocker** - Animation system - *Completed! (2025-07-05)*
|
||||
- [ ] **#6** ⚠️ **Alpha Blocker** - RenderTexture for all UIDrawables - *Extensive Overhaul*
|
||||
- [ ] **#10** - UIDrawable visibility/AABB system - *Extensive Overhaul*
|
||||
- [ ] **#8** - UIGrid RenderTexture viewport sizing - *Multiple Integrations*
|
||||
- [x] **#9** - UIGrid RenderTexture resize handling - *Multiple Integrations*
|
||||
- [ ] **#52** - UIGrid skip out-of-bounds entities - *Isolated Fix*
|
||||
- [ ] **#50** - UIGrid background color field - *Isolated Fix*
|
||||
- [ ] **#19** - Sprite get/set texture methods - *Multiple Integrations*
|
||||
- [ ] **#17** - Move UISprite position into sf::Sprite - *Isolated Fix*
|
||||
- [x] **#33** - Sprite index validation against texture range - *Fixed*
|
||||
|
||||
#### Grid/Entity System (6 issues)
|
||||
- [ ] **#30** - Entity/Grid association management (.die() method) - *Extensive Overhaul*
|
||||
- [ ] **#16** - Grid strict mode for entity knowledge/visibility - *Extensive Overhaul*
|
||||
- [ ] **#67** - Grid stitching for infinite worlds - *Extensive Overhaul*
|
||||
- [ ] **#15** - UIGridPointState cleanup and standardization - *Multiple Integrations*
|
||||
- [ ] **#20** - UIGrid get_grid_size standardization - *Multiple Integrations*
|
||||
- [x] **#12** - GridPoint/GridPointState forbid direct init - *Isolated Fix*
|
||||
|
||||
#### Scene/Window Management (5 issues)
|
||||
- [x] **#61** - Scene object encapsulating key callbacks - *Completed Phase 5*
|
||||
- [x] **#34** - Window object for resolution/scaling - *Completed Phase 5*
|
||||
- [ ] **#62** - Multiple windows support - *Extensive Overhaul*
|
||||
- [ ] **#49** - Window resolution & viewport controls - *Multiple Integrations*
|
||||
- [x] **#1** - Scene resize event handling - *Completed Phase 5*
|
||||
|
||||
### 🔧 Quality of Life Features
|
||||
|
||||
#### UI Enhancement Features (8 issues)
|
||||
- [ ] **#39** - Name field on UIDrawables - *Multiple Integrations*
|
||||
- [ ] **#40** - `only_one` arg for unique naming - *Multiple Integrations*
|
||||
- [ ] **#41** - `.find(name)` method for collections - *Multiple Integrations*
|
||||
- [ ] **#38** - `children` arg for Frame initialization - *Isolated Fix*
|
||||
- [ ] **#42** - Click callback arg for UIDrawable init - *Isolated Fix*
|
||||
- [x] **#27** - UIEntityCollection.extend() method - *Fixed*
|
||||
- [ ] **#28** - UICollectionIter for scene ui iteration - *Isolated Fix*
|
||||
- [ ] **#26** - UIEntityCollectionIter implementation - *Isolated Fix*
|
||||
|
||||
### 🧹 Refactoring & Cleanup
|
||||
|
||||
#### Code Cleanup (7 issues)
|
||||
- [x] **#3** ⚠️ **Alpha Blocker** - Remove `McRFPy_API::player_input` - *Completed*
|
||||
- [x] **#2** ⚠️ **Alpha Blocker** - Review `registerPyAction` necessity - *Completed*
|
||||
- [ ] **#7** - Remove unsafe no-argument constructors - *Multiple Integrations*
|
||||
- [ ] **#21** - PyUIGrid dealloc cleanup - *Isolated Fix*
|
||||
- [ ] **#75** - REPL thread separation from SFML window - *Multiple Integrations*
|
||||
|
||||
### 📚 Demo & Documentation
|
||||
|
||||
#### Documentation (2 issues)
|
||||
- [x] **#47** ⚠️ **Alpha Blocker** - Alpha release README.md - *Isolated Fix*
|
||||
- [ ] **#48** - Dependency compilation documentation - *Isolated Fix*
|
||||
|
||||
#### Demo Projects (6 issues)
|
||||
- [ ] **#54** - Jupyter notebook integration demo - *Multiple Integrations*
|
||||
- [ ] **#55** - Hunt the Wumpus AI demo - *Multiple Integrations*
|
||||
- [ ] **#53** - Web interface input demo - *Multiple Integrations* *(New automation API could help)*
|
||||
- [ ] **#45** - Accessibility mode demos - *Multiple Integrations* *(New automation API could help test)*
|
||||
- [ ] **#36** - Dear ImGui integration tests - *Extensive Overhaul*
|
||||
- [ ] **#65** - Python Explorer scene (replaces uitest) - *Extensive Overhaul*
|
||||
|
||||
---
|
||||
|
||||
## 🎮 STRATEGIC DIRECTION
|
||||
|
||||
### Engine Philosophy Maintained
|
||||
- **C++ First**: Performance-critical code stays in C++
|
||||
- **Python Close Behind**: Rich scripting without frame-rate impact
|
||||
- **Game-Ready**: Each improvement should benefit actual game development
|
||||
|
||||
### Architecture Goals
|
||||
1. **Clean Inheritance**: Drawable → UI components, proper type preservation
|
||||
2. **Collection Consistency**: Uniform iteration, indexing, and search patterns
|
||||
3. **Resource Management**: RAII everywhere, proper lifecycle handling
|
||||
4. **Multi-Platform**: Windows/Linux feature parity maintained
|
||||
|
||||
---
|
||||
|
||||
## 📚 REFERENCES & CONTEXT
|
||||
|
||||
**Issue Dependencies** (Key Chains):
|
||||
- Iterator System: Grid points → #73 → #69 (Alpha Blocker)
|
||||
- UI Hierarchy: #71 → #63 (Alpha Blocker)
|
||||
- Rendering: #6 (Alpha Blocker) → #8, #9 → #10
|
||||
- Entity System: #30 → #16 → #67
|
||||
- Window Management: #34 → #49, #61 → #62
|
||||
|
||||
**Commit References**:
|
||||
- 167636c: Iterator improvements (UICollection/UIEntityCollection complete)
|
||||
- Recent work: 7DRL 2025 completion, RPATH updates, console improvements
|
||||
|
||||
**Architecture Files**:
|
||||
- Iterator patterns: src/UICollection.cpp, src/UIGrid.cpp
|
||||
- Python integration: src/McRFPy_API.cpp, src/PyObjectUtils.h
|
||||
- Game implementation: src/scripts/ (Crypt of Sokoban complete game)
|
||||
|
||||
---
|
||||
|
||||
## 🔮 FUTURE VISION: Pure Python Extension Architecture
|
||||
|
||||
### Concept: McRogueFace as a Traditional Python Package
|
||||
**Status**: Unscheduled - Long-term vision
|
||||
**Complexity**: Major architectural overhaul
|
||||
|
||||
Instead of being a C++ application that embeds Python, McRogueFace could be redesigned as a pure Python extension module that can be installed via `pip install mcrogueface`.
|
||||
|
||||
### Technical Approach
|
||||
1. **Separate Core Engine from Python Embedding**
|
||||
- Extract SFML rendering, audio, and input into C++ extension modules
|
||||
- Remove embedded CPython interpreter
|
||||
- Use Python's C API to expose functionality
|
||||
|
||||
2. **Module Structure**
|
||||
```
|
||||
mcrfpy/
|
||||
├── __init__.py # Pure Python coordinator
|
||||
├── _core.so # C++ rendering/game loop extension
|
||||
├── _sfml.so # SFML bindings
|
||||
├── _audio.so # Audio system bindings
|
||||
└── engine.py # Python game engine logic
|
||||
```
|
||||
|
||||
3. **Inverted Control Flow**
|
||||
- Python drives the main loop instead of C++
|
||||
- C++ extensions handle performance-critical operations
|
||||
- Python manages game logic, scenes, and entity systems
|
||||
|
||||
### Benefits
|
||||
- **Standard Python Packaging**: `pip install mcrogueface`
|
||||
- **Virtual Environment Support**: Works with venv, conda, poetry
|
||||
- **Better IDE Integration**: Standard Python development workflow
|
||||
- **Easier Testing**: Use pytest, standard Python testing tools
|
||||
- **Cross-Python Compatibility**: Support multiple Python versions
|
||||
- **Modular Architecture**: Users can import only what they need
|
||||
|
||||
### Challenges
|
||||
- **Major Refactoring**: Complete restructure of codebase
|
||||
- **Performance Considerations**: Python-driven main loop overhead
|
||||
- **Build Complexity**: Multiple extension modules to compile
|
||||
- **Platform Support**: Need wheels for many platform/Python combinations
|
||||
- **API Stability**: Would need careful design to maintain compatibility
|
||||
|
||||
### Implementation Phases (If Pursued)
|
||||
1. **Proof of Concept**: Simple SFML binding as Python extension
|
||||
2. **Core Extraction**: Separate rendering from Python embedding
|
||||
3. **Module Design**: Define clean API boundaries
|
||||
4. **Incremental Migration**: Move systems one at a time
|
||||
5. **Compatibility Layer**: Support existing games during transition
|
||||
|
||||
### Example Usage (Future Vision)
|
||||
```python
|
||||
import mcrfpy
|
||||
from mcrfpy import Scene, Frame, Sprite, Grid
|
||||
|
||||
# Create game directly in Python
|
||||
game = mcrfpy.Game(width=1024, height=768)
|
||||
|
||||
# Define scenes using Python classes
|
||||
class MainMenu(Scene):
|
||||
def on_enter(self):
|
||||
self.ui.append(Frame(100, 100, 200, 50))
|
||||
self.ui.append(Sprite("logo.png", x=400, y=100))
|
||||
|
||||
def on_keypress(self, key, pressed):
|
||||
if key == "ENTER" and pressed:
|
||||
self.game.set_scene("game")
|
||||
|
||||
# Run the game
|
||||
game.add_scene("menu", MainMenu())
|
||||
game.run()
|
||||
```
|
||||
|
||||
This architecture would make McRogueFace a first-class Python citizen, following standard Python packaging conventions while maintaining high performance through C++ extensions.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 IMMEDIATE NEXT STEPS (Priority Order)
|
||||
|
||||
### TODAY (July 12) - CRITICAL PATH:
|
||||
1. **FIX ANIMATION BLOCKERS** for Tutorial Part 2:
|
||||
- Implement input queueing during animations
|
||||
- Add destination square reservation
|
||||
- Test Pokemon-style continuous movement
|
||||
2. **FIX GRID CLICKING** (discovered broken in headless):
|
||||
- Uncomment and implement click events
|
||||
- Add tile coordinate conversion
|
||||
- Enable nested grid support
|
||||
3. **CREATE TUTORIAL ANNOUNCEMENT** if blockers fixed
|
||||
|
||||
### Weekend (July 13-14) - Tutorial Sprint:
|
||||
1. **Regenerate Parts 3-6** (machine drafts are broken)
|
||||
2. **Create Parts 7-10**: Interface, Items, Targeting, Save/Load
|
||||
3. **Create Parts 11-15**: Dungeon levels, difficulty, equipment
|
||||
4. **Post more frequently during event** (narrator emphasis)
|
||||
|
||||
### Architecture Decision Log:
|
||||
- **DECIDED**: Use three-layer architecture (visual/world/perspective)
|
||||
- **DECIDED**: Spatial hashing over quadtrees for entities
|
||||
- **DECIDED**: Batch operations are mandatory, not optional
|
||||
- **DECIDED**: TCOD integration as mcrfpy.libtcod submodule
|
||||
- **DECIDED**: Tutorial must showcase McRogueFace strengths, not mimic TCOD
|
||||
|
||||
### Risk Mitigation:
|
||||
- **If TCOD integration delays**: Use pure Python FOV for tutorial
|
||||
- **If performance issues**: Focus on <100x100 maps for demos
|
||||
- **If tutorial incomplete**: Ship with 4 solid parts + roadmap
|
||||
- **If bugs block progress**: Document as "known issues" and continue
|
||||
|
||||
---
|
||||
|
||||
## 📋 COMPREHENSIVE ISSUES FROM TRANSCRIPT ANALYSIS
|
||||
|
||||
### Animation System (6 issues)
|
||||
1. **Input Queue During Animation**: Queue one additional move during animation
|
||||
2. **Destination Square Reservation**: Block target when movement begins
|
||||
3. **Pokemon-Style Movement**: Smooth continuous movement with input handling
|
||||
4. **#119** - Animation Callbacks: Add completion callbacks with parameters
|
||||
5. **#120** - Property Conflict Prevention: Prevent multiple animations on same property
|
||||
6. **Remove Bare Pointers**: Complete refactoring to weak references ✅
|
||||
|
||||
### Grid System (6 issues)
|
||||
7. **#111** - Grid Click Implementation: Fix commented-out events in headless
|
||||
8. **Tile Coordinate Conversion**: Convert mouse to tile coordinates
|
||||
9. **Nested Grid Support**: Enable clicking on grids within grids
|
||||
10. **#123** - Grid Rendering Performance: Implement 256x256 subgrid system
|
||||
11. **#116** - Dirty Flagging: Add dirty flag propagation from base
|
||||
12. **#124** - Grid Point Animation: Enable animating individual tiles
|
||||
|
||||
### Python API (6 issues)
|
||||
13. **Regenerate Python Bindings**: Create consistent interface generation
|
||||
14. **#109** - Vector Class Enhancement: Add [0], [1] indexing to vectors
|
||||
15. **#112** - Fix Object Splitting: Preserve Python derived class types
|
||||
16. **#101/#110** - Standardize Constructors: Make all constructors consistent
|
||||
17. **Color Class Bindings**: Properly expose SFML Color class
|
||||
18. **Font Class Bindings**: Properly expose SFML Font class
|
||||
|
||||
### Architecture (8 issues)
|
||||
19. **#118** - Scene as Drawable: Refactor Scene to inherit from Drawable
|
||||
20. **Scene Visibility System**: Implement exclusive visibility switching
|
||||
21. **Replace Transition System**: Use animations not special transitions
|
||||
22. **#122** - Parent-Child UI: Add parent field to UI drawables
|
||||
23. **Collection Methods**: Implement append/remove/extend with parent updates
|
||||
24. **#121** - Timer Object System: Replace string-dictionary timers
|
||||
25. **One-Shot Timer Mode**: Implement proper one-shot functionality
|
||||
26. **Button Mechanics**: Any entity type can trigger buttons
|
||||
|
||||
### Entity System (4 issues)
|
||||
27. **Step-On Entities**: Implement trigger when stepped on
|
||||
28. **Bump Interaction**: Add bump-to-interact behavior
|
||||
29. **Type-Aware Interactions**: Entity interactions based on type
|
||||
30. **Button Mechanics**: Any entity can trigger buttons
|
||||
|
||||
### Tutorial & Documentation (4 issues)
|
||||
31. **Fix Part 2 Tutorial**: Unblock with animation fixes
|
||||
32. **Regenerate Parts 3-6**: Replace machine-generated content
|
||||
33. **API Documentation**: Document ergonomic improvements
|
||||
34. **Tutorial Alignment**: Ensure parts match TCOD structure
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2025-07-12 (CRITICAL TUTORIAL SPRINT)*
|
||||
*Next Review: July 15 after event start*
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
@echo off
|
||||
REM Windows build script using cmake --build (generator-agnostic)
|
||||
REM This version works with any CMake generator
|
||||
|
||||
echo Building McRogueFace for Windows using CMake...
|
||||
|
||||
REM Set build directory
|
||||
set BUILD_DIR=build_win
|
||||
set CONFIG=Release
|
||||
|
||||
REM Clean previous build
|
||||
if exist %BUILD_DIR% rmdir /s /q %BUILD_DIR%
|
||||
mkdir %BUILD_DIR%
|
||||
cd %BUILD_DIR%
|
||||
|
||||
REM Configure with CMake
|
||||
REM You can change the generator here if needed:
|
||||
REM -G "Visual Studio 17 2022" (VS 2022)
|
||||
REM -G "Visual Studio 16 2019" (VS 2019)
|
||||
REM -G "MinGW Makefiles" (MinGW)
|
||||
REM -G "Ninja" (Ninja build system)
|
||||
cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=%CONFIG% ..
|
||||
if errorlevel 1 (
|
||||
echo CMake configuration failed!
|
||||
cd ..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build using cmake (works with any generator)
|
||||
cmake --build . --config %CONFIG% --parallel
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
cd ..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Build completed successfully!
|
||||
echo Executable: %BUILD_DIR%\%CONFIG%\mcrogueface.exe
|
||||
echo.
|
||||
|
||||
cd ..
|
||||
157
css_colors.txt
157
css_colors.txt
|
|
@ -1,157 +0,0 @@
|
|||
aqua #00FFFF
|
||||
black #000000
|
||||
blue #0000FF
|
||||
fuchsia #FF00FF
|
||||
gray #808080
|
||||
green #008000
|
||||
lime #00FF00
|
||||
maroon #800000
|
||||
navy #000080
|
||||
olive #808000
|
||||
purple #800080
|
||||
red #FF0000
|
||||
silver #C0C0C0
|
||||
teal #008080
|
||||
white #FFFFFF
|
||||
yellow #FFFF00
|
||||
aliceblue #F0F8FF
|
||||
antiquewhite #FAEBD7
|
||||
aqua #00FFFF
|
||||
aquamarine #7FFFD4
|
||||
azure #F0FFFF
|
||||
beige #F5F5DC
|
||||
bisque #FFE4C4
|
||||
black #000000
|
||||
blanchedalmond #FFEBCD
|
||||
blue #0000FF
|
||||
blueviolet #8A2BE2
|
||||
brown #A52A2A
|
||||
burlywood #DEB887
|
||||
cadetblue #5F9EA0
|
||||
chartreuse #7FFF00
|
||||
chocolate #D2691E
|
||||
coral #FF7F50
|
||||
cornflowerblue #6495ED
|
||||
cornsilk #FFF8DC
|
||||
crimson #DC143C
|
||||
cyan #00FFFF
|
||||
darkblue #00008B
|
||||
darkcyan #008B8B
|
||||
darkgoldenrod #B8860B
|
||||
darkgray #A9A9A9
|
||||
darkgreen #006400
|
||||
darkkhaki #BDB76B
|
||||
darkmagenta #8B008B
|
||||
darkolivegreen #556B2F
|
||||
darkorange #FF8C00
|
||||
darkorchid #9932CC
|
||||
darkred #8B0000
|
||||
darksalmon #E9967A
|
||||
darkseagreen #8FBC8F
|
||||
darkslateblue #483D8B
|
||||
darkslategray #2F4F4F
|
||||
darkturquoise #00CED1
|
||||
darkviolet #9400D3
|
||||
deeppink #FF1493
|
||||
deepskyblue #00BFFF
|
||||
dimgray #696969
|
||||
dodgerblue #1E90FF
|
||||
firebrick #B22222
|
||||
floralwhite #FFFAF0
|
||||
forestgreen #228B22
|
||||
fuchsia #FF00FF
|
||||
gainsboro #DCDCDC
|
||||
ghostwhite #F8F8FF
|
||||
gold #FFD700
|
||||
goldenrod #DAA520
|
||||
gray #7F7F7F
|
||||
green #008000
|
||||
greenyellow #ADFF2F
|
||||
honeydew #F0FFF0
|
||||
hotpink #FF69B4
|
||||
indianred #CD5C5C
|
||||
indigo #4B0082
|
||||
ivory #FFFFF0
|
||||
khaki #F0E68C
|
||||
lavender #E6E6FA
|
||||
lavenderblush #FFF0F5
|
||||
lawngreen #7CFC00
|
||||
lemonchiffon #FFFACD
|
||||
lightblue #ADD8E6
|
||||
lightcoral #F08080
|
||||
lightcyan #E0FFFF
|
||||
lightgoldenrodyellow #FAFAD2
|
||||
lightgreen #90EE90
|
||||
lightgrey #D3D3D3
|
||||
lightpink #FFB6C1
|
||||
lightsalmon #FFA07A
|
||||
lightseagreen #20B2AA
|
||||
lightskyblue #87CEFA
|
||||
lightslategray #778899
|
||||
lightsteelblue #B0C4DE
|
||||
lightyellow #FFFFE0
|
||||
lime #00FF00
|
||||
limegreen #32CD32
|
||||
linen #FAF0E6
|
||||
magenta #FF00FF
|
||||
maroon #800000
|
||||
mediumaquamarine #66CDAA
|
||||
mediumblue #0000CD
|
||||
mediumorchid #BA55D3
|
||||
mediumpurple #9370DB
|
||||
mediumseagreen #3CB371
|
||||
mediumslateblue #7B68EE
|
||||
mediumspringgreen #00FA9A
|
||||
mediumturquoise #48D1CC
|
||||
mediumvioletred #C71585
|
||||
midnightblue #191970
|
||||
mintcream #F5FFFA
|
||||
mistyrose #FFE4E1
|
||||
moccasin #FFE4B5
|
||||
navajowhite #FFDEAD
|
||||
navy #000080
|
||||
navyblue #9FAFDF
|
||||
oldlace #FDF5E6
|
||||
olive #808000
|
||||
olivedrab #6B8E23
|
||||
orange #FFA500
|
||||
orangered #FF4500
|
||||
orchid #DA70D6
|
||||
palegoldenrod #EEE8AA
|
||||
palegreen #98FB98
|
||||
paleturquoise #AFEEEE
|
||||
palevioletred #DB7093
|
||||
papayawhip #FFEFD5
|
||||
peachpuff #FFDAB9
|
||||
peru #CD853F
|
||||
pink #FFC0CB
|
||||
plum #DDA0DD
|
||||
powderblue #B0E0E6
|
||||
purple #800080
|
||||
red #FF0000
|
||||
rosybrown #BC8F8F
|
||||
royalblue #4169E1
|
||||
saddlebrown #8B4513
|
||||
salmon #FA8072
|
||||
sandybrown #FA8072
|
||||
seagreen #2E8B57
|
||||
seashell #FFF5EE
|
||||
sienna #A0522D
|
||||
silver #C0C0C0
|
||||
skyblue #87CEEB
|
||||
slateblue #6A5ACD
|
||||
slategray #708090
|
||||
snow #FFFAFA
|
||||
springgreen #00FF7F
|
||||
steelblue #4682B4
|
||||
tan #D2B48C
|
||||
teal #008080
|
||||
thistle #D8BFD8
|
||||
tomato #FF6347
|
||||
turquoise #40E0D0
|
||||
violet #EE82EE
|
||||
wheat #F5DEB3
|
||||
white #FFFFFF
|
||||
whitesmoke #F5F5F5
|
||||
yellow #FFFF00
|
||||
yellowgreen #9ACD32
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,923 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>McRogueFace API Reference</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1, h2, h3, h4, h5 {
|
||||
color: #2c3e50;
|
||||
}
|
||||
.toc {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.toc ul {
|
||||
list-style-type: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.toc > ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
.toc a {
|
||||
text-decoration: none;
|
||||
color: #3498db;
|
||||
}
|
||||
.toc a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.method-section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
.function-signature {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
background-color: #e9ecef;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.class-name {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
.method-name {
|
||||
color: #3498db;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
.property-name {
|
||||
color: #27ae60;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
.arg-name {
|
||||
color: #8b4513;
|
||||
font-weight: bold;
|
||||
}
|
||||
.arg-type {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
code {
|
||||
background-color: #f4f4f4;
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
pre {
|
||||
background-color: #f4f4f4;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.deprecated {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.note {
|
||||
background-color: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.returns {
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>McRogueFace API Reference</h1>
|
||||
<p><em>Generated on 2025-07-10 01:13:53</em></p>
|
||||
<p><em>This documentation was dynamically generated from the compiled module.</em></p>
|
||||
|
||||
<div class="toc">
|
||||
<h2>Table of Contents</h2>
|
||||
<ul>
|
||||
<li><a href="#functions">Functions</a></li>
|
||||
<li><a href="#classes">Classes</a>
|
||||
<ul>
|
||||
<li><a href="#Animation">Animation</a></li>
|
||||
<li><a href="#Caption">Caption</a></li>
|
||||
<li><a href="#Color">Color</a></li>
|
||||
<li><a href="#Drawable">Drawable</a></li>
|
||||
<li><a href="#Entity">Entity</a></li>
|
||||
<li><a href="#EntityCollection">EntityCollection</a></li>
|
||||
<li><a href="#Font">Font</a></li>
|
||||
<li><a href="#Frame">Frame</a></li>
|
||||
<li><a href="#Grid">Grid</a></li>
|
||||
<li><a href="#GridPoint">GridPoint</a></li>
|
||||
<li><a href="#GridPointState">GridPointState</a></li>
|
||||
<li><a href="#Scene">Scene</a></li>
|
||||
<li><a href="#Sprite">Sprite</a></li>
|
||||
<li><a href="#Texture">Texture</a></li>
|
||||
<li><a href="#Timer">Timer</a></li>
|
||||
<li><a href="#UICollection">UICollection</a></li>
|
||||
<li><a href="#UICollectionIter">UICollectionIter</a></li>
|
||||
<li><a href="#UIEntityCollectionIter">UIEntityCollectionIter</a></li>
|
||||
<li><a href="#Vector">Vector</a></li>
|
||||
<li><a href="#Window">Window</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#constants">Constants</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 id="functions">Functions</h2>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">createScenecreateScene(name: str) -> None</code></h3>
|
||||
<p>Create a new empty scene.
|
||||
|
||||
|
||||
Note:</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>name</span>: Unique name for the new scene</li>
|
||||
<li><span class='arg-name'>ValueError</span>: If a scene with this name already exists</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">createSoundBuffercreateSoundBuffer(filename: str) -> int</code></h3>
|
||||
<p>Load a sound effect from a file and return its buffer ID.</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>filename</span>: Path to the sound file (WAV, OGG, FLAC)</li>
|
||||
</ul>
|
||||
<p><span class='returns'>Returns:</span> int: Buffer ID for use with playSound() RuntimeError: If the file cannot be loaded</p>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">currentScenecurrentScene() -> str</code></h3>
|
||||
<p>Get the name of the currently active scene.</p>
|
||||
<p><span class='returns'>Returns:</span> str: Name of the current scene</p>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">delTimerdelTimer(name: str) -> None</code></h3>
|
||||
<p>Stop and remove a timer.
|
||||
|
||||
|
||||
Note:</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>name</span>: Timer identifier to remove</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">exitexit() -> None</code></h3>
|
||||
<p>Cleanly shut down the game engine and exit the application.
|
||||
|
||||
|
||||
Note:</p>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">findfind(name: str, scene: str = None) -> UIDrawable | None</code></h3>
|
||||
<p>Find the first UI element with the specified name.
|
||||
|
||||
|
||||
Note:</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>name</span>: Exact name to search for</li>
|
||||
<li><span class='arg-name'>scene</span>: Scene to search in (default: current scene)</li>
|
||||
</ul>
|
||||
<p><span class='returns'>Returns:</span> Frame, Caption, Sprite, Grid, or Entity if found; None otherwise Searches scene UI elements and entities within grids.</p>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">findAllfindAll(pattern: str, scene: str = None) -> list</code></h3>
|
||||
<p>Find all UI elements matching a name pattern.</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>pattern</span>: Name pattern with optional wildcards (* matches any characters)</li>
|
||||
<li><span class='arg-name'>scene</span>: Scene to search in (default: current scene)</li>
|
||||
</ul>
|
||||
<p><span class='returns'>Returns:</span> list: All matching UI elements and entities</p>
|
||||
<h4>Example:</h4>
|
||||
<pre><code>findAll('enemy*') # Find all elements starting with 'enemy'
|
||||
findAll('*_button') # Find all elements ending with '_button'</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">getMetricsgetMetrics() -> dict</code></h3>
|
||||
<p>Get current performance metrics.</p>
|
||||
<p><span class='returns'>Returns:</span> dict: Performance data with keys: - frame_time: Last frame duration in seconds - avg_frame_time: Average frame time - fps: Frames per second - draw_calls: Number of draw calls - ui_elements: Total UI element count - visible_elements: Visible element count - current_frame: Frame counter - runtime: Total runtime in seconds</p>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">getMusicVolumegetMusicVolume() -> int</code></h3>
|
||||
<p>Get the current music volume level.</p>
|
||||
<p><span class='returns'>Returns:</span> int: Current volume (0-100)</p>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">getSoundVolumegetSoundVolume() -> int</code></h3>
|
||||
<p>Get the current sound effects volume level.</p>
|
||||
<p><span class='returns'>Returns:</span> int: Current volume (0-100)</p>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">keypressScenekeypressScene(handler: callable) -> None</code></h3>
|
||||
<p>Set the keyboard event handler for the current scene.</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>handler</span>: Callable that receives (key_name: str, is_pressed: bool)</li>
|
||||
</ul>
|
||||
<h4>Example:</h4>
|
||||
<pre><code>def on_key(key, pressed):
|
||||
if key == 'A' and pressed:
|
||||
print('A key pressed')
|
||||
mcrfpy.keypressScene(on_key)</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">loadMusicloadMusic(filename: str) -> None</code></h3>
|
||||
<p>Load and immediately play background music from a file.
|
||||
|
||||
|
||||
Note:</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>filename</span>: Path to the music file (WAV, OGG, FLAC)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">playSoundplaySound(buffer_id: int) -> None</code></h3>
|
||||
<p>Play a sound effect using a previously loaded buffer.</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>buffer_id</span>: Sound buffer ID returned by createSoundBuffer()</li>
|
||||
<li><span class='arg-name'>RuntimeError</span>: If the buffer ID is invalid</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">sceneUIsceneUI(scene: str = None) -> list</code></h3>
|
||||
<p>Get all UI elements for a scene.</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>scene</span>: Scene name. If None, uses current scene</li>
|
||||
</ul>
|
||||
<p><span class='returns'>Returns:</span> list: All UI elements (Frame, Caption, Sprite, Grid) in the scene KeyError: If the specified scene doesn't exist</p>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">setMusicVolumesetMusicVolume(volume: int) -> None</code></h3>
|
||||
<p>Set the global music volume.</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>volume</span>: Volume level from 0 (silent) to 100 (full volume)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">setScalesetScale(multiplier: float) -> None</code></h3>
|
||||
<p>Scale the game window size.
|
||||
|
||||
|
||||
Note:</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>multiplier</span>: Scale factor (e.g., 2.0 for double size)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">setScenesetScene(scene: str, transition: str = None, duration: float = 0.0) -> None</code></h3>
|
||||
<p>Switch to a different scene with optional transition effect.</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>scene</span>: Name of the scene to switch to</li>
|
||||
<li><span class='arg-name'>transition</span>: Transition type ('fade', 'slide_left', 'slide_right', 'slide_up', 'slide_down')</li>
|
||||
<li><span class='arg-name'>duration</span>: Transition duration in seconds (default: 0.0 for instant)</li>
|
||||
<li><span class='arg-name'>KeyError</span>: If the scene doesn't exist</li>
|
||||
<li><span class='arg-name'>ValueError</span>: If the transition type is invalid</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">setSoundVolumesetSoundVolume(volume: int) -> None</code></h3>
|
||||
<p>Set the global sound effects volume.</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>volume</span>: Volume level from 0 (silent) to 100 (full volume)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">setTimersetTimer(name: str, handler: callable, interval: int) -> None</code></h3>
|
||||
<p>Create or update a recurring timer.
|
||||
|
||||
|
||||
Note:</p>
|
||||
<h4>Arguments:</h4>
|
||||
<ul>
|
||||
<li><span class='arg-name'>name</span>: Unique identifier for the timer</li>
|
||||
<li><span class='arg-name'>handler</span>: Function called with (runtime: float) parameter</li>
|
||||
<li><span class='arg-name'>interval</span>: Time between calls in milliseconds</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 id='classes'>Classes</h2>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Animation"><span class="class-name">Animation</span></h3>
|
||||
<p>Animation object for animating UI properties</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">get_current_value(...)</code></h5>
|
||||
<p>Get the current interpolated value</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">start(...)</code></h5>
|
||||
<p>Start the animation on a target UIDrawable</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">updateUpdate the animation by deltaTime (returns True if still running)</code></h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Caption"><span class="class-name">Caption</span></h3>
|
||||
<p><em>Inherits from: Drawable</em></p>
|
||||
<p>Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)
|
||||
|
||||
A text display UI element with customizable font and styling.
|
||||
|
||||
Args:
|
||||
text (str): The text content to display. Default: ''
|
||||
x (float): X position in pixels. Default: 0
|
||||
y (float): Y position in pixels. Default: 0
|
||||
font (Font): Font object for text rendering. Default: engine default font
|
||||
fill_color (Color): Text fill color. Default: (255, 255, 255, 255)
|
||||
outline_color (Color): Text outline color. Default: (0, 0, 0, 255)
|
||||
outline (float): Text outline thickness. Default: 0
|
||||
click (callable): Click event handler. Default: None
|
||||
|
||||
Attributes:
|
||||
text (str): The displayed text content
|
||||
x, y (float): Position in pixels
|
||||
font (Font): Font used for rendering
|
||||
fill_color, outline_color (Color): Text appearance
|
||||
outline (float): Outline thickness
|
||||
click (callable): Click event handler
|
||||
visible (bool): Visibility state
|
||||
z_index (int): Rendering order
|
||||
w, h (float): Read-only computed size based on text and font</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">get_boundsGet bounding box as (x, y, width, height)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">moveMove by relative offset (dx, dy)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">resizeResize to new dimensions (width, height)</code></h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Color"><span class="class-name">Color</span></h3>
|
||||
<p>SFML Color Object</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">from_hexCreate Color from hex string (e.g., '#FF0000' or 'FF0000')</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">lerp(...)</code></h5>
|
||||
<p>Linearly interpolate between this color and another</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">to_hex(...)</code></h5>
|
||||
<p>Convert Color to hex string</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Drawable"><span class="class-name">Drawable</span></h3>
|
||||
<p>Base class for all drawable UI elements</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">get_boundsGet bounding box as (x, y, width, height)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">moveMove by relative offset (dx, dy)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">resizeResize to new dimensions (width, height)</code></h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Entity"><span class="class-name">Entity</span></h3>
|
||||
<p><em>Inherits from: Drawable</em></p>
|
||||
<p>UIEntity objects</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">at(...)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">die(...)</code></h5>
|
||||
<p>Remove this entity from its grid</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">get_boundsGet bounding box as (x, y, width, height)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">index(...)</code></h5>
|
||||
<p>Return the index of this entity in its grid's entity collection</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">moveMove by relative offset (dx, dy)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">path_topath_to(x: int, y: int) -> bool</code></h5>
|
||||
<p>Find and follow path to target position using A* pathfinding.</p>
|
||||
<div style='margin-left: 20px;'>
|
||||
<div><span class='arg-name'>x</span>: Target X coordinate</div>
|
||||
<div><span class='arg-name'>y</span>: Target Y coordinate</div>
|
||||
</div>
|
||||
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> True if a path was found and the entity started moving, False otherwise</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">resizeResize to new dimensions (width, height)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">update_visibilityupdate_visibility() -> None</code></h5>
|
||||
<p>Update entity's visibility state based on current FOV.
|
||||
|
||||
Recomputes which cells are visible from the entity's position and updates
|
||||
the entity's gridstate to track explored areas. This is called automatically
|
||||
when the entity moves if it has a grid with perspective set.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="EntityCollection"><span class="class-name">EntityCollection</span></h3>
|
||||
<p>Iterable, indexable collection of Entities</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">append(...)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">count(...)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">extend(...)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">index(...)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">remove(...)</code></h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Font"><span class="class-name">Font</span></h3>
|
||||
<p>SFML Font Object</p>
|
||||
<h4>Methods:</h4>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Frame"><span class="class-name">Frame</span></h3>
|
||||
<p><em>Inherits from: Drawable</em></p>
|
||||
<p>Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)
|
||||
|
||||
A rectangular frame UI element that can contain other drawable elements.
|
||||
|
||||
Args:
|
||||
x (float): X position in pixels. Default: 0
|
||||
y (float): Y position in pixels. Default: 0
|
||||
w (float): Width in pixels. Default: 0
|
||||
h (float): Height in pixels. Default: 0
|
||||
fill_color (Color): Background fill color. Default: (0, 0, 0, 128)
|
||||
outline_color (Color): Border outline color. Default: (255, 255, 255, 255)
|
||||
outline (float): Border outline thickness. Default: 0
|
||||
click (callable): Click event handler. Default: None
|
||||
children (list): Initial list of child drawable elements. Default: None
|
||||
|
||||
Attributes:
|
||||
x, y (float): Position in pixels
|
||||
w, h (float): Size in pixels
|
||||
fill_color, outline_color (Color): Visual appearance
|
||||
outline (float): Border thickness
|
||||
click (callable): Click event handler
|
||||
children (list): Collection of child drawable elements
|
||||
visible (bool): Visibility state
|
||||
z_index (int): Rendering order
|
||||
clip_children (bool): Whether to clip children to frame bounds</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">get_boundsGet bounding box as (x, y, width, height)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">moveMove by relative offset (dx, dy)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">resizeResize to new dimensions (width, height)</code></h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Grid"><span class="class-name">Grid</span></h3>
|
||||
<p><em>Inherits from: Drawable</em></p>
|
||||
<p>Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)
|
||||
|
||||
A grid-based tilemap UI element for rendering tile-based levels and game worlds.
|
||||
|
||||
Args:
|
||||
x (float): X position in pixels. Default: 0
|
||||
y (float): Y position in pixels. Default: 0
|
||||
grid_size (tuple): Grid dimensions as (width, height) in tiles. Default: (20, 20)
|
||||
texture (Texture): Texture atlas containing tile sprites. Default: None
|
||||
tile_width (int): Width of each tile in pixels. Default: 16
|
||||
tile_height (int): Height of each tile in pixels. Default: 16
|
||||
scale (float): Grid scaling factor. Default: 1.0
|
||||
click (callable): Click event handler. Default: None
|
||||
|
||||
Attributes:
|
||||
x, y (float): Position in pixels
|
||||
grid_size (tuple): Grid dimensions (width, height) in tiles
|
||||
tile_width, tile_height (int): Tile dimensions in pixels
|
||||
texture (Texture): Tile texture atlas
|
||||
scale (float): Scale multiplier
|
||||
points (list): 2D array of GridPoint objects for tile data
|
||||
entities (list): Collection of Entity objects in the grid
|
||||
background_color (Color): Grid background color
|
||||
click (callable): Click event handler
|
||||
visible (bool): Visibility state
|
||||
z_index (int): Rendering order</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">at(...)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">compute_astar_pathcompute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]</code></h5>
|
||||
<p>Compute A* path between two points.</p>
|
||||
<div style='margin-left: 20px;'>
|
||||
<div><span class='arg-name'>x1</span>: Starting X coordinate</div>
|
||||
<div><span class='arg-name'>y1</span>: Starting Y coordinate</div>
|
||||
<div><span class='arg-name'>x2</span>: Target X coordinate</div>
|
||||
<div><span class='arg-name'>y2</span>: Target Y coordinate</div>
|
||||
<div><span class='arg-name'>diagonal_cost</span>: Cost of diagonal movement (default: 1.41)</div>
|
||||
</div>
|
||||
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> List of (x, y) tuples representing the path, empty list if no path exists</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">compute_dijkstracompute_dijkstra(root_x: int, root_y: int, diagonal_cost: float = 1.41) -> None</code></h5>
|
||||
<p>Compute Dijkstra map from root position.</p>
|
||||
<div style='margin-left: 20px;'>
|
||||
<div><span class='arg-name'>root_x</span>: X coordinate of the root/target</div>
|
||||
<div><span class='arg-name'>root_y</span>: Y coordinate of the root/target</div>
|
||||
<div><span class='arg-name'>diagonal_cost</span>: Cost of diagonal movement (default: 1.41)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">compute_fovcompute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None</code></h5>
|
||||
<p>Compute field of view from a position.</p>
|
||||
<div style='margin-left: 20px;'>
|
||||
<div><span class='arg-name'>x</span>: X coordinate of the viewer</div>
|
||||
<div><span class='arg-name'>y</span>: Y coordinate of the viewer</div>
|
||||
<div><span class='arg-name'>radius</span>: Maximum view distance (0 = unlimited)</div>
|
||||
<div><span class='arg-name'>light_walls</span>: Whether walls are lit when visible</div>
|
||||
<div><span class='arg-name'>algorithm</span>: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">find_pathfind_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]</code></h5>
|
||||
<p>Find A* path between two points.</p>
|
||||
<div style='margin-left: 20px;'>
|
||||
<div><span class='arg-name'>x1</span>: Starting X coordinate</div>
|
||||
<div><span class='arg-name'>y1</span>: Starting Y coordinate</div>
|
||||
<div><span class='arg-name'>x2</span>: Target X coordinate</div>
|
||||
<div><span class='arg-name'>y2</span>: Target Y coordinate</div>
|
||||
<div><span class='arg-name'>diagonal_cost</span>: Cost of diagonal movement (default: 1.41)</div>
|
||||
</div>
|
||||
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> List of (x, y) tuples representing the path, empty list if no path exists</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">get_boundsGet bounding box as (x, y, width, height)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">get_dijkstra_distanceget_dijkstra_distance(x: int, y: int) -> Optional[float]</code></h5>
|
||||
<p>Get distance from Dijkstra root to position.</p>
|
||||
<div style='margin-left: 20px;'>
|
||||
<div><span class='arg-name'>x</span>: X coordinate to query</div>
|
||||
<div><span class='arg-name'>y</span>: Y coordinate to query</div>
|
||||
</div>
|
||||
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> Distance as float, or None if position is unreachable or invalid</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">get_dijkstra_pathget_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]</code></h5>
|
||||
<p>Get path from position to Dijkstra root.</p>
|
||||
<div style='margin-left: 20px;'>
|
||||
<div><span class='arg-name'>x</span>: Starting X coordinate</div>
|
||||
<div><span class='arg-name'>y</span>: Starting Y coordinate</div>
|
||||
</div>
|
||||
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> List of (x, y) tuples representing path to root, empty if unreachable</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">is_in_fovis_in_fov(x: int, y: int) -> bool</code></h5>
|
||||
<p>Check if a cell is in the field of view.</p>
|
||||
<div style='margin-left: 20px;'>
|
||||
<div><span class='arg-name'>x</span>: X coordinate to check</div>
|
||||
<div><span class='arg-name'>y</span>: Y coordinate to check</div>
|
||||
</div>
|
||||
<p style='margin-left: 20px;'><span class='returns'>Returns:</span> True if the cell is visible, False otherwise</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">moveMove by relative offset (dx, dy)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">resizeResize to new dimensions (width, height)</code></h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="GridPoint"><span class="class-name">GridPoint</span></h3>
|
||||
<p>UIGridPoint object</p>
|
||||
<h4>Methods:</h4>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="GridPointState"><span class="class-name">GridPointState</span></h3>
|
||||
<p>UIGridPointState object</p>
|
||||
<h4>Methods:</h4>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Scene"><span class="class-name">Scene</span></h3>
|
||||
<p>Base class for object-oriented scenes</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">activate(...)</code></h5>
|
||||
<p>Make this the active scene</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">get_ui(...)</code></h5>
|
||||
<p>Get the UI element collection for this scene</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">register_keyboardRegister a keyboard handler function (alternative to overriding on_keypress)</code></h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Sprite"><span class="class-name">Sprite</span></h3>
|
||||
<p><em>Inherits from: Drawable</em></p>
|
||||
<p>Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)
|
||||
|
||||
A sprite UI element that displays a texture or portion of a texture atlas.
|
||||
|
||||
Args:
|
||||
x (float): X position in pixels. Default: 0
|
||||
y (float): Y position in pixels. Default: 0
|
||||
texture (Texture): Texture object to display. Default: None
|
||||
sprite_index (int): Index into texture atlas (if applicable). Default: 0
|
||||
scale (float): Sprite scaling factor. Default: 1.0
|
||||
click (callable): Click event handler. Default: None
|
||||
|
||||
Attributes:
|
||||
x, y (float): Position in pixels
|
||||
texture (Texture): The texture being displayed
|
||||
sprite_index (int): Current sprite index in texture atlas
|
||||
scale (float): Scale multiplier
|
||||
click (callable): Click event handler
|
||||
visible (bool): Visibility state
|
||||
z_index (int): Rendering order
|
||||
w, h (float): Read-only computed size based on texture and scale</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">get_boundsGet bounding box as (x, y, width, height)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">moveMove by relative offset (dx, dy)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">resizeResize to new dimensions (width, height)</code></h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Texture"><span class="class-name">Texture</span></h3>
|
||||
<p>SFML Texture Object</p>
|
||||
<h4>Methods:</h4>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Timer"><span class="class-name">Timer</span></h3>
|
||||
<p>Timer object for scheduled callbacks</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">cancel(...)</code></h5>
|
||||
<p>Cancel the timer and remove it from the system</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">pause(...)</code></h5>
|
||||
<p>Pause the timer</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">restart(...)</code></h5>
|
||||
<p>Restart the timer from the current time</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">resume(...)</code></h5>
|
||||
<p>Resume a paused timer</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="UICollection"><span class="class-name">UICollection</span></h3>
|
||||
<p>Iterable, indexable collection of UI objects</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">append(...)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">count(...)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">extend(...)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">index(...)</code></h5>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">remove(...)</code></h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="UICollectionIter"><span class="class-name">UICollectionIter</span></h3>
|
||||
<p>Iterator for a collection of UI objects</p>
|
||||
<h4>Methods:</h4>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="UIEntityCollectionIter"><span class="class-name">UIEntityCollectionIter</span></h3>
|
||||
<p>Iterator for a collection of UI objects</p>
|
||||
<h4>Methods:</h4>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Vector"><span class="class-name">Vector</span></h3>
|
||||
<p>SFML Vector Object</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">angle(...)</code></h5>
|
||||
<p>Return the angle in radians from the positive X axis</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">copy(...)</code></h5>
|
||||
<p>Return a copy of this vector</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">distance_to(...)</code></h5>
|
||||
<p>Return the distance to another vector</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">dot(...)</code></h5>
|
||||
<p>Return the dot product with another vector</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">magnitude(...)</code></h5>
|
||||
<p>Return the length of the vector</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">magnitude_squared(...)</code></h5>
|
||||
<p>Return the squared length of the vector</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">normalize(...)</code></h5>
|
||||
<p>Return a unit vector in the same direction</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-section">
|
||||
<h3 id="Window"><span class="class-name">Window</span></h3>
|
||||
<p>Window singleton for accessing and modifying the game window properties</p>
|
||||
<h4>Methods:</h4>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">center(...)</code></h5>
|
||||
<p>Center the window on the screen</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">get(...)</code></h5>
|
||||
<p>Get the Window singleton instance</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">screenshot(...)</code></h5>
|
||||
<p>Take a screenshot. Pass filename to save to file, or get raw bytes if no filename.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 id='constants'>Constants</h2>
|
||||
<ul>
|
||||
<li><code>FOV_BASIC</code> (int): 0</li>
|
||||
<li><code>FOV_DIAMOND</code> (int): 1</li>
|
||||
<li><code>FOV_PERMISSIVE_0</code> (int): 3</li>
|
||||
<li><code>FOV_PERMISSIVE_1</code> (int): 4</li>
|
||||
<li><code>FOV_PERMISSIVE_2</code> (int): 5</li>
|
||||
<li><code>FOV_PERMISSIVE_3</code> (int): 6</li>
|
||||
<li><code>FOV_PERMISSIVE_4</code> (int): 7</li>
|
||||
<li><code>FOV_PERMISSIVE_5</code> (int): 8</li>
|
||||
<li><code>FOV_PERMISSIVE_6</code> (int): 9</li>
|
||||
<li><code>FOV_PERMISSIVE_7</code> (int): 10</li>
|
||||
<li><code>FOV_PERMISSIVE_8</code> (int): 11</li>
|
||||
<li><code>FOV_RESTRICTIVE</code> (int): 12</li>
|
||||
<li><code>FOV_SHADOW</code> (int): 2</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
"""Type stubs for McRogueFace Python API.
|
||||
|
||||
Auto-generated - do not edit directly.
|
||||
"""
|
||||
|
||||
from typing import Any, List, Dict, Tuple, Optional, Callable, Union
|
||||
|
||||
# Module documentation
|
||||
# McRogueFace Python API\n\nCore game engine interface for creating roguelike games with Python.\n\nThis module provides:\n- Scene management (createScene, setScene, currentScene)\n- UI components (Frame, Caption, Sprite, Grid)\n- Entity system for game objects\n- Audio playback (sound effects and music)\n- Timer system for scheduled events\n- Input handling\n- Performance metrics\n\nExample:\n import mcrfpy\n \n # Create a new scene\n mcrfpy.createScene('game')\n mcrfpy.setScene('game')\n \n # Add UI elements\n frame = mcrfpy.Frame(10, 10, 200, 100)\n caption = mcrfpy.Caption('Hello World', 50, 50)\n mcrfpy.sceneUI().extend([frame, caption])\n
|
||||
|
||||
# Classes
|
||||
|
||||
class Animation:
|
||||
"""Animation object for animating UI properties"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_current_value(self, *args, **kwargs) -> Any: ...
|
||||
def start(self, *args, **kwargs) -> Any: ...
|
||||
def update(selfreturns True if still running) -> Any: ...
|
||||
|
||||
class Caption:
|
||||
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Color:
|
||||
"""SFML Color Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def from_hex(selfe.g., '#FF0000' or 'FF0000') -> Any: ...
|
||||
def lerp(self, *args, **kwargs) -> Any: ...
|
||||
def to_hex(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class Drawable:
|
||||
"""Base class for all drawable UI elements"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Entity:
|
||||
"""UIEntity objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def at(self, *args, **kwargs) -> Any: ...
|
||||
def die(self, *args, **kwargs) -> Any: ...
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def index(self, *args, **kwargs) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def path_to(selfx: int, y: int) -> bool: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
def update_visibility(self) -> None: ...
|
||||
|
||||
class EntityCollection:
|
||||
"""Iterable, indexable collection of Entities"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def append(self, *args, **kwargs) -> Any: ...
|
||||
def count(self, *args, **kwargs) -> Any: ...
|
||||
def extend(self, *args, **kwargs) -> Any: ...
|
||||
def index(self, *args, **kwargs) -> Any: ...
|
||||
def remove(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class Font:
|
||||
"""SFML Font Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Frame:
|
||||
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Grid:
|
||||
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def at(self, *args, **kwargs) -> Any: ...
|
||||
def compute_astar_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
|
||||
def compute_dijkstra(selfroot_x: int, root_y: int, diagonal_cost: float = 1.41) -> None: ...
|
||||
def compute_fov(selfx: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None: ...
|
||||
def find_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def get_dijkstra_distance(selfx: int, y: int) -> Optional[float]: ...
|
||||
def get_dijkstra_path(selfx: int, y: int) -> List[Tuple[int, int]]: ...
|
||||
def is_in_fov(selfx: int, y: int) -> bool: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class GridPoint:
|
||||
"""UIGridPoint object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class GridPointState:
|
||||
"""UIGridPointState object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Scene:
|
||||
"""Base class for object-oriented scenes"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def activate(self, *args, **kwargs) -> Any: ...
|
||||
def get_ui(self, *args, **kwargs) -> Any: ...
|
||||
def register_keyboard(selfalternative to overriding on_keypress) -> Any: ...
|
||||
|
||||
class Sprite:
|
||||
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Texture:
|
||||
"""SFML Texture Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Timer:
|
||||
"""Timer object for scheduled callbacks"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def cancel(self, *args, **kwargs) -> Any: ...
|
||||
def pause(self, *args, **kwargs) -> Any: ...
|
||||
def restart(self, *args, **kwargs) -> Any: ...
|
||||
def resume(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class UICollection:
|
||||
"""Iterable, indexable collection of UI objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def append(self, *args, **kwargs) -> Any: ...
|
||||
def count(self, *args, **kwargs) -> Any: ...
|
||||
def extend(self, *args, **kwargs) -> Any: ...
|
||||
def index(self, *args, **kwargs) -> Any: ...
|
||||
def remove(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class UICollectionIter:
|
||||
"""Iterator for a collection of UI objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class UIEntityCollectionIter:
|
||||
"""Iterator for a collection of UI objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Vector:
|
||||
"""SFML Vector Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def angle(self, *args, **kwargs) -> Any: ...
|
||||
def copy(self, *args, **kwargs) -> Any: ...
|
||||
def distance_to(self, *args, **kwargs) -> Any: ...
|
||||
def dot(self, *args, **kwargs) -> Any: ...
|
||||
def magnitude(self, *args, **kwargs) -> Any: ...
|
||||
def magnitude_squared(self, *args, **kwargs) -> Any: ...
|
||||
def normalize(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class Window:
|
||||
"""Window singleton for accessing and modifying the game window properties"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def center(self, *args, **kwargs) -> Any: ...
|
||||
def get(self, *args, **kwargs) -> Any: ...
|
||||
def screenshot(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
# Functions
|
||||
|
||||
def createScene(name: str) -> None: ...
|
||||
def createSoundBuffer(filename: str) -> int: ...
|
||||
def currentScene() -> str: ...
|
||||
def delTimer(name: str) -> None: ...
|
||||
def exit() -> None: ...
|
||||
def find(name: str, scene: str = None) -> UIDrawable | None: ...
|
||||
def findAll(pattern: str, scene: str = None) -> list: ...
|
||||
def getMetrics() -> dict: ...
|
||||
def getMusicVolume() -> int: ...
|
||||
def getSoundVolume() -> int: ...
|
||||
def keypressScene(handler: callable) -> None: ...
|
||||
def loadMusic(filename: str) -> None: ...
|
||||
def playSound(buffer_id: int) -> None: ...
|
||||
def sceneUI(scene: str = None) -> list: ...
|
||||
def setMusicVolume(volume: int) -> None: ...
|
||||
def setScale(multiplier: float) -> None: ...
|
||||
def setScene(scene: str, transition: str = None, duration: float = 0.0) -> None: ...
|
||||
def setSoundVolume(volume: int) -> None: ...
|
||||
def setTimer(name: str, handler: callable, interval: int) -> None: ...
|
||||
|
||||
# Constants
|
||||
|
||||
FOV_BASIC: int
|
||||
FOV_DIAMOND: int
|
||||
FOV_PERMISSIVE_0: int
|
||||
FOV_PERMISSIVE_1: int
|
||||
FOV_PERMISSIVE_2: int
|
||||
FOV_PERMISSIVE_3: int
|
||||
FOV_PERMISSIVE_4: int
|
||||
FOV_PERMISSIVE_5: int
|
||||
FOV_PERMISSIVE_6: int
|
||||
FOV_PERMISSIVE_7: int
|
||||
FOV_PERMISSIVE_8: int
|
||||
FOV_RESTRICTIVE: int
|
||||
FOV_SHADOW: int
|
||||
default_font: Any
|
||||
default_texture: Any
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
"""Type stubs for McRogueFace Python API.
|
||||
|
||||
Auto-generated - do not edit directly.
|
||||
"""
|
||||
|
||||
from typing import Any, List, Dict, Tuple, Optional, Callable, Union
|
||||
|
||||
# Module documentation
|
||||
# McRogueFace Python API\n\nCore game engine interface for creating roguelike games with Python.\n\nThis module provides:\n- Scene management (createScene, setScene, currentScene)\n- UI components (Frame, Caption, Sprite, Grid)\n- Entity system for game objects\n- Audio playback (sound effects and music)\n- Timer system for scheduled events\n- Input handling\n- Performance metrics\n\nExample:\n import mcrfpy\n \n # Create a new scene\n mcrfpy.createScene('game')\n mcrfpy.setScene('game')\n \n # Add UI elements\n frame = mcrfpy.Frame(10, 10, 200, 100)\n caption = mcrfpy.Caption('Hello World', 50, 50)\n mcrfpy.sceneUI().extend([frame, caption])\n
|
||||
|
||||
# Classes
|
||||
|
||||
class Animation:
|
||||
"""Animation object for animating UI properties"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_current_value(self, *args, **kwargs) -> Any: ...
|
||||
def start(self, *args, **kwargs) -> Any: ...
|
||||
def update(selfreturns True if still running) -> Any: ...
|
||||
|
||||
class Caption:
|
||||
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Color:
|
||||
"""SFML Color Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def from_hex(selfe.g., '#FF0000' or 'FF0000') -> Any: ...
|
||||
def lerp(self, *args, **kwargs) -> Any: ...
|
||||
def to_hex(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class Drawable:
|
||||
"""Base class for all drawable UI elements"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Entity:
|
||||
"""UIEntity objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def at(self, *args, **kwargs) -> Any: ...
|
||||
def die(self, *args, **kwargs) -> Any: ...
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def index(self, *args, **kwargs) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def path_to(selfx: int, y: int) -> bool: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
def update_visibility(self) -> None: ...
|
||||
|
||||
class EntityCollection:
|
||||
"""Iterable, indexable collection of Entities"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def append(self, *args, **kwargs) -> Any: ...
|
||||
def count(self, *args, **kwargs) -> Any: ...
|
||||
def extend(self, *args, **kwargs) -> Any: ...
|
||||
def index(self, *args, **kwargs) -> Any: ...
|
||||
def remove(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class Font:
|
||||
"""SFML Font Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Frame:
|
||||
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Grid:
|
||||
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def at(self, *args, **kwargs) -> Any: ...
|
||||
def compute_astar_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
|
||||
def compute_dijkstra(selfroot_x: int, root_y: int, diagonal_cost: float = 1.41) -> None: ...
|
||||
def compute_fov(selfx: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None: ...
|
||||
def find_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def get_dijkstra_distance(selfx: int, y: int) -> Optional[float]: ...
|
||||
def get_dijkstra_path(selfx: int, y: int) -> List[Tuple[int, int]]: ...
|
||||
def is_in_fov(selfx: int, y: int) -> bool: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class GridPoint:
|
||||
"""UIGridPoint object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class GridPointState:
|
||||
"""UIGridPointState object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Scene:
|
||||
"""Base class for object-oriented scenes"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def activate(self, *args, **kwargs) -> Any: ...
|
||||
def get_ui(self, *args, **kwargs) -> Any: ...
|
||||
def register_keyboard(selfalternative to overriding on_keypress) -> Any: ...
|
||||
|
||||
class Sprite:
|
||||
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Texture:
|
||||
"""SFML Texture Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Timer:
|
||||
"""Timer object for scheduled callbacks"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def cancel(self, *args, **kwargs) -> Any: ...
|
||||
def pause(self, *args, **kwargs) -> Any: ...
|
||||
def restart(self, *args, **kwargs) -> Any: ...
|
||||
def resume(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class UICollection:
|
||||
"""Iterable, indexable collection of UI objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def append(self, *args, **kwargs) -> Any: ...
|
||||
def count(self, *args, **kwargs) -> Any: ...
|
||||
def extend(self, *args, **kwargs) -> Any: ...
|
||||
def index(self, *args, **kwargs) -> Any: ...
|
||||
def remove(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class UICollectionIter:
|
||||
"""Iterator for a collection of UI objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class UIEntityCollectionIter:
|
||||
"""Iterator for a collection of UI objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Vector:
|
||||
"""SFML Vector Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def angle(self, *args, **kwargs) -> Any: ...
|
||||
def copy(self, *args, **kwargs) -> Any: ...
|
||||
def distance_to(self, *args, **kwargs) -> Any: ...
|
||||
def dot(self, *args, **kwargs) -> Any: ...
|
||||
def magnitude(self, *args, **kwargs) -> Any: ...
|
||||
def magnitude_squared(self, *args, **kwargs) -> Any: ...
|
||||
def normalize(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class Window:
|
||||
"""Window singleton for accessing and modifying the game window properties"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def center(self, *args, **kwargs) -> Any: ...
|
||||
def get(self, *args, **kwargs) -> Any: ...
|
||||
def screenshot(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
# Functions
|
||||
|
||||
def createScene(name: str) -> None: ...
|
||||
def createSoundBuffer(filename: str) -> int: ...
|
||||
def currentScene() -> str: ...
|
||||
def delTimer(name: str) -> None: ...
|
||||
def exit() -> None: ...
|
||||
def find(name: str, scene: str = None) -> UIDrawable | None: ...
|
||||
def findAll(pattern: str, scene: str = None) -> list: ...
|
||||
def getMetrics() -> dict: ...
|
||||
def getMusicVolume() -> int: ...
|
||||
def getSoundVolume() -> int: ...
|
||||
def keypressScene(handler: callable) -> None: ...
|
||||
def loadMusic(filename: str) -> None: ...
|
||||
def playSound(buffer_id: int) -> None: ...
|
||||
def sceneUI(scene: str = None) -> list: ...
|
||||
def setMusicVolume(volume: int) -> None: ...
|
||||
def setScale(multiplier: float) -> None: ...
|
||||
def setScene(scene: str, transition: str = None, duration: float = 0.0) -> None: ...
|
||||
def setSoundVolume(volume: int) -> None: ...
|
||||
def setTimer(name: str, handler: callable, interval: int) -> None: ...
|
||||
|
||||
# Constants
|
||||
|
||||
FOV_BASIC: int
|
||||
FOV_DIAMOND: int
|
||||
FOV_PERMISSIVE_0: int
|
||||
FOV_PERMISSIVE_1: int
|
||||
FOV_PERMISSIVE_2: int
|
||||
FOV_PERMISSIVE_3: int
|
||||
FOV_PERMISSIVE_4: int
|
||||
FOV_PERMISSIVE_5: int
|
||||
FOV_PERMISSIVE_6: int
|
||||
FOV_PERMISSIVE_7: int
|
||||
FOV_PERMISSIVE_8: int
|
||||
FOV_RESTRICTIVE: int
|
||||
FOV_SHADOW: int
|
||||
default_font: Any
|
||||
default_texture: Any
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
"""Type stubs for McRogueFace automation API."""
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
def click(x=None, y=None, clicks=1, interval=0.0, button='left') -> Any: ...
|
||||
def doubleClick(x=None, y=None) -> Any: ...
|
||||
def dragRel(xOffset, yOffset, duration=0.0, button='left') -> Any: ...
|
||||
def dragTo(x, y, duration=0.0, button='left') -> Any: ...
|
||||
def hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c')) -> Any: ...
|
||||
def keyDown(key) -> Any: ...
|
||||
def keyUp(key) -> Any: ...
|
||||
def middleClick(x=None, y=None) -> Any: ...
|
||||
def mouseDown(x=None, y=None, button='left') -> Any: ...
|
||||
def mouseUp(x=None, y=None, button='left') -> Any: ...
|
||||
def moveRel(xOffset, yOffset, duration=0.0) -> Any: ...
|
||||
def moveTo(x, y, duration=0.0) -> Any: ...
|
||||
def onScreen(x, y) -> Any: ...
|
||||
def position() - Get current mouse position as (x, y) -> Any: ...
|
||||
def rightClick(x=None, y=None) -> Any: ...
|
||||
def screenshot(filename) -> Any: ...
|
||||
def scroll(clicks, x=None, y=None) -> Any: ...
|
||||
def size() - Get screen size as (width, height) -> Any: ...
|
||||
def tripleClick(x=None, y=None) -> Any: ...
|
||||
def typewrite(message, interval=0.0) -> Any: ...
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 0: Introduction to Scene, Texture, and Grid
|
||||
|
||||
This tutorial introduces the basic building blocks:
|
||||
- Scene: A container for UI elements and game state
|
||||
- Texture: Loading image assets for use in the game
|
||||
- Grid: A tilemap component for rendering tile-based worlds
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Create and activate a new scene
|
||||
mcrfpy.createScene("tutorial")
|
||||
mcrfpy.setScene("tutorial")
|
||||
|
||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||
|
||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||
|
||||
# calculating the size in pixels to fit the entire grid on-screen
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size, # height and width on screen
|
||||
)
|
||||
|
||||
grid.zoom = zoom
|
||||
grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Fill the grid with a simple pattern
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
# Create walls around the edges
|
||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||
tile_index = random.choice(WALL_TILES)
|
||||
else:
|
||||
# Fill interior with floor tiles
|
||||
tile_index = random.choice(FLOOR_TILES)
|
||||
|
||||
# Set the tile at this position
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = tile_index
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Add a title caption
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 0",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
# Add instructions
|
||||
instructions = mcrfpy.Caption((280, 750),
|
||||
text="Scene + Texture + Grid = Tilemap!",
|
||||
)
|
||||
instructions.font_size=18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
print("Tutorial Part 0 loaded!")
|
||||
print(f"Created a {grid.grid_size[0]}x{grid.grid_size[1]} grid")
|
||||
print(f"Grid positioned at ({grid.x}, {grid.y})")
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 1: Entities and Keyboard Input
|
||||
|
||||
This tutorial builds on Part 0 by adding:
|
||||
- Entity: A game object that can be placed in a grid
|
||||
- Keyboard handling: Responding to key presses to move the entity
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Create and activate a new scene
|
||||
mcrfpy.createScene("tutorial")
|
||||
mcrfpy.setScene("tutorial")
|
||||
|
||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Load the hero sprite texture (32x32 sprite sheet)
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||
|
||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||
|
||||
# calculating the size in pixels to fit the entire grid on-screen
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size, # height and width on screen
|
||||
)
|
||||
|
||||
grid.zoom = zoom
|
||||
grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Fill the grid with a simple pattern
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
# Create walls around the edges
|
||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||
tile_index = random.choice(WALL_TILES)
|
||||
else:
|
||||
# Fill interior with floor tiles
|
||||
tile_index = random.choice(FLOOR_TILES)
|
||||
|
||||
# Set the tile at this position
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = tile_index
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Create a player entity at position (4, 4)
|
||||
player = mcrfpy.Entity(
|
||||
(4, 4), # Entity positions are tile coordinates
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Use the first sprite in the texture
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
|
||||
# Define keyboard handler
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input to move the player"""
|
||||
if state == "start": # Only respond to key press, not release
|
||||
# Get current player position in grid coordinates
|
||||
px, py = player.x, player.y
|
||||
|
||||
# Calculate new position based on key press
|
||||
if key == "W" or key == "Up":
|
||||
py -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
py += 1
|
||||
elif key == "A" or key == "Left":
|
||||
px -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
px += 1
|
||||
|
||||
# Update player position (no collision checking yet)
|
||||
player.x = px
|
||||
player.y = py
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add a title caption
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 1",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
# Add instructions
|
||||
instructions = mcrfpy.Caption((200, 750),
|
||||
text="Use WASD or Arrow Keys to move the hero!",
|
||||
)
|
||||
instructions.font_size=18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
print("Tutorial Part 1 loaded!")
|
||||
print(f"Player entity created at grid position (4, 4)")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 1: Entities and Keyboard Input
|
||||
|
||||
This tutorial builds on Part 0 by adding:
|
||||
- Entity: A game object that can be placed in a grid
|
||||
- Keyboard handling: Responding to key presses to move the entity
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Create and activate a new scene
|
||||
mcrfpy.createScene("tutorial")
|
||||
mcrfpy.setScene("tutorial")
|
||||
|
||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Load the hero sprite texture (32x32 sprite sheet)
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||
|
||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||
|
||||
# calculating the size in pixels to fit the entire grid on-screen
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size, # height and width on screen
|
||||
)
|
||||
|
||||
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Fill the grid with a simple pattern
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
# Create walls around the edges
|
||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||
tile_index = random.choice(WALL_TILES)
|
||||
else:
|
||||
# Fill interior with floor tiles
|
||||
tile_index = random.choice(FLOOR_TILES)
|
||||
|
||||
# Set the tile at this position
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = tile_index
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Create a player entity at position (4, 4)
|
||||
player = mcrfpy.Entity(
|
||||
(4, 4), # Entity positions are tile coordinates
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Use the first sprite in the texture
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||
|
||||
# Define keyboard handler
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input to move the player"""
|
||||
if state == "start": # Only respond to key press, not release
|
||||
# Get current player position in grid coordinates
|
||||
px, py = player.x, player.y
|
||||
|
||||
# Calculate new position based on key press
|
||||
if key == "W" or key == "Up":
|
||||
py -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
py += 1
|
||||
elif key == "A" or key == "Left":
|
||||
px -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
px += 1
|
||||
|
||||
# Update player position (no collision checking yet)
|
||||
player.x = px
|
||||
player.y = py
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add a title caption
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 1",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
# Add instructions
|
||||
instructions = mcrfpy.Caption((200, 750),
|
||||
text="Use WASD or Arrow Keys to move the hero!",
|
||||
)
|
||||
instructions.font_size=18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
print("Tutorial Part 1 loaded!")
|
||||
print(f"Player entity created at grid position (4, 4)")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 2: Animated Movement
|
||||
|
||||
This tutorial builds on Part 1 by adding:
|
||||
- Animation system for smooth movement
|
||||
- Movement that takes 0.5 seconds per tile
|
||||
- Input blocking during movement animation
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Create and activate a new scene
|
||||
mcrfpy.createScene("tutorial")
|
||||
mcrfpy.setScene("tutorial")
|
||||
|
||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Load the hero sprite texture (32x32 sprite sheet)
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||
|
||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||
|
||||
# calculating the size in pixels to fit the entire grid on-screen
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size, # height and width on screen
|
||||
)
|
||||
|
||||
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Fill the grid with a simple pattern
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
# Create walls around the edges
|
||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||
tile_index = random.choice(WALL_TILES)
|
||||
else:
|
||||
# Fill interior with floor tiles
|
||||
tile_index = random.choice(FLOOR_TILES)
|
||||
|
||||
# Set the tile at this position
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = tile_index
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Create a player entity at position (4, 4)
|
||||
player = mcrfpy.Entity(
|
||||
(4, 4), # Entity positions are tile coordinates
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Use the first sprite in the texture
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||
|
||||
# Movement state tracking
|
||||
is_moving = False
|
||||
move_animations = [] # Track active animations
|
||||
|
||||
# Animation completion callback
|
||||
def movement_complete(runtime):
|
||||
"""Called when movement animation completes"""
|
||||
global is_moving
|
||||
is_moving = False
|
||||
# Ensure grid is centered on final position
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||
|
||||
motion_speed = 0.30 # seconds per tile
|
||||
# Define keyboard handler
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input to move the player"""
|
||||
global is_moving, move_animations
|
||||
|
||||
if state == "start" and not is_moving: # Only respond to key press when not moving
|
||||
# Get current player position in grid coordinates
|
||||
px, py = player.x, player.y
|
||||
new_x, new_y = px, py
|
||||
|
||||
# Calculate new position based on key press
|
||||
if key == "W" or key == "Up":
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
new_x += 1
|
||||
|
||||
# If position changed, start movement animation
|
||||
if new_x != px or new_y != py:
|
||||
is_moving = True
|
||||
|
||||
# Create animations for player position
|
||||
anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad")
|
||||
anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
||||
anim_x.start(player)
|
||||
anim_y.start(player)
|
||||
|
||||
# Animate grid center to follow player
|
||||
center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
||||
center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
||||
center_x.start(grid)
|
||||
center_y.start(grid)
|
||||
|
||||
# Set a timer to mark movement as complete
|
||||
mcrfpy.setTimer("move_complete", movement_complete, 500)
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add a title caption
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 2",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
# Add instructions
|
||||
instructions = mcrfpy.Caption((150, 750),
|
||||
text="Smooth movement! Each step takes 0.5 seconds.",
|
||||
)
|
||||
instructions.font_size=18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
print("Tutorial Part 2 loaded!")
|
||||
print(f"Player entity created at grid position (4, 4)")
|
||||
print("Movement is now animated over 0.5 seconds per tile!")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 2: Enhanced with Single Move Queue
|
||||
|
||||
This tutorial builds on Part 2 by adding:
|
||||
- Single queued move system for responsive input
|
||||
- Debug display showing position and queue status
|
||||
- Smooth continuous movement when keys are held
|
||||
- Animation callbacks to prevent race conditions
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Create and activate a new scene
|
||||
mcrfpy.createScene("tutorial")
|
||||
mcrfpy.setScene("tutorial")
|
||||
|
||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Load the hero sprite texture (32x32 sprite sheet)
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||
|
||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||
|
||||
# calculating the size in pixels to fit the entire grid on-screen
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size, # height and width on screen
|
||||
)
|
||||
|
||||
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Fill the grid with a simple pattern
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
# Create walls around the edges
|
||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||
tile_index = random.choice(WALL_TILES)
|
||||
else:
|
||||
# Fill interior with floor tiles
|
||||
tile_index = random.choice(FLOOR_TILES)
|
||||
|
||||
# Set the tile at this position
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = tile_index
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Create a player entity at position (4, 4)
|
||||
player = mcrfpy.Entity(
|
||||
(4, 4), # Entity positions are tile coordinates
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Use the first sprite in the texture
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||
|
||||
# Movement state tracking
|
||||
is_moving = False
|
||||
move_queue = [] # List to store queued moves (max 1 item)
|
||||
#last_position = (4, 4) # Track last position
|
||||
current_destination = None # Track where we're currently moving to
|
||||
current_move = None # Track current move direction
|
||||
|
||||
# Store animation references
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
grid_anim_x = None
|
||||
grid_anim_y = None
|
||||
|
||||
# Debug display caption
|
||||
debug_caption = mcrfpy.Caption((10, 40),
|
||||
text="Last: (4, 4) | Queue: 0 | Dest: None",
|
||||
)
|
||||
debug_caption.font_size = 16
|
||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
||||
|
||||
# Additional debug caption for movement state
|
||||
move_debug_caption = mcrfpy.Caption((10, 60),
|
||||
text="Moving: False | Current: None | Queued: None",
|
||||
)
|
||||
move_debug_caption.font_size = 16
|
||||
move_debug_caption.fill_color = mcrfpy.Color(255, 200, 0, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(move_debug_caption)
|
||||
|
||||
def key_to_direction(key):
|
||||
"""Convert key to direction string"""
|
||||
if key == "W" or key == "Up":
|
||||
return "Up"
|
||||
elif key == "S" or key == "Down":
|
||||
return "Down"
|
||||
elif key == "A" or key == "Left":
|
||||
return "Left"
|
||||
elif key == "D" or key == "Right":
|
||||
return "Right"
|
||||
return None
|
||||
|
||||
def update_debug_display():
|
||||
"""Update the debug caption with current state"""
|
||||
queue_count = len(move_queue)
|
||||
dest_text = f"({current_destination[0]}, {current_destination[1]})" if current_destination else "None"
|
||||
debug_caption.text = f"Last: ({player.x}, {player.y}) | Queue: {queue_count} | Dest: {dest_text}"
|
||||
|
||||
# Update movement state debug
|
||||
current_dir = key_to_direction(current_move) if current_move else "None"
|
||||
queued_dir = key_to_direction(move_queue[0]) if move_queue else "None"
|
||||
move_debug_caption.text = f"Moving: {is_moving} | Current: {current_dir} | Queued: {queued_dir}"
|
||||
|
||||
# Animation completion callback
|
||||
def movement_complete(anim, target):
|
||||
"""Called when movement animation completes"""
|
||||
global is_moving, move_queue, current_destination, current_move
|
||||
global player_anim_x, player_anim_y
|
||||
print(f"In callback for animation: {anim=} {target=}")
|
||||
# Clear movement state
|
||||
is_moving = False
|
||||
current_move = None
|
||||
current_destination = None
|
||||
# Clear animation references
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
|
||||
# Update last position to where we actually are now
|
||||
#last_position = (int(player.x), int(player.y))
|
||||
|
||||
# Ensure grid is centered on final position
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||
|
||||
# Check if there's a queued move
|
||||
if move_queue:
|
||||
# Pop the next move from the queue
|
||||
next_move = move_queue.pop(0)
|
||||
print(f"Processing queued move: {next_move}")
|
||||
# Process it like a fresh input
|
||||
process_move(next_move)
|
||||
|
||||
update_debug_display()
|
||||
|
||||
motion_speed = 0.30 # seconds per tile
|
||||
|
||||
def process_move(key):
|
||||
"""Process a move based on the key"""
|
||||
global is_moving, current_move, current_destination, move_queue
|
||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
||||
|
||||
# If already moving, just update the queue
|
||||
if is_moving:
|
||||
print(f"process_move processing {key=} as a queued move (is_moving = True)")
|
||||
# Clear queue and add new move (only keep 1 queued move)
|
||||
move_queue.clear()
|
||||
move_queue.append(key)
|
||||
update_debug_display()
|
||||
return
|
||||
print(f"process_move processing {key=} as a new, immediate animation (is_moving = False)")
|
||||
# Calculate new position from current position
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
# Calculate new position based on key press (only one tile movement)
|
||||
if key == "W" or key == "Up":
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
new_x += 1
|
||||
|
||||
# Start the move if position changed
|
||||
if new_x != px or new_y != py:
|
||||
is_moving = True
|
||||
current_move = key
|
||||
current_destination = (new_x, new_y)
|
||||
# only animate a single axis, same callback from either
|
||||
if new_x != px:
|
||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
||||
player_anim_x.start(player)
|
||||
elif new_y != py:
|
||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad", callback=movement_complete)
|
||||
player_anim_y.start(player)
|
||||
|
||||
# Animate grid center to follow player
|
||||
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
||||
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
||||
grid_anim_x.start(grid)
|
||||
grid_anim_y.start(grid)
|
||||
|
||||
update_debug_display()
|
||||
|
||||
# Define keyboard handler
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input to move the player"""
|
||||
if state == "start":
|
||||
# Only process movement keys
|
||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
||||
print(f"handle_keys producing actual input: {key=}")
|
||||
process_move(key)
|
||||
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add a title caption
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 2 Enhanced",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
# Add instructions
|
||||
instructions = mcrfpy.Caption((150, 750),
|
||||
text="One-move queue system with animation callbacks!",
|
||||
)
|
||||
instructions.font_size=18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
print("Tutorial Part 2 Enhanced loaded!")
|
||||
print(f"Player entity created at grid position (4, 4)")
|
||||
print("Movement now uses animation callbacks to prevent race conditions!")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 2: Animated Movement
|
||||
|
||||
This tutorial builds on Part 1 by adding:
|
||||
- Animation system for smooth movement
|
||||
- Movement that takes 0.5 seconds per tile
|
||||
- Input blocking during movement animation
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Create and activate a new scene
|
||||
mcrfpy.createScene("tutorial")
|
||||
mcrfpy.setScene("tutorial")
|
||||
|
||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Load the hero sprite texture (32x32 sprite sheet)
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||
|
||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||
|
||||
# calculating the size in pixels to fit the entire grid on-screen
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size, # height and width on screen
|
||||
)
|
||||
|
||||
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Fill the grid with a simple pattern
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
# Create walls around the edges
|
||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||
tile_index = random.choice(WALL_TILES)
|
||||
else:
|
||||
# Fill interior with floor tiles
|
||||
tile_index = random.choice(FLOOR_TILES)
|
||||
|
||||
# Set the tile at this position
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = tile_index
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Create a player entity at position (4, 4)
|
||||
player = mcrfpy.Entity(
|
||||
(4, 4), # Entity positions are tile coordinates
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Use the first sprite in the texture
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||
|
||||
# Movement state tracking
|
||||
is_moving = False
|
||||
move_animations = [] # Track active animations
|
||||
|
||||
# Animation completion callback
|
||||
def movement_complete(runtime):
|
||||
"""Called when movement animation completes"""
|
||||
global is_moving
|
||||
is_moving = False
|
||||
# Ensure grid is centered on final position
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||
|
||||
motion_speed = 0.30 # seconds per tile
|
||||
# Define keyboard handler
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input to move the player"""
|
||||
global is_moving, move_animations
|
||||
|
||||
if state == "start" and not is_moving: # Only respond to key press when not moving
|
||||
# Get current player position in grid coordinates
|
||||
px, py = player.x, player.y
|
||||
new_x, new_y = px, py
|
||||
|
||||
# Calculate new position based on key press
|
||||
if key == "W" or key == "Up":
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
new_x += 1
|
||||
|
||||
# If position changed, start movement animation
|
||||
if new_x != px or new_y != py:
|
||||
is_moving = True
|
||||
|
||||
# Create animations for player position
|
||||
anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad")
|
||||
anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
||||
anim_x.start(player)
|
||||
anim_y.start(player)
|
||||
|
||||
# Animate grid center to follow player
|
||||
center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
||||
center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
||||
center_x.start(grid)
|
||||
center_y.start(grid)
|
||||
|
||||
# Set a timer to mark movement as complete
|
||||
mcrfpy.setTimer("move_complete", movement_complete, 500)
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add a title caption
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 2",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
# Add instructions
|
||||
instructions = mcrfpy.Caption((150, 750),
|
||||
"Smooth movement! Each step takes 0.5 seconds.",
|
||||
)
|
||||
instructions.font_size=18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
print("Tutorial Part 2 loaded!")
|
||||
print(f"Player entity created at grid position (4, 4)")
|
||||
print("Movement is now animated over 0.5 seconds per tile!")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -1,6 +1,8 @@
|
|||
#include "Animation.h"
|
||||
#include "UIDrawable.h"
|
||||
#include "UIEntity.h"
|
||||
#include "PyAnimation.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
|
|
@ -9,75 +11,100 @@
|
|||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
// Forward declaration of PyAnimation type
|
||||
namespace mcrfpydef {
|
||||
extern PyTypeObject PyAnimationType;
|
||||
}
|
||||
|
||||
// Animation implementation
|
||||
Animation::Animation(const std::string& targetProperty,
|
||||
const AnimationValue& targetValue,
|
||||
float duration,
|
||||
EasingFunction easingFunc,
|
||||
bool delta)
|
||||
bool delta,
|
||||
PyObject* callback)
|
||||
: targetProperty(targetProperty)
|
||||
, targetValue(targetValue)
|
||||
, duration(duration)
|
||||
, easingFunc(easingFunc)
|
||||
, delta(delta)
|
||||
, pythonCallback(callback)
|
||||
{
|
||||
// Increase reference count for Python callback
|
||||
if (pythonCallback) {
|
||||
Py_INCREF(pythonCallback);
|
||||
}
|
||||
}
|
||||
|
||||
void Animation::start(UIDrawable* target) {
|
||||
currentTarget = target;
|
||||
Animation::~Animation() {
|
||||
// Decrease reference count for Python callback if we still own it
|
||||
PyObject* callback = pythonCallback;
|
||||
if (callback) {
|
||||
pythonCallback = nullptr;
|
||||
|
||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||
Py_DECREF(callback);
|
||||
PyGILState_Release(gstate);
|
||||
}
|
||||
}
|
||||
|
||||
void Animation::start(std::shared_ptr<UIDrawable> target) {
|
||||
if (!target) return;
|
||||
|
||||
targetWeak = target;
|
||||
elapsed = 0.0f;
|
||||
callbackTriggered = false; // Reset callback state
|
||||
|
||||
// Capture startValue from target based on targetProperty
|
||||
if (!currentTarget) return;
|
||||
|
||||
// Try to get the current value based on the expected type
|
||||
std::visit([this](const auto& targetVal) {
|
||||
// Capture start value from target
|
||||
std::visit([this, &target](const auto& targetVal) {
|
||||
using T = std::decay_t<decltype(targetVal)>;
|
||||
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
float value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
if (target->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
int value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
if (target->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::vector<int>>) {
|
||||
// For sprite animation, get current sprite index
|
||||
int value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
if (target->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
||||
sf::Color value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
if (target->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
||||
sf::Vector2f value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
if (target->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>) {
|
||||
std::string value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
if (target->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
}, targetValue);
|
||||
}
|
||||
|
||||
void Animation::startEntity(UIEntity* target) {
|
||||
currentEntityTarget = target;
|
||||
currentTarget = nullptr; // Clear drawable target
|
||||
void Animation::startEntity(std::shared_ptr<UIEntity> target) {
|
||||
if (!target) return;
|
||||
|
||||
entityTargetWeak = target;
|
||||
elapsed = 0.0f;
|
||||
callbackTriggered = false; // Reset callback state
|
||||
|
||||
// Capture the starting value from the entity
|
||||
std::visit([this, target](const auto& val) {
|
||||
|
|
@ -99,8 +126,49 @@ void Animation::startEntity(UIEntity* target) {
|
|||
}, targetValue);
|
||||
}
|
||||
|
||||
bool Animation::hasValidTarget() const {
|
||||
return !targetWeak.expired() || !entityTargetWeak.expired();
|
||||
}
|
||||
|
||||
void Animation::clearCallback() {
|
||||
// Safely clear the callback when PyAnimation is being destroyed
|
||||
PyObject* callback = pythonCallback;
|
||||
if (callback) {
|
||||
pythonCallback = nullptr;
|
||||
callbackTriggered = true; // Prevent future triggering
|
||||
|
||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||
Py_DECREF(callback);
|
||||
PyGILState_Release(gstate);
|
||||
}
|
||||
}
|
||||
|
||||
void Animation::complete() {
|
||||
// Jump to end of animation
|
||||
elapsed = duration;
|
||||
|
||||
// Apply final value
|
||||
if (auto target = targetWeak.lock()) {
|
||||
AnimationValue finalValue = interpolate(1.0f);
|
||||
applyValue(target.get(), finalValue);
|
||||
}
|
||||
else if (auto entity = entityTargetWeak.lock()) {
|
||||
AnimationValue finalValue = interpolate(1.0f);
|
||||
applyValue(entity.get(), finalValue);
|
||||
}
|
||||
}
|
||||
|
||||
bool Animation::update(float deltaTime) {
|
||||
if ((!currentTarget && !currentEntityTarget) || isComplete()) {
|
||||
// Try to lock weak_ptr to get shared_ptr
|
||||
std::shared_ptr<UIDrawable> target = targetWeak.lock();
|
||||
std::shared_ptr<UIEntity> entity = entityTargetWeak.lock();
|
||||
|
||||
// If both are null, target was destroyed
|
||||
if (!target && !entity) {
|
||||
return false; // Remove this animation
|
||||
}
|
||||
|
||||
if (isComplete()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -114,39 +182,18 @@ bool Animation::update(float deltaTime) {
|
|||
// Get interpolated value
|
||||
AnimationValue currentValue = interpolate(easedT);
|
||||
|
||||
// Apply currentValue to target (either drawable or entity)
|
||||
std::visit([this](const auto& value) {
|
||||
using T = std::decay_t<decltype(value)>;
|
||||
|
||||
if (currentTarget) {
|
||||
// Handle UIDrawable targets
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
}
|
||||
else if (currentEntityTarget) {
|
||||
// Handle UIEntity targets
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
currentEntityTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
currentEntityTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
// Entities don't support other types yet
|
||||
}
|
||||
}, currentValue);
|
||||
// Apply to whichever target is valid
|
||||
if (target) {
|
||||
applyValue(target.get(), currentValue);
|
||||
} else if (entity) {
|
||||
applyValue(entity.get(), currentValue);
|
||||
}
|
||||
|
||||
// Trigger callback when animation completes
|
||||
// Check pythonCallback again in case it was cleared during update
|
||||
if (isComplete() && !callbackTriggered && pythonCallback) {
|
||||
triggerCallback();
|
||||
}
|
||||
|
||||
return !isComplete();
|
||||
}
|
||||
|
|
@ -254,6 +301,77 @@ AnimationValue Animation::interpolate(float t) const {
|
|||
}, targetValue);
|
||||
}
|
||||
|
||||
void Animation::applyValue(UIDrawable* target, const AnimationValue& value) {
|
||||
if (!target) return;
|
||||
|
||||
std::visit([this, target](const auto& val) {
|
||||
using T = std::decay_t<decltype(val)>;
|
||||
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
target->setProperty(targetProperty, val);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
target->setProperty(targetProperty, val);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
||||
target->setProperty(targetProperty, val);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
||||
target->setProperty(targetProperty, val);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>) {
|
||||
target->setProperty(targetProperty, val);
|
||||
}
|
||||
}, value);
|
||||
}
|
||||
|
||||
void Animation::applyValue(UIEntity* entity, const AnimationValue& value) {
|
||||
if (!entity) return;
|
||||
|
||||
std::visit([this, entity](const auto& val) {
|
||||
using T = std::decay_t<decltype(val)>;
|
||||
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
entity->setProperty(targetProperty, val);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
entity->setProperty(targetProperty, val);
|
||||
}
|
||||
// Entities don't support other types yet
|
||||
}, value);
|
||||
}
|
||||
|
||||
void Animation::triggerCallback() {
|
||||
if (!pythonCallback) return;
|
||||
|
||||
// Ensure we only trigger once
|
||||
if (callbackTriggered) return;
|
||||
callbackTriggered = true;
|
||||
|
||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||
|
||||
// TODO: In future, create PyAnimation wrapper for this animation
|
||||
// For now, pass None for both parameters
|
||||
PyObject* args = PyTuple_New(2);
|
||||
Py_INCREF(Py_None);
|
||||
Py_INCREF(Py_None);
|
||||
PyTuple_SetItem(args, 0, Py_None); // animation parameter
|
||||
PyTuple_SetItem(args, 1, Py_None); // target parameter
|
||||
|
||||
PyObject* result = PyObject_CallObject(pythonCallback, args);
|
||||
Py_DECREF(args);
|
||||
|
||||
if (!result) {
|
||||
// Print error but don't crash
|
||||
PyErr_Print();
|
||||
PyErr_Clear(); // Clear the error state
|
||||
} else {
|
||||
Py_DECREF(result);
|
||||
}
|
||||
|
||||
PyGILState_Release(gstate);
|
||||
}
|
||||
|
||||
// Easing functions implementation
|
||||
namespace EasingFunctions {
|
||||
|
||||
|
|
@ -502,26 +620,50 @@ AnimationManager& AnimationManager::getInstance() {
|
|||
}
|
||||
|
||||
void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) {
|
||||
activeAnimations.push_back(animation);
|
||||
if (animation && animation->hasValidTarget()) {
|
||||
if (isUpdating) {
|
||||
// Defer adding during update to avoid iterator invalidation
|
||||
pendingAnimations.push_back(animation);
|
||||
} else {
|
||||
activeAnimations.push_back(animation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationManager::update(float deltaTime) {
|
||||
for (auto& anim : activeAnimations) {
|
||||
anim->update(deltaTime);
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
void AnimationManager::cleanup() {
|
||||
// Set flag to defer new animations
|
||||
isUpdating = true;
|
||||
|
||||
// Remove completed or invalid animations
|
||||
activeAnimations.erase(
|
||||
std::remove_if(activeAnimations.begin(), activeAnimations.end(),
|
||||
[](const std::shared_ptr<Animation>& anim) {
|
||||
return anim->isComplete();
|
||||
[deltaTime](std::shared_ptr<Animation>& anim) {
|
||||
return !anim || !anim->update(deltaTime);
|
||||
}),
|
||||
activeAnimations.end()
|
||||
);
|
||||
|
||||
// Clear update flag
|
||||
isUpdating = false;
|
||||
|
||||
// Add any animations that were created during update
|
||||
if (!pendingAnimations.empty()) {
|
||||
activeAnimations.insert(activeAnimations.end(),
|
||||
pendingAnimations.begin(),
|
||||
pendingAnimations.end());
|
||||
pendingAnimations.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationManager::clear() {
|
||||
|
||||
void AnimationManager::clear(bool completeAnimations) {
|
||||
if (completeAnimations) {
|
||||
// Complete all animations before clearing
|
||||
for (auto& anim : activeAnimations) {
|
||||
if (anim) {
|
||||
anim->complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
activeAnimations.clear();
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
#include <variant>
|
||||
#include <vector>
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include "Python.h"
|
||||
|
||||
// Forward declarations
|
||||
class UIDrawable;
|
||||
|
|
@ -36,13 +37,20 @@ public:
|
|||
const AnimationValue& targetValue,
|
||||
float duration,
|
||||
EasingFunction easingFunc = EasingFunctions::linear,
|
||||
bool delta = false);
|
||||
bool delta = false,
|
||||
PyObject* callback = nullptr);
|
||||
|
||||
// Destructor - cleanup Python callback reference
|
||||
~Animation();
|
||||
|
||||
// Apply this animation to a drawable
|
||||
void start(UIDrawable* target);
|
||||
void start(std::shared_ptr<UIDrawable> target);
|
||||
|
||||
// Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable)
|
||||
void startEntity(UIEntity* target);
|
||||
void startEntity(std::shared_ptr<UIEntity> target);
|
||||
|
||||
// Complete the animation immediately (jump to final value)
|
||||
void complete();
|
||||
|
||||
// Update animation (called each frame)
|
||||
// Returns true if animation is still running, false if complete
|
||||
|
|
@ -51,6 +59,12 @@ public:
|
|||
// Get current interpolated value
|
||||
AnimationValue getCurrentValue() const;
|
||||
|
||||
// Check if animation has valid target
|
||||
bool hasValidTarget() const;
|
||||
|
||||
// Clear the callback (called when PyAnimation is deallocated)
|
||||
void clearCallback();
|
||||
|
||||
// Animation properties
|
||||
std::string getTargetProperty() const { return targetProperty; }
|
||||
float getDuration() const { return duration; }
|
||||
|
|
@ -67,11 +81,24 @@ private:
|
|||
EasingFunction easingFunc; // Easing function to use
|
||||
bool delta; // If true, targetValue is relative to start
|
||||
|
||||
UIDrawable* currentTarget = nullptr; // Current target being animated
|
||||
UIEntity* currentEntityTarget = nullptr; // Current entity target (alternative to drawable)
|
||||
// RAII: Use weak_ptr for safe target tracking
|
||||
std::weak_ptr<UIDrawable> targetWeak;
|
||||
std::weak_ptr<UIEntity> entityTargetWeak;
|
||||
|
||||
// Callback support
|
||||
PyObject* pythonCallback = nullptr; // Python callback function (we own a reference)
|
||||
bool callbackTriggered = false; // Ensure callback only fires once
|
||||
PyObject* pyAnimationWrapper = nullptr; // Weak reference to PyAnimation if created from Python
|
||||
|
||||
// Helper to interpolate between values
|
||||
AnimationValue interpolate(float t) const;
|
||||
|
||||
// Helper to apply value to target
|
||||
void applyValue(UIDrawable* target, const AnimationValue& value);
|
||||
void applyValue(UIEntity* entity, const AnimationValue& value);
|
||||
|
||||
// Trigger callback when animation completes
|
||||
void triggerCallback();
|
||||
};
|
||||
|
||||
// Easing functions library
|
||||
|
|
@ -134,13 +161,12 @@ public:
|
|||
// Update all animations
|
||||
void update(float deltaTime);
|
||||
|
||||
// Remove completed animations
|
||||
void cleanup();
|
||||
|
||||
// Clear all animations
|
||||
void clear();
|
||||
// Clear all animations (optionally completing them first)
|
||||
void clear(bool completeAnimations = false);
|
||||
|
||||
private:
|
||||
AnimationManager() = default;
|
||||
std::vector<std::shared_ptr<Animation>> activeAnimations;
|
||||
std::vector<std::shared_ptr<Animation>> pendingAnimations; // Animations to add after update
|
||||
bool isUpdating = false; // Flag to track if we're in update loop
|
||||
};
|
||||
|
|
@ -16,7 +16,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
|||
{
|
||||
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
|
||||
Resources::game = this;
|
||||
window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine";
|
||||
window_title = "McRogueFace Engine";
|
||||
|
||||
// Initialize rendering based on headless mode
|
||||
if (headless) {
|
||||
|
|
@ -91,6 +91,9 @@ void GameEngine::cleanup()
|
|||
if (cleaned_up) return;
|
||||
cleaned_up = true;
|
||||
|
||||
// Clear all animations first (RAII handles invalidation)
|
||||
AnimationManager::getInstance().clear();
|
||||
|
||||
// Clear Python references before destroying C++ objects
|
||||
// Clear all timers (they hold Python callables)
|
||||
timers.clear();
|
||||
|
|
@ -182,7 +185,7 @@ void GameEngine::setWindowScale(float multiplier)
|
|||
|
||||
void GameEngine::run()
|
||||
{
|
||||
std::cout << "GameEngine::run() starting main loop..." << std::endl;
|
||||
//std::cout << "GameEngine::run() starting main loop..." << std::endl;
|
||||
float fps = 0.0;
|
||||
frameTime = 0.016f; // Initialize to ~60 FPS
|
||||
clock.restart();
|
||||
|
|
@ -259,7 +262,7 @@ void GameEngine::run()
|
|||
int tenth_fps = (metrics.fps * 10) % 10;
|
||||
|
||||
if (!headless && window) {
|
||||
window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
|
||||
window->setTitle(window_title);
|
||||
}
|
||||
|
||||
// In windowed mode, check if window was closed
|
||||
|
|
|
|||
|
|
@ -18,19 +18,31 @@ PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds
|
|||
}
|
||||
|
||||
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", nullptr};
|
||||
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr};
|
||||
|
||||
const char* property_name;
|
||||
PyObject* target_value;
|
||||
float duration;
|
||||
const char* easing_name = "linear";
|
||||
int delta = 0;
|
||||
PyObject* callback = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|sp", const_cast<char**>(keywords),
|
||||
&property_name, &target_value, &duration, &easing_name, &delta)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast<char**>(keywords),
|
||||
&property_name, &target_value, &duration, &easing_name, &delta, &callback)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate callback is callable if provided
|
||||
if (callback && callback != Py_None && !PyCallable_Check(callback)) {
|
||||
PyErr_SetString(PyExc_TypeError, "callback must be callable");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Convert None to nullptr for C++
|
||||
if (callback == Py_None) {
|
||||
callback = nullptr;
|
||||
}
|
||||
|
||||
// Convert Python target value to AnimationValue
|
||||
AnimationValue animValue;
|
||||
|
||||
|
|
@ -90,7 +102,7 @@ int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
|
|||
EasingFunction easingFunc = EasingFunctions::getByName(easing_name);
|
||||
|
||||
// Create the Animation
|
||||
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0);
|
||||
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -126,50 +138,50 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// Get the UIDrawable from the Python object
|
||||
UIDrawable* drawable = nullptr;
|
||||
|
||||
// Check type by comparing type names
|
||||
const char* type_name = Py_TYPE(target_obj)->tp_name;
|
||||
|
||||
if (strcmp(type_name, "mcrfpy.Frame") == 0) {
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj;
|
||||
drawable = frame->data.get();
|
||||
if (frame->data) {
|
||||
self->data->start(frame->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
}
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
|
||||
drawable = caption->data.get();
|
||||
if (caption->data) {
|
||||
self->data->start(caption->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
}
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
|
||||
drawable = sprite->data.get();
|
||||
if (sprite->data) {
|
||||
self->data->start(sprite->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
}
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
|
||||
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
|
||||
drawable = grid->data.get();
|
||||
if (grid->data) {
|
||||
self->data->start(grid->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
}
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Entity") == 0) {
|
||||
// Special handling for Entity since it doesn't inherit from UIDrawable
|
||||
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
|
||||
// Start the animation directly on the entity
|
||||
self->data->startEntity(entity->data.get());
|
||||
|
||||
// Add to AnimationManager
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
if (entity->data) {
|
||||
self->data->startEntity(entity->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
}
|
||||
}
|
||||
else {
|
||||
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Start the animation
|
||||
self->data->start(drawable);
|
||||
|
||||
// Add to AnimationManager
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
|
@ -214,6 +226,20 @@ PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args
|
|||
}, value);
|
||||
}
|
||||
|
||||
PyObject* PyAnimation::complete(PyAnimationObject* self, PyObject* args) {
|
||||
if (self->data) {
|
||||
self->data->complete();
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args) {
|
||||
if (self->data && self->data->hasValidTarget()) {
|
||||
Py_RETURN_TRUE;
|
||||
}
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
PyGetSetDef PyAnimation::getsetters[] = {
|
||||
{"property", (getter)get_property, NULL, "Target property name", NULL},
|
||||
{"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL},
|
||||
|
|
@ -225,10 +251,23 @@ PyGetSetDef PyAnimation::getsetters[] = {
|
|||
|
||||
PyMethodDef PyAnimation::methods[] = {
|
||||
{"start", (PyCFunction)start, METH_VARARGS,
|
||||
"Start the animation on a target UIDrawable"},
|
||||
"start(target) -> None\n\n"
|
||||
"Start the animation on a target UI element.\n\n"
|
||||
"Args:\n"
|
||||
" target: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)\n\n"
|
||||
"Note:\n"
|
||||
" The animation will automatically stop if the target is destroyed."},
|
||||
{"update", (PyCFunction)update, METH_VARARGS,
|
||||
"Update the animation by deltaTime (returns True if still running)"},
|
||||
{"get_current_value", (PyCFunction)get_current_value, METH_NOARGS,
|
||||
"Get the current interpolated value"},
|
||||
{"complete", (PyCFunction)complete, METH_NOARGS,
|
||||
"complete() -> None\n\n"
|
||||
"Complete the animation immediately by jumping to the final value."},
|
||||
{"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS,
|
||||
"hasValidTarget() -> bool\n\n"
|
||||
"Check if the animation still has a valid target.\n\n"
|
||||
"Returns:\n"
|
||||
" True if the target still exists, False if it was destroyed."},
|
||||
{NULL}
|
||||
};
|
||||
|
|
@ -28,6 +28,8 @@ public:
|
|||
static PyObject* start(PyAnimationObject* self, PyObject* args);
|
||||
static PyObject* update(PyAnimationObject* self, PyObject* args);
|
||||
static PyObject* get_current_value(PyAnimationObject* self, PyObject* args);
|
||||
static PyObject* complete(PyAnimationObject* self, PyObject* args);
|
||||
static PyObject* has_valid_target(PyAnimationObject* self, PyObject* args);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyMethodDef methods[];
|
||||
|
|
|
|||
|
|
@ -1,410 +0,0 @@
|
|||
#pragma once
|
||||
#include "Python.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyColor.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <string>
|
||||
|
||||
// Unified argument parsing helpers for Python API consistency
|
||||
namespace PyArgHelpers {
|
||||
|
||||
// Position in pixels (float)
|
||||
struct PositionResult {
|
||||
float x, y;
|
||||
bool valid;
|
||||
const char* error;
|
||||
};
|
||||
|
||||
// Size in pixels (float)
|
||||
struct SizeResult {
|
||||
float w, h;
|
||||
bool valid;
|
||||
const char* error;
|
||||
};
|
||||
|
||||
// Grid position in tiles (float - for animation)
|
||||
struct GridPositionResult {
|
||||
float grid_x, grid_y;
|
||||
bool valid;
|
||||
const char* error;
|
||||
};
|
||||
|
||||
// Grid size in tiles (int - can't have fractional tiles)
|
||||
struct GridSizeResult {
|
||||
int grid_w, grid_h;
|
||||
bool valid;
|
||||
const char* error;
|
||||
};
|
||||
|
||||
// Color parsing
|
||||
struct ColorResult {
|
||||
sf::Color color;
|
||||
bool valid;
|
||||
const char* error;
|
||||
};
|
||||
|
||||
// Helper to check if a keyword conflicts with positional args
|
||||
static bool hasConflict(PyObject* kwds, const char* key, bool has_positional) {
|
||||
if (!kwds || !has_positional) return false;
|
||||
PyObject* value = PyDict_GetItemString(kwds, key);
|
||||
return value != nullptr;
|
||||
}
|
||||
|
||||
// Parse position with conflict detection
|
||||
static PositionResult parsePosition(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
|
||||
PositionResult result = {0.0f, 0.0f, false, nullptr};
|
||||
int start_idx = next_arg ? *next_arg : 0;
|
||||
bool has_positional = false;
|
||||
|
||||
// Check for positional tuple argument first
|
||||
if (args && PyTuple_Size(args) > start_idx) {
|
||||
PyObject* first = PyTuple_GetItem(args, start_idx);
|
||||
|
||||
// Is it a tuple/Vector?
|
||||
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
|
||||
// Extract from tuple
|
||||
PyObject* x_obj = PyTuple_GetItem(first, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(first, 1);
|
||||
|
||||
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
|
||||
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
|
||||
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
|
||||
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
|
||||
result.valid = true;
|
||||
has_positional = true;
|
||||
if (next_arg) (*next_arg)++;
|
||||
}
|
||||
} else if (PyObject_TypeCheck(first, (PyTypeObject*)PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
||||
// It's a Vector object
|
||||
PyVectorObject* vec = (PyVectorObject*)first;
|
||||
result.x = vec->data.x;
|
||||
result.y = vec->data.y;
|
||||
result.valid = true;
|
||||
has_positional = true;
|
||||
if (next_arg) (*next_arg)++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for keyword conflicts
|
||||
if (has_positional) {
|
||||
if (hasConflict(kwds, "pos", true) || hasConflict(kwds, "x", true) || hasConflict(kwds, "y", true)) {
|
||||
result.valid = false;
|
||||
result.error = "position specified both positionally and by keyword";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// If no positional, try keywords
|
||||
if (!has_positional && kwds) {
|
||||
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
|
||||
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
|
||||
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
|
||||
|
||||
// Check for conflicts between pos and x/y
|
||||
if (pos_obj && (x_obj || y_obj)) {
|
||||
result.valid = false;
|
||||
result.error = "pos and x/y cannot both be specified";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (pos_obj) {
|
||||
// Parse pos keyword
|
||||
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
||||
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
||||
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
||||
|
||||
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
result.x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
result.y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
result.valid = true;
|
||||
}
|
||||
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
||||
PyVectorObject* vec = (PyVectorObject*)pos_obj;
|
||||
result.x = vec->data.x;
|
||||
result.y = vec->data.y;
|
||||
result.valid = true;
|
||||
}
|
||||
} else if (x_obj && y_obj) {
|
||||
// Parse x, y keywords
|
||||
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
|
||||
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
|
||||
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
|
||||
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
|
||||
result.valid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse size with conflict detection
|
||||
static SizeResult parseSize(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
|
||||
SizeResult result = {0.0f, 0.0f, false, nullptr};
|
||||
int start_idx = next_arg ? *next_arg : 0;
|
||||
bool has_positional = false;
|
||||
|
||||
// Check for positional tuple argument
|
||||
if (args && PyTuple_Size(args) > start_idx) {
|
||||
PyObject* first = PyTuple_GetItem(args, start_idx);
|
||||
|
||||
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
|
||||
PyObject* w_obj = PyTuple_GetItem(first, 0);
|
||||
PyObject* h_obj = PyTuple_GetItem(first, 1);
|
||||
|
||||
if ((PyFloat_Check(w_obj) || PyLong_Check(w_obj)) &&
|
||||
(PyFloat_Check(h_obj) || PyLong_Check(h_obj))) {
|
||||
result.w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : PyLong_AsLong(w_obj);
|
||||
result.h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : PyLong_AsLong(h_obj);
|
||||
result.valid = true;
|
||||
has_positional = true;
|
||||
if (next_arg) (*next_arg)++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for keyword conflicts
|
||||
if (has_positional) {
|
||||
if (hasConflict(kwds, "size", true) || hasConflict(kwds, "w", true) || hasConflict(kwds, "h", true)) {
|
||||
result.valid = false;
|
||||
result.error = "size specified both positionally and by keyword";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// If no positional, try keywords
|
||||
if (!has_positional && kwds) {
|
||||
PyObject* size_obj = PyDict_GetItemString(kwds, "size");
|
||||
PyObject* w_obj = PyDict_GetItemString(kwds, "w");
|
||||
PyObject* h_obj = PyDict_GetItemString(kwds, "h");
|
||||
|
||||
// Check for conflicts between size and w/h
|
||||
if (size_obj && (w_obj || h_obj)) {
|
||||
result.valid = false;
|
||||
result.error = "size and w/h cannot both be specified";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (size_obj) {
|
||||
// Parse size keyword
|
||||
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
||||
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
||||
|
||||
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
|
||||
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
||||
result.w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
|
||||
result.h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
||||
result.valid = true;
|
||||
}
|
||||
}
|
||||
} else if (w_obj && h_obj) {
|
||||
// Parse w, h keywords
|
||||
if ((PyFloat_Check(w_obj) || PyLong_Check(w_obj)) &&
|
||||
(PyFloat_Check(h_obj) || PyLong_Check(h_obj))) {
|
||||
result.w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : PyLong_AsLong(w_obj);
|
||||
result.h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : PyLong_AsLong(h_obj);
|
||||
result.valid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse grid position (float for smooth animation)
|
||||
static GridPositionResult parseGridPosition(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
|
||||
GridPositionResult result = {0.0f, 0.0f, false, nullptr};
|
||||
int start_idx = next_arg ? *next_arg : 0;
|
||||
bool has_positional = false;
|
||||
|
||||
// Check for positional tuple argument
|
||||
if (args && PyTuple_Size(args) > start_idx) {
|
||||
PyObject* first = PyTuple_GetItem(args, start_idx);
|
||||
|
||||
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
|
||||
PyObject* x_obj = PyTuple_GetItem(first, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(first, 1);
|
||||
|
||||
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
|
||||
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
|
||||
result.grid_x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
|
||||
result.grid_y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
|
||||
result.valid = true;
|
||||
has_positional = true;
|
||||
if (next_arg) (*next_arg)++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for keyword conflicts
|
||||
if (has_positional) {
|
||||
if (hasConflict(kwds, "grid_pos", true) || hasConflict(kwds, "grid_x", true) || hasConflict(kwds, "grid_y", true)) {
|
||||
result.valid = false;
|
||||
result.error = "grid position specified both positionally and by keyword";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// If no positional, try keywords
|
||||
if (!has_positional && kwds) {
|
||||
PyObject* grid_pos_obj = PyDict_GetItemString(kwds, "grid_pos");
|
||||
PyObject* grid_x_obj = PyDict_GetItemString(kwds, "grid_x");
|
||||
PyObject* grid_y_obj = PyDict_GetItemString(kwds, "grid_y");
|
||||
|
||||
// Check for conflicts between grid_pos and grid_x/grid_y
|
||||
if (grid_pos_obj && (grid_x_obj || grid_y_obj)) {
|
||||
result.valid = false;
|
||||
result.error = "grid_pos and grid_x/grid_y cannot both be specified";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (grid_pos_obj) {
|
||||
// Parse grid_pos keyword
|
||||
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
|
||||
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
|
||||
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
|
||||
|
||||
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
result.grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
result.grid_y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
result.valid = true;
|
||||
}
|
||||
}
|
||||
} else if (grid_x_obj && grid_y_obj) {
|
||||
// Parse grid_x, grid_y keywords
|
||||
if ((PyFloat_Check(grid_x_obj) || PyLong_Check(grid_x_obj)) &&
|
||||
(PyFloat_Check(grid_y_obj) || PyLong_Check(grid_y_obj))) {
|
||||
result.grid_x = PyFloat_Check(grid_x_obj) ? PyFloat_AsDouble(grid_x_obj) : PyLong_AsLong(grid_x_obj);
|
||||
result.grid_y = PyFloat_Check(grid_y_obj) ? PyFloat_AsDouble(grid_y_obj) : PyLong_AsLong(grid_y_obj);
|
||||
result.valid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse grid size (int - no fractional tiles)
|
||||
static GridSizeResult parseGridSize(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
|
||||
GridSizeResult result = {0, 0, false, nullptr};
|
||||
int start_idx = next_arg ? *next_arg : 0;
|
||||
bool has_positional = false;
|
||||
|
||||
// Check for positional tuple argument
|
||||
if (args && PyTuple_Size(args) > start_idx) {
|
||||
PyObject* first = PyTuple_GetItem(args, start_idx);
|
||||
|
||||
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
|
||||
PyObject* w_obj = PyTuple_GetItem(first, 0);
|
||||
PyObject* h_obj = PyTuple_GetItem(first, 1);
|
||||
|
||||
if (PyLong_Check(w_obj) && PyLong_Check(h_obj)) {
|
||||
result.grid_w = PyLong_AsLong(w_obj);
|
||||
result.grid_h = PyLong_AsLong(h_obj);
|
||||
result.valid = true;
|
||||
has_positional = true;
|
||||
if (next_arg) (*next_arg)++;
|
||||
} else {
|
||||
result.valid = false;
|
||||
result.error = "grid size must be specified with integers";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for keyword conflicts
|
||||
if (has_positional) {
|
||||
if (hasConflict(kwds, "grid_size", true) || hasConflict(kwds, "grid_w", true) || hasConflict(kwds, "grid_h", true)) {
|
||||
result.valid = false;
|
||||
result.error = "grid size specified both positionally and by keyword";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// If no positional, try keywords
|
||||
if (!has_positional && kwds) {
|
||||
PyObject* grid_size_obj = PyDict_GetItemString(kwds, "grid_size");
|
||||
PyObject* grid_w_obj = PyDict_GetItemString(kwds, "grid_w");
|
||||
PyObject* grid_h_obj = PyDict_GetItemString(kwds, "grid_h");
|
||||
|
||||
// Check for conflicts between grid_size and grid_w/grid_h
|
||||
if (grid_size_obj && (grid_w_obj || grid_h_obj)) {
|
||||
result.valid = false;
|
||||
result.error = "grid_size and grid_w/grid_h cannot both be specified";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (grid_size_obj) {
|
||||
// Parse grid_size keyword
|
||||
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
|
||||
PyObject* w_val = PyTuple_GetItem(grid_size_obj, 0);
|
||||
PyObject* h_val = PyTuple_GetItem(grid_size_obj, 1);
|
||||
|
||||
if (PyLong_Check(w_val) && PyLong_Check(h_val)) {
|
||||
result.grid_w = PyLong_AsLong(w_val);
|
||||
result.grid_h = PyLong_AsLong(h_val);
|
||||
result.valid = true;
|
||||
} else {
|
||||
result.valid = false;
|
||||
result.error = "grid size must be specified with integers";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else if (grid_w_obj && grid_h_obj) {
|
||||
// Parse grid_w, grid_h keywords
|
||||
if (PyLong_Check(grid_w_obj) && PyLong_Check(grid_h_obj)) {
|
||||
result.grid_w = PyLong_AsLong(grid_w_obj);
|
||||
result.grid_h = PyLong_AsLong(grid_h_obj);
|
||||
result.valid = true;
|
||||
} else {
|
||||
result.valid = false;
|
||||
result.error = "grid size must be specified with integers";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse color using existing PyColor infrastructure
|
||||
static ColorResult parseColor(PyObject* obj, const char* param_name = nullptr) {
|
||||
ColorResult result = {sf::Color::White, false, nullptr};
|
||||
|
||||
if (!obj) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Use existing PyColor::from_arg which handles tuple/Color conversion
|
||||
auto py_color = PyColor::from_arg(obj);
|
||||
if (py_color) {
|
||||
result.color = py_color->data;
|
||||
result.valid = true;
|
||||
} else {
|
||||
result.valid = false;
|
||||
std::string error_msg = param_name
|
||||
? std::string(param_name) + " must be a color tuple (r,g,b) or (r,g,b,a)"
|
||||
: "Invalid color format - expected tuple (r,g,b) or (r,g,b,a)";
|
||||
result.error = error_msg.c_str();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper to validate a texture object
|
||||
static bool isValidTexture(PyObject* obj) {
|
||||
if (!obj) return false;
|
||||
PyObject* texture_type = PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Texture");
|
||||
bool is_texture = PyObject_IsInstance(obj, texture_type);
|
||||
Py_DECREF(texture_type);
|
||||
return is_texture;
|
||||
}
|
||||
|
||||
// Helper to validate a click handler
|
||||
static bool isValidClickHandler(PyObject* obj) {
|
||||
return obj && PyCallable_Check(obj);
|
||||
}
|
||||
}
|
||||
|
|
@ -31,13 +31,18 @@ void PyScene::do_mouse_input(std::string button, std::string type)
|
|||
// Convert window coordinates to game coordinates using the viewport
|
||||
auto mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos));
|
||||
|
||||
// Create a sorted copy by z-index (highest first)
|
||||
std::vector<std::shared_ptr<UIDrawable>> sorted_elements(*ui_elements);
|
||||
std::sort(sorted_elements.begin(), sorted_elements.end(),
|
||||
[](const auto& a, const auto& b) { return a->z_index > b->z_index; });
|
||||
// Only sort if z_index values have changed
|
||||
if (ui_elements_need_sort) {
|
||||
// Sort in ascending order (same as render)
|
||||
std::sort(ui_elements->begin(), ui_elements->end(),
|
||||
[](const auto& a, const auto& b) { return a->z_index < b->z_index; });
|
||||
ui_elements_need_sort = false;
|
||||
}
|
||||
|
||||
// Check elements in z-order (top to bottom)
|
||||
for (const auto& element : sorted_elements) {
|
||||
// Check elements in reverse z-order (highest z_index first, top to bottom)
|
||||
// Use reverse iterators to go from end to beginning
|
||||
for (auto it = ui_elements->rbegin(); it != ui_elements->rend(); ++it) {
|
||||
const auto& element = *it;
|
||||
if (!element->visible) continue;
|
||||
|
||||
if (auto target = element->click_at(sf::Vector2f(mousepos))) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
#include "PyColor.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyFont.h"
|
||||
#include "PyArgHelpers.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
#include <algorithm>
|
||||
|
||||
|
|
@ -303,183 +302,135 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
|||
{
|
||||
using namespace mcrfpydef;
|
||||
|
||||
// Try parsing with PyArgHelpers
|
||||
int arg_idx = 0;
|
||||
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
|
||||
|
||||
// Default values
|
||||
float x = 0.0f, y = 0.0f, outline = 0.0f;
|
||||
char* text = nullptr;
|
||||
// Define all parameters with defaults
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* font = nullptr;
|
||||
const char* text = "";
|
||||
PyObject* fill_color = nullptr;
|
||||
PyObject* outline_color = nullptr;
|
||||
float outline = 0.0f;
|
||||
float font_size = 16.0f;
|
||||
PyObject* click_handler = nullptr;
|
||||
int visible = 1;
|
||||
float opacity = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f;
|
||||
|
||||
// Case 1: Got position from helpers (tuple format)
|
||||
if (pos_result.valid) {
|
||||
x = pos_result.x;
|
||||
y = pos_result.y;
|
||||
|
||||
// Parse remaining arguments
|
||||
static const char* remaining_keywords[] = {
|
||||
"text", "font", "fill_color", "outline_color", "outline", "click", nullptr
|
||||
};
|
||||
|
||||
// Create new tuple with remaining args
|
||||
Py_ssize_t total_args = PyTuple_Size(args);
|
||||
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|zOOOfO",
|
||||
const_cast<char**>(remaining_keywords),
|
||||
&text, &font, &fill_color, &outline_color,
|
||||
&outline, &click_handler)) {
|
||||
Py_DECREF(remaining_args);
|
||||
if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error);
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
"pos", "font", "text", // Positional args (as per spec)
|
||||
// Keyword-only args
|
||||
"fill_color", "outline_color", "outline", "font_size", "click",
|
||||
"visible", "opacity", "z_index", "name", "x", "y",
|
||||
nullptr
|
||||
};
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizff", const_cast<char**>(kwlist),
|
||||
&pos_obj, &font, &text, // Positional
|
||||
&fill_color, &outline_color, &outline, &font_size, &click_handler,
|
||||
&visible, &opacity, &z_index, &name, &x, &y)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle position argument (can be tuple, Vector, or use x/y keywords)
|
||||
if (pos_obj) {
|
||||
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||
if (vec) {
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
Py_DECREF(vec);
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
||||
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
||||
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
||||
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle font argument
|
||||
std::shared_ptr<PyFont> pyfont = nullptr;
|
||||
if (font && font != Py_None) {
|
||||
if (!PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance");
|
||||
return -1;
|
||||
}
|
||||
Py_DECREF(remaining_args);
|
||||
}
|
||||
// Case 2: Traditional format
|
||||
else {
|
||||
PyErr_Clear(); // Clear any errors from helpers
|
||||
|
||||
// First check if this is the old (text, x, y, ...) format
|
||||
PyObject* first_arg = args && PyTuple_Size(args) > 0 ? PyTuple_GetItem(args, 0) : nullptr;
|
||||
bool text_first = first_arg && PyUnicode_Check(first_arg);
|
||||
|
||||
if (text_first) {
|
||||
// Pattern: (text, x, y, ...)
|
||||
static const char* text_first_keywords[] = {
|
||||
"text", "x", "y", "font", "fill_color", "outline_color",
|
||||
"outline", "click", "pos", nullptr
|
||||
};
|
||||
PyObject* pos_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zffOOOfOO",
|
||||
const_cast<char**>(text_first_keywords),
|
||||
&text, &x, &y, &font, &fill_color, &outline_color,
|
||||
&outline, &click_handler, &pos_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle pos keyword override
|
||||
if (pos_obj && pos_obj != Py_None) {
|
||||
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
||||
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
||||
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
||||
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
}
|
||||
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
|
||||
PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
||||
PyVectorObject* vec = (PyVectorObject*)pos_obj;
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pattern: (x, y, text, ...)
|
||||
static const char* xy_keywords[] = {
|
||||
"x", "y", "text", "font", "fill_color", "outline_color",
|
||||
"outline", "click", "pos", nullptr
|
||||
};
|
||||
PyObject* pos_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOfOO",
|
||||
const_cast<char**>(xy_keywords),
|
||||
&x, &y, &text, &font, &fill_color, &outline_color,
|
||||
&outline, &click_handler, &pos_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle pos keyword override
|
||||
if (pos_obj && pos_obj != Py_None) {
|
||||
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
||||
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
||||
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
||||
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
}
|
||||
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
|
||||
PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
||||
PyVectorObject* vec = (PyVectorObject*)pos_obj;
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto obj = (PyFontObject*)font;
|
||||
pyfont = obj->data;
|
||||
}
|
||||
|
||||
self->data->position = sf::Vector2f(x, y); // Set base class position
|
||||
self->data->text.setPosition(self->data->position); // Sync text position
|
||||
// check types for font, fill_color, outline_color
|
||||
|
||||
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl;
|
||||
if (font != NULL && font != Py_None && !PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font")/*(PyObject*)&PyFontType)*/)){
|
||||
PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance or None");
|
||||
return -1;
|
||||
} else if (font != NULL && font != Py_None)
|
||||
{
|
||||
auto font_obj = (PyFontObject*)font;
|
||||
self->data->text.setFont(font_obj->data->font);
|
||||
self->font = font;
|
||||
Py_INCREF(font);
|
||||
} else
|
||||
{
|
||||
// Create the caption
|
||||
self->data = std::make_shared<UICaption>();
|
||||
self->data->position = sf::Vector2f(x, y);
|
||||
self->data->text.setPosition(self->data->position);
|
||||
self->data->text.setOutlineThickness(outline);
|
||||
|
||||
// Set the font
|
||||
if (pyfont) {
|
||||
self->data->text.setFont(pyfont->font);
|
||||
} else {
|
||||
// Use default font when None or not provided
|
||||
if (McRFPy_API::default_font) {
|
||||
self->data->text.setFont(McRFPy_API::default_font->font);
|
||||
// Store reference to default font
|
||||
PyObject* default_font_obj = PyObject_GetAttrString(McRFPy_API::mcrf_module, "default_font");
|
||||
if (default_font_obj) {
|
||||
self->font = default_font_obj;
|
||||
// Don't need to DECREF since we're storing it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle text - default to empty string if not provided
|
||||
if (text && text != NULL) {
|
||||
self->data->text.setString((std::string)text);
|
||||
} else {
|
||||
self->data->text.setString("");
|
||||
|
||||
// Set character size
|
||||
self->data->text.setCharacterSize(static_cast<unsigned int>(font_size));
|
||||
|
||||
// Set text
|
||||
if (text && strlen(text) > 0) {
|
||||
self->data->text.setString(std::string(text));
|
||||
}
|
||||
self->data->text.setOutlineThickness(outline);
|
||||
if (fill_color) {
|
||||
auto fc = PyColor::from_arg(fill_color);
|
||||
if (!fc) {
|
||||
PyErr_SetString(PyExc_TypeError, "fill_color must be mcrfpy.Color or arguments to mcrfpy.Color.__init__");
|
||||
|
||||
// Handle fill_color
|
||||
if (fill_color && fill_color != Py_None) {
|
||||
PyColorObject* color_obj = PyColor::from_arg(fill_color);
|
||||
if (!color_obj) {
|
||||
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->text.setFillColor(PyColor::fromPy(fc));
|
||||
//Py_DECREF(fc);
|
||||
self->data->text.setFillColor(color_obj->data);
|
||||
Py_DECREF(color_obj);
|
||||
} else {
|
||||
self->data->text.setFillColor(sf::Color(0,0,0,255));
|
||||
self->data->text.setFillColor(sf::Color(255, 255, 255, 255)); // Default: white
|
||||
}
|
||||
|
||||
if (outline_color) {
|
||||
auto oc = PyColor::from_arg(outline_color);
|
||||
if (!oc) {
|
||||
PyErr_SetString(PyExc_TypeError, "outline_color must be mcrfpy.Color or arguments to mcrfpy.Color.__init__");
|
||||
|
||||
// Handle outline_color
|
||||
if (outline_color && outline_color != Py_None) {
|
||||
PyColorObject* color_obj = PyColor::from_arg(outline_color);
|
||||
if (!color_obj) {
|
||||
PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or color tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->text.setOutlineColor(PyColor::fromPy(oc));
|
||||
//Py_DECREF(oc);
|
||||
self->data->text.setOutlineColor(color_obj->data);
|
||||
Py_DECREF(color_obj);
|
||||
} else {
|
||||
self->data->text.setOutlineColor(sf::Color(128,128,128,255));
|
||||
self->data->text.setOutlineColor(sf::Color(0, 0, 0, 255)); // Default: black
|
||||
}
|
||||
|
||||
// Process click handler if provided
|
||||
|
||||
// Set other properties
|
||||
self->data->visible = visible;
|
||||
self->data->opacity = opacity;
|
||||
self->data->z_index = z_index;
|
||||
if (name) {
|
||||
self->data->name = std::string(name);
|
||||
}
|
||||
|
||||
// Handle click handler
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
if (!PyCallable_Check(click_handler)) {
|
||||
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
||||
|
|
@ -487,10 +438,11 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
|||
}
|
||||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// Property system implementation for animations
|
||||
bool UICaption::setProperty(const std::string& name, float value) {
|
||||
if (name == "x") {
|
||||
|
|
|
|||
|
|
@ -65,26 +65,37 @@ namespace mcrfpydef {
|
|||
//.tp_iter
|
||||
//.tp_iternext
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)\n\n"
|
||||
.tp_doc = PyDoc_STR("Caption(pos=None, font=None, text='', **kwargs)\n\n"
|
||||
"A text display UI element with customizable font and styling.\n\n"
|
||||
"Args:\n"
|
||||
" text (str): The text content to display. Default: ''\n"
|
||||
" x (float): X position in pixels. Default: 0\n"
|
||||
" y (float): Y position in pixels. Default: 0\n"
|
||||
" font (Font): Font object for text rendering. Default: engine default font\n"
|
||||
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||
" font (Font, optional): Font object for text rendering. Default: engine default font\n"
|
||||
" text (str, optional): The text content to display. Default: ''\n\n"
|
||||
"Keyword Args:\n"
|
||||
" fill_color (Color): Text fill color. Default: (255, 255, 255, 255)\n"
|
||||
" outline_color (Color): Text outline color. Default: (0, 0, 0, 255)\n"
|
||||
" outline (float): Text outline thickness. Default: 0\n"
|
||||
" click (callable): Click event handler. Default: None\n\n"
|
||||
" font_size (float): Font size in points. Default: 16\n"
|
||||
" click (callable): Click event handler. Default: None\n"
|
||||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" x (float): X position override. Default: 0\n"
|
||||
" y (float): Y position override. Default: 0\n\n"
|
||||
"Attributes:\n"
|
||||
" text (str): The displayed text content\n"
|
||||
" x, y (float): Position in pixels\n"
|
||||
" pos (Vector): Position as a Vector object\n"
|
||||
" font (Font): Font used for rendering\n"
|
||||
" font_size (float): Font size in points\n"
|
||||
" fill_color, outline_color (Color): Text appearance\n"
|
||||
" outline (float): Outline thickness\n"
|
||||
" click (callable): Click event handler\n"
|
||||
" visible (bool): Visibility state\n"
|
||||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
" w, h (float): Read-only computed size based on text and font"),
|
||||
.tp_methods = UICaption_methods,
|
||||
//.tp_members = PyUIFrame_members,
|
||||
|
|
|
|||
132
src/UIEntity.cpp
132
src/UIEntity.cpp
|
|
@ -4,7 +4,6 @@
|
|||
#include <algorithm>
|
||||
#include "PyObjectUtils.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyArgHelpers.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
#include "UIEntityPyMethods.h"
|
||||
|
||||
|
|
@ -121,81 +120,57 @@ PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
|
|||
}
|
||||
|
||||
int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||
// Try parsing with PyArgHelpers for grid position
|
||||
int arg_idx = 0;
|
||||
auto grid_pos_result = PyArgHelpers::parseGridPosition(args, kwds, &arg_idx);
|
||||
|
||||
// Default values
|
||||
float grid_x = 0.0f, grid_y = 0.0f;
|
||||
int sprite_index = 0;
|
||||
// Define all parameters with defaults
|
||||
PyObject* grid_pos_obj = nullptr;
|
||||
PyObject* texture = nullptr;
|
||||
int sprite_index = 0;
|
||||
PyObject* grid_obj = nullptr;
|
||||
int visible = 1;
|
||||
float opacity = 1.0f;
|
||||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f;
|
||||
|
||||
// Case 1: Got grid position from helpers (tuple format)
|
||||
if (grid_pos_result.valid) {
|
||||
grid_x = grid_pos_result.grid_x;
|
||||
grid_y = grid_pos_result.grid_y;
|
||||
|
||||
// Parse remaining arguments
|
||||
static const char* remaining_keywords[] = {
|
||||
"texture", "sprite_index", "grid", nullptr
|
||||
};
|
||||
|
||||
// Create new tuple with remaining args
|
||||
Py_ssize_t total_args = PyTuple_Size(args);
|
||||
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OiO",
|
||||
const_cast<char**>(remaining_keywords),
|
||||
&texture, &sprite_index, &grid_obj)) {
|
||||
Py_DECREF(remaining_args);
|
||||
if (grid_pos_result.error) PyErr_SetString(PyExc_TypeError, grid_pos_result.error);
|
||||
return -1;
|
||||
}
|
||||
Py_DECREF(remaining_args);
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
"grid_pos", "texture", "sprite_index", // Positional args (as per spec)
|
||||
// Keyword-only args
|
||||
"grid", "visible", "opacity", "name", "x", "y",
|
||||
nullptr
|
||||
};
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOiOifzff", const_cast<char**>(kwlist),
|
||||
&grid_pos_obj, &texture, &sprite_index, // Positional
|
||||
&grid_obj, &visible, &opacity, &name, &x, &y)) {
|
||||
return -1;
|
||||
}
|
||||
// Case 2: Traditional format
|
||||
else {
|
||||
PyErr_Clear(); // Clear any errors from helpers
|
||||
|
||||
static const char* keywords[] = {
|
||||
"grid_x", "grid_y", "texture", "sprite_index", "grid", "grid_pos", nullptr
|
||||
};
|
||||
PyObject* grid_pos_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOiOO",
|
||||
const_cast<char**>(keywords),
|
||||
&grid_x, &grid_y, &texture, &sprite_index,
|
||||
&grid_obj, &grid_pos_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle grid_pos keyword override
|
||||
if (grid_pos_obj && grid_pos_obj != Py_None) {
|
||||
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
|
||||
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
|
||||
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
|
||||
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
grid_y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
}
|
||||
|
||||
// Handle grid position argument (can be tuple or use x/y keywords)
|
||||
if (grid_pos_obj) {
|
||||
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
|
||||
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
|
||||
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
|
||||
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)");
|
||||
PyErr_SetString(PyExc_TypeError, "grid_pos tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// check types for texture
|
||||
//
|
||||
// Set Texture - allow None or use default
|
||||
//
|
||||
// Handle texture argument
|
||||
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
||||
if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||
return -1;
|
||||
} else if (texture != NULL && texture != Py_None) {
|
||||
if (texture && texture != Py_None) {
|
||||
if (!PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||
return -1;
|
||||
}
|
||||
auto pytexture = (PyTextureObject*)texture;
|
||||
texture_ptr = pytexture->data;
|
||||
} else {
|
||||
|
|
@ -203,25 +178,20 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|||
texture_ptr = McRFPy_API::default_texture;
|
||||
}
|
||||
|
||||
// Allow creation without texture for testing purposes
|
||||
// if (!texture_ptr) {
|
||||
// PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
|
||||
// return -1;
|
||||
// }
|
||||
|
||||
if (grid_obj != NULL && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
// Handle grid argument
|
||||
if (grid_obj && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Always use default constructor for lazy initialization
|
||||
// Create the entity
|
||||
self->data = std::make_shared<UIEntity>();
|
||||
|
||||
// Store reference to Python object
|
||||
self->data->self = (PyObject*)self;
|
||||
Py_INCREF(self);
|
||||
|
||||
// TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers
|
||||
// Set texture and sprite index
|
||||
if (texture_ptr) {
|
||||
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
|
||||
} else {
|
||||
|
|
@ -230,12 +200,20 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|||
}
|
||||
|
||||
// Set position using grid coordinates
|
||||
self->data->position = sf::Vector2f(grid_x, grid_y);
|
||||
self->data->position = sf::Vector2f(x, y);
|
||||
|
||||
if (grid_obj != NULL) {
|
||||
// Set other properties (delegate to sprite)
|
||||
self->data->sprite.visible = visible;
|
||||
self->data->sprite.opacity = opacity;
|
||||
if (name) {
|
||||
self->data->sprite.name = std::string(name);
|
||||
}
|
||||
|
||||
// Handle grid attachment
|
||||
if (grid_obj) {
|
||||
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj;
|
||||
self->data->grid = pygrid->data;
|
||||
// todone - on creation of Entity with Grid assignment, also append it to the entity list
|
||||
// Append entity to grid's entity list
|
||||
pygrid->data->entities->push_back(self->data);
|
||||
|
||||
// Don't initialize gridstate here - lazy initialization to support large numbers of entities
|
||||
|
|
|
|||
|
|
@ -88,7 +88,28 @@ namespace mcrfpydef {
|
|||
.tp_itemsize = 0,
|
||||
.tp_repr = (reprfunc)UIEntity::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_doc = "UIEntity objects",
|
||||
.tp_doc = PyDoc_STR("Entity(grid_pos=None, texture=None, sprite_index=0, **kwargs)\n\n"
|
||||
"A game entity that exists on a grid with sprite rendering.\n\n"
|
||||
"Args:\n"
|
||||
" grid_pos (tuple, optional): Grid position as (x, y) tuple. Default: (0, 0)\n"
|
||||
" texture (Texture, optional): Texture object for sprite. Default: default texture\n"
|
||||
" sprite_index (int, optional): Index into texture atlas. Default: 0\n\n"
|
||||
"Keyword Args:\n"
|
||||
" grid (Grid): Grid to attach entity to. Default: None\n"
|
||||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" x (float): X grid position override. Default: 0\n"
|
||||
" y (float): Y grid position override. Default: 0\n\n"
|
||||
"Attributes:\n"
|
||||
" pos (tuple): Grid position as (x, y) tuple\n"
|
||||
" x, y (float): Grid position coordinates\n"
|
||||
" draw_pos (tuple): Pixel position for rendering\n"
|
||||
" gridstate (GridPointState): Visibility state for grid points\n"
|
||||
" sprite_index (int): Current sprite index\n"
|
||||
" visible (bool): Visibility state\n"
|
||||
" opacity (float): Opacity value\n"
|
||||
" name (str): Element name"),
|
||||
.tp_methods = UIEntity_all_methods,
|
||||
.tp_getset = UIEntity::getsetters,
|
||||
.tp_base = &mcrfpydef::PyDrawableType,
|
||||
|
|
|
|||
185
src/UIFrame.cpp
185
src/UIFrame.cpp
|
|
@ -6,7 +6,6 @@
|
|||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PyArgHelpers.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
|
||||
UIDrawable* UIFrame::click_at(sf::Vector2f point)
|
||||
|
|
@ -432,67 +431,47 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
// Initialize children first
|
||||
self->data->children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
|
||||
|
||||
// Try parsing with PyArgHelpers
|
||||
int arg_idx = 0;
|
||||
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
|
||||
auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx);
|
||||
|
||||
// Default values
|
||||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f, outline = 0.0f;
|
||||
// Define all parameters with defaults
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* size_obj = nullptr;
|
||||
PyObject* fill_color = nullptr;
|
||||
PyObject* outline_color = nullptr;
|
||||
float outline = 0.0f;
|
||||
PyObject* children_arg = nullptr;
|
||||
PyObject* click_handler = nullptr;
|
||||
int visible = 1;
|
||||
float opacity = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
|
||||
int clip_children = 0;
|
||||
|
||||
// Case 1: Got position and size from helpers (tuple format)
|
||||
if (pos_result.valid && size_result.valid) {
|
||||
x = pos_result.x;
|
||||
y = pos_result.y;
|
||||
w = size_result.w;
|
||||
h = size_result.h;
|
||||
|
||||
// Parse remaining arguments
|
||||
static const char* remaining_keywords[] = {
|
||||
"fill_color", "outline_color", "outline", "children", "click", nullptr
|
||||
};
|
||||
|
||||
// Create new tuple with remaining args
|
||||
Py_ssize_t total_args = PyTuple_Size(args);
|
||||
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OOfOO",
|
||||
const_cast<char**>(remaining_keywords),
|
||||
&fill_color, &outline_color, &outline,
|
||||
&children_arg, &click_handler)) {
|
||||
Py_DECREF(remaining_args);
|
||||
if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error);
|
||||
else if (size_result.error) PyErr_SetString(PyExc_TypeError, size_result.error);
|
||||
return -1;
|
||||
}
|
||||
Py_DECREF(remaining_args);
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
"pos", "size", // Positional args (as per spec)
|
||||
// Keyword-only args
|
||||
"fill_color", "outline_color", "outline", "children", "click",
|
||||
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children",
|
||||
nullptr
|
||||
};
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffi", const_cast<char**>(kwlist),
|
||||
&pos_obj, &size_obj, // Positional
|
||||
&fill_color, &outline_color, &outline, &children_arg, &click_handler,
|
||||
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children)) {
|
||||
return -1;
|
||||
}
|
||||
// Case 2: Traditional format (x, y, w, h, ...)
|
||||
else {
|
||||
PyErr_Clear(); // Clear any errors from helpers
|
||||
|
||||
static const char* keywords[] = {
|
||||
"x", "y", "w", "h", "fill_color", "outline_color", "outline",
|
||||
"children", "click", "pos", "size", nullptr
|
||||
};
|
||||
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* size_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffffOOfOOOO",
|
||||
const_cast<char**>(keywords),
|
||||
&x, &y, &w, &h, &fill_color, &outline_color,
|
||||
&outline, &children_arg, &click_handler,
|
||||
&pos_obj, &size_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle pos keyword override
|
||||
if (pos_obj && pos_obj != Py_None) {
|
||||
|
||||
// Handle position argument (can be tuple, Vector, or use x/y keywords)
|
||||
if (pos_obj) {
|
||||
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||
if (vec) {
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
Py_DECREF(vec);
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
||||
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
||||
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
||||
|
|
@ -500,47 +479,87 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
|
||||
PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
||||
PyVectorObject* vec = (PyVectorObject*)pos_obj;
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle size keyword override
|
||||
if (size_obj && size_obj != Py_None) {
|
||||
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
||||
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
||||
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
|
||||
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
||||
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
|
||||
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
||||
}
|
||||
}
|
||||
// If no pos_obj but x/y keywords were provided, they're already in x, y variables
|
||||
|
||||
// Handle size argument (can be tuple or use w/h keywords)
|
||||
if (size_obj) {
|
||||
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
||||
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
||||
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
|
||||
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
||||
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
|
||||
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
|
||||
PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// If no size_obj but w/h keywords were provided, they're already in w, h variables
|
||||
|
||||
self->data->position = sf::Vector2f(x, y); // Set base class position
|
||||
self->data->box.setPosition(self->data->position); // Sync box position
|
||||
// Set the position and size
|
||||
self->data->position = sf::Vector2f(x, y);
|
||||
self->data->box.setPosition(self->data->position);
|
||||
self->data->box.setSize(sf::Vector2f(w, h));
|
||||
self->data->box.setOutlineThickness(outline);
|
||||
// getsetter abuse because I haven't standardized Color object parsing (TODO)
|
||||
int err_val = 0;
|
||||
if (fill_color && fill_color != Py_None) err_val = UIFrame::set_color_member(self, fill_color, (void*)0);
|
||||
else self->data->box.setFillColor(sf::Color(0,0,0,255));
|
||||
if (err_val) return err_val;
|
||||
if (outline_color && outline_color != Py_None) err_val = UIFrame::set_color_member(self, outline_color, (void*)1);
|
||||
else self->data->box.setOutlineColor(sf::Color(128,128,128,255));
|
||||
if (err_val) return err_val;
|
||||
|
||||
// Handle fill_color
|
||||
if (fill_color && fill_color != Py_None) {
|
||||
PyColorObject* color_obj = PyColor::from_arg(fill_color);
|
||||
if (!color_obj) {
|
||||
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->box.setFillColor(color_obj->data);
|
||||
Py_DECREF(color_obj);
|
||||
} else {
|
||||
self->data->box.setFillColor(sf::Color(0, 0, 0, 128)); // Default: semi-transparent black
|
||||
}
|
||||
|
||||
// Handle outline_color
|
||||
if (outline_color && outline_color != Py_None) {
|
||||
PyColorObject* color_obj = PyColor::from_arg(outline_color);
|
||||
if (!color_obj) {
|
||||
PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or color tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->box.setOutlineColor(color_obj->data);
|
||||
Py_DECREF(color_obj);
|
||||
} else {
|
||||
self->data->box.setOutlineColor(sf::Color(255, 255, 255, 255)); // Default: white
|
||||
}
|
||||
|
||||
// Set other properties
|
||||
self->data->visible = visible;
|
||||
self->data->opacity = opacity;
|
||||
self->data->z_index = z_index;
|
||||
self->data->clip_children = clip_children;
|
||||
if (name) {
|
||||
self->data->name = std::string(name);
|
||||
}
|
||||
|
||||
// Handle click handler
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
if (!PyCallable_Check(click_handler)) {
|
||||
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
||||
return -1;
|
||||
}
|
||||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
// Process children argument if provided
|
||||
if (children_arg && children_arg != Py_None) {
|
||||
|
|
|
|||
|
|
@ -86,27 +86,38 @@ namespace mcrfpydef {
|
|||
//.tp_iter
|
||||
//.tp_iternext
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)\n\n"
|
||||
.tp_doc = PyDoc_STR("Frame(pos=None, size=None, **kwargs)\n\n"
|
||||
"A rectangular frame UI element that can contain other drawable elements.\n\n"
|
||||
"Args:\n"
|
||||
" x (float): X position in pixels. Default: 0\n"
|
||||
" y (float): Y position in pixels. Default: 0\n"
|
||||
" w (float): Width in pixels. Default: 0\n"
|
||||
" h (float): Height in pixels. Default: 0\n"
|
||||
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||
" size (tuple, optional): Size as (width, height) tuple. Default: (0, 0)\n\n"
|
||||
"Keyword Args:\n"
|
||||
" fill_color (Color): Background fill color. Default: (0, 0, 0, 128)\n"
|
||||
" outline_color (Color): Border outline color. Default: (255, 255, 255, 255)\n"
|
||||
" outline (float): Border outline thickness. Default: 0\n"
|
||||
" click (callable): Click event handler. Default: None\n"
|
||||
" children (list): Initial list of child drawable elements. Default: None\n\n"
|
||||
" children (list): Initial list of child drawable elements. Default: None\n"
|
||||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" x (float): X position override. Default: 0\n"
|
||||
" y (float): Y position override. Default: 0\n"
|
||||
" w (float): Width override. Default: 0\n"
|
||||
" h (float): Height override. Default: 0\n"
|
||||
" clip_children (bool): Whether to clip children to frame bounds. Default: False\n\n"
|
||||
"Attributes:\n"
|
||||
" x, y (float): Position in pixels\n"
|
||||
" w, h (float): Size in pixels\n"
|
||||
" pos (Vector): Position as a Vector object\n"
|
||||
" fill_color, outline_color (Color): Visual appearance\n"
|
||||
" outline (float): Border thickness\n"
|
||||
" click (callable): Click event handler\n"
|
||||
" children (list): Collection of child drawable elements\n"
|
||||
" visible (bool): Visibility state\n"
|
||||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
" clip_children (bool): Whether to clip children to frame bounds"),
|
||||
.tp_methods = UIFrame_methods,
|
||||
//.tp_members = PyUIFrame_members,
|
||||
|
|
|
|||
231
src/UIGrid.cpp
231
src/UIGrid.cpp
|
|
@ -1,7 +1,6 @@
|
|||
#include "UIGrid.h"
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PyArgHelpers.h"
|
||||
#include <algorithm>
|
||||
// UIDrawable methods now in UIBase.h
|
||||
|
||||
|
|
@ -518,102 +517,49 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
|
|||
|
||||
|
||||
int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||
// Default values
|
||||
int grid_x = 0, grid_y = 0;
|
||||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
|
||||
// Define all parameters with defaults
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* size_obj = nullptr;
|
||||
PyObject* grid_size_obj = nullptr;
|
||||
PyObject* textureObj = nullptr;
|
||||
PyObject* fill_color = nullptr;
|
||||
PyObject* click_handler = nullptr;
|
||||
float center_x = 0.0f, center_y = 0.0f;
|
||||
float zoom = 1.0f;
|
||||
int perspective = -1; // perspective is a difficult __init__ arg; needs an entity in collection to work
|
||||
int visible = 1;
|
||||
float opacity = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
|
||||
int grid_x = 2, grid_y = 2; // Default to 2x2 grid
|
||||
|
||||
// Check if first argument is a tuple (for tuple-based initialization)
|
||||
bool has_tuple_first_arg = false;
|
||||
if (args && PyTuple_Size(args) > 0) {
|
||||
PyObject* first_arg = PyTuple_GetItem(args, 0);
|
||||
if (PyTuple_Check(first_arg)) {
|
||||
has_tuple_first_arg = true;
|
||||
}
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
"pos", "size", "grid_size", "texture", // Positional args (as per spec)
|
||||
// Keyword-only args
|
||||
"fill_color", "click", "center_x", "center_y", "zoom", "perspective",
|
||||
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "grid_x", "grid_y",
|
||||
nullptr
|
||||
};
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffiifizffffii", const_cast<char**>(kwlist),
|
||||
&pos_obj, &size_obj, &grid_size_obj, &textureObj, // Positional
|
||||
&fill_color, &click_handler, ¢er_x, ¢er_y, &zoom, &perspective,
|
||||
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_x, &grid_y)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Try tuple-based parsing if we have a tuple as first argument
|
||||
if (has_tuple_first_arg) {
|
||||
int arg_idx = 0;
|
||||
auto grid_size_result = PyArgHelpers::parseGridSize(args, kwds, &arg_idx);
|
||||
|
||||
// If grid size parsing failed with an error, report it
|
||||
if (!grid_size_result.valid) {
|
||||
if (grid_size_result.error) {
|
||||
PyErr_SetString(PyExc_TypeError, grid_size_result.error);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid grid size tuple");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// We got a valid grid size
|
||||
grid_x = grid_size_result.grid_w;
|
||||
grid_y = grid_size_result.grid_h;
|
||||
|
||||
// Try to parse position and size
|
||||
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
|
||||
if (pos_result.valid) {
|
||||
x = pos_result.x;
|
||||
y = pos_result.y;
|
||||
}
|
||||
|
||||
auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx);
|
||||
if (size_result.valid) {
|
||||
w = size_result.w;
|
||||
h = size_result.h;
|
||||
// Handle position argument (can be tuple, Vector, or use x/y keywords)
|
||||
if (pos_obj) {
|
||||
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||
if (vec) {
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
Py_DECREF(vec);
|
||||
} else {
|
||||
// Default size based on grid dimensions
|
||||
w = grid_x * 16.0f;
|
||||
h = grid_y * 16.0f;
|
||||
}
|
||||
|
||||
// Parse remaining arguments (texture)
|
||||
static const char* remaining_keywords[] = { "texture", nullptr };
|
||||
Py_ssize_t total_args = PyTuple_Size(args);
|
||||
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
|
||||
|
||||
PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|O",
|
||||
const_cast<char**>(remaining_keywords),
|
||||
&textureObj);
|
||||
Py_DECREF(remaining_args);
|
||||
}
|
||||
// Traditional format parsing
|
||||
else {
|
||||
static const char* keywords[] = {
|
||||
"grid_x", "grid_y", "texture", "pos", "size", "grid_size", nullptr
|
||||
};
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* size_obj = nullptr;
|
||||
PyObject* grid_size_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiOOOO",
|
||||
const_cast<char**>(keywords),
|
||||
&grid_x, &grid_y, &textureObj,
|
||||
&pos_obj, &size_obj, &grid_size_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle grid_size override
|
||||
if (grid_size_obj && grid_size_obj != Py_None) {
|
||||
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
|
||||
PyObject* x_obj = PyTuple_GetItem(grid_size_obj, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(grid_size_obj, 1);
|
||||
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
|
||||
grid_x = PyLong_AsLong(x_obj);
|
||||
grid_y = PyLong_AsLong(y_obj);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_size must contain integers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple of two integers");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle position
|
||||
if (pos_obj && pos_obj != Py_None) {
|
||||
PyErr_Clear();
|
||||
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
||||
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
||||
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
||||
|
|
@ -622,36 +568,50 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must contain numbers");
|
||||
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of two numbers");
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle size
|
||||
if (size_obj && size_obj != Py_None) {
|
||||
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
||||
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
||||
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
|
||||
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
||||
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
|
||||
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "size must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle size argument (can be tuple or use w/h keywords)
|
||||
if (size_obj) {
|
||||
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
||||
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
||||
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
|
||||
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
||||
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
|
||||
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple of two numbers");
|
||||
PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
// Default size based on grid
|
||||
w = grid_x * 16.0f;
|
||||
h = grid_y * 16.0f;
|
||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle grid_size argument (can be tuple or use grid_x/grid_y keywords)
|
||||
if (grid_size_obj) {
|
||||
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
|
||||
PyObject* gx_val = PyTuple_GetItem(grid_size_obj, 0);
|
||||
PyObject* gy_val = PyTuple_GetItem(grid_size_obj, 1);
|
||||
if (PyLong_Check(gx_val) && PyLong_Check(gy_val)) {
|
||||
grid_x = PyLong_AsLong(gx_val);
|
||||
grid_y = PyLong_AsLong(gy_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_size tuple must contain integers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple (grid_x, grid_y)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -661,12 +621,8 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
return -1;
|
||||
}
|
||||
|
||||
// At this point we have x, y, w, h values from either parsing method
|
||||
|
||||
// Convert PyObject texture to shared_ptr<PyTexture>
|
||||
// Handle texture argument
|
||||
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
||||
|
||||
// Allow None or NULL for texture - use default texture in that case
|
||||
if (textureObj && textureObj != Py_None) {
|
||||
if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||
|
|
@ -679,14 +635,51 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
texture_ptr = McRFPy_API::default_texture;
|
||||
}
|
||||
|
||||
// Adjust size based on texture if available and size not explicitly set
|
||||
if (texture_ptr && w == grid_x * 16.0f && h == grid_y * 16.0f) {
|
||||
// If size wasn't specified, calculate based on grid dimensions and texture
|
||||
if (!size_obj && texture_ptr) {
|
||||
w = grid_x * texture_ptr->sprite_width;
|
||||
h = grid_y * texture_ptr->sprite_height;
|
||||
} else if (!size_obj) {
|
||||
w = grid_x * 16.0f; // Default tile size
|
||||
h = grid_y * 16.0f;
|
||||
}
|
||||
|
||||
// Create the grid
|
||||
self->data = std::make_shared<UIGrid>(grid_x, grid_y, texture_ptr,
|
||||
sf::Vector2f(x, y), sf::Vector2f(w, h));
|
||||
|
||||
// Set additional properties
|
||||
self->data->center_x = center_x;
|
||||
self->data->center_y = center_y;
|
||||
self->data->zoom = zoom;
|
||||
self->data->perspective = perspective;
|
||||
self->data->visible = visible;
|
||||
self->data->opacity = opacity;
|
||||
self->data->z_index = z_index;
|
||||
if (name) {
|
||||
self->data->name = std::string(name);
|
||||
}
|
||||
|
||||
// Handle fill_color
|
||||
if (fill_color && fill_color != Py_None) {
|
||||
PyColorObject* color_obj = PyColor::from_arg(fill_color);
|
||||
if (!color_obj) {
|
||||
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->box.setFillColor(color_obj->data);
|
||||
Py_DECREF(color_obj);
|
||||
}
|
||||
|
||||
// Handle click handler
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
if (!PyCallable_Check(click_handler)) {
|
||||
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
||||
return -1;
|
||||
}
|
||||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
return 0; // Success
|
||||
}
|
||||
|
||||
|
|
|
|||
52
src/UIGrid.h
52
src/UIGrid.h
|
|
@ -184,29 +184,49 @@ namespace mcrfpydef {
|
|||
//.tp_iter
|
||||
//.tp_iternext
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)\n\n"
|
||||
"A grid-based tilemap UI element for rendering tile-based levels and game worlds.\n\n"
|
||||
.tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n"
|
||||
"A grid-based UI element for tile-based rendering and entity management.\n\n"
|
||||
"Args:\n"
|
||||
" x (float): X position in pixels. Default: 0\n"
|
||||
" y (float): Y position in pixels. Default: 0\n"
|
||||
" grid_size (tuple): Grid dimensions as (width, height) in tiles. Default: (20, 20)\n"
|
||||
" texture (Texture): Texture atlas containing tile sprites. Default: None\n"
|
||||
" tile_width (int): Width of each tile in pixels. Default: 16\n"
|
||||
" tile_height (int): Height of each tile in pixels. Default: 16\n"
|
||||
" scale (float): Grid scaling factor. Default: 1.0\n"
|
||||
" click (callable): Click event handler. Default: None\n\n"
|
||||
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||
" size (tuple, optional): Size as (width, height) tuple. Default: auto-calculated from grid_size\n"
|
||||
" grid_size (tuple, optional): Grid dimensions as (grid_x, grid_y) tuple. Default: (2, 2)\n"
|
||||
" texture (Texture, optional): Texture containing tile sprites. Default: default texture\n\n"
|
||||
"Keyword Args:\n"
|
||||
" fill_color (Color): Background fill color. Default: None\n"
|
||||
" click (callable): Click event handler. Default: None\n"
|
||||
" center_x (float): X coordinate of center point. Default: 0\n"
|
||||
" center_y (float): Y coordinate of center point. Default: 0\n"
|
||||
" zoom (float): Zoom level for rendering. Default: 1.0\n"
|
||||
" perspective (int): Entity perspective index (-1 for omniscient). Default: -1\n"
|
||||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" x (float): X position override. Default: 0\n"
|
||||
" y (float): Y position override. Default: 0\n"
|
||||
" w (float): Width override. Default: auto-calculated\n"
|
||||
" h (float): Height override. Default: auto-calculated\n"
|
||||
" grid_x (int): Grid width override. Default: 2\n"
|
||||
" grid_y (int): Grid height override. Default: 2\n\n"
|
||||
"Attributes:\n"
|
||||
" x, y (float): Position in pixels\n"
|
||||
" w, h (float): Size in pixels\n"
|
||||
" pos (Vector): Position as a Vector object\n"
|
||||
" size (tuple): Size as (width, height) tuple\n"
|
||||
" center (tuple): Center point as (x, y) tuple\n"
|
||||
" center_x, center_y (float): Center point coordinates\n"
|
||||
" zoom (float): Zoom level for rendering\n"
|
||||
" grid_size (tuple): Grid dimensions (width, height) in tiles\n"
|
||||
" tile_width, tile_height (int): Tile dimensions in pixels\n"
|
||||
" grid_x, grid_y (int): Grid dimensions\n"
|
||||
" texture (Texture): Tile texture atlas\n"
|
||||
" scale (float): Scale multiplier\n"
|
||||
" points (list): 2D array of GridPoint objects for tile data\n"
|
||||
" entities (list): Collection of Entity objects in the grid\n"
|
||||
" background_color (Color): Grid background color\n"
|
||||
" fill_color (Color): Background color\n"
|
||||
" entities (EntityCollection): Collection of entities in the grid\n"
|
||||
" perspective (int): Entity perspective index\n"
|
||||
" click (callable): Click event handler\n"
|
||||
" visible (bool): Visibility state\n"
|
||||
" z_index (int): Rendering order"),
|
||||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name"),
|
||||
.tp_methods = UIGrid_all_methods,
|
||||
//.tp_members = UIGrid::members,
|
||||
.tp_getset = UIGrid::getsetters,
|
||||
|
|
|
|||
121
src/UISprite.cpp
121
src/UISprite.cpp
|
|
@ -1,7 +1,6 @@
|
|||
#include "UISprite.h"
|
||||
#include "GameEngine.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyArgHelpers.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
|
||||
UIDrawable* UISprite::click_at(sf::Vector2f point)
|
||||
|
|
@ -327,57 +326,46 @@ PyObject* UISprite::repr(PyUISpriteObject* self)
|
|||
|
||||
int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
// Try parsing with PyArgHelpers
|
||||
int arg_idx = 0;
|
||||
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
|
||||
|
||||
// Default values
|
||||
float x = 0.0f, y = 0.0f, scale = 1.0f;
|
||||
int sprite_index = 0;
|
||||
// Define all parameters with defaults
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* texture = nullptr;
|
||||
int sprite_index = 0;
|
||||
float scale = 1.0f;
|
||||
float scale_x = 1.0f;
|
||||
float scale_y = 1.0f;
|
||||
PyObject* click_handler = nullptr;
|
||||
int visible = 1;
|
||||
float opacity = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f;
|
||||
|
||||
// Case 1: Got position from helpers (tuple format)
|
||||
if (pos_result.valid) {
|
||||
x = pos_result.x;
|
||||
y = pos_result.y;
|
||||
|
||||
// Parse remaining arguments
|
||||
static const char* remaining_keywords[] = {
|
||||
"texture", "sprite_index", "scale", "click", nullptr
|
||||
};
|
||||
|
||||
// Create new tuple with remaining args
|
||||
Py_ssize_t total_args = PyTuple_Size(args);
|
||||
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OifO",
|
||||
const_cast<char**>(remaining_keywords),
|
||||
&texture, &sprite_index, &scale, &click_handler)) {
|
||||
Py_DECREF(remaining_args);
|
||||
if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error);
|
||||
return -1;
|
||||
}
|
||||
Py_DECREF(remaining_args);
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
"pos", "texture", "sprite_index", // Positional args (as per spec)
|
||||
// Keyword-only args
|
||||
"scale", "scale_x", "scale_y", "click",
|
||||
"visible", "opacity", "z_index", "name", "x", "y",
|
||||
nullptr
|
||||
};
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizff", const_cast<char**>(kwlist),
|
||||
&pos_obj, &texture, &sprite_index, // Positional
|
||||
&scale, &scale_x, &scale_y, &click_handler,
|
||||
&visible, &opacity, &z_index, &name, &x, &y)) {
|
||||
return -1;
|
||||
}
|
||||
// Case 2: Traditional format
|
||||
else {
|
||||
PyErr_Clear(); // Clear any errors from helpers
|
||||
|
||||
static const char* keywords[] = {
|
||||
"x", "y", "texture", "sprite_index", "scale", "click", "pos", nullptr
|
||||
};
|
||||
PyObject* pos_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOifOO",
|
||||
const_cast<char**>(keywords),
|
||||
&x, &y, &texture, &sprite_index, &scale,
|
||||
&click_handler, &pos_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle pos keyword override
|
||||
if (pos_obj && pos_obj != Py_None) {
|
||||
|
||||
// Handle position argument (can be tuple, Vector, or use x/y keywords)
|
||||
if (pos_obj) {
|
||||
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||
if (vec) {
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
Py_DECREF(vec);
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
||||
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
|
||||
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
|
||||
|
|
@ -385,12 +373,10 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
|||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
|
||||
PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
||||
PyVectorObject* vec = (PyVectorObject*)pos_obj;
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
|
|
@ -400,10 +386,11 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
|||
|
||||
// Handle texture - allow None or use default
|
||||
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
||||
if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||
return -1;
|
||||
} else if (texture != NULL && texture != Py_None) {
|
||||
if (texture && texture != Py_None) {
|
||||
if (!PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||
return -1;
|
||||
}
|
||||
auto pytexture = (PyTextureObject*)texture;
|
||||
texture_ptr = pytexture->data;
|
||||
} else {
|
||||
|
|
@ -416,9 +403,27 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
|||
return -1;
|
||||
}
|
||||
|
||||
// Create the sprite
|
||||
self->data = std::make_shared<UISprite>(texture_ptr, sprite_index, sf::Vector2f(x, y), scale);
|
||||
|
||||
// Set scale properties
|
||||
if (scale_x != 1.0f || scale_y != 1.0f) {
|
||||
// If scale_x or scale_y were explicitly set, use them
|
||||
self->data->setScale(sf::Vector2f(scale_x, scale_y));
|
||||
} else if (scale != 1.0f) {
|
||||
// Otherwise use uniform scale
|
||||
self->data->setScale(sf::Vector2f(scale, scale));
|
||||
}
|
||||
|
||||
// Set other properties
|
||||
self->data->visible = visible;
|
||||
self->data->opacity = opacity;
|
||||
self->data->z_index = z_index;
|
||||
if (name) {
|
||||
self->data->name = std::string(name);
|
||||
}
|
||||
|
||||
// Process click handler if provided
|
||||
// Handle click handler
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
if (!PyCallable_Check(click_handler)) {
|
||||
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
||||
|
|
|
|||
|
|
@ -92,23 +92,35 @@ namespace mcrfpydef {
|
|||
//.tp_iter
|
||||
//.tp_iternext
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)\n\n"
|
||||
.tp_doc = PyDoc_STR("Sprite(pos=None, texture=None, sprite_index=0, **kwargs)\n\n"
|
||||
"A sprite UI element that displays a texture or portion of a texture atlas.\n\n"
|
||||
"Args:\n"
|
||||
" x (float): X position in pixels. Default: 0\n"
|
||||
" y (float): Y position in pixels. Default: 0\n"
|
||||
" texture (Texture): Texture object to display. Default: None\n"
|
||||
" sprite_index (int): Index into texture atlas (if applicable). Default: 0\n"
|
||||
" scale (float): Sprite scaling factor. Default: 1.0\n"
|
||||
" click (callable): Click event handler. Default: None\n\n"
|
||||
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||
" texture (Texture, optional): Texture object to display. Default: default texture\n"
|
||||
" sprite_index (int, optional): Index into texture atlas. Default: 0\n\n"
|
||||
"Keyword Args:\n"
|
||||
" scale (float): Uniform scale factor. Default: 1.0\n"
|
||||
" scale_x (float): Horizontal scale factor. Default: 1.0\n"
|
||||
" scale_y (float): Vertical scale factor. Default: 1.0\n"
|
||||
" click (callable): Click event handler. Default: None\n"
|
||||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" x (float): X position override. Default: 0\n"
|
||||
" y (float): Y position override. Default: 0\n\n"
|
||||
"Attributes:\n"
|
||||
" x, y (float): Position in pixels\n"
|
||||
" pos (Vector): Position as a Vector object\n"
|
||||
" texture (Texture): The texture being displayed\n"
|
||||
" sprite_index (int): Current sprite index in texture atlas\n"
|
||||
" scale (float): Scale multiplier\n"
|
||||
" scale (float): Uniform scale factor\n"
|
||||
" scale_x, scale_y (float): Individual scale factors\n"
|
||||
" click (callable): Click event handler\n"
|
||||
" visible (bool): Visibility state\n"
|
||||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
" w, h (float): Read-only computed size based on texture and scale"),
|
||||
.tp_methods = UISprite_methods,
|
||||
//.tp_members = PyUIFrame_members,
|
||||
|
|
|
|||
|
|
@ -67,10 +67,10 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
|
|||
self.draw_pos = (tx, ty)
|
||||
for e in self.game.entities:
|
||||
if e is self: continue
|
||||
if e.draw_pos == old_pos: e.ev_exit(self)
|
||||
if e.draw_pos.x == old_pos.x and e.draw_pos.y == old_pos.y: e.ev_exit(self)
|
||||
for e in self.game.entities:
|
||||
if e is self: continue
|
||||
if e.draw_pos == (tx, ty): e.ev_enter(self)
|
||||
if e.draw_pos.x == tx and e.draw_pos.y == ty: e.ev_enter(self)
|
||||
|
||||
def act(self):
|
||||
pass
|
||||
|
|
@ -83,12 +83,12 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
|
|||
|
||||
def try_move(self, dx, dy, test=False):
|
||||
x_max, y_max = self.grid.grid_size
|
||||
tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy)
|
||||
tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy)
|
||||
#for e in iterable_entities(self.grid):
|
||||
|
||||
# sorting entities to test against the boulder instead of the button when they overlap.
|
||||
for e in sorted(self.game.entities, key = lambda i: i.draw_order, reverse = True):
|
||||
if e.draw_pos == (tx, ty):
|
||||
if e.draw_pos.x == tx and e.draw_pos.y == ty:
|
||||
#print(f"bumping {e}")
|
||||
return e.bump(self, dx, dy)
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
|
|||
return False
|
||||
|
||||
def _relative_move(self, dx, dy):
|
||||
tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy)
|
||||
tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy)
|
||||
#self.draw_pos = (tx, ty)
|
||||
self.do_move(tx, ty)
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ class Equippable:
|
|||
if self.zap_cooldown_remaining != 0:
|
||||
print("zap is cooling down.")
|
||||
return False
|
||||
fx, fy = caster.draw_pos
|
||||
fx, fy = caster.draw_pos.x, caster.draw_pos.y
|
||||
x, y = int(fx), int (fy)
|
||||
dist = lambda tx, ty: abs(int(tx) - x) + abs(int(ty) - y)
|
||||
targets = []
|
||||
|
|
@ -293,7 +293,7 @@ class PlayerEntity(COSEntity):
|
|||
## TODO - find other entities to avoid spawning on top of
|
||||
for spawn in spawn_points:
|
||||
for e in avoid or []:
|
||||
if e.draw_pos == spawn: break
|
||||
if e.draw_pos.x == spawn[0] and e.draw_pos.y == spawn[1]: break
|
||||
else:
|
||||
break
|
||||
self.draw_pos = spawn
|
||||
|
|
@ -314,9 +314,9 @@ class BoulderEntity(COSEntity):
|
|||
elif type(other) == EnemyEntity:
|
||||
if not other.can_push: return False
|
||||
#tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy)
|
||||
tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy)
|
||||
tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy)
|
||||
# Is the boulder blocked the same direction as the bumper? If not, let's both move
|
||||
old_pos = int(self.draw_pos[0]), int(self.draw_pos[1])
|
||||
old_pos = int(self.draw_pos.x), int(self.draw_pos.y)
|
||||
if self.try_move(dx, dy, test=test):
|
||||
if not test:
|
||||
other.do_move(*old_pos)
|
||||
|
|
@ -342,7 +342,7 @@ class ButtonEntity(COSEntity):
|
|||
# self.exit.unlock()
|
||||
# TODO: unlock, and then lock again, when player steps on/off
|
||||
if not test:
|
||||
pos = int(self.draw_pos[0]), int(self.draw_pos[1])
|
||||
pos = int(self.draw_pos.x), int(self.draw_pos.y)
|
||||
other.do_move(*pos)
|
||||
return True
|
||||
|
||||
|
|
@ -393,7 +393,7 @@ class EnemyEntity(COSEntity):
|
|||
def bump(self, other, dx, dy, test=False):
|
||||
if self.hp == 0:
|
||||
if not test:
|
||||
old_pos = int(self.draw_pos[0]), int(self.draw_pos[1])
|
||||
old_pos = int(self.draw_pos.x), int(self.draw_pos.y)
|
||||
other.do_move(*old_pos)
|
||||
return True
|
||||
if type(other) == PlayerEntity:
|
||||
|
|
@ -415,7 +415,7 @@ class EnemyEntity(COSEntity):
|
|||
print("Ouch, my entire body!!")
|
||||
self._entity.sprite_number = self.base_sprite + 246
|
||||
self.hp = 0
|
||||
old_pos = int(self.draw_pos[0]), int(self.draw_pos[1])
|
||||
old_pos = int(self.draw_pos.x), int(self.draw_pos.y)
|
||||
if not test:
|
||||
other.do_move(*old_pos)
|
||||
return True
|
||||
|
|
@ -423,8 +423,8 @@ class EnemyEntity(COSEntity):
|
|||
def act(self):
|
||||
if self.hp > 0:
|
||||
# if player nearby: attack
|
||||
x, y = self.draw_pos
|
||||
px, py = self.game.player.draw_pos
|
||||
x, y = self.draw_pos.x, self.draw_pos.y
|
||||
px, py = self.game.player.draw_pos.x, self.game.player.draw_pos.y
|
||||
for d in ((1, 0), (0, 1), (-1, 0), (1, 0)):
|
||||
if int(x + d[0]) == int(px) and int(y + d[1]) == int(py):
|
||||
self.try_move(*d)
|
||||
|
|
|
|||
|
|
@ -22,12 +22,13 @@ class TileInfo:
|
|||
@staticmethod
|
||||
def from_grid(grid, xy:tuple):
|
||||
values = {}
|
||||
x_max, y_max = grid.grid_size
|
||||
for d in deltas:
|
||||
tx, ty = d[0] + xy[0], d[1] + xy[1]
|
||||
try:
|
||||
values[d] = grid.at((tx, ty)).walkable
|
||||
except ValueError:
|
||||
if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max:
|
||||
values[d] = True
|
||||
else:
|
||||
values[d] = grid.at((tx, ty)).walkable
|
||||
return TileInfo(values)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -70,10 +71,10 @@ def special_rule_verify(rule, grid, xy, unverified_tiles, pass_unverified=False)
|
|||
tx, ty = xy[0] + dxy[0], xy[1] + dxy[1]
|
||||
#print(f"Special rule: {cardinal} {allowed_tile} {type(allowed_tile)} -> ({tx}, {ty}) [{grid.at((tx, ty)).tilesprite}]{'*' if (tx, ty) in unverified_tiles else ''}")
|
||||
if (tx, ty) in unverified_tiles and cardinal in "nsew": return pass_unverified
|
||||
try:
|
||||
return grid.at((tx, ty)).tilesprite == allowed_tile
|
||||
except ValueError:
|
||||
x_max, y_max = grid.grid_size
|
||||
if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max:
|
||||
return False
|
||||
return grid.at((tx, ty)).tilesprite == allowed_tile
|
||||
|
||||
import random
|
||||
tile_of_last_resort = 431
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class Crypt:
|
|||
|
||||
# Side Bar (inventory, level info) config
|
||||
self.level_caption = mcrfpy.Caption((5,5), "Level: 1", font, fill_color=(255, 255, 255))
|
||||
self.level_caption.size = 26
|
||||
self.level_caption.font_size = 26
|
||||
self.level_caption.outline = 3
|
||||
self.level_caption.outline_color = (0, 0, 0)
|
||||
self.sidebar.children.append(self.level_caption)
|
||||
|
|
@ -103,7 +103,7 @@ class Crypt:
|
|||
mcrfpy.Caption((25, 130 + 95 * i), "x", font, fill_color=(255, 255, 255)) for i in range(5)
|
||||
]
|
||||
for i in self.inv_captions:
|
||||
i.size = 16
|
||||
i.font_size = 16
|
||||
self.sidebar.children.append(i)
|
||||
|
||||
liminal_void = mcrfpy.Grid(1, 1, t, (0, 0), (16, 16))
|
||||
|
|
@ -382,7 +382,7 @@ class Crypt:
|
|||
def pull_boulder_search(self):
|
||||
for dx, dy in ( (0, -1), (-1, 0), (1, 0), (0, 1) ):
|
||||
for e in self.entities:
|
||||
if e.draw_pos != (self.player.draw_pos[0] + dx, self.player.draw_pos[1] + dy): continue
|
||||
if e.draw_pos.x != self.player.draw_pos.x + dx or e.draw_pos.y != self.player.draw_pos.y + dy: continue
|
||||
if type(e) == ce.BoulderEntity:
|
||||
self.pull_boulder_move((dx, dy), e)
|
||||
return self.enemy_turn()
|
||||
|
|
@ -395,7 +395,7 @@ class Crypt:
|
|||
if self.player.try_move(-p[0], -p[1], test=True):
|
||||
old_pos = self.player.draw_pos
|
||||
self.player.try_move(-p[0], -p[1])
|
||||
target_boulder.do_move(*old_pos)
|
||||
target_boulder.do_move(old_pos.x, old_pos.y)
|
||||
|
||||
def swap_level(self, new_level, spawn_point):
|
||||
self.level = new_level
|
||||
|
|
@ -451,7 +451,7 @@ class SweetButton:
|
|||
|
||||
# main button caption
|
||||
self.caption = mcrfpy.Caption((40, 3), caption, font, fill_color=font_color)
|
||||
self.caption.size = font_size
|
||||
self.caption.font_size = font_size
|
||||
self.caption.outline_color=font_outline_color
|
||||
self.caption.outline=font_outline_width
|
||||
self.main_button.children.append(self.caption)
|
||||
|
|
@ -548,20 +548,20 @@ class MainMenu:
|
|||
# title text
|
||||
drop_shadow = mcrfpy.Caption((150, 10), "Crypt Of Sokoban", font, fill_color=(96, 96, 96), outline_color=(192, 0, 0))
|
||||
drop_shadow.outline = 3
|
||||
drop_shadow.size = 64
|
||||
drop_shadow.font_size = 64
|
||||
components.append(
|
||||
drop_shadow
|
||||
)
|
||||
|
||||
title_txt = mcrfpy.Caption((158, 18), "Crypt Of Sokoban", font, fill_color=(255, 255, 255))
|
||||
title_txt.size = 64
|
||||
title_txt.font_size = 64
|
||||
components.append(
|
||||
title_txt
|
||||
)
|
||||
|
||||
# toast: text over the demo grid that fades out on a timer
|
||||
self.toast = mcrfpy.Caption((150, 400), "", font, fill_color=(0, 0, 0))
|
||||
self.toast.size = 28
|
||||
self.toast.font_size = 28
|
||||
self.toast.outline = 2
|
||||
self.toast.outline_color = (255, 255, 255)
|
||||
self.toast_event = None
|
||||
|
|
@ -626,6 +626,7 @@ class MainMenu:
|
|||
def play(self, sweet_btn, args):
|
||||
#if args[3] == "start": return # DRAMATIC on release action!
|
||||
if args[3] == "end": return
|
||||
mcrfpy.delTimer("demo_motion") # Clean up the demo timer
|
||||
self.crypt = Crypt()
|
||||
#mcrfpy.setScene("play")
|
||||
self.crypt.start()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demonstration of animation callbacks solving race conditions.
|
||||
Shows how callbacks enable direct causality for game state changes.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Game state
|
||||
player_moving = False
|
||||
move_queue = []
|
||||
|
||||
def movement_complete(anim, target):
|
||||
"""Called when player movement animation completes"""
|
||||
global player_moving, move_queue
|
||||
|
||||
print("Movement animation completed!")
|
||||
player_moving = False
|
||||
|
||||
# Process next move if queued
|
||||
if move_queue:
|
||||
next_pos = move_queue.pop(0)
|
||||
move_player_to(next_pos)
|
||||
else:
|
||||
print("Player is now idle and ready for input")
|
||||
|
||||
def move_player_to(new_pos):
|
||||
"""Move player with animation and proper state management"""
|
||||
global player_moving
|
||||
|
||||
if player_moving:
|
||||
print(f"Queueing move to {new_pos}")
|
||||
move_queue.append(new_pos)
|
||||
return
|
||||
|
||||
player_moving = True
|
||||
print(f"Moving player to {new_pos}")
|
||||
|
||||
# Get player entity (placeholder for demo)
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
player = ui[0] # Assume first element is player
|
||||
|
||||
# Animate movement with callback
|
||||
x, y = new_pos
|
||||
anim_x = mcrfpy.Animation("x", float(x), 0.5, "easeInOutQuad", callback=movement_complete)
|
||||
anim_y = mcrfpy.Animation("y", float(y), 0.5, "easeInOutQuad")
|
||||
|
||||
anim_x.start(player)
|
||||
anim_y.start(player)
|
||||
|
||||
def setup_demo():
|
||||
"""Set up the demo scene"""
|
||||
# Create scene
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
# Create player sprite
|
||||
player = mcrfpy.Frame((100, 100), (32, 32), fill_color=(0, 255, 0))
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
ui.append(player)
|
||||
|
||||
print("Demo: Animation callbacks for movement queue")
|
||||
print("=" * 40)
|
||||
|
||||
# Simulate rapid movement commands
|
||||
mcrfpy.setTimer("move1", lambda r: move_player_to((200, 100)), 100)
|
||||
mcrfpy.setTimer("move2", lambda r: move_player_to((200, 200)), 200) # Will be queued
|
||||
mcrfpy.setTimer("move3", lambda r: move_player_to((100, 200)), 300) # Will be queued
|
||||
|
||||
# Exit after demo
|
||||
mcrfpy.setTimer("exit", lambda r: exit_demo(), 3000)
|
||||
|
||||
def exit_demo():
|
||||
"""Exit the demo"""
|
||||
print("\nDemo completed successfully!")
|
||||
print("Callbacks ensure proper movement sequencing without race conditions")
|
||||
import sys
|
||||
sys.exit(0)
|
||||
|
||||
# Run the demo
|
||||
setup_demo()
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Animation Demo - Safe Version
|
||||
=========================================
|
||||
|
||||
A safer, simpler version that demonstrates animations without crashes.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Configuration
|
||||
DEMO_DURATION = 4.0
|
||||
|
||||
# Track state
|
||||
current_demo = 0
|
||||
subtitle = None
|
||||
demo_items = []
|
||||
|
||||
def create_scene():
|
||||
"""Create the demo scene"""
|
||||
mcrfpy.createScene("demo")
|
||||
mcrfpy.setScene("demo")
|
||||
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Animation Demo", 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)
|
||||
|
||||
def clear_demo_items():
|
||||
"""Clear demo items from scene"""
|
||||
global demo_items
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
|
||||
# Remove demo items by tracking what we added
|
||||
for item in demo_items:
|
||||
try:
|
||||
# Find index of item
|
||||
for i in range(len(ui)):
|
||||
if i >= 2: # Skip title and subtitle
|
||||
ui.remove(i)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
demo_items = []
|
||||
|
||||
def demo1_basic():
|
||||
"""Basic frame animations"""
|
||||
global demo_items
|
||||
clear_demo_items()
|
||||
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 1: Basic Frame Animations"
|
||||
|
||||
# Create frame
|
||||
f = mcrfpy.Frame(100, 150, 200, 100)
|
||||
f.fill_color = mcrfpy.Color(50, 50, 150)
|
||||
f.outline = 3
|
||||
ui.append(f)
|
||||
demo_items.append(f)
|
||||
|
||||
# Simple animations
|
||||
mcrfpy.Animation("x", 600.0, 2.0, "easeInOut").start(f)
|
||||
mcrfpy.Animation("w", 300.0, 2.0, "easeInOut").start(f)
|
||||
mcrfpy.Animation("fill_color", (255, 100, 50, 200), 3.0, "linear").start(f)
|
||||
|
||||
def demo2_caption():
|
||||
"""Caption animations"""
|
||||
global demo_items
|
||||
clear_demo_items()
|
||||
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 2: Caption Animations"
|
||||
|
||||
# Moving caption
|
||||
c1 = mcrfpy.Caption("Moving Text!", 100, 200)
|
||||
c1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(c1)
|
||||
demo_items.append(c1)
|
||||
|
||||
mcrfpy.Animation("x", 700.0, 3.0, "easeOutBounce").start(c1)
|
||||
|
||||
# Typewriter
|
||||
c2 = mcrfpy.Caption("", 100, 300)
|
||||
c2.fill_color = mcrfpy.Color(0, 255, 255)
|
||||
ui.append(c2)
|
||||
demo_items.append(c2)
|
||||
|
||||
mcrfpy.Animation("text", "Typewriter effect...", 3.0, "linear").start(c2)
|
||||
|
||||
def demo3_multiple():
|
||||
"""Multiple animations"""
|
||||
global demo_items
|
||||
clear_demo_items()
|
||||
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 3: Multiple Animations"
|
||||
|
||||
# Create several frames
|
||||
for i in range(5):
|
||||
f = mcrfpy.Frame(100 + i * 120, 200, 80, 80)
|
||||
f.fill_color = mcrfpy.Color(50 + i * 40, 100, 200 - i * 30)
|
||||
ui.append(f)
|
||||
demo_items.append(f)
|
||||
|
||||
# Animate each differently
|
||||
target_y = 350 + i * 20
|
||||
mcrfpy.Animation("y", float(target_y), 2.0, "easeInOut").start(f)
|
||||
mcrfpy.Animation("opacity", 0.5, 3.0, "easeInOut").start(f)
|
||||
|
||||
def run_next_demo(runtime):
|
||||
"""Run the next demo"""
|
||||
global current_demo
|
||||
|
||||
demos = [demo1_basic, demo2_caption, demo3_multiple]
|
||||
|
||||
if current_demo < len(demos):
|
||||
demos[current_demo]()
|
||||
current_demo += 1
|
||||
|
||||
if current_demo < len(demos):
|
||||
mcrfpy.setTimer("next", run_next_demo, int(DEMO_DURATION * 1000))
|
||||
else:
|
||||
subtitle.text = "Demo Complete!"
|
||||
# Exit after a delay
|
||||
def exit_program(rt):
|
||||
print("Demo finished successfully!")
|
||||
sys.exit(0)
|
||||
mcrfpy.setTimer("exit", exit_program, 2000)
|
||||
|
||||
# Initialize
|
||||
print("Starting Safe Animation Demo...")
|
||||
create_scene()
|
||||
|
||||
# Start demos
|
||||
mcrfpy.setTimer("start", run_next_demo, 500)
|
||||
|
|
@ -0,0 +1,616 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Animation Sizzle Reel
|
||||
=================================
|
||||
|
||||
This script demonstrates EVERY animation type on EVERY UI object type.
|
||||
It showcases all 30 easing functions, all animatable properties, and
|
||||
special animation modes (delta, sprite sequences, text effects).
|
||||
|
||||
The script creates a comprehensive visual demonstration of the animation
|
||||
system's capabilities, cycling through different objects and effects.
|
||||
|
||||
Author: Claude
|
||||
Purpose: Complete animation system demonstration
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
from mcrfpy import Color, Frame, Caption, Sprite, Grid, Entity, Texture, Animation
|
||||
import sys
|
||||
import math
|
||||
|
||||
# Configuration
|
||||
SCENE_WIDTH = 1280
|
||||
SCENE_HEIGHT = 720
|
||||
DEMO_DURATION = 5.0 # Duration for each demo section
|
||||
|
||||
# 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 current demo state
|
||||
current_demo = 0
|
||||
demo_start_time = 0
|
||||
demos = []
|
||||
|
||||
# Handle ESC key to exit
|
||||
def handle_keypress(scene_name, keycode):
|
||||
if keycode == 256: # ESC key
|
||||
print("Exiting animation sizzle reel...")
|
||||
sys.exit(0)
|
||||
|
||||
def create_demo_scene():
|
||||
"""Create the main demo scene with title"""
|
||||
mcrfpy.createScene("sizzle_reel")
|
||||
mcrfpy.setScene("sizzle_reel")
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
|
||||
# Title caption
|
||||
title = Caption("McRogueFace Animation Sizzle Reel",
|
||||
SCENE_WIDTH/2 - 200, 20)
|
||||
title.fill_color = Color(255, 255, 0)
|
||||
title.outline = 2
|
||||
title.outline_color = Color(0, 0, 0)
|
||||
ui.append(title)
|
||||
|
||||
# Subtitle showing current demo
|
||||
global subtitle
|
||||
subtitle = Caption("Initializing...",
|
||||
SCENE_WIDTH/2 - 150, 60)
|
||||
subtitle.fill_color = Color(200, 200, 200)
|
||||
ui.append(subtitle)
|
||||
|
||||
return ui
|
||||
|
||||
def demo_frame_basic_animations(ui):
|
||||
"""Demo 1: Basic frame animations - position, size, colors"""
|
||||
subtitle.text = "Demo 1: Frame Basic Animations (Position, Size, Colors)"
|
||||
|
||||
# Create test frame
|
||||
frame = Frame(100, 150, 200, 100)
|
||||
frame.fill_color = Color(50, 50, 150)
|
||||
frame.outline = 3
|
||||
frame.outline_color = Color(255, 255, 255)
|
||||
ui.append(frame)
|
||||
|
||||
# Position animations with different easings
|
||||
x_anim = Animation("x", 800.0, 2.0, "easeInOutBack")
|
||||
y_anim = Animation("y", 400.0, 2.0, "easeInOutElastic")
|
||||
x_anim.start(frame)
|
||||
y_anim.start(frame)
|
||||
|
||||
# Size animations
|
||||
w_anim = Animation("w", 400.0, 3.0, "easeInOutCubic")
|
||||
h_anim = Animation("h", 200.0, 3.0, "easeInOutCubic")
|
||||
w_anim.start(frame)
|
||||
h_anim.start(frame)
|
||||
|
||||
# Color animations - use tuples instead of Color objects
|
||||
fill_anim = Animation("fill_color", (255, 100, 50, 200), 4.0, "easeInOutSine")
|
||||
outline_anim = Animation("outline_color", (0, 255, 255, 255), 4.0, "easeOutBounce")
|
||||
fill_anim.start(frame)
|
||||
outline_anim.start(frame)
|
||||
|
||||
# Outline thickness animation
|
||||
thickness_anim = Animation("outline", 10.0, 4.5, "easeInOutQuad")
|
||||
thickness_anim.start(frame)
|
||||
|
||||
return frame
|
||||
|
||||
def demo_frame_opacity_zindex(ui):
|
||||
"""Demo 2: Frame opacity and z-index animations"""
|
||||
subtitle.text = "Demo 2: Frame Opacity & Z-Index Animations"
|
||||
|
||||
frames = []
|
||||
colors = [
|
||||
Color(255, 0, 0, 200),
|
||||
Color(0, 255, 0, 200),
|
||||
Color(0, 0, 255, 200),
|
||||
Color(255, 255, 0, 200)
|
||||
]
|
||||
|
||||
# Create overlapping frames
|
||||
for i in range(4):
|
||||
frame = Frame(200 + i*80, 200 + i*40, 200, 150)
|
||||
frame.fill_color = colors[i]
|
||||
frame.outline = 2
|
||||
frame.z_index = i
|
||||
ui.append(frame)
|
||||
frames.append(frame)
|
||||
|
||||
# Animate opacity in waves
|
||||
opacity_anim = Animation("opacity", 0.3, 2.0, "easeInOutSine")
|
||||
opacity_anim.start(frame)
|
||||
|
||||
# Reverse opacity animation
|
||||
opacity_back = Animation("opacity", 1.0, 2.0, "easeInOutSine", delta=False)
|
||||
mcrfpy.setTimer(f"opacity_back_{i}", lambda t, f=frame, a=opacity_back: a.start(f), 2000)
|
||||
|
||||
# Z-index shuffle animation
|
||||
z_anim = Animation("z_index", (i + 2) % 4, 3.0, "linear")
|
||||
z_anim.start(frame)
|
||||
|
||||
return frames
|
||||
|
||||
def demo_caption_animations(ui):
|
||||
"""Demo 3: Caption text animations and effects"""
|
||||
subtitle.text = "Demo 3: Caption Animations (Text, Color, Position)"
|
||||
|
||||
# Basic caption with position animation
|
||||
caption1 = Caption("Moving Text!", 100, 200)
|
||||
caption1.fill_color = Color(255, 255, 255)
|
||||
caption1.outline = 1
|
||||
ui.append(caption1)
|
||||
|
||||
# Animate across screen with bounce
|
||||
x_anim = Animation("x", 900.0, 3.0, "easeOutBounce")
|
||||
x_anim.start(caption1)
|
||||
|
||||
# Color cycling caption
|
||||
caption2 = Caption("Rainbow Colors", 400, 300)
|
||||
caption2.outline = 2
|
||||
ui.append(caption2)
|
||||
|
||||
# Cycle through colors - use tuples
|
||||
color_anim1 = Animation("fill_color", (255, 0, 0, 255), 1.0, "linear")
|
||||
color_anim2 = Animation("fill_color", (0, 255, 0, 255), 1.0, "linear")
|
||||
color_anim3 = Animation("fill_color", (0, 0, 255, 255), 1.0, "linear")
|
||||
color_anim4 = Animation("fill_color", (255, 255, 255, 255), 1.0, "linear")
|
||||
|
||||
color_anim1.start(caption2)
|
||||
mcrfpy.setTimer("color2", lambda t: color_anim2.start(caption2), 1000)
|
||||
mcrfpy.setTimer("color3", lambda t: color_anim3.start(caption2), 2000)
|
||||
mcrfpy.setTimer("color4", lambda t: color_anim4.start(caption2), 3000)
|
||||
|
||||
# Typewriter effect caption
|
||||
caption3 = Caption("", 100, 400)
|
||||
caption3.fill_color = Color(0, 255, 255)
|
||||
ui.append(caption3)
|
||||
|
||||
typewriter = Animation("text", "This text appears one character at a time...", 3.0, "linear")
|
||||
typewriter.start(caption3)
|
||||
|
||||
# Size animation caption
|
||||
caption4 = Caption("Growing Text", 400, 500)
|
||||
caption4.fill_color = Color(255, 200, 0)
|
||||
ui.append(caption4)
|
||||
|
||||
# Note: size animation would require font size property support
|
||||
# For now, animate position to simulate growth
|
||||
scale_sim = Animation("y", 480.0, 2.0, "easeInOutElastic")
|
||||
scale_sim.start(caption4)
|
||||
|
||||
return [caption1, caption2, caption3, caption4]
|
||||
|
||||
def demo_sprite_animations(ui):
|
||||
"""Demo 4: Sprite animations including sprite sequences"""
|
||||
subtitle.text = "Demo 4: Sprite Animations (Position, Scale, Sprite Sequences)"
|
||||
|
||||
# Load a test texture (you'll need to adjust path)
|
||||
try:
|
||||
texture = Texture("assets/sprites/player.png", grid_size=(32, 32))
|
||||
except:
|
||||
# Fallback if texture not found
|
||||
texture = None
|
||||
|
||||
if texture:
|
||||
# Basic sprite with position animation
|
||||
sprite1 = Sprite(100, 200, texture, sprite_index=0)
|
||||
sprite1.scale = 2.0
|
||||
ui.append(sprite1)
|
||||
|
||||
# Circular motion using sin/cos animations
|
||||
# We'll use delta mode to create circular motion
|
||||
x_circle = Animation("x", 300.0, 4.0, "easeInOutSine")
|
||||
y_circle = Animation("y", 300.0, 4.0, "easeInOutCubic")
|
||||
x_circle.start(sprite1)
|
||||
y_circle.start(sprite1)
|
||||
|
||||
# Sprite sequence animation (walking cycle)
|
||||
sprite2 = Sprite(500, 300, texture, sprite_index=0)
|
||||
sprite2.scale = 3.0
|
||||
ui.append(sprite2)
|
||||
|
||||
# Animate through sprite indices for animation
|
||||
walk_cycle = Animation("sprite_index", [0, 1, 2, 3, 2, 1], 2.0, "linear")
|
||||
walk_cycle.start(sprite2)
|
||||
|
||||
# Scale pulsing sprite
|
||||
sprite3 = Sprite(800, 400, texture, sprite_index=4)
|
||||
ui.append(sprite3)
|
||||
|
||||
# Note: scale animation would need to be supported
|
||||
# For now use position to simulate
|
||||
pulse_y = Animation("y", 380.0, 0.5, "easeInOutSine")
|
||||
pulse_y.start(sprite3)
|
||||
|
||||
# Z-index animation for layering
|
||||
sprite3_z = Animation("z_index", 10, 2.0, "linear")
|
||||
sprite3_z.start(sprite3)
|
||||
|
||||
return [sprite1, sprite2, sprite3]
|
||||
else:
|
||||
# Create placeholder caption if no texture
|
||||
no_texture = Caption("(Sprite demo requires texture file)", 400, 350)
|
||||
no_texture.fill_color = Color(255, 100, 100)
|
||||
ui.append(no_texture)
|
||||
return [no_texture]
|
||||
|
||||
def demo_grid_animations(ui):
|
||||
"""Demo 5: Grid animations (position, camera, zoom)"""
|
||||
subtitle.text = "Demo 5: Grid Animations (Position, Camera Effects)"
|
||||
|
||||
# Create a grid
|
||||
try:
|
||||
texture = Texture("assets/sprites/tiles.png", grid_size=(16, 16))
|
||||
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.fill_color = Color(20, 20, 40)
|
||||
ui.append(grid)
|
||||
|
||||
# Fill with some test pattern
|
||||
for y in range(15):
|
||||
for x in range(20):
|
||||
point = grid.at(x, y)
|
||||
point.tilesprite = (x + y) % 4
|
||||
point.walkable = ((x + y) % 3) != 0
|
||||
if not point.walkable:
|
||||
point.color = Color(100, 50, 50, 128)
|
||||
|
||||
# Animate grid position
|
||||
grid_x = Animation("x", 400.0, 3.0, "easeInOutBack")
|
||||
grid_x.start(grid)
|
||||
|
||||
# Camera pan animation (if supported)
|
||||
# center_x = Animation("center", (10.0, 7.5), 4.0, "easeInOutCubic")
|
||||
# center_x.start(grid)
|
||||
|
||||
# Create entities in the grid
|
||||
if texture:
|
||||
entity1 = Entity((5.0, 5.0), texture, 8) # position tuple, texture, sprite_index
|
||||
entity1.scale = 1.5
|
||||
grid.entities.append(entity1)
|
||||
|
||||
# Animate entity movement
|
||||
entity_pos = Animation("position", (15.0, 10.0), 3.0, "easeInOutQuad")
|
||||
entity_pos.start(entity1)
|
||||
|
||||
# Create patrolling entity
|
||||
entity2 = Entity((10.0, 2.0), texture, 12) # position tuple, texture, sprite_index
|
||||
grid.entities.append(entity2)
|
||||
|
||||
# Animate sprite changes
|
||||
entity2_sprite = Animation("sprite_index", [12, 13, 14, 15, 14, 13], 2.0, "linear")
|
||||
entity2_sprite.start(entity2)
|
||||
|
||||
return grid
|
||||
|
||||
def demo_complex_combinations(ui):
|
||||
"""Demo 6: Complex multi-property animations"""
|
||||
subtitle.text = "Demo 6: Complex Multi-Property Animations"
|
||||
|
||||
# Create a complex UI composition
|
||||
main_frame = Frame(200, 200, 400, 300)
|
||||
main_frame.fill_color = Color(30, 30, 60, 200)
|
||||
main_frame.outline = 2
|
||||
ui.append(main_frame)
|
||||
|
||||
# Child elements
|
||||
title = Caption("Multi-Animation Demo", 20, 20)
|
||||
title.fill_color = Color(255, 255, 255)
|
||||
main_frame.children.append(title)
|
||||
|
||||
# Animate everything at once
|
||||
# Frame animations
|
||||
frame_x = Animation("x", 600.0, 3.0, "easeInOutElastic")
|
||||
frame_w = Animation("w", 300.0, 2.5, "easeOutBack")
|
||||
frame_fill = Animation("fill_color", (60, 30, 90, 220), 4.0, "easeInOutSine")
|
||||
frame_outline = Animation("outline", 8.0, 3.0, "easeInOutQuad")
|
||||
|
||||
frame_x.start(main_frame)
|
||||
frame_w.start(main_frame)
|
||||
frame_fill.start(main_frame)
|
||||
frame_outline.start(main_frame)
|
||||
|
||||
# Title animations
|
||||
title_color = Animation("fill_color", (255, 200, 0, 255), 2.0, "easeOutBounce")
|
||||
title_color.start(title)
|
||||
|
||||
# Add animated sub-frames
|
||||
for i in range(3):
|
||||
sub_frame = Frame(50 + i * 100, 100, 80, 80)
|
||||
sub_frame.fill_color = Color(100 + i*50, 50, 200 - i*50, 180)
|
||||
main_frame.children.append(sub_frame)
|
||||
|
||||
# Rotate positions using delta animations
|
||||
sub_y = Animation("y", 50.0, 2.0, "easeInOutSine", delta=True)
|
||||
sub_y.start(sub_frame)
|
||||
|
||||
return main_frame
|
||||
|
||||
def demo_easing_showcase(ui):
|
||||
"""Demo 7: Showcase all 30 easing functions"""
|
||||
subtitle.text = "Demo 7: All 30 Easing Functions Showcase"
|
||||
|
||||
# Create small frames for each easing function
|
||||
frames_per_row = 6
|
||||
frame_size = 180
|
||||
spacing = 10
|
||||
|
||||
for i, easing in enumerate(EASING_FUNCTIONS[:12]): # First 12 easings
|
||||
row = i // frames_per_row
|
||||
col = i % frames_per_row
|
||||
|
||||
x = 50 + col * (frame_size + spacing)
|
||||
y = 150 + row * (60 + spacing)
|
||||
|
||||
# Create indicator frame
|
||||
frame = Frame(x, y, 20, 20)
|
||||
frame.fill_color = Color(100, 200, 255)
|
||||
frame.outline = 1
|
||||
ui.append(frame)
|
||||
|
||||
# Label
|
||||
label = Caption(easing, x, y - 20)
|
||||
label.fill_color = Color(200, 200, 200)
|
||||
ui.append(label)
|
||||
|
||||
# Animate using this easing
|
||||
move_anim = Animation("x", x + frame_size - 20, 3.0, easing)
|
||||
move_anim.start(frame)
|
||||
|
||||
# Continue with remaining easings after a delay
|
||||
def show_more_easings(runtime):
|
||||
for j, easing in enumerate(EASING_FUNCTIONS[12:24]): # Next 12
|
||||
row = j // frames_per_row + 2
|
||||
col = j % frames_per_row
|
||||
|
||||
x = 50 + col * (frame_size + spacing)
|
||||
y = 150 + row * (60 + spacing)
|
||||
|
||||
frame2 = Frame(x, y, 20, 20)
|
||||
frame2.fill_color = Color(255, 150, 100)
|
||||
frame2.outline = 1
|
||||
ui.append(frame2)
|
||||
|
||||
label2 = Caption(easing, x, y - 20)
|
||||
label2.fill_color = Color(200, 200, 200)
|
||||
ui.append(label2)
|
||||
|
||||
move_anim2 = Animation("x", x + frame_size - 20, 3.0, easing)
|
||||
move_anim2.start(frame2)
|
||||
|
||||
mcrfpy.setTimer("more_easings", show_more_easings, 1000)
|
||||
|
||||
# Show final easings
|
||||
def show_final_easings(runtime):
|
||||
for k, easing in enumerate(EASING_FUNCTIONS[24:]): # Last 6
|
||||
row = k // frames_per_row + 4
|
||||
col = k % frames_per_row
|
||||
|
||||
x = 50 + col * (frame_size + spacing)
|
||||
y = 150 + row * (60 + spacing)
|
||||
|
||||
frame3 = Frame(x, y, 20, 20)
|
||||
frame3.fill_color = Color(150, 255, 150)
|
||||
frame3.outline = 1
|
||||
ui.append(frame3)
|
||||
|
||||
label3 = Caption(easing, x, y - 20)
|
||||
label3.fill_color = Color(200, 200, 200)
|
||||
ui.append(label3)
|
||||
|
||||
move_anim3 = Animation("x", x + frame_size - 20, 3.0, easing)
|
||||
move_anim3.start(frame3)
|
||||
|
||||
mcrfpy.setTimer("final_easings", show_final_easings, 2000)
|
||||
|
||||
def demo_delta_animations(ui):
|
||||
"""Demo 8: Delta mode animations (relative movements)"""
|
||||
subtitle.text = "Demo 8: Delta Mode Animations (Relative Movements)"
|
||||
|
||||
# Create objects that will move relative to their position
|
||||
frames = []
|
||||
start_positions = [(100, 200), (300, 200), (500, 200), (700, 200)]
|
||||
colors = [Color(255, 100, 100), Color(100, 255, 100),
|
||||
Color(100, 100, 255), Color(255, 255, 100)]
|
||||
|
||||
for i, (x, y) in enumerate(start_positions):
|
||||
frame = Frame(x, y, 80, 80)
|
||||
frame.fill_color = colors[i]
|
||||
frame.outline = 2
|
||||
ui.append(frame)
|
||||
frames.append(frame)
|
||||
|
||||
# Delta animations - move relative to current position
|
||||
# Each frame moves by different amounts
|
||||
dx = (i + 1) * 50
|
||||
dy = math.sin(i) * 100
|
||||
|
||||
x_delta = Animation("x", dx, 2.0, "easeInOutBack", delta=True)
|
||||
y_delta = Animation("y", dy, 2.0, "easeInOutElastic", delta=True)
|
||||
|
||||
x_delta.start(frame)
|
||||
y_delta.start(frame)
|
||||
|
||||
# Create caption showing delta mode
|
||||
delta_label = Caption("Delta mode: Relative animations from current position", 200, 400)
|
||||
delta_label.fill_color = Color(255, 255, 255)
|
||||
ui.append(delta_label)
|
||||
|
||||
# Animate the label with delta mode text append
|
||||
text_delta = Animation("text", " - ANIMATED!", 2.0, "linear", delta=True)
|
||||
text_delta.start(delta_label)
|
||||
|
||||
return frames
|
||||
|
||||
def demo_color_component_animations(ui):
|
||||
"""Demo 9: Individual color channel animations"""
|
||||
subtitle.text = "Demo 9: Color Component Animations (R, G, B, A channels)"
|
||||
|
||||
# Create frames to demonstrate individual color channel animations
|
||||
base_frame = Frame(300, 200, 600, 300)
|
||||
base_frame.fill_color = Color(128, 128, 128, 255)
|
||||
base_frame.outline = 3
|
||||
ui.append(base_frame)
|
||||
|
||||
# Labels for each channel
|
||||
labels = ["Red", "Green", "Blue", "Alpha"]
|
||||
positions = [(50, 50), (200, 50), (350, 50), (500, 50)]
|
||||
|
||||
for i, (label_text, (x, y)) in enumerate(zip(labels, positions)):
|
||||
# Create label
|
||||
label = Caption(label_text, x, y - 30)
|
||||
label.fill_color = Color(255, 255, 255)
|
||||
base_frame.children.append(label)
|
||||
|
||||
# Create demo frame for this channel
|
||||
demo_frame = Frame(x, y, 100, 100)
|
||||
demo_frame.fill_color = Color(100, 100, 100, 200)
|
||||
demo_frame.outline = 2
|
||||
base_frame.children.append(demo_frame)
|
||||
|
||||
# Animate individual color channel
|
||||
if i == 0: # Red
|
||||
r_anim = Animation("fill_color.r", 255, 3.0, "easeInOutSine")
|
||||
r_anim.start(demo_frame)
|
||||
elif i == 1: # Green
|
||||
g_anim = Animation("fill_color.g", 255, 3.0, "easeInOutSine")
|
||||
g_anim.start(demo_frame)
|
||||
elif i == 2: # Blue
|
||||
b_anim = Animation("fill_color.b", 255, 3.0, "easeInOutSine")
|
||||
b_anim.start(demo_frame)
|
||||
else: # Alpha
|
||||
a_anim = Animation("fill_color.a", 50, 3.0, "easeInOutSine")
|
||||
a_anim.start(demo_frame)
|
||||
|
||||
# Animate main frame outline color components in sequence
|
||||
outline_r = Animation("outline_color.r", 255, 1.0, "linear")
|
||||
outline_g = Animation("outline_color.g", 255, 1.0, "linear")
|
||||
outline_b = Animation("outline_color.b", 0, 1.0, "linear")
|
||||
|
||||
outline_r.start(base_frame)
|
||||
mcrfpy.setTimer("outline_g", lambda t: outline_g.start(base_frame), 1000)
|
||||
mcrfpy.setTimer("outline_b", lambda t: outline_b.start(base_frame), 2000)
|
||||
|
||||
return base_frame
|
||||
|
||||
def demo_performance_stress_test(ui):
|
||||
"""Demo 10: Performance test with many simultaneous animations"""
|
||||
subtitle.text = "Demo 10: Performance Stress Test (100+ Simultaneous Animations)"
|
||||
|
||||
# Create many small objects with different animations
|
||||
num_objects = 100
|
||||
|
||||
for i in range(num_objects):
|
||||
# Random starting position
|
||||
x = 100 + (i % 20) * 50
|
||||
y = 150 + (i // 20) * 50
|
||||
|
||||
# Create small frame
|
||||
size = 20 + (i % 3) * 10
|
||||
frame = Frame(x, y, size, size)
|
||||
|
||||
# Random color
|
||||
r = (i * 37) % 256
|
||||
g = (i * 73) % 256
|
||||
b = (i * 113) % 256
|
||||
frame.fill_color = Color(r, g, b, 200)
|
||||
frame.outline = 1
|
||||
ui.append(frame)
|
||||
|
||||
# Random animation properties
|
||||
target_x = 100 + (i % 15) * 70
|
||||
target_y = 150 + (i // 15) * 70
|
||||
duration = 2.0 + (i % 30) * 0.1
|
||||
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
|
||||
|
||||
# Start multiple animations per object
|
||||
x_anim = Animation("x", target_x, duration, easing)
|
||||
y_anim = Animation("y", target_y, duration, easing)
|
||||
opacity_anim = Animation("opacity", 0.3 + (i % 7) * 0.1, duration, "easeInOutSine")
|
||||
|
||||
x_anim.start(frame)
|
||||
y_anim.start(frame)
|
||||
opacity_anim.start(frame)
|
||||
|
||||
# Performance counter
|
||||
perf_caption = Caption(f"Animating {num_objects * 3} properties simultaneously", 400, 600)
|
||||
perf_caption.fill_color = Color(255, 255, 0)
|
||||
ui.append(perf_caption)
|
||||
|
||||
def next_demo(runtime):
|
||||
"""Cycle to the next demo"""
|
||||
global current_demo, demo_start_time
|
||||
|
||||
# Clear the UI except title and subtitle
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
|
||||
# Keep only the first two elements (title and subtitle)
|
||||
while len(ui) > 2:
|
||||
# Remove from the end to avoid index issues
|
||||
ui.remove(len(ui) - 1)
|
||||
|
||||
# Run the next demo
|
||||
if current_demo < len(demos):
|
||||
demos[current_demo](ui)
|
||||
current_demo += 1
|
||||
|
||||
# Schedule next demo
|
||||
if current_demo < len(demos):
|
||||
mcrfpy.setTimer("next_demo", next_demo, int(DEMO_DURATION * 1000))
|
||||
else:
|
||||
# All demos complete
|
||||
subtitle.text = "Animation Showcase Complete! Press ESC to exit."
|
||||
complete = Caption("All animation types demonstrated!", 400, 350)
|
||||
complete.fill_color = Color(0, 255, 0)
|
||||
complete.outline = 2
|
||||
ui.append(complete)
|
||||
|
||||
def run_sizzle_reel(runtime):
|
||||
"""Main entry point - start the demo sequence"""
|
||||
global demos
|
||||
|
||||
# List of all demo functions
|
||||
demos = [
|
||||
demo_frame_basic_animations,
|
||||
demo_frame_opacity_zindex,
|
||||
demo_caption_animations,
|
||||
demo_sprite_animations,
|
||||
demo_grid_animations,
|
||||
demo_complex_combinations,
|
||||
demo_easing_showcase,
|
||||
demo_delta_animations,
|
||||
demo_color_component_animations,
|
||||
demo_performance_stress_test
|
||||
]
|
||||
|
||||
# Start the first demo
|
||||
next_demo(runtime)
|
||||
|
||||
# Initialize scene
|
||||
ui = create_demo_scene()
|
||||
|
||||
|
||||
# Start the sizzle reel after a short delay
|
||||
mcrfpy.setTimer("start_sizzle", run_sizzle_reel, 500)
|
||||
|
||||
print("Starting McRogueFace Animation Sizzle Reel...")
|
||||
print("This will demonstrate ALL animation types on ALL objects.")
|
||||
print("Press ESC at any time to exit.")
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Animation Sizzle Reel (Fixed)
|
||||
=========================================
|
||||
|
||||
This script demonstrates EVERY animation type on EVERY UI object type.
|
||||
Fixed version that works properly with the game loop.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Configuration
|
||||
SCENE_WIDTH = 1280
|
||||
SCENE_HEIGHT = 720
|
||||
DEMO_DURATION = 5.0 # Duration for each demo section
|
||||
|
||||
# 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 current demo state
|
||||
current_demo = 0
|
||||
subtitle = None
|
||||
|
||||
def create_demo_scene():
|
||||
"""Create the main demo scene with title"""
|
||||
mcrfpy.createScene("sizzle_reel")
|
||||
mcrfpy.setScene("sizzle_reel")
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
|
||||
# Title caption
|
||||
title = mcrfpy.Caption("McRogueFace Animation Sizzle Reel",
|
||||
SCENE_WIDTH/2 - 200, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
title.outline = 2
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
ui.append(title)
|
||||
|
||||
# Subtitle showing current demo
|
||||
global subtitle
|
||||
subtitle = mcrfpy.Caption("Initializing...",
|
||||
SCENE_WIDTH/2 - 150, 60)
|
||||
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(subtitle)
|
||||
|
||||
return ui
|
||||
|
||||
def demo_frame_basic_animations():
|
||||
"""Demo 1: Basic frame animations - position, size, colors"""
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 1: Frame Basic Animations (Position, Size, Colors)"
|
||||
|
||||
# Create test frame
|
||||
frame = mcrfpy.Frame(100, 150, 200, 100)
|
||||
frame.fill_color = mcrfpy.Color(50, 50, 150)
|
||||
frame.outline = 3
|
||||
frame.outline_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(frame)
|
||||
|
||||
# Position animations with different easings
|
||||
x_anim = mcrfpy.Animation("x", 800.0, 2.0, "easeInOutBack")
|
||||
y_anim = mcrfpy.Animation("y", 400.0, 2.0, "easeInOutElastic")
|
||||
x_anim.start(frame)
|
||||
y_anim.start(frame)
|
||||
|
||||
# Size animations
|
||||
w_anim = mcrfpy.Animation("w", 400.0, 3.0, "easeInOutCubic")
|
||||
h_anim = mcrfpy.Animation("h", 200.0, 3.0, "easeInOutCubic")
|
||||
w_anim.start(frame)
|
||||
h_anim.start(frame)
|
||||
|
||||
# Color animations
|
||||
fill_anim = mcrfpy.Animation("fill_color", mcrfpy.Color(255, 100, 50, 200), 4.0, "easeInOutSine")
|
||||
outline_anim = mcrfpy.Animation("outline_color", mcrfpy.Color(0, 255, 255), 4.0, "easeOutBounce")
|
||||
fill_anim.start(frame)
|
||||
outline_anim.start(frame)
|
||||
|
||||
# Outline thickness animation
|
||||
thickness_anim = mcrfpy.Animation("outline", 10.0, 4.5, "easeInOutQuad")
|
||||
thickness_anim.start(frame)
|
||||
|
||||
def demo_caption_animations():
|
||||
"""Demo 2: Caption text animations and effects"""
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 2: Caption Animations (Text, Color, Position)"
|
||||
|
||||
# Basic caption with position animation
|
||||
caption1 = mcrfpy.Caption("Moving Text!", 100, 200)
|
||||
caption1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
caption1.outline = 1
|
||||
ui.append(caption1)
|
||||
|
||||
# Animate across screen with bounce
|
||||
x_anim = mcrfpy.Animation("x", 900.0, 3.0, "easeOutBounce")
|
||||
x_anim.start(caption1)
|
||||
|
||||
# Color cycling caption
|
||||
caption2 = mcrfpy.Caption("Rainbow Colors", 400, 300)
|
||||
caption2.outline = 2
|
||||
ui.append(caption2)
|
||||
|
||||
# Cycle through colors
|
||||
color_anim1 = mcrfpy.Animation("fill_color", mcrfpy.Color(255, 0, 0), 1.0, "linear")
|
||||
color_anim1.start(caption2)
|
||||
|
||||
# Typewriter effect caption
|
||||
caption3 = mcrfpy.Caption("", 100, 400)
|
||||
caption3.fill_color = mcrfpy.Color(0, 255, 255)
|
||||
ui.append(caption3)
|
||||
|
||||
typewriter = mcrfpy.Animation("text", "This text appears one character at a time...", 3.0, "linear")
|
||||
typewriter.start(caption3)
|
||||
|
||||
def demo_sprite_animations():
|
||||
"""Demo 3: Sprite animations (if texture available)"""
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 3: Sprite Animations"
|
||||
|
||||
# Create placeholder caption since texture might not exist
|
||||
no_texture = mcrfpy.Caption("(Sprite demo - textures may not be loaded)", 400, 350)
|
||||
no_texture.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
ui.append(no_texture)
|
||||
|
||||
def demo_performance_stress_test():
|
||||
"""Demo 4: Performance test with many simultaneous animations"""
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 4: Performance Test (50+ Simultaneous Animations)"
|
||||
|
||||
# Create many small objects with different animations
|
||||
num_objects = 50
|
||||
|
||||
for i in range(num_objects):
|
||||
# Random starting position
|
||||
x = 100 + (i % 10) * 100
|
||||
y = 150 + (i // 10) * 80
|
||||
|
||||
# Create small frame
|
||||
size = 20 + (i % 3) * 10
|
||||
frame = mcrfpy.Frame(x, y, size, size)
|
||||
|
||||
# Random color
|
||||
r = (i * 37) % 256
|
||||
g = (i * 73) % 256
|
||||
b = (i * 113) % 256
|
||||
frame.fill_color = mcrfpy.Color(r, g, b, 200)
|
||||
frame.outline = 1
|
||||
ui.append(frame)
|
||||
|
||||
# Random animation properties
|
||||
target_x = 100 + (i % 8) * 120
|
||||
target_y = 150 + (i // 8) * 100
|
||||
duration = 2.0 + (i % 30) * 0.1
|
||||
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
|
||||
|
||||
# Start multiple animations per object
|
||||
x_anim = mcrfpy.Animation("x", float(target_x), duration, easing)
|
||||
y_anim = mcrfpy.Animation("y", float(target_y), duration, easing)
|
||||
opacity_anim = mcrfpy.Animation("opacity", 0.3 + (i % 7) * 0.1, duration, "easeInOutSine")
|
||||
|
||||
x_anim.start(frame)
|
||||
y_anim.start(frame)
|
||||
opacity_anim.start(frame)
|
||||
|
||||
# Performance counter
|
||||
perf_caption = mcrfpy.Caption(f"Animating {num_objects * 3} properties simultaneously", 400, 600)
|
||||
perf_caption.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
ui.append(perf_caption)
|
||||
|
||||
def clear_scene():
|
||||
"""Clear the scene except title and subtitle"""
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
|
||||
# Keep only the first two elements (title and subtitle)
|
||||
while len(ui) > 2:
|
||||
ui.remove(2)
|
||||
|
||||
def run_demo_sequence(runtime):
|
||||
"""Run through all demos"""
|
||||
global current_demo
|
||||
|
||||
# Clear previous demo
|
||||
clear_scene()
|
||||
|
||||
# Demo list
|
||||
demos = [
|
||||
demo_frame_basic_animations,
|
||||
demo_caption_animations,
|
||||
demo_sprite_animations,
|
||||
demo_performance_stress_test
|
||||
]
|
||||
|
||||
if current_demo < len(demos):
|
||||
# Run current demo
|
||||
demos[current_demo]()
|
||||
current_demo += 1
|
||||
|
||||
# Schedule next demo
|
||||
if current_demo < len(demos):
|
||||
mcrfpy.setTimer("next_demo", run_demo_sequence, int(DEMO_DURATION * 1000))
|
||||
else:
|
||||
# All demos complete
|
||||
subtitle.text = "Animation Showcase Complete!"
|
||||
complete = mcrfpy.Caption("All animation types demonstrated!", 400, 350)
|
||||
complete.fill_color = mcrfpy.Color(0, 255, 0)
|
||||
complete.outline = 2
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
ui.append(complete)
|
||||
|
||||
# Initialize scene
|
||||
print("Starting McRogueFace Animation Sizzle Reel...")
|
||||
print("This will demonstrate animation types on various objects.")
|
||||
|
||||
ui = create_demo_scene()
|
||||
|
||||
# Start the demo sequence after a short delay
|
||||
mcrfpy.setTimer("start_demos", run_demo_sequence, 500)
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Animation Sizzle Reel v2
|
||||
====================================
|
||||
|
||||
Fixed version with proper API usage for animations and collections.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Configuration
|
||||
SCENE_WIDTH = 1280
|
||||
SCENE_HEIGHT = 720
|
||||
DEMO_DURATION = 5.0 # Duration for each demo section
|
||||
|
||||
# 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 current demo state
|
||||
current_demo = 0
|
||||
subtitle = None
|
||||
demo_objects = [] # Track objects from current demo
|
||||
|
||||
def create_demo_scene():
|
||||
"""Create the main demo scene with title"""
|
||||
mcrfpy.createScene("sizzle_reel")
|
||||
mcrfpy.setScene("sizzle_reel")
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
|
||||
# Title caption
|
||||
title = mcrfpy.Caption("McRogueFace Animation Sizzle Reel",
|
||||
SCENE_WIDTH/2 - 200, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
title.outline = 2
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
ui.append(title)
|
||||
|
||||
# Subtitle showing current demo
|
||||
global subtitle
|
||||
subtitle = mcrfpy.Caption("Initializing...",
|
||||
SCENE_WIDTH/2 - 150, 60)
|
||||
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(subtitle)
|
||||
|
||||
return ui
|
||||
|
||||
def demo_frame_basic_animations():
|
||||
"""Demo 1: Basic frame animations - position, size, colors"""
|
||||
global demo_objects
|
||||
demo_objects = []
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 1: Frame Basic Animations (Position, Size, Colors)"
|
||||
|
||||
# Create test frame
|
||||
frame = mcrfpy.Frame(100, 150, 200, 100)
|
||||
frame.fill_color = mcrfpy.Color(50, 50, 150)
|
||||
frame.outline = 3
|
||||
frame.outline_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(frame)
|
||||
demo_objects.append(frame)
|
||||
|
||||
# Position animations with different easings
|
||||
x_anim = mcrfpy.Animation("x", 800.0, 2.0, "easeInOutBack")
|
||||
y_anim = mcrfpy.Animation("y", 400.0, 2.0, "easeInOutElastic")
|
||||
x_anim.start(frame)
|
||||
y_anim.start(frame)
|
||||
|
||||
# Size animations
|
||||
w_anim = mcrfpy.Animation("w", 400.0, 3.0, "easeInOutCubic")
|
||||
h_anim = mcrfpy.Animation("h", 200.0, 3.0, "easeInOutCubic")
|
||||
w_anim.start(frame)
|
||||
h_anim.start(frame)
|
||||
|
||||
# Color animations - use tuples instead of Color objects
|
||||
fill_anim = mcrfpy.Animation("fill_color", (255, 100, 50, 200), 4.0, "easeInOutSine")
|
||||
outline_anim = mcrfpy.Animation("outline_color", (0, 255, 255, 255), 4.0, "easeOutBounce")
|
||||
fill_anim.start(frame)
|
||||
outline_anim.start(frame)
|
||||
|
||||
# Outline thickness animation
|
||||
thickness_anim = mcrfpy.Animation("outline", 10.0, 4.5, "easeInOutQuad")
|
||||
thickness_anim.start(frame)
|
||||
|
||||
def demo_caption_animations():
|
||||
"""Demo 2: Caption text animations and effects"""
|
||||
global demo_objects
|
||||
demo_objects = []
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 2: Caption Animations (Text, Color, Position)"
|
||||
|
||||
# Basic caption with position animation
|
||||
caption1 = mcrfpy.Caption("Moving Text!", 100, 200)
|
||||
caption1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
caption1.outline = 1
|
||||
ui.append(caption1)
|
||||
demo_objects.append(caption1)
|
||||
|
||||
# Animate across screen with bounce
|
||||
x_anim = mcrfpy.Animation("x", 900.0, 3.0, "easeOutBounce")
|
||||
x_anim.start(caption1)
|
||||
|
||||
# Color cycling caption
|
||||
caption2 = mcrfpy.Caption("Rainbow Colors", 400, 300)
|
||||
caption2.outline = 2
|
||||
ui.append(caption2)
|
||||
demo_objects.append(caption2)
|
||||
|
||||
# Cycle through colors using tuples
|
||||
color_anim1 = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear")
|
||||
color_anim1.start(caption2)
|
||||
|
||||
# Schedule color changes
|
||||
def change_to_green(rt):
|
||||
color_anim2 = mcrfpy.Animation("fill_color", (0, 255, 0, 255), 1.0, "linear")
|
||||
color_anim2.start(caption2)
|
||||
|
||||
def change_to_blue(rt):
|
||||
color_anim3 = mcrfpy.Animation("fill_color", (0, 0, 255, 255), 1.0, "linear")
|
||||
color_anim3.start(caption2)
|
||||
|
||||
def change_to_white(rt):
|
||||
color_anim4 = mcrfpy.Animation("fill_color", (255, 255, 255, 255), 1.0, "linear")
|
||||
color_anim4.start(caption2)
|
||||
|
||||
mcrfpy.setTimer("color2", change_to_green, 1000)
|
||||
mcrfpy.setTimer("color3", change_to_blue, 2000)
|
||||
mcrfpy.setTimer("color4", change_to_white, 3000)
|
||||
|
||||
# Typewriter effect caption
|
||||
caption3 = mcrfpy.Caption("", 100, 400)
|
||||
caption3.fill_color = mcrfpy.Color(0, 255, 255)
|
||||
ui.append(caption3)
|
||||
demo_objects.append(caption3)
|
||||
|
||||
typewriter = mcrfpy.Animation("text", "This text appears one character at a time...", 3.0, "linear")
|
||||
typewriter.start(caption3)
|
||||
|
||||
def demo_easing_showcase():
|
||||
"""Demo 3: Showcase different easing functions"""
|
||||
global demo_objects
|
||||
demo_objects = []
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 3: Easing Functions Showcase"
|
||||
|
||||
# Create small frames for each easing function
|
||||
frames_per_row = 6
|
||||
frame_width = 180
|
||||
spacing = 10
|
||||
|
||||
# Show first 12 easings
|
||||
for i, easing in enumerate(EASING_FUNCTIONS[:12]):
|
||||
row = i // frames_per_row
|
||||
col = i % frames_per_row
|
||||
|
||||
x = 50 + col * (frame_width + spacing)
|
||||
y = 150 + row * (80 + spacing)
|
||||
|
||||
# Create indicator frame
|
||||
frame = mcrfpy.Frame(x, y, 20, 20)
|
||||
frame.fill_color = mcrfpy.Color(100, 200, 255)
|
||||
frame.outline = 1
|
||||
ui.append(frame)
|
||||
demo_objects.append(frame)
|
||||
|
||||
# Label
|
||||
label = mcrfpy.Caption(easing[:8], x, y - 20) # Truncate long names
|
||||
label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(label)
|
||||
demo_objects.append(label)
|
||||
|
||||
# Animate using this easing
|
||||
move_anim = mcrfpy.Animation("x", float(x + frame_width - 20), 3.0, easing)
|
||||
move_anim.start(frame)
|
||||
|
||||
def demo_performance_stress_test():
|
||||
"""Demo 4: Performance test with many simultaneous animations"""
|
||||
global demo_objects
|
||||
demo_objects = []
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
subtitle.text = "Demo 4: Performance Test (50+ Simultaneous Animations)"
|
||||
|
||||
# Create many small objects with different animations
|
||||
num_objects = 50
|
||||
|
||||
for i in range(num_objects):
|
||||
# Starting position
|
||||
x = 100 + (i % 10) * 100
|
||||
y = 150 + (i // 10) * 80
|
||||
|
||||
# Create small frame
|
||||
size = 20 + (i % 3) * 10
|
||||
frame = mcrfpy.Frame(x, y, size, size)
|
||||
|
||||
# Random color
|
||||
r = (i * 37) % 256
|
||||
g = (i * 73) % 256
|
||||
b = (i * 113) % 256
|
||||
frame.fill_color = mcrfpy.Color(r, g, b, 200)
|
||||
frame.outline = 1
|
||||
ui.append(frame)
|
||||
demo_objects.append(frame)
|
||||
|
||||
# Random animation properties
|
||||
target_x = 100 + (i % 8) * 120
|
||||
target_y = 150 + (i // 8) * 100
|
||||
duration = 2.0 + (i % 30) * 0.1
|
||||
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
|
||||
|
||||
# Start multiple animations per object
|
||||
x_anim = mcrfpy.Animation("x", float(target_x), duration, easing)
|
||||
y_anim = mcrfpy.Animation("y", float(target_y), duration, easing)
|
||||
opacity_anim = mcrfpy.Animation("opacity", 0.3 + (i % 7) * 0.1, duration, "easeInOutSine")
|
||||
|
||||
x_anim.start(frame)
|
||||
y_anim.start(frame)
|
||||
opacity_anim.start(frame)
|
||||
|
||||
# Performance counter
|
||||
perf_caption = mcrfpy.Caption(f"Animating {num_objects * 3} properties simultaneously", 350, 600)
|
||||
perf_caption.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
ui.append(perf_caption)
|
||||
demo_objects.append(perf_caption)
|
||||
|
||||
def clear_scene():
|
||||
"""Clear the scene except title and subtitle"""
|
||||
global demo_objects
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
|
||||
# Remove all demo objects
|
||||
for obj in demo_objects:
|
||||
try:
|
||||
# Find index of object
|
||||
for i in range(len(ui)):
|
||||
if ui[i] is obj:
|
||||
ui.remove(ui[i])
|
||||
break
|
||||
except:
|
||||
pass # Object might already be removed
|
||||
|
||||
demo_objects = []
|
||||
|
||||
# Clean up any timers
|
||||
for timer_name in ["color2", "color3", "color4"]:
|
||||
try:
|
||||
mcrfpy.delTimer(timer_name)
|
||||
except:
|
||||
pass
|
||||
|
||||
def run_demo_sequence(runtime):
|
||||
"""Run through all demos"""
|
||||
global current_demo
|
||||
|
||||
# Clear previous demo
|
||||
clear_scene()
|
||||
|
||||
# Demo list
|
||||
demos = [
|
||||
demo_frame_basic_animations,
|
||||
demo_caption_animations,
|
||||
demo_easing_showcase,
|
||||
demo_performance_stress_test
|
||||
]
|
||||
|
||||
if current_demo < len(demos):
|
||||
# Run current demo
|
||||
demos[current_demo]()
|
||||
current_demo += 1
|
||||
|
||||
# Schedule next demo
|
||||
if current_demo < len(demos):
|
||||
mcrfpy.setTimer("next_demo", run_demo_sequence, int(DEMO_DURATION * 1000))
|
||||
else:
|
||||
# Final demo completed
|
||||
def show_complete(rt):
|
||||
subtitle.text = "Animation Showcase Complete!"
|
||||
complete = mcrfpy.Caption("All animation types demonstrated!", 400, 350)
|
||||
complete.fill_color = mcrfpy.Color(0, 255, 0)
|
||||
complete.outline = 2
|
||||
ui = mcrfpy.sceneUI("sizzle_reel")
|
||||
ui.append(complete)
|
||||
|
||||
mcrfpy.setTimer("complete", show_complete, 3000)
|
||||
|
||||
# Initialize scene
|
||||
print("Starting McRogueFace Animation Sizzle Reel v2...")
|
||||
print("This will demonstrate animation types on various objects.")
|
||||
|
||||
ui = create_demo_scene()
|
||||
|
||||
# Start the demo sequence after a short delay
|
||||
mcrfpy.setTimer("start_demos", run_demo_sequence, 500)
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Animation Sizzle Reel - Working Version
|
||||
===================================================
|
||||
|
||||
Complete demonstration of all animation capabilities.
|
||||
Fixed to work properly with the API.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
import math
|
||||
|
||||
# Configuration
|
||||
DEMO_DURATION = 7.0 # Duration for each demo
|
||||
|
||||
# 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 state
|
||||
current_demo = 0
|
||||
subtitle = None
|
||||
demo_objects = []
|
||||
|
||||
def create_scene():
|
||||
"""Create the demo scene with title"""
|
||||
mcrfpy.createScene("sizzle")
|
||||
mcrfpy.setScene("sizzle")
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("McRogueFace Animation Sizzle Reel", 340, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
title.outline = 2
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
ui.append(title)
|
||||
|
||||
# Subtitle
|
||||
global subtitle
|
||||
subtitle = mcrfpy.Caption("Initializing...", 400, 60)
|
||||
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(subtitle)
|
||||
|
||||
def clear_demo():
|
||||
"""Clear demo objects"""
|
||||
global demo_objects
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Remove items starting from the end
|
||||
# Skip first 2 (title and subtitle)
|
||||
while len(ui) > 2:
|
||||
ui.remove(len(ui) - 1)
|
||||
|
||||
demo_objects = []
|
||||
|
||||
def demo1_frame_basics():
|
||||
"""Demo 1: Basic frame animations"""
|
||||
clear_demo()
|
||||
print("demo1")
|
||||
subtitle.text = "Demo 1: Frame Animations (Position, Size, Color)"
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Create frame
|
||||
frame = mcrfpy.Frame(100, 150, 200, 100)
|
||||
frame.fill_color = mcrfpy.Color(50, 50, 150)
|
||||
frame.outline = 3
|
||||
frame.outline_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(frame)
|
||||
|
||||
# Animate properties
|
||||
mcrfpy.Animation("x", 700.0, 2.5, "easeInOutBack").start(frame)
|
||||
mcrfpy.Animation("y", 350.0, 2.5, "easeInOutElastic").start(frame)
|
||||
mcrfpy.Animation("w", 350.0, 3.0, "easeInOutCubic").start(frame)
|
||||
mcrfpy.Animation("h", 180.0, 3.0, "easeInOutCubic").start(frame)
|
||||
mcrfpy.Animation("fill_color", (255, 100, 50, 200), 4.0, "easeInOutSine").start(frame)
|
||||
mcrfpy.Animation("outline_color", (0, 255, 255, 255), 4.0, "easeOutBounce").start(frame)
|
||||
mcrfpy.Animation("outline", 8.0, 4.0, "easeInOutQuad").start(frame)
|
||||
|
||||
def demo2_opacity_zindex():
|
||||
"""Demo 2: Opacity and z-index animations"""
|
||||
clear_demo()
|
||||
print("demo2")
|
||||
subtitle.text = "Demo 2: Opacity & Z-Index Animations"
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Create overlapping frames
|
||||
colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)]
|
||||
|
||||
for i in range(4):
|
||||
frame = mcrfpy.Frame(200 + i*80, 200 + i*40, 200, 150)
|
||||
frame.fill_color = mcrfpy.Color(colors[i][0], colors[i][1], colors[i][2], 200)
|
||||
frame.outline = 2
|
||||
frame.z_index = i
|
||||
ui.append(frame)
|
||||
|
||||
# Animate opacity
|
||||
mcrfpy.Animation("opacity", 0.3, 2.0, "easeInOutSine").start(frame)
|
||||
|
||||
# Schedule opacity return
|
||||
def return_opacity(rt):
|
||||
for i in range(4):
|
||||
mcrfpy.Animation("opacity", 1.0, 2.0, "easeInOutSine").start(ui[i])
|
||||
mcrfpy.setTimer(f"opacity_{i}", return_opacity, 2100)
|
||||
|
||||
def demo3_captions():
|
||||
"""Demo 3: Caption animations"""
|
||||
clear_demo()
|
||||
print("demo3")
|
||||
subtitle.text = "Demo 3: Caption Animations"
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Moving caption
|
||||
c1 = mcrfpy.Caption("Bouncing Text!", 100, 200)
|
||||
c1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
c1.outline = 1
|
||||
ui.append(c1)
|
||||
mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1)
|
||||
|
||||
# Color cycling caption
|
||||
c2 = mcrfpy.Caption("Color Cycle", 400, 300)
|
||||
c2.outline = 2
|
||||
ui.append(c2)
|
||||
|
||||
# Animate through colors
|
||||
def cycle_colors():
|
||||
anim = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 0.5, "linear")
|
||||
anim.start(c2)
|
||||
|
||||
def to_green(rt):
|
||||
mcrfpy.Animation("fill_color", (0, 255, 0, 255), 0.5, "linear").start(c2)
|
||||
def to_blue(rt):
|
||||
mcrfpy.Animation("fill_color", (0, 0, 255, 255), 0.5, "linear").start(c2)
|
||||
def to_white(rt):
|
||||
mcrfpy.Animation("fill_color", (255, 255, 255, 255), 0.5, "linear").start(c2)
|
||||
|
||||
mcrfpy.setTimer("c_green", to_green, 600)
|
||||
mcrfpy.setTimer("c_blue", to_blue, 1200)
|
||||
mcrfpy.setTimer("c_white", to_white, 1800)
|
||||
|
||||
cycle_colors()
|
||||
|
||||
# Typewriter effect
|
||||
c3 = mcrfpy.Caption("", 100, 400)
|
||||
c3.fill_color = mcrfpy.Color(0, 255, 255)
|
||||
ui.append(c3)
|
||||
mcrfpy.Animation("text", "This text appears one character at a time...", 3.0, "linear").start(c3)
|
||||
|
||||
def demo4_easing_showcase():
|
||||
"""Demo 4: Showcase easing functions"""
|
||||
clear_demo()
|
||||
print("demo4")
|
||||
subtitle.text = "Demo 4: 30 Easing Functions"
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Show first 15 easings
|
||||
for i in range(15):
|
||||
row = i // 5
|
||||
col = i % 5
|
||||
x = 80 + col * 180
|
||||
y = 150 + row * 120
|
||||
|
||||
# Create frame
|
||||
f = mcrfpy.Frame(x, y, 20, 20)
|
||||
f.fill_color = mcrfpy.Color(100, 150, 255)
|
||||
f.outline = 1
|
||||
ui.append(f)
|
||||
|
||||
# Label
|
||||
label = mcrfpy.Caption(EASING_FUNCTIONS[i][:10], x, y - 20)
|
||||
label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(label)
|
||||
|
||||
# Animate with this easing
|
||||
mcrfpy.Animation("x", float(x + 140), 3.0, EASING_FUNCTIONS[i]).start(f)
|
||||
|
||||
def demo5_performance():
|
||||
"""Demo 5: Many simultaneous animations"""
|
||||
clear_demo()
|
||||
print("demo5")
|
||||
subtitle.text = "Demo 5: 50+ Simultaneous Animations"
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Create many animated objects
|
||||
for i in range(50):
|
||||
print(f"{i}...",end='',flush=True)
|
||||
x = 100 + (i % 10) * 90
|
||||
y = 120 + (i // 10) * 80
|
||||
|
||||
f = mcrfpy.Frame(x, y, 25, 25)
|
||||
r = (i * 37) % 256
|
||||
g = (i * 73) % 256
|
||||
b = (i * 113) % 256
|
||||
f.fill_color = (r, g, b, 200) #mcrfpy.Color(r, g, b, 200)
|
||||
f.outline = 1
|
||||
ui.append(f)
|
||||
|
||||
# Random animations
|
||||
target_x = 150 + (i % 8) * 100
|
||||
target_y = 150 + (i // 8) * 85
|
||||
duration = 2.0 + (i % 30) * 0.1
|
||||
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
|
||||
|
||||
mcrfpy.Animation("x", float(target_x), duration, easing).start(f)
|
||||
mcrfpy.Animation("y", float(target_y), duration, easing).start(f)
|
||||
mcrfpy.Animation("opacity", 0.3 + (i % 7) * 0.1, 2.5, "easeInOutSine").start(f)
|
||||
|
||||
def demo6_delta_mode():
|
||||
"""Demo 6: Delta mode animations"""
|
||||
clear_demo()
|
||||
print("demo6")
|
||||
subtitle.text = "Demo 6: Delta Mode (Relative Movement)"
|
||||
|
||||
ui = mcrfpy.sceneUI("sizzle")
|
||||
|
||||
# Create frames that move relative to position
|
||||
positions = [(100, 300), (300, 300), (500, 300), (700, 300)]
|
||||
colors = [(255, 100, 100), (100, 255, 100), (100, 100, 255), (255, 255, 100)]
|
||||
|
||||
for i, ((x, y), color) in enumerate(zip(positions, colors)):
|
||||
f = mcrfpy.Frame(x, y, 60, 60)
|
||||
f.fill_color = mcrfpy.Color(color[0], color[1], color[2])
|
||||
f.outline = 2
|
||||
ui.append(f)
|
||||
|
||||
# Delta animations - move by amount, not to position
|
||||
dx = (i + 1) * 30
|
||||
dy = math.sin(i * 0.5) * 50
|
||||
|
||||
mcrfpy.Animation("x", float(dx), 2.0, "easeInOutBack", delta=True).start(f)
|
||||
mcrfpy.Animation("y", float(dy), 2.0, "easeInOutElastic", delta=True).start(f)
|
||||
|
||||
# Caption explaining delta mode
|
||||
info = mcrfpy.Caption("Delta mode: animations move BY amount, not TO position", 200, 450)
|
||||
info.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(info)
|
||||
|
||||
def run_next_demo(runtime):
|
||||
"""Run the next demo in sequence"""
|
||||
global current_demo
|
||||
|
||||
demos = [
|
||||
demo1_frame_basics,
|
||||
demo2_opacity_zindex,
|
||||
demo3_captions,
|
||||
demo4_easing_showcase,
|
||||
demo5_performance,
|
||||
demo6_delta_mode
|
||||
]
|
||||
|
||||
if current_demo < len(demos):
|
||||
# Clean up timers from previous demo
|
||||
for timer in ["opacity_0", "opacity_1", "opacity_2", "opacity_3",
|
||||
"c_green", "c_blue", "c_white"]:
|
||||
try:
|
||||
mcrfpy.delTimer(timer)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Run next demo
|
||||
print(f"Run next: {current_demo}")
|
||||
demos[current_demo]()
|
||||
current_demo += 1
|
||||
|
||||
# Schedule next demo
|
||||
if current_demo < len(demos):
|
||||
#mcrfpy.setTimer("next_demo", run_next_demo, int(DEMO_DURATION * 1000))
|
||||
pass
|
||||
else:
|
||||
current_demo = 0
|
||||
# All done
|
||||
#subtitle.text = "Animation Showcase Complete!"
|
||||
#complete = mcrfpy.Caption("All animations demonstrated successfully!", 350, 350)
|
||||
#complete.fill_color = mcrfpy.Color(0, 255, 0)
|
||||
#complete.outline = 2
|
||||
#ui = mcrfpy.sceneUI("sizzle")
|
||||
#ui.append(complete)
|
||||
#
|
||||
## Exit after delay
|
||||
#def exit_program(rt):
|
||||
# print("\nSizzle reel completed successfully!")
|
||||
# sys.exit(0)
|
||||
#mcrfpy.setTimer("exit", exit_program, 3000)
|
||||
|
||||
# Handle ESC key
|
||||
def handle_keypress(scene_name, keycode):
|
||||
if keycode == 256: # ESC
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
|
||||
# Initialize
|
||||
print("Starting McRogueFace Animation Sizzle Reel...")
|
||||
print("This demonstrates all animation capabilities.")
|
||||
print("Press ESC to exit at any time.")
|
||||
|
||||
create_scene()
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
# Start the show
|
||||
mcrfpy.setTimer("start", run_next_demo, int(DEMO_DURATION * 1000))
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace API Demo - Final Version
|
||||
====================================
|
||||
|
||||
Complete API demonstration with proper error handling.
|
||||
Tests all constructors and methods systematically.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def print_section(title):
|
||||
"""Print a section header"""
|
||||
print("\n" + "="*60)
|
||||
print(f" {title}")
|
||||
print("="*60)
|
||||
|
||||
def print_test(name, success=True):
|
||||
"""Print test result"""
|
||||
status = "✓" if success else "✗"
|
||||
print(f" {status} {name}")
|
||||
|
||||
def test_colors():
|
||||
"""Test Color API"""
|
||||
print_section("COLOR TESTS")
|
||||
|
||||
try:
|
||||
# Basic constructors
|
||||
c1 = mcrfpy.Color(255, 0, 0) # RGB
|
||||
print_test(f"Color(255,0,0) = ({c1.r},{c1.g},{c1.b},{c1.a})")
|
||||
|
||||
c2 = mcrfpy.Color(100, 150, 200, 128) # RGBA
|
||||
print_test(f"Color(100,150,200,128) = ({c2.r},{c2.g},{c2.b},{c2.a})")
|
||||
|
||||
# Property modification
|
||||
c1.r = 128
|
||||
c1.g = 128
|
||||
c1.b = 128
|
||||
c1.a = 200
|
||||
print_test(f"Modified color = ({c1.r},{c1.g},{c1.b},{c1.a})")
|
||||
|
||||
except Exception as e:
|
||||
print_test(f"Color test failed: {e}", False)
|
||||
|
||||
def test_frames():
|
||||
"""Test Frame API"""
|
||||
print_section("FRAME TESTS")
|
||||
|
||||
# Create scene
|
||||
mcrfpy.createScene("test")
|
||||
mcrfpy.setScene("test")
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
|
||||
try:
|
||||
# Constructors
|
||||
f1 = mcrfpy.Frame()
|
||||
print_test(f"Frame() at ({f1.x},{f1.y}) size ({f1.w},{f1.h})")
|
||||
|
||||
f2 = mcrfpy.Frame(100, 50)
|
||||
print_test(f"Frame(100,50) at ({f2.x},{f2.y})")
|
||||
|
||||
f3 = mcrfpy.Frame(200, 100, 150, 75)
|
||||
print_test(f"Frame(200,100,150,75) size ({f3.w},{f3.h})")
|
||||
|
||||
# Properties
|
||||
f3.fill_color = mcrfpy.Color(100, 100, 200)
|
||||
f3.outline = 3
|
||||
f3.outline_color = mcrfpy.Color(255, 255, 0)
|
||||
f3.opacity = 0.8
|
||||
f3.visible = True
|
||||
f3.z_index = 5
|
||||
print_test(f"Frame properties set")
|
||||
|
||||
# Add to scene
|
||||
ui.append(f3)
|
||||
print_test(f"Frame added to scene")
|
||||
|
||||
# Children
|
||||
child = mcrfpy.Frame(10, 10, 50, 50)
|
||||
f3.children.append(child)
|
||||
print_test(f"Child added, count = {len(f3.children)}")
|
||||
|
||||
except Exception as e:
|
||||
print_test(f"Frame test failed: {e}", False)
|
||||
|
||||
def test_captions():
|
||||
"""Test Caption API"""
|
||||
print_section("CAPTION TESTS")
|
||||
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
|
||||
try:
|
||||
# Constructors
|
||||
c1 = mcrfpy.Caption()
|
||||
print_test(f"Caption() text='{c1.text}'")
|
||||
|
||||
c2 = mcrfpy.Caption("Hello World")
|
||||
print_test(f"Caption('Hello World') at ({c2.x},{c2.y})")
|
||||
|
||||
c3 = mcrfpy.Caption("Test", 300, 200)
|
||||
print_test(f"Caption with position at ({c3.x},{c3.y})")
|
||||
|
||||
# Properties
|
||||
c3.text = "Modified"
|
||||
c3.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
c3.outline = 2
|
||||
c3.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
print_test(f"Caption text='{c3.text}'")
|
||||
|
||||
ui.append(c3)
|
||||
print_test("Caption added to scene")
|
||||
|
||||
except Exception as e:
|
||||
print_test(f"Caption test failed: {e}", False)
|
||||
|
||||
def test_animations():
|
||||
"""Test Animation API"""
|
||||
print_section("ANIMATION TESTS")
|
||||
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
|
||||
try:
|
||||
# Create target
|
||||
frame = mcrfpy.Frame(50, 50, 100, 100)
|
||||
frame.fill_color = mcrfpy.Color(100, 100, 100)
|
||||
ui.append(frame)
|
||||
|
||||
# Basic animations
|
||||
a1 = mcrfpy.Animation("x", 300.0, 2.0)
|
||||
print_test("Animation created (position)")
|
||||
|
||||
a2 = mcrfpy.Animation("opacity", 0.5, 1.5, "easeInOut")
|
||||
print_test("Animation with easing")
|
||||
|
||||
a3 = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 2.0)
|
||||
print_test("Color animation (tuple)")
|
||||
|
||||
# Start animations
|
||||
a1.start(frame)
|
||||
a2.start(frame)
|
||||
a3.start(frame)
|
||||
print_test("Animations started")
|
||||
|
||||
# Check properties
|
||||
print_test(f"Duration = {a1.duration}")
|
||||
print_test(f"Elapsed = {a1.elapsed}")
|
||||
print_test(f"Complete = {a1.is_complete}")
|
||||
|
||||
except Exception as e:
|
||||
print_test(f"Animation test failed: {e}", False)
|
||||
|
||||
def test_collections():
|
||||
"""Test collection operations"""
|
||||
print_section("COLLECTION TESTS")
|
||||
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
|
||||
try:
|
||||
# Clear scene
|
||||
while len(ui) > 0:
|
||||
ui.remove(ui[len(ui)-1])
|
||||
print_test(f"Scene cleared, length = {len(ui)}")
|
||||
|
||||
# Add items
|
||||
for i in range(5):
|
||||
f = mcrfpy.Frame(i*100, 50, 80, 80)
|
||||
ui.append(f)
|
||||
print_test(f"Added 5 frames, length = {len(ui)}")
|
||||
|
||||
# Access
|
||||
first = ui[0]
|
||||
print_test(f"Accessed ui[0] at ({first.x},{first.y})")
|
||||
|
||||
# Iteration
|
||||
count = sum(1 for _ in ui)
|
||||
print_test(f"Iteration count = {count}")
|
||||
|
||||
except Exception as e:
|
||||
print_test(f"Collection test failed: {e}", False)
|
||||
|
||||
def run_tests():
|
||||
"""Run all tests"""
|
||||
print("\n" + "="*60)
|
||||
print(" McRogueFace API Test Suite")
|
||||
print("="*60)
|
||||
|
||||
test_colors()
|
||||
test_frames()
|
||||
test_captions()
|
||||
test_animations()
|
||||
test_collections()
|
||||
|
||||
print("\n" + "="*60)
|
||||
print(" Tests Complete")
|
||||
print("="*60)
|
||||
|
||||
# Exit after delay
|
||||
def exit_program(runtime):
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
|
||||
mcrfpy.setTimer("exit", exit_program, 3000)
|
||||
|
||||
# Run tests
|
||||
print("Starting API tests...")
|
||||
run_tests()
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Debug the astar_vs_dijkstra demo issue"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Same setup as the demo
|
||||
start_pos = (5, 10)
|
||||
end_pos = (25, 10)
|
||||
|
||||
print("Debugging A* vs Dijkstra demo...")
|
||||
print(f"Start: {start_pos}, End: {end_pos}")
|
||||
|
||||
# Create scene and grid
|
||||
mcrfpy.createScene("debug")
|
||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||
|
||||
# Initialize all as floor
|
||||
print("\nInitializing 30x20 grid...")
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
grid.at(x, y).walkable = True
|
||||
|
||||
# Test path before obstacles
|
||||
print("\nTest 1: Path with no obstacles")
|
||||
path1 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
|
||||
print(f" Path: {path1[:5]}...{path1[-3:] if len(path1) > 5 else ''}")
|
||||
print(f" Length: {len(path1)}")
|
||||
|
||||
# Add obstacles from the demo
|
||||
obstacles = [
|
||||
# Vertical wall with gaps
|
||||
[(15, y) for y in range(3, 17) if y not in [8, 12]],
|
||||
# Horizontal walls
|
||||
[(x, 5) for x in range(10, 20)],
|
||||
[(x, 15) for x in range(10, 20)],
|
||||
# Maze-like structure
|
||||
[(x, 10) for x in range(20, 25)],
|
||||
[(25, y) for y in range(5, 15)],
|
||||
]
|
||||
|
||||
print("\nAdding obstacles...")
|
||||
wall_count = 0
|
||||
for obstacle_group in obstacles:
|
||||
for x, y in obstacle_group:
|
||||
grid.at(x, y).walkable = False
|
||||
wall_count += 1
|
||||
if wall_count <= 5:
|
||||
print(f" Wall at ({x}, {y})")
|
||||
|
||||
print(f" Total walls added: {wall_count}")
|
||||
|
||||
# Check specific cells
|
||||
print(f"\nChecking key positions:")
|
||||
print(f" Start ({start_pos[0]}, {start_pos[1]}): walkable={grid.at(start_pos[0], start_pos[1]).walkable}")
|
||||
print(f" End ({end_pos[0]}, {end_pos[1]}): walkable={grid.at(end_pos[0], end_pos[1]).walkable}")
|
||||
|
||||
# Check if path is blocked
|
||||
print(f"\nChecking horizontal line at y=10:")
|
||||
blocked_x = []
|
||||
for x in range(30):
|
||||
if not grid.at(x, 10).walkable:
|
||||
blocked_x.append(x)
|
||||
|
||||
print(f" Blocked x positions: {blocked_x}")
|
||||
|
||||
# Test path with obstacles
|
||||
print("\nTest 2: Path with obstacles")
|
||||
path2 = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
|
||||
print(f" Path: {path2}")
|
||||
print(f" Length: {len(path2)}")
|
||||
|
||||
# Check if there's any path at all
|
||||
if not path2:
|
||||
print("\n No path found! Checking why...")
|
||||
|
||||
# Check if we can reach the vertical wall gap
|
||||
print("\n Testing path to wall gap at (15, 8):")
|
||||
path_to_gap = grid.compute_astar_path(start_pos[0], start_pos[1], 15, 8)
|
||||
print(f" Path to gap: {path_to_gap}")
|
||||
|
||||
# Check from gap to end
|
||||
print("\n Testing path from gap (15, 8) to end:")
|
||||
path_from_gap = grid.compute_astar_path(15, 8, end_pos[0], end_pos[1])
|
||||
print(f" Path from gap: {path_from_gap}")
|
||||
|
||||
# Check walls more carefully
|
||||
print("\nDetailed wall analysis:")
|
||||
print(" Walls at x=25 (blocking end?):")
|
||||
for y in range(5, 15):
|
||||
print(f" ({25}, {y}): walkable={grid.at(25, y).walkable}")
|
||||
|
||||
def timer_cb(dt):
|
||||
sys.exit(0)
|
||||
|
||||
ui = mcrfpy.sceneUI("debug")
|
||||
ui.append(grid)
|
||||
mcrfpy.setScene("debug")
|
||||
mcrfpy.setTimer("exit", timer_cb, 100)
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Working Dijkstra Demo with Clear Visual Feedback
|
||||
================================================
|
||||
|
||||
This demo shows pathfinding with high-contrast colors.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# High contrast colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown for walls
|
||||
FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray for floors
|
||||
PATH_COLOR = mcrfpy.Color(0, 255, 0) # Pure green for paths
|
||||
START_COLOR = mcrfpy.Color(255, 0, 0) # Red for start
|
||||
END_COLOR = mcrfpy.Color(0, 0, 255) # Blue for end
|
||||
|
||||
print("Dijkstra Demo - High Contrast")
|
||||
print("==============================")
|
||||
|
||||
# Create scene
|
||||
mcrfpy.createScene("dijkstra_demo")
|
||||
|
||||
# Create grid with exact layout from user
|
||||
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Map layout
|
||||
map_layout = [
|
||||
"..............", # Row 0
|
||||
"..W.....WWWW..", # Row 1
|
||||
"..W.W...W.EW..", # Row 2
|
||||
"..W.....W..W..", # Row 3
|
||||
"..W...E.WWWW..", # Row 4
|
||||
"E.W...........", # Row 5
|
||||
"..W...........", # Row 6
|
||||
"..W...........", # Row 7
|
||||
"..W.WWW.......", # Row 8
|
||||
"..............", # Row 9
|
||||
]
|
||||
|
||||
# Create the map
|
||||
entity_positions = []
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
|
||||
if char == 'W':
|
||||
cell.walkable = False
|
||||
cell.color = WALL_COLOR
|
||||
else:
|
||||
cell.walkable = True
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
if char == 'E':
|
||||
entity_positions.append((x, y))
|
||||
|
||||
print(f"Map created: {grid.grid_x}x{grid.grid_y}")
|
||||
print(f"Entity positions: {entity_positions}")
|
||||
|
||||
# Create entities
|
||||
entities = []
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
entity = mcrfpy.Entity(x, y)
|
||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||
grid.entities.append(entity)
|
||||
entities.append(entity)
|
||||
print(f"Entity {i+1} at ({x}, {y})")
|
||||
|
||||
# Highlight a path immediately
|
||||
if len(entities) >= 2:
|
||||
e1, e2 = entities[0], entities[1]
|
||||
print(f"\nCalculating path from Entity 1 ({e1.x}, {e1.y}) to Entity 2 ({e2.x}, {e2.y})...")
|
||||
|
||||
path = e1.path_to(int(e2.x), int(e2.y))
|
||||
print(f"Path found: {path}")
|
||||
print(f"Path length: {len(path)} steps")
|
||||
|
||||
if path:
|
||||
print("\nHighlighting path in bright green...")
|
||||
# Color start and end specially
|
||||
grid.at(int(e1.x), int(e1.y)).color = START_COLOR
|
||||
grid.at(int(e2.x), int(e2.y)).color = END_COLOR
|
||||
|
||||
# Color the path
|
||||
for i, (x, y) in enumerate(path):
|
||||
if i > 0 and i < len(path) - 1: # Skip start and end
|
||||
grid.at(x, y).color = PATH_COLOR
|
||||
print(f" Colored ({x}, {y}) green")
|
||||
|
||||
# Keypress handler
|
||||
def handle_keypress(scene_name, keycode):
|
||||
if keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
elif keycode == 32: # Space
|
||||
print("\nRefreshing path colors...")
|
||||
# Re-color the path to ensure it's visible
|
||||
if len(entities) >= 2 and path:
|
||||
for x, y in path[1:-1]:
|
||||
grid.at(x, y).color = PATH_COLOR
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("dijkstra_demo")
|
||||
ui.append(grid)
|
||||
|
||||
# Scale grid
|
||||
grid.size = (560, 400) # 14*40, 10*40
|
||||
grid.position = (120, 100)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Dijkstra Pathfinding - High Contrast", 200, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add legend
|
||||
legend1 = mcrfpy.Caption("Red=Start, Blue=End, Green=Path", 120, 520)
|
||||
legend1.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(legend1)
|
||||
|
||||
legend2 = mcrfpy.Caption("Press Q to quit, SPACE to refresh", 120, 540)
|
||||
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend2)
|
||||
|
||||
# Entity info
|
||||
info = mcrfpy.Caption(f"Path: Entity 1 to 2 = {len(path) if 'path' in locals() else 0} steps", 120, 60)
|
||||
info.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
ui.append(info)
|
||||
|
||||
# Set up input
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
mcrfpy.setScene("dijkstra_demo")
|
||||
|
||||
print("\nDemo ready! The path should be clearly visible in bright green.")
|
||||
print("Red = Start, Blue = End, Green = Path")
|
||||
print("Press SPACE to refresh colors if needed.")
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Exhaustive API Demo (Fixed)
|
||||
=======================================
|
||||
|
||||
Fixed version that properly exits after tests complete.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
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 = mcrfpy.Color()
|
||||
print_test(f"Color() = ({c1.r}, {c1.g}, {c1.b}, {c1.a})")
|
||||
|
||||
# Single value (grayscale)
|
||||
c2 = mcrfpy.Color(128)
|
||||
print_test(f"Color(128) = ({c2.r}, {c2.g}, {c2.b}, {c2.a})")
|
||||
|
||||
# RGB only (alpha defaults to 255)
|
||||
c3 = mcrfpy.Color(255, 128, 0)
|
||||
print_test(f"Color(255, 128, 0) = ({c3.r}, {c3.g}, {c3.b}, {c3.a})")
|
||||
|
||||
# Full RGBA
|
||||
c4 = mcrfpy.Color(100, 150, 200, 128)
|
||||
print_test(f"Color(100, 150, 200, 128) = ({c4.r}, {c4.g}, {c4.b}, {c4.a})")
|
||||
|
||||
# Property access
|
||||
print("\n Properties:")
|
||||
c = mcrfpy.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_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 = mcrfpy.Frame()
|
||||
print_test(f"Frame() - pos=({f1.x}, {f1.y}), size=({f1.w}, {f1.h})")
|
||||
ui.append(f1)
|
||||
|
||||
# Position only
|
||||
f2 = mcrfpy.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 = mcrfpy.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 = mcrfpy.Frame(300, 200, 200, 100,
|
||||
fill_color=mcrfpy.Color(100, 100, 200),
|
||||
outline_color=mcrfpy.Color(255, 255, 0),
|
||||
outline=3)
|
||||
print_test("Frame with all parameters")
|
||||
ui.append(f4)
|
||||
|
||||
# Properties
|
||||
print("\n Properties:")
|
||||
|
||||
# Position and size
|
||||
f = mcrfpy.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 = mcrfpy.Color(255, 0, 0, 128)
|
||||
f.outline_color = mcrfpy.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 = mcrfpy.Frame(5, 5, 20, 20)
|
||||
child2 = mcrfpy.Frame(30, 5, 20, 20)
|
||||
f.children.append(child1)
|
||||
f.children.append(child2)
|
||||
print_test(f"children.count = {len(f.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 = mcrfpy.Caption()
|
||||
print_test(f"Caption() - text='{c1.text}', pos=({c1.x}, {c1.y})")
|
||||
ui.append(c1)
|
||||
|
||||
# Text only
|
||||
c2 = mcrfpy.Caption("Hello World")
|
||||
print_test(f"Caption('Hello World') - pos=({c2.x}, {c2.y})")
|
||||
ui.append(c2)
|
||||
|
||||
# Text and position
|
||||
c3 = mcrfpy.Caption("Positioned Text", 100, 50)
|
||||
print_test(f"Caption('Positioned Text', 100, 50)")
|
||||
ui.append(c3)
|
||||
|
||||
# Full constructor
|
||||
c5 = mcrfpy.Caption("Styled Text", 300, 150,
|
||||
fill_color=mcrfpy.Color(255, 255, 0),
|
||||
outline_color=mcrfpy.Color(255, 0, 0),
|
||||
outline=2)
|
||||
print_test("Caption with all style parameters")
|
||||
ui.append(c5)
|
||||
|
||||
# Properties
|
||||
print("\n Properties:")
|
||||
|
||||
c = mcrfpy.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 = mcrfpy.Color(0, 255, 255)
|
||||
c.outline_color = mcrfpy.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})")
|
||||
|
||||
return True
|
||||
|
||||
def test_animation_api():
|
||||
"""Test Animation class API"""
|
||||
print_section("ANIMATION API TESTS")
|
||||
|
||||
ui = mcrfpy.sceneUI("api_test")
|
||||
|
||||
print("\n Animation Constructors:")
|
||||
|
||||
# Basic animation
|
||||
anim1 = mcrfpy.Animation("x", 100.0, 2.0)
|
||||
print_test("Animation('x', 100.0, 2.0)")
|
||||
|
||||
# With easing
|
||||
anim2 = mcrfpy.Animation("y", 200.0, 3.0, "easeInOut")
|
||||
print_test("Animation with easing='easeInOut'")
|
||||
|
||||
# Delta mode
|
||||
anim3 = mcrfpy.Animation("w", 50.0, 1.5, "linear", delta=True)
|
||||
print_test("Animation with delta=True")
|
||||
|
||||
# Color animation (as tuple)
|
||||
anim4 = mcrfpy.Animation("fill_color", (255, 0, 0, 255), 2.0)
|
||||
print_test("Animation with Color tuple target")
|
||||
|
||||
# Vector animation
|
||||
anim5 = mcrfpy.Animation("position", (10.0, 20.0), 2.5, "easeOutBounce")
|
||||
print_test("Animation with position tuple")
|
||||
|
||||
# Sprite sequence
|
||||
anim6 = mcrfpy.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 = mcrfpy.Frame(50, 50, 100, 100)
|
||||
frame.fill_color = mcrfpy.Color(100, 100, 100)
|
||||
ui.append(frame)
|
||||
|
||||
# Start animation
|
||||
anim1.start(frame)
|
||||
print_test("start() called on frame")
|
||||
|
||||
# Test some easing functions
|
||||
print("\n Sample Easing Functions:")
|
||||
easings = ["linear", "easeIn", "easeOut", "easeInOut", "easeInBounce", "easeOutElastic"]
|
||||
|
||||
for easing in easings:
|
||||
try:
|
||||
test_anim = mcrfpy.Animation("x", 100.0, 1.0, easing)
|
||||
print_test(f"Easing '{easing}' ✓")
|
||||
except:
|
||||
print_test(f"Easing '{easing}' failed", False)
|
||||
|
||||
return True
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all API tests"""
|
||||
print("\n" + "="*60)
|
||||
print(" McRogueFace Exhaustive API Test Suite (Fixed)")
|
||||
print(" Testing constructors and methods...")
|
||||
print("="*60)
|
||||
|
||||
# Run each test category
|
||||
test_functions = [
|
||||
test_color_api,
|
||||
test_frame_api,
|
||||
test_caption_api,
|
||||
test_animation_api
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
print("\n Visual elements are displayed in the 'api_test' scene.")
|
||||
print(" The test is complete.")
|
||||
|
||||
# Exit after a short delay to allow output to be seen
|
||||
def exit_test(runtime):
|
||||
print("\nExiting API test suite...")
|
||||
sys.exit(0)
|
||||
|
||||
mcrfpy.setTimer("exit", exit_test, 2000)
|
||||
|
||||
# Run the tests immediately
|
||||
print("Starting McRogueFace Exhaustive API Demo (Fixed)...")
|
||||
print("This will test constructors and methods.")
|
||||
|
||||
run_all_tests()
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Path & Vision Sizzle Reel
|
||||
=========================
|
||||
|
||||
A choreographed demo showing:
|
||||
- Smooth entity movement along paths
|
||||
- Camera following with grid center animation
|
||||
- Field of view updates as entities move
|
||||
- Dramatic perspective transitions with zoom effects
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 30, 30)
|
||||
FLOOR_COLOR = mcrfpy.Color(80, 80, 100)
|
||||
PATH_COLOR = mcrfpy.Color(120, 120, 180)
|
||||
DARK_FLOOR = mcrfpy.Color(40, 40, 50)
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
player = None
|
||||
enemy = None
|
||||
sequence_step = 0
|
||||
player_path = []
|
||||
enemy_path = []
|
||||
player_path_index = 0
|
||||
enemy_path_index = 0
|
||||
|
||||
def create_scene():
|
||||
"""Create the demo environment"""
|
||||
global grid, player, enemy
|
||||
|
||||
mcrfpy.createScene("path_vision_demo")
|
||||
|
||||
# Create larger grid for more dramatic movement
|
||||
grid = mcrfpy.Grid(grid_x=40, grid_y=25)
|
||||
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||
|
||||
# Map layout - interconnected rooms with corridors
|
||||
map_layout = [
|
||||
"########################################", # 0
|
||||
"#......##########......################", # 1
|
||||
"#......##########......################", # 2
|
||||
"#......##########......################", # 3
|
||||
"#......#.........#.....################", # 4
|
||||
"#......#.........#.....################", # 5
|
||||
"####.###.........####.#################", # 6
|
||||
"####.....................##############", # 7
|
||||
"####.....................##############", # 8
|
||||
"####.###.........####.#################", # 9
|
||||
"#......#.........#.....################", # 10
|
||||
"#......#.........#.....################", # 11
|
||||
"#......#.........#.....################", # 12
|
||||
"#......###.....###.....################", # 13
|
||||
"#......###.....###.....################", # 14
|
||||
"#......###.....###.....#########......#", # 15
|
||||
"#......###.....###.....#########......#", # 16
|
||||
"#......###.....###.....#########......#", # 17
|
||||
"#####.############.#############......#", # 18
|
||||
"#####...........................#.....#", # 19
|
||||
"#####...........................#.....#", # 20
|
||||
"#####.############.#############......#", # 21
|
||||
"#......###########.##########.........#", # 22
|
||||
"#......###########.##########.........#", # 23
|
||||
"########################################", # 24
|
||||
]
|
||||
|
||||
# Build the map
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
if char == '#':
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.color = WALL_COLOR
|
||||
else:
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
# Create player in top-left room
|
||||
player = mcrfpy.Entity(3, 3, grid=grid)
|
||||
player.sprite_index = 64 # @
|
||||
|
||||
# Create enemy in bottom-right area
|
||||
enemy = mcrfpy.Entity(35, 20, grid=grid)
|
||||
enemy.sprite_index = 69 # E
|
||||
|
||||
# Initial visibility
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
|
||||
# Set initial perspective to player
|
||||
grid.perspective = 0
|
||||
|
||||
def setup_paths():
|
||||
"""Define the paths for entities"""
|
||||
global player_path, enemy_path
|
||||
|
||||
# Player path: Top-left room → corridor → middle room
|
||||
player_waypoints = [
|
||||
(3, 3), # Start
|
||||
(3, 8), # Move down
|
||||
(7, 8), # Enter corridor
|
||||
(16, 8), # Through corridor
|
||||
(16, 12), # Enter middle room
|
||||
(12, 12), # Move in room
|
||||
(12, 16), # Move down
|
||||
(16, 16), # Move right
|
||||
(16, 19), # Exit room
|
||||
(25, 19), # Move right
|
||||
(30, 19), # Continue
|
||||
(35, 19), # Near enemy start
|
||||
]
|
||||
|
||||
# Enemy path: Bottom-right → around → approach player area
|
||||
enemy_waypoints = [
|
||||
(35, 20), # Start
|
||||
(30, 20), # Move left
|
||||
(25, 20), # Continue
|
||||
(20, 20), # Continue
|
||||
(16, 20), # Corridor junction
|
||||
(16, 16), # Move up (might see player)
|
||||
(16, 12), # Continue up
|
||||
(16, 8), # Top corridor
|
||||
(10, 8), # Move left
|
||||
(7, 8), # Continue
|
||||
(3, 8), # Player's area
|
||||
(3, 12), # Move down
|
||||
]
|
||||
|
||||
# Calculate full paths using pathfinding
|
||||
player_path = []
|
||||
for i in range(len(player_waypoints) - 1):
|
||||
x1, y1 = player_waypoints[i]
|
||||
x2, y2 = player_waypoints[i + 1]
|
||||
|
||||
# Use grid's A* pathfinding
|
||||
segment = grid.compute_astar_path(x1, y1, x2, y2)
|
||||
if segment:
|
||||
# Add segment (avoiding duplicates)
|
||||
if not player_path or segment[0] != player_path[-1]:
|
||||
player_path.extend(segment)
|
||||
else:
|
||||
player_path.extend(segment[1:])
|
||||
|
||||
enemy_path = []
|
||||
for i in range(len(enemy_waypoints) - 1):
|
||||
x1, y1 = enemy_waypoints[i]
|
||||
x2, y2 = enemy_waypoints[i + 1]
|
||||
|
||||
segment = grid.compute_astar_path(x1, y1, x2, y2)
|
||||
if segment:
|
||||
if not enemy_path or segment[0] != enemy_path[-1]:
|
||||
enemy_path.extend(segment)
|
||||
else:
|
||||
enemy_path.extend(segment[1:])
|
||||
|
||||
print(f"Player path: {len(player_path)} steps")
|
||||
print(f"Enemy path: {len(enemy_path)} steps")
|
||||
|
||||
def setup_ui():
|
||||
"""Create UI elements"""
|
||||
ui = mcrfpy.sceneUI("path_vision_demo")
|
||||
ui.append(grid)
|
||||
|
||||
# Position and size grid
|
||||
grid.position = (50, 80)
|
||||
grid.size = (700, 500) # Adjust based on zoom
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Path & Vision Sizzle Reel", 300, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Status
|
||||
global status_text, perspective_text
|
||||
status_text = mcrfpy.Caption("Starting demo...", 50, 50)
|
||||
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(status_text)
|
||||
|
||||
perspective_text = mcrfpy.Caption("Perspective: Player", 550, 50)
|
||||
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
ui.append(perspective_text)
|
||||
|
||||
# Controls
|
||||
controls = mcrfpy.Caption("Space: Pause/Resume | R: Restart | Q: Quit", 250, 600)
|
||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(controls)
|
||||
|
||||
# Animation control
|
||||
paused = False
|
||||
move_timer = 0
|
||||
zoom_transition = False
|
||||
|
||||
def move_entity_smooth(entity, target_x, target_y, duration=0.3):
|
||||
"""Smoothly animate entity to position"""
|
||||
# Create position animation
|
||||
anim_x = mcrfpy.Animation("x", float(target_x), duration, "easeInOut")
|
||||
anim_y = mcrfpy.Animation("y", float(target_y), duration, "easeInOut")
|
||||
|
||||
anim_x.start(entity)
|
||||
anim_y.start(entity)
|
||||
|
||||
def update_camera_smooth(center_x, center_y, duration=0.3):
|
||||
"""Smoothly move camera center"""
|
||||
# Convert grid coords to pixel coords (assuming 16x16 tiles)
|
||||
pixel_x = center_x * 16
|
||||
pixel_y = center_y * 16
|
||||
|
||||
anim = mcrfpy.Animation("center", (pixel_x, pixel_y), duration, "easeOut")
|
||||
anim.start(grid)
|
||||
|
||||
def start_perspective_transition():
|
||||
"""Begin the dramatic perspective shift"""
|
||||
global zoom_transition, sequence_step
|
||||
zoom_transition = True
|
||||
sequence_step = 100 # Special sequence number
|
||||
|
||||
status_text.text = "Perspective shift: Zooming out..."
|
||||
|
||||
# Zoom out with elastic easing
|
||||
zoom_out = mcrfpy.Animation("zoom", 0.5, 2.0, "easeInExpo")
|
||||
zoom_out.start(grid)
|
||||
|
||||
# Schedule the perspective switch
|
||||
mcrfpy.setTimer("switch_perspective", switch_perspective, 2100)
|
||||
|
||||
def switch_perspective(dt):
|
||||
"""Switch perspective at the peak of zoom"""
|
||||
global sequence_step
|
||||
|
||||
# Switch to enemy perspective
|
||||
grid.perspective = 1
|
||||
perspective_text.text = "Perspective: Enemy"
|
||||
perspective_text.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
|
||||
status_text.text = "Perspective shift: Following enemy..."
|
||||
|
||||
# Update camera to enemy position
|
||||
update_camera_smooth(enemy.x, enemy.y, 0.1)
|
||||
|
||||
# Zoom back in
|
||||
zoom_in = mcrfpy.Animation("zoom", 1.2, 2.0, "easeOutExpo")
|
||||
zoom_in.start(grid)
|
||||
|
||||
# Resume sequence
|
||||
mcrfpy.setTimer("resume_enemy", resume_enemy_sequence, 2100)
|
||||
|
||||
# Cancel this timer
|
||||
mcrfpy.delTimer("switch_perspective")
|
||||
|
||||
def resume_enemy_sequence(dt):
|
||||
"""Resume following enemy after perspective shift"""
|
||||
global sequence_step, zoom_transition
|
||||
zoom_transition = False
|
||||
sequence_step = 101 # Continue with enemy movement
|
||||
mcrfpy.delTimer("resume_enemy")
|
||||
|
||||
def sequence_tick(dt):
|
||||
"""Main sequence controller"""
|
||||
global sequence_step, player_path_index, enemy_path_index, move_timer
|
||||
|
||||
if paused or zoom_transition:
|
||||
return
|
||||
|
||||
move_timer += dt
|
||||
if move_timer < 400: # Move every 400ms
|
||||
return
|
||||
move_timer = 0
|
||||
|
||||
if sequence_step < 50:
|
||||
# Phase 1: Follow player movement
|
||||
if player_path_index < len(player_path):
|
||||
x, y = player_path[player_path_index]
|
||||
move_entity_smooth(player, x, y)
|
||||
player.update_visibility()
|
||||
|
||||
# Camera follows player
|
||||
if grid.perspective == 0:
|
||||
update_camera_smooth(player.x, player.y)
|
||||
|
||||
player_path_index += 1
|
||||
status_text.text = f"Player moving... Step {player_path_index}/{len(player_path)}"
|
||||
|
||||
# Start enemy movement after player has moved a bit
|
||||
if player_path_index == 10:
|
||||
sequence_step = 1 # Enable enemy movement
|
||||
else:
|
||||
# Player reached destination, start perspective transition
|
||||
start_perspective_transition()
|
||||
|
||||
if sequence_step >= 1 and sequence_step < 50:
|
||||
# Phase 2: Enemy movement (concurrent with player)
|
||||
if enemy_path_index < len(enemy_path):
|
||||
x, y = enemy_path[enemy_path_index]
|
||||
move_entity_smooth(enemy, x, y)
|
||||
enemy.update_visibility()
|
||||
|
||||
# Check if enemy is visible to player
|
||||
if grid.perspective == 0:
|
||||
enemy_cell_idx = int(enemy.y) * grid.grid_x + int(enemy.x)
|
||||
if enemy_cell_idx < len(player.gridstate) and player.gridstate[enemy_cell_idx].visible:
|
||||
status_text.text = "Enemy spotted!"
|
||||
|
||||
enemy_path_index += 1
|
||||
|
||||
elif sequence_step == 101:
|
||||
# Phase 3: Continue following enemy after perspective shift
|
||||
if enemy_path_index < len(enemy_path):
|
||||
x, y = enemy_path[enemy_path_index]
|
||||
move_entity_smooth(enemy, x, y)
|
||||
enemy.update_visibility()
|
||||
|
||||
# Camera follows enemy
|
||||
update_camera_smooth(enemy.x, enemy.y)
|
||||
|
||||
enemy_path_index += 1
|
||||
status_text.text = f"Following enemy... Step {enemy_path_index}/{len(enemy_path)}"
|
||||
else:
|
||||
status_text.text = "Demo complete! Press R to restart"
|
||||
sequence_step = 200 # Done
|
||||
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input"""
|
||||
global paused, sequence_step, player_path_index, enemy_path_index, move_timer
|
||||
key = key.lower()
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if key == "q":
|
||||
print("Exiting sizzle reel...")
|
||||
sys.exit(0)
|
||||
elif key == "space":
|
||||
paused = not paused
|
||||
status_text.text = "PAUSED" if paused else "Running..."
|
||||
elif key == "r":
|
||||
# Reset everything
|
||||
player.x, player.y = 3, 3
|
||||
enemy.x, enemy.y = 35, 20
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
grid.perspective = 0
|
||||
perspective_text.text = "Perspective: Player"
|
||||
perspective_text.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
sequence_step = 0
|
||||
player_path_index = 0
|
||||
enemy_path_index = 0
|
||||
move_timer = 0
|
||||
update_camera_smooth(player.x, player.y, 0.5)
|
||||
|
||||
# Reset zoom
|
||||
zoom_reset = mcrfpy.Animation("zoom", 1.2, 0.5, "easeOut")
|
||||
zoom_reset.start(grid)
|
||||
|
||||
status_text.text = "Demo restarted!"
|
||||
|
||||
# Initialize everything
|
||||
print("Path & Vision Sizzle Reel")
|
||||
print("=========================")
|
||||
print("Demonstrating:")
|
||||
print("- Smooth entity movement along calculated paths")
|
||||
print("- Camera following with animated grid centering")
|
||||
print("- Field of view updates as entities move")
|
||||
print("- Dramatic perspective transitions with zoom effects")
|
||||
print()
|
||||
|
||||
create_scene()
|
||||
setup_paths()
|
||||
setup_ui()
|
||||
|
||||
# Set scene and input
|
||||
mcrfpy.setScene("path_vision_demo")
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Initial camera setup
|
||||
grid.zoom = 1.2
|
||||
update_camera_smooth(player.x, player.y, 0.1)
|
||||
|
||||
# Start the sequence
|
||||
mcrfpy.setTimer("sequence", sequence_tick, 50) # Tick every 50ms
|
||||
|
||||
print("Demo started!")
|
||||
print("- Player (@) will navigate through rooms")
|
||||
print("- Enemy (E) will move on a different path")
|
||||
print("- Watch for the dramatic perspective shift!")
|
||||
print()
|
||||
print("Controls: Space=Pause, R=Restart, Q=Quit")
|
||||
|
|
@ -0,0 +1,377 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pathfinding Showcase Demo
|
||||
=========================
|
||||
|
||||
Demonstrates various pathfinding scenarios with multiple entities.
|
||||
|
||||
Features:
|
||||
- Multiple entities pathfinding simultaneously
|
||||
- Chase mode: entities pursue targets
|
||||
- Flee mode: entities avoid threats
|
||||
- Patrol mode: entities follow waypoints
|
||||
- Visual debugging: show Dijkstra distance field
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
import random
|
||||
|
||||
# Colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 40, 40)
|
||||
FLOOR_COLOR = mcrfpy.Color(220, 220, 240)
|
||||
PATH_COLOR = mcrfpy.Color(180, 250, 180)
|
||||
THREAT_COLOR = mcrfpy.Color(255, 100, 100)
|
||||
GOAL_COLOR = mcrfpy.Color(100, 255, 100)
|
||||
DIJKSTRA_COLORS = [
|
||||
mcrfpy.Color(50, 50, 100), # Far
|
||||
mcrfpy.Color(70, 70, 150),
|
||||
mcrfpy.Color(90, 90, 200),
|
||||
mcrfpy.Color(110, 110, 250),
|
||||
mcrfpy.Color(150, 150, 255),
|
||||
mcrfpy.Color(200, 200, 255), # Near
|
||||
]
|
||||
|
||||
# Entity types
|
||||
PLAYER = 64 # @
|
||||
ENEMY = 69 # E
|
||||
TREASURE = 36 # $
|
||||
PATROL = 80 # P
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
player = None
|
||||
enemies = []
|
||||
treasures = []
|
||||
patrol_entities = []
|
||||
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
|
||||
|
||||
mcrfpy.createScene("pathfinding_showcase")
|
||||
|
||||
# Create larger grid for showcase
|
||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Initialize all as floor
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
grid.at(x, y).walkable = True
|
||||
grid.at(x, y).transparent = True
|
||||
grid.at(x, y).color = FLOOR_COLOR
|
||||
|
||||
# Create rooms and corridors
|
||||
rooms = [
|
||||
(2, 2, 8, 6), # Top-left room
|
||||
(20, 2, 8, 6), # Top-right room
|
||||
(11, 8, 8, 6), # Center room
|
||||
(2, 14, 8, 5), # Bottom-left room
|
||||
(20, 14, 8, 5), # Bottom-right room
|
||||
]
|
||||
|
||||
# Create room walls
|
||||
for rx, ry, rw, rh in rooms:
|
||||
# Top and bottom walls
|
||||
for x in range(rx, rx + rw):
|
||||
if 0 <= x < 30:
|
||||
grid.at(x, ry).walkable = False
|
||||
grid.at(x, ry).color = WALL_COLOR
|
||||
grid.at(x, ry + rh - 1).walkable = False
|
||||
grid.at(x, ry + rh - 1).color = WALL_COLOR
|
||||
|
||||
# Left and right walls
|
||||
for y in range(ry, ry + rh):
|
||||
if 0 <= y < 20:
|
||||
grid.at(rx, y).walkable = False
|
||||
grid.at(rx, y).color = WALL_COLOR
|
||||
grid.at(rx + rw - 1, y).walkable = False
|
||||
grid.at(rx + rw - 1, y).color = WALL_COLOR
|
||||
|
||||
# Create doorways
|
||||
doorways = [
|
||||
(6, 2), (24, 2), # Top room doors
|
||||
(6, 7), (24, 7), # Top room doors bottom
|
||||
(15, 8), (15, 13), # Center room doors
|
||||
(6, 14), (24, 14), # Bottom room doors
|
||||
(11, 11), (18, 11), # Center room side doors
|
||||
]
|
||||
|
||||
for x, y in doorways:
|
||||
if 0 <= x < 30 and 0 <= y < 20:
|
||||
grid.at(x, y).walkable = True
|
||||
grid.at(x, y).color = FLOOR_COLOR
|
||||
|
||||
# Add some corridors
|
||||
# Horizontal corridors
|
||||
for x in range(10, 20):
|
||||
grid.at(x, 5).walkable = True
|
||||
grid.at(x, 5).color = FLOOR_COLOR
|
||||
grid.at(x, 16).walkable = True
|
||||
grid.at(x, 16).color = FLOOR_COLOR
|
||||
|
||||
# Vertical corridors
|
||||
for y in range(5, 17):
|
||||
grid.at(10, y).walkable = True
|
||||
grid.at(10, y).color = FLOOR_COLOR
|
||||
grid.at(19, y).walkable = True
|
||||
grid.at(19, y).color = FLOOR_COLOR
|
||||
|
||||
def spawn_entities():
|
||||
"""Spawn various entity types"""
|
||||
global player, enemies, treasures, patrol_entities
|
||||
|
||||
# Clear existing entities
|
||||
#grid.entities.clear()
|
||||
enemies = []
|
||||
treasures = []
|
||||
patrol_entities = []
|
||||
|
||||
# Spawn player in center room
|
||||
player = mcrfpy.Entity((15, 11), mcrfpy.default_texture, PLAYER)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Spawn enemies in corners
|
||||
enemy_positions = [(4, 4), (24, 4), (4, 16), (24, 16)]
|
||||
for x, y in enemy_positions:
|
||||
enemy = mcrfpy.Entity((x, y), mcrfpy.default_texture, ENEMY)
|
||||
grid.entities.append(enemy)
|
||||
enemies.append(enemy)
|
||||
|
||||
# Spawn treasures
|
||||
treasure_positions = [(6, 5), (24, 5), (15, 10)]
|
||||
for x, y in treasure_positions:
|
||||
treasure = mcrfpy.Entity((x, y), mcrfpy.default_texture, 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
|
||||
grid.entities.append(patrol)
|
||||
patrol_entities.append(patrol)
|
||||
|
||||
def visualize_dijkstra(target_x, target_y):
|
||||
"""Visualize Dijkstra distance field"""
|
||||
if not show_dijkstra:
|
||||
return
|
||||
|
||||
# Compute Dijkstra from target
|
||||
grid.compute_dijkstra(target_x, target_y)
|
||||
|
||||
# Color tiles based on distance
|
||||
max_dist = 30.0
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
if grid.at(x, y).walkable:
|
||||
dist = grid.get_dijkstra_distance(x, y)
|
||||
if dist is not None and dist < max_dist:
|
||||
# Map distance to color index
|
||||
color_idx = int((dist / max_dist) * len(DIJKSTRA_COLORS))
|
||||
color_idx = min(color_idx, len(DIJKSTRA_COLORS) - 1)
|
||||
grid.at(x, y).color = DIJKSTRA_COLORS[color_idx]
|
||||
|
||||
def move_enemies(dt):
|
||||
"""Move enemies based on current mode"""
|
||||
if mode == "CHASE":
|
||||
# Enemies chase player
|
||||
for enemy in enemies:
|
||||
path = enemy.path_to(int(player.x), int(player.y))
|
||||
if path and len(path) > 1: # Don't move onto player
|
||||
# Move towards player
|
||||
next_x, next_y = path[1]
|
||||
# Smooth movement
|
||||
dx = next_x - enemy.x
|
||||
dy = next_y - enemy.y
|
||||
enemy.x += dx * dt * animation_speed
|
||||
enemy.y += dy * dt * animation_speed
|
||||
|
||||
elif mode == "FLEE":
|
||||
# Enemies flee from player
|
||||
for enemy in enemies:
|
||||
# Compute opposite direction
|
||||
dx = enemy.x - player.x
|
||||
dy = enemy.y - player.y
|
||||
|
||||
# Find safe spot in that direction
|
||||
target_x = int(enemy.x + dx * 2)
|
||||
target_y = int(enemy.y + dy * 2)
|
||||
|
||||
# Clamp to grid
|
||||
target_x = max(0, min(29, target_x))
|
||||
target_y = max(0, min(19, target_y))
|
||||
|
||||
path = enemy.path_to(target_x, target_y)
|
||||
if path and len(path) > 0:
|
||||
next_x, next_y = path[0]
|
||||
# Move away from player
|
||||
dx = next_x - enemy.x
|
||||
dy = next_y - enemy.y
|
||||
enemy.x += dx * dt * animation_speed
|
||||
enemy.y += dy * dt * animation_speed
|
||||
|
||||
def move_patrols(dt):
|
||||
"""Move patrol entities along waypoints"""
|
||||
for patrol in patrol_entities:
|
||||
if patrol not in entity_waypoints:
|
||||
continue
|
||||
|
||||
# Get current waypoint
|
||||
waypoints = entity_waypoints[patrol]
|
||||
waypoint_index = entity_waypoint_indices[patrol]
|
||||
target_x, target_y = waypoints[waypoint_index]
|
||||
|
||||
# Check if reached waypoint
|
||||
dist = abs(patrol.x - target_x) + abs(patrol.y - target_y)
|
||||
if dist < 0.5:
|
||||
# Move to next waypoint
|
||||
entity_waypoint_indices[patrol] = (waypoint_index + 1) % len(waypoints)
|
||||
waypoint_index = entity_waypoint_indices[patrol]
|
||||
target_x, target_y = waypoints[waypoint_index]
|
||||
|
||||
# Path to waypoint
|
||||
path = patrol.path_to(target_x, target_y)
|
||||
if path and len(path) > 0:
|
||||
next_x, next_y = path[0]
|
||||
dx = next_x - patrol.x
|
||||
dy = next_y - patrol.y
|
||||
patrol.x += dx * dt * animation_speed * 0.5 # Slower patrol speed
|
||||
patrol.y += dy * dt * animation_speed * 0.5
|
||||
|
||||
def update_entities(dt):
|
||||
"""Update all entity movements"""
|
||||
move_enemies(dt / 1000.0) # Convert to seconds
|
||||
move_patrols(dt / 1000.0)
|
||||
|
||||
# Update Dijkstra visualization
|
||||
if show_dijkstra and player:
|
||||
visualize_dijkstra(int(player.x), int(player.y))
|
||||
|
||||
def handle_keypress(scene_name, keycode):
|
||||
"""Handle keyboard input"""
|
||||
global mode, show_dijkstra, player
|
||||
|
||||
# Mode switching
|
||||
if keycode == 49: # '1'
|
||||
mode = "CHASE"
|
||||
mode_text.text = "Mode: CHASE - Enemies pursue player"
|
||||
clear_colors()
|
||||
elif keycode == 50: # '2'
|
||||
mode = "FLEE"
|
||||
mode_text.text = "Mode: FLEE - Enemies avoid player"
|
||||
clear_colors()
|
||||
elif keycode == 51: # '3'
|
||||
mode = "PATROL"
|
||||
mode_text.text = "Mode: PATROL - Entities follow waypoints"
|
||||
clear_colors()
|
||||
|
||||
# Toggle Dijkstra visualization
|
||||
elif keycode == 68 or keycode == 100: # 'D' or 'd'
|
||||
show_dijkstra = not show_dijkstra
|
||||
debug_text.text = f"Dijkstra Debug: {'ON' if show_dijkstra else 'OFF'}"
|
||||
if not show_dijkstra:
|
||||
clear_colors()
|
||||
|
||||
# Move player with arrow keys or WASD
|
||||
elif keycode in [87, 119]: # W/w - Up
|
||||
if player.y > 0:
|
||||
path = player.path_to(int(player.x), int(player.y) - 1)
|
||||
if path:
|
||||
player.y -= 1
|
||||
elif keycode in [83, 115]: # S/s - Down
|
||||
if player.y < 19:
|
||||
path = player.path_to(int(player.x), int(player.y) + 1)
|
||||
if path:
|
||||
player.y += 1
|
||||
elif keycode in [65, 97]: # A/a - Left
|
||||
if player.x > 0:
|
||||
path = player.path_to(int(player.x) - 1, int(player.y))
|
||||
if path:
|
||||
player.x -= 1
|
||||
elif keycode in [68, 100]: # D/d - Right
|
||||
if player.x < 29:
|
||||
path = player.path_to(int(player.x) + 1, int(player.y))
|
||||
if path:
|
||||
player.x += 1
|
||||
|
||||
# Reset
|
||||
elif keycode == 82 or keycode == 114: # 'R' or 'r'
|
||||
spawn_entities()
|
||||
clear_colors()
|
||||
|
||||
# Quit
|
||||
elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
|
||||
print("\nExiting pathfinding showcase...")
|
||||
sys.exit(0)
|
||||
|
||||
def clear_colors():
|
||||
"""Reset floor colors"""
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
if grid.at(x, y).walkable:
|
||||
grid.at(x, y).color = FLOOR_COLOR
|
||||
|
||||
# Create the showcase
|
||||
print("Pathfinding Showcase Demo")
|
||||
print("=========================")
|
||||
print("Controls:")
|
||||
print(" WASD - Move player")
|
||||
print(" 1 - Chase mode (enemies pursue)")
|
||||
print(" 2 - Flee mode (enemies avoid)")
|
||||
print(" 3 - Patrol mode")
|
||||
print(" D - Toggle Dijkstra visualization")
|
||||
print(" R - Reset entities")
|
||||
print(" Q/ESC - Quit")
|
||||
|
||||
# Create dungeon
|
||||
create_dungeon()
|
||||
spawn_entities()
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("pathfinding_showcase")
|
||||
ui.append(grid)
|
||||
|
||||
# Scale and position
|
||||
grid.size = (750, 500) # 30*25, 20*25
|
||||
grid.position = (25, 60)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Pathfinding Showcase", 300, 10)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add mode text
|
||||
mode_text = mcrfpy.Caption("Mode: CHASE - Enemies pursue player", 25, 580)
|
||||
mode_text.fill_color = mcrfpy.Color(255, 255, 200)
|
||||
ui.append(mode_text)
|
||||
|
||||
# Add debug text
|
||||
debug_text = mcrfpy.Caption("Dijkstra Debug: OFF", 25, 600)
|
||||
debug_text.fill_color = mcrfpy.Color(200, 200, 255)
|
||||
ui.append(debug_text)
|
||||
|
||||
# Add legend
|
||||
legend = mcrfpy.Caption("@ Player E Enemy $ Treasure P Patrol", 25, 620)
|
||||
legend.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend)
|
||||
|
||||
# Set up input handling
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
# Set up animation timer
|
||||
mcrfpy.setTimer("entities", update_entities, 16) # 60 FPS
|
||||
|
||||
# Show scene
|
||||
mcrfpy.setScene("pathfinding_showcase")
|
||||
|
||||
print("\nShowcase ready! Move with WASD and watch entities react.")
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple Text Input Widget for McRogueFace
|
||||
Minimal implementation focusing on core functionality
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
|
||||
class TextInput:
|
||||
"""Simple text input widget"""
|
||||
def __init__(self, x, y, width, label=""):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.label = label
|
||||
self.text = ""
|
||||
self.cursor_pos = 0
|
||||
self.focused = False
|
||||
|
||||
# Create UI elements
|
||||
self.frame = mcrfpy.Frame(self.x, self.y, self.width, 24)
|
||||
self.frame.fill_color = (255, 255, 255, 255)
|
||||
self.frame.outline_color = (128, 128, 128, 255)
|
||||
self.frame.outline = 2
|
||||
|
||||
# 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)
|
||||
|
||||
# Text display
|
||||
self.text_caption = mcrfpy.Caption("", self.x + 4, self.y + 4)
|
||||
self.text_caption.fill_color = (0, 0, 0, 255)
|
||||
|
||||
# Cursor (a simple vertical line using a frame)
|
||||
self.cursor = mcrfpy.Frame(self.x + 4, self.y + 4, 2, 16)
|
||||
self.cursor.fill_color = (0, 0, 0, 255)
|
||||
self.cursor.visible = False
|
||||
|
||||
# Click handler
|
||||
self.frame.click = self._on_click
|
||||
|
||||
def _on_click(self, x, y, button):
|
||||
"""Handle clicks"""
|
||||
if button == 1: # Left click
|
||||
# Request focus
|
||||
global current_focus
|
||||
if current_focus and current_focus != self:
|
||||
current_focus.blur()
|
||||
current_focus = self
|
||||
self.focus()
|
||||
|
||||
def focus(self):
|
||||
"""Give focus to this input"""
|
||||
self.focused = True
|
||||
self.frame.outline_color = (0, 120, 255, 255)
|
||||
self.frame.outline = 3
|
||||
self.cursor.visible = True
|
||||
self._update_cursor()
|
||||
|
||||
def blur(self):
|
||||
"""Remove focus"""
|
||||
self.focused = False
|
||||
self.frame.outline_color = (128, 128, 128, 255)
|
||||
self.frame.outline = 2
|
||||
self.cursor.visible = False
|
||||
|
||||
def handle_key(self, key):
|
||||
"""Process keyboard input"""
|
||||
if not self.focused:
|
||||
return False
|
||||
|
||||
if key == "BackSpace":
|
||||
if self.cursor_pos > 0:
|
||||
self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:]
|
||||
self.cursor_pos -= 1
|
||||
elif key == "Delete":
|
||||
if self.cursor_pos < len(self.text):
|
||||
self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:]
|
||||
elif key == "Left":
|
||||
self.cursor_pos = max(0, self.cursor_pos - 1)
|
||||
elif key == "Right":
|
||||
self.cursor_pos = min(len(self.text), self.cursor_pos + 1)
|
||||
elif key == "Home":
|
||||
self.cursor_pos = 0
|
||||
elif key == "End":
|
||||
self.cursor_pos = len(self.text)
|
||||
elif len(key) == 1 and key.isprintable():
|
||||
self.text = self.text[:self.cursor_pos] + key + self.text[self.cursor_pos:]
|
||||
self.cursor_pos += 1
|
||||
else:
|
||||
return False
|
||||
|
||||
self._update_display()
|
||||
return True
|
||||
|
||||
def _update_display(self):
|
||||
"""Update text display"""
|
||||
self.text_caption.text = self.text
|
||||
self._update_cursor()
|
||||
|
||||
def _update_cursor(self):
|
||||
"""Update cursor position"""
|
||||
if self.focused:
|
||||
# Estimate character width (roughly 10 pixels per char)
|
||||
self.cursor.x = self.x + 4 + (self.cursor_pos * 10)
|
||||
|
||||
def add_to_scene(self, scene):
|
||||
"""Add all components to scene"""
|
||||
scene.append(self.frame)
|
||||
if hasattr(self, 'label_caption'):
|
||||
scene.append(self.label_caption)
|
||||
scene.append(self.text_caption)
|
||||
scene.append(self.cursor)
|
||||
|
||||
|
||||
# Global focus tracking
|
||||
current_focus = None
|
||||
text_inputs = []
|
||||
|
||||
|
||||
def demo_test(timer_name):
|
||||
"""Run automated demo after scene loads"""
|
||||
print("\n=== Text Input Widget Demo ===")
|
||||
|
||||
# Test typing in first field
|
||||
print("Testing first input field...")
|
||||
text_inputs[0].focus()
|
||||
for char in "Hello":
|
||||
text_inputs[0].handle_key(char)
|
||||
|
||||
print(f"First field contains: '{text_inputs[0].text}'")
|
||||
|
||||
# Test second field
|
||||
print("\nTesting second input field...")
|
||||
text_inputs[1].focus()
|
||||
for char in "World":
|
||||
text_inputs[1].handle_key(char)
|
||||
|
||||
print(f"Second field contains: '{text_inputs[1].text}'")
|
||||
|
||||
# Test text operations
|
||||
print("\nTesting cursor movement and deletion...")
|
||||
text_inputs[1].handle_key("Home")
|
||||
text_inputs[1].handle_key("Delete")
|
||||
print(f"After delete at start: '{text_inputs[1].text}'")
|
||||
|
||||
text_inputs[1].handle_key("End")
|
||||
text_inputs[1].handle_key("BackSpace")
|
||||
print(f"After backspace at end: '{text_inputs[1].text}'")
|
||||
|
||||
print("\n=== Demo Complete! ===")
|
||||
print("Text input widget is working successfully!")
|
||||
print("Features demonstrated:")
|
||||
print(" - Text entry")
|
||||
print(" - Focus management (blue outline)")
|
||||
print(" - Cursor positioning")
|
||||
print(" - Delete/Backspace operations")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def create_scene():
|
||||
"""Create the demo scene"""
|
||||
global text_inputs
|
||||
|
||||
mcrfpy.createScene("demo")
|
||||
scene = mcrfpy.sceneUI("demo")
|
||||
|
||||
# Background
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600)
|
||||
bg.fill_color = (40, 40, 40, 255)
|
||||
scene.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Text Input Widget Demo", 10, 10)
|
||||
title.fill_color = (255, 255, 255, 255)
|
||||
scene.append(title)
|
||||
|
||||
# Create input fields
|
||||
input1 = TextInput(50, 100, 300, "Name:")
|
||||
input1.add_to_scene(scene)
|
||||
text_inputs.append(input1)
|
||||
|
||||
input2 = TextInput(50, 160, 300, "Email:")
|
||||
input2.add_to_scene(scene)
|
||||
text_inputs.append(input2)
|
||||
|
||||
input3 = TextInput(50, 220, 400, "Comment:")
|
||||
input3.add_to_scene(scene)
|
||||
text_inputs.append(input3)
|
||||
|
||||
# Status text
|
||||
status = mcrfpy.Caption("Click to focus, type to enter text", 50, 280)
|
||||
status.fill_color = (200, 200, 200, 255)
|
||||
scene.append(status)
|
||||
|
||||
# Keyboard handler
|
||||
def handle_keys(scene_name, key):
|
||||
global current_focus, text_inputs
|
||||
|
||||
# Tab to switch fields
|
||||
if key == "Tab" and current_focus:
|
||||
idx = text_inputs.index(current_focus)
|
||||
next_idx = (idx + 1) % len(text_inputs)
|
||||
text_inputs[next_idx]._on_click(0, 0, 1)
|
||||
else:
|
||||
# Pass to focused input
|
||||
if current_focus:
|
||||
current_focus.handle_key(key)
|
||||
# Update status
|
||||
texts = [inp.text for inp in text_inputs]
|
||||
status.text = f"Values: {texts[0]} | {texts[1]} | {texts[2]}"
|
||||
|
||||
mcrfpy.keypressScene("demo", handle_keys)
|
||||
mcrfpy.setScene("demo")
|
||||
|
||||
# Schedule test
|
||||
mcrfpy.setTimer("test", demo_test, 500)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting simple text input demo...")
|
||||
create_scene()
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
title.font_size = 28
|
||||
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 demo1_frame_animations():
|
||||
"""Frame position, size, and color animations"""
|
||||
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)
|
||||
|
||||
# Animate properties
|
||||
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"""
|
||||
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)
|
||||
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)
|
||||
|
||||
def demo3_easing_showcase():
|
||||
"""Show all 30 easing functions"""
|
||||
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)
|
||||
|
||||
# Label
|
||||
label = mcrfpy.Caption(easing[:10], x, y - 20)
|
||||
label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(label)
|
||||
|
||||
# Animate with this easing
|
||||
mcrfpy.Animation("x", float(x + 150), 3.0, easing).start(f)
|
||||
|
||||
def demo4_performance():
|
||||
"""Many simultaneous animations"""
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 4: 50+ Simultaneous Animations"
|
||||
|
||||
for i in range(50):
|
||||
x = 100 + (i % 10) * 100
|
||||
y = 150 + (i // 10) * 100
|
||||
|
||||
f = mcrfpy.Frame(x, y, 30, 30)
|
||||
f.fill_color = mcrfpy.Color((i*37)%256, (i*73)%256, (i*113)%256)
|
||||
ui.append(f)
|
||||
|
||||
# Animate to random position
|
||||
target_x = 150 + (i % 8) * 110
|
||||
target_y = 200 + (i // 8) * 90
|
||||
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)
|
||||
mcrfpy.Animation("opacity", 0.3 + (i%7)*0.1, 2.0, "easeInOutSine").start(f)
|
||||
|
||||
def clear_demo_objects():
|
||||
"""Clear scene except title and subtitle"""
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
# Keep removing items after the first 2 (title and subtitle)
|
||||
while len(ui) > 2:
|
||||
# Remove the last item
|
||||
ui.remove(len(ui)-1)
|
||||
|
||||
def next_demo(runtime):
|
||||
"""Run the next demo"""
|
||||
global current_demo
|
||||
|
||||
clear_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))
|
||||
pass
|
||||
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)
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
McRogueFace Animation Sizzle Reel - Fixed Version
|
||||
=================================================
|
||||
|
||||
This version works around the animation crash by:
|
||||
1. Using shorter demo durations to ensure animations complete before clearing
|
||||
2. Adding a delay before clearing to let animations finish
|
||||
3. Not removing objects, just hiding them off-screen instead
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Configuration
|
||||
DEMO_DURATION = 3.5 # Slightly shorter to ensure animations complete
|
||||
CLEAR_DELAY = 0.5 # Extra delay before clearing
|
||||
|
||||
# All available easing functions
|
||||
EASING_FUNCTIONS = [
|
||||
"linear", "easeIn", "easeOut", "easeInOut",
|
||||
"easeInQuad", "easeOutQuad", "easeInOutQuad",
|
||||
"easeInCubic", "easeOutCubic", "easeInOutCubic",
|
||||
"easeInQuart", "easeOutQuart", "easeInOutQuart",
|
||||
"easeInSine", "easeOutSine", "easeInOutSine",
|
||||
"easeInExpo", "easeOutExpo", "easeInOutExpo",
|
||||
"easeInCirc", "easeOutCirc", "easeInOutCirc",
|
||||
"easeInElastic", "easeOutElastic", "easeInOutElastic",
|
||||
"easeInBack", "easeOutBack", "easeInOutBack",
|
||||
"easeInBounce", "easeOutBounce", "easeInOutBounce"
|
||||
]
|
||||
|
||||
# Track demo state
|
||||
current_demo = 0
|
||||
subtitle = None
|
||||
demo_objects = [] # Track objects to hide instead of remove
|
||||
|
||||
def create_scene():
|
||||
"""Create the demo scene"""
|
||||
mcrfpy.createScene("demo")
|
||||
mcrfpy.setScene("demo")
|
||||
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Animation Sizzle Reel", 500, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
title.outline = 2
|
||||
ui.append(title)
|
||||
|
||||
# Subtitle
|
||||
global subtitle
|
||||
subtitle = mcrfpy.Caption("Starting...", 450, 60)
|
||||
subtitle.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(subtitle)
|
||||
|
||||
return ui
|
||||
|
||||
def hide_demo_objects():
|
||||
"""Hide demo objects by moving them off-screen instead of removing"""
|
||||
global demo_objects
|
||||
# Move all demo objects far off-screen
|
||||
for obj in demo_objects:
|
||||
obj.x = -1000
|
||||
obj.y = -1000
|
||||
demo_objects = []
|
||||
|
||||
def demo1_frame_animations():
|
||||
"""Frame position, size, and color animations"""
|
||||
global demo_objects
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 1: Frame Animations"
|
||||
|
||||
# Create frame
|
||||
f = mcrfpy.Frame(100, 150, 200, 100)
|
||||
f.fill_color = mcrfpy.Color(50, 50, 150)
|
||||
f.outline = 3
|
||||
f.outline_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(f)
|
||||
demo_objects.append(f)
|
||||
|
||||
# Animate properties with shorter durations
|
||||
mcrfpy.Animation("x", 600.0, 2.0, "easeInOutBack").start(f)
|
||||
mcrfpy.Animation("y", 300.0, 2.0, "easeInOutElastic").start(f)
|
||||
mcrfpy.Animation("w", 300.0, 2.5, "easeInOutCubic").start(f)
|
||||
mcrfpy.Animation("h", 150.0, 2.5, "easeInOutCubic").start(f)
|
||||
mcrfpy.Animation("fill_color", (255, 100, 50, 200), 3.0, "easeInOutSine").start(f)
|
||||
mcrfpy.Animation("outline", 8.0, 3.0, "easeInOutQuad").start(f)
|
||||
|
||||
def demo2_caption_animations():
|
||||
"""Caption movement and text effects"""
|
||||
global demo_objects
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 2: Caption Animations"
|
||||
|
||||
# Moving caption
|
||||
c1 = mcrfpy.Caption("Bouncing Text!", 100, 200)
|
||||
c1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(c1)
|
||||
demo_objects.append(c1)
|
||||
mcrfpy.Animation("x", 800.0, 3.0, "easeOutBounce").start(c1)
|
||||
|
||||
# Color cycling
|
||||
c2 = mcrfpy.Caption("Color Cycle", 400, 300)
|
||||
c2.outline = 2
|
||||
ui.append(c2)
|
||||
demo_objects.append(c2)
|
||||
mcrfpy.Animation("fill_color", (255, 0, 0, 255), 1.0, "linear").start(c2)
|
||||
|
||||
# Static text (no typewriter effect to avoid issues)
|
||||
c3 = mcrfpy.Caption("Animation Demo", 100, 400)
|
||||
c3.fill_color = mcrfpy.Color(0, 255, 255)
|
||||
ui.append(c3)
|
||||
demo_objects.append(c3)
|
||||
|
||||
def demo3_easing_showcase():
|
||||
"""Show all 30 easing functions"""
|
||||
global demo_objects
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 3: All 30 Easing Functions"
|
||||
|
||||
# Create a small frame for each easing
|
||||
for i, easing in enumerate(EASING_FUNCTIONS[:15]): # First 15
|
||||
row = i // 5
|
||||
col = i % 5
|
||||
x = 100 + col * 200
|
||||
y = 150 + row * 100
|
||||
|
||||
# Frame
|
||||
f = mcrfpy.Frame(x, y, 20, 20)
|
||||
f.fill_color = mcrfpy.Color(100, 150, 255)
|
||||
ui.append(f)
|
||||
demo_objects.append(f)
|
||||
|
||||
# Label
|
||||
label = mcrfpy.Caption(easing[:10], x, y - 20)
|
||||
label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(label)
|
||||
demo_objects.append(label)
|
||||
|
||||
# Animate with this easing
|
||||
mcrfpy.Animation("x", float(x + 150), 3.0, easing).start(f)
|
||||
|
||||
def demo4_performance():
|
||||
"""Many simultaneous animations"""
|
||||
global demo_objects
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
subtitle.text = "Demo 4: 50+ Simultaneous Animations"
|
||||
|
||||
for i in range(50):
|
||||
x = 100 + (i % 10) * 80
|
||||
y = 150 + (i // 10) * 80
|
||||
|
||||
f = mcrfpy.Frame(x, y, 30, 30)
|
||||
f.fill_color = mcrfpy.Color((i*37)%256, (i*73)%256, (i*113)%256)
|
||||
ui.append(f)
|
||||
demo_objects.append(f)
|
||||
|
||||
# Animate to random position
|
||||
target_x = 150 + (i % 8) * 90
|
||||
target_y = 200 + (i // 8) * 70
|
||||
easing = EASING_FUNCTIONS[i % len(EASING_FUNCTIONS)]
|
||||
|
||||
mcrfpy.Animation("x", float(target_x), 2.5, easing).start(f)
|
||||
mcrfpy.Animation("y", float(target_y), 2.5, easing).start(f)
|
||||
|
||||
def next_demo(runtime):
|
||||
"""Run the next demo with proper cleanup"""
|
||||
global current_demo
|
||||
|
||||
# First hide old objects
|
||||
hide_demo_objects()
|
||||
|
||||
demos = [
|
||||
demo1_frame_animations,
|
||||
demo2_caption_animations,
|
||||
demo3_easing_showcase,
|
||||
demo4_performance
|
||||
]
|
||||
|
||||
if current_demo < len(demos):
|
||||
demos[current_demo]()
|
||||
current_demo += 1
|
||||
|
||||
if current_demo < len(demos):
|
||||
mcrfpy.setTimer("next", next_demo, int(DEMO_DURATION * 1000))
|
||||
else:
|
||||
subtitle.text = "Demo Complete!"
|
||||
mcrfpy.setTimer("exit", lambda t: mcrfpy.exit(), 2000)
|
||||
|
||||
# Initialize
|
||||
print("Starting Animation Sizzle Reel (Fixed)...")
|
||||
create_scene()
|
||||
mcrfpy.setTimer("start", next_demo, 500)
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Text Input Demo with Auto-Test
|
||||
Demonstrates the text input widget system with automated testing
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
from text_input_widget import FocusManager, TextInput
|
||||
|
||||
|
||||
def test_text_input(timer_name):
|
||||
"""Automated test that runs after scene is loaded"""
|
||||
print("Testing text input widget system...")
|
||||
|
||||
# Take a screenshot of the initial state
|
||||
automation.screenshot("text_input_initial.png")
|
||||
|
||||
# Simulate typing in the first field
|
||||
print("Clicking on first field...")
|
||||
automation.click(200, 130) # Click on name field
|
||||
|
||||
# Type some text
|
||||
for char in "John Doe":
|
||||
mcrfpy.keypressScene("text_input_demo", char)
|
||||
|
||||
# Tab to next field
|
||||
mcrfpy.keypressScene("text_input_demo", "Tab")
|
||||
|
||||
# Type email
|
||||
for char in "john@example.com":
|
||||
mcrfpy.keypressScene("text_input_demo", char)
|
||||
|
||||
# Tab to comment field
|
||||
mcrfpy.keypressScene("text_input_demo", "Tab")
|
||||
|
||||
# Type comment
|
||||
for char in "Testing the widget!":
|
||||
mcrfpy.keypressScene("text_input_demo", char)
|
||||
|
||||
# Take final screenshot
|
||||
automation.screenshot("text_input_filled.png")
|
||||
|
||||
print("Text input test complete!")
|
||||
print("Screenshots saved: text_input_initial.png, text_input_filled.png")
|
||||
|
||||
# Exit after test
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def create_demo():
|
||||
"""Create a demo scene with multiple text input fields"""
|
||||
mcrfpy.createScene("text_input_demo")
|
||||
scene = mcrfpy.sceneUI("text_input_demo")
|
||||
|
||||
# Create background
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600)
|
||||
bg.fill_color = (40, 40, 40, 255)
|
||||
scene.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(10, 10, "Text Input Widget Demo - Auto Test")
|
||||
title.color = (255, 255, 255, 255)
|
||||
scene.append(title)
|
||||
|
||||
# Instructions
|
||||
instructions = mcrfpy.Caption(10, 50, "This will automatically test the text input system")
|
||||
instructions.color = (200, 200, 200, 255)
|
||||
scene.append(instructions)
|
||||
|
||||
# Create focus manager
|
||||
focus_manager = FocusManager()
|
||||
|
||||
# Create text input fields
|
||||
fields = []
|
||||
|
||||
# Name field
|
||||
name_input = TextInput(50, 120, 300, "Name:", 16)
|
||||
name_input._focus_manager = focus_manager
|
||||
focus_manager.register(name_input)
|
||||
scene.append(name_input.frame)
|
||||
if hasattr(name_input, 'label_text'):
|
||||
scene.append(name_input.label_text)
|
||||
scene.append(name_input.text_display)
|
||||
scene.append(name_input.cursor)
|
||||
fields.append(name_input)
|
||||
|
||||
# Email field
|
||||
email_input = TextInput(50, 180, 300, "Email:", 16)
|
||||
email_input._focus_manager = focus_manager
|
||||
focus_manager.register(email_input)
|
||||
scene.append(email_input.frame)
|
||||
if hasattr(email_input, 'label_text'):
|
||||
scene.append(email_input.label_text)
|
||||
scene.append(email_input.text_display)
|
||||
scene.append(email_input.cursor)
|
||||
fields.append(email_input)
|
||||
|
||||
# Comment field
|
||||
comment_input = TextInput(50, 240, 400, "Comment:", 16)
|
||||
comment_input._focus_manager = focus_manager
|
||||
focus_manager.register(comment_input)
|
||||
scene.append(comment_input.frame)
|
||||
if hasattr(comment_input, 'label_text'):
|
||||
scene.append(comment_input.label_text)
|
||||
scene.append(comment_input.text_display)
|
||||
scene.append(comment_input.cursor)
|
||||
fields.append(comment_input)
|
||||
|
||||
# Result display
|
||||
result_text = mcrfpy.Caption(50, 320, "Values will appear here as you type...")
|
||||
result_text.color = (150, 255, 150, 255)
|
||||
scene.append(result_text)
|
||||
|
||||
def update_result(*args):
|
||||
"""Update the result display with current field values"""
|
||||
name = fields[0].get_text()
|
||||
email = fields[1].get_text()
|
||||
comment = fields[2].get_text()
|
||||
result_text.text = f"Name: {name} | Email: {email} | Comment: {comment}"
|
||||
|
||||
# Set change handlers
|
||||
for field in fields:
|
||||
field.on_change = update_result
|
||||
|
||||
# Keyboard handler
|
||||
def handle_keys(scene_name, key):
|
||||
"""Global keyboard handler"""
|
||||
# Let focus manager handle the key first
|
||||
if not focus_manager.handle_key(key):
|
||||
# Handle focus switching
|
||||
if key == "Tab":
|
||||
focus_manager.focus_next()
|
||||
elif key == "Escape":
|
||||
print("Demo terminated by user")
|
||||
sys.exit(0)
|
||||
|
||||
mcrfpy.keypressScene("text_input_demo", handle_keys)
|
||||
|
||||
# Set the scene
|
||||
mcrfpy.setScene("text_input_demo")
|
||||
|
||||
# Schedule the automated test
|
||||
mcrfpy.setTimer("test", test_text_input, 500) # Run test after 500ms
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_demo()
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Standalone Text Input Widget System for McRogueFace
|
||||
Complete implementation with demo and automated test
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
|
||||
class FocusManager:
|
||||
"""Manages focus state across multiple widgets"""
|
||||
def __init__(self):
|
||||
self.widgets = []
|
||||
self.focused_widget = None
|
||||
self.focus_index = -1
|
||||
|
||||
def register(self, widget):
|
||||
"""Register a widget with the focus manager"""
|
||||
self.widgets.append(widget)
|
||||
if self.focused_widget is None:
|
||||
self.focus(widget)
|
||||
|
||||
def focus(self, widget):
|
||||
"""Set focus to a specific widget"""
|
||||
if self.focused_widget:
|
||||
self.focused_widget.on_blur()
|
||||
|
||||
self.focused_widget = widget
|
||||
self.focus_index = self.widgets.index(widget) if widget in self.widgets else -1
|
||||
|
||||
if widget:
|
||||
widget.on_focus()
|
||||
|
||||
def focus_next(self):
|
||||
"""Focus the next widget in the list"""
|
||||
if not self.widgets:
|
||||
return
|
||||
|
||||
self.focus_index = (self.focus_index + 1) % len(self.widgets)
|
||||
self.focus(self.widgets[self.focus_index])
|
||||
|
||||
def handle_key(self, key):
|
||||
"""Route key events to focused widget. Returns True if handled."""
|
||||
if self.focused_widget:
|
||||
return self.focused_widget.handle_key(key)
|
||||
return False
|
||||
|
||||
|
||||
class TextInput:
|
||||
"""A text input widget with cursor support"""
|
||||
def __init__(self, x, y, width, label="", font_size=16):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.label = label
|
||||
self.font_size = font_size
|
||||
|
||||
# Text state
|
||||
self.text = ""
|
||||
self.cursor_pos = 0
|
||||
|
||||
# Visual state
|
||||
self.focused = False
|
||||
|
||||
# Create UI elements
|
||||
self._create_ui()
|
||||
|
||||
def _create_ui(self):
|
||||
"""Create the visual components"""
|
||||
# Background frame
|
||||
self.frame = mcrfpy.Frame(self.x, self.y, self.width, self.font_size + 8)
|
||||
self.frame.outline = 2
|
||||
self.frame.fill_color = (255, 255, 255, 255)
|
||||
self.frame.outline_color = (128, 128, 128, 255)
|
||||
|
||||
# Label (if provided)
|
||||
if self.label:
|
||||
self.label_text = mcrfpy.Caption(
|
||||
self.x - 5,
|
||||
self.y - self.font_size - 5,
|
||||
self.label
|
||||
)
|
||||
self.label_text.color = (255, 255, 255, 255)
|
||||
|
||||
# Text display
|
||||
self.text_display = mcrfpy.Caption(
|
||||
self.x + 4,
|
||||
self.y + 4,
|
||||
""
|
||||
)
|
||||
self.text_display.color = (0, 0, 0, 255)
|
||||
|
||||
# Cursor (using a thin frame)
|
||||
self.cursor = mcrfpy.Frame(
|
||||
self.x + 4,
|
||||
self.y + 4,
|
||||
2,
|
||||
self.font_size
|
||||
)
|
||||
self.cursor.fill_color = (0, 0, 0, 255)
|
||||
self.cursor.visible = False
|
||||
|
||||
# Click handler
|
||||
self.frame.click = self._on_click
|
||||
|
||||
def _on_click(self, x, y, button):
|
||||
"""Handle mouse clicks on the input field"""
|
||||
if button == 1: # Left click
|
||||
if hasattr(self, '_focus_manager'):
|
||||
self._focus_manager.focus(self)
|
||||
|
||||
def on_focus(self):
|
||||
"""Called when this widget receives focus"""
|
||||
self.focused = True
|
||||
self.frame.outline_color = (0, 120, 255, 255)
|
||||
self.frame.outline = 3
|
||||
self.cursor.visible = True
|
||||
self._update_cursor_position()
|
||||
|
||||
def on_blur(self):
|
||||
"""Called when this widget loses focus"""
|
||||
self.focused = False
|
||||
self.frame.outline_color = (128, 128, 128, 255)
|
||||
self.frame.outline = 2
|
||||
self.cursor.visible = False
|
||||
|
||||
def handle_key(self, key):
|
||||
"""Handle keyboard input. Returns True if key was handled."""
|
||||
if not self.focused:
|
||||
return False
|
||||
|
||||
handled = True
|
||||
|
||||
# Special keys
|
||||
if key == "BackSpace":
|
||||
if self.cursor_pos > 0:
|
||||
self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:]
|
||||
self.cursor_pos -= 1
|
||||
elif key == "Delete":
|
||||
if self.cursor_pos < len(self.text):
|
||||
self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:]
|
||||
elif key == "Left":
|
||||
self.cursor_pos = max(0, self.cursor_pos - 1)
|
||||
elif key == "Right":
|
||||
self.cursor_pos = min(len(self.text), self.cursor_pos + 1)
|
||||
elif key == "Home":
|
||||
self.cursor_pos = 0
|
||||
elif key == "End":
|
||||
self.cursor_pos = len(self.text)
|
||||
elif key == "Tab":
|
||||
handled = False # Let focus manager handle
|
||||
elif len(key) == 1 and key.isprintable():
|
||||
# Regular character input
|
||||
self.text = self.text[:self.cursor_pos] + key + self.text[self.cursor_pos:]
|
||||
self.cursor_pos += 1
|
||||
else:
|
||||
handled = False
|
||||
|
||||
# Update display
|
||||
self._update_display()
|
||||
|
||||
return handled
|
||||
|
||||
def _update_display(self):
|
||||
"""Update the text display and cursor position"""
|
||||
self.text_display.text = self.text
|
||||
self._update_cursor_position()
|
||||
|
||||
def _update_cursor_position(self):
|
||||
"""Update cursor visual position based on text position"""
|
||||
if not self.focused:
|
||||
return
|
||||
|
||||
# Simple character width estimation (monospace assumption)
|
||||
char_width = self.font_size * 0.6
|
||||
cursor_x = self.x + 4 + int(self.cursor_pos * char_width)
|
||||
self.cursor.x = cursor_x
|
||||
|
||||
def get_text(self):
|
||||
"""Get the current text content"""
|
||||
return self.text
|
||||
|
||||
def add_to_scene(self, scene):
|
||||
"""Add all components to a scene"""
|
||||
scene.append(self.frame)
|
||||
if hasattr(self, 'label_text'):
|
||||
scene.append(self.label_text)
|
||||
scene.append(self.text_display)
|
||||
scene.append(self.cursor)
|
||||
|
||||
|
||||
def run_automated_test(timer_name):
|
||||
"""Automated test that demonstrates the text input functionality"""
|
||||
print("\n=== Running Text Input Widget Test ===")
|
||||
|
||||
# Take initial screenshot
|
||||
if hasattr(mcrfpy, 'automation'):
|
||||
mcrfpy.automation.screenshot("text_input_test_1_initial.png")
|
||||
print("Screenshot 1: Initial state saved")
|
||||
|
||||
# Simulate some typing
|
||||
print("Simulating keyboard input...")
|
||||
|
||||
# The scene's keyboard handler will process these
|
||||
test_sequence = [
|
||||
("H", "Typing 'H'"),
|
||||
("e", "Typing 'e'"),
|
||||
("l", "Typing 'l'"),
|
||||
("l", "Typing 'l'"),
|
||||
("o", "Typing 'o'"),
|
||||
("Tab", "Switching to next field"),
|
||||
("T", "Typing 'T'"),
|
||||
("e", "Typing 'e'"),
|
||||
("s", "Typing 's'"),
|
||||
("t", "Typing 't'"),
|
||||
("Tab", "Switching to comment field"),
|
||||
("W", "Typing 'W'"),
|
||||
("o", "Typing 'o'"),
|
||||
("r", "Typing 'r'"),
|
||||
("k", "Typing 'k'"),
|
||||
("s", "Typing 's'"),
|
||||
("!", "Typing '!'"),
|
||||
]
|
||||
|
||||
# Process each key
|
||||
for key, desc in test_sequence:
|
||||
print(f" - {desc}")
|
||||
# Trigger the scene's keyboard handler
|
||||
if hasattr(mcrfpy, '_scene_key_handler'):
|
||||
mcrfpy._scene_key_handler("text_input_demo", key)
|
||||
|
||||
# Take final screenshot
|
||||
if hasattr(mcrfpy, 'automation'):
|
||||
mcrfpy.automation.screenshot("text_input_test_2_filled.png")
|
||||
print("Screenshot 2: Filled state saved")
|
||||
|
||||
print("\n=== Text Input Test Complete! ===")
|
||||
print("The text input widget system is working correctly.")
|
||||
print("Features demonstrated:")
|
||||
print(" - Focus management (blue outline on focused field)")
|
||||
print(" - Text entry with cursor")
|
||||
print(" - Tab navigation between fields")
|
||||
print(" - Visual feedback")
|
||||
|
||||
# Exit successfully
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def create_demo():
|
||||
"""Create the demo scene"""
|
||||
mcrfpy.createScene("text_input_demo")
|
||||
scene = mcrfpy.sceneUI("text_input_demo")
|
||||
|
||||
# Create background
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600)
|
||||
bg.fill_color = (40, 40, 40, 255)
|
||||
scene.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(10, 10, "Text Input Widget System")
|
||||
title.color = (255, 255, 255, 255)
|
||||
scene.append(title)
|
||||
|
||||
# Instructions
|
||||
info = mcrfpy.Caption(10, 50, "Click to focus | Tab to switch fields | Type to enter text")
|
||||
info.color = (200, 200, 200, 255)
|
||||
scene.append(info)
|
||||
|
||||
# Create focus manager
|
||||
focus_manager = FocusManager()
|
||||
|
||||
# Create text inputs
|
||||
name_input = TextInput(50, 120, 300, "Name:", 16)
|
||||
name_input._focus_manager = focus_manager
|
||||
focus_manager.register(name_input)
|
||||
name_input.add_to_scene(scene)
|
||||
|
||||
email_input = TextInput(50, 180, 300, "Email:", 16)
|
||||
email_input._focus_manager = focus_manager
|
||||
focus_manager.register(email_input)
|
||||
email_input.add_to_scene(scene)
|
||||
|
||||
comment_input = TextInput(50, 240, 400, "Comment:", 16)
|
||||
comment_input._focus_manager = focus_manager
|
||||
focus_manager.register(comment_input)
|
||||
comment_input.add_to_scene(scene)
|
||||
|
||||
# Status display
|
||||
status = mcrfpy.Caption(50, 320, "Ready for input...")
|
||||
status.color = (150, 255, 150, 255)
|
||||
scene.append(status)
|
||||
|
||||
# Store references for the keyboard handler
|
||||
widgets = [name_input, email_input, comment_input]
|
||||
|
||||
# Keyboard handler
|
||||
def handle_keys(scene_name, key):
|
||||
"""Global keyboard handler"""
|
||||
if not focus_manager.handle_key(key):
|
||||
if key == "Tab":
|
||||
focus_manager.focus_next()
|
||||
|
||||
# Update status
|
||||
texts = [w.get_text() for w in widgets]
|
||||
status.text = f"Name: '{texts[0]}' | Email: '{texts[1]}' | Comment: '{texts[2]}'"
|
||||
|
||||
# Store handler reference for test
|
||||
mcrfpy._scene_key_handler = handle_keys
|
||||
|
||||
mcrfpy.keypressScene("text_input_demo", handle_keys)
|
||||
mcrfpy.setScene("text_input_demo")
|
||||
|
||||
# Schedule automated test
|
||||
mcrfpy.setTimer("test", run_automated_test, 1000) # Run after 1 second
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting Text Input Widget Demo...")
|
||||
create_demo()
|
||||
|
|
@ -0,0 +1,320 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Text Input Widget System for McRogueFace
|
||||
A pure Python implementation of focusable text input fields
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List, Callable
|
||||
|
||||
|
||||
class FocusManager:
|
||||
"""Manages focus state across multiple widgets"""
|
||||
def __init__(self):
|
||||
self.widgets: List['TextInput'] = []
|
||||
self.focused_widget: Optional['TextInput'] = None
|
||||
self.focus_index: int = -1
|
||||
|
||||
def register(self, widget: 'TextInput'):
|
||||
"""Register a widget with the focus manager"""
|
||||
self.widgets.append(widget)
|
||||
if self.focused_widget is None:
|
||||
self.focus(widget)
|
||||
|
||||
def focus(self, widget: 'TextInput'):
|
||||
"""Set focus to a specific widget"""
|
||||
if self.focused_widget:
|
||||
self.focused_widget.on_blur()
|
||||
|
||||
self.focused_widget = widget
|
||||
self.focus_index = self.widgets.index(widget) if widget in self.widgets else -1
|
||||
|
||||
if widget:
|
||||
widget.on_focus()
|
||||
|
||||
def focus_next(self):
|
||||
"""Focus the next widget in the list"""
|
||||
if not self.widgets:
|
||||
return
|
||||
|
||||
self.focus_index = (self.focus_index + 1) % len(self.widgets)
|
||||
self.focus(self.widgets[self.focus_index])
|
||||
|
||||
def focus_prev(self):
|
||||
"""Focus the previous widget in the list"""
|
||||
if not self.widgets:
|
||||
return
|
||||
|
||||
self.focus_index = (self.focus_index - 1) % len(self.widgets)
|
||||
self.focus(self.widgets[self.focus_index])
|
||||
|
||||
def handle_key(self, key: str) -> bool:
|
||||
"""Route key events to focused widget. Returns True if handled."""
|
||||
if self.focused_widget:
|
||||
return self.focused_widget.handle_key(key)
|
||||
return False
|
||||
|
||||
|
||||
class TextInput:
|
||||
"""A text input widget with cursor and selection support"""
|
||||
def __init__(self, x: int, y: int, width: int = 200, label: str = "",
|
||||
font_size: int = 16, on_change: Optional[Callable] = None):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.label = label
|
||||
self.font_size = font_size
|
||||
self.on_change = on_change
|
||||
|
||||
# Text state
|
||||
self.text = ""
|
||||
self.cursor_pos = 0
|
||||
self.selection_start = -1
|
||||
self.selection_end = -1
|
||||
|
||||
# Visual state
|
||||
self.focused = False
|
||||
self.cursor_visible = True
|
||||
self.cursor_blink_timer = 0
|
||||
|
||||
# Create UI elements
|
||||
self._create_ui()
|
||||
|
||||
def _create_ui(self):
|
||||
"""Create the visual components"""
|
||||
# Background frame
|
||||
self.frame = mcrfpy.Frame(self.x, self.y, self.width, self.font_size + 8)
|
||||
self.frame.outline = 2
|
||||
self.frame.fill_color = (255, 255, 255, 255)
|
||||
self.frame.outline_color = (128, 128, 128, 255)
|
||||
|
||||
# Label (if provided)
|
||||
if self.label:
|
||||
self.label_text = mcrfpy.Caption(
|
||||
self.x - 5,
|
||||
self.y - self.font_size - 5,
|
||||
self.label
|
||||
)
|
||||
self.label_text.color = (255, 255, 255, 255)
|
||||
|
||||
# Text display
|
||||
self.text_display = mcrfpy.Caption(
|
||||
self.x + 4,
|
||||
self.y + 4,
|
||||
""
|
||||
)
|
||||
self.text_display.color = (0, 0, 0, 255)
|
||||
|
||||
# Cursor (using a thin frame)
|
||||
self.cursor = mcrfpy.Frame(
|
||||
self.x + 4,
|
||||
self.y + 4,
|
||||
2,
|
||||
self.font_size
|
||||
)
|
||||
self.cursor.fill_color = (0, 0, 0, 255)
|
||||
self.cursor.visible = False
|
||||
|
||||
# Click handler
|
||||
self.frame.click = self._on_click
|
||||
|
||||
def _on_click(self, x: int, y: int, button: int):
|
||||
"""Handle mouse clicks on the input field"""
|
||||
if button == 1: # Left click
|
||||
# Request focus through the focus manager
|
||||
if hasattr(self, '_focus_manager'):
|
||||
self._focus_manager.focus(self)
|
||||
|
||||
def on_focus(self):
|
||||
"""Called when this widget receives focus"""
|
||||
self.focused = True
|
||||
self.frame.outline_color = (0, 120, 255, 255)
|
||||
self.frame.outline = 3
|
||||
self.cursor.visible = True
|
||||
self._update_cursor_position()
|
||||
|
||||
def on_blur(self):
|
||||
"""Called when this widget loses focus"""
|
||||
self.focused = False
|
||||
self.frame.outline_color = (128, 128, 128, 255)
|
||||
self.frame.outline = 2
|
||||
self.cursor.visible = False
|
||||
|
||||
def handle_key(self, key: str) -> bool:
|
||||
"""Handle keyboard input. Returns True if key was handled."""
|
||||
if not self.focused:
|
||||
return False
|
||||
|
||||
handled = True
|
||||
old_text = self.text
|
||||
|
||||
# Special keys
|
||||
if key == "BackSpace":
|
||||
if self.cursor_pos > 0:
|
||||
self.text = self.text[:self.cursor_pos-1] + self.text[self.cursor_pos:]
|
||||
self.cursor_pos -= 1
|
||||
elif key == "Delete":
|
||||
if self.cursor_pos < len(self.text):
|
||||
self.text = self.text[:self.cursor_pos] + self.text[self.cursor_pos+1:]
|
||||
elif key == "Left":
|
||||
self.cursor_pos = max(0, self.cursor_pos - 1)
|
||||
elif key == "Right":
|
||||
self.cursor_pos = min(len(self.text), self.cursor_pos + 1)
|
||||
elif key == "Home":
|
||||
self.cursor_pos = 0
|
||||
elif key == "End":
|
||||
self.cursor_pos = len(self.text)
|
||||
elif key == "Return":
|
||||
handled = False # Let parent handle submit
|
||||
elif key == "Tab":
|
||||
handled = False # Let focus manager handle
|
||||
elif len(key) == 1 and key.isprintable():
|
||||
# Regular character input
|
||||
self.text = self.text[:self.cursor_pos] + key + self.text[self.cursor_pos:]
|
||||
self.cursor_pos += 1
|
||||
else:
|
||||
handled = False
|
||||
|
||||
# Update display
|
||||
if old_text != self.text:
|
||||
self._update_display()
|
||||
if self.on_change:
|
||||
self.on_change(self.text)
|
||||
else:
|
||||
self._update_cursor_position()
|
||||
|
||||
return handled
|
||||
|
||||
def _update_display(self):
|
||||
"""Update the text display and cursor position"""
|
||||
self.text_display.text = self.text
|
||||
self._update_cursor_position()
|
||||
|
||||
def _update_cursor_position(self):
|
||||
"""Update cursor visual position based on text position"""
|
||||
if not self.focused:
|
||||
return
|
||||
|
||||
# Simple character width estimation (monospace assumption)
|
||||
char_width = self.font_size * 0.6
|
||||
cursor_x = self.x + 4 + int(self.cursor_pos * char_width)
|
||||
self.cursor.x = cursor_x
|
||||
|
||||
def set_text(self, text: str):
|
||||
"""Set the text content"""
|
||||
self.text = text
|
||||
self.cursor_pos = len(text)
|
||||
self._update_display()
|
||||
|
||||
def get_text(self) -> str:
|
||||
"""Get the current text content"""
|
||||
return self.text
|
||||
|
||||
|
||||
# Demo application
|
||||
def create_demo():
|
||||
"""Create a demo scene with multiple text input fields"""
|
||||
mcrfpy.createScene("text_input_demo")
|
||||
scene = mcrfpy.sceneUI("text_input_demo")
|
||||
|
||||
# Create background
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600)
|
||||
bg.fill_color = (40, 40, 40, 255)
|
||||
scene.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(10, 10, "Text Input Widget Demo")
|
||||
title.color = (255, 255, 255, 255)
|
||||
scene.append(title)
|
||||
|
||||
# Instructions
|
||||
instructions = mcrfpy.Caption(10, 50, "Click to focus, Tab to switch fields, Type to enter text")
|
||||
instructions.color = (200, 200, 200, 255)
|
||||
scene.append(instructions)
|
||||
|
||||
# Create focus manager
|
||||
focus_manager = FocusManager()
|
||||
|
||||
# Create text input fields
|
||||
fields = []
|
||||
|
||||
# Name field
|
||||
name_input = TextInput(50, 120, 300, "Name:", 16)
|
||||
name_input._focus_manager = focus_manager
|
||||
focus_manager.register(name_input)
|
||||
scene.append(name_input.frame)
|
||||
if hasattr(name_input, 'label_text'):
|
||||
scene.append(name_input.label_text)
|
||||
scene.append(name_input.text_display)
|
||||
scene.append(name_input.cursor)
|
||||
fields.append(name_input)
|
||||
|
||||
# Email field
|
||||
email_input = TextInput(50, 180, 300, "Email:", 16)
|
||||
email_input._focus_manager = focus_manager
|
||||
focus_manager.register(email_input)
|
||||
scene.append(email_input.frame)
|
||||
if hasattr(email_input, 'label_text'):
|
||||
scene.append(email_input.label_text)
|
||||
scene.append(email_input.text_display)
|
||||
scene.append(email_input.cursor)
|
||||
fields.append(email_input)
|
||||
|
||||
# Comment field
|
||||
comment_input = TextInput(50, 240, 400, "Comment:", 16)
|
||||
comment_input._focus_manager = focus_manager
|
||||
focus_manager.register(comment_input)
|
||||
scene.append(comment_input.frame)
|
||||
if hasattr(comment_input, 'label_text'):
|
||||
scene.append(comment_input.label_text)
|
||||
scene.append(comment_input.text_display)
|
||||
scene.append(comment_input.cursor)
|
||||
fields.append(comment_input)
|
||||
|
||||
# Result display
|
||||
result_text = mcrfpy.Caption(50, 320, "Type in the fields above...")
|
||||
result_text.color = (150, 255, 150, 255)
|
||||
scene.append(result_text)
|
||||
|
||||
def update_result(*args):
|
||||
"""Update the result display with current field values"""
|
||||
name = fields[0].get_text()
|
||||
email = fields[1].get_text()
|
||||
comment = fields[2].get_text()
|
||||
result_text.text = f"Name: {name} | Email: {email} | Comment: {comment}"
|
||||
|
||||
# Set change handlers
|
||||
for field in fields:
|
||||
field.on_change = update_result
|
||||
|
||||
# Keyboard handler
|
||||
def handle_keys(scene_name, key):
|
||||
"""Global keyboard handler"""
|
||||
# Let focus manager handle the key first
|
||||
if not focus_manager.handle_key(key):
|
||||
# Handle focus switching
|
||||
if key == "Tab":
|
||||
focus_manager.focus_next()
|
||||
elif key == "Escape":
|
||||
print("Demo complete!")
|
||||
sys.exit(0)
|
||||
|
||||
mcrfpy.keypressScene("text_input_demo", handle_keys)
|
||||
|
||||
# Set the scene
|
||||
mcrfpy.setScene("text_input_demo")
|
||||
|
||||
# Add a timer for cursor blinking (optional enhancement)
|
||||
def blink_cursor(timer_name):
|
||||
"""Blink the cursor for the focused widget"""
|
||||
if focus_manager.focused_widget and focus_manager.focused_widget.focused:
|
||||
cursor = focus_manager.focused_widget.cursor
|
||||
cursor.visible = not cursor.visible
|
||||
|
||||
mcrfpy.setTimer("cursor_blink", blink_cursor, 500) # Blink every 500ms
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_demo()
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
A* vs Dijkstra Visual Comparison
|
||||
=================================
|
||||
|
||||
Shows the difference between A* (single target) and Dijkstra (multi-target).
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 20, 20)
|
||||
FLOOR_COLOR = mcrfpy.Color(60, 60, 80)
|
||||
ASTAR_COLOR = mcrfpy.Color(0, 255, 0) # Green for A*
|
||||
DIJKSTRA_COLOR = mcrfpy.Color(0, 150, 255) # Blue for Dijkstra
|
||||
START_COLOR = mcrfpy.Color(255, 100, 100) # Red for start
|
||||
END_COLOR = mcrfpy.Color(255, 255, 100) # Yellow for end
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
mode = "ASTAR"
|
||||
start_pos = (5, 10)
|
||||
end_pos = (27, 10) # Changed from 25 to 27 to avoid the wall
|
||||
|
||||
def create_map():
|
||||
"""Create a map with obstacles to show pathfinding differences"""
|
||||
global grid
|
||||
|
||||
mcrfpy.createScene("pathfinding_comparison")
|
||||
|
||||
# Create grid
|
||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Initialize all as floor
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
grid.at(x, y).walkable = True
|
||||
grid.at(x, y).color = FLOOR_COLOR
|
||||
|
||||
# Create obstacles that make A* and Dijkstra differ
|
||||
obstacles = [
|
||||
# Vertical wall with gaps
|
||||
[(15, y) for y in range(3, 17) if y not in [8, 12]],
|
||||
# Horizontal walls
|
||||
[(x, 5) for x in range(10, 20)],
|
||||
[(x, 15) for x in range(10, 20)],
|
||||
# Maze-like structure
|
||||
[(x, 10) for x in range(20, 25)],
|
||||
[(25, y) for y in range(5, 15)],
|
||||
]
|
||||
|
||||
for obstacle_group in obstacles:
|
||||
for x, y in obstacle_group:
|
||||
grid.at(x, y).walkable = False
|
||||
grid.at(x, y).color = WALL_COLOR
|
||||
|
||||
# Mark start and end
|
||||
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
|
||||
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
|
||||
|
||||
def clear_paths():
|
||||
"""Clear path highlighting"""
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
cell = grid.at(x, y)
|
||||
if cell.walkable:
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
# Restore start and end colors
|
||||
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
|
||||
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
|
||||
|
||||
def show_astar():
|
||||
"""Show A* path"""
|
||||
clear_paths()
|
||||
|
||||
# Compute A* path
|
||||
path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
|
||||
|
||||
# Color the path
|
||||
for i, (x, y) in enumerate(path):
|
||||
if (x, y) != start_pos and (x, y) != end_pos:
|
||||
grid.at(x, y).color = ASTAR_COLOR
|
||||
|
||||
status_text.text = f"A* Path: {len(path)} steps (optimized for single target)"
|
||||
status_text.fill_color = ASTAR_COLOR
|
||||
|
||||
def show_dijkstra():
|
||||
"""Show Dijkstra exploration"""
|
||||
clear_paths()
|
||||
|
||||
# Compute Dijkstra from start
|
||||
grid.compute_dijkstra(start_pos[0], start_pos[1])
|
||||
|
||||
# Color cells by distance (showing exploration)
|
||||
max_dist = 40.0
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
if grid.at(x, y).walkable:
|
||||
dist = grid.get_dijkstra_distance(x, y)
|
||||
if dist is not None and dist < max_dist:
|
||||
# Color based on distance
|
||||
intensity = int(255 * (1 - dist / max_dist))
|
||||
grid.at(x, y).color = mcrfpy.Color(0, intensity // 2, intensity)
|
||||
|
||||
# Get the actual path
|
||||
path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
|
||||
|
||||
# Highlight the actual path more brightly
|
||||
for x, y in path:
|
||||
if (x, y) != start_pos and (x, y) != end_pos:
|
||||
grid.at(x, y).color = DIJKSTRA_COLOR
|
||||
|
||||
# Restore start and end
|
||||
grid.at(start_pos[0], start_pos[1]).color = START_COLOR
|
||||
grid.at(end_pos[0], end_pos[1]).color = END_COLOR
|
||||
|
||||
status_text.text = f"Dijkstra: {len(path)} steps (explores all directions)"
|
||||
status_text.fill_color = DIJKSTRA_COLOR
|
||||
|
||||
def show_both():
|
||||
"""Show both paths overlaid"""
|
||||
clear_paths()
|
||||
|
||||
# Get both paths
|
||||
astar_path = grid.compute_astar_path(start_pos[0], start_pos[1], end_pos[0], end_pos[1])
|
||||
grid.compute_dijkstra(start_pos[0], start_pos[1])
|
||||
dijkstra_path = grid.get_dijkstra_path(end_pos[0], end_pos[1])
|
||||
|
||||
print(astar_path, dijkstra_path)
|
||||
|
||||
# Color Dijkstra path first (blue)
|
||||
for x, y in dijkstra_path:
|
||||
if (x, y) != start_pos and (x, y) != end_pos:
|
||||
grid.at(x, y).color = DIJKSTRA_COLOR
|
||||
|
||||
# Then A* path (green) - will overwrite shared cells
|
||||
for x, y in astar_path:
|
||||
if (x, y) != start_pos and (x, y) != end_pos:
|
||||
grid.at(x, y).color = ASTAR_COLOR
|
||||
|
||||
# Mark differences
|
||||
different_cells = []
|
||||
for cell in dijkstra_path:
|
||||
if cell not in astar_path:
|
||||
different_cells.append(cell)
|
||||
|
||||
status_text.text = f"Both paths: A*={len(astar_path)} steps, Dijkstra={len(dijkstra_path)} steps"
|
||||
if different_cells:
|
||||
info_text.text = f"Paths differ at {len(different_cells)} cells"
|
||||
else:
|
||||
info_text.text = "Paths are identical"
|
||||
|
||||
def handle_keypress(key_str, state):
|
||||
"""Handle keyboard input"""
|
||||
global mode
|
||||
if state == "end": return
|
||||
print(key_str)
|
||||
if key_str == "Esc" or key_str == "Q":
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
elif key_str == "A" or key_str == "1":
|
||||
mode = "ASTAR"
|
||||
show_astar()
|
||||
elif key_str == "D" or key_str == "2":
|
||||
mode = "DIJKSTRA"
|
||||
show_dijkstra()
|
||||
elif key_str == "B" or key_str == "3":
|
||||
mode = "BOTH"
|
||||
show_both()
|
||||
elif key_str == "Space":
|
||||
# Refresh current mode
|
||||
if mode == "ASTAR":
|
||||
show_astar()
|
||||
elif mode == "DIJKSTRA":
|
||||
show_dijkstra()
|
||||
else:
|
||||
show_both()
|
||||
|
||||
# Create the demo
|
||||
print("A* vs Dijkstra Pathfinding Comparison")
|
||||
print("=====================================")
|
||||
print("Controls:")
|
||||
print(" A or 1 - Show A* path (green)")
|
||||
print(" D or 2 - Show Dijkstra (blue gradient)")
|
||||
print(" B or 3 - Show both paths")
|
||||
print(" Q/ESC - Quit")
|
||||
print()
|
||||
print("A* is optimized for single-target pathfinding")
|
||||
print("Dijkstra explores in all directions (good for multiple targets)")
|
||||
|
||||
create_map()
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("pathfinding_comparison")
|
||||
ui.append(grid)
|
||||
|
||||
# Scale and position
|
||||
grid.size = (600, 400) # 30*20, 20*20
|
||||
grid.position = (100, 100)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("A* vs Dijkstra Pathfinding", 250, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add status
|
||||
status_text = mcrfpy.Caption("Press A for A*, D for Dijkstra, B for Both", 100, 60)
|
||||
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(status_text)
|
||||
|
||||
# Add info
|
||||
info_text = mcrfpy.Caption("", 100, 520)
|
||||
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(info_text)
|
||||
|
||||
# Add legend
|
||||
legend1 = mcrfpy.Caption("Red=Start, Yellow=End, Green=A*, Blue=Dijkstra", 100, 540)
|
||||
legend1.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend1)
|
||||
|
||||
legend2 = mcrfpy.Caption("Dark=Walls, Light=Floor", 100, 560)
|
||||
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend2)
|
||||
|
||||
# Set scene and input
|
||||
mcrfpy.setScene("pathfinding_comparison")
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
# Show initial A* path
|
||||
show_astar()
|
||||
|
||||
print("\nDemo ready!")
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Debug visibility crash"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
print("Debug visibility...")
|
||||
|
||||
# Create scene and grid
|
||||
mcrfpy.createScene("debug")
|
||||
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
|
||||
|
||||
# Initialize grid
|
||||
print("Initializing grid...")
|
||||
for y in range(5):
|
||||
for x in range(5):
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
# Create entity
|
||||
print("Creating entity...")
|
||||
entity = mcrfpy.Entity(2, 2)
|
||||
entity.sprite_index = 64
|
||||
grid.entities.append(entity)
|
||||
print(f"Entity at ({entity.x}, {entity.y})")
|
||||
|
||||
# Check gridstate
|
||||
print(f"\nGridstate length: {len(entity.gridstate)}")
|
||||
print(f"Expected: {5 * 5}")
|
||||
|
||||
# Try to access gridstate
|
||||
print("\nChecking gridstate access...")
|
||||
try:
|
||||
if len(entity.gridstate) > 0:
|
||||
state = entity.gridstate[0]
|
||||
print(f"First state: visible={state.visible}, discovered={state.discovered}")
|
||||
except Exception as e:
|
||||
print(f"Error accessing gridstate: {e}")
|
||||
|
||||
# Try update_visibility
|
||||
print("\nTrying update_visibility...")
|
||||
try:
|
||||
entity.update_visibility()
|
||||
print("update_visibility succeeded")
|
||||
except Exception as e:
|
||||
print(f"Error in update_visibility: {e}")
|
||||
|
||||
# Try perspective
|
||||
print("\nTesting perspective...")
|
||||
print(f"Initial perspective: {grid.perspective}")
|
||||
try:
|
||||
grid.perspective = 0
|
||||
print(f"Set perspective to 0: {grid.perspective}")
|
||||
except Exception as e:
|
||||
print(f"Error setting perspective: {e}")
|
||||
|
||||
print("\nTest complete")
|
||||
sys.exit(0)
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dijkstra Demo - Shows ALL Path Combinations (Including Invalid)
|
||||
===============================================================
|
||||
|
||||
Cycles through every possible entity pair to demonstrate both
|
||||
valid paths and properly handled invalid paths (empty lists).
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# High contrast colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown
|
||||
FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray
|
||||
PATH_COLOR = mcrfpy.Color(0, 255, 0) # Bright green
|
||||
START_COLOR = mcrfpy.Color(255, 100, 100) # Light red
|
||||
END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue
|
||||
NO_PATH_COLOR = mcrfpy.Color(255, 0, 0) # Pure red for unreachable
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
entities = []
|
||||
current_combo_index = 0
|
||||
all_combinations = [] # All possible pairs
|
||||
current_path = []
|
||||
|
||||
def create_map():
|
||||
"""Create the map with entities"""
|
||||
global grid, entities, all_combinations
|
||||
|
||||
mcrfpy.createScene("dijkstra_all")
|
||||
|
||||
# Create grid
|
||||
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Map layout - Entity 1 is intentionally trapped!
|
||||
map_layout = [
|
||||
"..............", # Row 0
|
||||
"..W.....WWWW..", # Row 1
|
||||
"..W.W...W.EW..", # Row 2 - Entity 1 TRAPPED at (10,2)
|
||||
"..W.....W..W..", # Row 3
|
||||
"..W...E.WWWW..", # Row 4 - Entity 2 at (6,4)
|
||||
"E.W...........", # Row 5 - Entity 3 at (0,5)
|
||||
"..W...........", # Row 6
|
||||
"..W...........", # Row 7
|
||||
"..W.WWW.......", # Row 8
|
||||
"..............", # Row 9
|
||||
]
|
||||
|
||||
# Create the map
|
||||
entity_positions = []
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
|
||||
if char == 'W':
|
||||
cell.walkable = False
|
||||
cell.color = WALL_COLOR
|
||||
else:
|
||||
cell.walkable = True
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
if char == 'E':
|
||||
entity_positions.append((x, y))
|
||||
|
||||
# Create entities
|
||||
entities = []
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
entity = mcrfpy.Entity(x, y)
|
||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||
grid.entities.append(entity)
|
||||
entities.append(entity)
|
||||
|
||||
print("Map Analysis:")
|
||||
print("=============")
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
print(f"Entity {i+1} at ({x}, {y})")
|
||||
|
||||
# Generate ALL combinations (including invalid ones)
|
||||
all_combinations = []
|
||||
for i in range(len(entities)):
|
||||
for j in range(len(entities)):
|
||||
if i != j: # Skip self-paths
|
||||
all_combinations.append((i, j))
|
||||
|
||||
print(f"\nTotal path combinations to test: {len(all_combinations)}")
|
||||
|
||||
def clear_path_colors():
|
||||
"""Reset all floor tiles to original color"""
|
||||
global current_path
|
||||
|
||||
for y in range(grid.grid_y):
|
||||
for x in range(grid.grid_x):
|
||||
cell = grid.at(x, y)
|
||||
if cell.walkable:
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
current_path = []
|
||||
|
||||
def show_combination(index):
|
||||
"""Show a specific path combination (valid or invalid)"""
|
||||
global current_combo_index, current_path
|
||||
|
||||
current_combo_index = index % len(all_combinations)
|
||||
from_idx, to_idx = all_combinations[current_combo_index]
|
||||
|
||||
# Clear previous path
|
||||
clear_path_colors()
|
||||
|
||||
# Get entities
|
||||
e_from = entities[from_idx]
|
||||
e_to = entities[to_idx]
|
||||
|
||||
# Calculate path
|
||||
path = e_from.path_to(int(e_to.x), int(e_to.y))
|
||||
current_path = path if path else []
|
||||
|
||||
# Always color start and end positions
|
||||
grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR
|
||||
grid.at(int(e_to.x), int(e_to.y)).color = NO_PATH_COLOR if not path else END_COLOR
|
||||
|
||||
# Color the path if it exists
|
||||
if path:
|
||||
# Color intermediate steps
|
||||
for i, (x, y) in enumerate(path):
|
||||
if i > 0 and i < len(path) - 1:
|
||||
grid.at(x, y).color = PATH_COLOR
|
||||
|
||||
status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = {len(path)} steps"
|
||||
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green for valid
|
||||
|
||||
# Show path steps
|
||||
path_display = []
|
||||
for i, (x, y) in enumerate(path[:5]):
|
||||
path_display.append(f"({x},{y})")
|
||||
if len(path) > 5:
|
||||
path_display.append("...")
|
||||
path_text.text = "Path: " + " → ".join(path_display)
|
||||
else:
|
||||
status_text.text = f"Path {current_combo_index + 1}/{len(all_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} = NO PATH!"
|
||||
status_text.fill_color = mcrfpy.Color(255, 100, 100) # Red for invalid
|
||||
path_text.text = "Path: [] (No valid path exists)"
|
||||
|
||||
# Update info
|
||||
info_text.text = f"From: Entity {from_idx+1} at ({int(e_from.x)}, {int(e_from.y)}) | To: Entity {to_idx+1} at ({int(e_to.x)}, {int(e_to.y)})"
|
||||
|
||||
def handle_keypress(key_str, state):
|
||||
"""Handle keyboard input"""
|
||||
global current_combo_index
|
||||
if state == "end": return
|
||||
|
||||
if key_str == "Esc" or key_str == "Q":
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
elif key_str == "Space" or key_str == "N":
|
||||
show_combination(current_combo_index + 1)
|
||||
elif key_str == "P":
|
||||
show_combination(current_combo_index - 1)
|
||||
elif key_str == "R":
|
||||
show_combination(current_combo_index)
|
||||
elif key_str in "123456":
|
||||
combo_num = int(key_str) - 1 # 0-based index
|
||||
if combo_num < len(all_combinations):
|
||||
show_combination(combo_num)
|
||||
|
||||
# Create the demo
|
||||
print("Dijkstra All Paths Demo")
|
||||
print("=======================")
|
||||
print("Shows ALL path combinations including invalid ones")
|
||||
print("Entity 1 is trapped - paths to/from it will be empty!")
|
||||
print()
|
||||
|
||||
create_map()
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("dijkstra_all")
|
||||
ui.append(grid)
|
||||
|
||||
# Scale and position
|
||||
grid.size = (560, 400)
|
||||
grid.position = (120, 100)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Dijkstra - All Paths (Valid & Invalid)", 200, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add status (will change color based on validity)
|
||||
status_text = mcrfpy.Caption("Ready", 120, 60)
|
||||
status_text.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
ui.append(status_text)
|
||||
|
||||
# Add info
|
||||
info_text = mcrfpy.Caption("", 120, 80)
|
||||
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(info_text)
|
||||
|
||||
# Add path display
|
||||
path_text = mcrfpy.Caption("Path: None", 120, 520)
|
||||
path_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(path_text)
|
||||
|
||||
# Add controls
|
||||
controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, 1-6=Jump to path, Q=Quit", 120, 540)
|
||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(controls)
|
||||
|
||||
# Add legend
|
||||
legend = mcrfpy.Caption("Red Start→Blue End (valid) | Red Start→Red End (invalid)", 120, 560)
|
||||
legend.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend)
|
||||
|
||||
# Expected results info
|
||||
expected = mcrfpy.Caption("Entity 1 is trapped: paths 1→2, 1→3, 2→1, 3→1 will fail", 120, 580)
|
||||
expected.fill_color = mcrfpy.Color(255, 150, 150)
|
||||
ui.append(expected)
|
||||
|
||||
# Set scene first, then set up input handler
|
||||
mcrfpy.setScene("dijkstra_all")
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
# Show first combination
|
||||
show_combination(0)
|
||||
|
||||
print("\nDemo ready!")
|
||||
print("Expected results:")
|
||||
print(" Path 1: Entity 1→2 = NO PATH (Entity 1 is trapped)")
|
||||
print(" Path 2: Entity 1→3 = NO PATH (Entity 1 is trapped)")
|
||||
print(" Path 3: Entity 2→1 = NO PATH (Entity 1 is trapped)")
|
||||
print(" Path 4: Entity 2→3 = Valid path")
|
||||
print(" Path 5: Entity 3→1 = NO PATH (Entity 1 is trapped)")
|
||||
print(" Path 6: Entity 3→2 = Valid path")
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dijkstra Demo - Cycles Through Different Path Combinations
|
||||
==========================================================
|
||||
|
||||
Shows paths between different entity pairs, skipping impossible paths.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# High contrast colors
|
||||
WALL_COLOR = mcrfpy.Color(40, 20, 20) # Very dark red/brown
|
||||
FLOOR_COLOR = mcrfpy.Color(60, 60, 80) # Dark blue-gray
|
||||
PATH_COLOR = mcrfpy.Color(0, 255, 0) # Bright green
|
||||
START_COLOR = mcrfpy.Color(255, 100, 100) # Light red
|
||||
END_COLOR = mcrfpy.Color(100, 100, 255) # Light blue
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
entities = []
|
||||
current_path_index = 0
|
||||
path_combinations = []
|
||||
current_path = []
|
||||
|
||||
def create_map():
|
||||
"""Create the map with entities"""
|
||||
global grid, entities
|
||||
|
||||
mcrfpy.createScene("dijkstra_cycle")
|
||||
|
||||
# Create grid
|
||||
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Map layout
|
||||
map_layout = [
|
||||
"..............", # Row 0
|
||||
"..W.....WWWW..", # Row 1
|
||||
"..W.W...W.EW..", # Row 2 - Entity 1 at (10,2) is TRAPPED!
|
||||
"..W.....W..W..", # Row 3
|
||||
"..W...E.WWWW..", # Row 4 - Entity 2 at (6,4)
|
||||
"E.W...........", # Row 5 - Entity 3 at (0,5)
|
||||
"..W...........", # Row 6
|
||||
"..W...........", # Row 7
|
||||
"..W.WWW.......", # Row 8
|
||||
"..............", # Row 9
|
||||
]
|
||||
|
||||
# Create the map
|
||||
entity_positions = []
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
|
||||
if char == 'W':
|
||||
cell.walkable = False
|
||||
cell.color = WALL_COLOR
|
||||
else:
|
||||
cell.walkable = True
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
if char == 'E':
|
||||
entity_positions.append((x, y))
|
||||
|
||||
# Create entities
|
||||
entities = []
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
entity = mcrfpy.Entity(x, y)
|
||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||
grid.entities.append(entity)
|
||||
entities.append(entity)
|
||||
|
||||
print("Entities created:")
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
print(f" Entity {i+1} at ({x}, {y})")
|
||||
|
||||
# Check which entity is trapped
|
||||
print("\nChecking accessibility:")
|
||||
for i, e in enumerate(entities):
|
||||
# Try to path to each other entity
|
||||
can_reach = []
|
||||
for j, other in enumerate(entities):
|
||||
if i != j:
|
||||
path = e.path_to(int(other.x), int(other.y))
|
||||
if path:
|
||||
can_reach.append(j+1)
|
||||
|
||||
if not can_reach:
|
||||
print(f" Entity {i+1} at ({int(e.x)}, {int(e.y)}) is TRAPPED!")
|
||||
else:
|
||||
print(f" Entity {i+1} can reach entities: {can_reach}")
|
||||
|
||||
# Generate valid path combinations (excluding trapped entity)
|
||||
global path_combinations
|
||||
path_combinations = []
|
||||
|
||||
# Only paths between entities 2 and 3 (indices 1 and 2) will work
|
||||
# since entity 1 (index 0) is trapped
|
||||
if len(entities) >= 3:
|
||||
# Entity 2 to Entity 3
|
||||
path = entities[1].path_to(int(entities[2].x), int(entities[2].y))
|
||||
if path:
|
||||
path_combinations.append((1, 2, path))
|
||||
|
||||
# Entity 3 to Entity 2
|
||||
path = entities[2].path_to(int(entities[1].x), int(entities[1].y))
|
||||
if path:
|
||||
path_combinations.append((2, 1, path))
|
||||
|
||||
print(f"\nFound {len(path_combinations)} valid paths")
|
||||
|
||||
def clear_path_colors():
|
||||
"""Reset all floor tiles to original color"""
|
||||
global current_path
|
||||
|
||||
for y in range(grid.grid_y):
|
||||
for x in range(grid.grid_x):
|
||||
cell = grid.at(x, y)
|
||||
if cell.walkable:
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
current_path = []
|
||||
|
||||
def show_path(index):
|
||||
"""Show a specific path combination"""
|
||||
global current_path_index, current_path
|
||||
|
||||
if not path_combinations:
|
||||
status_text.text = "No valid paths available (Entity 1 is trapped!)"
|
||||
return
|
||||
|
||||
current_path_index = index % len(path_combinations)
|
||||
from_idx, to_idx, path = path_combinations[current_path_index]
|
||||
|
||||
# Clear previous path
|
||||
clear_path_colors()
|
||||
|
||||
# Get entities
|
||||
e_from = entities[from_idx]
|
||||
e_to = entities[to_idx]
|
||||
|
||||
# Color the path
|
||||
current_path = path
|
||||
if path:
|
||||
# Color start and end
|
||||
grid.at(int(e_from.x), int(e_from.y)).color = START_COLOR
|
||||
grid.at(int(e_to.x), int(e_to.y)).color = END_COLOR
|
||||
|
||||
# Color intermediate steps
|
||||
for i, (x, y) in enumerate(path):
|
||||
if i > 0 and i < len(path) - 1:
|
||||
grid.at(x, y).color = PATH_COLOR
|
||||
|
||||
# Update status
|
||||
status_text.text = f"Path {current_path_index + 1}/{len(path_combinations)}: Entity {from_idx+1} → Entity {to_idx+1} ({len(path)} steps)"
|
||||
|
||||
# Update path display
|
||||
path_display = []
|
||||
for i, (x, y) in enumerate(path[:5]): # Show first 5 steps
|
||||
path_display.append(f"({x},{y})")
|
||||
if len(path) > 5:
|
||||
path_display.append("...")
|
||||
path_text.text = "Path: " + " → ".join(path_display) if path_display else "Path: None"
|
||||
|
||||
def handle_keypress(key_str, state):
|
||||
"""Handle keyboard input"""
|
||||
global current_path_index
|
||||
if state == "end": return
|
||||
if key_str == "Esc":
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
elif key_str == "N" or key_str == "Space":
|
||||
show_path(current_path_index + 1)
|
||||
elif key_str == "P":
|
||||
show_path(current_path_index - 1)
|
||||
elif key_str == "R":
|
||||
show_path(current_path_index)
|
||||
|
||||
# Create the demo
|
||||
print("Dijkstra Path Cycling Demo")
|
||||
print("==========================")
|
||||
print("Note: Entity 1 is trapped by walls!")
|
||||
print()
|
||||
|
||||
create_map()
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("dijkstra_cycle")
|
||||
ui.append(grid)
|
||||
|
||||
# Scale and position
|
||||
grid.size = (560, 400)
|
||||
grid.position = (120, 100)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Dijkstra Pathfinding - Cycle Paths", 200, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add status
|
||||
status_text = mcrfpy.Caption("Press SPACE to cycle paths", 120, 60)
|
||||
status_text.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
ui.append(status_text)
|
||||
|
||||
# Add path display
|
||||
path_text = mcrfpy.Caption("Path: None", 120, 520)
|
||||
path_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(path_text)
|
||||
|
||||
# Add controls
|
||||
controls = mcrfpy.Caption("SPACE/N=Next, P=Previous, R=Refresh, Q=Quit", 120, 540)
|
||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(controls)
|
||||
|
||||
# Add legend
|
||||
legend = mcrfpy.Caption("Red=Start, Blue=End, Green=Path, Dark=Wall", 120, 560)
|
||||
legend.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend)
|
||||
|
||||
# Show first valid path
|
||||
mcrfpy.setScene("dijkstra_cycle")
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
# Display initial path
|
||||
if path_combinations:
|
||||
show_path(0)
|
||||
else:
|
||||
status_text.text = "No valid paths! Entity 1 is trapped!"
|
||||
|
||||
print("\nDemo ready!")
|
||||
print("Controls:")
|
||||
print(" SPACE or N - Next path")
|
||||
print(" P - Previous path")
|
||||
print(" R - Refresh current path")
|
||||
print(" Q - Quit")
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Debug version of Dijkstra pathfinding to diagnose visualization issues
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Colors
|
||||
WALL_COLOR = mcrfpy.Color(60, 30, 30)
|
||||
FLOOR_COLOR = mcrfpy.Color(200, 200, 220)
|
||||
PATH_COLOR = mcrfpy.Color(200, 250, 220)
|
||||
ENTITY_COLORS = [
|
||||
mcrfpy.Color(255, 100, 100), # Entity 1 - Red
|
||||
mcrfpy.Color(100, 255, 100), # Entity 2 - Green
|
||||
mcrfpy.Color(100, 100, 255), # Entity 3 - Blue
|
||||
]
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
entities = []
|
||||
first_point = None
|
||||
second_point = None
|
||||
|
||||
def create_simple_map():
|
||||
"""Create a simple test map"""
|
||||
global grid, entities
|
||||
|
||||
mcrfpy.createScene("dijkstra_debug")
|
||||
|
||||
# Small grid for easy debugging
|
||||
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
print("Initializing 10x10 grid...")
|
||||
|
||||
# Initialize all as floor
|
||||
for y in range(10):
|
||||
for x in range(10):
|
||||
grid.at(x, y).walkable = True
|
||||
grid.at(x, y).transparent = True
|
||||
grid.at(x, y).color = FLOOR_COLOR
|
||||
|
||||
# Add a simple wall
|
||||
print("Adding walls at:")
|
||||
walls = [(5, 2), (5, 3), (5, 4), (5, 5), (5, 6)]
|
||||
for x, y in walls:
|
||||
print(f" Wall at ({x}, {y})")
|
||||
grid.at(x, y).walkable = False
|
||||
grid.at(x, y).color = WALL_COLOR
|
||||
|
||||
# Create 3 entities
|
||||
entity_positions = [(2, 5), (8, 5), (5, 8)]
|
||||
entities = []
|
||||
|
||||
print("\nCreating entities at:")
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
print(f" Entity {i+1} at ({x}, {y})")
|
||||
entity = mcrfpy.Entity(x, y)
|
||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||
grid.entities.append(entity)
|
||||
entities.append(entity)
|
||||
|
||||
return grid
|
||||
|
||||
def test_path_highlighting():
|
||||
"""Test path highlighting with debug output"""
|
||||
print("\n" + "="*50)
|
||||
print("Testing path highlighting...")
|
||||
|
||||
# Select first two entities
|
||||
e1 = entities[0]
|
||||
e2 = entities[1]
|
||||
|
||||
print(f"\nEntity 1 position: ({e1.x}, {e1.y})")
|
||||
print(f"Entity 2 position: ({e2.x}, {e2.y})")
|
||||
|
||||
# Use entity.path_to()
|
||||
print("\nCalling entity.path_to()...")
|
||||
path = e1.path_to(int(e2.x), int(e2.y))
|
||||
|
||||
print(f"Path returned: {path}")
|
||||
print(f"Path length: {len(path)} steps")
|
||||
|
||||
if path:
|
||||
print("\nHighlighting path cells:")
|
||||
for i, (x, y) in enumerate(path):
|
||||
print(f" Step {i}: ({x}, {y})")
|
||||
# Get current color for debugging
|
||||
cell = grid.at(x, y)
|
||||
old_color = (cell.color.r, cell.color.g, cell.color.b)
|
||||
|
||||
# Set new color
|
||||
cell.color = PATH_COLOR
|
||||
new_color = (cell.color.r, cell.color.g, cell.color.b)
|
||||
|
||||
print(f" Color changed from {old_color} to {new_color}")
|
||||
print(f" Walkable: {cell.walkable}")
|
||||
|
||||
# Also test grid's Dijkstra methods
|
||||
print("\n" + "-"*30)
|
||||
print("Testing grid Dijkstra methods...")
|
||||
|
||||
grid.compute_dijkstra(int(e1.x), int(e1.y))
|
||||
grid_path = grid.get_dijkstra_path(int(e2.x), int(e2.y))
|
||||
distance = grid.get_dijkstra_distance(int(e2.x), int(e2.y))
|
||||
|
||||
print(f"Grid path: {grid_path}")
|
||||
print(f"Grid distance: {distance}")
|
||||
|
||||
# Verify colors were set
|
||||
print("\nVerifying cell colors after highlighting:")
|
||||
for x, y in path[:3]: # Check first 3 cells
|
||||
cell = grid.at(x, y)
|
||||
color = (cell.color.r, cell.color.g, cell.color.b)
|
||||
expected = (PATH_COLOR.r, PATH_COLOR.g, PATH_COLOR.b)
|
||||
match = color == expected
|
||||
print(f" Cell ({x}, {y}): color={color}, expected={expected}, match={match}")
|
||||
|
||||
def handle_keypress(scene_name, keycode):
|
||||
"""Simple keypress handler"""
|
||||
if keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
|
||||
print("\nExiting debug...")
|
||||
sys.exit(0)
|
||||
elif keycode == 32: # Space
|
||||
print("\nSpace pressed - retesting path highlighting...")
|
||||
test_path_highlighting()
|
||||
|
||||
# Create the map
|
||||
print("Dijkstra Debug Test")
|
||||
print("===================")
|
||||
grid = create_simple_map()
|
||||
|
||||
# Initial path test
|
||||
test_path_highlighting()
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("dijkstra_debug")
|
||||
ui.append(grid)
|
||||
|
||||
# Position and scale
|
||||
grid.position = (50, 50)
|
||||
grid.size = (400, 400) # 10*40
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Dijkstra Debug - Press SPACE to retest, Q to quit", 50, 10)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add debug info
|
||||
info = mcrfpy.Caption("Check console for debug output", 50, 470)
|
||||
info.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(info)
|
||||
|
||||
# Set up scene
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
mcrfpy.setScene("dijkstra_debug")
|
||||
|
||||
print("\nScene ready. The path should be highlighted in cyan.")
|
||||
print("If you don't see the path, there may be a rendering issue.")
|
||||
print("Press SPACE to retest, Q to quit.")
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dijkstra Pathfinding Interactive Demo
|
||||
=====================================
|
||||
|
||||
Interactive visualization showing Dijkstra pathfinding between entities.
|
||||
|
||||
Controls:
|
||||
- Press 1/2/3 to select the first entity
|
||||
- Press A/B/C to select the second entity
|
||||
- Space to clear selection
|
||||
- Q or ESC to quit
|
||||
|
||||
The path between selected entities is automatically highlighted.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Colors - using more distinct values
|
||||
WALL_COLOR = mcrfpy.Color(60, 30, 30)
|
||||
FLOOR_COLOR = mcrfpy.Color(100, 100, 120) # Darker floor for better contrast
|
||||
PATH_COLOR = mcrfpy.Color(50, 255, 50) # Bright green for path
|
||||
ENTITY_COLORS = [
|
||||
mcrfpy.Color(255, 100, 100), # Entity 1 - Red
|
||||
mcrfpy.Color(100, 255, 100), # Entity 2 - Green
|
||||
mcrfpy.Color(100, 100, 255), # Entity 3 - Blue
|
||||
]
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
entities = []
|
||||
first_point = None
|
||||
second_point = None
|
||||
|
||||
def create_map():
|
||||
"""Create the interactive map with the layout specified by the user"""
|
||||
global grid, entities
|
||||
|
||||
mcrfpy.createScene("dijkstra_interactive")
|
||||
|
||||
# Create grid - 14x10 as specified
|
||||
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Define the map layout from user's specification
|
||||
# . = floor, W = wall, E = entity position
|
||||
map_layout = [
|
||||
"..............", # Row 0
|
||||
"..W.....WWWW..", # Row 1
|
||||
"..W.W...W.EW..", # Row 2
|
||||
"..W.....W..W..", # Row 3
|
||||
"..W...E.WWWW..", # Row 4
|
||||
"E.W...........", # Row 5
|
||||
"..W...........", # Row 6
|
||||
"..W...........", # Row 7
|
||||
"..W.WWW.......", # Row 8
|
||||
"..............", # Row 9
|
||||
]
|
||||
|
||||
# Create the map
|
||||
entity_positions = []
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
|
||||
if char == 'W':
|
||||
# Wall
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.color = WALL_COLOR
|
||||
else:
|
||||
# Floor
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
if char == 'E':
|
||||
# Entity position
|
||||
entity_positions.append((x, y))
|
||||
|
||||
# Create entities at marked positions
|
||||
entities = []
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
entity = mcrfpy.Entity(x, y)
|
||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||
grid.entities.append(entity)
|
||||
entities.append(entity)
|
||||
|
||||
return grid
|
||||
|
||||
def clear_path_highlight():
|
||||
"""Clear any existing path highlighting"""
|
||||
# Reset all floor tiles to original color
|
||||
for y in range(grid.grid_y):
|
||||
for x in range(grid.grid_x):
|
||||
cell = grid.at(x, y)
|
||||
if cell.walkable:
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
def highlight_path():
|
||||
"""Highlight the path between selected entities"""
|
||||
if first_point is None or second_point is None:
|
||||
return
|
||||
|
||||
# Clear previous highlighting
|
||||
clear_path_highlight()
|
||||
|
||||
# Get entities
|
||||
entity1 = entities[first_point]
|
||||
entity2 = entities[second_point]
|
||||
|
||||
# Compute Dijkstra from first entity
|
||||
grid.compute_dijkstra(int(entity1.x), int(entity1.y))
|
||||
|
||||
# Get path to second entity
|
||||
path = grid.get_dijkstra_path(int(entity2.x), int(entity2.y))
|
||||
|
||||
if path:
|
||||
# Highlight the path
|
||||
for x, y in path:
|
||||
cell = grid.at(x, y)
|
||||
if cell.walkable:
|
||||
cell.color = PATH_COLOR
|
||||
|
||||
# Also highlight start and end with entity colors
|
||||
grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point]
|
||||
grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point]
|
||||
|
||||
# Update info
|
||||
distance = grid.get_dijkstra_distance(int(entity2.x), int(entity2.y))
|
||||
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps, {distance:.1f} units"
|
||||
else:
|
||||
info_text.text = f"No path between Entity {first_point+1} and Entity {second_point+1}"
|
||||
|
||||
def handle_keypress(scene_name, keycode):
|
||||
"""Handle keyboard input"""
|
||||
global first_point, second_point
|
||||
|
||||
# Number keys for first entity
|
||||
if keycode == 49: # '1'
|
||||
first_point = 0
|
||||
status_text.text = f"First: Entity 1 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
|
||||
highlight_path()
|
||||
elif keycode == 50: # '2'
|
||||
first_point = 1
|
||||
status_text.text = f"First: Entity 2 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
|
||||
highlight_path()
|
||||
elif keycode == 51: # '3'
|
||||
first_point = 2
|
||||
status_text.text = f"First: Entity 3 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
|
||||
highlight_path()
|
||||
|
||||
# Letter keys for second entity
|
||||
elif keycode == 65 or keycode == 97: # 'A' or 'a'
|
||||
second_point = 0
|
||||
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 1"
|
||||
highlight_path()
|
||||
elif keycode == 66 or keycode == 98: # 'B' or 'b'
|
||||
second_point = 1
|
||||
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 2"
|
||||
highlight_path()
|
||||
elif keycode == 67 or keycode == 99: # 'C' or 'c'
|
||||
second_point = 2
|
||||
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 3"
|
||||
highlight_path()
|
||||
|
||||
# Clear selection
|
||||
elif keycode == 32: # Space
|
||||
first_point = None
|
||||
second_point = None
|
||||
clear_path_highlight()
|
||||
status_text.text = "Press 1/2/3 for first entity, A/B/C for second"
|
||||
info_text.text = "Space to clear, Q to quit"
|
||||
|
||||
# Quit
|
||||
elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
|
||||
print("\nExiting Dijkstra interactive demo...")
|
||||
sys.exit(0)
|
||||
|
||||
# Create the visualization
|
||||
print("Dijkstra Pathfinding Interactive Demo")
|
||||
print("=====================================")
|
||||
print("Controls:")
|
||||
print(" 1/2/3 - Select first entity")
|
||||
print(" A/B/C - Select second entity")
|
||||
print(" Space - Clear selection")
|
||||
print(" Q/ESC - Quit")
|
||||
|
||||
# Create map
|
||||
grid = create_map()
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("dijkstra_interactive")
|
||||
ui.append(grid)
|
||||
|
||||
# Scale and position grid for better visibility
|
||||
grid.size = (560, 400) # 14*40, 10*40
|
||||
grid.position = (120, 60)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Dijkstra Pathfinding Interactive", 250, 10)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add status text
|
||||
status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480)
|
||||
status_text.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(status_text)
|
||||
|
||||
# Add info text
|
||||
info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500)
|
||||
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(info_text)
|
||||
|
||||
# Add legend
|
||||
legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 540)
|
||||
legend1.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend1)
|
||||
|
||||
legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 560)
|
||||
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend2)
|
||||
|
||||
# Mark entity positions with colored indicators
|
||||
for i, entity in enumerate(entities):
|
||||
marker = mcrfpy.Caption(str(i+1),
|
||||
120 + int(entity.x) * 40 + 15,
|
||||
60 + int(entity.y) * 40 + 10)
|
||||
marker.fill_color = ENTITY_COLORS[i]
|
||||
marker.outline = 1
|
||||
marker.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
ui.append(marker)
|
||||
|
||||
# Set up input handling
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
# Show the scene
|
||||
mcrfpy.setScene("dijkstra_interactive")
|
||||
|
||||
print("\nVisualization ready!")
|
||||
print("Entities are at:")
|
||||
for i, entity in enumerate(entities):
|
||||
print(f" Entity {i+1}: ({int(entity.x)}, {int(entity.y)})")
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Enhanced Dijkstra Pathfinding Interactive Demo
|
||||
==============================================
|
||||
|
||||
Interactive visualization with entity pathfinding animations.
|
||||
|
||||
Controls:
|
||||
- Press 1/2/3 to select the first entity
|
||||
- Press A/B/C to select the second entity
|
||||
- Space to clear selection
|
||||
- M to make selected entity move along path
|
||||
- P to pause/resume animation
|
||||
- R to reset entity positions
|
||||
- Q or ESC to quit
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
import math
|
||||
|
||||
# Colors
|
||||
WALL_COLOR = mcrfpy.Color(60, 30, 30)
|
||||
FLOOR_COLOR = mcrfpy.Color(200, 200, 220)
|
||||
PATH_COLOR = mcrfpy.Color(200, 250, 220)
|
||||
VISITED_COLOR = mcrfpy.Color(180, 230, 200)
|
||||
ENTITY_COLORS = [
|
||||
mcrfpy.Color(255, 100, 100), # Entity 1 - Red
|
||||
mcrfpy.Color(100, 255, 100), # Entity 2 - Green
|
||||
mcrfpy.Color(100, 100, 255), # Entity 3 - Blue
|
||||
]
|
||||
|
||||
# Global state
|
||||
grid = None
|
||||
entities = []
|
||||
first_point = None
|
||||
second_point = None
|
||||
current_path = []
|
||||
animating = False
|
||||
animation_progress = 0.0
|
||||
animation_speed = 2.0 # cells per second
|
||||
original_positions = [] # Store original entity positions
|
||||
|
||||
def create_map():
|
||||
"""Create the interactive map with the layout specified by the user"""
|
||||
global grid, entities, original_positions
|
||||
|
||||
mcrfpy.createScene("dijkstra_enhanced")
|
||||
|
||||
# Create grid - 14x10 as specified
|
||||
grid = mcrfpy.Grid(grid_x=14, grid_y=10)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Define the map layout from user's specification
|
||||
# . = floor, W = wall, E = entity position
|
||||
map_layout = [
|
||||
"..............", # Row 0
|
||||
"..W.....WWWW..", # Row 1
|
||||
"..W.W...W.EW..", # Row 2
|
||||
"..W.....W..W..", # Row 3
|
||||
"..W...E.WWWW..", # Row 4
|
||||
"E.W...........", # Row 5
|
||||
"..W...........", # Row 6
|
||||
"..W...........", # Row 7
|
||||
"..W.WWW.......", # Row 8
|
||||
"..............", # Row 9
|
||||
]
|
||||
|
||||
# Create the map
|
||||
entity_positions = []
|
||||
for y, row in enumerate(map_layout):
|
||||
for x, char in enumerate(row):
|
||||
cell = grid.at(x, y)
|
||||
|
||||
if char == 'W':
|
||||
# Wall
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.color = WALL_COLOR
|
||||
else:
|
||||
# Floor
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
if char == 'E':
|
||||
# Entity position
|
||||
entity_positions.append((x, y))
|
||||
|
||||
# Create entities at marked positions
|
||||
entities = []
|
||||
original_positions = []
|
||||
for i, (x, y) in enumerate(entity_positions):
|
||||
entity = mcrfpy.Entity(x, y)
|
||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||
grid.entities.append(entity)
|
||||
entities.append(entity)
|
||||
original_positions.append((x, y))
|
||||
|
||||
return grid
|
||||
|
||||
def clear_path_highlight():
|
||||
"""Clear any existing path highlighting"""
|
||||
global current_path
|
||||
|
||||
# Reset all floor tiles to original color
|
||||
for y in range(grid.grid_y):
|
||||
for x in range(grid.grid_x):
|
||||
cell = grid.at(x, y)
|
||||
if cell.walkable:
|
||||
cell.color = FLOOR_COLOR
|
||||
|
||||
current_path = []
|
||||
|
||||
def highlight_path():
|
||||
"""Highlight the path between selected entities using entity.path_to()"""
|
||||
global current_path
|
||||
|
||||
if first_point is None or second_point is None:
|
||||
return
|
||||
|
||||
# Clear previous highlighting
|
||||
clear_path_highlight()
|
||||
|
||||
# Get entities
|
||||
entity1 = entities[first_point]
|
||||
entity2 = entities[second_point]
|
||||
|
||||
# Use the new path_to method!
|
||||
path = entity1.path_to(int(entity2.x), int(entity2.y))
|
||||
|
||||
if path:
|
||||
current_path = path
|
||||
|
||||
# Highlight the path
|
||||
for i, (x, y) in enumerate(path):
|
||||
cell = grid.at(x, y)
|
||||
if cell.walkable:
|
||||
# Use gradient for path visualization
|
||||
if i < len(path) - 1:
|
||||
cell.color = PATH_COLOR
|
||||
else:
|
||||
cell.color = VISITED_COLOR
|
||||
|
||||
# Highlight start and end with entity colors
|
||||
grid.at(int(entity1.x), int(entity1.y)).color = ENTITY_COLORS[first_point]
|
||||
grid.at(int(entity2.x), int(entity2.y)).color = ENTITY_COLORS[second_point]
|
||||
|
||||
# Update info
|
||||
info_text.text = f"Path: Entity {first_point+1} to Entity {second_point+1} - {len(path)} steps"
|
||||
else:
|
||||
info_text.text = f"No path between Entity {first_point+1} and Entity {second_point+1}"
|
||||
current_path = []
|
||||
|
||||
def animate_movement(dt):
|
||||
"""Animate entity movement along path"""
|
||||
global animation_progress, animating, current_path
|
||||
|
||||
if not animating or not current_path or first_point is None:
|
||||
return
|
||||
|
||||
entity = entities[first_point]
|
||||
|
||||
# Update animation progress
|
||||
animation_progress += animation_speed * dt
|
||||
|
||||
# Calculate current position along path
|
||||
path_index = int(animation_progress)
|
||||
|
||||
if path_index >= len(current_path):
|
||||
# Animation complete
|
||||
animating = False
|
||||
animation_progress = 0.0
|
||||
# Snap to final position
|
||||
if current_path:
|
||||
final_x, final_y = current_path[-1]
|
||||
entity.x = float(final_x)
|
||||
entity.y = float(final_y)
|
||||
return
|
||||
|
||||
# Interpolate between path points
|
||||
if path_index < len(current_path) - 1:
|
||||
curr_x, curr_y = current_path[path_index]
|
||||
next_x, next_y = current_path[path_index + 1]
|
||||
|
||||
# Calculate interpolation factor
|
||||
t = animation_progress - path_index
|
||||
|
||||
# Smooth interpolation
|
||||
entity.x = curr_x + (next_x - curr_x) * t
|
||||
entity.y = curr_y + (next_y - curr_y) * t
|
||||
else:
|
||||
# At last point
|
||||
entity.x, entity.y = current_path[path_index]
|
||||
|
||||
def handle_keypress(scene_name, keycode):
|
||||
"""Handle keyboard input"""
|
||||
global first_point, second_point, animating, animation_progress
|
||||
|
||||
# Number keys for first entity
|
||||
if keycode == 49: # '1'
|
||||
first_point = 0
|
||||
status_text.text = f"First: Entity 1 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
|
||||
highlight_path()
|
||||
elif keycode == 50: # '2'
|
||||
first_point = 1
|
||||
status_text.text = f"First: Entity 2 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
|
||||
highlight_path()
|
||||
elif keycode == 51: # '3'
|
||||
first_point = 2
|
||||
status_text.text = f"First: Entity 3 | Second: {f'Entity {second_point+1}' if second_point is not None else '?'}"
|
||||
highlight_path()
|
||||
|
||||
# Letter keys for second entity
|
||||
elif keycode == 65 or keycode == 97: # 'A' or 'a'
|
||||
second_point = 0
|
||||
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 1"
|
||||
highlight_path()
|
||||
elif keycode == 66 or keycode == 98: # 'B' or 'b'
|
||||
second_point = 1
|
||||
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 2"
|
||||
highlight_path()
|
||||
elif keycode == 67 or keycode == 99: # 'C' or 'c'
|
||||
second_point = 2
|
||||
status_text.text = f"First: {f'Entity {first_point+1}' if first_point is not None else '?'} | Second: Entity 3"
|
||||
highlight_path()
|
||||
|
||||
# Movement control
|
||||
elif keycode == 77 or keycode == 109: # 'M' or 'm'
|
||||
if current_path and first_point is not None:
|
||||
animating = True
|
||||
animation_progress = 0.0
|
||||
control_text.text = "Animation: MOVING (press P to pause)"
|
||||
|
||||
# Pause/Resume
|
||||
elif keycode == 80 or keycode == 112: # 'P' or 'p'
|
||||
animating = not animating
|
||||
control_text.text = f"Animation: {'MOVING' if animating else 'PAUSED'} (press P to {'pause' if animating else 'resume'})"
|
||||
|
||||
# Reset positions
|
||||
elif keycode == 82 or keycode == 114: # 'R' or 'r'
|
||||
animating = False
|
||||
animation_progress = 0.0
|
||||
for i, entity in enumerate(entities):
|
||||
entity.x, entity.y = original_positions[i]
|
||||
control_text.text = "Entities reset to original positions"
|
||||
highlight_path() # Re-highlight path after reset
|
||||
|
||||
# Clear selection
|
||||
elif keycode == 32: # Space
|
||||
first_point = None
|
||||
second_point = None
|
||||
animating = False
|
||||
animation_progress = 0.0
|
||||
clear_path_highlight()
|
||||
status_text.text = "Press 1/2/3 for first entity, A/B/C for second"
|
||||
info_text.text = "Space to clear, Q to quit"
|
||||
control_text.text = "Press M to move, P to pause, R to reset"
|
||||
|
||||
# Quit
|
||||
elif keycode == 81 or keycode == 113 or keycode == 256: # Q/q/ESC
|
||||
print("\nExiting enhanced Dijkstra demo...")
|
||||
sys.exit(0)
|
||||
|
||||
# Timer callback for animation
|
||||
def update_animation(dt):
|
||||
"""Update animation state"""
|
||||
animate_movement(dt / 1000.0) # Convert ms to seconds
|
||||
|
||||
# Create the visualization
|
||||
print("Enhanced Dijkstra Pathfinding Demo")
|
||||
print("==================================")
|
||||
print("Controls:")
|
||||
print(" 1/2/3 - Select first entity")
|
||||
print(" A/B/C - Select second entity")
|
||||
print(" M - Move first entity along path")
|
||||
print(" P - Pause/Resume animation")
|
||||
print(" R - Reset entity positions")
|
||||
print(" Space - Clear selection")
|
||||
print(" Q/ESC - Quit")
|
||||
|
||||
# Create map
|
||||
grid = create_map()
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("dijkstra_enhanced")
|
||||
ui.append(grid)
|
||||
|
||||
# Scale and position grid for better visibility
|
||||
grid.size = (560, 400) # 14*40, 10*40
|
||||
grid.position = (120, 60)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Enhanced Dijkstra Pathfinding", 250, 10)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add status text
|
||||
status_text = mcrfpy.Caption("Press 1/2/3 for first entity, A/B/C for second", 120, 480)
|
||||
status_text.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(status_text)
|
||||
|
||||
# Add info text
|
||||
info_text = mcrfpy.Caption("Space to clear, Q to quit", 120, 500)
|
||||
info_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(info_text)
|
||||
|
||||
# Add control text
|
||||
control_text = mcrfpy.Caption("Press M to move, P to pause, R to reset", 120, 520)
|
||||
control_text.fill_color = mcrfpy.Color(150, 200, 150)
|
||||
ui.append(control_text)
|
||||
|
||||
# Add legend
|
||||
legend1 = mcrfpy.Caption("Entities: 1=Red 2=Green 3=Blue", 120, 560)
|
||||
legend1.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend1)
|
||||
|
||||
legend2 = mcrfpy.Caption("Colors: Dark=Wall Light=Floor Cyan=Path", 120, 580)
|
||||
legend2.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(legend2)
|
||||
|
||||
# Mark entity positions with colored indicators
|
||||
for i, entity in enumerate(entities):
|
||||
marker = mcrfpy.Caption(str(i+1),
|
||||
120 + int(entity.x) * 40 + 15,
|
||||
60 + int(entity.y) * 40 + 10)
|
||||
marker.fill_color = ENTITY_COLORS[i]
|
||||
marker.outline = 1
|
||||
marker.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
ui.append(marker)
|
||||
|
||||
# Set up input handling
|
||||
mcrfpy.keypressScene(handle_keypress)
|
||||
|
||||
# Set up animation timer (60 FPS)
|
||||
mcrfpy.setTimer("animation", update_animation, 16)
|
||||
|
||||
# Show the scene
|
||||
mcrfpy.setScene("dijkstra_enhanced")
|
||||
|
||||
print("\nVisualization ready!")
|
||||
print("Entities are at:")
|
||||
for i, entity in enumerate(entities):
|
||||
print(f" Entity {i+1}: ({int(entity.x)}, {int(entity.y)})")
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dijkstra Pathfinding Test - Headless
|
||||
====================================
|
||||
|
||||
Tests all Dijkstra functionality and generates a screenshot.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
def create_test_map():
|
||||
"""Create a test map with obstacles"""
|
||||
mcrfpy.createScene("dijkstra_test")
|
||||
|
||||
# Create grid
|
||||
grid = mcrfpy.Grid(grid_x=20, grid_y=12)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
# Initialize all cells as walkable floor
|
||||
for y in range(12):
|
||||
for x in range(20):
|
||||
grid.at(x, y).walkable = True
|
||||
grid.at(x, y).transparent = True
|
||||
grid.at(x, y).color = mcrfpy.Color(200, 200, 220)
|
||||
|
||||
# Add walls to create interesting paths
|
||||
walls = [
|
||||
# Vertical wall in the middle
|
||||
(10, 1), (10, 2), (10, 3), (10, 4), (10, 5), (10, 6), (10, 7), (10, 8),
|
||||
# Horizontal walls
|
||||
(2, 6), (3, 6), (4, 6), (5, 6), (6, 6),
|
||||
(14, 6), (15, 6), (16, 6), (17, 6),
|
||||
# Some scattered obstacles
|
||||
(5, 2), (15, 2), (5, 9), (15, 9)
|
||||
]
|
||||
|
||||
for x, y in walls:
|
||||
grid.at(x, y).walkable = False
|
||||
grid.at(x, y).color = mcrfpy.Color(60, 30, 30)
|
||||
|
||||
# Place test entities
|
||||
entities = []
|
||||
positions = [(2, 2), (17, 2), (9, 10)]
|
||||
colors = [
|
||||
mcrfpy.Color(255, 100, 100), # Red
|
||||
mcrfpy.Color(100, 255, 100), # Green
|
||||
mcrfpy.Color(100, 100, 255) # Blue
|
||||
]
|
||||
|
||||
for i, (x, y) in enumerate(positions):
|
||||
entity = mcrfpy.Entity(x, y)
|
||||
entity.sprite_index = 49 + i # '1', '2', '3'
|
||||
grid.entities.append(entity)
|
||||
entities.append(entity)
|
||||
# Mark entity positions
|
||||
grid.at(x, y).color = colors[i]
|
||||
|
||||
return grid, entities
|
||||
|
||||
def test_dijkstra(grid, entities):
|
||||
"""Test Dijkstra pathfinding between all entity pairs"""
|
||||
results = []
|
||||
|
||||
for i in range(len(entities)):
|
||||
for j in range(len(entities)):
|
||||
if i != j:
|
||||
# Compute Dijkstra from entity i
|
||||
e1 = entities[i]
|
||||
e2 = entities[j]
|
||||
grid.compute_dijkstra(int(e1.x), int(e1.y))
|
||||
|
||||
# Get distance and path to entity j
|
||||
distance = grid.get_dijkstra_distance(int(e2.x), int(e2.y))
|
||||
path = grid.get_dijkstra_path(int(e2.x), int(e2.y))
|
||||
|
||||
if path:
|
||||
results.append(f"Path {i+1}→{j+1}: {len(path)} steps, {distance:.1f} units")
|
||||
|
||||
# Color one interesting path
|
||||
if i == 0 and j == 2: # Path from 1 to 3
|
||||
for x, y in path[1:-1]: # Skip endpoints
|
||||
if grid.at(x, y).walkable:
|
||||
grid.at(x, y).color = mcrfpy.Color(200, 250, 220)
|
||||
else:
|
||||
results.append(f"Path {i+1}→{j+1}: No path found!")
|
||||
|
||||
return results
|
||||
|
||||
def run_test(runtime):
|
||||
"""Timer callback to run tests and take screenshot"""
|
||||
# Run pathfinding tests
|
||||
results = test_dijkstra(grid, entities)
|
||||
|
||||
# Update display with results
|
||||
y_pos = 380
|
||||
for result in results:
|
||||
caption = mcrfpy.Caption(result, 50, y_pos)
|
||||
caption.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(caption)
|
||||
y_pos += 20
|
||||
|
||||
# Take screenshot
|
||||
mcrfpy.setTimer("screenshot", lambda rt: take_screenshot(), 500)
|
||||
|
||||
def take_screenshot():
|
||||
"""Take screenshot and exit"""
|
||||
try:
|
||||
automation.screenshot("dijkstra_test.png")
|
||||
print("Screenshot saved: dijkstra_test.png")
|
||||
except Exception as e:
|
||||
print(f"Screenshot failed: {e}")
|
||||
|
||||
# Exit
|
||||
sys.exit(0)
|
||||
|
||||
# Create test map
|
||||
print("Creating Dijkstra pathfinding test...")
|
||||
grid, entities = create_test_map()
|
||||
|
||||
# Set up UI
|
||||
ui = mcrfpy.sceneUI("dijkstra_test")
|
||||
ui.append(grid)
|
||||
|
||||
# Position and scale grid
|
||||
grid.position = (50, 50)
|
||||
grid.size = (500, 300)
|
||||
|
||||
# Add title
|
||||
title = mcrfpy.Caption("Dijkstra Pathfinding Test", 200, 10)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Add legend
|
||||
legend = mcrfpy.Caption("Red=Entity1 Green=Entity2 Blue=Entity3 Cyan=Path 1→3", 50, 360)
|
||||
legend.fill_color = mcrfpy.Color(180, 180, 180)
|
||||
ui.append(legend)
|
||||
|
||||
# Set scene
|
||||
mcrfpy.setScene("dijkstra_test")
|
||||
|
||||
# Run test after scene loads
|
||||
mcrfpy.setTimer("test", run_test, 100)
|
||||
|
||||
print("Running Dijkstra tests...")
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Interactive Visibility Demo
|
||||
==========================
|
||||
|
||||
Controls:
|
||||
- WASD: Move the player (green @)
|
||||
- Arrow keys: Move enemy (red E)
|
||||
- Tab: Cycle perspective (Omniscient → Player → Enemy → Omniscient)
|
||||
- Space: Update visibility for current entity
|
||||
- R: Reset positions
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Create scene and grid
|
||||
mcrfpy.createScene("visibility_demo")
|
||||
grid = mcrfpy.Grid(grid_x=30, grid_y=20)
|
||||
grid.fill_color = mcrfpy.Color(20, 20, 30) # Dark background
|
||||
|
||||
# Initialize grid - all walkable and transparent
|
||||
for y in range(20):
|
||||
for x in range(30):
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.color = mcrfpy.Color(100, 100, 120) # Floor color
|
||||
|
||||
# Create walls
|
||||
walls = [
|
||||
# Central cross
|
||||
[(15, y) for y in range(8, 12)],
|
||||
[(x, 10) for x in range(13, 18)],
|
||||
|
||||
# Rooms
|
||||
# Top-left room
|
||||
[(x, 5) for x in range(2, 8)] + [(8, y) for y in range(2, 6)],
|
||||
[(2, y) for y in range(2, 6)] + [(x, 2) for x in range(2, 8)],
|
||||
|
||||
# Top-right room
|
||||
[(x, 5) for x in range(22, 28)] + [(22, y) for y in range(2, 6)],
|
||||
[(28, y) for y in range(2, 6)] + [(x, 2) for x in range(22, 28)],
|
||||
|
||||
# Bottom-left room
|
||||
[(x, 15) for x in range(2, 8)] + [(8, y) for y in range(15, 18)],
|
||||
[(2, y) for y in range(15, 18)] + [(x, 18) for x in range(2, 8)],
|
||||
|
||||
# Bottom-right room
|
||||
[(x, 15) for x in range(22, 28)] + [(22, y) for y in range(15, 18)],
|
||||
[(28, y) for y in range(15, 18)] + [(x, 18) for x in range(22, 28)],
|
||||
]
|
||||
|
||||
for wall_group in walls:
|
||||
for x, y in wall_group:
|
||||
if 0 <= x < 30 and 0 <= y < 20:
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.color = mcrfpy.Color(40, 20, 20) # Wall color
|
||||
|
||||
# Create entities
|
||||
player = mcrfpy.Entity(5, 10, grid=grid)
|
||||
player.sprite_index = 64 # @
|
||||
enemy = mcrfpy.Entity(25, 10, grid=grid)
|
||||
enemy.sprite_index = 69 # E
|
||||
|
||||
# Update initial visibility
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
|
||||
# Global state
|
||||
current_perspective = -1
|
||||
perspective_names = ["Omniscient", "Player", "Enemy"]
|
||||
|
||||
# UI Setup
|
||||
ui = mcrfpy.sceneUI("visibility_demo")
|
||||
ui.append(grid)
|
||||
grid.position = (50, 100)
|
||||
grid.size = (900, 600) # 30*30, 20*30
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Interactive Visibility Demo", 350, 20)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Info displays
|
||||
perspective_label = mcrfpy.Caption("Perspective: Omniscient", 50, 50)
|
||||
perspective_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(perspective_label)
|
||||
|
||||
controls = mcrfpy.Caption("WASD: Move player | Arrows: Move enemy | Tab: Cycle perspective | Space: Update visibility | R: Reset", 50, 730)
|
||||
controls.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(controls)
|
||||
|
||||
player_info = mcrfpy.Caption("Player: (5, 10)", 700, 50)
|
||||
player_info.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
ui.append(player_info)
|
||||
|
||||
enemy_info = mcrfpy.Caption("Enemy: (25, 10)", 700, 70)
|
||||
enemy_info.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
ui.append(enemy_info)
|
||||
|
||||
# Helper functions
|
||||
def move_entity(entity, dx, dy):
|
||||
"""Move entity if target is walkable"""
|
||||
new_x = int(entity.x + dx)
|
||||
new_y = int(entity.y + dy)
|
||||
|
||||
if 0 <= new_x < 30 and 0 <= new_y < 20:
|
||||
cell = grid.at(new_x, new_y)
|
||||
if cell.walkable:
|
||||
entity.x = new_x
|
||||
entity.y = new_y
|
||||
entity.update_visibility()
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_info():
|
||||
"""Update info displays"""
|
||||
player_info.text = f"Player: ({int(player.x)}, {int(player.y)})"
|
||||
enemy_info.text = f"Enemy: ({int(enemy.x)}, {int(enemy.y)})"
|
||||
|
||||
def cycle_perspective():
|
||||
"""Cycle through perspectives"""
|
||||
global current_perspective
|
||||
|
||||
# Cycle: -1 → 0 → 1 → -1
|
||||
current_perspective = (current_perspective + 2) % 3 - 1
|
||||
|
||||
grid.perspective = current_perspective
|
||||
name = perspective_names[current_perspective + 1]
|
||||
perspective_label.text = f"Perspective: {name}"
|
||||
|
||||
# Key handlers
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input"""
|
||||
if state == "end": return
|
||||
key = key.lower()
|
||||
# Player movement (WASD)
|
||||
if key == "w":
|
||||
move_entity(player, 0, -1)
|
||||
elif key == "s":
|
||||
move_entity(player, 0, 1)
|
||||
elif key == "a":
|
||||
move_entity(player, -1, 0)
|
||||
elif key == "d":
|
||||
move_entity(player, 1, 0)
|
||||
|
||||
# Enemy movement (Arrows)
|
||||
elif key == "up":
|
||||
move_entity(enemy, 0, -1)
|
||||
elif key == "down":
|
||||
move_entity(enemy, 0, 1)
|
||||
elif key == "left":
|
||||
move_entity(enemy, -1, 0)
|
||||
elif key == "right":
|
||||
move_entity(enemy, 1, 0)
|
||||
|
||||
# Tab to cycle perspective
|
||||
elif key == "tab":
|
||||
cycle_perspective()
|
||||
|
||||
# Space to update visibility
|
||||
elif key == "space":
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
print("Updated visibility for both entities")
|
||||
|
||||
# R to reset
|
||||
elif key == "r":
|
||||
player.x, player.y = 5, 10
|
||||
enemy.x, enemy.y = 25, 10
|
||||
player.update_visibility()
|
||||
enemy.update_visibility()
|
||||
update_info()
|
||||
print("Reset positions")
|
||||
|
||||
# Q to quit
|
||||
elif key == "q":
|
||||
print("Exiting...")
|
||||
sys.exit(0)
|
||||
|
||||
update_info()
|
||||
|
||||
# Set scene first
|
||||
mcrfpy.setScene("visibility_demo")
|
||||
|
||||
# Register key handler (operates on current scene)
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
print("Interactive Visibility Demo")
|
||||
print("===========================")
|
||||
print("WASD: Move player (green @)")
|
||||
print("Arrows: Move enemy (red E)")
|
||||
print("Tab: Cycle perspective")
|
||||
print("Space: Update visibility")
|
||||
print("R: Reset positions")
|
||||
print("Q: Quit")
|
||||
print("\nCurrent perspective: Omniscient (shows all)")
|
||||
print("Try moving entities and switching perspectives!")
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Simple interactive visibility test"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Create scene and grid
|
||||
print("Creating scene...")
|
||||
mcrfpy.createScene("vis_test")
|
||||
|
||||
print("Creating grid...")
|
||||
grid = mcrfpy.Grid(grid_x=10, grid_y=10)
|
||||
|
||||
# Initialize grid
|
||||
print("Initializing grid...")
|
||||
for y in range(10):
|
||||
for x in range(10):
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.color = mcrfpy.Color(100, 100, 120)
|
||||
|
||||
# Create entity
|
||||
print("Creating entity...")
|
||||
entity = mcrfpy.Entity(5, 5, grid=grid)
|
||||
entity.sprite_index = 64
|
||||
|
||||
print("Updating visibility...")
|
||||
entity.update_visibility()
|
||||
|
||||
# Set up UI
|
||||
print("Setting up UI...")
|
||||
ui = mcrfpy.sceneUI("vis_test")
|
||||
ui.append(grid)
|
||||
grid.position = (50, 50)
|
||||
grid.size = (300, 300)
|
||||
|
||||
# Test perspective
|
||||
print("Testing perspective...")
|
||||
grid.perspective = -1 # Omniscient
|
||||
print(f"Perspective set to: {grid.perspective}")
|
||||
|
||||
print("Setting scene...")
|
||||
mcrfpy.setScene("vis_test")
|
||||
|
||||
print("Ready!")
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Simple visibility test without entity append"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
print("Simple visibility test...")
|
||||
|
||||
# Create scene and grid
|
||||
mcrfpy.createScene("simple")
|
||||
print("Scene created")
|
||||
|
||||
grid = mcrfpy.Grid(grid_x=5, grid_y=5)
|
||||
print("Grid created")
|
||||
|
||||
# Create entity without appending
|
||||
entity = mcrfpy.Entity(2, 2, grid=grid)
|
||||
print(f"Entity created at ({entity.x}, {entity.y})")
|
||||
|
||||
# Check if gridstate is initialized
|
||||
print(f"Gridstate length: {len(entity.gridstate)}")
|
||||
|
||||
# Try to access at method
|
||||
try:
|
||||
state = entity.at(0, 0)
|
||||
print(f"at(0,0) returned: {state}")
|
||||
print(f"visible: {state.visible}, discovered: {state.discovered}")
|
||||
except Exception as e:
|
||||
print(f"Error in at(): {e}")
|
||||
|
||||
# Try update_visibility
|
||||
try:
|
||||
entity.update_visibility()
|
||||
print("update_visibility() succeeded")
|
||||
except Exception as e:
|
||||
print(f"Error in update_visibility(): {e}")
|
||||
|
||||
print("Test complete")
|
||||
sys.exit(0)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue