Compare commits
12 Commits
master
...
alpha_pres
| Author | SHA1 | Date |
|---|---|---|
|
|
c5e7e8e298 | |
|
|
6d29652ae7 | |
|
|
a010e5fa96 | |
|
|
9c8d6c4591 | |
|
|
dcd1b0ca33 | |
|
|
6813fb5129 | |
|
|
6f67fbb51e | |
|
|
eb88c7b3aa | |
|
|
9fb428dd01 | |
|
|
bde82028b5 | |
|
|
062e4dadc4 | |
|
|
98fc49a978 |
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"gitea": {
|
||||
"type": "stdio",
|
||||
"command": "/home/john/Development/discord_for_claude/forgejo-mcp.linux.amd64",
|
||||
"args": ["stdio", "--server", "https://gamedev.ffwf.net/gitea/", "--token", "f58ec698a5edee82db4960920b13d3f7d0d58d8e"]
|
||||
}
|
||||
}
|
||||
}
|
||||
568
CLAUDE.md
568
CLAUDE.md
|
|
@ -1,568 +0,0 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Gitea-First Workflow
|
||||
|
||||
**IMPORTANT**: This project uses Gitea for issue tracking, documentation, and project management. Always consult and update Gitea resources before and during development work.
|
||||
|
||||
**Gitea Instance**: https://gamedev.ffwf.net/gitea/john/McRogueFace
|
||||
|
||||
### Core Principles
|
||||
|
||||
1. **Gitea is the Single Source of Truth**
|
||||
- Issue tracker contains current tasks, bugs, and feature requests
|
||||
- Wiki contains living documentation and architecture decisions
|
||||
- Use Gitea MCP tools to query and update issues programmatically
|
||||
|
||||
2. **Always Check Gitea First**
|
||||
- Before starting work: Check open issues for related tasks or blockers
|
||||
- When using `/roadmap` command: Query Gitea for up-to-date issue status
|
||||
- When researching a feature: Search Gitea wiki and issues before grepping codebase
|
||||
- When encountering a bug: Check if an issue already exists
|
||||
|
||||
3. **Create Granular Issues**
|
||||
- Break large features into separate, focused issues
|
||||
- Each issue should address one specific problem or enhancement
|
||||
- Tag issues appropriately: `[Bugfix]`, `[Major Feature]`, `[Minor Feature]`, etc.
|
||||
- Link related issues using dependencies or blocking relationships
|
||||
|
||||
4. **Document as You Go**
|
||||
- When work on one issue interacts with another system: Add notes to related issues
|
||||
- When discovering undocumented behavior: Create task to document it
|
||||
- When documentation misleads you: Create task to correct or expand it
|
||||
- When implementing a feature: Update the Gitea wiki if appropriate
|
||||
|
||||
5. **Cross-Reference Everything**
|
||||
- Commit messages should reference issue numbers (e.g., "Fixes #104", "Addresses #125")
|
||||
- Issue comments should link to commits when work is done
|
||||
- Wiki pages should reference relevant issues for implementation details
|
||||
- Issues should link to each other when dependencies exist
|
||||
|
||||
### Workflow Pattern
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 1. Check Gitea Issues & Wiki │
|
||||
│ - Is there an existing issue for this? │
|
||||
│ - What's the current status? │
|
||||
│ - Are there related issues or blockers? │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 2. Create Issues (if needed) │
|
||||
│ - Break work into granular tasks │
|
||||
│ - Tag appropriately │
|
||||
│ - Link dependencies │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 3. Do the Work │
|
||||
│ - Implement/fix/document │
|
||||
│ - Write tests first (TDD) │
|
||||
│ - Add inline documentation │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 4. Update Gitea │
|
||||
│ - Add notes to affected issues │
|
||||
│ - Create follow-up issues for discovered work │
|
||||
│ - Update wiki if architecture/APIs changed │
|
||||
│ - Add documentation correction tasks │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 5. Commit & Reference │
|
||||
│ - Commit messages reference issue numbers │
|
||||
│ - Close issues or update status │
|
||||
│ - Add commit links to issue comments │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Benefits of Gitea-First Approach
|
||||
|
||||
- **Reduced Context Switching**: Check brief issue descriptions instead of re-reading entire codebase
|
||||
- **Better Planning**: Issues provide roadmap; avoid duplicate or contradictory work
|
||||
- **Living Documentation**: Wiki and issues stay current as work progresses
|
||||
- **Historical Context**: Issue comments capture why decisions were made
|
||||
- **Efficiency**: MCP tools allow programmatic access to project state
|
||||
|
||||
### MCP Tools Available
|
||||
|
||||
Claude Code has access to Gitea MCP tools for:
|
||||
- `list_repo_issues` - Query current issues with filtering
|
||||
- `get_issue` - Get detailed issue information
|
||||
- `create_issue` - Create new issues programmatically
|
||||
- `create_issue_comment` - Add comments to issues
|
||||
- `edit_issue` - Update issue status, title, body
|
||||
- `add_issue_labels` - Tag issues appropriately
|
||||
- `add_issue_dependency` / `add_issue_blocking` - Link related issues
|
||||
- Plus wiki, milestone, and label management tools
|
||||
|
||||
Use these tools liberally to keep the project organized!
|
||||
|
||||
### Gitea Label System
|
||||
|
||||
**IMPORTANT**: Always apply appropriate labels when creating new issues!
|
||||
|
||||
The project uses a structured label system to organize issues:
|
||||
|
||||
**Label Categories:**
|
||||
|
||||
1. **System Labels** (identify affected codebase area):
|
||||
- `system:rendering` - Rendering pipeline and visuals
|
||||
- `system:ui-hierarchy` - UI component hierarchy and composition
|
||||
- `system:grid` - Grid system and spatial containers
|
||||
- `system:animation` - Animation and property interpolation
|
||||
- `system:python-binding` - Python/C++ binding layer
|
||||
- `system:input` - Input handling and events
|
||||
- `system:performance` - Performance optimization and profiling
|
||||
- `system:documentation` - Documentation infrastructure
|
||||
|
||||
2. **Priority Labels** (development timeline):
|
||||
- `priority:tier1-active` - Current development focus - critical path to v1.0
|
||||
- `priority:tier2-foundation` - Important foundation work - not blocking v1.0
|
||||
- `priority:tier3-future` - Future features - deferred until after v1.0
|
||||
|
||||
3. **Type/Scope Labels** (effort and complexity):
|
||||
- `Major Feature` - Significant time and effort required
|
||||
- `Minor Feature` - Some effort required to create or overhaul functionality
|
||||
- `Tiny Feature` - Quick and easy - a few lines or little interconnection
|
||||
- `Bugfix` - Fixes incorrect behavior
|
||||
- `Refactoring & Cleanup` - No new functionality, just improving codebase
|
||||
- `Documentation` - Documentation work
|
||||
- `Demo Target` - Functionality to demonstrate
|
||||
|
||||
4. **Workflow Labels** (current blockers/needs):
|
||||
- `workflow:blocked` - Blocked by other work - waiting on dependencies
|
||||
- `workflow:needs-documentation` - Needs documentation before or after implementation
|
||||
- `workflow:needs-benchmark` - Needs performance testing and benchmarks
|
||||
- `Alpha Release Requirement` - Blocker to 0.1 Alpha release
|
||||
|
||||
**When creating issues:**
|
||||
- Apply at least one `system:*` label (what part of codebase)
|
||||
- Apply one `priority:tier*` label (when to address it)
|
||||
- Apply one type label (`Major Feature`, `Minor Feature`, `Tiny Feature`, or `Bugfix`)
|
||||
- Apply `workflow:*` labels if applicable (blocked, needs docs, needs benchmarks)
|
||||
|
||||
**Example label combinations:**
|
||||
- New rendering feature: `system:rendering`, `priority:tier2-foundation`, `Major Feature`
|
||||
- Python API improvement: `system:python-binding`, `priority:tier1-active`, `Minor Feature`
|
||||
- Performance work: `system:performance`, `priority:tier1-active`, `Major Feature`, `workflow:needs-benchmark`
|
||||
|
||||
**Note**: The Gitea MCP tool has unreliable label application. The `add_issue_labels` and `replace_issue_labels` functions often apply wrong labels even with correct IDs.
|
||||
|
||||
**STRONGLY RECOMMENDED**: Apply labels manually via web interface:
|
||||
`https://gamedev.ffwf.net/gitea/john/McRogueFace/issues/<number>`
|
||||
|
||||
**Label ID Reference** (for documentation purposes - see issue #131 for details):
|
||||
```
|
||||
1=Major Feature, 2=Alpha Release, 3=Bugfix, 4=Demo Target, 5=Documentation,
|
||||
6=Minor Feature, 7=tier1-active, 8=tier2-foundation, 9=tier3-future,
|
||||
10=Refactoring, 11=animation, 12=docs, 13=grid, 14=input, 15=performance,
|
||||
16=python-binding, 17=rendering, 18=ui-hierarchy, 19=Tiny Feature,
|
||||
20=blocked, 21=needs-benchmark, 22=needs-documentation
|
||||
```
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Build the project (compiles to ./build directory)
|
||||
make
|
||||
|
||||
# Or use the build script directly
|
||||
./build.sh
|
||||
|
||||
# Run the game
|
||||
make run
|
||||
|
||||
# Clean build artifacts
|
||||
make clean
|
||||
|
||||
# The executable and all assets are in ./build/
|
||||
cd build
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
## Project Architecture
|
||||
|
||||
McRogueFace is a C++ game engine with Python scripting support, designed for creating roguelike games. The architecture consists of:
|
||||
|
||||
### Core Engine (C++)
|
||||
- **Entry Point**: `src/main.cpp` initializes the game engine
|
||||
- **Scene System**: `Scene.h/cpp` manages game states
|
||||
- **Entity System**: `UIEntity.h/cpp` provides game objects
|
||||
- **Python Integration**: `McRFPy_API.h/cpp` exposes engine functionality to Python
|
||||
- **UI Components**: `UIFrame`, `UICaption`, `UISprite`, `UIGrid` for rendering
|
||||
|
||||
### Game Logic (Python)
|
||||
- **Main Script**: `src/scripts/game.py` contains game initialization and scene setup
|
||||
- **Entity System**: `src/scripts/cos_entities.py` implements game entities (Player, Enemy, Boulder, etc.)
|
||||
- **Level Generation**: `src/scripts/cos_level.py` uses BSP for procedural dungeon generation
|
||||
- **Tile System**: `src/scripts/cos_tiles.py` implements Wave Function Collapse for tile placement
|
||||
|
||||
### Key Python API (`mcrfpy` module)
|
||||
The C++ engine exposes these primary functions to Python:
|
||||
- Scene Management: `createScene()`, `setScene()`, `sceneUI()`
|
||||
- Entity Creation: `Entity()` with position and sprite properties
|
||||
- Grid Management: `Grid()` for tilemap rendering
|
||||
- Input Handling: `keypressScene()` for keyboard events
|
||||
- Audio: `createSoundBuffer()`, `playSound()`, `setVolume()`
|
||||
- Timers: `setTimer()`, `delTimer()` for event scheduling
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Running the Game
|
||||
After building, the executable expects:
|
||||
- `assets/` directory with sprites, fonts, and audio
|
||||
- `scripts/` directory with Python game files
|
||||
- Python 3.12 shared libraries in `./lib/`
|
||||
|
||||
### Modifying Game Logic
|
||||
- Game scripts are in `src/scripts/`
|
||||
- Main game entry is `game.py`
|
||||
- Entity behavior in `cos_entities.py`
|
||||
- Level generation in `cos_level.py`
|
||||
|
||||
### Adding New Features
|
||||
1. C++ API additions go in `src/McRFPy_API.cpp`
|
||||
2. Expose to Python using the existing binding pattern
|
||||
3. Update Python scripts to use new functionality
|
||||
|
||||
## Testing Game Changes
|
||||
|
||||
Currently no automated test suite. Manual testing workflow:
|
||||
1. Build with `make`
|
||||
2. Run `make run` or `cd build && ./mcrogueface`
|
||||
3. Test specific features through gameplay
|
||||
4. Check console output for Python errors
|
||||
|
||||
### Quick Testing Commands
|
||||
```bash
|
||||
# Test basic functionality
|
||||
make test
|
||||
|
||||
# Run in Python interactive mode
|
||||
make python
|
||||
|
||||
# Test headless mode
|
||||
cd build
|
||||
./mcrogueface --headless -c "import mcrfpy; print('Headless test')"
|
||||
```
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Compiling McRogueFace
|
||||
```bash
|
||||
# Standard build (to ./build directory)
|
||||
make
|
||||
|
||||
# Full rebuild
|
||||
make clean && make
|
||||
|
||||
# Manual CMake build
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# The library path issue: if linking fails, check that libraries are in __lib/
|
||||
# CMakeLists.txt expects: link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
```
|
||||
|
||||
### Running and Capturing Output
|
||||
```bash
|
||||
# Run with timeout and capture output
|
||||
cd build
|
||||
timeout 5 ./mcrogueface 2>&1 | tee output.log
|
||||
|
||||
# Run in background and kill after delay
|
||||
./mcrogueface > output.txt 2>&1 & PID=$!; sleep 3; kill $PID 2>/dev/null
|
||||
|
||||
# Just capture first N lines (useful for crashes)
|
||||
./mcrogueface 2>&1 | head -50
|
||||
```
|
||||
|
||||
### Debugging with GDB
|
||||
```bash
|
||||
# Interactive debugging
|
||||
gdb ./mcrogueface
|
||||
(gdb) run
|
||||
(gdb) bt # backtrace after crash
|
||||
|
||||
# Batch mode debugging (non-interactive)
|
||||
gdb -batch -ex run -ex where -ex quit ./mcrogueface 2>&1
|
||||
|
||||
# Get just the backtrace after a crash
|
||||
gdb -batch -ex "run" -ex "bt" ./mcrogueface 2>&1 | head -50
|
||||
|
||||
# Debug with specific commands
|
||||
echo -e "run\nbt 5\nquit\ny" | gdb ./mcrogueface 2>&1
|
||||
```
|
||||
|
||||
### Testing Different Python Scripts
|
||||
```bash
|
||||
# The game automatically runs build/scripts/game.py on startup
|
||||
# To test different behavior:
|
||||
|
||||
# Option 1: Replace game.py temporarily
|
||||
cd build
|
||||
cp scripts/my_test_script.py scripts/game.py
|
||||
./mcrogueface
|
||||
|
||||
# Option 2: Backup original and test
|
||||
mv scripts/game.py scripts/game.py.bak
|
||||
cp my_test.py scripts/game.py
|
||||
./mcrogueface
|
||||
mv scripts/game.py.bak scripts/game.py
|
||||
|
||||
# Option 3: For quick tests, create minimal game.py
|
||||
echo 'import mcrfpy; print("Test"); mcrfpy.createScene("test")' > scripts/game.py
|
||||
```
|
||||
|
||||
### Understanding Key Macros and Patterns
|
||||
|
||||
#### RET_PY_INSTANCE Macro (UIDrawable.h)
|
||||
This macro handles converting C++ UI objects to their Python equivalents:
|
||||
```cpp
|
||||
RET_PY_INSTANCE(target);
|
||||
// Expands to a switch on target->derived_type() that:
|
||||
// 1. Allocates the correct Python object type (Frame, Caption, Sprite, Grid)
|
||||
// 2. Sets the shared_ptr data member
|
||||
// 3. Returns the PyObject*
|
||||
```
|
||||
|
||||
#### Collection Patterns
|
||||
- `UICollection` wraps `std::vector<std::shared_ptr<UIDrawable>>`
|
||||
- `UIEntityCollection` wraps `std::list<std::shared_ptr<UIEntity>>`
|
||||
- Different containers require different iteration code (vector vs list)
|
||||
|
||||
#### Python Object Creation Patterns
|
||||
```cpp
|
||||
// Pattern 1: Using tp_alloc (most common)
|
||||
auto o = (PyUIFrameObject*)type->tp_alloc(type, 0);
|
||||
o->data = std::make_shared<UIFrame>();
|
||||
|
||||
// Pattern 2: Getting type from module
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
||||
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
||||
|
||||
// Pattern 3: Direct shared_ptr assignment
|
||||
iterObj->data = self->data; // Shares the C++ object
|
||||
```
|
||||
|
||||
### Working Directory Structure
|
||||
```
|
||||
build/
|
||||
├── mcrogueface # The executable
|
||||
├── scripts/
|
||||
│ └── game.py # Auto-loaded Python script
|
||||
├── assets/ # Copied from source during build
|
||||
└── lib/ # Python libraries (copied from __lib/)
|
||||
```
|
||||
|
||||
### Quick Iteration Tips
|
||||
- Keep a test script ready for quick experiments
|
||||
- Use `timeout` to auto-kill hanging processes
|
||||
- The game expects a window manager; use Xvfb for headless testing
|
||||
- Python errors go to stderr, game output to stdout
|
||||
- Segfaults usually mean Python type initialization issues
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The project uses SFML for graphics/audio and libtcod for roguelike utilities
|
||||
- Python scripts are loaded at runtime from the `scripts/` directory
|
||||
- Asset loading expects specific paths relative to the executable
|
||||
- The game was created for 7DRL 2025 as "Crypt of Sokoban"
|
||||
- Iterator implementations require careful handling of C++/Python boundaries
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Test-Driven Development
|
||||
- **Always write tests first**: Create automation tests in `./tests/` for all bugs and new features
|
||||
- **Practice TDD**: Write tests that fail to demonstrate the issue, then pass after the fix is applied
|
||||
- **Close the loop**: Reproduce issue → change code → recompile → verify behavior change
|
||||
|
||||
### Two Types of Tests
|
||||
|
||||
#### 1. Direct Execution Tests (No Game Loop)
|
||||
For tests that only need class initialization or direct code execution:
|
||||
```python
|
||||
# These tests can treat McRogueFace like a Python interpreter
|
||||
import mcrfpy
|
||||
|
||||
# Test code here
|
||||
result = mcrfpy.some_function()
|
||||
assert result == expected_value
|
||||
print("PASS" if condition else "FAIL")
|
||||
```
|
||||
|
||||
#### 2. Game Loop Tests (Timer-Based)
|
||||
For tests requiring rendering, game state, or elapsed time:
|
||||
```python
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
def run_test(runtime):
|
||||
"""Timer callback - runs after game loop starts"""
|
||||
# Now rendering is active, screenshots will work
|
||||
automation.screenshot("test_result.png")
|
||||
|
||||
# Run your tests here
|
||||
automation.click(100, 100)
|
||||
|
||||
# Always exit at the end
|
||||
print("PASS" if success else "FAIL")
|
||||
sys.exit(0)
|
||||
|
||||
# Set up the test scene
|
||||
mcrfpy.createScene("test")
|
||||
# ... add UI elements ...
|
||||
|
||||
# Schedule test to run after game loop starts
|
||||
mcrfpy.setTimer("test", run_test, 100) # 0.1 seconds
|
||||
```
|
||||
|
||||
### Key Testing Principles
|
||||
- **Timer callbacks are essential**: Screenshots and UI interactions only work after the render loop starts
|
||||
- **Use automation API**: Always create and examine screenshots when visual feedback is required
|
||||
- **Exit properly**: Call `sys.exit()` at the end of timer-based tests to prevent hanging
|
||||
- **Headless mode**: Use `--exec` flag for automated testing: `./mcrogueface --headless --exec tests/my_test.py`
|
||||
|
||||
### Example Test Pattern
|
||||
```bash
|
||||
# Run a test that requires game loop
|
||||
./build/mcrogueface --headless --exec tests/issue_78_middle_click_test.py
|
||||
|
||||
# The test will:
|
||||
# 1. Set up the scene during script execution
|
||||
# 2. Register a timer callback
|
||||
# 3. Game loop starts
|
||||
# 4. Timer fires after 100ms
|
||||
# 5. Test runs with full rendering available
|
||||
# 6. Test takes screenshots and validates behavior
|
||||
# 7. Test calls sys.exit() to terminate
|
||||
```
|
||||
|
||||
## Development Best Practices
|
||||
|
||||
### Testing and Deployment
|
||||
- **Keep tests in ./tests, not ./build/tests** - ./build gets shipped, and tests shouldn't be included
|
||||
|
||||
## Documentation Guidelines
|
||||
|
||||
### Documentation Macro System
|
||||
|
||||
**As of 2025-10-30, McRogueFace uses a macro-based documentation system** (`src/McRFPy_Doc.h`) that ensures consistent, complete docstrings across all Python bindings.
|
||||
|
||||
#### Include the Header
|
||||
|
||||
```cpp
|
||||
#include "McRFPy_Doc.h"
|
||||
```
|
||||
|
||||
#### Documenting Methods
|
||||
|
||||
For methods in PyMethodDef arrays, use `MCRF_METHOD`:
|
||||
|
||||
```cpp
|
||||
{"method_name", (PyCFunction)Class::method, METH_VARARGS,
|
||||
MCRF_METHOD(ClassName, method_name,
|
||||
MCRF_SIG("(arg1: type, arg2: type)", "return_type"),
|
||||
MCRF_DESC("Brief description of what the method does."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("arg1", "Description of first argument")
|
||||
MCRF_ARG("arg2", "Description of second argument")
|
||||
MCRF_RETURNS("Description of return value")
|
||||
MCRF_RAISES("ValueError", "Condition that raises this exception")
|
||||
MCRF_NOTE("Important notes or caveats")
|
||||
MCRF_LINK("docs/guide.md", "Related Documentation")
|
||||
)},
|
||||
```
|
||||
|
||||
#### Documenting Properties
|
||||
|
||||
For properties in PyGetSetDef arrays, use `MCRF_PROPERTY`:
|
||||
|
||||
```cpp
|
||||
{"property_name", (getter)getter_func, (setter)setter_func,
|
||||
MCRF_PROPERTY(property_name,
|
||||
"Brief description of the property. "
|
||||
"Additional details about valid values, side effects, etc."
|
||||
), NULL},
|
||||
```
|
||||
|
||||
#### Available Macros
|
||||
|
||||
- `MCRF_SIG(params, ret)` - Method signature
|
||||
- `MCRF_DESC(text)` - Description paragraph
|
||||
- `MCRF_ARGS_START` - Begin arguments section
|
||||
- `MCRF_ARG(name, desc)` - Individual argument
|
||||
- `MCRF_RETURNS(text)` - Return value description
|
||||
- `MCRF_RAISES(exception, condition)` - Exception documentation
|
||||
- `MCRF_NOTE(text)` - Important notes
|
||||
- `MCRF_LINK(path, text)` - Reference to external documentation
|
||||
|
||||
#### Documentation Prose Guidelines
|
||||
|
||||
**Keep C++ docstrings concise** (1-2 sentences per section). For complex topics, use `MCRF_LINK` to reference external guides:
|
||||
|
||||
```cpp
|
||||
MCRF_LINK("docs/animation-guide.md", "Animation System Tutorial")
|
||||
```
|
||||
|
||||
**External documentation** (in `docs/`) can be verbose with examples, tutorials, and design rationale.
|
||||
|
||||
### Regenerating Documentation
|
||||
|
||||
After modifying C++ inline documentation with MCRF_* macros:
|
||||
|
||||
1. **Rebuild the project**: `make -j$(nproc)`
|
||||
|
||||
2. **Generate all documentation** (recommended - single command):
|
||||
```bash
|
||||
./tools/generate_all_docs.sh
|
||||
```
|
||||
This creates:
|
||||
- `docs/api_reference_dynamic.html` - HTML API reference
|
||||
- `docs/API_REFERENCE_DYNAMIC.md` - Markdown API reference
|
||||
- `docs/mcrfpy.3` - Unix man page (section 3)
|
||||
- `stubs/mcrfpy.pyi` - Type stubs for IDE support
|
||||
|
||||
3. **Or generate individually**:
|
||||
```bash
|
||||
# API docs (HTML + Markdown)
|
||||
./build/mcrogueface --headless --exec tools/generate_dynamic_docs.py
|
||||
|
||||
# Type stubs (manually-maintained with @overload support)
|
||||
./build/mcrogueface --headless --exec tools/generate_stubs_v2.py
|
||||
|
||||
# Man page (requires pandoc)
|
||||
./tools/generate_man_page.sh
|
||||
```
|
||||
|
||||
**System Requirements:**
|
||||
- `pandoc` must be installed for man page generation: `sudo apt-get install pandoc`
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Single source of truth**: Documentation lives in C++ source files via MCRF_* macros
|
||||
- **McRogueFace as Python interpreter**: Documentation scripts MUST be run using McRogueFace itself, not system Python
|
||||
- **Use --headless --exec**: For non-interactive documentation generation
|
||||
- **Link transformation**: `MCRF_LINK` references are transformed to appropriate format (HTML, Markdown, etc.)
|
||||
- **No manual dictionaries**: The old hardcoded documentation system has been removed
|
||||
|
||||
### Documentation Pipeline Architecture
|
||||
|
||||
1. **C++ Source** → MCRF_* macros in PyMethodDef/PyGetSetDef arrays
|
||||
2. **Compilation** → Macros expand to complete docstrings embedded in module
|
||||
3. **Introspection** → Scripts use `dir()`, `getattr()`, `__doc__` to extract
|
||||
4. **Generation** → HTML/Markdown/Stub files created with transformed links
|
||||
5. **No drift**: Impossible for docs and code to disagree - they're the same file!
|
||||
|
||||
The macro system ensures complete, consistent documentation across all Python bindings.
|
||||
- Close issues automatically in gitea by adding to the commit message "closes #X", where X is the issue number. This associates the issue closure with the specific commit, so granular commits are preferred. You should only use the MCP tool to close issues directly when discovering that the issue is already complete; when committing changes, always such "closes" (or the opposite, "reopens") references to related issues. If on a feature branch, the issue will be referenced by the commit, and when merged to master, the issue will be actually closed (or reopened).
|
||||
38
README.md
38
README.md
|
|
@ -57,28 +57,18 @@ mcrfpy.setScene("intro")
|
|||
|
||||
## Documentation
|
||||
|
||||
### 📚 Developer Documentation
|
||||
### 📚 Full Documentation Site
|
||||
|
||||
For comprehensive documentation about systems, architecture, and development workflows:
|
||||
For comprehensive documentation, tutorials, and API reference, visit:
|
||||
**[https://mcrogueface.github.io](https://mcrogueface.github.io)**
|
||||
|
||||
**[Project Wiki](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki)**
|
||||
The documentation site includes:
|
||||
|
||||
Key wiki pages:
|
||||
|
||||
- **[Home](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Home)** - Documentation hub with multiple entry points
|
||||
- **[Grid System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Grid-System)** - Three-layer grid architecture
|
||||
- **[Python Binding System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Python-Binding-System)** - C++/Python integration
|
||||
- **[Performance and Profiling](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Performance-and-Profiling)** - Optimization tools
|
||||
- **[Adding Python Bindings](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Adding-Python-Bindings)** - Step-by-step binding guide
|
||||
- **[Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap)** - All 46 open issues organized by system
|
||||
|
||||
### 📖 Development Guides
|
||||
|
||||
In the repository root:
|
||||
|
||||
- **[CLAUDE.md](CLAUDE.md)** - Build instructions, testing guidelines, common tasks
|
||||
- **[ROADMAP.md](ROADMAP.md)** - Strategic vision and development phases
|
||||
- **[roguelike_tutorial/](roguelike_tutorial/)** - Complete roguelike tutorial implementations
|
||||
- **[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
|
||||
|
||||
|
|
@ -124,15 +114,7 @@ If you are writing a game in Python using McRogueFace, you only need to rename a
|
|||
|
||||
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.
|
||||
|
||||
### Issue Tracking
|
||||
|
||||
The project uses [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for task tracking and bug reports. Issues are organized with labels:
|
||||
|
||||
- **System labels** (grid, animation, python-binding, etc.) - identify which codebase area
|
||||
- **Priority labels** (tier1-active, tier2-foundation, tier3-future) - development timeline
|
||||
- **Type labels** (Major Feature, Minor Feature, Bugfix, etc.) - effort and scope
|
||||
|
||||
See the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap) on the wiki for organized view of all open tasks.
|
||||
The project has a private roadmap and issue list. Reach out via email or social media if you have bugs or feature requests.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
924
ROADMAP.md
924
ROADMAP.md
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,5 @@
|
|||
# McRogueFace API Reference
|
||||
|
||||
*Generated on 2025-07-15 21:28:42*
|
||||
|
||||
## Overview
|
||||
|
||||
McRogueFace Python API
|
||||
|
|
@ -375,6 +373,14 @@ A rectangular frame UI element that can contain other drawable elements.
|
|||
|
||||
#### Methods
|
||||
|
||||
#### `get_bounds()`
|
||||
|
||||
Get the bounding rectangle of this drawable element.
|
||||
|
||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||
|
||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||
|
||||
#### `resize(width, height)`
|
||||
|
||||
Resize the element to new dimensions.
|
||||
|
|
@ -395,14 +401,6 @@ Move the element by a relative offset.
|
|||
|
||||
**Note:** This modifies the x and y position properties by the given amounts.
|
||||
|
||||
#### `get_bounds()`
|
||||
|
||||
Get the bounding rectangle of this drawable element.
|
||||
|
||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||
|
||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||
|
||||
---
|
||||
|
||||
### class `Caption`
|
||||
|
|
@ -411,6 +409,14 @@ A text display UI element with customizable font and styling.
|
|||
|
||||
#### Methods
|
||||
|
||||
#### `get_bounds()`
|
||||
|
||||
Get the bounding rectangle of this drawable element.
|
||||
|
||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||
|
||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||
|
||||
#### `resize(width, height)`
|
||||
|
||||
Resize the element to new dimensions.
|
||||
|
|
@ -431,14 +437,6 @@ Move the element by a relative offset.
|
|||
|
||||
**Note:** This modifies the x and y position properties by the given amounts.
|
||||
|
||||
#### `get_bounds()`
|
||||
|
||||
Get the bounding rectangle of this drawable element.
|
||||
|
||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||
|
||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||
|
||||
---
|
||||
|
||||
### class `Sprite`
|
||||
|
|
@ -447,6 +445,14 @@ A sprite UI element that displays a texture or portion of a texture atlas.
|
|||
|
||||
#### Methods
|
||||
|
||||
#### `get_bounds()`
|
||||
|
||||
Get the bounding rectangle of this drawable element.
|
||||
|
||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||
|
||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||
|
||||
#### `resize(width, height)`
|
||||
|
||||
Resize the element to new dimensions.
|
||||
|
|
@ -467,14 +473,6 @@ Move the element by a relative offset.
|
|||
|
||||
**Note:** This modifies the x and y position properties by the given amounts.
|
||||
|
||||
#### `get_bounds()`
|
||||
|
||||
Get the bounding rectangle of this drawable element.
|
||||
|
||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||
|
||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||
|
||||
---
|
||||
|
||||
### class `Grid`
|
||||
|
|
@ -483,16 +481,6 @@ A grid-based tilemap UI element for rendering tile-based levels and game worlds.
|
|||
|
||||
#### Methods
|
||||
|
||||
#### `resize(width, height)`
|
||||
|
||||
Resize the element to new dimensions.
|
||||
|
||||
**Arguments:**
|
||||
- `width` (*float*): New width in pixels
|
||||
- `height` (*float*): New height in pixels
|
||||
|
||||
**Note:** For Caption and Sprite, this may not change actual size if determined by content.
|
||||
|
||||
#### `at(x, y)`
|
||||
|
||||
Get the GridPoint at the specified grid coordinates.
|
||||
|
|
@ -503,6 +491,24 @@ Get the GridPoint at the specified grid coordinates.
|
|||
|
||||
**Returns:** GridPoint or None: The grid point at (x, y), or None if out of bounds
|
||||
|
||||
#### `get_bounds()`
|
||||
|
||||
Get the bounding rectangle of this drawable element.
|
||||
|
||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||
|
||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||
|
||||
#### `resize(width, height)`
|
||||
|
||||
Resize the element to new dimensions.
|
||||
|
||||
**Arguments:**
|
||||
- `width` (*float*): New width in pixels
|
||||
- `height` (*float*): New height in pixels
|
||||
|
||||
**Note:** For Caption and Sprite, this may not change actual size if determined by content.
|
||||
|
||||
#### `move(dx, dy)`
|
||||
|
||||
Move the element by a relative offset.
|
||||
|
|
@ -513,14 +519,6 @@ Move the element by a relative offset.
|
|||
|
||||
**Note:** This modifies the x and y position properties by the given amounts.
|
||||
|
||||
#### `get_bounds()`
|
||||
|
||||
Get the bounding rectangle of this drawable element.
|
||||
|
||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||
|
||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||
|
||||
---
|
||||
|
||||
### class `Entity`
|
||||
|
|
@ -529,6 +527,12 @@ Game entity that can be placed in a Grid.
|
|||
|
||||
#### Methods
|
||||
|
||||
#### `die()`
|
||||
|
||||
Remove this entity from its parent grid.
|
||||
|
||||
**Note:** The entity object remains valid but is no longer rendered or updated.
|
||||
|
||||
#### `move(dx, dy)`
|
||||
|
||||
Move the element by a relative offset.
|
||||
|
|
@ -557,11 +561,11 @@ Get the bounding rectangle of this drawable element.
|
|||
|
||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||
|
||||
#### `die()`
|
||||
#### `index()`
|
||||
|
||||
Remove this entity from its parent grid.
|
||||
Get the index of this entity in its parent grid's entity list.
|
||||
|
||||
**Note:** The entity object remains valid but is no longer rendered or updated.
|
||||
**Returns:** int: Index position, or -1 if not in a grid
|
||||
|
||||
#### `resize(width, height)`
|
||||
|
||||
|
|
@ -573,12 +577,6 @@ Resize the element to new dimensions.
|
|||
|
||||
**Note:** For Caption and Sprite, this may not change actual size if determined by content.
|
||||
|
||||
#### `index()`
|
||||
|
||||
Get the index of this entity in its parent grid's entity list.
|
||||
|
||||
**Returns:** int: Index position, or -1 if not in a grid
|
||||
|
||||
---
|
||||
|
||||
### Collections
|
||||
|
|
@ -589,6 +587,13 @@ Container for Entity objects in a Grid. Supports iteration and indexing.
|
|||
|
||||
#### Methods
|
||||
|
||||
#### `append(entity)`
|
||||
|
||||
Add an entity to the end of the collection.
|
||||
|
||||
**Arguments:**
|
||||
- `entity` (*Entity*): The entity to add
|
||||
|
||||
#### `remove(entity)`
|
||||
|
||||
Remove the first occurrence of an entity from the collection.
|
||||
|
|
@ -598,13 +603,6 @@ Remove the first occurrence of an entity from the collection.
|
|||
|
||||
**Raises:** ValueError: If entity is not in collection
|
||||
|
||||
#### `extend(iterable)`
|
||||
|
||||
Add all entities from an iterable to the collection.
|
||||
|
||||
**Arguments:**
|
||||
- `iterable` (*Iterable[Entity]*): Entities to add
|
||||
|
||||
#### `count(entity)`
|
||||
|
||||
Count the number of occurrences of an entity in the collection.
|
||||
|
|
@ -625,12 +623,12 @@ Find the index of the first occurrence of an entity.
|
|||
|
||||
**Raises:** ValueError: If entity is not in collection
|
||||
|
||||
#### `append(entity)`
|
||||
#### `extend(iterable)`
|
||||
|
||||
Add an entity to the end of the collection.
|
||||
Add all entities from an iterable to the collection.
|
||||
|
||||
**Arguments:**
|
||||
- `entity` (*Entity*): The entity to add
|
||||
- `iterable` (*Iterable[Entity]*): Entities to add
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -640,6 +638,13 @@ Container for UI drawable elements. Supports iteration and indexing.
|
|||
|
||||
#### Methods
|
||||
|
||||
#### `append(drawable)`
|
||||
|
||||
Add a drawable element to the end of the collection.
|
||||
|
||||
**Arguments:**
|
||||
- `drawable` (*UIDrawable*): The drawable element to add
|
||||
|
||||
#### `remove(drawable)`
|
||||
|
||||
Remove the first occurrence of a drawable from the collection.
|
||||
|
|
@ -649,13 +654,6 @@ Remove the first occurrence of a drawable from the collection.
|
|||
|
||||
**Raises:** ValueError: If drawable is not in collection
|
||||
|
||||
#### `extend(iterable)`
|
||||
|
||||
Add all drawables from an iterable to the collection.
|
||||
|
||||
**Arguments:**
|
||||
- `iterable` (*Iterable[UIDrawable]*): Drawables to add
|
||||
|
||||
#### `count(drawable)`
|
||||
|
||||
Count the number of occurrences of a drawable in the collection.
|
||||
|
|
@ -676,12 +674,12 @@ Find the index of the first occurrence of a drawable.
|
|||
|
||||
**Raises:** ValueError: If drawable is not in collection
|
||||
|
||||
#### `append(drawable)`
|
||||
#### `extend(iterable)`
|
||||
|
||||
Add a drawable element to the end of the collection.
|
||||
Add all drawables from an iterable to the collection.
|
||||
|
||||
**Arguments:**
|
||||
- `drawable` (*UIDrawable*): The drawable element to add
|
||||
- `iterable` (*Iterable[UIDrawable]*): Drawables to add
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -705,17 +703,6 @@ RGBA color representation.
|
|||
|
||||
#### Methods
|
||||
|
||||
#### `to_hex()`
|
||||
|
||||
Convert this Color to a hexadecimal string.
|
||||
|
||||
**Returns:** str: Hex color string in format "#RRGGBB"
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
hex_str = color.to_hex() # Returns "#FF0000"
|
||||
```
|
||||
|
||||
#### `from_hex(hex_string)`
|
||||
|
||||
Create a Color from a hexadecimal color string.
|
||||
|
|
@ -730,6 +717,17 @@ Create a Color from a hexadecimal color string.
|
|||
red = Color.from_hex("#FF0000")
|
||||
```
|
||||
|
||||
#### `to_hex()`
|
||||
|
||||
Convert this Color to a hexadecimal string.
|
||||
|
||||
**Returns:** str: Hex color string in format "#RRGGBB"
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
hex_str = color.to_hex() # Returns "#FF0000"
|
||||
```
|
||||
|
||||
#### `lerp(other, t)`
|
||||
|
||||
Linearly interpolate between this color and another.
|
||||
|
|
@ -759,23 +757,6 @@ Calculate the length/magnitude of this vector.
|
|||
|
||||
**Returns:** float: The magnitude of the vector
|
||||
|
||||
#### `normalize()`
|
||||
|
||||
Return a unit vector in the same direction.
|
||||
|
||||
**Returns:** Vector: New normalized vector with magnitude 1.0
|
||||
|
||||
**Raises:** ValueError: If vector has zero magnitude
|
||||
|
||||
#### `dot(other)`
|
||||
|
||||
Calculate the dot product with another vector.
|
||||
|
||||
**Arguments:**
|
||||
- `other` (*Vector*): The other vector
|
||||
|
||||
**Returns:** float: Dot product of the two vectors
|
||||
|
||||
#### `distance_to(other)`
|
||||
|
||||
Calculate the distance to another vector.
|
||||
|
|
@ -785,11 +766,14 @@ Calculate the distance to another vector.
|
|||
|
||||
**Returns:** float: Distance between the two vectors
|
||||
|
||||
#### `copy()`
|
||||
#### `dot(other)`
|
||||
|
||||
Create a copy of this vector.
|
||||
Calculate the dot product with another vector.
|
||||
|
||||
**Returns:** Vector: New Vector object with same x and y values
|
||||
**Arguments:**
|
||||
- `other` (*Vector*): The other vector
|
||||
|
||||
**Returns:** float: Dot product of the two vectors
|
||||
|
||||
#### `angle()`
|
||||
|
||||
|
|
@ -805,6 +789,20 @@ Calculate the squared magnitude of this vector.
|
|||
|
||||
**Note:** Use this for comparisons to avoid expensive square root calculation.
|
||||
|
||||
#### `copy()`
|
||||
|
||||
Create a copy of this vector.
|
||||
|
||||
**Returns:** Vector: New Vector object with same x and y values
|
||||
|
||||
#### `normalize()`
|
||||
|
||||
Return a unit vector in the same direction.
|
||||
|
||||
**Returns:** Vector: New normalized vector with magnitude 1.0
|
||||
|
||||
**Raises:** ValueError: If vector has zero magnitude
|
||||
|
||||
---
|
||||
|
||||
### class `Texture`
|
||||
|
|
@ -836,12 +834,6 @@ Animate UI element properties over time.
|
|||
|
||||
#### Methods
|
||||
|
||||
#### `get_current_value()`
|
||||
|
||||
Get the current interpolated value of the animation.
|
||||
|
||||
**Returns:** float: Current animation value between start and end
|
||||
|
||||
#### `update(delta_time)`
|
||||
|
||||
Update the animation by the given time delta.
|
||||
|
|
@ -860,6 +852,12 @@ Start the animation on a target UI element.
|
|||
|
||||
**Note:** The target must have the property specified in the animation constructor.
|
||||
|
||||
#### `get_current_value()`
|
||||
|
||||
Get the current interpolated value of the animation.
|
||||
|
||||
**Returns:** float: Current animation value between start and end
|
||||
|
||||
---
|
||||
|
||||
### class `Drawable`
|
||||
|
|
@ -868,6 +866,14 @@ Base class for all drawable UI elements.
|
|||
|
||||
#### Methods
|
||||
|
||||
#### `get_bounds()`
|
||||
|
||||
Get the bounding rectangle of this drawable element.
|
||||
|
||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||
|
||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||
|
||||
#### `resize(width, height)`
|
||||
|
||||
Resize the element to new dimensions.
|
||||
|
|
@ -888,14 +894,6 @@ Move the element by a relative offset.
|
|||
|
||||
**Note:** This modifies the x and y position properties by the given amounts.
|
||||
|
||||
#### `get_bounds()`
|
||||
|
||||
Get the bounding rectangle of this drawable element.
|
||||
|
||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||
|
||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||
|
||||
---
|
||||
|
||||
### class `GridPoint`
|
||||
|
|
@ -947,18 +945,18 @@ def handle_keyboard(key, action):
|
|||
scene.register_keyboard(handle_keyboard)
|
||||
```
|
||||
|
||||
#### `get_ui()`
|
||||
|
||||
Get the UI element collection for this scene.
|
||||
|
||||
**Returns:** UICollection: Collection of all UI elements in this scene
|
||||
|
||||
#### `activate()`
|
||||
|
||||
Make this scene the active scene.
|
||||
|
||||
**Note:** Equivalent to calling setScene() with this scene's name.
|
||||
|
||||
#### `get_ui()`
|
||||
|
||||
Get the UI element collection for this scene.
|
||||
|
||||
**Returns:** UICollection: Collection of all UI elements in this scene
|
||||
|
||||
#### `keypress(handler)`
|
||||
|
||||
Register a keyboard handler function for this scene.
|
||||
|
|
@ -976,18 +974,6 @@ Timer object for scheduled callbacks.
|
|||
|
||||
#### Methods
|
||||
|
||||
#### `pause()`
|
||||
|
||||
Pause the timer, stopping its callback execution.
|
||||
|
||||
**Note:** Use resume() to continue the timer from where it was paused.
|
||||
|
||||
#### `resume()`
|
||||
|
||||
Resume a paused timer.
|
||||
|
||||
**Note:** Has no effect if timer is not paused.
|
||||
|
||||
#### `restart()`
|
||||
|
||||
Restart the timer from the beginning.
|
||||
|
|
@ -1000,6 +986,18 @@ Cancel the timer and remove it from the system.
|
|||
|
||||
**Note:** After cancelling, the timer object cannot be reused.
|
||||
|
||||
#### `pause()`
|
||||
|
||||
Pause the timer, stopping its callback execution.
|
||||
|
||||
**Note:** Use resume() to continue the timer from where it was paused.
|
||||
|
||||
#### `resume()`
|
||||
|
||||
Resume a paused timer.
|
||||
|
||||
**Note:** Has no effect if timer is not paused.
|
||||
|
||||
---
|
||||
|
||||
### class `Window`
|
||||
|
|
@ -1008,6 +1006,14 @@ Window singleton for accessing and modifying the game window properties.
|
|||
|
||||
#### Methods
|
||||
|
||||
#### `get()`
|
||||
|
||||
Get the Window singleton instance.
|
||||
|
||||
**Returns:** Window: The singleton window object
|
||||
|
||||
**Note:** This is a static method that returns the same instance every time.
|
||||
|
||||
#### `screenshot(filename)`
|
||||
|
||||
Take a screenshot and save it to a file.
|
||||
|
|
@ -1017,14 +1023,6 @@ Take a screenshot and save it to a file.
|
|||
|
||||
**Note:** Supports PNG, JPG, and BMP formats based on file extension.
|
||||
|
||||
#### `get()`
|
||||
|
||||
Get the Window singleton instance.
|
||||
|
||||
**Returns:** Window: The singleton window object
|
||||
|
||||
**Note:** This is a static method that returns the same instance every time.
|
||||
|
||||
#### `center()`
|
||||
|
||||
Center the window on the screen.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1070
docs/mcrfpy.3
1070
docs/mcrfpy.3
File diff suppressed because it is too large
Load Diff
|
|
@ -1,532 +1,209 @@
|
|||
"""Type stubs for McRogueFace Python API.
|
||||
|
||||
Core game engine interface for creating roguelike games with Python.
|
||||
Auto-generated - do not edit directly.
|
||||
"""
|
||||
|
||||
from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
|
||||
from typing import Any, List, Dict, Tuple, Optional, Callable, Union
|
||||
|
||||
# Type aliases
|
||||
UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid']
|
||||
Transition = Union[str, None]
|
||||
# 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 for RGBA colors."""
|
||||
|
||||
r: int
|
||||
g: int
|
||||
b: int
|
||||
a: int
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ...
|
||||
|
||||
def from_hex(self, hex_string: str) -> 'Color':
|
||||
"""Create color from hex string (e.g., '#FF0000' or 'FF0000')."""
|
||||
...
|
||||
|
||||
def to_hex(self) -> str:
|
||||
"""Convert color to hex string format."""
|
||||
...
|
||||
|
||||
def lerp(self, other: 'Color', t: float) -> 'Color':
|
||||
"""Linear interpolation between two colors."""
|
||||
...
|
||||
"""SFML Color Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Vector:
|
||||
"""SFML Vector Object for 2D coordinates."""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float, y: float) -> None: ...
|
||||
|
||||
def add(self, other: 'Vector') -> 'Vector': ...
|
||||
def subtract(self, other: 'Vector') -> 'Vector': ...
|
||||
def multiply(self, scalar: float) -> 'Vector': ...
|
||||
def divide(self, scalar: float) -> 'Vector': ...
|
||||
def distance(self, other: 'Vector') -> float: ...
|
||||
def normalize(self) -> 'Vector': ...
|
||||
def dot(self, other: 'Vector') -> float: ...
|
||||
|
||||
class Texture:
|
||||
"""SFML Texture Object for images."""
|
||||
|
||||
def __init__(self, filename: str) -> None: ...
|
||||
|
||||
filename: str
|
||||
width: int
|
||||
height: int
|
||||
sprite_count: int
|
||||
|
||||
class Font:
|
||||
"""SFML Font Object for text rendering."""
|
||||
|
||||
def __init__(self, filename: str) -> None: ...
|
||||
|
||||
filename: str
|
||||
family: str
|
||||
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."""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
visible: bool
|
||||
z_index: int
|
||||
name: str
|
||||
pos: Vector
|
||||
|
||||
def get_bounds(self) -> Tuple[float, float, float, float]:
|
||||
"""Get bounding box as (x, y, width, height)."""
|
||||
...
|
||||
|
||||
def move(self, dx: float, dy: float) -> None:
|
||||
"""Move by relative offset (dx, dy)."""
|
||||
...
|
||||
|
||||
def resize(self, width: float, height: float) -> None:
|
||||
"""Resize to new dimensions (width, height)."""
|
||||
...
|
||||
"""Base class for all drawable UI elements"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Frame(Drawable):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0,
|
||||
fill_color: Optional[Color] = None, outline_color: Optional[Color] = None,
|
||||
outline: float = 0, click: Optional[Callable] = None,
|
||||
children: Optional[List[UIElement]] = None) -> None: ...
|
||||
|
||||
w: float
|
||||
h: float
|
||||
fill_color: Color
|
||||
outline_color: Color
|
||||
outline: float
|
||||
click: Optional[Callable[[float, float, int], None]]
|
||||
children: 'UICollection'
|
||||
clip_children: bool
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Caption(Drawable):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, text: str = '', x: float = 0, y: float = 0,
|
||||
font: Optional[Font] = None, fill_color: Optional[Color] = None,
|
||||
outline_color: Optional[Color] = None, outline: float = 0,
|
||||
click: Optional[Callable] = None) -> None: ...
|
||||
|
||||
text: str
|
||||
font: Font
|
||||
fill_color: Color
|
||||
outline_color: Color
|
||||
outline: float
|
||||
click: Optional[Callable[[float, float, int], None]]
|
||||
w: float # Read-only, computed from text
|
||||
h: float # Read-only, computed from text
|
||||
class Entity:
|
||||
"""UIEntity objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Sprite(Drawable):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None,
|
||||
sprite_index: int = 0, scale: float = 1.0,
|
||||
click: Optional[Callable] = None) -> None: ...
|
||||
|
||||
texture: Texture
|
||||
sprite_index: int
|
||||
scale: float
|
||||
click: Optional[Callable[[float, float, int], None]]
|
||||
w: float # Read-only, computed from texture
|
||||
h: float # Read-only, computed from texture
|
||||
|
||||
class Grid(Drawable):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20),
|
||||
texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16,
|
||||
scale: float = 1.0, click: Optional[Callable] = None) -> None: ...
|
||||
|
||||
grid_size: Tuple[int, int]
|
||||
tile_width: int
|
||||
tile_height: int
|
||||
texture: Texture
|
||||
scale: float
|
||||
points: List[List['GridPoint']]
|
||||
entities: 'EntityCollection'
|
||||
background_color: Color
|
||||
click: Optional[Callable[[int, int, int], None]]
|
||||
|
||||
def at(self, x: int, y: int) -> 'GridPoint':
|
||||
"""Get grid point at tile coordinates."""
|
||||
...
|
||||
|
||||
class GridPoint:
|
||||
"""Grid point representing a single tile."""
|
||||
|
||||
texture_index: int
|
||||
solid: bool
|
||||
color: Color
|
||||
|
||||
class GridPointState:
|
||||
"""State information for a grid point."""
|
||||
|
||||
texture_index: int
|
||||
color: Color
|
||||
|
||||
class Entity(Drawable):
|
||||
"""Entity(grid_x=0, grid_y=0, texture=None, sprite_index=0, name='')
|
||||
|
||||
Game entity that lives within a Grid.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, grid_x: float = 0, grid_y: float = 0, texture: Optional[Texture] = None,
|
||||
sprite_index: int = 0, name: str = '') -> None: ...
|
||||
|
||||
grid_x: float
|
||||
grid_y: float
|
||||
texture: Texture
|
||||
sprite_index: int
|
||||
grid: Optional[Grid]
|
||||
|
||||
def at(self, grid_x: float, grid_y: float) -> None:
|
||||
"""Move entity to grid position."""
|
||||
...
|
||||
|
||||
def die(self) -> None:
|
||||
"""Remove entity from its grid."""
|
||||
...
|
||||
|
||||
def index(self) -> int:
|
||||
"""Get index in parent grid's entity collection."""
|
||||
...
|
||||
|
||||
class UICollection:
|
||||
"""Collection of UI drawable elements (Frame, Caption, Sprite, Grid)."""
|
||||
|
||||
def __len__(self) -> int: ...
|
||||
def __getitem__(self, index: int) -> UIElement: ...
|
||||
def __setitem__(self, index: int, value: UIElement) -> None: ...
|
||||
def __delitem__(self, index: int) -> None: ...
|
||||
def __contains__(self, item: UIElement) -> bool: ...
|
||||
def __iter__(self) -> Any: ...
|
||||
def __add__(self, other: 'UICollection') -> 'UICollection': ...
|
||||
def __iadd__(self, other: 'UICollection') -> 'UICollection': ...
|
||||
|
||||
def append(self, item: UIElement) -> None: ...
|
||||
def extend(self, items: List[UIElement]) -> None: ...
|
||||
def remove(self, item: UIElement) -> None: ...
|
||||
def index(self, item: UIElement) -> int: ...
|
||||
def count(self, item: UIElement) -> int: ...
|
||||
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:
|
||||
"""Collection of Entity objects."""
|
||||
|
||||
def __len__(self) -> int: ...
|
||||
def __getitem__(self, index: int) -> Entity: ...
|
||||
def __setitem__(self, index: int, value: Entity) -> None: ...
|
||||
def __delitem__(self, index: int) -> None: ...
|
||||
def __contains__(self, item: Entity) -> bool: ...
|
||||
def __iter__(self) -> Any: ...
|
||||
def __add__(self, other: 'EntityCollection') -> 'EntityCollection': ...
|
||||
def __iadd__(self, other: 'EntityCollection') -> 'EntityCollection': ...
|
||||
|
||||
def append(self, item: Entity) -> None: ...
|
||||
def extend(self, items: List[Entity]) -> None: ...
|
||||
def remove(self, item: Entity) -> None: ...
|
||||
def index(self, item: Entity) -> int: ...
|
||||
def count(self, item: Entity) -> int: ...
|
||||
"""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."""
|
||||
|
||||
name: str
|
||||
|
||||
def __init__(self, name: str) -> None: ...
|
||||
|
||||
def activate(self) -> None:
|
||||
"""Called when scene becomes active."""
|
||||
...
|
||||
|
||||
def deactivate(self) -> None:
|
||||
"""Called when scene becomes inactive."""
|
||||
...
|
||||
|
||||
def get_ui(self) -> UICollection:
|
||||
"""Get UI elements collection."""
|
||||
...
|
||||
|
||||
def on_keypress(self, key: str, pressed: bool) -> None:
|
||||
"""Handle keyboard events."""
|
||||
...
|
||||
|
||||
def on_click(self, x: float, y: float, button: int) -> None:
|
||||
"""Handle mouse clicks."""
|
||||
...
|
||||
|
||||
def on_enter(self) -> None:
|
||||
"""Called when entering the scene."""
|
||||
...
|
||||
|
||||
def on_exit(self) -> None:
|
||||
"""Called when leaving the scene."""
|
||||
...
|
||||
|
||||
def on_resize(self, width: int, height: int) -> None:
|
||||
"""Handle window resize events."""
|
||||
...
|
||||
|
||||
def update(self, dt: float) -> None:
|
||||
"""Update scene logic."""
|
||||
...
|
||||
"""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."""
|
||||
|
||||
name: str
|
||||
interval: int
|
||||
active: bool
|
||||
|
||||
def __init__(self, name: str, callback: Callable[[float], None], interval: int) -> None: ...
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pause the timer."""
|
||||
...
|
||||
|
||||
def resume(self) -> None:
|
||||
"""Resume the timer."""
|
||||
...
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""Cancel and remove the 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 managing the game window."""
|
||||
|
||||
resolution: Tuple[int, int]
|
||||
fullscreen: bool
|
||||
vsync: bool
|
||||
title: str
|
||||
fps_limit: int
|
||||
game_resolution: Tuple[int, int]
|
||||
scaling_mode: str
|
||||
|
||||
@staticmethod
|
||||
def get() -> 'Window':
|
||||
"""Get the window singleton instance."""
|
||||
...
|
||||
"""Window singleton for accessing and modifying the game window properties"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Animation:
|
||||
"""Animation object for animating UI properties."""
|
||||
|
||||
target: Any
|
||||
property: str
|
||||
duration: float
|
||||
easing: str
|
||||
loop: bool
|
||||
on_complete: Optional[Callable]
|
||||
|
||||
def __init__(self, target: Any, property: str, start_value: Any, end_value: Any,
|
||||
duration: float, easing: str = 'linear', loop: bool = False,
|
||||
on_complete: Optional[Callable] = None) -> None: ...
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the animation."""
|
||||
...
|
||||
|
||||
def update(self, dt: float) -> bool:
|
||||
"""Update animation, returns True if still running."""
|
||||
...
|
||||
|
||||
def get_current_value(self) -> Any:
|
||||
"""Get the current interpolated value."""
|
||||
...
|
||||
def center(self, *args, **kwargs) -> Any: ...
|
||||
def get(self, *args, **kwargs) -> Any: ...
|
||||
def screenshot(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
# Module functions
|
||||
# Functions
|
||||
|
||||
def createSoundBuffer(filename: str) -> int:
|
||||
"""Load a sound effect from a file and return its buffer ID."""
|
||||
...
|
||||
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: ...
|
||||
|
||||
def loadMusic(filename: str) -> None:
|
||||
"""Load and immediately play background music from a file."""
|
||||
...
|
||||
# Constants
|
||||
|
||||
def setMusicVolume(volume: int) -> None:
|
||||
"""Set the global music volume (0-100)."""
|
||||
...
|
||||
|
||||
def setSoundVolume(volume: int) -> None:
|
||||
"""Set the global sound effects volume (0-100)."""
|
||||
...
|
||||
|
||||
def playSound(buffer_id: int) -> None:
|
||||
"""Play a sound effect using a previously loaded buffer."""
|
||||
...
|
||||
|
||||
def getMusicVolume() -> int:
|
||||
"""Get the current music volume level (0-100)."""
|
||||
...
|
||||
|
||||
def getSoundVolume() -> int:
|
||||
"""Get the current sound effects volume level (0-100)."""
|
||||
...
|
||||
|
||||
def sceneUI(scene: Optional[str] = None) -> UICollection:
|
||||
"""Get all UI elements for a scene."""
|
||||
...
|
||||
|
||||
def currentScene() -> str:
|
||||
"""Get the name of the currently active scene."""
|
||||
...
|
||||
|
||||
def setScene(scene: str, transition: Optional[str] = None, duration: float = 0.0) -> None:
|
||||
"""Switch to a different scene with optional transition effect."""
|
||||
...
|
||||
|
||||
def createScene(name: str) -> None:
|
||||
"""Create a new empty scene."""
|
||||
...
|
||||
|
||||
def keypressScene(handler: Callable[[str, bool], None]) -> None:
|
||||
"""Set the keyboard event handler for the current scene."""
|
||||
...
|
||||
|
||||
def setTimer(name: str, handler: Callable[[float], None], interval: int) -> None:
|
||||
"""Create or update a recurring timer."""
|
||||
...
|
||||
|
||||
def delTimer(name: str) -> None:
|
||||
"""Stop and remove a timer."""
|
||||
...
|
||||
|
||||
def exit() -> None:
|
||||
"""Cleanly shut down the game engine and exit the application."""
|
||||
...
|
||||
|
||||
def setScale(multiplier: float) -> None:
|
||||
"""Scale the game window size (deprecated - use Window.resolution)."""
|
||||
...
|
||||
|
||||
def find(name: str, scene: Optional[str] = None) -> Optional[UIElement]:
|
||||
"""Find the first UI element with the specified name."""
|
||||
...
|
||||
|
||||
def findAll(pattern: str, scene: Optional[str] = None) -> List[UIElement]:
|
||||
"""Find all UI elements matching a name pattern (supports * wildcards)."""
|
||||
...
|
||||
|
||||
def getMetrics() -> Dict[str, Union[int, float]]:
|
||||
"""Get current performance metrics."""
|
||||
...
|
||||
|
||||
# Submodule
|
||||
class automation:
|
||||
"""Automation API for testing and scripting."""
|
||||
|
||||
@staticmethod
|
||||
def screenshot(filename: str) -> bool:
|
||||
"""Save a screenshot to the specified file."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def position() -> Tuple[int, int]:
|
||||
"""Get current mouse position as (x, y) tuple."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def size() -> Tuple[int, int]:
|
||||
"""Get screen size as (width, height) tuple."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def onScreen(x: int, y: int) -> bool:
|
||||
"""Check if coordinates are within screen bounds."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def moveTo(x: int, y: int, duration: float = 0.0) -> None:
|
||||
"""Move mouse to absolute position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def moveRel(xOffset: int, yOffset: int, duration: float = 0.0) -> None:
|
||||
"""Move mouse relative to current position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def dragTo(x: int, y: int, duration: float = 0.0, button: str = 'left') -> None:
|
||||
"""Drag mouse to position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def dragRel(xOffset: int, yOffset: int, duration: float = 0.0, button: str = 'left') -> None:
|
||||
"""Drag mouse relative to current position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def click(x: Optional[int] = None, y: Optional[int] = None, clicks: int = 1,
|
||||
interval: float = 0.0, button: str = 'left') -> None:
|
||||
"""Click mouse at position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def mouseDown(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
|
||||
"""Press mouse button down."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def mouseUp(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
|
||||
"""Release mouse button."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def keyDown(key: str) -> None:
|
||||
"""Press key down."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def keyUp(key: str) -> None:
|
||||
"""Release key."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def press(key: str) -> None:
|
||||
"""Press and release a key."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def typewrite(text: str, interval: float = 0.0) -> None:
|
||||
"""Type text with optional interval between characters."""
|
||||
...
|
||||
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
|
||||
Binary file not shown.
|
|
@ -1,393 +0,0 @@
|
|||
# McRogueFace Tutorial Parts 6-8: Implementation Plan
|
||||
|
||||
**Date**: Monday, July 28, 2025
|
||||
**Target Delivery**: Tuesday, July 29, 2025
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines the implementation plan for Parts 6-8 of the McRogueFace roguelike tutorial, adapting the libtcod Python tutorial to McRogueFace's architecture. The key discovery is that Python classes can successfully inherit from `mcrfpy.Entity` and store custom attributes, enabling a clean, Pythonic implementation.
|
||||
|
||||
## Key Architectural Insights
|
||||
|
||||
### Entity Inheritance Works!
|
||||
```python
|
||||
class GameEntity(mcrfpy.Entity):
|
||||
def __init__(self, x, y, **kwargs):
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
# Custom attributes work perfectly!
|
||||
self.hp = 10
|
||||
self.inventory = []
|
||||
self.any_attribute = "works"
|
||||
```
|
||||
|
||||
This completely changes our approach from wrapper patterns to direct inheritance.
|
||||
|
||||
---
|
||||
|
||||
## Part 6: Doing (and Taking) Some Damage
|
||||
|
||||
### Overview
|
||||
Implement a combat system with HP tracking, damage calculation, and death mechanics using entity inheritance.
|
||||
|
||||
### Core Components
|
||||
|
||||
#### 1. CombatEntity Base Class
|
||||
```python
|
||||
class CombatEntity(mcrfpy.Entity):
|
||||
"""Base class for entities that can fight and take damage"""
|
||||
def __init__(self, x, y, hp=10, defense=0, power=1, **kwargs):
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
# Combat stats as direct attributes
|
||||
self.hp = hp
|
||||
self.max_hp = hp
|
||||
self.defense = defense
|
||||
self.power = power
|
||||
self.is_alive = True
|
||||
self.blocks_movement = True
|
||||
|
||||
def calculate_damage(self, attacker):
|
||||
"""Simple damage formula: power - defense"""
|
||||
return max(0, attacker.power - self.defense)
|
||||
|
||||
def take_damage(self, damage, attacker=None):
|
||||
"""Apply damage and handle death"""
|
||||
self.hp = max(0, self.hp - damage)
|
||||
|
||||
if self.hp == 0 and self.is_alive:
|
||||
self.is_alive = False
|
||||
self.on_death(attacker)
|
||||
|
||||
def on_death(self, killer=None):
|
||||
"""Handle death - override in subclasses"""
|
||||
self.sprite_index = self.sprite_index + 180 # Corpse offset
|
||||
self.blocks_movement = False
|
||||
```
|
||||
|
||||
#### 2. Entity Types
|
||||
```python
|
||||
class PlayerEntity(CombatEntity):
|
||||
"""Player: HP=30, Defense=2, Power=5"""
|
||||
def __init__(self, x, y, **kwargs):
|
||||
kwargs['sprite_index'] = 64 # Hero sprite
|
||||
super().__init__(x=x, y=y, hp=30, defense=2, power=5, **kwargs)
|
||||
self.entity_type = "player"
|
||||
|
||||
class OrcEntity(CombatEntity):
|
||||
"""Orc: HP=10, Defense=0, Power=3"""
|
||||
def __init__(self, x, y, **kwargs):
|
||||
kwargs['sprite_index'] = 65 # Orc sprite
|
||||
super().__init__(x=x, y=y, hp=10, defense=0, power=3, **kwargs)
|
||||
self.entity_type = "orc"
|
||||
|
||||
class TrollEntity(CombatEntity):
|
||||
"""Troll: HP=16, Defense=1, Power=4"""
|
||||
def __init__(self, x, y, **kwargs):
|
||||
kwargs['sprite_index'] = 66 # Troll sprite
|
||||
super().__init__(x=x, y=y, hp=16, defense=1, power=4, **kwargs)
|
||||
self.entity_type = "troll"
|
||||
```
|
||||
|
||||
#### 3. Combat Integration
|
||||
- Extend `on_bump()` from Part 5 to include combat
|
||||
- Add attack animations (quick bump toward target)
|
||||
- Console messages initially, UI messages in Part 7
|
||||
- Death changes sprite and removes blocking
|
||||
|
||||
### Key Differences from Original Tutorial
|
||||
- No Fighter component - stats are direct attributes
|
||||
- No AI component - behavior in entity methods
|
||||
- Integrated animations for visual feedback
|
||||
- Simpler architecture overall
|
||||
|
||||
---
|
||||
|
||||
## Part 7: Creating the Interface
|
||||
|
||||
### Overview
|
||||
Add visual UI elements including health bars, message logs, and colored feedback for combat events.
|
||||
|
||||
### Core Components
|
||||
|
||||
#### 1. Health Bar
|
||||
```python
|
||||
class HealthBar:
|
||||
"""Health bar that reads entity HP directly"""
|
||||
def __init__(self, entity, pos=(10, 740), size=(200, 20)):
|
||||
self.entity = entity # Direct reference!
|
||||
|
||||
# Background (dark red)
|
||||
self.bg = mcrfpy.Frame(pos=pos, size=size)
|
||||
self.bg.fill_color = mcrfpy.Color(64, 16, 16)
|
||||
|
||||
# Foreground (green)
|
||||
self.fg = mcrfpy.Frame(pos=pos, size=size)
|
||||
self.fg.fill_color = mcrfpy.Color(0, 96, 0)
|
||||
|
||||
# Text overlay
|
||||
self.text = mcrfpy.Caption(
|
||||
pos=(pos[0] + 5, pos[1] + 2),
|
||||
text=f"HP: {entity.hp}/{entity.max_hp}"
|
||||
)
|
||||
|
||||
def update(self):
|
||||
"""Update based on entity's current HP"""
|
||||
ratio = self.entity.hp / self.entity.max_hp
|
||||
self.fg.w = int(self.bg.w * ratio)
|
||||
self.text.text = f"HP: {self.entity.hp}/{self.entity.max_hp}"
|
||||
|
||||
# Color changes at low health
|
||||
if ratio < 0.25:
|
||||
self.fg.fill_color = mcrfpy.Color(196, 16, 16) # Red
|
||||
elif ratio < 0.5:
|
||||
self.fg.fill_color = mcrfpy.Color(196, 196, 16) # Yellow
|
||||
```
|
||||
|
||||
#### 2. Message Log
|
||||
```python
|
||||
class MessageLog:
|
||||
"""Scrolling message log for combat feedback"""
|
||||
def __init__(self, pos=(10, 600), size=(400, 120), max_messages=6):
|
||||
self.frame = mcrfpy.Frame(pos=pos, size=size)
|
||||
self.messages = [] # List of (text, color) tuples
|
||||
self.captions = [] # Pre-allocated Caption pool
|
||||
|
||||
def add_message(self, text, color=None):
|
||||
"""Add message with optional color"""
|
||||
# Handle duplicate detection (x2, x3, etc.)
|
||||
# Update caption display
|
||||
```
|
||||
|
||||
#### 3. Color System
|
||||
```python
|
||||
class Colors:
|
||||
# Combat colors
|
||||
PLAYER_ATTACK = mcrfpy.Color(224, 224, 224)
|
||||
ENEMY_ATTACK = mcrfpy.Color(255, 192, 192)
|
||||
PLAYER_DEATH = mcrfpy.Color(255, 48, 48)
|
||||
ENEMY_DEATH = mcrfpy.Color(255, 160, 48)
|
||||
HEALTH_RECOVERED = mcrfpy.Color(0, 255, 0)
|
||||
```
|
||||
|
||||
### UI Layout
|
||||
- Health bar at bottom of screen
|
||||
- Message log above health bar
|
||||
- Direct binding to entity attributes
|
||||
- Real-time updates during gameplay
|
||||
|
||||
---
|
||||
|
||||
## Part 8: Items and Inventory
|
||||
|
||||
### Overview
|
||||
Implement items as entities, inventory management, and a hotbar-style UI for item usage.
|
||||
|
||||
### Core Components
|
||||
|
||||
#### 1. Item Entities
|
||||
```python
|
||||
class ItemEntity(mcrfpy.Entity):
|
||||
"""Base class for pickupable items"""
|
||||
def __init__(self, x, y, name, sprite, **kwargs):
|
||||
kwargs['sprite_index'] = sprite
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
self.item_name = name
|
||||
self.blocks_movement = False
|
||||
self.item_type = "generic"
|
||||
|
||||
class HealingPotion(ItemEntity):
|
||||
"""Consumable healing item"""
|
||||
def __init__(self, x, y, healing_amount=4):
|
||||
super().__init__(x, y, "Healing Potion", sprite=33)
|
||||
self.healing_amount = healing_amount
|
||||
self.item_type = "consumable"
|
||||
|
||||
def use(self, user):
|
||||
"""Use the potion - returns (success, message)"""
|
||||
if hasattr(user, 'hp'):
|
||||
healed = min(self.healing_amount, user.max_hp - user.hp)
|
||||
if healed > 0:
|
||||
user.hp += healed
|
||||
return True, f"You heal {healed} HP!"
|
||||
```
|
||||
|
||||
#### 2. Inventory System
|
||||
```python
|
||||
class InventoryMixin:
|
||||
"""Mixin for entities with inventory"""
|
||||
def __init__(self, *args, capacity=10, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.inventory = []
|
||||
self.inventory_capacity = capacity
|
||||
|
||||
def pickup_item(self, item):
|
||||
"""Pick up an item entity"""
|
||||
if len(self.inventory) >= self.inventory_capacity:
|
||||
return False, "Inventory full!"
|
||||
self.inventory.append(item)
|
||||
item.die() # Remove from grid
|
||||
return True, f"Picked up {item.item_name}."
|
||||
```
|
||||
|
||||
#### 3. Inventory UI
|
||||
```python
|
||||
class InventoryDisplay:
|
||||
"""Hotbar-style inventory display"""
|
||||
def __init__(self, entity, pos=(200, 700), slots=10):
|
||||
# Create slot frames and sprites
|
||||
# Number keys 1-9, 0 for slots
|
||||
# Highlight selected slot
|
||||
# Update based on entity.inventory
|
||||
```
|
||||
|
||||
### Key Features
|
||||
- Items exist as entities on the grid
|
||||
- Direct inventory attribute on player
|
||||
- Hotkey-based usage (1-9, 0)
|
||||
- Visual hotbar display
|
||||
- Item effects (healing, future: damage boost, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
### Tuesday Morning (Priority 1: Core Systems)
|
||||
1. **8:00-9:30**: Implement CombatEntity and entity types
|
||||
2. **9:30-10:30**: Add combat to bump interactions
|
||||
3. **10:30-11:30**: Basic health display (text or simple bar)
|
||||
4. **11:30-12:00**: ItemEntity and pickup system
|
||||
|
||||
### Tuesday Afternoon (Priority 2: Integration)
|
||||
1. **1:00-2:00**: Message log implementation
|
||||
2. **2:00-3:00**: Full health bar with colors
|
||||
3. **3:00-4:00**: Inventory UI (hotbar)
|
||||
4. **4:00-5:00**: Testing and bug fixes
|
||||
|
||||
### Tuesday Evening (Priority 3: Polish)
|
||||
1. **5:00-6:00**: Combat animations and effects
|
||||
2. **6:00-7:00**: Sound integration (use CoS splat sounds)
|
||||
3. **7:00-8:00**: Additional item types
|
||||
4. **8:00-9:00**: Documentation and cleanup
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Automated Tests
|
||||
```python
|
||||
# tests/test_part6_combat.py
|
||||
- Test damage calculation
|
||||
- Test death mechanics
|
||||
- Test combat messages
|
||||
|
||||
# tests/test_part7_ui.py
|
||||
- Test health bar updates
|
||||
- Test message log scrolling
|
||||
- Test color system
|
||||
|
||||
# tests/test_part8_inventory.py
|
||||
- Test item pickup/drop
|
||||
- Test inventory capacity
|
||||
- Test item usage
|
||||
```
|
||||
|
||||
### Visual Tests
|
||||
- Screenshot combat states
|
||||
- Verify UI element positioning
|
||||
- Check animation smoothness
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
```
|
||||
roguelike_tutorial/
|
||||
├── part_6.py # Combat implementation
|
||||
├── part_7.py # UI enhancements
|
||||
├── part_8.py # Inventory system
|
||||
├── combat.py # Shared combat utilities
|
||||
├── ui_components.py # Reusable UI classes
|
||||
├── colors.py # Color definitions
|
||||
└── items.py # Item definitions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
### Potential Issues
|
||||
1. **Performance**: Many UI updates per frame
|
||||
- Solution: Update only on state changes
|
||||
|
||||
2. **Entity Collection Bugs**: Known segfault issues
|
||||
- Solution: Use index-based access when needed
|
||||
|
||||
3. **Animation Timing**: Complex with turn-based combat
|
||||
- Solution: Queue animations, process sequentially
|
||||
|
||||
### Fallback Options
|
||||
1. Start with console messages, add UI later
|
||||
2. Simple health numbers before bars
|
||||
3. Basic inventory list before hotbar
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Part 6
|
||||
- [x] Entities can have HP and take damage
|
||||
- [x] Death changes sprite and walkability
|
||||
- [x] Combat messages appear
|
||||
- [x] Player can kill enemies
|
||||
|
||||
### Part 7
|
||||
- [x] Health bar shows current/max HP
|
||||
- [x] Messages appear in scrolling log
|
||||
- [x] Colors differentiate message types
|
||||
- [x] UI updates in real-time
|
||||
|
||||
### Part 8
|
||||
- [x] Items can be picked up
|
||||
- [x] Inventory has capacity limit
|
||||
- [x] Items can be used/consumed
|
||||
- [x] Hotbar shows inventory items
|
||||
|
||||
---
|
||||
|
||||
## Notes for Implementation
|
||||
|
||||
1. **Keep It Simple**: Start with minimum viable features
|
||||
2. **Build Incrementally**: Test each component before integrating
|
||||
3. **Use Part 5**: Leverage existing entity interaction system
|
||||
4. **Document Well**: Clear comments for tutorial purposes
|
||||
5. **Visual Feedback**: McRogueFace excels at animations - use them!
|
||||
|
||||
---
|
||||
|
||||
## Comparison with Original Tutorial
|
||||
|
||||
### What We Keep
|
||||
- Same combat formula (power - defense)
|
||||
- Same entity stats (Player, Orc, Troll)
|
||||
- Same item types (healing potions to start)
|
||||
- Same UI elements (health bar, message log)
|
||||
|
||||
### What's Different
|
||||
- Direct inheritance instead of components
|
||||
- Integrated animations and visual effects
|
||||
- Hotbar inventory instead of menu
|
||||
- Built-in sound support
|
||||
- Cleaner architecture overall
|
||||
|
||||
### What's Better
|
||||
- More Pythonic with real inheritance
|
||||
- Better visual feedback
|
||||
- Smoother animations
|
||||
- Simpler to understand
|
||||
- Leverages McRogueFace's strengths
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
This implementation plan leverages McRogueFace's support for Python entity inheritance to create a clean, intuitive tutorial series. By using direct attributes instead of components, we simplify the architecture while maintaining all the functionality of the original tutorial. The addition of animations, sound effects, and rich UI elements showcases McRogueFace's capabilities while keeping the code beginner-friendly.
|
||||
|
||||
The Tuesday delivery timeline is aggressive but achievable by focusing on core functionality first, then integration, then polish. The modular design allows for easy testing and incremental development.
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
# Simple TCOD Tutorial Part 1 - Drawing the player sprite and moving it around
|
||||
|
||||
This is Part 1 of the Simple TCOD Tutorial adapted for McRogueFace. It implements the sophisticated, refactored TCOD tutorial approach with professional architecture from day one.
|
||||
|
||||
## Running the Code
|
||||
|
||||
From your tutorial build directory (separate from the engine development build):
|
||||
```bash
|
||||
cd simple_tcod_tutorial/build
|
||||
./mcrogueface scripts/main.py
|
||||
```
|
||||
|
||||
Note: The `scripts` folder should be a symlink to your `simple_tcod_tutorial` directory.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Package Structure
|
||||
```
|
||||
simple_tcod_tutorial/
|
||||
├── main.py # Entry point - ties everything together
|
||||
├── game/ # Game package with proper separation
|
||||
│ ├── __init__.py
|
||||
│ ├── entity.py # Entity class - all game objects
|
||||
│ ├── engine.py # Engine class - game coordinator
|
||||
│ ├── actions.py # Action classes - command pattern
|
||||
│ └── input_handlers.py # Input handling - extensible system
|
||||
```
|
||||
|
||||
### Key Concepts Demonstrated
|
||||
|
||||
1. **Entity-Centric Design**
|
||||
- Everything in the game is an Entity
|
||||
- Entities have position, appearance, and behavior
|
||||
- Designed to scale to items, NPCs, and effects
|
||||
|
||||
2. **Action-Based Command Pattern**
|
||||
- All player actions are Action objects
|
||||
- Separates input from game logic
|
||||
- Enables undo, replay, and AI using same system
|
||||
|
||||
3. **Professional Input Handling**
|
||||
- BaseEventHandler for different input contexts
|
||||
- Complete movement key support (arrows, numpad, vi, WASD)
|
||||
- Ready for menus, targeting, and other modes
|
||||
|
||||
4. **Engine as Coordinator**
|
||||
- Manages game state without becoming a god object
|
||||
- Delegates to appropriate systems
|
||||
- Clean boundaries between systems
|
||||
|
||||
5. **Type Safety**
|
||||
- Full type annotations throughout
|
||||
- Forward references with TYPE_CHECKING
|
||||
- Modern Python best practices
|
||||
|
||||
## Differences from Vanilla McRogueFace Tutorial
|
||||
|
||||
### Removed
|
||||
- Animation system (instant movement instead)
|
||||
- Complex UI elements (focus on core mechanics)
|
||||
- Real-time features (pure turn-based)
|
||||
- Visual effects (camera following, smooth scrolling)
|
||||
- Entity color property (sprites handle appearance)
|
||||
|
||||
### Added
|
||||
- Complete movement key support
|
||||
- Professional architecture patterns
|
||||
- Proper package structure
|
||||
- Type annotations
|
||||
- Action-based design
|
||||
- Extensible handler system
|
||||
- Proper exit handling (Escape/Q actually quits)
|
||||
|
||||
### Adapted
|
||||
- Grid rendering with proper centering
|
||||
- Simplified entity system (position + sprite ID)
|
||||
- Using simple_tutorial.png sprite sheet (12 sprites)
|
||||
- Floor tiles using ground sprites (indices 1 and 2)
|
||||
- Direct sprite indices instead of character mapping
|
||||
|
||||
## Learning Objectives
|
||||
|
||||
Students completing Part 1 will understand:
|
||||
- How to structure a game project professionally
|
||||
- The value of entity-centric design
|
||||
- Command pattern for game actions
|
||||
- Input handling that scales to complex UIs
|
||||
- Type-driven development in Python
|
||||
- Architecture that grows without refactoring
|
||||
|
||||
## What's Next
|
||||
|
||||
Part 2 will add:
|
||||
- The GameMap class for world representation
|
||||
- Tile-based movement and collision
|
||||
- Multiple entities in the world
|
||||
- Basic terrain (walls and floors)
|
||||
- Rendering order for entities
|
||||
|
||||
The architecture we've built in Part 1 makes these additions natural and painless, demonstrating the value of starting with good patterns.
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
# Simple TCOD Tutorial Part 2 - The generic Entity, the map, and walls
|
||||
|
||||
This is Part 2 of the Simple TCOD Tutorial adapted for McRogueFace. Building on Part 1's foundation, we now introduce proper world representation and collision detection.
|
||||
|
||||
## Running the Code
|
||||
|
||||
From your tutorial build directory:
|
||||
```bash
|
||||
cd simple_tcod_tutorial/build
|
||||
./mcrogueface scripts/main.py
|
||||
```
|
||||
|
||||
## New Architecture Components
|
||||
|
||||
### GameMap Class (`game/game_map.py`)
|
||||
The GameMap inherits from `mcrfpy.Grid` and adds:
|
||||
- **Tile Management**: Uses Grid's built-in point system with walkable property
|
||||
- **Entity Container**: Manages entity lifecycle with `add_entity()` and `remove_entity()`
|
||||
- **Spatial Queries**: `get_entities_at()`, `get_blocking_entity_at()`, `is_walkable()`
|
||||
- **Direct Integration**: Leverages Grid's walkable and tilesprite properties
|
||||
|
||||
### Tiles System (`game/tiles.py`)
|
||||
- **Simple Tile Types**: Using NamedTuple for clean tile definitions
|
||||
- **Tile Types**: Floor (walkable) and Wall (blocks movement)
|
||||
- **Grid Integration**: Maps directly to Grid point properties
|
||||
- **Future-Ready**: Includes transparency for FOV system in Part 4
|
||||
|
||||
### Entity Placement System
|
||||
- **Bidirectional References**: Entities know their map, maps track their entities
|
||||
- **`place()` Method**: Handles all bookkeeping when entities move between maps
|
||||
- **Lifecycle Management**: Automatic cleanup when entities leave maps
|
||||
|
||||
## Key Changes from Part 1
|
||||
|
||||
### Engine Updates
|
||||
- Replaced direct grid management with GameMap
|
||||
- Engine creates and configures the GameMap
|
||||
- Player is placed using the new `place()` method
|
||||
|
||||
### Movement System
|
||||
- MovementAction now checks `is_walkable()` before moving
|
||||
- Collision detection for both walls and blocking entities
|
||||
- Clean separation between validation and execution
|
||||
|
||||
### Visual Changes
|
||||
- Walls rendered as trees (sprite index 3)
|
||||
- Border of walls around the map edge
|
||||
- Floor tiles still use alternating pattern
|
||||
|
||||
## Architectural Benefits
|
||||
|
||||
### McRogueFace Integration
|
||||
- **No NumPy Dependency**: Uses Grid's native tile management
|
||||
- **Direct Walkability**: Grid points have built-in walkable property
|
||||
- **Unified System**: Visual and logical tile data in one place
|
||||
|
||||
### Separation of Concerns
|
||||
- **GameMap**: Knows about tiles and spatial relationships
|
||||
- **Engine**: Coordinates high-level game state
|
||||
- **Entity**: Manages its own lifecycle through `place()`
|
||||
- **Actions**: Validate their own preconditions
|
||||
|
||||
### Extensibility
|
||||
- Easy to add new tile types
|
||||
- Simple to implement different map generation
|
||||
- Ready for FOV, pathfinding, and complex queries
|
||||
- Entity system scales to items and NPCs
|
||||
|
||||
### Type Safety
|
||||
- TYPE_CHECKING imports prevent circular dependencies
|
||||
- Proper type hints throughout
|
||||
- Forward references maintain clean architecture
|
||||
|
||||
## What's Next
|
||||
|
||||
Part 3 will add:
|
||||
- Procedural dungeon generation
|
||||
- Room and corridor creation
|
||||
- Multiple entities in the world
|
||||
- Foundation for enemy placement
|
||||
|
||||
The architecture established in Part 2 makes these additions straightforward, demonstrating the value of proper design from the beginning.
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
# Simple TCOD Tutorial Part 3 - Generating a dungeon
|
||||
|
||||
This is Part 3 of the Simple TCOD Tutorial adapted for McRogueFace. We now add procedural dungeon generation to create interesting, playable levels.
|
||||
|
||||
## Running the Code
|
||||
|
||||
From your tutorial build directory:
|
||||
```bash
|
||||
cd simple_tcod_tutorial/build
|
||||
./mcrogueface scripts/main.py
|
||||
```
|
||||
|
||||
## New Features
|
||||
|
||||
### Procedural Generation Module (`game/procgen.py`)
|
||||
|
||||
This dedicated module demonstrates separation of concerns - dungeon generation logic is kept separate from the game map implementation.
|
||||
|
||||
#### RectangularRoom Class
|
||||
- **Clean Abstraction**: Represents a room with position and dimensions
|
||||
- **Utility Properties**:
|
||||
- `center` - Returns room center for connections
|
||||
- `inner` - Returns slice objects for efficient carving
|
||||
- **Intersection Detection**: `intersects()` method prevents overlapping rooms
|
||||
|
||||
#### Tunnel Generation
|
||||
- **L-Shaped Corridors**: Simple but effective connection method
|
||||
- **Iterator Pattern**: `tunnel_between()` yields coordinates efficiently
|
||||
- **Random Variation**: 50/50 chance of horizontal-first vs vertical-first
|
||||
|
||||
#### Dungeon Generation Algorithm
|
||||
```python
|
||||
def generate_dungeon(max_rooms, room_min_size, room_max_size,
|
||||
map_width, map_height, engine) -> GameMap:
|
||||
```
|
||||
- **Simple Algorithm**: Try to place random rooms, reject overlaps
|
||||
- **Automatic Connection**: Each room connects to the previous one
|
||||
- **Player Placement**: First room contains the player
|
||||
- **Entity-Centric**: Uses `player.place()` for proper lifecycle
|
||||
|
||||
## Architecture Benefits
|
||||
|
||||
### Modular Design
|
||||
- Generation logic separate from GameMap
|
||||
- Easy to swap algorithms later
|
||||
- Room class reusable for other features
|
||||
|
||||
### Forward Thinking
|
||||
- Engine parameter anticipates entity spawning
|
||||
- Room list available for future features
|
||||
- Iterator-based tunnel generation is memory efficient
|
||||
|
||||
### Clean Integration
|
||||
- Works seamlessly with existing entity placement
|
||||
- Respects GameMap's tile management
|
||||
- No special cases or hacks needed
|
||||
|
||||
## Visual Changes
|
||||
|
||||
- Map size increased to 80x45 for better dungeons
|
||||
- Zoom reduced to 1.0 to see more of the map
|
||||
- Random room layouts each time
|
||||
- Connected rooms and corridors
|
||||
|
||||
## Algorithm Details
|
||||
|
||||
The generation follows these steps:
|
||||
1. Start with a map filled with walls
|
||||
2. Try to place up to `max_rooms` rooms
|
||||
3. For each room attempt:
|
||||
- Generate random size and position
|
||||
- Check for intersections with existing rooms
|
||||
- If valid, carve out the room
|
||||
- Connect to previous room (if any)
|
||||
4. Place player in center of first room
|
||||
|
||||
This simple algorithm creates playable dungeons while being easy to understand and modify.
|
||||
|
||||
## What's Next
|
||||
|
||||
Part 4 will add:
|
||||
- Field of View (FOV) system
|
||||
- Explored vs unexplored areas
|
||||
- Light and dark tile rendering
|
||||
- Torch radius around player
|
||||
|
||||
The modular dungeon generation makes it easy to add these visual features without touching the generation code.
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
# Part 4: Field of View and Exploration
|
||||
|
||||
## Overview
|
||||
|
||||
Part 4 introduces the Field of View (FOV) system, transforming our fully-visible dungeon into an atmospheric exploration experience. We leverage McRogueFace's built-in FOV capabilities and perspective system for efficient rendering.
|
||||
|
||||
## What's New in Part 4
|
||||
|
||||
### Field of View System
|
||||
- **FOV Calculation**: Using `Grid.compute_fov()` with configurable radius
|
||||
- **Perspective System**: Grid tracks which entity is the viewer
|
||||
- **Visibility States**: Unexplored (black), explored (dark), visible (lit)
|
||||
- **Automatic Updates**: FOV recalculates on player movement
|
||||
|
||||
### Implementation Details
|
||||
|
||||
#### FOV with McRogueFace's Grid
|
||||
|
||||
Unlike TCOD which uses numpy arrays for visibility tracking, McRogueFace's Grid has built-in FOV support:
|
||||
|
||||
```python
|
||||
# In GameMap.update_fov()
|
||||
self.compute_fov(viewer_x, viewer_y, radius, light_walls=True, algorithm=mcrfpy.FOV_BASIC)
|
||||
```
|
||||
|
||||
The Grid automatically:
|
||||
- Tracks which tiles have been explored
|
||||
- Applies appropriate color overlays (shroud, dark, light)
|
||||
- Updates entity visibility based on FOV
|
||||
|
||||
#### Perspective System
|
||||
|
||||
McRogueFace uses a perspective-based rendering approach:
|
||||
|
||||
```python
|
||||
# Set the viewer
|
||||
self.game_map.perspective = self.player
|
||||
|
||||
# Grid automatically renders from this entity's viewpoint
|
||||
```
|
||||
|
||||
This is more efficient than manually updating tile colors every turn.
|
||||
|
||||
#### Color Overlays
|
||||
|
||||
We define overlay colors but let the Grid handle application:
|
||||
|
||||
```python
|
||||
# In tiles.py
|
||||
SHROUD = mcrfpy.Color(0, 0, 0, 255) # Unexplored
|
||||
DARK = mcrfpy.Color(100, 100, 150, 128) # Explored but not visible
|
||||
LIGHT = mcrfpy.Color(255, 255, 255, 0) # Currently visible
|
||||
```
|
||||
|
||||
### Key Differences from TCOD
|
||||
|
||||
| TCOD Approach | McRogueFace Approach |
|
||||
|---------------|----------------------|
|
||||
| `visible` and `explored` numpy arrays | Grid's built-in FOV state |
|
||||
| Manual tile color switching | Automatic overlay system |
|
||||
| `tcod.map.compute_fov()` | `Grid.compute_fov()` |
|
||||
| Render conditionals for each tile | Perspective-based rendering |
|
||||
|
||||
### Movement and FOV Updates
|
||||
|
||||
The action system now updates FOV after player movement:
|
||||
|
||||
```python
|
||||
# In MovementAction.perform()
|
||||
if self.entity == engine.player:
|
||||
engine.update_fov()
|
||||
```
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### Why Grid Perspective?
|
||||
|
||||
The perspective system provides several benefits:
|
||||
1. **Efficiency**: No per-tile color updates needed
|
||||
2. **Flexibility**: Easy to switch viewpoints (for debugging or features)
|
||||
3. **Automatic**: Grid handles all rendering details
|
||||
4. **Clean**: Separates game logic from rendering concerns
|
||||
|
||||
### Entity Visibility
|
||||
|
||||
Entities automatically update their visibility state:
|
||||
|
||||
```python
|
||||
# After FOV calculation
|
||||
self.player.update_visibility()
|
||||
```
|
||||
|
||||
This ensures entities are only rendered when visible to the current perspective.
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `game/tiles.py`: Added FOV color overlay constants
|
||||
- `game/game_map.py`: Added `update_fov()` method
|
||||
- `game/engine.py`: Added FOV initialization and update method
|
||||
- `game/actions.py`: Update FOV after player movement
|
||||
- `main.py`: Updated part description
|
||||
|
||||
## What's Next
|
||||
|
||||
Part 5 will add enemies to our dungeon, introducing:
|
||||
- Enemy entities with AI
|
||||
- Combat system
|
||||
- Turn-based gameplay
|
||||
- Health and damage
|
||||
|
||||
The FOV system will make enemies appear and disappear as you explore, adding tension and strategy to the gameplay.
|
||||
|
||||
## Learning Points
|
||||
|
||||
1. **Leverage Framework Features**: Use McRogueFace's built-in systems rather than reimplementing
|
||||
2. **Perspective-Based Design**: Think in terms of viewpoints, not global state
|
||||
3. **Automatic Systems**: Let the framework handle rendering details
|
||||
4. **Clean Integration**: FOV updates fit naturally into the action system
|
||||
|
||||
## Running Part 4
|
||||
|
||||
```bash
|
||||
cd simple_tcod_tutorial/build
|
||||
./mcrogueface scripts/main.py
|
||||
```
|
||||
|
||||
You'll now see:
|
||||
- Black unexplored areas
|
||||
- Dark blue tint on previously seen areas
|
||||
- Full brightness only in your field of view
|
||||
- Smooth exploration as you move through the dungeon
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
# Part 5: Placing Enemies and Fighting Them
|
||||
|
||||
## Overview
|
||||
|
||||
Part 5 brings our dungeon to life with enemies! We add rats and spiders that populate the rooms, implement a combat system with melee attacks, and handle entity death by turning creatures into gravestones.
|
||||
|
||||
## What's New in Part 5
|
||||
|
||||
### Actor System
|
||||
- **Actor Class**: Extends Entity with combat stats (HP, defense, power)
|
||||
- **Combat Properties**: Health tracking, damage calculation, alive status
|
||||
- **Death Handling**: Entities become gravestones when killed
|
||||
|
||||
### Enemy Types
|
||||
Using our sprite sheet, we have two enemy types:
|
||||
- **Rat** (sprite 5): 10 HP, 0 defense, 3 power - Common enemy
|
||||
- **Spider** (sprite 4): 16 HP, 1 defense, 4 power - Tougher enemy
|
||||
|
||||
### Combat System
|
||||
|
||||
#### Bump-to-Attack
|
||||
When the player tries to move into an enemy:
|
||||
```python
|
||||
# In MovementAction.perform()
|
||||
target = engine.game_map.get_blocking_entity_at(dest_x, dest_y)
|
||||
if target:
|
||||
if self.entity == engine.player:
|
||||
from game.entity import Actor
|
||||
if isinstance(target, Actor) and target != engine.player:
|
||||
return MeleeAction(self.entity, self.dx, self.dy).perform(engine)
|
||||
```
|
||||
|
||||
#### Damage Calculation
|
||||
Simple formula with defense reduction:
|
||||
```python
|
||||
damage = attacker.power - target.defense
|
||||
```
|
||||
|
||||
#### Death System
|
||||
Dead entities become gravestones:
|
||||
```python
|
||||
def die(self) -> None:
|
||||
"""Handle death by becoming a gravestone."""
|
||||
self.sprite_index = 6 # Tombstone sprite
|
||||
self.blocks_movement = False
|
||||
self.name = f"Grave of {self.name}"
|
||||
```
|
||||
|
||||
### Entity Factories
|
||||
|
||||
Factory functions create pre-configured entities:
|
||||
```python
|
||||
def rat(x: int, y: int, texture: mcrfpy.Texture) -> Actor:
|
||||
return Actor(
|
||||
x=x, y=y,
|
||||
sprite_id=5, # Rat sprite
|
||||
texture=texture,
|
||||
name="Rat",
|
||||
hp=10, defense=0, power=3,
|
||||
)
|
||||
```
|
||||
|
||||
### Dungeon Population
|
||||
|
||||
Enemies are placed randomly in rooms:
|
||||
```python
|
||||
def place_entities(room, dungeon, max_monsters, texture):
|
||||
number_of_monsters = random.randint(0, max_monsters)
|
||||
|
||||
for _ in range(number_of_monsters):
|
||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||||
|
||||
if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
|
||||
# 80% rats, 20% spiders
|
||||
if random.random() < 0.8:
|
||||
monster = entity_factories.rat(x, y, texture)
|
||||
else:
|
||||
monster = entity_factories.spider(x, y, texture)
|
||||
monster.place(x, y, dungeon)
|
||||
```
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
### FOV and Enemy Visibility
|
||||
Enemies are automatically shown/hidden by the FOV system:
|
||||
```python
|
||||
def update_fov(self) -> None:
|
||||
# Update visibility for all entities
|
||||
for entity in self.game_map.entities:
|
||||
entity.update_visibility()
|
||||
```
|
||||
|
||||
### Action System Extension
|
||||
The action system now handles combat:
|
||||
- **MovementAction**: Detects collision, triggers attack
|
||||
- **MeleeAction**: New action for melee combat
|
||||
- Actions remain decoupled from entity logic
|
||||
|
||||
### Gravestone System
|
||||
Instead of removing dead entities:
|
||||
- Sprite changes to tombstone (index 6)
|
||||
- Name changes to "Grave of [Name]"
|
||||
- No longer blocks movement
|
||||
- Remains visible as dungeon decoration
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### Why Actor Extends Entity?
|
||||
- Maintains entity hierarchy
|
||||
- Combat stats only for creatures
|
||||
- Future items/decorations won't have HP
|
||||
- Clean separation of concerns
|
||||
|
||||
### Why Factory Functions?
|
||||
- Centralized entity configuration
|
||||
- Easy to add new enemy types
|
||||
- Consistent stat management
|
||||
- Type-safe entity creation
|
||||
|
||||
### Combat in Actions
|
||||
Combat logic lives in actions, not entities:
|
||||
- Entities store stats
|
||||
- Actions perform combat
|
||||
- Clean separation of data and behavior
|
||||
- Extensible for future combat types
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `game/entity.py`: Added Actor class with combat stats and death handling
|
||||
- `game/entity_factories.py`: New module with entity creation functions
|
||||
- `game/actions.py`: Added MeleeAction for combat
|
||||
- `game/procgen.py`: Added enemy placement in rooms
|
||||
- `game/engine.py`: Updated to use Actor type and handle all entity visibility
|
||||
- `main.py`: Updated to use entity factories and Part 5 description
|
||||
|
||||
## What's Next
|
||||
|
||||
Part 6 will enhance the combat experience with:
|
||||
- Health display UI
|
||||
- Game over conditions
|
||||
- Combat messages window
|
||||
- More strategic combat mechanics
|
||||
|
||||
## Learning Points
|
||||
|
||||
1. **Entity Specialization**: Use inheritance to add features to specific entity types
|
||||
2. **Factory Pattern**: Centralize object creation for consistency
|
||||
3. **State Transformation**: Dead entities become decorations, not deletions
|
||||
4. **Action Extensions**: Combat fits naturally into the action system
|
||||
5. **Automatic Systems**: FOV handles entity visibility without special code
|
||||
|
||||
## Running Part 5
|
||||
|
||||
```bash
|
||||
cd simple_tcod_tutorial/build
|
||||
./mcrogueface scripts/main.py
|
||||
```
|
||||
|
||||
You'll now encounter rats and spiders as you explore! Walk into them to attack. Dead enemies become gravestones that mark your battles.
|
||||
|
||||
## Sprite Adaptations
|
||||
|
||||
Following our sprite sheet (`sprite_sheet.md`), we made these thematic changes:
|
||||
- Orcs → Rats (same stats, different sprite)
|
||||
- Trolls → Spiders (same stats, different sprite)
|
||||
- Corpses → Gravestones (all use same tombstone sprite)
|
||||
|
||||
The gameplay remains identical to the TCOD tutorial, just with different visual theming.
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
# Part 6: Doing (and Taking) Damage
|
||||
|
||||
## Overview
|
||||
|
||||
Part 6 transforms our basic combat into a complete gameplay loop with visual feedback, enemy AI, and win/lose conditions. We add a health bar, message log, enemy AI that pursues the player, and proper game over handling.
|
||||
|
||||
## What's New in Part 6
|
||||
|
||||
### User Interface Components
|
||||
|
||||
#### Health Bar
|
||||
A visual representation of the player's current health:
|
||||
```python
|
||||
class HealthBar:
|
||||
def create_ui(self) -> List[mcrfpy.UIDrawable]:
|
||||
# Dark red background
|
||||
self.background = mcrfpy.Frame(pos=(x, y), size=(width, height))
|
||||
self.background.fill_color = mcrfpy.Color(100, 0, 0, 255)
|
||||
|
||||
# Bright colored bar (green/yellow/red based on HP)
|
||||
self.bar = mcrfpy.Frame(pos=(x, y), size=(width, height))
|
||||
|
||||
# Text overlay showing HP numbers
|
||||
self.text = mcrfpy.Caption(pos=(x+5, y+2),
|
||||
text=f"HP: {hp}/{max_hp}")
|
||||
```
|
||||
|
||||
The bar changes color based on health percentage:
|
||||
- Green (>60% health)
|
||||
- Yellow (30-60% health)
|
||||
- Red (<30% health)
|
||||
|
||||
#### Message Log
|
||||
A scrolling combat log that replaces console print statements:
|
||||
```python
|
||||
class MessageLog:
|
||||
def __init__(self, max_messages: int = 5):
|
||||
self.messages: deque[str] = deque(maxlen=max_messages)
|
||||
|
||||
def add_message(self, message: str) -> None:
|
||||
self.messages.append(message)
|
||||
self.update_display()
|
||||
```
|
||||
|
||||
Messages include:
|
||||
- Combat actions ("Rat attacks Player for 3 hit points.")
|
||||
- Death notifications ("Spider is dead!")
|
||||
- Game state changes ("You have died! Press Escape to quit.")
|
||||
|
||||
### Enemy AI System
|
||||
|
||||
#### Basic AI Component
|
||||
Enemies now actively pursue and attack the player:
|
||||
```python
|
||||
class BasicAI:
|
||||
def take_turn(self, engine: Engine) -> None:
|
||||
distance = max(abs(dx), abs(dy)) # Chebyshev distance
|
||||
|
||||
if distance <= 1:
|
||||
# Adjacent: Attack!
|
||||
MeleeAction(self.entity, attack_dx, attack_dy).perform(engine)
|
||||
elif distance <= 6:
|
||||
# Can see player: Move closer
|
||||
MovementAction(self.entity, move_dx, move_dy).perform(engine)
|
||||
```
|
||||
|
||||
#### Turn-Based System
|
||||
After each player action, all enemies take their turn:
|
||||
```python
|
||||
def handle_enemy_turns(self) -> None:
|
||||
for entity in self.game_map.entities:
|
||||
if isinstance(entity, Actor) and entity.ai and entity.is_alive:
|
||||
entity.ai.take_turn(self)
|
||||
```
|
||||
|
||||
### Game Over Condition
|
||||
|
||||
When the player dies:
|
||||
1. Game state flag is set (`engine.game_over = True`)
|
||||
2. Player becomes a gravestone (sprite changes)
|
||||
3. Input is restricted (only Escape works)
|
||||
4. Death message appears in the message log
|
||||
|
||||
```python
|
||||
def handle_player_death(self) -> None:
|
||||
self.game_over = True
|
||||
self.message_log.add_message("You have died! Press Escape to quit.")
|
||||
```
|
||||
|
||||
## Architecture Improvements
|
||||
|
||||
### UI Module (`game/ui.py`)
|
||||
Separates UI concerns from game logic:
|
||||
- `MessageLog`: Manages combat messages
|
||||
- `HealthBar`: Displays player health
|
||||
- Clean interface for updating displays
|
||||
|
||||
### AI Module (`game/ai.py`)
|
||||
Encapsulates enemy behavior:
|
||||
- `BasicAI`: Simple pursue-and-attack behavior
|
||||
- Extensible for different AI types
|
||||
- Uses existing action system
|
||||
|
||||
### Turn Management
|
||||
Player actions trigger enemy turns:
|
||||
- Movement → Enemy turns
|
||||
- Attack → Enemy turns
|
||||
- Wait → Enemy turns
|
||||
- Maintains turn-based feel
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
### UI Updates
|
||||
Health bar updates occur:
|
||||
- After player takes damage
|
||||
- Automatically via `engine.update_ui()`
|
||||
- Color changes based on HP percentage
|
||||
|
||||
### Message Flow
|
||||
Combat messages follow this pattern:
|
||||
1. Action generates message text
|
||||
2. `engine.message_log.add_message(text)`
|
||||
3. Message appears in UI Caption
|
||||
4. Old messages scroll up
|
||||
|
||||
### AI Decision Making
|
||||
Basic AI uses simple rules:
|
||||
1. Check if player is adjacent → Attack
|
||||
2. Check if player is visible (within 6 tiles) → Move toward
|
||||
3. Otherwise → Do nothing
|
||||
|
||||
### Game State Management
|
||||
The `game_over` flag prevents:
|
||||
- Player movement
|
||||
- Player attacks
|
||||
- Player waiting
|
||||
- But allows Escape to quit
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `game/ui.py`: New module for UI components
|
||||
- `game/ai.py`: New module for enemy AI
|
||||
- `game/engine.py`: Added UI setup, enemy turns, game over handling
|
||||
- `game/entity.py`: Added AI component to Actor
|
||||
- `game/entity_factories.py`: Attached AI to enemies
|
||||
- `game/actions.py`: Integrated message log, added enemy turn triggers
|
||||
- `main.py`: Updated part description
|
||||
|
||||
## What's Next
|
||||
|
||||
Part 7 will expand the user interface further with:
|
||||
- More detailed entity inspection
|
||||
- Possibly inventory display
|
||||
- Additional UI panels
|
||||
- Mouse interaction
|
||||
|
||||
## Learning Points
|
||||
|
||||
1. **UI Separation**: Keep UI logic separate from game logic
|
||||
2. **Component Systems**: AI as a component allows different behaviors
|
||||
3. **Turn-Based Flow**: Player action → Enemy reactions creates tactical gameplay
|
||||
4. **Visual Feedback**: Health bars and message logs improve player understanding
|
||||
5. **State Management**: Game over flag controls available actions
|
||||
|
||||
## Running Part 6
|
||||
|
||||
```bash
|
||||
cd simple_tcod_tutorial/build
|
||||
./mcrogueface scripts/main.py
|
||||
```
|
||||
|
||||
You'll now see:
|
||||
- Health bar at the top showing your current HP
|
||||
- Message log at the bottom showing combat events
|
||||
- Enemies that chase you when you're nearby
|
||||
- Enemies that attack when adjacent
|
||||
- Death state when HP reaches 0
|
||||
|
||||
## Combat Strategy
|
||||
|
||||
With enemy AI active, combat becomes more tactical:
|
||||
- Enemies pursue when they see you
|
||||
- Fighting in corridors limits how many can attack
|
||||
- Running away is sometimes the best option
|
||||
- Health management becomes critical
|
||||
|
||||
The game now has a complete combat loop with clear win/lose conditions!
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
# Part 7: Creating the User Interface
|
||||
|
||||
## Overview
|
||||
|
||||
Part 7 significantly enhances the user interface, transforming our roguelike from a basic game into a more polished experience. We add mouse interaction, help displays, information panels, and better visual feedback systems.
|
||||
|
||||
## What's New in Part 7
|
||||
|
||||
### Mouse Interaction
|
||||
|
||||
#### Click-to-Inspect System
|
||||
Since McRogueFace doesn't have mouse motion events, we use click events to show entity information:
|
||||
```python
|
||||
def grid_click_handler(pixel_x, pixel_y, button, state):
|
||||
# Convert pixel coordinates to grid coordinates
|
||||
grid_x = int(pixel_x / (self.tile_size * self.zoom))
|
||||
grid_y = int(pixel_y / (self.tile_size * self.zoom))
|
||||
|
||||
# Update hover display for this position
|
||||
self.update_mouse_hover(grid_x, grid_y)
|
||||
```
|
||||
|
||||
Click displays show:
|
||||
- Entity names
|
||||
- Current HP for living creatures
|
||||
- Multiple entities if stacked (e.g., "Grave of Rat")
|
||||
|
||||
#### Mouse Handler Registration
|
||||
The click handler is registered as a local function to avoid issues with bound methods:
|
||||
```python
|
||||
# Use a local function instead of a bound method
|
||||
self.game_map.click = grid_click_handler
|
||||
```
|
||||
|
||||
### Help System
|
||||
|
||||
#### Toggle Help Display
|
||||
Press `?`, `H`, or `F1` to show/hide help:
|
||||
```python
|
||||
class HelpDisplay:
|
||||
def toggle(self) -> None:
|
||||
self.visible = not self.visible
|
||||
self.panel.frame.visible = self.visible
|
||||
```
|
||||
|
||||
The help panel includes:
|
||||
- Movement controls for all input methods
|
||||
- Combat instructions
|
||||
- Mouse usage tips
|
||||
- Gameplay strategies
|
||||
|
||||
### Information Panels
|
||||
|
||||
#### Player Stats Panel
|
||||
Always-visible panel showing:
|
||||
- Player name
|
||||
- Current/Max HP
|
||||
- Power and Defense stats
|
||||
- Current grid position
|
||||
|
||||
```python
|
||||
class InfoPanel:
|
||||
def create_ui(self, title: str) -> List[mcrfpy.Drawable]:
|
||||
# Semi-transparent background frame
|
||||
self.frame = mcrfpy.Frame(pos=(x, y), size=(width, height))
|
||||
self.frame.fill_color = mcrfpy.Color(20, 20, 40, 200)
|
||||
|
||||
# Title and content captions as children
|
||||
self.frame.children.append(self.title_caption)
|
||||
self.frame.children.append(self.content_caption)
|
||||
```
|
||||
|
||||
#### Reusable Panel System
|
||||
The `InfoPanel` class provides:
|
||||
- Titled panels with borders
|
||||
- Semi-transparent backgrounds
|
||||
- Easy content updates
|
||||
- Consistent visual style
|
||||
|
||||
### Enhanced UI Components
|
||||
|
||||
#### MouseHoverDisplay Class
|
||||
Manages tooltip-style hover information:
|
||||
- Follows mouse position
|
||||
- Shows/hides automatically
|
||||
- Offset to avoid cursor overlap
|
||||
- Multiple entity support
|
||||
|
||||
#### UI Module Organization
|
||||
Clean separation of UI components:
|
||||
- `MessageLog`: Combat messages
|
||||
- `HealthBar`: HP visualization
|
||||
- `MouseHoverDisplay`: Entity inspection
|
||||
- `InfoPanel`: Generic information display
|
||||
- `HelpDisplay`: Keyboard controls
|
||||
|
||||
## Architecture Improvements
|
||||
|
||||
### UI Composition
|
||||
Using McRogueFace's parent-child system:
|
||||
```python
|
||||
# Add caption as child of frame
|
||||
self.frame.children.append(self.text_caption)
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Automatic relative positioning
|
||||
- Group visibility control
|
||||
- Clean hierarchy
|
||||
|
||||
### Event Handler Extensions
|
||||
Input handler now manages:
|
||||
- Keyboard input (existing)
|
||||
- Mouse motion (new)
|
||||
- Mouse clicks (prepared for future)
|
||||
- UI toggles (help display)
|
||||
|
||||
### Dynamic Content Updates
|
||||
All UI elements support real-time updates:
|
||||
```python
|
||||
def update_stats_panel(self) -> None:
|
||||
stats_text = f"""Name: {self.player.name}
|
||||
HP: {self.player.hp}/{self.player.max_hp}
|
||||
Power: {self.player.power}
|
||||
Defense: {self.player.defense}"""
|
||||
self.stats_panel.update_content(stats_text)
|
||||
```
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
### Mouse Coordinate Conversion
|
||||
Pixel to grid conversion:
|
||||
```python
|
||||
grid_x = int(x / (self.engine.tile_size * self.engine.zoom))
|
||||
grid_y = int(y / (self.engine.tile_size * self.engine.zoom))
|
||||
```
|
||||
|
||||
### Visibility Management
|
||||
UI elements can be toggled:
|
||||
- Help panel starts hidden
|
||||
- Mouse hover hides when not over entities
|
||||
- Panels can be shown/hidden dynamically
|
||||
|
||||
### Color and Transparency
|
||||
UI uses semi-transparent overlays:
|
||||
- Panel backgrounds: `Color(20, 20, 40, 200)`
|
||||
- Hover tooltips: `Color(255, 255, 200, 255)`
|
||||
- Borders and outlines for readability
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `game/ui.py`: Added MouseHoverDisplay, InfoPanel, HelpDisplay classes
|
||||
- `game/engine.py`: Integrated new UI components, mouse hover handling
|
||||
- `game/input_handlers.py`: Added mouse motion handling, help toggle
|
||||
- `main.py`: Registered mouse handlers, updated part description
|
||||
|
||||
## What's Next
|
||||
|
||||
Part 8 will add items and inventory:
|
||||
- Collectible items (potions, equipment)
|
||||
- Inventory management UI
|
||||
- Item usage mechanics
|
||||
- Equipment system
|
||||
|
||||
## Learning Points
|
||||
|
||||
1. **UI Composition**: Use parent-child relationships for complex UI
|
||||
2. **Event Delegation**: Separate input handling from UI updates
|
||||
3. **Information Layers**: Multiple UI systems can coexist (hover, panels, help)
|
||||
4. **Visual Polish**: Small touches like transparency and borders improve UX
|
||||
5. **Reusable Components**: Generic panels can be specialized for different uses
|
||||
|
||||
## Running Part 7
|
||||
|
||||
```bash
|
||||
cd simple_tcod_tutorial/build
|
||||
./mcrogueface scripts/main.py
|
||||
```
|
||||
|
||||
New features to try:
|
||||
- Click on entities to see their details
|
||||
- Press ? or H to toggle help display
|
||||
- Watch the stats panel update as you take damage
|
||||
- See entity HP in hover tooltips
|
||||
- Notice the visual polish in UI panels
|
||||
|
||||
## UI Design Principles
|
||||
|
||||
### Consistency
|
||||
- All panels use similar visual style
|
||||
- Consistent color scheme
|
||||
- Uniform text sizing
|
||||
|
||||
### Non-Intrusive
|
||||
- Semi-transparent panels don't block view
|
||||
- Hover info appears near cursor
|
||||
- Help can be toggled off
|
||||
|
||||
### Information Hierarchy
|
||||
- Critical info (health) always visible
|
||||
- Contextual info (hover) on demand
|
||||
- Help info toggleable
|
||||
|
||||
The UI now provides a professional feel while maintaining the roguelike aesthetic!
|
||||
|
|
@ -1,297 +0,0 @@
|
|||
# Part 8: Items and Inventory
|
||||
|
||||
## Overview
|
||||
|
||||
Part 8 transforms our roguelike into a proper loot-driven game by adding items that can be collected, managed, and used. We implement a flexible inventory system with capacity limits, create consumable items like healing potions, and build UI for inventory management.
|
||||
|
||||
## What's New in Part 8
|
||||
|
||||
### Parent-Child Entity Architecture
|
||||
|
||||
#### Flexible Entity Ownership
|
||||
Entities now have parent containers, allowing them to exist in different contexts:
|
||||
```python
|
||||
class Entity(mcrfpy.Entity):
|
||||
def __init__(self, parent: Optional[Union[GameMap, Inventory]] = None):
|
||||
self.parent = parent
|
||||
|
||||
@property
|
||||
def gamemap(self) -> Optional[GameMap]:
|
||||
"""Get the GameMap through the parent chain"""
|
||||
if isinstance(self.parent, Inventory):
|
||||
return self.parent.gamemap
|
||||
return self.parent
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Items can exist in the world or in inventories
|
||||
- Clean ownership transfer when picking up/dropping
|
||||
- Automatic visibility management
|
||||
|
||||
### Inventory System
|
||||
|
||||
#### Container-Based Design
|
||||
The inventory acts like a specialized entity container:
|
||||
```python
|
||||
class Inventory:
|
||||
def __init__(self, capacity: int):
|
||||
self.capacity = capacity
|
||||
self.items: List[Item] = []
|
||||
self.parent: Optional[Actor] = None
|
||||
|
||||
def add_item(self, item: Item) -> None:
|
||||
if len(self.items) >= self.capacity:
|
||||
raise Impossible("Your inventory is full.")
|
||||
|
||||
# Transfer ownership
|
||||
self.items.append(item)
|
||||
item.parent = self
|
||||
item.visible = False # Hide from map
|
||||
```
|
||||
|
||||
Features:
|
||||
- Capacity limits (26 items for letter selection)
|
||||
- Clean item transfer between world and inventory
|
||||
- Automatic visual management
|
||||
|
||||
### Item System
|
||||
|
||||
#### Item Entity Class
|
||||
Items are entities with consumable components:
|
||||
```python
|
||||
class Item(Entity):
|
||||
def __init__(self, consumable: Optional = None):
|
||||
super().__init__(blocks_movement=False)
|
||||
self.consumable = consumable
|
||||
if consumable:
|
||||
consumable.parent = self
|
||||
```
|
||||
|
||||
#### Consumable Components
|
||||
Modular system for item effects:
|
||||
```python
|
||||
class HealingConsumable(Consumable):
|
||||
def activate(self, action: ItemAction) -> None:
|
||||
if consumer.hp >= consumer.max_hp:
|
||||
raise Impossible("You are already at full health.")
|
||||
|
||||
amount_recovered = min(self.amount, consumer.max_hp - consumer.hp)
|
||||
consumer.hp += amount_recovered
|
||||
self.consume() # Remove item after use
|
||||
```
|
||||
|
||||
### Exception-Driven Feedback
|
||||
|
||||
#### Clean Error Handling
|
||||
Using exceptions for user feedback:
|
||||
```python
|
||||
class Impossible(Exception):
|
||||
"""Action cannot be performed"""
|
||||
pass
|
||||
|
||||
class PickupAction(Action):
|
||||
def perform(self, engine: Engine) -> None:
|
||||
if not items_here:
|
||||
raise Impossible("There is nothing here to pick up.")
|
||||
|
||||
try:
|
||||
inventory.add_item(item)
|
||||
engine.message_log.add_message(f"You picked up the {item.name}!")
|
||||
except Impossible as e:
|
||||
engine.message_log.add_message(str(e))
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Consistent error messaging
|
||||
- Clean control flow
|
||||
- Centralized feedback handling
|
||||
|
||||
### Inventory UI
|
||||
|
||||
#### Modal Inventory Screen
|
||||
Interactive inventory management:
|
||||
```python
|
||||
class InventoryEventHandler(BaseEventHandler):
|
||||
def create_ui(self) -> None:
|
||||
# Semi-transparent background
|
||||
self.background = mcrfpy.Frame(pos=(100, 100), size=(400, 400))
|
||||
self.background.fill_color = mcrfpy.Color(0, 0, 0, 200)
|
||||
|
||||
# List items with letter keys
|
||||
for i, item in enumerate(inventory.items):
|
||||
item_caption = mcrfpy.Caption(
|
||||
pos=(20, 80 + i * 20),
|
||||
text=f"{chr(ord('a') + i)}) {item.name}"
|
||||
)
|
||||
```
|
||||
|
||||
Features:
|
||||
- Letter-based selection (a-z)
|
||||
- Separate handlers for use/drop
|
||||
- ESC to cancel
|
||||
- Visual feedback
|
||||
|
||||
### Enhanced Actions
|
||||
|
||||
#### Item Actions
|
||||
New actions for item management:
|
||||
```python
|
||||
class PickupAction(Action):
|
||||
"""Pick up items at current location"""
|
||||
|
||||
class ItemAction(Action):
|
||||
"""Base for item usage actions"""
|
||||
|
||||
class DropAction(ItemAction):
|
||||
"""Drop item from inventory"""
|
||||
```
|
||||
|
||||
Each action:
|
||||
- Self-validates
|
||||
- Provides feedback
|
||||
- Triggers enemy turns
|
||||
|
||||
## Architecture Improvements
|
||||
|
||||
### Component Relationships
|
||||
Parent-based component system:
|
||||
```python
|
||||
# Components know their parent
|
||||
consumable.parent = item
|
||||
item.parent = inventory
|
||||
inventory.parent = actor
|
||||
actor.parent = gamemap
|
||||
gamemap.engine = engine
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Access to game context from any component
|
||||
- Clean ownership transfer
|
||||
- Simplified entity lifecycle
|
||||
|
||||
### Input Handler States
|
||||
Modal UI through handler switching:
|
||||
```python
|
||||
# Main game
|
||||
engine.current_handler = MainGameEventHandler(engine)
|
||||
|
||||
# Open inventory
|
||||
engine.current_handler = InventoryActivateHandler(engine)
|
||||
|
||||
# Back to game
|
||||
engine.current_handler = MainGameEventHandler(engine)
|
||||
```
|
||||
|
||||
### Entity Lifecycle Management
|
||||
Proper creation and cleanup:
|
||||
```python
|
||||
# Item spawning
|
||||
item = entity_factories.health_potion(x, y, texture)
|
||||
item.place(x, y, dungeon)
|
||||
|
||||
# Pickup
|
||||
inventory.add_item(item) # Removes from map
|
||||
|
||||
# Drop
|
||||
inventory.drop(item) # Returns to map
|
||||
|
||||
# Death
|
||||
actor.die() # Drops all items
|
||||
```
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
### Visibility Management
|
||||
Items hide/show based on container:
|
||||
```python
|
||||
def add_item(self, item):
|
||||
item.visible = False # Hide when in inventory
|
||||
|
||||
def drop(self, item):
|
||||
item.visible = True # Show when on map
|
||||
```
|
||||
|
||||
### Inventory Capacity
|
||||
Limited to alphabet keys:
|
||||
```python
|
||||
if len(inventory.items) >= 26:
|
||||
raise Impossible("Your inventory is full.")
|
||||
```
|
||||
|
||||
### Item Generation
|
||||
Procedural item placement:
|
||||
```python
|
||||
def place_entities(room, dungeon, max_monsters, max_items, texture):
|
||||
# Place 0-2 items per room
|
||||
number_of_items = random.randint(0, max_items)
|
||||
|
||||
for _ in range(number_of_items):
|
||||
if space_available:
|
||||
item = entity_factories.health_potion(x, y, texture)
|
||||
item.place(x, y, dungeon)
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `game/entity.py`: Added parent system, Item class, inventory to Actor
|
||||
- `game/inventory.py`: New inventory container system
|
||||
- `game/consumable.py`: New consumable component system
|
||||
- `game/exceptions.py`: New Impossible exception
|
||||
- `game/actions.py`: Added PickupAction, ItemAction, DropAction
|
||||
- `game/input_handlers.py`: Added InventoryEventHandler classes
|
||||
- `game/engine.py`: Added current_handler, inventory UI methods
|
||||
- `game/procgen.py`: Added item generation
|
||||
- `game/entity_factories.py`: Added health_potion factory
|
||||
- `game/ui.py`: Updated help text with inventory controls
|
||||
- `main.py`: Updated to Part 8, handler management
|
||||
|
||||
## What's Next
|
||||
|
||||
Part 9 will add ranged attacks and targeting:
|
||||
- Targeting UI for selecting enemies
|
||||
- Ranged damage items (lightning staff)
|
||||
- Area-of-effect items (fireball staff)
|
||||
- Confusion effects
|
||||
|
||||
## Learning Points
|
||||
|
||||
1. **Container Architecture**: Entity ownership through parent relationships
|
||||
2. **Component Systems**: Modular, reusable components with parent references
|
||||
3. **Exception Handling**: Clean error propagation and user feedback
|
||||
4. **Modal UI**: State-based input handling for different screens
|
||||
5. **Item Systems**: Flexible consumable architecture for varied effects
|
||||
6. **Lifecycle Management**: Proper entity creation, transfer, and cleanup
|
||||
|
||||
## Running Part 8
|
||||
|
||||
```bash
|
||||
cd simple_tcod_tutorial/build
|
||||
./mcrogueface scripts/main.py
|
||||
```
|
||||
|
||||
New features to try:
|
||||
- Press G to pick up healing potions
|
||||
- Press I to open inventory and use items
|
||||
- Press O to drop items from inventory
|
||||
- Heal yourself when injured in combat
|
||||
- Manage limited inventory space (26 slots)
|
||||
- Items drop from dead enemies
|
||||
|
||||
## Design Principles
|
||||
|
||||
### Flexibility Through Composition
|
||||
- Items gain behavior through consumable components
|
||||
- Easy to add new item types
|
||||
- Reusable effect system
|
||||
|
||||
### Clean Ownership Transfer
|
||||
- Entities always have clear parent
|
||||
- Automatic visibility management
|
||||
- No orphaned entities
|
||||
|
||||
### User-Friendly Feedback
|
||||
- Clear error messages
|
||||
- Consistent UI patterns
|
||||
- Intuitive controls
|
||||
|
||||
The inventory system provides the foundation for equipment, spells, and complex item interactions in future parts!
|
||||
|
|
@ -1,625 +0,0 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 5: Entity Interactions
|
||||
|
||||
This tutorial builds on Part 4 by adding:
|
||||
- Entity class hierarchy (PlayerEntity, EnemyEntity, BoulderEntity, ButtonEntity)
|
||||
- Non-blocking movement animations with destination tracking
|
||||
- Bump interactions (combat, pushing)
|
||||
- Step-on interactions (buttons, doors)
|
||||
- Concurrent enemy AI with smooth animations
|
||||
|
||||
Key concepts:
|
||||
- Entities inherit from mcrfpy.Entity for proper C++/Python integration
|
||||
- Logic operates on destination positions during animations
|
||||
- Player input is processed immediately, not blocked by animations
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# ============================================================================
|
||||
# Entity Classes - Inherit from mcrfpy.Entity
|
||||
# ============================================================================
|
||||
|
||||
class GameEntity(mcrfpy.Entity):
|
||||
"""Base class for all game entities with interaction logic"""
|
||||
def __init__(self, x, y, **kwargs):
|
||||
# Extract grid before passing to parent
|
||||
grid = kwargs.pop('grid', None)
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
|
||||
# Current position is tracked by parent Entity.x/y
|
||||
# Add destination tracking for animation system
|
||||
self.dest_x = x
|
||||
self.dest_y = y
|
||||
self.is_moving = False
|
||||
|
||||
# Game properties
|
||||
self.blocks_movement = True
|
||||
self.hp = 10
|
||||
self.max_hp = 10
|
||||
self.entity_type = "generic"
|
||||
|
||||
# Add to grid if provided
|
||||
if grid:
|
||||
grid.entities.append(self)
|
||||
|
||||
def start_move(self, new_x, new_y, duration=0.2, callback=None):
|
||||
"""Start animating movement to new position"""
|
||||
self.dest_x = new_x
|
||||
self.dest_y = new_y
|
||||
self.is_moving = True
|
||||
|
||||
# Create animations for smooth movement
|
||||
if callback:
|
||||
# Only x animation needs callback since they run in parallel
|
||||
anim_x = mcrfpy.Animation("x", float(new_x), duration, "easeInOutQuad", callback=callback)
|
||||
else:
|
||||
anim_x = mcrfpy.Animation("x", float(new_x), duration, "easeInOutQuad")
|
||||
anim_y = mcrfpy.Animation("y", float(new_y), duration, "easeInOutQuad")
|
||||
|
||||
anim_x.start(self)
|
||||
anim_y.start(self)
|
||||
|
||||
def get_position(self):
|
||||
"""Get logical position (destination if moving, otherwise current)"""
|
||||
if self.is_moving:
|
||||
return (self.dest_x, self.dest_y)
|
||||
return (int(self.x), int(self.y))
|
||||
|
||||
def on_bump(self, other):
|
||||
"""Called when another entity tries to move into our space"""
|
||||
return False # Block movement by default
|
||||
|
||||
def on_step(self, other):
|
||||
"""Called when another entity steps on us (non-blocking)"""
|
||||
pass
|
||||
|
||||
def take_damage(self, damage):
|
||||
"""Apply damage and handle death"""
|
||||
self.hp -= damage
|
||||
if self.hp <= 0:
|
||||
self.hp = 0
|
||||
self.die()
|
||||
|
||||
def die(self):
|
||||
"""Remove entity from grid"""
|
||||
# The C++ die() method handles removal from grid
|
||||
super().die()
|
||||
|
||||
class PlayerEntity(GameEntity):
|
||||
"""The player character"""
|
||||
def __init__(self, x, y, **kwargs):
|
||||
kwargs['sprite_index'] = 64 # Hero sprite
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
self.damage = 3
|
||||
self.entity_type = "player"
|
||||
self.blocks_movement = True
|
||||
|
||||
def on_bump(self, other):
|
||||
"""Player bumps into something"""
|
||||
if other.entity_type == "enemy":
|
||||
# Deal damage
|
||||
other.take_damage(self.damage)
|
||||
return False # Can't move into enemy space
|
||||
elif other.entity_type == "boulder":
|
||||
# Try to push
|
||||
dx = self.dest_x - int(self.x)
|
||||
dy = self.dest_y - int(self.y)
|
||||
return other.try_push(dx, dy)
|
||||
return False
|
||||
|
||||
class EnemyEntity(GameEntity):
|
||||
"""Basic enemy with AI"""
|
||||
def __init__(self, x, y, **kwargs):
|
||||
kwargs['sprite_index'] = 65 # Enemy sprite
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
self.damage = 1
|
||||
self.entity_type = "enemy"
|
||||
self.ai_state = "wander"
|
||||
self.hp = 5
|
||||
self.max_hp = 5
|
||||
|
||||
def on_bump(self, other):
|
||||
"""Enemy bumps into something"""
|
||||
if other.entity_type == "player":
|
||||
other.take_damage(self.damage)
|
||||
return False
|
||||
return False
|
||||
|
||||
def can_see_player(self, player_pos, grid):
|
||||
"""Check if enemy can see the player position"""
|
||||
# Simple check: within 6 tiles and has line of sight
|
||||
mx, my = self.get_position()
|
||||
px, py = player_pos
|
||||
|
||||
dist = abs(px - mx) + abs(py - my)
|
||||
if dist > 6:
|
||||
return False
|
||||
|
||||
# Use libtcod for line of sight
|
||||
line = list(mcrfpy.libtcod.line(mx, my, px, py))
|
||||
if len(line) > 7: # Too far
|
||||
return False
|
||||
for x, y in line[1:-1]: # Skip start and end points
|
||||
cell = grid.at(x, y)
|
||||
if cell and not cell.transparent:
|
||||
return False
|
||||
return True
|
||||
|
||||
def ai_turn(self, grid, player):
|
||||
"""Decide next move"""
|
||||
px, py = player.get_position()
|
||||
mx, my = self.get_position()
|
||||
|
||||
# Simple AI: move toward player if visible
|
||||
if self.can_see_player((px, py), grid):
|
||||
# Calculate direction toward player
|
||||
dx = 0
|
||||
dy = 0
|
||||
if px > mx:
|
||||
dx = 1
|
||||
elif px < mx:
|
||||
dx = -1
|
||||
if py > my:
|
||||
dy = 1
|
||||
elif py < my:
|
||||
dy = -1
|
||||
|
||||
# Prefer cardinal movement
|
||||
if dx != 0 and dy != 0:
|
||||
# Pick horizontal or vertical based on greater distance
|
||||
if abs(px - mx) > abs(py - my):
|
||||
dy = 0
|
||||
else:
|
||||
dx = 0
|
||||
|
||||
return (mx + dx, my + dy)
|
||||
else:
|
||||
# Random movement
|
||||
dx, dy = random.choice([(0,1), (0,-1), (1,0), (-1,0)])
|
||||
return (mx + dx, my + dy)
|
||||
|
||||
class BoulderEntity(GameEntity):
|
||||
"""Pushable boulder"""
|
||||
def __init__(self, x, y, **kwargs):
|
||||
kwargs['sprite_index'] = 7 # Boulder sprite
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
self.entity_type = "boulder"
|
||||
self.pushable = True
|
||||
|
||||
def try_push(self, dx, dy):
|
||||
"""Attempt to push boulder in direction"""
|
||||
new_x = int(self.x) + dx
|
||||
new_y = int(self.y) + dy
|
||||
|
||||
# Check if destination is free
|
||||
if can_move_to(new_x, new_y):
|
||||
self.start_move(new_x, new_y)
|
||||
return True
|
||||
return False
|
||||
|
||||
class ButtonEntity(GameEntity):
|
||||
"""Pressure plate that triggers when stepped on"""
|
||||
def __init__(self, x, y, target=None, **kwargs):
|
||||
kwargs['sprite_index'] = 8 # Button sprite
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
self.blocks_movement = False # Can be walked over
|
||||
self.entity_type = "button"
|
||||
self.pressed = False
|
||||
self.pressed_by = set() # Track who's pressing
|
||||
self.target = target # Door or other triggerable
|
||||
|
||||
def on_step(self, other):
|
||||
"""Activate when stepped on"""
|
||||
if other not in self.pressed_by:
|
||||
self.pressed_by.add(other)
|
||||
if not self.pressed:
|
||||
self.pressed = True
|
||||
self.sprite_index = 9 # Pressed sprite
|
||||
if self.target:
|
||||
self.target.activate()
|
||||
|
||||
def on_leave(self, other):
|
||||
"""Deactivate when entity leaves"""
|
||||
if other in self.pressed_by:
|
||||
self.pressed_by.remove(other)
|
||||
if len(self.pressed_by) == 0 and self.pressed:
|
||||
self.pressed = False
|
||||
self.sprite_index = 8 # Unpressed sprite
|
||||
if self.target:
|
||||
self.target.deactivate()
|
||||
|
||||
class DoorEntity(GameEntity):
|
||||
"""Door that can be opened by buttons"""
|
||||
def __init__(self, x, y, **kwargs):
|
||||
kwargs['sprite_index'] = 3 # Closed door sprite
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
self.entity_type = "door"
|
||||
self.is_open = False
|
||||
|
||||
def activate(self):
|
||||
"""Open the door"""
|
||||
self.is_open = True
|
||||
self.blocks_movement = False
|
||||
self.sprite_index = 11 # Open door sprite
|
||||
|
||||
def deactivate(self):
|
||||
"""Close the door"""
|
||||
self.is_open = False
|
||||
self.blocks_movement = True
|
||||
self.sprite_index = 3 # Closed door sprite
|
||||
|
||||
# ============================================================================
|
||||
# Global Game State
|
||||
# ============================================================================
|
||||
|
||||
# Create and activate a new scene
|
||||
mcrfpy.createScene("tutorial")
|
||||
mcrfpy.setScene("tutorial")
|
||||
|
||||
# Load the texture
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
grid_width, grid_height = 40, 30
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
# Create the grid
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size,
|
||||
)
|
||||
grid.zoom = zoom
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Game state
|
||||
player = None
|
||||
enemies = []
|
||||
all_entities = []
|
||||
is_player_turn = True
|
||||
move_duration = 0.2
|
||||
|
||||
# ============================================================================
|
||||
# Dungeon Generation (from Part 3)
|
||||
# ============================================================================
|
||||
|
||||
class Room:
|
||||
def __init__(self, x, y, width, height):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
def center(self):
|
||||
return ((self.x1 + self.x2) // 2, (self.y1 + self.y2) // 2)
|
||||
|
||||
def intersects(self, other):
|
||||
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
|
||||
self.y1 <= other.y2 and self.y2 >= other.y1)
|
||||
|
||||
def create_room(room):
|
||||
"""Carve out a room in the grid"""
|
||||
for x in range(room.x1 + 1, room.x2):
|
||||
for y in range(room.y1 + 1, room.y2):
|
||||
cell = grid.at(x, y)
|
||||
if cell:
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.tilesprite = random.choice(FLOOR_TILES)
|
||||
|
||||
def create_l_shaped_hallway(x1, y1, x2, y2):
|
||||
"""Create L-shaped hallway between two points"""
|
||||
corner_x = x2
|
||||
corner_y = y1
|
||||
|
||||
if random.random() < 0.5:
|
||||
corner_x = x1
|
||||
corner_y = y2
|
||||
|
||||
for x, y in mcrfpy.libtcod.line(x1, y1, corner_x, corner_y):
|
||||
cell = grid.at(x, y)
|
||||
if cell:
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.tilesprite = random.choice(FLOOR_TILES)
|
||||
|
||||
for x, y in mcrfpy.libtcod.line(corner_x, corner_y, x2, y2):
|
||||
cell = grid.at(x, y)
|
||||
if cell:
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.tilesprite = random.choice(FLOOR_TILES)
|
||||
|
||||
def generate_dungeon():
|
||||
"""Generate a simple dungeon with rooms and hallways"""
|
||||
# Initialize all cells as walls
|
||||
for x in range(grid_width):
|
||||
for y in range(grid_height):
|
||||
cell = grid.at(x, y)
|
||||
if cell:
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.tilesprite = random.choice(WALL_TILES)
|
||||
|
||||
rooms = []
|
||||
num_rooms = 0
|
||||
|
||||
for _ in range(30):
|
||||
w = random.randint(4, 8)
|
||||
h = random.randint(4, 8)
|
||||
x = random.randint(0, grid_width - w - 1)
|
||||
y = random.randint(0, grid_height - h - 1)
|
||||
|
||||
new_room = Room(x, y, w, h)
|
||||
|
||||
# Check if room intersects with existing rooms
|
||||
if any(new_room.intersects(other_room) for other_room in rooms):
|
||||
continue
|
||||
|
||||
create_room(new_room)
|
||||
|
||||
if num_rooms > 0:
|
||||
# Connect to previous room
|
||||
new_x, new_y = new_room.center()
|
||||
prev_x, prev_y = rooms[num_rooms - 1].center()
|
||||
create_l_shaped_hallway(prev_x, prev_y, new_x, new_y)
|
||||
|
||||
rooms.append(new_room)
|
||||
num_rooms += 1
|
||||
|
||||
return rooms
|
||||
|
||||
# ============================================================================
|
||||
# Entity Management
|
||||
# ============================================================================
|
||||
|
||||
def get_entities_at(x, y):
|
||||
"""Get all entities at a specific position (including moving ones)"""
|
||||
entities = []
|
||||
for entity in all_entities:
|
||||
ex, ey = entity.get_position()
|
||||
if ex == x and ey == y:
|
||||
entities.append(entity)
|
||||
return entities
|
||||
|
||||
def get_blocking_entity_at(x, y):
|
||||
"""Get the first blocking entity at position"""
|
||||
for entity in get_entities_at(x, y):
|
||||
if entity.blocks_movement:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def can_move_to(x, y):
|
||||
"""Check if a position is valid for movement"""
|
||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
||||
return False
|
||||
|
||||
cell = grid.at(x, y)
|
||||
if not cell or not cell.walkable:
|
||||
return False
|
||||
|
||||
# Check for blocking entities
|
||||
if get_blocking_entity_at(x, y):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def can_entity_move_to(entity, x, y):
|
||||
"""Check if specific entity can move to position"""
|
||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
||||
return False
|
||||
|
||||
cell = grid.at(x, y)
|
||||
if not cell or not cell.walkable:
|
||||
return False
|
||||
|
||||
# Check for other blocking entities (not self)
|
||||
blocker = get_blocking_entity_at(x, y)
|
||||
if blocker and blocker != entity:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# ============================================================================
|
||||
# Turn Management
|
||||
# ============================================================================
|
||||
|
||||
def process_player_move(key):
|
||||
"""Handle player input with immediate response"""
|
||||
global is_player_turn
|
||||
|
||||
if not is_player_turn or player.is_moving:
|
||||
return # Not player's turn or still animating
|
||||
|
||||
px, py = player.get_position()
|
||||
new_x, new_y = px, py
|
||||
|
||||
# Calculate movement direction
|
||||
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
|
||||
else:
|
||||
return # Not a movement key
|
||||
|
||||
if new_x == px and new_y == py:
|
||||
return # No movement
|
||||
|
||||
# Check what's at destination
|
||||
cell = grid.at(new_x, new_y)
|
||||
if not cell or not cell.walkable:
|
||||
return # Can't move into walls
|
||||
|
||||
blocking_entity = get_blocking_entity_at(new_x, new_y)
|
||||
|
||||
if blocking_entity:
|
||||
# Try bump interaction
|
||||
if not player.on_bump(blocking_entity):
|
||||
# Movement blocked, but turn still happens
|
||||
is_player_turn = False
|
||||
mcrfpy.setTimer("enemy_turn", process_enemy_turns, 50)
|
||||
return
|
||||
|
||||
# Movement is valid - start player animation
|
||||
is_player_turn = False
|
||||
player.start_move(new_x, new_y, duration=move_duration, callback=player_move_complete)
|
||||
|
||||
# Update grid center to follow player
|
||||
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, move_duration, "linear")
|
||||
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, move_duration, "linear")
|
||||
grid_anim_x.start(grid)
|
||||
grid_anim_y.start(grid)
|
||||
|
||||
# Start enemy turns after a short delay (so player sees their move start first)
|
||||
mcrfpy.setTimer("enemy_turn", process_enemy_turns, 50)
|
||||
|
||||
def process_enemy_turns(timer_name):
|
||||
"""Process all enemy AI decisions and start their animations"""
|
||||
enemies_to_move = []
|
||||
|
||||
for enemy in enemies:
|
||||
if enemy.hp <= 0: # Skip dead enemies
|
||||
continue
|
||||
|
||||
if enemy.is_moving:
|
||||
continue # Skip if still animating
|
||||
|
||||
# AI decides next move based on player's destination
|
||||
target_x, target_y = enemy.ai_turn(grid, player)
|
||||
|
||||
# Check if move is valid
|
||||
cell = grid.at(target_x, target_y)
|
||||
if not cell or not cell.walkable:
|
||||
continue
|
||||
|
||||
# Check what's at the destination
|
||||
blocking_entity = get_blocking_entity_at(target_x, target_y)
|
||||
|
||||
if blocking_entity and blocking_entity != enemy:
|
||||
# Try bump interaction
|
||||
enemy.on_bump(blocking_entity)
|
||||
# Enemy doesn't move but still took its turn
|
||||
else:
|
||||
# Valid move - add to list
|
||||
enemies_to_move.append((enemy, target_x, target_y))
|
||||
|
||||
# Start all enemy animations simultaneously
|
||||
for enemy, tx, ty in enemies_to_move:
|
||||
enemy.start_move(tx, ty, duration=move_duration)
|
||||
|
||||
def player_move_complete(anim, entity):
|
||||
"""Called when player animation finishes"""
|
||||
global is_player_turn
|
||||
|
||||
player.is_moving = False
|
||||
|
||||
# Check for step-on interactions at new position
|
||||
for entity in get_entities_at(int(player.x), int(player.y)):
|
||||
if entity != player and not entity.blocks_movement:
|
||||
entity.on_step(player)
|
||||
|
||||
# Update FOV from new position
|
||||
update_fov()
|
||||
|
||||
# Player's turn is ready again
|
||||
is_player_turn = True
|
||||
|
||||
def update_fov():
|
||||
"""Update field of view from player position"""
|
||||
px, py = int(player.x), int(player.y)
|
||||
grid.compute_fov(px, py, radius=8)
|
||||
player.update_visibility()
|
||||
|
||||
# ============================================================================
|
||||
# Input Handling
|
||||
# ============================================================================
|
||||
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input"""
|
||||
if state == "start":
|
||||
# Movement keys
|
||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
||||
process_player_move(key)
|
||||
|
||||
# Register the key handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# ============================================================================
|
||||
# Initialize Game
|
||||
# ============================================================================
|
||||
|
||||
# Generate dungeon
|
||||
rooms = generate_dungeon()
|
||||
|
||||
# Place player in first room
|
||||
if rooms:
|
||||
start_x, start_y = rooms[0].center()
|
||||
player = PlayerEntity(start_x, start_y, grid=grid)
|
||||
all_entities.append(player)
|
||||
|
||||
# Place enemies in other rooms
|
||||
for i in range(1, min(6, len(rooms))):
|
||||
room = rooms[i]
|
||||
ex, ey = room.center()
|
||||
enemy = EnemyEntity(ex, ey, grid=grid)
|
||||
enemies.append(enemy)
|
||||
all_entities.append(enemy)
|
||||
|
||||
# Place some boulders
|
||||
for i in range(3):
|
||||
room = random.choice(rooms[1:])
|
||||
bx = random.randint(room.x1 + 1, room.x2 - 1)
|
||||
by = random.randint(room.y1 + 1, room.y2 - 1)
|
||||
if can_move_to(bx, by):
|
||||
boulder = BoulderEntity(bx, by, grid=grid)
|
||||
all_entities.append(boulder)
|
||||
|
||||
# Place a button and door in one of the rooms
|
||||
if len(rooms) > 2:
|
||||
button_room = rooms[-2]
|
||||
door_room = rooms[-1]
|
||||
|
||||
# Place door at entrance to last room
|
||||
dx, dy = door_room.center()
|
||||
door = DoorEntity(dx, door_room.y1, grid=grid)
|
||||
all_entities.append(door)
|
||||
|
||||
# Place button in second to last room
|
||||
bx, by = button_room.center()
|
||||
button = ButtonEntity(bx, by, target=door, grid=grid)
|
||||
all_entities.append(button)
|
||||
|
||||
# Set grid perspective to player
|
||||
grid.perspective = player
|
||||
grid.center_x = (start_x + 0.5) * 16
|
||||
grid.center_y = (start_y + 0.5) * 16
|
||||
|
||||
# Initial FOV calculation
|
||||
update_fov()
|
||||
|
||||
# Add grid to scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Show instructions
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="Part 5: Entity Interactions - WASD to move, bump enemies, push boulders!",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
print("Part 5: Entity Interactions - Tutorial loaded!")
|
||||
print("- Bump into enemies to attack them")
|
||||
print("- Push boulders by walking into them")
|
||||
print("- Step on buttons to open doors")
|
||||
print("- Enemies will pursue you when they can see you")
|
||||
|
|
@ -373,7 +373,7 @@ class Engine:
|
|||
|
||||
self.ui = mcrfpy.sceneUI("game")
|
||||
|
||||
background = mcrfpy.Frame((0, 0), (1024, 768))
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
self.ui.append(background)
|
||||
|
||||
|
|
@ -565,4 +565,4 @@ class Engine:
|
|||
# Create and run the game
|
||||
engine = Engine()
|
||||
print("Part 6: Combat System!")
|
||||
print("Attack enemies to defeat them, but watch your HP!")
|
||||
print("Attack enemies to defeat them, but watch your HP!")
|
||||
|
|
@ -1,313 +0,0 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 3: Procedural Dungeon Generation
|
||||
|
||||
This tutorial builds on Part 2 by adding:
|
||||
- Binary Space Partition (BSP) dungeon generation
|
||||
- Rooms connected by hallways using libtcod.line()
|
||||
- Walkable/non-walkable terrain
|
||||
- Player spawning in a valid location
|
||||
- Wall tiles that block movement
|
||||
|
||||
Key code references:
|
||||
- src/scripts/cos_level.py (lines 7-15, 184-217, 218-224) - BSP algorithm
|
||||
- mcrfpy.libtcod.line() for smooth hallway generation
|
||||
"""
|
||||
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
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
grid_width, grid_height = 40, 30 # Larger grid for dungeon
|
||||
|
||||
# Calculate the size in pixels to fit the entire grid on-screen
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# Calculate the position to center the grid on the screen
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
# Create the grid with a TCODMap for pathfinding/FOV
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size,
|
||||
)
|
||||
|
||||
grid.zoom = zoom
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Room class for BSP
|
||||
class Room:
|
||||
def __init__(self, x, y, w, h):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + w
|
||||
self.y2 = y + h
|
||||
self.w = w
|
||||
self.h = h
|
||||
|
||||
def center(self):
|
||||
"""Return the center coordinates of the room"""
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return (center_x, center_y)
|
||||
|
||||
def intersects(self, other):
|
||||
"""Return True if this room overlaps with another"""
|
||||
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
|
||||
self.y1 <= other.y2 and self.y2 >= other.y1)
|
||||
|
||||
# Dungeon generation functions
|
||||
def carve_room(room):
|
||||
"""Carve out a room in the grid - referenced from cos_level.py lines 117-120"""
|
||||
# Using individual updates for now (batch updates would be more efficient)
|
||||
for x in range(room.x1, room.x2):
|
||||
for y in range(room.y1, room.y2):
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(FLOOR_TILES)
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
def carve_hallway(x1, y1, x2, y2):
|
||||
"""Carve a hallway between two points using libtcod.line()
|
||||
Referenced from cos_level.py lines 184-217, improved with libtcod.line()
|
||||
"""
|
||||
# Get all points along the line
|
||||
|
||||
# Simple solution: works if your characters have diagonal movement
|
||||
#points = mcrfpy.libtcod.line(x1, y1, x2, y2)
|
||||
|
||||
# We don't, so we're going to carve a path with an elbow in it
|
||||
points = []
|
||||
if random.choice([True, False]):
|
||||
# x1,y1 -> x2,y1 -> x2,y2
|
||||
points.extend(mcrfpy.libtcod.line(x1, y1, x2, y1))
|
||||
points.extend(mcrfpy.libtcod.line(x2, y1, x2, y2))
|
||||
else:
|
||||
# x1,y1 -> x1,y2 -> x2,y2
|
||||
points.extend(mcrfpy.libtcod.line(x1, y1, x1, y2))
|
||||
points.extend(mcrfpy.libtcod.line(x1, y2, x2, y2))
|
||||
|
||||
|
||||
# Carve out each point
|
||||
for x, y in points:
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(FLOOR_TILES)
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
def generate_dungeon(max_rooms=10, room_min_size=4, room_max_size=10):
|
||||
"""Generate a dungeon using simplified BSP approach
|
||||
Referenced from cos_level.py lines 218-224
|
||||
"""
|
||||
rooms = []
|
||||
|
||||
# First, fill everything with walls
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(WALL_TILES)
|
||||
point.walkable = False
|
||||
point.transparent = False
|
||||
|
||||
# Generate rooms
|
||||
for _ in range(max_rooms):
|
||||
# Random room size
|
||||
w = random.randint(room_min_size, room_max_size)
|
||||
h = random.randint(room_min_size, room_max_size)
|
||||
|
||||
# Random position (with margin from edges)
|
||||
x = random.randint(1, grid_width - w - 1)
|
||||
y = random.randint(1, grid_height - h - 1)
|
||||
|
||||
new_room = Room(x, y, w, h)
|
||||
|
||||
# Check if it overlaps with existing rooms
|
||||
failed = False
|
||||
for other_room in rooms:
|
||||
if new_room.intersects(other_room):
|
||||
failed = True
|
||||
break
|
||||
|
||||
if not failed:
|
||||
# Carve out the room
|
||||
carve_room(new_room)
|
||||
|
||||
# If not the first room, connect to previous room
|
||||
if rooms:
|
||||
# Get centers
|
||||
prev_x, prev_y = rooms[-1].center()
|
||||
new_x, new_y = new_room.center()
|
||||
|
||||
# Carve hallway using libtcod.line()
|
||||
carve_hallway(prev_x, prev_y, new_x, new_y)
|
||||
|
||||
rooms.append(new_room)
|
||||
|
||||
return rooms
|
||||
|
||||
# Generate the dungeon
|
||||
rooms = generate_dungeon(max_rooms=8, room_min_size=4, room_max_size=8)
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Spawn player in the first room
|
||||
if rooms:
|
||||
spawn_x, spawn_y = rooms[0].center()
|
||||
else:
|
||||
# Fallback spawn position
|
||||
spawn_x, spawn_y = 4, 4
|
||||
|
||||
# Create a player entity at the spawn position
|
||||
player = mcrfpy.Entity(
|
||||
(spawn_x, spawn_y),
|
||||
texture=hero_texture,
|
||||
sprite_index=0
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||
|
||||
# Movement state tracking (from Part 2)
|
||||
is_moving = False
|
||||
move_queue = []
|
||||
current_destination = None
|
||||
current_move = None
|
||||
|
||||
# Store animation references
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
grid_anim_x = None
|
||||
grid_anim_y = None
|
||||
|
||||
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
|
||||
|
||||
is_moving = False
|
||||
current_move = None
|
||||
current_destination = None
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||
|
||||
if move_queue:
|
||||
next_move = move_queue.pop(0)
|
||||
process_move(next_move)
|
||||
|
||||
motion_speed = 0.20 # Slightly faster for dungeon exploration
|
||||
|
||||
def can_move_to(x, y):
|
||||
"""Check if a position is valid for movement"""
|
||||
# Boundary check
|
||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
||||
return False
|
||||
|
||||
# Walkability check
|
||||
point = grid.at(x, y)
|
||||
if point and point.walkable:
|
||||
return True
|
||||
return False
|
||||
|
||||
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 is_moving:
|
||||
move_queue.clear()
|
||||
move_queue.append(key)
|
||||
return
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
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
|
||||
|
||||
# Check if we can move to the new position
|
||||
if new_x != px or new_y != py:
|
||||
if can_move_to(new_x, new_y):
|
||||
is_moving = True
|
||||
current_move = key
|
||||
current_destination = (new_x, new_y)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
else:
|
||||
# Play a "bump" sound or visual feedback here
|
||||
print(f"Can't move to ({new_x}, {new_y}) - blocked!")
|
||||
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input to move the player"""
|
||||
if state == "start":
|
||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
||||
process_move(key)
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add UI elements
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 3: Dungeon Generation",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
instructions = mcrfpy.Caption((150, 750),
|
||||
text=f"Procedural dungeon with {len(rooms)} rooms connected by hallways!",
|
||||
)
|
||||
instructions.font_size = 18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
# Debug info
|
||||
debug_caption = mcrfpy.Caption((10, 40),
|
||||
text=f"Grid: {grid_width}x{grid_height} | Player spawned at ({spawn_x}, {spawn_y})",
|
||||
)
|
||||
debug_caption.font_size = 16
|
||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
||||
|
||||
print("Tutorial Part 3 loaded!")
|
||||
print(f"Generated dungeon with {len(rooms)} rooms")
|
||||
print(f"Player spawned at ({spawn_x}, {spawn_y})")
|
||||
print("Walls now block movement!")
|
||||
print("Use WASD or Arrow keys to explore the dungeon!")
|
||||
|
|
@ -1,366 +0,0 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 4: Field of View
|
||||
|
||||
This tutorial builds on Part 3 by adding:
|
||||
- Field of view calculation using grid.compute_fov()
|
||||
- Entity perspective rendering with grid.perspective
|
||||
- Three visibility states: unexplored (black), explored (dark), visible (lit)
|
||||
- Memory of previously seen areas
|
||||
- Enemy entity to demonstrate perspective switching
|
||||
|
||||
Key code references:
|
||||
- tests/unit/test_tcod_fov_entities.py (lines 89-118) - FOV with multiple entities
|
||||
- ROADMAP.md (lines 216-229) - FOV system implementation details
|
||||
"""
|
||||
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
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
grid_width, grid_height = 40, 30
|
||||
|
||||
# Calculate the size in pixels
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# Calculate the position to center the grid on the screen
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
# Create the grid with a TCODMap for pathfinding/FOV
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size,
|
||||
)
|
||||
|
||||
grid.zoom = zoom
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Room class for BSP
|
||||
class Room:
|
||||
def __init__(self, x, y, w, h):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + w
|
||||
self.y2 = y + h
|
||||
self.w = w
|
||||
self.h = h
|
||||
|
||||
def center(self):
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return (center_x, center_y)
|
||||
|
||||
def intersects(self, other):
|
||||
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
|
||||
self.y1 <= other.y2 and self.y2 >= other.y1)
|
||||
|
||||
# Dungeon generation functions (from Part 3)
|
||||
def carve_room(room):
|
||||
for x in range(room.x1, room.x2):
|
||||
for y in range(room.y1, room.y2):
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(FLOOR_TILES)
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
def carve_hallway(x1, y1, x2, y2):
|
||||
#points = mcrfpy.libtcod.line(x1, y1, x2, y2)
|
||||
points = []
|
||||
if random.choice([True, False]):
|
||||
# x1,y1 -> x2,y1 -> x2,y2
|
||||
points.extend(mcrfpy.libtcod.line(x1, y1, x2, y1))
|
||||
points.extend(mcrfpy.libtcod.line(x2, y1, x2, y2))
|
||||
else:
|
||||
# x1,y1 -> x1,y2 -> x2,y2
|
||||
points.extend(mcrfpy.libtcod.line(x1, y1, x1, y2))
|
||||
points.extend(mcrfpy.libtcod.line(x1, y2, x2, y2))
|
||||
|
||||
for x, y in points:
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(FLOOR_TILES)
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
def generate_dungeon(max_rooms=10, room_min_size=4, room_max_size=10):
|
||||
rooms = []
|
||||
|
||||
# Fill with walls
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(WALL_TILES)
|
||||
point.walkable = False
|
||||
point.transparent = False
|
||||
|
||||
# Generate rooms
|
||||
for _ in range(max_rooms):
|
||||
w = random.randint(room_min_size, room_max_size)
|
||||
h = random.randint(room_min_size, room_max_size)
|
||||
x = random.randint(1, grid_width - w - 1)
|
||||
y = random.randint(1, grid_height - h - 1)
|
||||
|
||||
new_room = Room(x, y, w, h)
|
||||
|
||||
failed = False
|
||||
for other_room in rooms:
|
||||
if new_room.intersects(other_room):
|
||||
failed = True
|
||||
break
|
||||
|
||||
if not failed:
|
||||
carve_room(new_room)
|
||||
|
||||
if rooms:
|
||||
prev_x, prev_y = rooms[-1].center()
|
||||
new_x, new_y = new_room.center()
|
||||
carve_hallway(prev_x, prev_y, new_x, new_y)
|
||||
|
||||
rooms.append(new_room)
|
||||
|
||||
return rooms
|
||||
|
||||
# Generate the dungeon
|
||||
rooms = generate_dungeon(max_rooms=8, room_min_size=4, room_max_size=8)
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Spawn player in the first room
|
||||
if rooms:
|
||||
spawn_x, spawn_y = rooms[0].center()
|
||||
else:
|
||||
spawn_x, spawn_y = 4, 4
|
||||
|
||||
# Create a player entity
|
||||
player = mcrfpy.Entity(
|
||||
(spawn_x, spawn_y),
|
||||
texture=hero_texture,
|
||||
sprite_index=0
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
|
||||
# Create an enemy entity in another room (to demonstrate perspective switching)
|
||||
enemy = None
|
||||
if len(rooms) > 1:
|
||||
enemy_x, enemy_y = rooms[1].center()
|
||||
enemy = mcrfpy.Entity(
|
||||
(enemy_x, enemy_y),
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Enemy sprite
|
||||
)
|
||||
grid.entities.append(enemy)
|
||||
|
||||
# Set the grid perspective to the player by default
|
||||
# Note: The new perspective system uses entity references directly
|
||||
grid.perspective = player
|
||||
|
||||
# Initial FOV computation
|
||||
def update_fov():
|
||||
"""Update field of view from current perspective
|
||||
Referenced from test_tcod_fov_entities.py lines 89-118
|
||||
"""
|
||||
if grid.perspective == player:
|
||||
grid.compute_fov(int(player.x), int(player.y), radius=8, algorithm=0)
|
||||
player.update_visibility()
|
||||
elif enemy and grid.perspective == enemy:
|
||||
grid.compute_fov(int(enemy.x), int(enemy.y), radius=6, algorithm=0)
|
||||
enemy.update_visibility()
|
||||
|
||||
# Perform initial FOV calculation
|
||||
update_fov()
|
||||
|
||||
# Center grid on current perspective
|
||||
def center_on_perspective():
|
||||
if grid.perspective == player:
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||
elif enemy and grid.perspective == enemy:
|
||||
grid.center = (enemy.x + 0.5) * 16, (enemy.y + 0.5) * 16
|
||||
|
||||
center_on_perspective()
|
||||
|
||||
# Movement state tracking (from Part 3)
|
||||
is_moving = False
|
||||
move_queue = []
|
||||
current_destination = None
|
||||
current_move = None
|
||||
|
||||
# Store animation references
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
grid_anim_x = None
|
||||
grid_anim_y = None
|
||||
|
||||
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
|
||||
|
||||
is_moving = False
|
||||
current_move = None
|
||||
current_destination = None
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
|
||||
# Update FOV after movement
|
||||
update_fov()
|
||||
center_on_perspective()
|
||||
|
||||
if move_queue:
|
||||
next_move = move_queue.pop(0)
|
||||
process_move(next_move)
|
||||
|
||||
motion_speed = 0.20
|
||||
|
||||
def can_move_to(x, y):
|
||||
"""Check if a position is valid for movement"""
|
||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
||||
return False
|
||||
|
||||
point = grid.at(x, y)
|
||||
if point and point.walkable:
|
||||
return True
|
||||
return False
|
||||
|
||||
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
|
||||
|
||||
# Only allow player movement when in player perspective
|
||||
if grid.perspective != player:
|
||||
return
|
||||
|
||||
if is_moving:
|
||||
move_queue.clear()
|
||||
move_queue.append(key)
|
||||
return
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
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 new_x != px or new_y != py:
|
||||
if can_move_to(new_x, new_y):
|
||||
is_moving = True
|
||||
current_move = key
|
||||
current_destination = (new_x, new_y)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input"""
|
||||
if state == "start":
|
||||
# Movement keys
|
||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
||||
process_move(key)
|
||||
|
||||
# Perspective switching
|
||||
elif key == "Tab":
|
||||
# Switch perspective between player and enemy
|
||||
if enemy:
|
||||
if grid.perspective == player:
|
||||
grid.perspective = enemy
|
||||
print("Switched to enemy perspective")
|
||||
else:
|
||||
grid.perspective = player
|
||||
print("Switched to player perspective")
|
||||
|
||||
# Update FOV and camera for new perspective
|
||||
update_fov()
|
||||
center_on_perspective()
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add UI elements
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 4: Field of View",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
instructions = mcrfpy.Caption((150, 720),
|
||||
text="Use WASD/Arrows to move. Press Tab to switch perspective!",
|
||||
)
|
||||
instructions.font_size = 18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
# FOV info
|
||||
fov_caption = mcrfpy.Caption((150, 745),
|
||||
text="FOV: Player (radius 8) | Enemy visible in other room",
|
||||
)
|
||||
fov_caption.font_size = 16
|
||||
fov_caption.fill_color = mcrfpy.Color(100, 200, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(fov_caption)
|
||||
|
||||
# Debug info
|
||||
debug_caption = mcrfpy.Caption((10, 40),
|
||||
text=f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Perspective: Player",
|
||||
)
|
||||
debug_caption.font_size = 16
|
||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
||||
|
||||
# Update function for perspective display
|
||||
def update_perspective_display():
|
||||
current_perspective = "Player" if grid.perspective == player else "Enemy"
|
||||
debug_caption.text = f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Perspective: {current_perspective}"
|
||||
|
||||
if grid.perspective == player:
|
||||
fov_caption.text = "FOV: Player (radius 8) | Tab to switch perspective"
|
||||
else:
|
||||
fov_caption.text = "FOV: Enemy (radius 6) | Tab to switch perspective"
|
||||
|
||||
# Timer to update display
|
||||
def update_display(runtime):
|
||||
update_perspective_display()
|
||||
|
||||
mcrfpy.setTimer("display_update", update_display, 100)
|
||||
|
||||
print("Tutorial Part 4 loaded!")
|
||||
print("Field of View system active!")
|
||||
print("- Unexplored areas are black")
|
||||
print("- Previously seen areas are dark")
|
||||
print("- Currently visible areas are lit")
|
||||
print("Press Tab to switch between player and enemy perspective!")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
|
|
@ -1,363 +0,0 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 5: Interacting with other entities
|
||||
|
||||
This tutorial builds on Part 4 by adding:
|
||||
- Subclassing mcrfpy.Entity
|
||||
- Non-blocking movement animations with destination tracking
|
||||
- Bump interactions (combat, pushing)
|
||||
"""
|
||||
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
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
grid_width, grid_height = 40, 30
|
||||
|
||||
# Calculate the size in pixels
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# Calculate the position to center the grid on the screen
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
# Create the grid with a TCODMap for pathfinding/FOV
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size,
|
||||
)
|
||||
|
||||
grid.zoom = zoom
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Room class for BSP
|
||||
class Room:
|
||||
def __init__(self, x, y, w, h):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + w
|
||||
self.y2 = y + h
|
||||
self.w = w
|
||||
self.h = h
|
||||
|
||||
def center(self):
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return (center_x, center_y)
|
||||
|
||||
def intersects(self, other):
|
||||
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
|
||||
self.y1 <= other.y2 and self.y2 >= other.y1)
|
||||
|
||||
# Dungeon generation functions (from Part 3)
|
||||
def carve_room(room):
|
||||
for x in range(room.x1, room.x2):
|
||||
for y in range(room.y1, room.y2):
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(FLOOR_TILES)
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
def carve_hallway(x1, y1, x2, y2):
|
||||
#points = mcrfpy.libtcod.line(x1, y1, x2, y2)
|
||||
points = []
|
||||
if random.choice([True, False]):
|
||||
# x1,y1 -> x2,y1 -> x2,y2
|
||||
points.extend(mcrfpy.libtcod.line(x1, y1, x2, y1))
|
||||
points.extend(mcrfpy.libtcod.line(x2, y1, x2, y2))
|
||||
else:
|
||||
# x1,y1 -> x1,y2 -> x2,y2
|
||||
points.extend(mcrfpy.libtcod.line(x1, y1, x1, y2))
|
||||
points.extend(mcrfpy.libtcod.line(x1, y2, x2, y2))
|
||||
|
||||
for x, y in points:
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(FLOOR_TILES)
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
def generate_dungeon(max_rooms=10, room_min_size=4, room_max_size=10):
|
||||
rooms = []
|
||||
|
||||
# Fill with walls
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(WALL_TILES)
|
||||
point.walkable = False
|
||||
point.transparent = False
|
||||
|
||||
# Generate rooms
|
||||
for _ in range(max_rooms):
|
||||
w = random.randint(room_min_size, room_max_size)
|
||||
h = random.randint(room_min_size, room_max_size)
|
||||
x = random.randint(1, grid_width - w - 1)
|
||||
y = random.randint(1, grid_height - h - 1)
|
||||
|
||||
new_room = Room(x, y, w, h)
|
||||
|
||||
failed = False
|
||||
for other_room in rooms:
|
||||
if new_room.intersects(other_room):
|
||||
failed = True
|
||||
break
|
||||
|
||||
if not failed:
|
||||
carve_room(new_room)
|
||||
|
||||
if rooms:
|
||||
prev_x, prev_y = rooms[-1].center()
|
||||
new_x, new_y = new_room.center()
|
||||
carve_hallway(prev_x, prev_y, new_x, new_y)
|
||||
|
||||
rooms.append(new_room)
|
||||
|
||||
return rooms
|
||||
|
||||
# Generate the dungeon
|
||||
rooms = generate_dungeon(max_rooms=8, room_min_size=4, room_max_size=8)
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Spawn player in the first room
|
||||
if rooms:
|
||||
spawn_x, spawn_y = rooms[0].center()
|
||||
else:
|
||||
spawn_x, spawn_y = 4, 4
|
||||
|
||||
class GameEntity(mcrfpy.Entity):
|
||||
"""An entity whose default behavior is to prevent others from moving into its tile."""
|
||||
|
||||
def __init__(self, x, y, walkable=False, **kwargs):
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
self.walkable = walkable
|
||||
self.dest_x = x
|
||||
self.dest_y = y
|
||||
self.is_moving = False
|
||||
|
||||
def get_position(self):
|
||||
"""Get logical position (destination if moving, otherwise current)"""
|
||||
if self.is_moving:
|
||||
return (self.dest_x, self.dest_y)
|
||||
return (int(self.x), int(self.y))
|
||||
|
||||
def on_bump(self, other):
|
||||
return self.walkable # allow other's motion to proceed if entity is walkable
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} x={self.x}, y={self.y}, sprite_index={self.sprite_index}>"
|
||||
|
||||
class BumpableEntity(GameEntity):
|
||||
def __init__(self, x, y, **kwargs):
|
||||
super().__init__(x, y, **kwargs)
|
||||
|
||||
def on_bump(self, other):
|
||||
print(f"Watch it, {other}! You bumped into {self}!")
|
||||
return False
|
||||
|
||||
# Create a player entity
|
||||
player = GameEntity(
|
||||
spawn_x, spawn_y,
|
||||
texture=hero_texture,
|
||||
sprite_index=0
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
for r in rooms:
|
||||
enemy_x, enemy_y = r.center()
|
||||
enemy = BumpableEntity(
|
||||
enemy_x, enemy_y,
|
||||
grid=grid,
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Enemy sprite
|
||||
)
|
||||
|
||||
# Set the grid perspective to the player by default
|
||||
# Note: The new perspective system uses entity references directly
|
||||
grid.perspective = player
|
||||
|
||||
# Initial FOV computation
|
||||
def update_fov():
|
||||
"""Update field of view from current perspective
|
||||
Referenced from test_tcod_fov_entities.py lines 89-118
|
||||
"""
|
||||
if grid.perspective == player:
|
||||
grid.compute_fov(int(player.x), int(player.y), radius=8, algorithm=0)
|
||||
player.update_visibility()
|
||||
elif enemy and grid.perspective == enemy:
|
||||
grid.compute_fov(int(enemy.x), int(enemy.y), radius=6, algorithm=0)
|
||||
enemy.update_visibility()
|
||||
|
||||
# Perform initial FOV calculation
|
||||
update_fov()
|
||||
|
||||
# Center grid on current perspective
|
||||
def center_on_perspective():
|
||||
if grid.perspective == player:
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||
elif enemy and grid.perspective == enemy:
|
||||
grid.center = (enemy.x + 0.5) * 16, (enemy.y + 0.5) * 16
|
||||
|
||||
center_on_perspective()
|
||||
|
||||
# Movement state tracking (from Part 3)
|
||||
#is_moving = False # make it an entity property
|
||||
move_queue = []
|
||||
current_destination = None
|
||||
current_move = None
|
||||
|
||||
# Store animation references
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
grid_anim_x = None
|
||||
grid_anim_y = None
|
||||
|
||||
def movement_complete(anim, target):
|
||||
"""Called when movement animation completes"""
|
||||
global move_queue, current_destination, current_move
|
||||
global player_anim_x, player_anim_y
|
||||
|
||||
player.is_moving = False
|
||||
current_move = None
|
||||
current_destination = None
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
|
||||
# Update FOV after movement
|
||||
update_fov()
|
||||
center_on_perspective()
|
||||
|
||||
if move_queue:
|
||||
next_move = move_queue.pop(0)
|
||||
process_move(next_move)
|
||||
|
||||
motion_speed = 0.20
|
||||
|
||||
def can_move_to(x, y):
|
||||
"""Check if a position is valid for movement"""
|
||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
||||
return False
|
||||
|
||||
point = grid.at(x, y)
|
||||
if point and point.walkable:
|
||||
for e in grid.entities:
|
||||
if not e.walkable and (x, y) == e.get_position(): # blocking the way
|
||||
e.on_bump(player)
|
||||
return False
|
||||
return True # all checks passed, no collision
|
||||
return False
|
||||
|
||||
def process_move(key):
|
||||
"""Process a move based on the key"""
|
||||
global current_move, current_destination, move_queue
|
||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
||||
|
||||
# Only allow player movement when in player perspective
|
||||
if grid.perspective != player:
|
||||
return
|
||||
|
||||
if player.is_moving:
|
||||
move_queue.clear()
|
||||
move_queue.append(key)
|
||||
return
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
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 new_x != px or new_y != py:
|
||||
if can_move_to(new_x, new_y):
|
||||
player.is_moving = True
|
||||
current_move = key
|
||||
current_destination = (new_x, new_y)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input"""
|
||||
if state == "start":
|
||||
# Movement keys
|
||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
||||
process_move(key)
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add UI elements
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 5: Entity Collision",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
instructions = mcrfpy.Caption((150, 720),
|
||||
text="Use WASD/Arrows to move. Try to bump into the other entity!",
|
||||
)
|
||||
instructions.font_size = 18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
# Debug info
|
||||
debug_caption = mcrfpy.Caption((10, 40),
|
||||
text=f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Perspective: Player",
|
||||
)
|
||||
debug_caption.font_size = 16
|
||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
||||
|
||||
# Update function for perspective display
|
||||
def update_perspective_display():
|
||||
current_perspective = "Player" if grid.perspective == player else "Enemy"
|
||||
debug_caption.text = f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Perspective: {current_perspective}"
|
||||
|
||||
# Timer to update display
|
||||
def update_display(runtime):
|
||||
update_perspective_display()
|
||||
|
||||
mcrfpy.setTimer("display_update", update_display, 100)
|
||||
|
||||
print("Tutorial Part 4 loaded!")
|
||||
print("Field of View system active!")
|
||||
print("- Unexplored areas are black")
|
||||
print("- Previously seen areas are dark")
|
||||
print("- Currently visible areas are lit")
|
||||
print("Press Tab to switch between player and enemy perspective!")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
|
|
@ -1,645 +0,0 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 6: Turn-based enemy movement
|
||||
|
||||
This tutorial builds on Part 5 by adding:
|
||||
- Turn cycles where enemies move after the player
|
||||
- Enemy AI that pursues or wanders
|
||||
- Shared collision detection for all entities
|
||||
"""
|
||||
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
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
grid_width, grid_height = 40, 30
|
||||
|
||||
# Calculate the size in pixels
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# Calculate the position to center the grid on the screen
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
# Create the grid with a TCODMap for pathfinding/FOV
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size,
|
||||
)
|
||||
|
||||
grid.zoom = zoom
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Room class for BSP
|
||||
class Room:
|
||||
def __init__(self, x, y, w, h):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + w
|
||||
self.y2 = y + h
|
||||
self.w = w
|
||||
self.h = h
|
||||
|
||||
def center(self):
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return (center_x, center_y)
|
||||
|
||||
def intersects(self, other):
|
||||
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
|
||||
self.y1 <= other.y2 and self.y2 >= other.y1)
|
||||
|
||||
# Dungeon generation functions (from Part 3)
|
||||
def carve_room(room):
|
||||
for x in range(room.x1, room.x2):
|
||||
for y in range(room.y1, room.y2):
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(FLOOR_TILES)
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
def carve_hallway(x1, y1, x2, y2):
|
||||
#points = mcrfpy.libtcod.line(x1, y1, x2, y2)
|
||||
points = []
|
||||
if random.choice([True, False]):
|
||||
# x1,y1 -> x2,y1 -> x2,y2
|
||||
points.extend(mcrfpy.libtcod.line(x1, y1, x2, y1))
|
||||
points.extend(mcrfpy.libtcod.line(x2, y1, x2, y2))
|
||||
else:
|
||||
# x1,y1 -> x1,y2 -> x2,y2
|
||||
points.extend(mcrfpy.libtcod.line(x1, y1, x1, y2))
|
||||
points.extend(mcrfpy.libtcod.line(x1, y2, x2, y2))
|
||||
|
||||
for x, y in points:
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(FLOOR_TILES)
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
def generate_dungeon(max_rooms=10, room_min_size=4, room_max_size=10):
|
||||
rooms = []
|
||||
|
||||
# Fill with walls
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(WALL_TILES)
|
||||
point.walkable = False
|
||||
point.transparent = False
|
||||
|
||||
# Generate rooms
|
||||
for _ in range(max_rooms):
|
||||
w = random.randint(room_min_size, room_max_size)
|
||||
h = random.randint(room_min_size, room_max_size)
|
||||
x = random.randint(1, grid_width - w - 1)
|
||||
y = random.randint(1, grid_height - h - 1)
|
||||
|
||||
new_room = Room(x, y, w, h)
|
||||
|
||||
failed = False
|
||||
for other_room in rooms:
|
||||
if new_room.intersects(other_room):
|
||||
failed = True
|
||||
break
|
||||
|
||||
if not failed:
|
||||
carve_room(new_room)
|
||||
|
||||
if rooms:
|
||||
prev_x, prev_y = rooms[-1].center()
|
||||
new_x, new_y = new_room.center()
|
||||
carve_hallway(prev_x, prev_y, new_x, new_y)
|
||||
|
||||
rooms.append(new_room)
|
||||
|
||||
return rooms
|
||||
|
||||
# Generate the dungeon
|
||||
rooms = generate_dungeon(max_rooms=8, room_min_size=4, room_max_size=8)
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Spawn player in the first room
|
||||
if rooms:
|
||||
spawn_x, spawn_y = rooms[0].center()
|
||||
else:
|
||||
spawn_x, spawn_y = 4, 4
|
||||
|
||||
class GameEntity(mcrfpy.Entity):
|
||||
"""An entity whose default behavior is to prevent others from moving into its tile."""
|
||||
|
||||
def __init__(self, x, y, walkable=False, **kwargs):
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
self.walkable = walkable
|
||||
self.dest_x = x
|
||||
self.dest_y = y
|
||||
self.is_moving = False
|
||||
|
||||
def get_position(self):
|
||||
"""Get logical position (destination if moving, otherwise current)"""
|
||||
if self.is_moving:
|
||||
return (self.dest_x, self.dest_y)
|
||||
return (int(self.x), int(self.y))
|
||||
|
||||
def on_bump(self, other):
|
||||
return self.walkable # allow other's motion to proceed if entity is walkable
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} x={self.x}, y={self.y}, sprite_index={self.sprite_index}>"
|
||||
|
||||
class CombatEntity(GameEntity):
|
||||
def __init__(self, x, y, hp=10, damage=(1,3), **kwargs):
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
self.hp = hp
|
||||
self.damage = damage
|
||||
|
||||
def is_dead(self):
|
||||
return self.hp <= 0
|
||||
|
||||
def start_move(self, new_x, new_y, duration=0.2, callback=None):
|
||||
"""Start animating movement to new position"""
|
||||
self.dest_x = new_x
|
||||
self.dest_y = new_y
|
||||
self.is_moving = True
|
||||
|
||||
# Define completion callback that resets is_moving
|
||||
def movement_done(anim, entity):
|
||||
self.is_moving = False
|
||||
if callback:
|
||||
callback(anim, entity)
|
||||
|
||||
# Create animations for smooth movement
|
||||
anim_x = mcrfpy.Animation("x", float(new_x), duration, "easeInOutQuad", callback=movement_done)
|
||||
anim_y = mcrfpy.Animation("y", float(new_y), duration, "easeInOutQuad")
|
||||
|
||||
anim_x.start(self)
|
||||
anim_y.start(self)
|
||||
|
||||
def can_see(self, target_x, target_y):
|
||||
"""Check if this entity can see the target position"""
|
||||
mx, my = self.get_position()
|
||||
|
||||
# Simple distance check first
|
||||
dist = abs(target_x - mx) + abs(target_y - my)
|
||||
if dist > 6:
|
||||
return False
|
||||
|
||||
# Line of sight check
|
||||
line = list(mcrfpy.libtcod.line(mx, my, target_x, target_y))
|
||||
for x, y in line[1:-1]: # Skip start and end
|
||||
cell = grid.at(x, y)
|
||||
if cell and not cell.transparent:
|
||||
return False
|
||||
return True
|
||||
|
||||
def ai_turn(self, player_pos):
|
||||
"""Decide next move"""
|
||||
mx, my = self.get_position()
|
||||
px, py = player_pos
|
||||
|
||||
# Simple AI: move toward player if visible
|
||||
if self.can_see(px, py):
|
||||
# Calculate direction toward player
|
||||
dx = 0
|
||||
dy = 0
|
||||
if px > mx:
|
||||
dx = 1
|
||||
elif px < mx:
|
||||
dx = -1
|
||||
if py > my:
|
||||
dy = 1
|
||||
elif py < my:
|
||||
dy = -1
|
||||
|
||||
# Prefer cardinal movement
|
||||
if dx != 0 and dy != 0:
|
||||
# Pick horizontal or vertical based on greater distance
|
||||
if abs(px - mx) > abs(py - my):
|
||||
dy = 0
|
||||
else:
|
||||
dx = 0
|
||||
|
||||
return (mx + dx, my + dy)
|
||||
else:
|
||||
# Random wander
|
||||
dx, dy = random.choice([(0,1), (0,-1), (1,0), (-1,0)])
|
||||
return (mx + dx, my + dy)
|
||||
|
||||
def ai_turn_dijkstra(self):
|
||||
"""Decide next move using precomputed Dijkstra map"""
|
||||
mx, my = self.get_position()
|
||||
|
||||
# Get current distance to player
|
||||
current_dist = grid.get_dijkstra_distance(mx, my)
|
||||
if current_dist is None or current_dist > 20:
|
||||
# Too far or unreachable - random wander
|
||||
dx, dy = random.choice([(0,1), (0,-1), (1,0), (-1,0)])
|
||||
return (mx + dx, my + dy)
|
||||
|
||||
# Check all adjacent cells for best move
|
||||
best_moves = []
|
||||
for dx, dy in [(0,1), (0,-1), (1,0), (-1,0)]:
|
||||
nx, ny = mx + dx, my + dy
|
||||
|
||||
# Skip if out of bounds
|
||||
if nx < 0 or nx >= grid_width or ny < 0 or ny >= grid_height:
|
||||
continue
|
||||
|
||||
# Skip if not walkable
|
||||
cell = grid.at(nx, ny)
|
||||
if not cell or not cell.walkable:
|
||||
continue
|
||||
|
||||
# Get distance from this cell
|
||||
dist = grid.get_dijkstra_distance(nx, ny)
|
||||
if dist is not None:
|
||||
best_moves.append((dist, nx, ny))
|
||||
|
||||
if best_moves:
|
||||
# Sort by distance
|
||||
best_moves.sort()
|
||||
|
||||
# If multiple moves have the same best distance, pick randomly
|
||||
best_dist = best_moves[0][0]
|
||||
equal_moves = [(nx, ny) for dist, nx, ny in best_moves if dist == best_dist]
|
||||
|
||||
if len(equal_moves) > 1:
|
||||
# Random choice among equally good moves
|
||||
nx, ny = random.choice(equal_moves)
|
||||
else:
|
||||
_, nx, ny = best_moves[0]
|
||||
|
||||
return (nx, ny)
|
||||
else:
|
||||
# No valid moves
|
||||
return (mx, my)
|
||||
|
||||
# Create a player entity
|
||||
player = CombatEntity(
|
||||
spawn_x, spawn_y,
|
||||
texture=hero_texture,
|
||||
sprite_index=0
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
|
||||
# Track all enemies
|
||||
enemies = []
|
||||
|
||||
# Spawn enemies in other rooms
|
||||
for i, room in enumerate(rooms[1:], 1): # Skip first room (player spawn)
|
||||
if i <= 3: # Limit to 3 enemies for now
|
||||
enemy_x, enemy_y = room.center()
|
||||
enemy = CombatEntity(
|
||||
enemy_x, enemy_y,
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Enemy sprite (borrow player's)
|
||||
)
|
||||
grid.entities.append(enemy)
|
||||
enemies.append(enemy)
|
||||
|
||||
# Set the grid perspective to the player by default
|
||||
# Note: The new perspective system uses entity references directly
|
||||
grid.perspective = player
|
||||
|
||||
# Initial FOV computation
|
||||
def update_fov():
|
||||
"""Update field of view from current perspective"""
|
||||
if grid.perspective == player:
|
||||
grid.compute_fov(int(player.x), int(player.y), radius=8, algorithm=0)
|
||||
player.update_visibility()
|
||||
|
||||
# Perform initial FOV calculation
|
||||
update_fov()
|
||||
|
||||
# Center grid on current perspective
|
||||
def center_on_perspective():
|
||||
if grid.perspective == player:
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||
|
||||
center_on_perspective()
|
||||
|
||||
# Movement state tracking (from Part 3)
|
||||
#is_moving = False # make it an entity property
|
||||
move_queue = []
|
||||
current_destination = None
|
||||
current_move = None
|
||||
|
||||
# Store animation references
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
grid_anim_x = None
|
||||
grid_anim_y = None
|
||||
|
||||
def movement_complete(anim, target):
|
||||
"""Called when movement animation completes"""
|
||||
global move_queue, current_destination, current_move
|
||||
global player_anim_x, player_anim_y, is_player_turn
|
||||
|
||||
player.is_moving = False
|
||||
current_move = None
|
||||
current_destination = None
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
|
||||
# Update FOV after movement
|
||||
update_fov()
|
||||
center_on_perspective()
|
||||
|
||||
# Player turn complete, start enemy turns and queued player move simultaneously
|
||||
is_player_turn = False
|
||||
process_enemy_turns_and_player_queue()
|
||||
|
||||
motion_speed = 0.20
|
||||
is_player_turn = True # Track whose turn it is
|
||||
|
||||
def get_blocking_entity_at(x, y):
|
||||
"""Get blocking entity at position"""
|
||||
for e in grid.entities:
|
||||
if not e.walkable and (x, y) == e.get_position():
|
||||
return e
|
||||
return None
|
||||
|
||||
def can_move_to(x, y, mover=None):
|
||||
"""Check if a position is valid for movement"""
|
||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
||||
return False
|
||||
|
||||
point = grid.at(x, y)
|
||||
if not point or not point.walkable:
|
||||
return False
|
||||
|
||||
# Check for blocking entities
|
||||
blocker = get_blocking_entity_at(x, y)
|
||||
if blocker and blocker != mover:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def process_enemy_turns_and_player_queue():
|
||||
"""Process all enemy AI decisions and player's queued move simultaneously"""
|
||||
global is_player_turn, move_queue
|
||||
|
||||
# Compute Dijkstra map once for all enemies (if using Dijkstra)
|
||||
if USE_DIJKSTRA:
|
||||
px, py = player.get_position()
|
||||
grid.compute_dijkstra(px, py, diagonal_cost=1.41)
|
||||
|
||||
enemies_to_move = []
|
||||
claimed_positions = set() # Track where enemies plan to move
|
||||
|
||||
# Collect all enemy moves
|
||||
for i, enemy in enumerate(enemies):
|
||||
if enemy.is_dead():
|
||||
continue
|
||||
|
||||
# AI decides next move
|
||||
if USE_DIJKSTRA:
|
||||
target_x, target_y = enemy.ai_turn_dijkstra()
|
||||
else:
|
||||
target_x, target_y = enemy.ai_turn(player.get_position())
|
||||
|
||||
# Check if move is valid and not claimed by another enemy
|
||||
if can_move_to(target_x, target_y, enemy) and (target_x, target_y) not in claimed_positions:
|
||||
enemies_to_move.append((enemy, target_x, target_y))
|
||||
claimed_positions.add((target_x, target_y))
|
||||
|
||||
# Start all enemy animations simultaneously
|
||||
any_enemy_moved = False
|
||||
if enemies_to_move:
|
||||
for enemy, tx, ty in enemies_to_move:
|
||||
enemy.start_move(tx, ty, duration=motion_speed)
|
||||
any_enemy_moved = True
|
||||
|
||||
# Process player's queued move at the same time
|
||||
if move_queue:
|
||||
next_move = move_queue.pop(0)
|
||||
process_player_queued_move(next_move)
|
||||
else:
|
||||
# No queued move, set up callback to return control when animations finish
|
||||
if any_enemy_moved:
|
||||
# Wait for animations to complete
|
||||
mcrfpy.setTimer("turn_complete", check_turn_complete, int(motion_speed * 1000) + 50)
|
||||
else:
|
||||
# No animations, return control immediately
|
||||
is_player_turn = True
|
||||
|
||||
def process_player_queued_move(key):
|
||||
"""Process player's queued move during enemy turn"""
|
||||
global current_move, current_destination
|
||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
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 new_x != px or new_y != py:
|
||||
# Check destination at animation end time (considering enemy destinations)
|
||||
future_blocker = get_future_blocking_entity_at(new_x, new_y)
|
||||
|
||||
if future_blocker:
|
||||
# Will bump at destination
|
||||
# Schedule bump for when animations complete
|
||||
mcrfpy.setTimer("delayed_bump", lambda t: handle_delayed_bump(future_blocker), int(motion_speed * 1000))
|
||||
elif can_move_to(new_x, new_y, player):
|
||||
# Valid move, start animation
|
||||
player.is_moving = True
|
||||
current_move = key
|
||||
current_destination = (new_x, new_y)
|
||||
player.dest_x = new_x
|
||||
player.dest_y = new_y
|
||||
|
||||
# Player animation with callback
|
||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=player_queued_move_complete)
|
||||
player_anim_x.start(player)
|
||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
||||
player_anim_y.start(player)
|
||||
|
||||
# Move camera with 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)
|
||||
else:
|
||||
# Blocked by wall, wait for turn to complete
|
||||
mcrfpy.setTimer("turn_complete", check_turn_complete, int(motion_speed * 1000) + 50)
|
||||
|
||||
def get_future_blocking_entity_at(x, y):
|
||||
"""Get entity that will be blocking at position after current animations"""
|
||||
for e in grid.entities:
|
||||
if not e.walkable and (x, y) == (e.dest_x, e.dest_y):
|
||||
return e
|
||||
return None
|
||||
|
||||
def handle_delayed_bump(entity):
|
||||
"""Handle bump after animations complete"""
|
||||
global is_player_turn
|
||||
entity.on_bump(player)
|
||||
is_player_turn = True
|
||||
|
||||
def player_queued_move_complete(anim, target):
|
||||
"""Called when player's queued movement completes"""
|
||||
global is_player_turn
|
||||
player.is_moving = False
|
||||
update_fov()
|
||||
center_on_perspective()
|
||||
is_player_turn = True
|
||||
|
||||
def check_turn_complete(timer_name):
|
||||
"""Check if all animations are complete"""
|
||||
global is_player_turn
|
||||
|
||||
# Check if any entity is still moving
|
||||
if player.is_moving:
|
||||
mcrfpy.setTimer("turn_complete", check_turn_complete, 50)
|
||||
return
|
||||
|
||||
for enemy in enemies:
|
||||
if enemy.is_moving:
|
||||
mcrfpy.setTimer("turn_complete", check_turn_complete, 50)
|
||||
return
|
||||
|
||||
# All done
|
||||
is_player_turn = True
|
||||
|
||||
def process_move(key):
|
||||
"""Process a move based on the key"""
|
||||
global current_move, current_destination, move_queue
|
||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y, is_player_turn
|
||||
|
||||
# Only allow player movement on player's turn
|
||||
if not is_player_turn:
|
||||
return
|
||||
|
||||
# Only allow player movement when in player perspective
|
||||
if grid.perspective != player:
|
||||
return
|
||||
|
||||
if player.is_moving:
|
||||
move_queue.clear()
|
||||
move_queue.append(key)
|
||||
return
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
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 new_x != px or new_y != py:
|
||||
# Check what's at destination
|
||||
blocker = get_blocking_entity_at(new_x, new_y)
|
||||
|
||||
if blocker:
|
||||
# Bump interaction (combat will go here later)
|
||||
blocker.on_bump(player)
|
||||
# Still counts as a turn
|
||||
is_player_turn = False
|
||||
process_enemy_turns_and_player_queue()
|
||||
elif can_move_to(new_x, new_y, player):
|
||||
player.is_moving = True
|
||||
current_move = key
|
||||
current_destination = (new_x, new_y)
|
||||
player.dest_x = new_x
|
||||
player.dest_y = new_y
|
||||
|
||||
# Start player move animation
|
||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
||||
player_anim_x.start(player)
|
||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
||||
player_anim_y.start(player)
|
||||
|
||||
# Move camera with 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)
|
||||
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input"""
|
||||
if state == "start":
|
||||
# Movement keys
|
||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
||||
process_move(key)
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add UI elements
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 6: Turn-based Movement",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
instructions = mcrfpy.Caption((150, 720),
|
||||
text="Use WASD/Arrows to move. Enemies move after you!",
|
||||
)
|
||||
instructions.font_size = 18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
# Debug info
|
||||
debug_caption = mcrfpy.Caption((10, 40),
|
||||
text=f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Enemies: {len(enemies)}",
|
||||
)
|
||||
debug_caption.font_size = 16
|
||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
||||
|
||||
# Update function for turn display
|
||||
def update_turn_display():
|
||||
turn_text = "Player" if is_player_turn else "Enemy"
|
||||
alive_enemies = sum(1 for e in enemies if not e.is_dead())
|
||||
debug_caption.text = f"Grid: {grid_width}x{grid_height} | Turn: {turn_text} | Enemies: {alive_enemies}/{len(enemies)}"
|
||||
|
||||
# Configuration toggle
|
||||
USE_DIJKSTRA = True # Set to False to use old line-of-sight AI
|
||||
|
||||
# Timer to update display
|
||||
def update_display(runtime):
|
||||
update_turn_display()
|
||||
|
||||
mcrfpy.setTimer("display_update", update_display, 100)
|
||||
|
||||
print("Tutorial Part 6 loaded!")
|
||||
print("Turn-based movement system active!")
|
||||
print(f"Using {'Dijkstra' if USE_DIJKSTRA else 'Line-of-sight'} AI pathfinding")
|
||||
print("- Enemies move after the player")
|
||||
print("- Enemies pursue when they can see you" if not USE_DIJKSTRA else "- Enemies use optimal pathfinding")
|
||||
print("- Enemies wander when they can't" if not USE_DIJKSTRA else "- All enemies share one pathfinding map")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
|
|
@ -1,582 +0,0 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 6: Turn-based enemy movement
|
||||
|
||||
This tutorial builds on Part 5 by adding:
|
||||
- Turn cycles where enemies move after the player
|
||||
- Enemy AI that pursues or wanders
|
||||
- Shared collision detection for all entities
|
||||
"""
|
||||
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
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
grid_width, grid_height = 40, 30
|
||||
|
||||
# Calculate the size in pixels
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# Calculate the position to center the grid on the screen
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
# Create the grid with a TCODMap for pathfinding/FOV
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size,
|
||||
)
|
||||
|
||||
grid.zoom = zoom
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Room class for BSP
|
||||
class Room:
|
||||
def __init__(self, x, y, w, h):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + w
|
||||
self.y2 = y + h
|
||||
self.w = w
|
||||
self.h = h
|
||||
|
||||
def center(self):
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return (center_x, center_y)
|
||||
|
||||
def intersects(self, other):
|
||||
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
|
||||
self.y1 <= other.y2 and self.y2 >= other.y1)
|
||||
|
||||
# Dungeon generation functions (from Part 3)
|
||||
def carve_room(room):
|
||||
for x in range(room.x1, room.x2):
|
||||
for y in range(room.y1, room.y2):
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(FLOOR_TILES)
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
def carve_hallway(x1, y1, x2, y2):
|
||||
#points = mcrfpy.libtcod.line(x1, y1, x2, y2)
|
||||
points = []
|
||||
if random.choice([True, False]):
|
||||
# x1,y1 -> x2,y1 -> x2,y2
|
||||
points.extend(mcrfpy.libtcod.line(x1, y1, x2, y1))
|
||||
points.extend(mcrfpy.libtcod.line(x2, y1, x2, y2))
|
||||
else:
|
||||
# x1,y1 -> x1,y2 -> x2,y2
|
||||
points.extend(mcrfpy.libtcod.line(x1, y1, x1, y2))
|
||||
points.extend(mcrfpy.libtcod.line(x1, y2, x2, y2))
|
||||
|
||||
for x, y in points:
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(FLOOR_TILES)
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
def generate_dungeon(max_rooms=10, room_min_size=4, room_max_size=10):
|
||||
rooms = []
|
||||
|
||||
# Fill with walls
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = random.choice(WALL_TILES)
|
||||
point.walkable = False
|
||||
point.transparent = False
|
||||
|
||||
# Generate rooms
|
||||
for _ in range(max_rooms):
|
||||
w = random.randint(room_min_size, room_max_size)
|
||||
h = random.randint(room_min_size, room_max_size)
|
||||
x = random.randint(1, grid_width - w - 1)
|
||||
y = random.randint(1, grid_height - h - 1)
|
||||
|
||||
new_room = Room(x, y, w, h)
|
||||
|
||||
failed = False
|
||||
for other_room in rooms:
|
||||
if new_room.intersects(other_room):
|
||||
failed = True
|
||||
break
|
||||
|
||||
if not failed:
|
||||
carve_room(new_room)
|
||||
|
||||
if rooms:
|
||||
prev_x, prev_y = rooms[-1].center()
|
||||
new_x, new_y = new_room.center()
|
||||
carve_hallway(prev_x, prev_y, new_x, new_y)
|
||||
|
||||
rooms.append(new_room)
|
||||
|
||||
return rooms
|
||||
|
||||
# Generate the dungeon
|
||||
rooms = generate_dungeon(max_rooms=8, room_min_size=4, room_max_size=8)
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Spawn player in the first room
|
||||
if rooms:
|
||||
spawn_x, spawn_y = rooms[0].center()
|
||||
else:
|
||||
spawn_x, spawn_y = 4, 4
|
||||
|
||||
class GameEntity(mcrfpy.Entity):
|
||||
"""An entity whose default behavior is to prevent others from moving into its tile."""
|
||||
|
||||
def __init__(self, x, y, walkable=False, **kwargs):
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
self.walkable = walkable
|
||||
self.dest_x = x
|
||||
self.dest_y = y
|
||||
self.is_moving = False
|
||||
|
||||
def get_position(self):
|
||||
"""Get logical position (destination if moving, otherwise current)"""
|
||||
if self.is_moving:
|
||||
return (self.dest_x, self.dest_y)
|
||||
return (int(self.x), int(self.y))
|
||||
|
||||
def on_bump(self, other):
|
||||
return self.walkable # allow other's motion to proceed if entity is walkable
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} x={self.x}, y={self.y}, sprite_index={self.sprite_index}>"
|
||||
|
||||
class CombatEntity(GameEntity):
|
||||
def __init__(self, x, y, hp=10, damage=(1,3), **kwargs):
|
||||
super().__init__(x=x, y=y, **kwargs)
|
||||
self.hp = hp
|
||||
self.damage = damage
|
||||
|
||||
def is_dead(self):
|
||||
return self.hp <= 0
|
||||
|
||||
def start_move(self, new_x, new_y, duration=0.2, callback=None):
|
||||
"""Start animating movement to new position"""
|
||||
self.dest_x = new_x
|
||||
self.dest_y = new_y
|
||||
self.is_moving = True
|
||||
|
||||
# Define completion callback that resets is_moving
|
||||
def movement_done(anim, entity):
|
||||
self.is_moving = False
|
||||
if callback:
|
||||
callback(anim, entity)
|
||||
|
||||
# Create animations for smooth movement
|
||||
anim_x = mcrfpy.Animation("x", float(new_x), duration, "easeInOutQuad", callback=movement_done)
|
||||
anim_y = mcrfpy.Animation("y", float(new_y), duration, "easeInOutQuad")
|
||||
|
||||
anim_x.start(self)
|
||||
anim_y.start(self)
|
||||
|
||||
def can_see(self, target_x, target_y):
|
||||
"""Check if this entity can see the target position"""
|
||||
mx, my = self.get_position()
|
||||
|
||||
# Simple distance check first
|
||||
dist = abs(target_x - mx) + abs(target_y - my)
|
||||
if dist > 6:
|
||||
return False
|
||||
|
||||
# Line of sight check
|
||||
line = list(mcrfpy.libtcod.line(mx, my, target_x, target_y))
|
||||
for x, y in line[1:-1]: # Skip start and end
|
||||
cell = grid.at(x, y)
|
||||
if cell and not cell.transparent:
|
||||
return False
|
||||
return True
|
||||
|
||||
def ai_turn(self, player_pos):
|
||||
"""Decide next move"""
|
||||
mx, my = self.get_position()
|
||||
px, py = player_pos
|
||||
|
||||
# Simple AI: move toward player if visible
|
||||
if self.can_see(px, py):
|
||||
# Calculate direction toward player
|
||||
dx = 0
|
||||
dy = 0
|
||||
if px > mx:
|
||||
dx = 1
|
||||
elif px < mx:
|
||||
dx = -1
|
||||
if py > my:
|
||||
dy = 1
|
||||
elif py < my:
|
||||
dy = -1
|
||||
|
||||
# Prefer cardinal movement
|
||||
if dx != 0 and dy != 0:
|
||||
# Pick horizontal or vertical based on greater distance
|
||||
if abs(px - mx) > abs(py - my):
|
||||
dy = 0
|
||||
else:
|
||||
dx = 0
|
||||
|
||||
return (mx + dx, my + dy)
|
||||
else:
|
||||
# Random wander
|
||||
dx, dy = random.choice([(0,1), (0,-1), (1,0), (-1,0)])
|
||||
return (mx + dx, my + dy)
|
||||
|
||||
# Create a player entity
|
||||
player = CombatEntity(
|
||||
spawn_x, spawn_y,
|
||||
texture=hero_texture,
|
||||
sprite_index=0
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
|
||||
# Track all enemies
|
||||
enemies = []
|
||||
|
||||
# Spawn enemies in other rooms
|
||||
for i, room in enumerate(rooms[1:], 1): # Skip first room (player spawn)
|
||||
if i <= 3: # Limit to 3 enemies for now
|
||||
enemy_x, enemy_y = room.center()
|
||||
enemy = CombatEntity(
|
||||
enemy_x, enemy_y,
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Enemy sprite (borrow player's)
|
||||
)
|
||||
grid.entities.append(enemy)
|
||||
enemies.append(enemy)
|
||||
|
||||
# Set the grid perspective to the player by default
|
||||
# Note: The new perspective system uses entity references directly
|
||||
grid.perspective = player
|
||||
|
||||
# Initial FOV computation
|
||||
def update_fov():
|
||||
"""Update field of view from current perspective"""
|
||||
if grid.perspective == player:
|
||||
grid.compute_fov(int(player.x), int(player.y), radius=8, algorithm=0)
|
||||
player.update_visibility()
|
||||
|
||||
# Perform initial FOV calculation
|
||||
update_fov()
|
||||
|
||||
# Center grid on current perspective
|
||||
def center_on_perspective():
|
||||
if grid.perspective == player:
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||
|
||||
center_on_perspective()
|
||||
|
||||
# Movement state tracking (from Part 3)
|
||||
#is_moving = False # make it an entity property
|
||||
move_queue = []
|
||||
current_destination = None
|
||||
current_move = None
|
||||
|
||||
# Store animation references
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
grid_anim_x = None
|
||||
grid_anim_y = None
|
||||
|
||||
def movement_complete(anim, target):
|
||||
"""Called when movement animation completes"""
|
||||
global move_queue, current_destination, current_move
|
||||
global player_anim_x, player_anim_y, is_player_turn
|
||||
|
||||
player.is_moving = False
|
||||
current_move = None
|
||||
current_destination = None
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
|
||||
# Update FOV after movement
|
||||
update_fov()
|
||||
center_on_perspective()
|
||||
|
||||
# Player turn complete, start enemy turns and queued player move simultaneously
|
||||
is_player_turn = False
|
||||
process_enemy_turns_and_player_queue()
|
||||
|
||||
motion_speed = 0.20
|
||||
is_player_turn = True # Track whose turn it is
|
||||
|
||||
def get_blocking_entity_at(x, y):
|
||||
"""Get blocking entity at position"""
|
||||
for e in grid.entities:
|
||||
if not e.walkable and (x, y) == e.get_position():
|
||||
return e
|
||||
return None
|
||||
|
||||
def can_move_to(x, y, mover=None):
|
||||
"""Check if a position is valid for movement"""
|
||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
||||
return False
|
||||
|
||||
point = grid.at(x, y)
|
||||
if not point or not point.walkable:
|
||||
return False
|
||||
|
||||
# Check for blocking entities
|
||||
blocker = get_blocking_entity_at(x, y)
|
||||
if blocker and blocker != mover:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def process_enemy_turns_and_player_queue():
|
||||
"""Process all enemy AI decisions and player's queued move simultaneously"""
|
||||
global is_player_turn, move_queue
|
||||
|
||||
enemies_to_move = []
|
||||
|
||||
# Collect all enemy moves
|
||||
for i, enemy in enumerate(enemies):
|
||||
if enemy.is_dead():
|
||||
continue
|
||||
|
||||
# AI decides next move based on player's position
|
||||
target_x, target_y = enemy.ai_turn(player.get_position())
|
||||
|
||||
# Check if move is valid
|
||||
if can_move_to(target_x, target_y, enemy):
|
||||
enemies_to_move.append((enemy, target_x, target_y))
|
||||
|
||||
# Start all enemy animations simultaneously
|
||||
any_enemy_moved = False
|
||||
if enemies_to_move:
|
||||
for enemy, tx, ty in enemies_to_move:
|
||||
enemy.start_move(tx, ty, duration=motion_speed)
|
||||
any_enemy_moved = True
|
||||
|
||||
# Process player's queued move at the same time
|
||||
if move_queue:
|
||||
next_move = move_queue.pop(0)
|
||||
process_player_queued_move(next_move)
|
||||
else:
|
||||
# No queued move, set up callback to return control when animations finish
|
||||
if any_enemy_moved:
|
||||
# Wait for animations to complete
|
||||
mcrfpy.setTimer("turn_complete", check_turn_complete, int(motion_speed * 1000) + 50)
|
||||
else:
|
||||
# No animations, return control immediately
|
||||
is_player_turn = True
|
||||
|
||||
def process_player_queued_move(key):
|
||||
"""Process player's queued move during enemy turn"""
|
||||
global current_move, current_destination
|
||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
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 new_x != px or new_y != py:
|
||||
# Check destination at animation end time (considering enemy destinations)
|
||||
future_blocker = get_future_blocking_entity_at(new_x, new_y)
|
||||
|
||||
if future_blocker:
|
||||
# Will bump at destination
|
||||
# Schedule bump for when animations complete
|
||||
mcrfpy.setTimer("delayed_bump", lambda t: handle_delayed_bump(future_blocker), int(motion_speed * 1000))
|
||||
elif can_move_to(new_x, new_y, player):
|
||||
# Valid move, start animation
|
||||
player.is_moving = True
|
||||
current_move = key
|
||||
current_destination = (new_x, new_y)
|
||||
player.dest_x = new_x
|
||||
player.dest_y = new_y
|
||||
|
||||
# Player animation with callback
|
||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=player_queued_move_complete)
|
||||
player_anim_x.start(player)
|
||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
||||
player_anim_y.start(player)
|
||||
|
||||
# Move camera with 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)
|
||||
else:
|
||||
# Blocked by wall, wait for turn to complete
|
||||
mcrfpy.setTimer("turn_complete", check_turn_complete, int(motion_speed * 1000) + 50)
|
||||
|
||||
def get_future_blocking_entity_at(x, y):
|
||||
"""Get entity that will be blocking at position after current animations"""
|
||||
for e in grid.entities:
|
||||
if not e.walkable and (x, y) == (e.dest_x, e.dest_y):
|
||||
return e
|
||||
return None
|
||||
|
||||
def handle_delayed_bump(entity):
|
||||
"""Handle bump after animations complete"""
|
||||
global is_player_turn
|
||||
entity.on_bump(player)
|
||||
is_player_turn = True
|
||||
|
||||
def player_queued_move_complete(anim, target):
|
||||
"""Called when player's queued movement completes"""
|
||||
global is_player_turn
|
||||
player.is_moving = False
|
||||
update_fov()
|
||||
center_on_perspective()
|
||||
is_player_turn = True
|
||||
|
||||
def check_turn_complete(timer_name):
|
||||
"""Check if all animations are complete"""
|
||||
global is_player_turn
|
||||
|
||||
# Check if any entity is still moving
|
||||
if player.is_moving:
|
||||
mcrfpy.setTimer("turn_complete", check_turn_complete, 50)
|
||||
return
|
||||
|
||||
for enemy in enemies:
|
||||
if enemy.is_moving:
|
||||
mcrfpy.setTimer("turn_complete", check_turn_complete, 50)
|
||||
return
|
||||
|
||||
# All done
|
||||
is_player_turn = True
|
||||
|
||||
def process_move(key):
|
||||
"""Process a move based on the key"""
|
||||
global current_move, current_destination, move_queue
|
||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y, is_player_turn
|
||||
|
||||
# Only allow player movement on player's turn
|
||||
if not is_player_turn:
|
||||
return
|
||||
|
||||
# Only allow player movement when in player perspective
|
||||
if grid.perspective != player:
|
||||
return
|
||||
|
||||
if player.is_moving:
|
||||
move_queue.clear()
|
||||
move_queue.append(key)
|
||||
return
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
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 new_x != px or new_y != py:
|
||||
# Check what's at destination
|
||||
blocker = get_blocking_entity_at(new_x, new_y)
|
||||
|
||||
if blocker:
|
||||
# Bump interaction (combat will go here later)
|
||||
blocker.on_bump(player)
|
||||
# Still counts as a turn
|
||||
is_player_turn = False
|
||||
process_enemy_turns_and_player_queue()
|
||||
elif can_move_to(new_x, new_y, player):
|
||||
player.is_moving = True
|
||||
current_move = key
|
||||
current_destination = (new_x, new_y)
|
||||
player.dest_x = new_x
|
||||
player.dest_y = new_y
|
||||
|
||||
# Start player move animation
|
||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
||||
player_anim_x.start(player)
|
||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
||||
player_anim_y.start(player)
|
||||
|
||||
# Move camera with 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)
|
||||
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input"""
|
||||
if state == "start":
|
||||
# Movement keys
|
||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
||||
process_move(key)
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add UI elements
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 6: Turn-based Movement",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
instructions = mcrfpy.Caption((150, 720),
|
||||
text="Use WASD/Arrows to move. Enemies move after you!",
|
||||
)
|
||||
instructions.font_size = 18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
# Debug info
|
||||
debug_caption = mcrfpy.Caption((10, 40),
|
||||
text=f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Enemies: {len(enemies)}",
|
||||
)
|
||||
debug_caption.font_size = 16
|
||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
||||
|
||||
# Update function for turn display
|
||||
def update_turn_display():
|
||||
turn_text = "Player" if is_player_turn else "Enemy"
|
||||
alive_enemies = sum(1 for e in enemies if not e.is_dead())
|
||||
debug_caption.text = f"Grid: {grid_width}x{grid_height} | Turn: {turn_text} | Enemies: {alive_enemies}/{len(enemies)}"
|
||||
|
||||
# Timer to update display
|
||||
def update_display(runtime):
|
||||
update_turn_display()
|
||||
|
||||
mcrfpy.setTimer("display_update", update_display, 100)
|
||||
|
||||
print("Tutorial Part 6 loaded!")
|
||||
print("Turn-based movement system active!")
|
||||
print("- Enemies move after the player")
|
||||
print("- Enemies pursue when they can see you")
|
||||
print("- Enemies wander when they can't")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
#include "UIEntity.h"
|
||||
#include "PyAnimation.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
|
|
@ -47,11 +46,6 @@ Animation::~Animation() {
|
|||
Py_DECREF(callback);
|
||||
PyGILState_Release(gstate);
|
||||
}
|
||||
|
||||
// Clean up cache entry
|
||||
if (serial_number != 0) {
|
||||
PythonObjectCache::getInstance().remove(serial_number);
|
||||
}
|
||||
}
|
||||
|
||||
void Animation::start(std::shared_ptr<UIDrawable> target) {
|
||||
|
|
|
|||
|
|
@ -90,9 +90,6 @@ private:
|
|||
bool callbackTriggered = false; // Ensure callback only fires once
|
||||
PyObject* pyAnimationWrapper = nullptr; // Weak reference to PyAnimation if created from Python
|
||||
|
||||
// Python object cache support
|
||||
uint64_t serial_number = 0;
|
||||
|
||||
// Helper to interpolate between values
|
||||
AnimationValue interpolate(float t) const;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
#include "UITestScene.h"
|
||||
#include "Resources.h"
|
||||
#include "Animation.h"
|
||||
#include "Timer.h"
|
||||
#include <cmath>
|
||||
|
||||
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
|
||||
|
|
@ -42,11 +41,8 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
|||
updateViewport();
|
||||
scene = "uitest";
|
||||
scenes["uitest"] = new UITestScene(this);
|
||||
|
||||
|
||||
McRFPy_API::game = this;
|
||||
|
||||
// Initialize profiler overlay
|
||||
profilerOverlay = new ProfilerOverlay(Resources::font);
|
||||
|
||||
// Only load game.py if no custom script/command/module/exec is specified
|
||||
bool should_load_game = config.script_path.empty() &&
|
||||
|
|
@ -88,7 +84,6 @@ GameEngine::~GameEngine()
|
|||
for (auto& [name, scene] : scenes) {
|
||||
delete scene;
|
||||
}
|
||||
delete profilerOverlay;
|
||||
}
|
||||
|
||||
void GameEngine::cleanup()
|
||||
|
|
@ -203,14 +198,10 @@ void GameEngine::run()
|
|||
testTimers();
|
||||
|
||||
// Update Python scenes
|
||||
{
|
||||
ScopedTimer pyTimer(metrics.pythonScriptTime);
|
||||
McRFPy_API::updatePythonScenes(frameTime);
|
||||
}
|
||||
McRFPy_API::updatePythonScenes(frameTime);
|
||||
|
||||
// Update animations (only if frameTime is valid)
|
||||
if (frameTime > 0.0f && frameTime < 1.0f) {
|
||||
ScopedTimer animTimer(metrics.animationTime);
|
||||
AnimationManager::getInstance().update(frameTime);
|
||||
}
|
||||
|
||||
|
|
@ -248,12 +239,6 @@ void GameEngine::run()
|
|||
currentScene()->render();
|
||||
}
|
||||
|
||||
// Update and render profiler overlay (if enabled)
|
||||
if (profilerOverlay && !headless) {
|
||||
profilerOverlay->update(metrics);
|
||||
profilerOverlay->render(*render_target);
|
||||
}
|
||||
|
||||
// Display the frame
|
||||
if (headless) {
|
||||
headless_renderer->display();
|
||||
|
|
@ -284,18 +269,13 @@ void GameEngine::run()
|
|||
if (!headless && window && !window->isOpen()) {
|
||||
running = false;
|
||||
}
|
||||
|
||||
// In headless exec mode, auto-exit when no timers remain
|
||||
if (config.auto_exit_after_exec && timers.empty()) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up before exiting the run loop
|
||||
cleanup();
|
||||
}
|
||||
|
||||
std::shared_ptr<Timer> GameEngine::getTimer(const std::string& name)
|
||||
std::shared_ptr<PyTimerCallable> GameEngine::getTimer(const std::string& name)
|
||||
{
|
||||
auto it = timers.find(name);
|
||||
if (it != timers.end()) {
|
||||
|
|
@ -313,7 +293,7 @@ void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
|
|||
{
|
||||
// Delete: Overwrite existing timer with one that calls None. This will be deleted in the next timer check
|
||||
// see gitea issue #4: this allows for a timer to be deleted during its own call to itself
|
||||
timers[name] = std::make_shared<Timer>(Py_None, 1000, runtime.getElapsedTime().asMilliseconds());
|
||||
timers[name] = std::make_shared<PyTimerCallable>(Py_None, 1000, runtime.getElapsedTime().asMilliseconds());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -322,7 +302,7 @@ void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
|
|||
std::cout << "Refusing to initialize timer to None. It's not an error, it's just pointless." << std::endl;
|
||||
return;
|
||||
}
|
||||
timers[name] = std::make_shared<Timer>(target, interval, runtime.getElapsedTime().asMilliseconds());
|
||||
timers[name] = std::make_shared<PyTimerCallable>(target, interval, runtime.getElapsedTime().asMilliseconds());
|
||||
}
|
||||
|
||||
void GameEngine::testTimers()
|
||||
|
|
@ -333,8 +313,7 @@ void GameEngine::testTimers()
|
|||
{
|
||||
it->second->test(now);
|
||||
|
||||
// Remove timers that have been cancelled or are one-shot and fired
|
||||
if (!it->second->getCallback() || it->second->getCallback() == Py_None)
|
||||
if (it->second->isNone())
|
||||
{
|
||||
it = timers.erase(it);
|
||||
}
|
||||
|
|
@ -349,14 +328,6 @@ void GameEngine::processEvent(const sf::Event& event)
|
|||
int actionCode = 0;
|
||||
|
||||
if (event.type == sf::Event::Closed) { running = false; return; }
|
||||
|
||||
// Handle F3 for profiler overlay toggle
|
||||
if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::F3) {
|
||||
if (profilerOverlay) {
|
||||
profilerOverlay->toggle();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Handle window resize events
|
||||
else if (event.type == sf::Event::Resized) {
|
||||
// Update the viewport to handle the new window size
|
||||
|
|
|
|||
|
|
@ -9,16 +9,11 @@
|
|||
#include "McRogueFaceConfig.h"
|
||||
#include "HeadlessRenderer.h"
|
||||
#include "SceneTransition.h"
|
||||
#include "Profiler.h"
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
|
||||
class GameEngine
|
||||
{
|
||||
public:
|
||||
// Forward declare nested class so private section can use it
|
||||
class ProfilerOverlay;
|
||||
|
||||
// Viewport modes (moved here so private section can use it)
|
||||
enum class ViewportMode {
|
||||
Center, // 1:1 pixels, viewport centered in window
|
||||
|
|
@ -56,19 +51,15 @@ private:
|
|||
sf::Vector2u gameResolution{1024, 768}; // Fixed game resolution
|
||||
sf::View gameView; // View for the game content
|
||||
ViewportMode viewportMode = ViewportMode::Fit;
|
||||
|
||||
// Profiling overlay
|
||||
bool showProfilerOverlay = false; // F3 key toggles this
|
||||
int overlayUpdateCounter = 0; // Only update overlay every N frames
|
||||
ProfilerOverlay* profilerOverlay = nullptr; // The actual overlay renderer
|
||||
|
||||
|
||||
void updateViewport();
|
||||
|
||||
void testTimers();
|
||||
|
||||
public:
|
||||
sf::Clock runtime;
|
||||
std::map<std::string, std::shared_ptr<Timer>> timers;
|
||||
//std::map<std::string, Timer> timers;
|
||||
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
|
||||
std::string scene;
|
||||
|
||||
// Profiling metrics
|
||||
|
|
@ -79,29 +70,17 @@ public:
|
|||
int drawCalls = 0; // Draw calls per frame
|
||||
int uiElements = 0; // Number of UI elements rendered
|
||||
int visibleElements = 0; // Number of visible elements
|
||||
|
||||
// Detailed timing breakdowns (added for profiling system)
|
||||
float gridRenderTime = 0.0f; // Time spent rendering grids (ms)
|
||||
float entityRenderTime = 0.0f; // Time spent rendering entities (ms)
|
||||
float fovOverlayTime = 0.0f; // Time spent rendering FOV overlays (ms)
|
||||
float pythonScriptTime = 0.0f; // Time spent in Python callbacks (ms)
|
||||
float animationTime = 0.0f; // Time spent updating animations (ms)
|
||||
|
||||
// Grid-specific metrics
|
||||
int gridCellsRendered = 0; // Number of grid cells drawn this frame
|
||||
int entitiesRendered = 0; // Number of entities drawn this frame
|
||||
int totalEntities = 0; // Total entities in scene
|
||||
|
||||
|
||||
// Frame time history for averaging
|
||||
static constexpr int HISTORY_SIZE = 60;
|
||||
float frameTimeHistory[HISTORY_SIZE] = {0};
|
||||
int historyIndex = 0;
|
||||
|
||||
|
||||
void updateFrameTime(float deltaMs) {
|
||||
frameTime = deltaMs;
|
||||
frameTimeHistory[historyIndex] = deltaMs;
|
||||
historyIndex = (historyIndex + 1) % HISTORY_SIZE;
|
||||
|
||||
|
||||
// Calculate average
|
||||
float sum = 0.0f;
|
||||
for (int i = 0; i < HISTORY_SIZE; ++i) {
|
||||
|
|
@ -110,26 +89,13 @@ public:
|
|||
avgFrameTime = sum / HISTORY_SIZE;
|
||||
fps = avgFrameTime > 0 ? static_cast<int>(1000.0f / avgFrameTime) : 0;
|
||||
}
|
||||
|
||||
|
||||
void resetPerFrame() {
|
||||
drawCalls = 0;
|
||||
uiElements = 0;
|
||||
visibleElements = 0;
|
||||
|
||||
// Reset per-frame timing metrics
|
||||
gridRenderTime = 0.0f;
|
||||
entityRenderTime = 0.0f;
|
||||
fovOverlayTime = 0.0f;
|
||||
pythonScriptTime = 0.0f;
|
||||
animationTime = 0.0f;
|
||||
|
||||
// Reset per-frame counters
|
||||
gridCellsRendered = 0;
|
||||
entitiesRendered = 0;
|
||||
totalEntities = 0;
|
||||
}
|
||||
} metrics;
|
||||
|
||||
GameEngine();
|
||||
GameEngine(const McRogueFaceConfig& cfg);
|
||||
~GameEngine();
|
||||
|
|
@ -150,7 +116,7 @@ public:
|
|||
float getFrameTime() { return frameTime; }
|
||||
sf::View getView() { return visible; }
|
||||
void manageTimer(std::string, PyObject*, int);
|
||||
std::shared_ptr<Timer> getTimer(const std::string& name);
|
||||
std::shared_ptr<PyTimerCallable> getTimer(const std::string& name);
|
||||
void setWindowScale(float);
|
||||
bool isHeadless() const { return headless; }
|
||||
void processEvent(const sf::Event& event);
|
||||
|
|
@ -179,30 +145,5 @@ public:
|
|||
sf::Music music;
|
||||
sf::Sound sfx;
|
||||
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> scene_ui(std::string scene);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Visual overlay that displays real-time profiling metrics
|
||||
*/
|
||||
class GameEngine::ProfilerOverlay {
|
||||
private:
|
||||
sf::Font& font;
|
||||
sf::Text text;
|
||||
sf::RectangleShape background;
|
||||
bool visible;
|
||||
int updateInterval;
|
||||
int frameCounter;
|
||||
|
||||
sf::Color getPerformanceColor(float frameTimeMs);
|
||||
std::string formatFloat(float value, int precision = 1);
|
||||
std::string formatPercentage(float part, float total);
|
||||
|
||||
public:
|
||||
ProfilerOverlay(sf::Font& fontRef);
|
||||
void toggle();
|
||||
void setVisible(bool vis);
|
||||
bool isVisible() const;
|
||||
void update(const ProfilingMetrics& metrics);
|
||||
void render(sf::RenderTarget& target);
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Automation.h"
|
||||
#include "McRFPy_Libtcod.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include "platform.h"
|
||||
#include "PyAnimation.h"
|
||||
#include "PyDrawable.h"
|
||||
|
|
@ -28,201 +27,188 @@ PyObject* McRFPy_API::mcrf_module;
|
|||
static PyMethodDef mcrfpyMethods[] = {
|
||||
|
||||
{"createSoundBuffer", McRFPy_API::_createSoundBuffer, METH_VARARGS,
|
||||
MCRF_FUNCTION(createSoundBuffer,
|
||||
MCRF_SIG("(filename: str)", "int"),
|
||||
MCRF_DESC("Load a sound effect from a file and return its buffer ID."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("filename", "Path to the sound file (WAV, OGG, FLAC)")
|
||||
MCRF_RETURNS("int: Buffer ID for use with playSound()")
|
||||
MCRF_RAISES("RuntimeError", "If the file cannot be loaded")
|
||||
)},
|
||||
"createSoundBuffer(filename: str) -> int\n\n"
|
||||
"Load a sound effect from a file and return its buffer ID.\n\n"
|
||||
"Args:\n"
|
||||
" filename: Path to the sound file (WAV, OGG, FLAC)\n\n"
|
||||
"Returns:\n"
|
||||
" int: Buffer ID for use with playSound()\n\n"
|
||||
"Raises:\n"
|
||||
" RuntimeError: If the file cannot be loaded"},
|
||||
{"loadMusic", McRFPy_API::_loadMusic, METH_VARARGS,
|
||||
MCRF_FUNCTION(loadMusic,
|
||||
MCRF_SIG("(filename: str)", "None"),
|
||||
MCRF_DESC("Load and immediately play background music from a file."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("filename", "Path to the music file (WAV, OGG, FLAC)")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Only one music track can play at a time. Loading new music stops the current track.")
|
||||
)},
|
||||
"loadMusic(filename: str) -> None\n\n"
|
||||
"Load and immediately play background music from a file.\n\n"
|
||||
"Args:\n"
|
||||
" filename: Path to the music file (WAV, OGG, FLAC)\n\n"
|
||||
"Note:\n"
|
||||
" Only one music track can play at a time. Loading new music stops the current track."},
|
||||
{"setMusicVolume", McRFPy_API::_setMusicVolume, METH_VARARGS,
|
||||
MCRF_FUNCTION(setMusicVolume,
|
||||
MCRF_SIG("(volume: int)", "None"),
|
||||
MCRF_DESC("Set the global music volume."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("volume", "Volume level from 0 (silent) to 100 (full volume)")
|
||||
MCRF_RETURNS("None")
|
||||
)},
|
||||
"setMusicVolume(volume: int) -> None\n\n"
|
||||
"Set the global music volume.\n\n"
|
||||
"Args:\n"
|
||||
" volume: Volume level from 0 (silent) to 100 (full volume)"},
|
||||
{"setSoundVolume", McRFPy_API::_setSoundVolume, METH_VARARGS,
|
||||
MCRF_FUNCTION(setSoundVolume,
|
||||
MCRF_SIG("(volume: int)", "None"),
|
||||
MCRF_DESC("Set the global sound effects volume."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("volume", "Volume level from 0 (silent) to 100 (full volume)")
|
||||
MCRF_RETURNS("None")
|
||||
)},
|
||||
"setSoundVolume(volume: int) -> None\n\n"
|
||||
"Set the global sound effects volume.\n\n"
|
||||
"Args:\n"
|
||||
" volume: Volume level from 0 (silent) to 100 (full volume)"},
|
||||
{"playSound", McRFPy_API::_playSound, METH_VARARGS,
|
||||
MCRF_FUNCTION(playSound,
|
||||
MCRF_SIG("(buffer_id: int)", "None"),
|
||||
MCRF_DESC("Play a sound effect using a previously loaded buffer."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("buffer_id", "Sound buffer ID returned by createSoundBuffer()")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_RAISES("RuntimeError", "If the buffer ID is invalid")
|
||||
)},
|
||||
"playSound(buffer_id: int) -> None\n\n"
|
||||
"Play a sound effect using a previously loaded buffer.\n\n"
|
||||
"Args:\n"
|
||||
" buffer_id: Sound buffer ID returned by createSoundBuffer()\n\n"
|
||||
"Raises:\n"
|
||||
" RuntimeError: If the buffer ID is invalid"},
|
||||
{"getMusicVolume", McRFPy_API::_getMusicVolume, METH_NOARGS,
|
||||
MCRF_FUNCTION(getMusicVolume,
|
||||
MCRF_SIG("()", "int"),
|
||||
MCRF_DESC("Get the current music volume level."),
|
||||
MCRF_RETURNS("int: Current volume (0-100)")
|
||||
)},
|
||||
"getMusicVolume() -> int\n\n"
|
||||
"Get the current music volume level.\n\n"
|
||||
"Returns:\n"
|
||||
" int: Current volume (0-100)"},
|
||||
{"getSoundVolume", McRFPy_API::_getSoundVolume, METH_NOARGS,
|
||||
MCRF_FUNCTION(getSoundVolume,
|
||||
MCRF_SIG("()", "int"),
|
||||
MCRF_DESC("Get the current sound effects volume level."),
|
||||
MCRF_RETURNS("int: Current volume (0-100)")
|
||||
)},
|
||||
"getSoundVolume() -> int\n\n"
|
||||
"Get the current sound effects volume level.\n\n"
|
||||
"Returns:\n"
|
||||
" int: Current volume (0-100)"},
|
||||
|
||||
{"sceneUI", McRFPy_API::_sceneUI, METH_VARARGS,
|
||||
MCRF_FUNCTION(sceneUI,
|
||||
MCRF_SIG("(scene: str = None)", "list"),
|
||||
MCRF_DESC("Get all UI elements for a scene."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("scene", "Scene name. If None, uses current scene")
|
||||
MCRF_RETURNS("list: All UI elements (Frame, Caption, Sprite, Grid) in the scene")
|
||||
MCRF_RAISES("KeyError", "If the specified scene doesn't exist")
|
||||
)},
|
||||
"sceneUI(scene: str = None) -> list\n\n"
|
||||
"Get all UI elements for a scene.\n\n"
|
||||
"Args:\n"
|
||||
" scene: Scene name. If None, uses current scene\n\n"
|
||||
"Returns:\n"
|
||||
" list: All UI elements (Frame, Caption, Sprite, Grid) in the scene\n\n"
|
||||
"Raises:\n"
|
||||
" KeyError: If the specified scene doesn't exist"},
|
||||
|
||||
{"currentScene", McRFPy_API::_currentScene, METH_NOARGS,
|
||||
MCRF_FUNCTION(currentScene,
|
||||
MCRF_SIG("()", "str"),
|
||||
MCRF_DESC("Get the name of the currently active scene."),
|
||||
MCRF_RETURNS("str: Name of the current scene")
|
||||
)},
|
||||
"currentScene() -> str\n\n"
|
||||
"Get the name of the currently active scene.\n\n"
|
||||
"Returns:\n"
|
||||
" str: Name of the current scene"},
|
||||
{"setScene", McRFPy_API::_setScene, METH_VARARGS,
|
||||
MCRF_FUNCTION(setScene,
|
||||
MCRF_SIG("(scene: str, transition: str = None, duration: float = 0.0)", "None"),
|
||||
MCRF_DESC("Switch to a different scene with optional transition effect."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("scene", "Name of the scene to switch to")
|
||||
MCRF_ARG("transition", "Transition type ('fade', 'slide_left', 'slide_right', 'slide_up', 'slide_down')")
|
||||
MCRF_ARG("duration", "Transition duration in seconds (default: 0.0 for instant)")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_RAISES("KeyError", "If the scene doesn't exist")
|
||||
MCRF_RAISES("ValueError", "If the transition type is invalid")
|
||||
)},
|
||||
"setScene(scene: str, transition: str = None, duration: float = 0.0) -> None\n\n"
|
||||
"Switch to a different scene with optional transition effect.\n\n"
|
||||
"Args:\n"
|
||||
" scene: Name of the scene to switch to\n"
|
||||
" transition: Transition type ('fade', 'slide_left', 'slide_right', 'slide_up', 'slide_down')\n"
|
||||
" duration: Transition duration in seconds (default: 0.0 for instant)\n\n"
|
||||
"Raises:\n"
|
||||
" KeyError: If the scene doesn't exist\n"
|
||||
" ValueError: If the transition type is invalid"},
|
||||
{"createScene", McRFPy_API::_createScene, METH_VARARGS,
|
||||
MCRF_FUNCTION(createScene,
|
||||
MCRF_SIG("(name: str)", "None"),
|
||||
MCRF_DESC("Create a new empty scene."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("name", "Unique name for the new scene")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_RAISES("ValueError", "If a scene with this name already exists")
|
||||
MCRF_NOTE("The scene is created but not made active. Use setScene() to switch to it.")
|
||||
)},
|
||||
"createScene(name: str) -> None\n\n"
|
||||
"Create a new empty scene.\n\n"
|
||||
"Args:\n"
|
||||
" name: Unique name for the new scene\n\n"
|
||||
"Raises:\n"
|
||||
" ValueError: If a scene with this name already exists\n\n"
|
||||
"Note:\n"
|
||||
" The scene is created but not made active. Use setScene() to switch to it."},
|
||||
{"keypressScene", McRFPy_API::_keypressScene, METH_VARARGS,
|
||||
MCRF_FUNCTION(keypressScene,
|
||||
MCRF_SIG("(handler: callable)", "None"),
|
||||
MCRF_DESC("Set the keyboard event handler for the current scene."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("handler", "Callable that receives (key_name: str, is_pressed: bool)")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Example: def on_key(key, pressed): if key == 'A' and pressed: print('A key pressed') mcrfpy.keypressScene(on_key)")
|
||||
)},
|
||||
"keypressScene(handler: callable) -> None\n\n"
|
||||
"Set the keyboard event handler for the current scene.\n\n"
|
||||
"Args:\n"
|
||||
" handler: Callable that receives (key_name: str, is_pressed: bool)\n\n"
|
||||
"Example:\n"
|
||||
" def on_key(key, pressed):\n"
|
||||
" if key == 'A' and pressed:\n"
|
||||
" print('A key pressed')\n"
|
||||
" mcrfpy.keypressScene(on_key)"},
|
||||
|
||||
{"setTimer", McRFPy_API::_setTimer, METH_VARARGS,
|
||||
MCRF_FUNCTION(setTimer,
|
||||
MCRF_SIG("(name: str, handler: callable, interval: int)", "None"),
|
||||
MCRF_DESC("Create or update a recurring timer."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("name", "Unique identifier for the timer")
|
||||
MCRF_ARG("handler", "Function called with (runtime: float) parameter")
|
||||
MCRF_ARG("interval", "Time between calls in milliseconds")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("If a timer with this name exists, it will be replaced. The handler receives the total runtime in seconds as its argument.")
|
||||
)},
|
||||
"setTimer(name: str, handler: callable, interval: int) -> None\n\n"
|
||||
"Create or update a recurring timer.\n\n"
|
||||
"Args:\n"
|
||||
" name: Unique identifier for the timer\n"
|
||||
" handler: Function called with (runtime: float) parameter\n"
|
||||
" interval: Time between calls in milliseconds\n\n"
|
||||
"Note:\n"
|
||||
" If a timer with this name exists, it will be replaced.\n"
|
||||
" The handler receives the total runtime in seconds as its argument."},
|
||||
{"delTimer", McRFPy_API::_delTimer, METH_VARARGS,
|
||||
MCRF_FUNCTION(delTimer,
|
||||
MCRF_SIG("(name: str)", "None"),
|
||||
MCRF_DESC("Stop and remove a timer."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("name", "Timer identifier to remove")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("No error is raised if the timer doesn't exist.")
|
||||
)},
|
||||
"delTimer(name: str) -> None\n\n"
|
||||
"Stop and remove a timer.\n\n"
|
||||
"Args:\n"
|
||||
" name: Timer identifier to remove\n\n"
|
||||
"Note:\n"
|
||||
" No error is raised if the timer doesn't exist."},
|
||||
{"exit", McRFPy_API::_exit, METH_NOARGS,
|
||||
MCRF_FUNCTION(exit,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Cleanly shut down the game engine and exit the application."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("This immediately closes the window and terminates the program.")
|
||||
)},
|
||||
"exit() -> None\n\n"
|
||||
"Cleanly shut down the game engine and exit the application.\n\n"
|
||||
"Note:\n"
|
||||
" This immediately closes the window and terminates the program."},
|
||||
{"setScale", McRFPy_API::_setScale, METH_VARARGS,
|
||||
MCRF_FUNCTION(setScale,
|
||||
MCRF_SIG("(multiplier: float)", "None"),
|
||||
MCRF_DESC("Scale the game window size."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("multiplier", "Scale factor (e.g., 2.0 for double size)")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("The internal resolution remains 1024x768, but the window is scaled. This is deprecated - use Window.resolution instead.")
|
||||
)},
|
||||
|
||||
"setScale(multiplier: float) -> None\n\n"
|
||||
"Scale the game window size.\n\n"
|
||||
"Args:\n"
|
||||
" multiplier: Scale factor (e.g., 2.0 for double size)\n\n"
|
||||
"Note:\n"
|
||||
" The internal resolution remains 1024x768, but the window is scaled.\n"
|
||||
" This is deprecated - use Window.resolution instead."},
|
||||
|
||||
{"find", McRFPy_API::_find, METH_VARARGS,
|
||||
MCRF_FUNCTION(find,
|
||||
MCRF_SIG("(name: str, scene: str = None)", "UIDrawable | None"),
|
||||
MCRF_DESC("Find the first UI element with the specified name."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("name", "Exact name to search for")
|
||||
MCRF_ARG("scene", "Scene to search in (default: current scene)")
|
||||
MCRF_RETURNS("Frame, Caption, Sprite, Grid, or Entity if found; None otherwise")
|
||||
MCRF_NOTE("Searches scene UI elements and entities within grids.")
|
||||
)},
|
||||
"find(name: str, scene: str = None) -> UIDrawable | None\n\n"
|
||||
"Find the first UI element with the specified name.\n\n"
|
||||
"Args:\n"
|
||||
" name: Exact name to search for\n"
|
||||
" scene: Scene to search in (default: current scene)\n\n"
|
||||
"Returns:\n"
|
||||
" Frame, Caption, Sprite, Grid, or Entity if found; None otherwise\n\n"
|
||||
"Note:\n"
|
||||
" Searches scene UI elements and entities within grids."},
|
||||
{"findAll", McRFPy_API::_findAll, METH_VARARGS,
|
||||
MCRF_FUNCTION(findAll,
|
||||
MCRF_SIG("(pattern: str, scene: str = None)", "list"),
|
||||
MCRF_DESC("Find all UI elements matching a name pattern."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("pattern", "Name pattern with optional wildcards (* matches any characters)")
|
||||
MCRF_ARG("scene", "Scene to search in (default: current scene)")
|
||||
MCRF_RETURNS("list: All matching UI elements and entities")
|
||||
MCRF_NOTE("Example: findAll('enemy*') finds all elements starting with 'enemy', findAll('*_button') finds all elements ending with '_button'")
|
||||
)},
|
||||
|
||||
"findAll(pattern: str, scene: str = None) -> list\n\n"
|
||||
"Find all UI elements matching a name pattern.\n\n"
|
||||
"Args:\n"
|
||||
" pattern: Name pattern with optional wildcards (* matches any characters)\n"
|
||||
" scene: Scene to search in (default: current scene)\n\n"
|
||||
"Returns:\n"
|
||||
" list: All matching UI elements and entities\n\n"
|
||||
"Example:\n"
|
||||
" findAll('enemy*') # Find all elements starting with 'enemy'\n"
|
||||
" findAll('*_button') # Find all elements ending with '_button'"},
|
||||
|
||||
{"getMetrics", McRFPy_API::_getMetrics, METH_NOARGS,
|
||||
MCRF_FUNCTION(getMetrics,
|
||||
MCRF_SIG("()", "dict"),
|
||||
MCRF_DESC("Get current performance metrics."),
|
||||
MCRF_RETURNS("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)")
|
||||
)},
|
||||
|
||||
"getMetrics() -> dict\n\n"
|
||||
"Get current performance metrics.\n\n"
|
||||
"Returns:\n"
|
||||
" dict: Performance data with keys:\n"
|
||||
" - frame_time: Last frame duration in seconds\n"
|
||||
" - avg_frame_time: Average frame time\n"
|
||||
" - fps: Frames per second\n"
|
||||
" - draw_calls: Number of draw calls\n"
|
||||
" - ui_elements: Total UI element count\n"
|
||||
" - visible_elements: Visible element count\n"
|
||||
" - current_frame: Frame counter\n"
|
||||
" - runtime: Total runtime in seconds"},
|
||||
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
static PyModuleDef mcrfpyModule = {
|
||||
PyModuleDef_HEAD_INIT, /* m_base - Always initialize this member to PyModuleDef_HEAD_INIT. */
|
||||
"mcrfpy", /* m_name */
|
||||
PyDoc_STR("McRogueFace Python API\n\n"
|
||||
"Core game engine interface for creating roguelike games with Python.\n\n"
|
||||
"This 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\n"
|
||||
"Example:\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"),
|
||||
PyDoc_STR("McRogueFace Python API\\n\\n"
|
||||
"Core game engine interface for creating roguelike games with Python.\\n\\n"
|
||||
"This 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\\n"
|
||||
"Example:\\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"),
|
||||
-1, /* m_size - Setting m_size to -1 means that the module does not support sub-interpreters, because it has global state. */
|
||||
mcrfpyMethods, /* m_methods */
|
||||
NULL, /* m_slots - An array of slot definitions ... When using single-phase initialization, m_slots must be NULL. */
|
||||
|
|
@ -281,14 +267,6 @@ PyObject* PyInit_mcrfpy()
|
|||
PySceneType.tp_methods = PySceneClass::methods;
|
||||
PySceneType.tp_getset = PySceneClass::getsetters;
|
||||
|
||||
// Set up weakref support for all types that need it
|
||||
PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist);
|
||||
PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);
|
||||
PyUICaptionType.tp_weaklistoffset = offsetof(PyUICaptionObject, weakreflist);
|
||||
PyUISpriteType.tp_weaklistoffset = offsetof(PyUISpriteObject, weakreflist);
|
||||
PyUIGridType.tp_weaklistoffset = offsetof(PyUIGridObject, weakreflist);
|
||||
PyUIEntityType.tp_weaklistoffset = offsetof(PyUIEntityObject, weakreflist);
|
||||
|
||||
int i = 0;
|
||||
auto t = pytypes[i];
|
||||
while (t != nullptr)
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
#ifndef MCRFPY_DOC_H
|
||||
#define MCRFPY_DOC_H
|
||||
|
||||
// Section builders for documentation
|
||||
#define MCRF_SIG(params, ret) params " -> " ret "\n\n"
|
||||
#define MCRF_DESC(text) text "\n\n"
|
||||
#define MCRF_ARGS_START "Args:\n"
|
||||
#define MCRF_ARG(name, desc) " " name ": " desc "\n"
|
||||
#define MCRF_RETURNS(text) "\nReturns:\n " text "\n"
|
||||
#define MCRF_RAISES(exc, desc) "\nRaises:\n " exc ": " desc "\n"
|
||||
#define MCRF_NOTE(text) "\nNote:\n " text "\n"
|
||||
|
||||
// Link to external documentation
|
||||
// Format: MCRF_LINK("docs/file.md", "Link Text")
|
||||
// Parsers detect this pattern and format per output type
|
||||
#define MCRF_LINK(ref, text) "\nSee also: " text " (" ref ")\n"
|
||||
|
||||
// Main documentation macros
|
||||
#define MCRF_METHOD_DOC(name, sig, desc, ...) \
|
||||
name sig desc __VA_ARGS__
|
||||
|
||||
#define MCRF_FUNCTION(name, ...) \
|
||||
MCRF_METHOD_DOC(#name, __VA_ARGS__)
|
||||
|
||||
#define MCRF_METHOD(cls, name, ...) \
|
||||
MCRF_METHOD_DOC(#name, __VA_ARGS__)
|
||||
|
||||
#define MCRF_PROPERTY(name, desc) \
|
||||
desc
|
||||
|
||||
#endif // MCRFPY_DOC_H
|
||||
|
|
@ -28,9 +28,6 @@ struct McRogueFaceConfig {
|
|||
// Screenshot functionality for headless mode
|
||||
std::string screenshot_path;
|
||||
bool take_screenshot = false;
|
||||
|
||||
// Auto-exit when no timers remain (for --headless --exec automation)
|
||||
bool auto_exit_after_exec = false;
|
||||
};
|
||||
|
||||
#endif // MCROGUEFACE_CONFIG_H
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
#include "Profiler.h"
|
||||
#include <iostream>
|
||||
|
||||
ProfilingLogger::ProfilingLogger()
|
||||
: headers_written(false)
|
||||
{
|
||||
}
|
||||
|
||||
ProfilingLogger::~ProfilingLogger() {
|
||||
close();
|
||||
}
|
||||
|
||||
bool ProfilingLogger::open(const std::string& filename, const std::vector<std::string>& columns) {
|
||||
column_names = columns;
|
||||
file.open(filename);
|
||||
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "Failed to open profiling log file: " << filename << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write CSV header
|
||||
for (size_t i = 0; i < columns.size(); ++i) {
|
||||
file << columns[i];
|
||||
if (i < columns.size() - 1) {
|
||||
file << ",";
|
||||
}
|
||||
}
|
||||
file << "\n";
|
||||
file.flush();
|
||||
|
||||
headers_written = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProfilingLogger::writeRow(const std::vector<float>& values) {
|
||||
if (!file.is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.size() != column_names.size()) {
|
||||
std::cerr << "ProfilingLogger: value count (" << values.size()
|
||||
<< ") doesn't match column count (" << column_names.size() << ")" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < values.size(); ++i) {
|
||||
file << values[i];
|
||||
if (i < values.size() - 1) {
|
||||
file << ",";
|
||||
}
|
||||
}
|
||||
file << "\n";
|
||||
}
|
||||
|
||||
void ProfilingLogger::close() {
|
||||
if (file.is_open()) {
|
||||
file.flush();
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
111
src/Profiler.h
111
src/Profiler.h
|
|
@ -1,111 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <fstream>
|
||||
|
||||
/**
|
||||
* @brief Simple RAII-based profiling timer for measuring code execution time
|
||||
*
|
||||
* Usage:
|
||||
* float timing = 0.0f;
|
||||
* {
|
||||
* ScopedTimer timer(timing);
|
||||
* // ... code to profile ...
|
||||
* } // timing now contains elapsed milliseconds
|
||||
*/
|
||||
class ScopedTimer {
|
||||
private:
|
||||
std::chrono::high_resolution_clock::time_point start;
|
||||
float& target_ms;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Scoped Timer and start timing
|
||||
* @param target Reference to float that will receive elapsed time in milliseconds
|
||||
*/
|
||||
explicit ScopedTimer(float& target)
|
||||
: target_ms(target)
|
||||
{
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Destructor automatically records elapsed time
|
||||
*/
|
||||
~ScopedTimer() {
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
target_ms = std::chrono::duration<float, std::milli>(end - start).count();
|
||||
}
|
||||
|
||||
// Prevent copying
|
||||
ScopedTimer(const ScopedTimer&) = delete;
|
||||
ScopedTimer& operator=(const ScopedTimer&) = delete;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Accumulating timer that adds elapsed time to existing value
|
||||
*
|
||||
* Useful for measuring total time across multiple calls in a single frame
|
||||
*/
|
||||
class AccumulatingTimer {
|
||||
private:
|
||||
std::chrono::high_resolution_clock::time_point start;
|
||||
float& target_ms;
|
||||
|
||||
public:
|
||||
explicit AccumulatingTimer(float& target)
|
||||
: target_ms(target)
|
||||
{
|
||||
start = std::chrono::high_resolution_clock::now();
|
||||
}
|
||||
|
||||
~AccumulatingTimer() {
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
target_ms += std::chrono::duration<float, std::milli>(end - start).count();
|
||||
}
|
||||
|
||||
AccumulatingTimer(const AccumulatingTimer&) = delete;
|
||||
AccumulatingTimer& operator=(const AccumulatingTimer&) = delete;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief CSV profiling data logger for batch analysis
|
||||
*
|
||||
* Writes profiling data to CSV file for later analysis with Python/pandas/Excel
|
||||
*/
|
||||
class ProfilingLogger {
|
||||
private:
|
||||
std::ofstream file;
|
||||
bool headers_written;
|
||||
std::vector<std::string> column_names;
|
||||
|
||||
public:
|
||||
ProfilingLogger();
|
||||
~ProfilingLogger();
|
||||
|
||||
/**
|
||||
* @brief Open a CSV file for writing profiling data
|
||||
* @param filename Path to CSV file
|
||||
* @param columns Column names for the CSV header
|
||||
* @return true if file opened successfully
|
||||
*/
|
||||
bool open(const std::string& filename, const std::vector<std::string>& columns);
|
||||
|
||||
/**
|
||||
* @brief Write a row of profiling data
|
||||
* @param values Data values (must match column count)
|
||||
*/
|
||||
void writeRow(const std::vector<float>& values);
|
||||
|
||||
/**
|
||||
* @brief Close the file and flush data
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* @brief Check if logger is ready to write
|
||||
*/
|
||||
bool isOpen() const { return file.is_open(); }
|
||||
};
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
#include "GameEngine.h"
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
GameEngine::ProfilerOverlay::ProfilerOverlay(sf::Font& fontRef)
|
||||
: font(fontRef), visible(false), updateInterval(10), frameCounter(0)
|
||||
{
|
||||
text.setFont(font);
|
||||
text.setCharacterSize(14);
|
||||
text.setFillColor(sf::Color::White);
|
||||
text.setPosition(10.0f, 10.0f);
|
||||
|
||||
// Semi-transparent dark background
|
||||
background.setFillColor(sf::Color(0, 0, 0, 180));
|
||||
background.setPosition(5.0f, 5.0f);
|
||||
}
|
||||
|
||||
void GameEngine::ProfilerOverlay::toggle() {
|
||||
visible = !visible;
|
||||
}
|
||||
|
||||
void GameEngine::ProfilerOverlay::setVisible(bool vis) {
|
||||
visible = vis;
|
||||
}
|
||||
|
||||
bool GameEngine::ProfilerOverlay::isVisible() const {
|
||||
return visible;
|
||||
}
|
||||
|
||||
sf::Color GameEngine::ProfilerOverlay::getPerformanceColor(float frameTimeMs) {
|
||||
if (frameTimeMs < 16.6f) {
|
||||
return sf::Color::Green; // 60+ FPS
|
||||
} else if (frameTimeMs < 33.3f) {
|
||||
return sf::Color::Yellow; // 30-60 FPS
|
||||
} else {
|
||||
return sf::Color::Red; // <30 FPS
|
||||
}
|
||||
}
|
||||
|
||||
std::string GameEngine::ProfilerOverlay::formatFloat(float value, int precision) {
|
||||
std::stringstream ss;
|
||||
ss << std::fixed << std::setprecision(precision) << value;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string GameEngine::ProfilerOverlay::formatPercentage(float part, float total) {
|
||||
if (total <= 0.0f) return "0%";
|
||||
float pct = (part / total) * 100.0f;
|
||||
return formatFloat(pct, 0) + "%";
|
||||
}
|
||||
|
||||
void GameEngine::ProfilerOverlay::update(const ProfilingMetrics& metrics) {
|
||||
if (!visible) return;
|
||||
|
||||
// Only update text every N frames to reduce overhead
|
||||
frameCounter++;
|
||||
if (frameCounter < updateInterval) {
|
||||
return;
|
||||
}
|
||||
frameCounter = 0;
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "McRogueFace Performance Monitor\n";
|
||||
ss << "================================\n";
|
||||
|
||||
// Frame time and FPS
|
||||
float frameMs = metrics.avgFrameTime;
|
||||
ss << "FPS: " << metrics.fps << " (" << formatFloat(frameMs, 1) << "ms/frame)\n";
|
||||
|
||||
// Performance warning
|
||||
if (frameMs > 33.3f) {
|
||||
ss << "WARNING: Frame time exceeds 30 FPS target!\n";
|
||||
}
|
||||
|
||||
ss << "\n";
|
||||
|
||||
// Timing breakdown
|
||||
ss << "Frame Time Breakdown:\n";
|
||||
ss << " Grid Render: " << formatFloat(metrics.gridRenderTime, 1) << "ms ("
|
||||
<< formatPercentage(metrics.gridRenderTime, frameMs) << ")\n";
|
||||
ss << " Cells: " << metrics.gridCellsRendered << " rendered\n";
|
||||
ss << " Entities: " << metrics.entitiesRendered << " / " << metrics.totalEntities << " drawn\n";
|
||||
|
||||
if (metrics.fovOverlayTime > 0.01f) {
|
||||
ss << " FOV Overlay: " << formatFloat(metrics.fovOverlayTime, 1) << "ms\n";
|
||||
}
|
||||
|
||||
if (metrics.entityRenderTime > 0.01f) {
|
||||
ss << " Entity Render: " << formatFloat(metrics.entityRenderTime, 1) << "ms ("
|
||||
<< formatPercentage(metrics.entityRenderTime, frameMs) << ")\n";
|
||||
}
|
||||
|
||||
if (metrics.pythonScriptTime > 0.01f) {
|
||||
ss << " Python: " << formatFloat(metrics.pythonScriptTime, 1) << "ms ("
|
||||
<< formatPercentage(metrics.pythonScriptTime, frameMs) << ")\n";
|
||||
}
|
||||
|
||||
if (metrics.animationTime > 0.01f) {
|
||||
ss << " Animations: " << formatFloat(metrics.animationTime, 1) << "ms ("
|
||||
<< formatPercentage(metrics.animationTime, frameMs) << ")\n";
|
||||
}
|
||||
|
||||
ss << "\n";
|
||||
|
||||
// Other metrics
|
||||
ss << "Draw Calls: " << metrics.drawCalls << "\n";
|
||||
ss << "UI Elements: " << metrics.uiElements << " (" << metrics.visibleElements << " visible)\n";
|
||||
|
||||
// Calculate unaccounted time
|
||||
float accountedTime = metrics.gridRenderTime + metrics.entityRenderTime +
|
||||
metrics.pythonScriptTime + metrics.animationTime;
|
||||
float unaccountedTime = frameMs - accountedTime;
|
||||
|
||||
if (unaccountedTime > 1.0f) {
|
||||
ss << "\n";
|
||||
ss << "Other: " << formatFloat(unaccountedTime, 1) << "ms ("
|
||||
<< formatPercentage(unaccountedTime, frameMs) << ")\n";
|
||||
}
|
||||
|
||||
ss << "\n";
|
||||
ss << "Press F3 to hide this overlay";
|
||||
|
||||
text.setString(ss.str());
|
||||
|
||||
// Update background size to fit text
|
||||
sf::FloatRect textBounds = text.getLocalBounds();
|
||||
background.setSize(sf::Vector2f(textBounds.width + 20.0f, textBounds.height + 20.0f));
|
||||
}
|
||||
|
||||
void GameEngine::ProfilerOverlay::render(sf::RenderTarget& target) {
|
||||
if (!visible) return;
|
||||
|
||||
target.draw(background);
|
||||
target.draw(text);
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
#include "PyAnimation.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include "UIDrawable.h"
|
||||
#include "UIFrame.h"
|
||||
#include "UICaption.h"
|
||||
|
|
@ -139,67 +138,47 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// Get type objects from the module to ensure they're initialized
|
||||
PyObject* frame_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame");
|
||||
PyObject* caption_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption");
|
||||
PyObject* sprite_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite");
|
||||
PyObject* grid_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
|
||||
PyObject* entity_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
||||
// Check type by comparing type names
|
||||
const char* type_name = Py_TYPE(target_obj)->tp_name;
|
||||
|
||||
bool handled = false;
|
||||
|
||||
// Use PyObject_IsInstance to support inheritance
|
||||
if (frame_type && PyObject_IsInstance(target_obj, frame_type)) {
|
||||
if (strcmp(type_name, "mcrfpy.Frame") == 0) {
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj;
|
||||
if (frame->data) {
|
||||
self->data->start(frame->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
else if (caption_type && PyObject_IsInstance(target_obj, caption_type)) {
|
||||
else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
|
||||
if (caption->data) {
|
||||
self->data->start(caption->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
else if (sprite_type && PyObject_IsInstance(target_obj, sprite_type)) {
|
||||
else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
|
||||
if (sprite->data) {
|
||||
self->data->start(sprite->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
else if (grid_type && PyObject_IsInstance(target_obj, grid_type)) {
|
||||
else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
|
||||
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
|
||||
if (grid->data) {
|
||||
self->data->start(grid->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
else if (entity_type && PyObject_IsInstance(target_obj, entity_type)) {
|
||||
else if (strcmp(type_name, "mcrfpy.Entity") == 0) {
|
||||
// Special handling for Entity since it doesn't inherit from UIDrawable
|
||||
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
|
||||
if (entity->data) {
|
||||
self->data->startEntity(entity->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up references
|
||||
Py_XDECREF(frame_type);
|
||||
Py_XDECREF(caption_type);
|
||||
Py_XDECREF(sprite_type);
|
||||
Py_XDECREF(grid_type);
|
||||
Py_XDECREF(entity_type);
|
||||
|
||||
if (!handled) {
|
||||
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity (or a subclass of these)");
|
||||
else {
|
||||
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -262,58 +241,33 @@ PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args)
|
|||
}
|
||||
|
||||
PyGetSetDef PyAnimation::getsetters[] = {
|
||||
{"property", (getter)get_property, NULL,
|
||||
MCRF_PROPERTY(property, "Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index')."), NULL},
|
||||
{"duration", (getter)get_duration, NULL,
|
||||
MCRF_PROPERTY(duration, "Animation duration in seconds (float, read-only). Total time for the animation to complete."), NULL},
|
||||
{"elapsed", (getter)get_elapsed, NULL,
|
||||
MCRF_PROPERTY(elapsed, "Elapsed time in seconds (float, read-only). Time since the animation started."), NULL},
|
||||
{"is_complete", (getter)get_is_complete, NULL,
|
||||
MCRF_PROPERTY(is_complete, "Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called."), NULL},
|
||||
{"is_delta", (getter)get_is_delta, NULL,
|
||||
MCRF_PROPERTY(is_delta, "Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value."), NULL},
|
||||
{"property", (getter)get_property, NULL, "Target property name", NULL},
|
||||
{"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL},
|
||||
{"elapsed", (getter)get_elapsed, NULL, "Elapsed time in seconds", NULL},
|
||||
{"is_complete", (getter)get_is_complete, NULL, "Whether animation is complete", NULL},
|
||||
{"is_delta", (getter)get_is_delta, NULL, "Whether animation uses delta mode", NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyMethodDef PyAnimation::methods[] = {
|
||||
{"start", (PyCFunction)start, METH_VARARGS,
|
||||
MCRF_METHOD(Animation, start,
|
||||
MCRF_SIG("(target: UIDrawable)", "None"),
|
||||
MCRF_DESC("Start the animation on a target UI element."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("target", "The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("The animation will automatically stop if the target is destroyed. Call AnimationManager.update(delta_time) each frame to progress animations.")
|
||||
)},
|
||||
"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,
|
||||
MCRF_METHOD(Animation, update,
|
||||
MCRF_SIG("(delta_time: float)", "bool"),
|
||||
MCRF_DESC("Update the animation by the given time delta."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("delta_time", "Time elapsed since last update in seconds")
|
||||
MCRF_RETURNS("bool: True if animation is still running, False if complete")
|
||||
MCRF_NOTE("Typically called by AnimationManager automatically. Manual calls only needed for custom animation control.")
|
||||
)},
|
||||
"Update the animation by deltaTime (returns True if still running)"},
|
||||
{"get_current_value", (PyCFunction)get_current_value, METH_NOARGS,
|
||||
MCRF_METHOD(Animation, get_current_value,
|
||||
MCRF_SIG("()", "Any"),
|
||||
MCRF_DESC("Get the current interpolated value of the animation."),
|
||||
MCRF_RETURNS("Any: Current value (type depends on property: float, int, Color tuple, Vector tuple, or str)")
|
||||
MCRF_NOTE("Return type matches the target property type. For sprite_index returns int, for pos returns (x, y), for fill_color returns (r, g, b, a).")
|
||||
)},
|
||||
"Get the current interpolated value"},
|
||||
{"complete", (PyCFunction)complete, METH_NOARGS,
|
||||
MCRF_METHOD(Animation, complete,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Complete the animation immediately by jumping to the final value."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Sets elapsed = duration and applies target value immediately. Completion callback will be called if set.")
|
||||
)},
|
||||
"complete() -> None\n\n"
|
||||
"Complete the animation immediately by jumping to the final value."},
|
||||
{"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS,
|
||||
MCRF_METHOD(Animation, hasValidTarget,
|
||||
MCRF_SIG("()", "bool"),
|
||||
MCRF_DESC("Check if the animation still has a valid target."),
|
||||
MCRF_RETURNS("bool: True if the target still exists, False if it was destroyed")
|
||||
MCRF_NOTE("Animations automatically clean up when targets are destroyed. Use this to check if manual cleanup is needed.")
|
||||
)},
|
||||
"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}
|
||||
};
|
||||
|
|
@ -5,21 +5,6 @@ PyCallable::PyCallable(PyObject* _target)
|
|||
target = Py_XNewRef(_target);
|
||||
}
|
||||
|
||||
PyCallable::PyCallable(const PyCallable& other)
|
||||
{
|
||||
target = Py_XNewRef(other.target);
|
||||
}
|
||||
|
||||
PyCallable& PyCallable::operator=(const PyCallable& other)
|
||||
{
|
||||
if (this != &other) {
|
||||
PyObject* old_target = target;
|
||||
target = Py_XNewRef(other.target);
|
||||
Py_XDECREF(old_target);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
PyCallable::~PyCallable()
|
||||
{
|
||||
if (target)
|
||||
|
|
@ -36,6 +21,103 @@ bool PyCallable::isNone() const
|
|||
return (target == Py_None || target == NULL);
|
||||
}
|
||||
|
||||
PyTimerCallable::PyTimerCallable(PyObject* _target, int _interval, int now)
|
||||
: PyCallable(_target), interval(_interval), last_ran(now),
|
||||
paused(false), pause_start_time(0), total_paused_time(0)
|
||||
{}
|
||||
|
||||
PyTimerCallable::PyTimerCallable()
|
||||
: PyCallable(Py_None), interval(0), last_ran(0),
|
||||
paused(false), pause_start_time(0), total_paused_time(0)
|
||||
{}
|
||||
|
||||
bool PyTimerCallable::hasElapsed(int now)
|
||||
{
|
||||
if (paused) return false;
|
||||
return now >= last_ran + interval;
|
||||
}
|
||||
|
||||
void PyTimerCallable::call(int now)
|
||||
{
|
||||
PyObject* args = Py_BuildValue("(i)", now);
|
||||
PyObject* retval = PyCallable::call(args, NULL);
|
||||
if (!retval)
|
||||
{
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
} else if (retval != Py_None)
|
||||
{
|
||||
std::cout << "timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
std::cout << PyUnicode_AsUTF8(PyObject_Repr(retval)) << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
bool PyTimerCallable::test(int now)
|
||||
{
|
||||
if(hasElapsed(now))
|
||||
{
|
||||
call(now);
|
||||
last_ran = now;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PyTimerCallable::pause(int current_time)
|
||||
{
|
||||
if (!paused) {
|
||||
paused = true;
|
||||
pause_start_time = current_time;
|
||||
}
|
||||
}
|
||||
|
||||
void PyTimerCallable::resume(int current_time)
|
||||
{
|
||||
if (paused) {
|
||||
paused = false;
|
||||
int paused_duration = current_time - pause_start_time;
|
||||
total_paused_time += paused_duration;
|
||||
// Adjust last_ran to account for the pause
|
||||
last_ran += paused_duration;
|
||||
}
|
||||
}
|
||||
|
||||
void PyTimerCallable::restart(int current_time)
|
||||
{
|
||||
last_ran = current_time;
|
||||
paused = false;
|
||||
pause_start_time = 0;
|
||||
total_paused_time = 0;
|
||||
}
|
||||
|
||||
void PyTimerCallable::cancel()
|
||||
{
|
||||
// Cancel by setting target to None
|
||||
if (target && target != Py_None) {
|
||||
Py_DECREF(target);
|
||||
}
|
||||
target = Py_None;
|
||||
Py_INCREF(Py_None);
|
||||
}
|
||||
|
||||
int PyTimerCallable::getRemaining(int current_time) const
|
||||
{
|
||||
if (paused) {
|
||||
// When paused, calculate time remaining from when it was paused
|
||||
int elapsed_when_paused = pause_start_time - last_ran;
|
||||
return interval - elapsed_when_paused;
|
||||
}
|
||||
int elapsed = current_time - last_ran;
|
||||
return interval - elapsed;
|
||||
}
|
||||
|
||||
void PyTimerCallable::setCallback(PyObject* new_callback)
|
||||
{
|
||||
if (target && target != Py_None) {
|
||||
Py_DECREF(target);
|
||||
}
|
||||
target = Py_XNewRef(new_callback);
|
||||
}
|
||||
|
||||
PyClickCallable::PyClickCallable(PyObject* _target)
|
||||
: PyCallable(_target)
|
||||
|
|
|
|||
|
|
@ -6,15 +6,45 @@ class PyCallable
|
|||
{
|
||||
protected:
|
||||
PyObject* target;
|
||||
|
||||
public:
|
||||
PyCallable(PyObject*);
|
||||
PyCallable(const PyCallable& other);
|
||||
PyCallable& operator=(const PyCallable& other);
|
||||
~PyCallable();
|
||||
PyObject* call(PyObject*, PyObject*);
|
||||
public:
|
||||
bool isNone() const;
|
||||
PyObject* borrow() const { return target; }
|
||||
};
|
||||
|
||||
class PyTimerCallable: public PyCallable
|
||||
{
|
||||
private:
|
||||
int interval;
|
||||
int last_ran;
|
||||
void call(int);
|
||||
|
||||
// Pause/resume support
|
||||
bool paused;
|
||||
int pause_start_time;
|
||||
int total_paused_time;
|
||||
|
||||
public:
|
||||
bool hasElapsed(int);
|
||||
bool test(int);
|
||||
PyTimerCallable(PyObject*, int, int);
|
||||
PyTimerCallable();
|
||||
|
||||
// Timer control methods
|
||||
void pause(int current_time);
|
||||
void resume(int current_time);
|
||||
void restart(int current_time);
|
||||
void cancel();
|
||||
|
||||
// Timer state queries
|
||||
bool isPaused() const { return paused; }
|
||||
bool isActive() const { return !isNone() && !paused; }
|
||||
int getInterval() const { return interval; }
|
||||
void setInterval(int new_interval) { interval = new_interval; }
|
||||
int getRemaining(int current_time) const;
|
||||
PyObject* getCallback() { return target; }
|
||||
void setCallback(PyObject* new_callback);
|
||||
};
|
||||
|
||||
class PyClickCallable: public PyCallable
|
||||
|
|
@ -24,11 +54,6 @@ public:
|
|||
PyObject* borrow();
|
||||
PyClickCallable(PyObject*);
|
||||
PyClickCallable();
|
||||
PyClickCallable(const PyClickCallable& other) : PyCallable(other) {}
|
||||
PyClickCallable& operator=(const PyClickCallable& other) {
|
||||
PyCallable::operator=(other);
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
class PyKeyCallable: public PyCallable
|
||||
|
|
|
|||
|
|
@ -2,50 +2,21 @@
|
|||
#include "McRFPy_API.h"
|
||||
#include "PyObjectUtils.h"
|
||||
#include "PyRAII.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <string>
|
||||
#include <cstdio>
|
||||
|
||||
PyGetSetDef PyColor::getsetters[] = {
|
||||
{"r", (getter)PyColor::get_member, (setter)PyColor::set_member,
|
||||
MCRF_PROPERTY(r, "Red component (0-255). Automatically clamped to valid range."), (void*)0},
|
||||
{"g", (getter)PyColor::get_member, (setter)PyColor::set_member,
|
||||
MCRF_PROPERTY(g, "Green component (0-255). Automatically clamped to valid range."), (void*)1},
|
||||
{"b", (getter)PyColor::get_member, (setter)PyColor::set_member,
|
||||
MCRF_PROPERTY(b, "Blue component (0-255). Automatically clamped to valid range."), (void*)2},
|
||||
{"a", (getter)PyColor::get_member, (setter)PyColor::set_member,
|
||||
MCRF_PROPERTY(a, "Alpha component (0-255, where 0=transparent, 255=opaque). Automatically clamped to valid range."), (void*)3},
|
||||
{"r", (getter)PyColor::get_member, (setter)PyColor::set_member, "Red component", (void*)0},
|
||||
{"g", (getter)PyColor::get_member, (setter)PyColor::set_member, "Green component", (void*)1},
|
||||
{"b", (getter)PyColor::get_member, (setter)PyColor::set_member, "Blue component", (void*)2},
|
||||
{"a", (getter)PyColor::get_member, (setter)PyColor::set_member, "Alpha component", (void*)3},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyMethodDef PyColor::methods[] = {
|
||||
{"from_hex", (PyCFunction)PyColor::from_hex, METH_VARARGS | METH_CLASS,
|
||||
MCRF_METHOD(Color, from_hex,
|
||||
MCRF_SIG("(hex_string: str)", "Color"),
|
||||
MCRF_DESC("Create a Color from a hexadecimal string."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("hex_string", "Hex color string (e.g., '#FF0000', 'FF0000', '#AABBCCDD' for RGBA)")
|
||||
MCRF_RETURNS("Color: New Color object with values from hex string")
|
||||
MCRF_RAISES("ValueError", "If hex string is not 6 or 8 characters (RGB or RGBA)")
|
||||
MCRF_NOTE("This is a class method. Call as Color.from_hex('#FF0000')")
|
||||
)},
|
||||
{"to_hex", (PyCFunction)PyColor::to_hex, METH_NOARGS,
|
||||
MCRF_METHOD(Color, to_hex,
|
||||
MCRF_SIG("()", "str"),
|
||||
MCRF_DESC("Convert this Color to a hexadecimal string."),
|
||||
MCRF_RETURNS("str: Hex string in format '#RRGGBB' or '#RRGGBBAA' (if alpha < 255)")
|
||||
MCRF_NOTE("Alpha component is only included if not fully opaque (< 255)")
|
||||
)},
|
||||
{"lerp", (PyCFunction)PyColor::lerp, METH_VARARGS,
|
||||
MCRF_METHOD(Color, lerp,
|
||||
MCRF_SIG("(other: Color, t: float)", "Color"),
|
||||
MCRF_DESC("Linearly interpolate between this color and another."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("other", "The target Color to interpolate towards")
|
||||
MCRF_ARG("t", "Interpolation factor (0.0 = this color, 1.0 = other color). Automatically clamped to [0.0, 1.0]")
|
||||
MCRF_RETURNS("Color: New Color representing the interpolated value")
|
||||
MCRF_NOTE("All components (r, g, b, a) are interpolated independently")
|
||||
)},
|
||||
{"from_hex", (PyCFunction)PyColor::from_hex, METH_VARARGS | METH_CLASS, "Create Color from hex string (e.g., '#FF0000' or 'FF0000')"},
|
||||
{"to_hex", (PyCFunction)PyColor::to_hex, METH_NOARGS, "Convert Color to hex string"},
|
||||
{"lerp", (PyCFunction)PyColor::lerp, METH_VARARGS, "Linearly interpolate between this color and another"},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#include "PyDrawable.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
|
||||
// Click property getter
|
||||
static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure)
|
||||
|
|
@ -99,26 +98,14 @@ static int PyDrawable_set_opacity(PyDrawableObject* self, PyObject* value, void*
|
|||
|
||||
// GetSetDef array for properties
|
||||
static PyGetSetDef PyDrawable_getsetters[] = {
|
||||
{"click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click,
|
||||
MCRF_PROPERTY(click,
|
||||
"Callable executed when object is clicked. "
|
||||
"Function receives (x, y) coordinates of click."
|
||||
), NULL},
|
||||
{"click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click,
|
||||
"Callable executed when object is clicked", NULL},
|
||||
{"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index,
|
||||
MCRF_PROPERTY(z_index,
|
||||
"Z-order for rendering (lower values rendered first). "
|
||||
"Automatically triggers scene resort when changed."
|
||||
), NULL},
|
||||
"Z-order for rendering (lower values rendered first)", NULL},
|
||||
{"visible", (getter)PyDrawable_get_visible, (setter)PyDrawable_set_visible,
|
||||
MCRF_PROPERTY(visible,
|
||||
"Whether the object is visible (bool). "
|
||||
"Invisible objects are not rendered or clickable."
|
||||
), NULL},
|
||||
"Whether the object is visible", NULL},
|
||||
{"opacity", (getter)PyDrawable_get_opacity, (setter)PyDrawable_set_opacity,
|
||||
MCRF_PROPERTY(opacity,
|
||||
"Opacity level (0.0 = transparent, 1.0 = opaque). "
|
||||
"Automatically clamped to valid range [0.0, 1.0]."
|
||||
), NULL},
|
||||
"Opacity level (0.0 = transparent, 1.0 = opaque)", NULL},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
||||
|
|
@ -156,30 +143,11 @@ static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args)
|
|||
// Method definitions
|
||||
static PyMethodDef PyDrawable_methods[] = {
|
||||
{"get_bounds", (PyCFunction)PyDrawable_get_bounds, METH_NOARGS,
|
||||
MCRF_METHOD(Drawable, get_bounds,
|
||||
MCRF_SIG("()", "tuple"),
|
||||
MCRF_DESC("Get the bounding rectangle of this drawable element."),
|
||||
MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds")
|
||||
MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.")
|
||||
)},
|
||||
"Get bounding box as (x, y, width, height)"},
|
||||
{"move", (PyCFunction)PyDrawable_move, METH_VARARGS,
|
||||
MCRF_METHOD(Drawable, move,
|
||||
MCRF_SIG("(dx: float, dy: float)", "None"),
|
||||
MCRF_DESC("Move the element by a relative offset."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("dx", "Horizontal offset in pixels")
|
||||
MCRF_ARG("dy", "Vertical offset in pixels")
|
||||
MCRF_NOTE("This modifies the x and y position properties by the given amounts.")
|
||||
)},
|
||||
"Move by relative offset (dx, dy)"},
|
||||
{"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS,
|
||||
MCRF_METHOD(Drawable, resize,
|
||||
MCRF_SIG("(width: float, height: float)", "None"),
|
||||
MCRF_DESC("Resize the element to new dimensions."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("width", "New width in pixels")
|
||||
MCRF_ARG("height", "New height in pixels")
|
||||
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.")
|
||||
)},
|
||||
"Resize to new dimensions (width, height)"},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#include "PyFont.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
|
||||
|
||||
PyFont::PyFont(std::string filename)
|
||||
|
|
@ -74,9 +73,7 @@ PyObject* PyFont::get_source(PyFontObject* self, void* closure)
|
|||
}
|
||||
|
||||
PyGetSetDef PyFont::getsetters[] = {
|
||||
{"family", (getter)PyFont::get_family, NULL,
|
||||
MCRF_PROPERTY(family, "Font family name (str, read-only). Retrieved from font metadata."), NULL},
|
||||
{"source", (getter)PyFont::get_source, NULL,
|
||||
MCRF_PROPERTY(source, "Source filename path (str, read-only). The path used to load this font."), NULL},
|
||||
{"family", (getter)PyFont::get_family, NULL, "Font family name", NULL},
|
||||
{"source", (getter)PyFont::get_source, NULL, "Source filename of the font", NULL},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
#include "PyScene.h"
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <iostream>
|
||||
|
||||
// Static map to store Python scene objects by name
|
||||
|
|
@ -214,38 +213,19 @@ void PySceneClass::call_on_resize(PySceneObject* self, int width, int height)
|
|||
|
||||
// Properties
|
||||
PyGetSetDef PySceneClass::getsetters[] = {
|
||||
{"name", (getter)get_name, NULL,
|
||||
MCRF_PROPERTY(name, "Scene name (str, read-only). Unique identifier for this scene."), NULL},
|
||||
{"active", (getter)get_active, NULL,
|
||||
MCRF_PROPERTY(active, "Whether this scene is currently active (bool, read-only). Only one scene can be active at a time."), NULL},
|
||||
{"name", (getter)get_name, NULL, "Scene name", NULL},
|
||||
{"active", (getter)get_active, NULL, "Whether this scene is currently active", NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
// Methods
|
||||
PyMethodDef PySceneClass::methods[] = {
|
||||
{"activate", (PyCFunction)activate, METH_NOARGS,
|
||||
MCRF_METHOD(SceneClass, activate,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Make this the active scene."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Deactivates the current scene and activates this one. Scene transitions and lifecycle callbacks are triggered.")
|
||||
)},
|
||||
"Make this the active scene"},
|
||||
{"get_ui", (PyCFunction)get_ui, METH_NOARGS,
|
||||
MCRF_METHOD(SceneClass, get_ui,
|
||||
MCRF_SIG("()", "UICollection"),
|
||||
MCRF_DESC("Get the UI element collection for this scene."),
|
||||
MCRF_RETURNS("UICollection: Collection of UI elements (Frames, Captions, Sprites, Grids) in this scene")
|
||||
MCRF_NOTE("Use to add, remove, or iterate over UI elements. Changes are reflected immediately.")
|
||||
)},
|
||||
"Get the UI element collection for this scene"},
|
||||
{"register_keyboard", (PyCFunction)register_keyboard, METH_VARARGS,
|
||||
MCRF_METHOD(SceneClass, register_keyboard,
|
||||
MCRF_SIG("(callback: callable)", "None"),
|
||||
MCRF_DESC("Register a keyboard event handler function."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("callback", "Function that receives (key: str, pressed: bool) when keyboard events occur")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Alternative to overriding on_keypress() method. Handler is called for both key press and release events.")
|
||||
)},
|
||||
"Register a keyboard handler function (alternative to overriding on_keypress)"},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#include "PyTexture.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
|
||||
PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
|
||||
: source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0)
|
||||
|
|
@ -132,17 +131,11 @@ PyObject* PyTexture::get_source(PyTextureObject* self, void* closure)
|
|||
}
|
||||
|
||||
PyGetSetDef PyTexture::getsetters[] = {
|
||||
{"sprite_width", (getter)PyTexture::get_sprite_width, NULL,
|
||||
MCRF_PROPERTY(sprite_width, "Width of each sprite in pixels (int, read-only). Specified during texture initialization."), NULL},
|
||||
{"sprite_height", (getter)PyTexture::get_sprite_height, NULL,
|
||||
MCRF_PROPERTY(sprite_height, "Height of each sprite in pixels (int, read-only). Specified during texture initialization."), NULL},
|
||||
{"sheet_width", (getter)PyTexture::get_sheet_width, NULL,
|
||||
MCRF_PROPERTY(sheet_width, "Number of sprite columns in the texture sheet (int, read-only). Calculated as texture_width / sprite_width."), NULL},
|
||||
{"sheet_height", (getter)PyTexture::get_sheet_height, NULL,
|
||||
MCRF_PROPERTY(sheet_height, "Number of sprite rows in the texture sheet (int, read-only). Calculated as texture_height / sprite_height."), NULL},
|
||||
{"sprite_count", (getter)PyTexture::get_sprite_count, NULL,
|
||||
MCRF_PROPERTY(sprite_count, "Total number of sprites in the texture sheet (int, read-only). Equals sheet_width * sheet_height."), NULL},
|
||||
{"source", (getter)PyTexture::get_source, NULL,
|
||||
MCRF_PROPERTY(source, "Source filename path (str, read-only). The path used to load this texture."), NULL},
|
||||
{"sprite_width", (getter)PyTexture::get_sprite_width, NULL, "Width of each sprite in pixels", NULL},
|
||||
{"sprite_height", (getter)PyTexture::get_sprite_height, NULL, "Height of each sprite in pixels", NULL},
|
||||
{"sheet_width", (getter)PyTexture::get_sheet_width, NULL, "Number of sprite columns in the texture", NULL},
|
||||
{"sheet_height", (getter)PyTexture::get_sheet_height, NULL, "Number of sprite rows in the texture", NULL},
|
||||
{"sprite_count", (getter)PyTexture::get_sprite_count, NULL, "Total number of sprites in the texture", NULL},
|
||||
{"source", (getter)PyTexture::get_source, NULL, "Source filename of the texture", NULL},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
|
|
|||
118
src/PyTimer.cpp
118
src/PyTimer.cpp
|
|
@ -1,9 +1,7 @@
|
|||
#include "PyTimer.h"
|
||||
#include "Timer.h"
|
||||
#include "PyCallable.h"
|
||||
#include "GameEngine.h"
|
||||
#include "Resources.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <sstream>
|
||||
|
||||
PyObject* PyTimer::repr(PyObject* self) {
|
||||
|
|
@ -13,22 +11,7 @@ PyObject* PyTimer::repr(PyObject* self) {
|
|||
|
||||
if (timer->data) {
|
||||
oss << "interval=" << timer->data->getInterval() << "ms ";
|
||||
if (timer->data->isOnce()) {
|
||||
oss << "once=True ";
|
||||
}
|
||||
if (timer->data->isPaused()) {
|
||||
oss << "paused";
|
||||
// Get current time to show remaining
|
||||
int current_time = 0;
|
||||
if (Resources::game) {
|
||||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
oss << " (remaining=" << timer->data->getRemaining(current_time) << "ms)";
|
||||
} else if (timer->data->isActive()) {
|
||||
oss << "active";
|
||||
} else {
|
||||
oss << "cancelled";
|
||||
}
|
||||
oss << (timer->data->isPaused() ? "paused" : "active");
|
||||
} else {
|
||||
oss << "uninitialized";
|
||||
}
|
||||
|
|
@ -42,20 +25,18 @@ PyObject* PyTimer::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
|
|||
if (self) {
|
||||
new(&self->name) std::string(); // Placement new for std::string
|
||||
self->data = nullptr;
|
||||
self->weakreflist = nullptr; // Initialize weakref list
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"name", "callback", "interval", "once", NULL};
|
||||
static const char* kwlist[] = {"name", "callback", "interval", NULL};
|
||||
const char* name = nullptr;
|
||||
PyObject* callback = nullptr;
|
||||
int interval = 0;
|
||||
int once = 0; // Use int for bool parameter
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi|p", const_cast<char**>(kwlist),
|
||||
&name, &callback, &interval, &once)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi", const_cast<char**>(kwlist),
|
||||
&name, &callback, &interval)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -77,18 +58,8 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
|
|||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
|
||||
// Create the timer
|
||||
self->data = std::make_shared<Timer>(callback, interval, current_time, (bool)once);
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref); // Cache owns the reference now
|
||||
}
|
||||
}
|
||||
// Create the timer callable
|
||||
self->data = std::make_shared<PyTimerCallable>(callback, interval, current_time);
|
||||
|
||||
// Register with game engine
|
||||
if (Resources::game) {
|
||||
|
|
@ -99,11 +70,6 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
|
|||
}
|
||||
|
||||
void PyTimer::dealloc(PyTimerObject* self) {
|
||||
// Clear weakrefs first
|
||||
if (self->weakreflist != nullptr) {
|
||||
PyObject_ClearWeakRefs((PyObject*)self);
|
||||
}
|
||||
|
||||
// Remove from game engine if still registered
|
||||
if (Resources::game && !self->name.empty()) {
|
||||
auto it = Resources::game->timers.find(self->name);
|
||||
|
|
@ -278,80 +244,28 @@ int PyTimer::set_callback(PyTimerObject* self, PyObject* value, void* closure) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_once(PyTimerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return PyBool_FromLong(self->data->isOnce());
|
||||
}
|
||||
|
||||
int PyTimer::set_once(PyTimerObject* self, PyObject* value, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyBool_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "once must be a boolean");
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->data->setOnce(PyObject_IsTrue(value));
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_name(PyTimerObject* self, void* closure) {
|
||||
return PyUnicode_FromString(self->name.c_str());
|
||||
}
|
||||
|
||||
PyGetSetDef PyTimer::getsetters[] = {
|
||||
{"name", (getter)PyTimer::get_name, NULL,
|
||||
MCRF_PROPERTY(name, "Timer name (str, read-only). Unique identifier for this timer."), NULL},
|
||||
{"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval,
|
||||
MCRF_PROPERTY(interval, "Timer interval in milliseconds (int). Must be positive. Can be changed while timer is running."), NULL},
|
||||
"Timer interval in milliseconds", NULL},
|
||||
{"remaining", (getter)PyTimer::get_remaining, NULL,
|
||||
MCRF_PROPERTY(remaining, "Time remaining until next trigger in milliseconds (int, read-only). Preserved when timer is paused."), NULL},
|
||||
"Time remaining until next trigger in milliseconds", NULL},
|
||||
{"paused", (getter)PyTimer::get_paused, NULL,
|
||||
MCRF_PROPERTY(paused, "Whether the timer is paused (bool, read-only). Paused timers preserve their remaining time."), NULL},
|
||||
"Whether the timer is paused", NULL},
|
||||
{"active", (getter)PyTimer::get_active, NULL,
|
||||
MCRF_PROPERTY(active, "Whether the timer is active and not paused (bool, read-only). False if cancelled or paused."), NULL},
|
||||
"Whether the timer is active and not paused", NULL},
|
||||
{"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback,
|
||||
MCRF_PROPERTY(callback, "The callback function to be called when timer fires (callable). Can be changed while timer is running."), NULL},
|
||||
{"once", (getter)PyTimer::get_once, (setter)PyTimer::set_once,
|
||||
MCRF_PROPERTY(once, "Whether the timer stops after firing once (bool). If False, timer repeats indefinitely."), NULL},
|
||||
"The callback function to be called", NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyMethodDef PyTimer::methods[] = {
|
||||
{"pause", (PyCFunction)PyTimer::pause, METH_NOARGS,
|
||||
MCRF_METHOD(Timer, pause,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Pause the timer, preserving the time remaining until next trigger."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("The timer can be resumed later with resume(). Time spent paused does not count toward the interval.")
|
||||
)},
|
||||
"Pause the timer"},
|
||||
{"resume", (PyCFunction)PyTimer::resume, METH_NOARGS,
|
||||
MCRF_METHOD(Timer, resume,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Resume a paused timer from where it left off."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Has no effect if the timer is not paused. Timer will fire after the remaining time elapses.")
|
||||
)},
|
||||
"Resume a paused timer"},
|
||||
{"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS,
|
||||
MCRF_METHOD(Timer, cancel,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Cancel the timer and remove it from the timer system."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("The timer will no longer fire and cannot be restarted. The callback will not be called again.")
|
||||
)},
|
||||
"Cancel the timer and remove it from the system"},
|
||||
{"restart", (PyCFunction)PyTimer::restart, METH_NOARGS,
|
||||
MCRF_METHOD(Timer, restart,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Restart the timer from the beginning."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Resets the timer to fire after a full interval from now, regardless of remaining time.")
|
||||
)},
|
||||
"Restart the timer from the current time"},
|
||||
{NULL}
|
||||
};
|
||||
|
|
@ -4,13 +4,12 @@
|
|||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class Timer;
|
||||
class PyTimerCallable;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<Timer> data;
|
||||
std::shared_ptr<PyTimerCallable> data;
|
||||
std::string name;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyTimerObject;
|
||||
|
||||
class PyTimer
|
||||
|
|
@ -29,7 +28,6 @@ public:
|
|||
static PyObject* restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
|
||||
|
||||
// Timer property getters
|
||||
static PyObject* get_name(PyTimerObject* self, void* closure);
|
||||
static PyObject* get_interval(PyTimerObject* self, void* closure);
|
||||
static int set_interval(PyTimerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_remaining(PyTimerObject* self, void* closure);
|
||||
|
|
@ -37,8 +35,6 @@ public:
|
|||
static PyObject* get_active(PyTimerObject* self, void* closure);
|
||||
static PyObject* get_callback(PyTimerObject* self, void* closure);
|
||||
static int set_callback(PyTimerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_once(PyTimerObject* self, void* closure);
|
||||
static int set_once(PyTimerObject* self, PyObject* value, void* closure);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyMethodDef methods[];
|
||||
|
|
@ -53,35 +49,7 @@ namespace mcrfpydef {
|
|||
.tp_dealloc = (destructor)PyTimer::dealloc,
|
||||
.tp_repr = PyTimer::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Timer(name, callback, interval, once=False)\n\n"
|
||||
"Create a timer that calls a function at regular intervals.\n\n"
|
||||
"Args:\n"
|
||||
" name (str): Unique identifier for the timer\n"
|
||||
" callback (callable): Function to call - receives (timer, runtime) args\n"
|
||||
" interval (int): Time between calls in milliseconds\n"
|
||||
" once (bool): If True, timer stops after first call. Default: False\n\n"
|
||||
"Attributes:\n"
|
||||
" interval (int): Time between calls in milliseconds\n"
|
||||
" remaining (int): Time until next call in milliseconds (read-only)\n"
|
||||
" paused (bool): Whether timer is paused (read-only)\n"
|
||||
" active (bool): Whether timer is active and not paused (read-only)\n"
|
||||
" callback (callable): The callback function\n"
|
||||
" once (bool): Whether timer stops after firing once\n\n"
|
||||
"Methods:\n"
|
||||
" pause(): Pause the timer, preserving time remaining\n"
|
||||
" resume(): Resume a paused timer\n"
|
||||
" cancel(): Stop and remove the timer\n"
|
||||
" restart(): Reset timer to start from beginning\n\n"
|
||||
"Example:\n"
|
||||
" def on_timer(timer, runtime):\n"
|
||||
" print(f'Timer {timer} fired at {runtime}ms')\n"
|
||||
" if runtime > 5000:\n"
|
||||
" timer.cancel()\n"
|
||||
" \n"
|
||||
" timer = mcrfpy.Timer('my_timer', on_timer, 1000)\n"
|
||||
" timer.pause() # Pause timer\n"
|
||||
" timer.resume() # Resume timer\n"
|
||||
" timer.once = True # Make it one-shot"),
|
||||
.tp_doc = PyDoc_STR("Timer object for scheduled callbacks"),
|
||||
.tp_methods = PyTimer::methods,
|
||||
.tp_getset = PyTimer::getsetters,
|
||||
.tp_init = (initproc)PyTimer::init,
|
||||
|
|
|
|||
|
|
@ -1,65 +1,21 @@
|
|||
#include "PyVector.h"
|
||||
#include "PyObjectUtils.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <cmath>
|
||||
|
||||
PyGetSetDef PyVector::getsetters[] = {
|
||||
{"x", (getter)PyVector::get_member, (setter)PyVector::set_member,
|
||||
MCRF_PROPERTY(x, "X coordinate of the vector (float)"), (void*)0},
|
||||
{"y", (getter)PyVector::get_member, (setter)PyVector::set_member,
|
||||
MCRF_PROPERTY(y, "Y coordinate of the vector (float)"), (void*)1},
|
||||
{"x", (getter)PyVector::get_member, (setter)PyVector::set_member, "X/horizontal component", (void*)0},
|
||||
{"y", (getter)PyVector::get_member, (setter)PyVector::set_member, "Y/vertical component", (void*)1},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyMethodDef PyVector::methods[] = {
|
||||
{"magnitude", (PyCFunction)PyVector::magnitude, METH_NOARGS,
|
||||
MCRF_METHOD(Vector, magnitude,
|
||||
MCRF_SIG("()", "float"),
|
||||
MCRF_DESC("Calculate the length/magnitude of this vector."),
|
||||
MCRF_RETURNS("float: The magnitude of the vector")
|
||||
)},
|
||||
{"magnitude_squared", (PyCFunction)PyVector::magnitude_squared, METH_NOARGS,
|
||||
MCRF_METHOD(Vector, magnitude_squared,
|
||||
MCRF_SIG("()", "float"),
|
||||
MCRF_DESC("Calculate the squared magnitude of this vector."),
|
||||
MCRF_RETURNS("float: The squared magnitude (faster than magnitude())")
|
||||
MCRF_NOTE("Use this for comparisons to avoid expensive square root calculation.")
|
||||
)},
|
||||
{"normalize", (PyCFunction)PyVector::normalize, METH_NOARGS,
|
||||
MCRF_METHOD(Vector, normalize,
|
||||
MCRF_SIG("()", "Vector"),
|
||||
MCRF_DESC("Return a unit vector in the same direction."),
|
||||
MCRF_RETURNS("Vector: New normalized vector with magnitude 1.0")
|
||||
MCRF_NOTE("For zero vectors (magnitude 0.0), returns a zero vector rather than raising an exception")
|
||||
)},
|
||||
{"dot", (PyCFunction)PyVector::dot, METH_O,
|
||||
MCRF_METHOD(Vector, dot,
|
||||
MCRF_SIG("(other: Vector)", "float"),
|
||||
MCRF_DESC("Calculate the dot product with another vector."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("other", "The other vector")
|
||||
MCRF_RETURNS("float: Dot product of the two vectors")
|
||||
)},
|
||||
{"distance_to", (PyCFunction)PyVector::distance_to, METH_O,
|
||||
MCRF_METHOD(Vector, distance_to,
|
||||
MCRF_SIG("(other: Vector)", "float"),
|
||||
MCRF_DESC("Calculate the distance to another vector."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("other", "The other vector")
|
||||
MCRF_RETURNS("float: Distance between the two vectors")
|
||||
)},
|
||||
{"angle", (PyCFunction)PyVector::angle, METH_NOARGS,
|
||||
MCRF_METHOD(Vector, angle,
|
||||
MCRF_SIG("()", "float"),
|
||||
MCRF_DESC("Get the angle of this vector in radians."),
|
||||
MCRF_RETURNS("float: Angle in radians from positive x-axis")
|
||||
)},
|
||||
{"copy", (PyCFunction)PyVector::copy, METH_NOARGS,
|
||||
MCRF_METHOD(Vector, copy,
|
||||
MCRF_SIG("()", "Vector"),
|
||||
MCRF_DESC("Create a copy of this vector."),
|
||||
MCRF_RETURNS("Vector: New Vector object with same x and y values")
|
||||
)},
|
||||
{"magnitude", (PyCFunction)PyVector::magnitude, METH_NOARGS, "Return the length of the vector"},
|
||||
{"magnitude_squared", (PyCFunction)PyVector::magnitude_squared, METH_NOARGS, "Return the squared length of the vector"},
|
||||
{"normalize", (PyCFunction)PyVector::normalize, METH_NOARGS, "Return a unit vector in the same direction"},
|
||||
{"dot", (PyCFunction)PyVector::dot, METH_O, "Return the dot product with another vector"},
|
||||
{"distance_to", (PyCFunction)PyVector::distance_to, METH_O, "Return the distance to another vector"},
|
||||
{"angle", (PyCFunction)PyVector::angle, METH_NOARGS, "Return the angle in radians from the positive X axis"},
|
||||
{"copy", (PyCFunction)PyVector::copy, METH_NOARGS, "Return a copy of this vector"},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#include "PyWindow.h"
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <cstring>
|
||||
|
||||
|
|
@ -484,49 +483,32 @@ int PyWindow::set_scaling_mode(PyWindowObject* self, PyObject* value, void* clos
|
|||
|
||||
// Property definitions
|
||||
PyGetSetDef PyWindow::getsetters[] = {
|
||||
{"resolution", (getter)get_resolution, (setter)set_resolution,
|
||||
MCRF_PROPERTY(resolution, "Window resolution as (width, height) tuple. Setting this recreates the window."), NULL},
|
||||
{"resolution", (getter)get_resolution, (setter)set_resolution,
|
||||
"Window resolution as (width, height) tuple", NULL},
|
||||
{"fullscreen", (getter)get_fullscreen, (setter)set_fullscreen,
|
||||
MCRF_PROPERTY(fullscreen, "Window fullscreen state (bool). Setting this recreates the window."), NULL},
|
||||
"Window fullscreen state", NULL},
|
||||
{"vsync", (getter)get_vsync, (setter)set_vsync,
|
||||
MCRF_PROPERTY(vsync, "Vertical sync enabled state (bool). Prevents screen tearing but may limit framerate."), NULL},
|
||||
"Vertical sync enabled state", NULL},
|
||||
{"title", (getter)get_title, (setter)set_title,
|
||||
MCRF_PROPERTY(title, "Window title string (str). Displayed in the window title bar."), NULL},
|
||||
"Window title string", NULL},
|
||||
{"visible", (getter)get_visible, (setter)set_visible,
|
||||
MCRF_PROPERTY(visible, "Window visibility state (bool). Hidden windows still process events."), NULL},
|
||||
"Window visibility state", NULL},
|
||||
{"framerate_limit", (getter)get_framerate_limit, (setter)set_framerate_limit,
|
||||
MCRF_PROPERTY(framerate_limit, "Frame rate limit in FPS (int, 0 for unlimited). Caps maximum frame rate."), NULL},
|
||||
"Frame rate limit (0 for unlimited)", NULL},
|
||||
{"game_resolution", (getter)get_game_resolution, (setter)set_game_resolution,
|
||||
MCRF_PROPERTY(game_resolution, "Fixed game resolution as (width, height) tuple. Enables resolution-independent rendering with scaling."), NULL},
|
||||
"Fixed game resolution as (width, height) tuple", NULL},
|
||||
{"scaling_mode", (getter)get_scaling_mode, (setter)set_scaling_mode,
|
||||
MCRF_PROPERTY(scaling_mode, "Viewport scaling mode (str): 'center' (no scaling), 'stretch' (fill window), or 'fit' (maintain aspect ratio)."), NULL},
|
||||
"Viewport scaling mode: 'center', 'stretch', or 'fit'", NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
// Method definitions
|
||||
PyMethodDef PyWindow::methods[] = {
|
||||
{"get", (PyCFunction)PyWindow::get, METH_VARARGS | METH_CLASS,
|
||||
MCRF_METHOD(Window, get,
|
||||
MCRF_SIG("()", "Window"),
|
||||
MCRF_DESC("Get the Window singleton instance."),
|
||||
MCRF_RETURNS("Window: The global window object")
|
||||
MCRF_NOTE("This is a class method. Call as Window.get(). There is only one window instance per application.")
|
||||
)},
|
||||
"Get the Window singleton instance"},
|
||||
{"center", (PyCFunction)PyWindow::center, METH_NOARGS,
|
||||
MCRF_METHOD(Window, center,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Center the window on the screen."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Only works in windowed mode. Has no effect when fullscreen or in headless mode.")
|
||||
)},
|
||||
"Center the window on the screen"},
|
||||
{"screenshot", (PyCFunction)PyWindow::screenshot, METH_VARARGS | METH_KEYWORDS,
|
||||
MCRF_METHOD(Window, screenshot,
|
||||
MCRF_SIG("(filename: str = None)", "bytes | None"),
|
||||
MCRF_DESC("Take a screenshot of the current window contents."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("filename", "Optional path to save screenshot. If omitted, returns raw RGBA bytes.")
|
||||
MCRF_RETURNS("bytes | None: Raw RGBA pixel data if no filename given, otherwise None after saving")
|
||||
MCRF_NOTE("Screenshot is taken at the actual window resolution. Use after render loop update for current frame.")
|
||||
)},
|
||||
"Take a screenshot. Pass filename to save to file, or get raw bytes if no filename."},
|
||||
{NULL}
|
||||
};
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
#include "PythonObjectCache.h"
|
||||
#include <iostream>
|
||||
|
||||
PythonObjectCache* PythonObjectCache::instance = nullptr;
|
||||
|
||||
PythonObjectCache& PythonObjectCache::getInstance() {
|
||||
if (!instance) {
|
||||
instance = new PythonObjectCache();
|
||||
}
|
||||
return *instance;
|
||||
}
|
||||
|
||||
PythonObjectCache::~PythonObjectCache() {
|
||||
clear();
|
||||
}
|
||||
|
||||
uint64_t PythonObjectCache::assignSerial() {
|
||||
return next_serial.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void PythonObjectCache::registerObject(uint64_t serial, PyObject* weakref) {
|
||||
if (!weakref || serial == 0) return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(serial_mutex);
|
||||
|
||||
// Clean up any existing entry
|
||||
auto it = cache.find(serial);
|
||||
if (it != cache.end()) {
|
||||
Py_DECREF(it->second);
|
||||
}
|
||||
|
||||
// Store the new weak reference
|
||||
Py_INCREF(weakref);
|
||||
cache[serial] = weakref;
|
||||
}
|
||||
|
||||
PyObject* PythonObjectCache::lookup(uint64_t serial) {
|
||||
if (serial == 0) return nullptr;
|
||||
|
||||
// No mutex needed for read - GIL protects PyWeakref_GetObject
|
||||
auto it = cache.find(serial);
|
||||
if (it != cache.end()) {
|
||||
PyObject* obj = PyWeakref_GetObject(it->second);
|
||||
if (obj && obj != Py_None) {
|
||||
Py_INCREF(obj);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void PythonObjectCache::remove(uint64_t serial) {
|
||||
if (serial == 0) return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(serial_mutex);
|
||||
auto it = cache.find(serial);
|
||||
if (it != cache.end()) {
|
||||
Py_DECREF(it->second);
|
||||
cache.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void PythonObjectCache::cleanup() {
|
||||
std::lock_guard<std::mutex> lock(serial_mutex);
|
||||
|
||||
auto it = cache.begin();
|
||||
while (it != cache.end()) {
|
||||
PyObject* obj = PyWeakref_GetObject(it->second);
|
||||
if (!obj || obj == Py_None) {
|
||||
Py_DECREF(it->second);
|
||||
it = cache.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PythonObjectCache::clear() {
|
||||
std::lock_guard<std::mutex> lock(serial_mutex);
|
||||
|
||||
for (auto& pair : cache) {
|
||||
Py_DECREF(pair.second);
|
||||
}
|
||||
cache.clear();
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <Python.h>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
class PythonObjectCache {
|
||||
private:
|
||||
static PythonObjectCache* instance;
|
||||
std::mutex serial_mutex;
|
||||
std::atomic<uint64_t> next_serial{1};
|
||||
std::unordered_map<uint64_t, PyObject*> cache;
|
||||
|
||||
PythonObjectCache() = default;
|
||||
~PythonObjectCache();
|
||||
|
||||
public:
|
||||
static PythonObjectCache& getInstance();
|
||||
|
||||
// Assign a new serial number
|
||||
uint64_t assignSerial();
|
||||
|
||||
// Register a Python object with a serial number
|
||||
void registerObject(uint64_t serial, PyObject* weakref);
|
||||
|
||||
// Lookup a Python object by serial number
|
||||
// Returns new reference or nullptr
|
||||
PyObject* lookup(uint64_t serial);
|
||||
|
||||
// Remove an entry from the cache
|
||||
void remove(uint64_t serial);
|
||||
|
||||
// Clean up dead weak references
|
||||
void cleanup();
|
||||
|
||||
// Clear entire cache (for module cleanup)
|
||||
void clear();
|
||||
};
|
||||
127
src/Timer.cpp
127
src/Timer.cpp
|
|
@ -1,140 +1,31 @@
|
|||
#include "Timer.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "PyCallable.h"
|
||||
|
||||
Timer::Timer(PyObject* _target, int _interval, int now, bool _once)
|
||||
: callback(std::make_shared<PyCallable>(_target)), interval(_interval), last_ran(now),
|
||||
paused(false), pause_start_time(0), total_paused_time(0), once(_once)
|
||||
Timer::Timer(PyObject* _target, int _interval, int now)
|
||||
: target(_target), interval(_interval), last_ran(now)
|
||||
{}
|
||||
|
||||
Timer::Timer()
|
||||
: callback(std::make_shared<PyCallable>(Py_None)), interval(0), last_ran(0),
|
||||
paused(false), pause_start_time(0), total_paused_time(0), once(false)
|
||||
: target(Py_None), interval(0), last_ran(0)
|
||||
{}
|
||||
|
||||
Timer::~Timer() {
|
||||
if (serial_number != 0) {
|
||||
PythonObjectCache::getInstance().remove(serial_number);
|
||||
}
|
||||
}
|
||||
|
||||
bool Timer::hasElapsed(int now) const
|
||||
{
|
||||
if (paused) return false;
|
||||
return now >= last_ran + interval;
|
||||
}
|
||||
|
||||
bool Timer::test(int now)
|
||||
{
|
||||
if (!callback || callback->isNone()) return false;
|
||||
|
||||
if (hasElapsed(now))
|
||||
if (!target || target == Py_None) return false;
|
||||
if (now > last_ran + interval)
|
||||
{
|
||||
last_ran = now;
|
||||
|
||||
// Get the PyTimer wrapper from cache to pass to callback
|
||||
PyObject* timer_obj = nullptr;
|
||||
if (serial_number != 0) {
|
||||
timer_obj = PythonObjectCache::getInstance().lookup(serial_number);
|
||||
}
|
||||
|
||||
// Build args: (timer, runtime) or just (runtime) if no wrapper found
|
||||
PyObject* args;
|
||||
if (timer_obj) {
|
||||
args = Py_BuildValue("(Oi)", timer_obj, now);
|
||||
} else {
|
||||
// Fallback to old behavior if no wrapper found
|
||||
args = Py_BuildValue("(i)", now);
|
||||
}
|
||||
|
||||
PyObject* retval = callback->call(args, NULL);
|
||||
Py_DECREF(args);
|
||||
|
||||
PyObject* args = Py_BuildValue("(i)", now);
|
||||
PyObject* retval = PyObject_Call(target, args, NULL);
|
||||
if (!retval)
|
||||
{
|
||||
std::cout << "Timer callback has raised an exception. It's going to STDERR and being dropped:" << std::endl;
|
||||
std::cout << "timer has raised an exception. It's going to STDERR and being dropped:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
} else if (retval != Py_None)
|
||||
{
|
||||
std::cout << "Timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
Py_DECREF(retval);
|
||||
std::cout << "timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
}
|
||||
|
||||
// Handle one-shot timers
|
||||
if (once) {
|
||||
cancel();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Timer::pause(int current_time)
|
||||
{
|
||||
if (!paused) {
|
||||
paused = true;
|
||||
pause_start_time = current_time;
|
||||
}
|
||||
}
|
||||
|
||||
void Timer::resume(int current_time)
|
||||
{
|
||||
if (paused) {
|
||||
paused = false;
|
||||
int paused_duration = current_time - pause_start_time;
|
||||
total_paused_time += paused_duration;
|
||||
// Adjust last_ran to account for the pause
|
||||
last_ran += paused_duration;
|
||||
}
|
||||
}
|
||||
|
||||
void Timer::restart(int current_time)
|
||||
{
|
||||
last_ran = current_time;
|
||||
paused = false;
|
||||
pause_start_time = 0;
|
||||
total_paused_time = 0;
|
||||
}
|
||||
|
||||
void Timer::cancel()
|
||||
{
|
||||
// Cancel by setting callback to None
|
||||
callback = std::make_shared<PyCallable>(Py_None);
|
||||
}
|
||||
|
||||
bool Timer::isActive() const
|
||||
{
|
||||
return callback && !callback->isNone() && !paused;
|
||||
}
|
||||
|
||||
int Timer::getRemaining(int current_time) const
|
||||
{
|
||||
if (paused) {
|
||||
// When paused, calculate time remaining from when it was paused
|
||||
int elapsed_when_paused = pause_start_time - last_ran;
|
||||
return interval - elapsed_when_paused;
|
||||
}
|
||||
int elapsed = current_time - last_ran;
|
||||
return interval - elapsed;
|
||||
}
|
||||
|
||||
int Timer::getElapsed(int current_time) const
|
||||
{
|
||||
if (paused) {
|
||||
return pause_start_time - last_ran;
|
||||
}
|
||||
return current_time - last_ran;
|
||||
}
|
||||
|
||||
PyObject* Timer::getCallback()
|
||||
{
|
||||
if (!callback) return Py_None;
|
||||
return callback->borrow();
|
||||
}
|
||||
|
||||
void Timer::setCallback(PyObject* new_callback)
|
||||
{
|
||||
callback = std::make_shared<PyCallable>(new_callback);
|
||||
}
|
||||
47
src/Timer.h
47
src/Timer.h
|
|
@ -1,54 +1,15 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <memory>
|
||||
|
||||
class PyCallable;
|
||||
class GameEngine; // forward declare
|
||||
|
||||
class Timer
|
||||
{
|
||||
private:
|
||||
std::shared_ptr<PyCallable> callback;
|
||||
public:
|
||||
PyObject* target;
|
||||
int interval;
|
||||
int last_ran;
|
||||
|
||||
// Pause/resume support
|
||||
bool paused;
|
||||
int pause_start_time;
|
||||
int total_paused_time;
|
||||
|
||||
// One-shot timer support
|
||||
bool once;
|
||||
|
||||
public:
|
||||
uint64_t serial_number = 0; // For Python object cache
|
||||
|
||||
Timer(); // for map to build
|
||||
Timer(PyObject* target, int interval, int now, bool once = false);
|
||||
~Timer();
|
||||
|
||||
// Core timer functionality
|
||||
bool test(int now);
|
||||
bool hasElapsed(int now) const;
|
||||
|
||||
// Timer control methods
|
||||
void pause(int current_time);
|
||||
void resume(int current_time);
|
||||
void restart(int current_time);
|
||||
void cancel();
|
||||
|
||||
// Timer state queries
|
||||
bool isPaused() const { return paused; }
|
||||
bool isActive() const;
|
||||
int getInterval() const { return interval; }
|
||||
void setInterval(int new_interval) { interval = new_interval; }
|
||||
int getRemaining(int current_time) const;
|
||||
int getElapsed(int current_time) const;
|
||||
bool isOnce() const { return once; }
|
||||
void setOnce(bool value) { once = value; }
|
||||
|
||||
// Callback management
|
||||
PyObject* getCallback();
|
||||
void setCallback(PyObject* new_callback);
|
||||
Timer(PyObject*, int, int);
|
||||
bool test(int);
|
||||
};
|
||||
|
|
|
|||
41
src/UIBase.h
41
src/UIBase.h
|
|
@ -1,20 +1,17 @@
|
|||
#pragma once
|
||||
#include "Python.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <memory>
|
||||
|
||||
class UIEntity;
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UIEntity> data;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyUIEntityObject;
|
||||
|
||||
class UIFrame;
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UIFrame> data;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyUIFrameObject;
|
||||
|
||||
class UICaption;
|
||||
|
|
@ -22,21 +19,18 @@ typedef struct {
|
|||
PyObject_HEAD
|
||||
std::shared_ptr<UICaption> data;
|
||||
PyObject* font;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyUICaptionObject;
|
||||
|
||||
class UIGrid;
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UIGrid> data;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyUIGridObject;
|
||||
|
||||
class UISprite;
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UISprite> data;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyUISpriteObject;
|
||||
|
||||
// Common Python method implementations for UIDrawable-derived classes
|
||||
|
|
@ -79,30 +73,11 @@ static PyObject* UIDrawable_resize(T* self, PyObject* args)
|
|||
// Macro to add common UIDrawable methods to a method array
|
||||
#define UIDRAWABLE_METHODS \
|
||||
{"get_bounds", (PyCFunction)UIDrawable_get_bounds<PyObjectType>, METH_NOARGS, \
|
||||
MCRF_METHOD(Drawable, get_bounds, \
|
||||
MCRF_SIG("()", "tuple"), \
|
||||
MCRF_DESC("Get the bounding rectangle of this drawable element."), \
|
||||
MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds") \
|
||||
MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.") \
|
||||
)}, \
|
||||
"Get bounding box as (x, y, width, height)"}, \
|
||||
{"move", (PyCFunction)UIDrawable_move<PyObjectType>, METH_VARARGS, \
|
||||
MCRF_METHOD(Drawable, move, \
|
||||
MCRF_SIG("(dx: float, dy: float)", "None"), \
|
||||
MCRF_DESC("Move the element by a relative offset."), \
|
||||
MCRF_ARGS_START \
|
||||
MCRF_ARG("dx", "Horizontal offset in pixels") \
|
||||
MCRF_ARG("dy", "Vertical offset in pixels") \
|
||||
MCRF_NOTE("This modifies the x and y position properties by the given amounts.") \
|
||||
)}, \
|
||||
"Move by relative offset (dx, dy)"}, \
|
||||
{"resize", (PyCFunction)UIDrawable_resize<PyObjectType>, METH_VARARGS, \
|
||||
MCRF_METHOD(Drawable, resize, \
|
||||
MCRF_SIG("(width: float, height: float)", "None"), \
|
||||
MCRF_DESC("Resize the element to new dimensions."), \
|
||||
MCRF_ARGS_START \
|
||||
MCRF_ARG("width", "New width in pixels") \
|
||||
MCRF_ARG("height", "New height in pixels") \
|
||||
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.") \
|
||||
)}
|
||||
"Resize to new dimensions (width, height)"}
|
||||
|
||||
// Property getters/setters for visible and opacity
|
||||
template<typename T>
|
||||
|
|
@ -152,14 +127,8 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure)
|
|||
// Macro to add common UIDrawable properties to a getsetters array
|
||||
#define UIDRAWABLE_GETSETTERS \
|
||||
{"visible", (getter)UIDrawable_get_visible<PyObjectType>, (setter)UIDrawable_set_visible<PyObjectType>, \
|
||||
MCRF_PROPERTY(visible, \
|
||||
"Whether the object is visible (bool). " \
|
||||
"Invisible objects are not rendered or clickable." \
|
||||
), NULL}, \
|
||||
"Visibility flag", NULL}, \
|
||||
{"opacity", (getter)UIDrawable_get_opacity<PyObjectType>, (setter)UIDrawable_set_opacity<PyObjectType>, \
|
||||
MCRF_PROPERTY(opacity, \
|
||||
"Opacity level (0.0 = transparent, 1.0 = opaque). " \
|
||||
"Automatically clamped to valid range [0.0, 1.0]." \
|
||||
), NULL}
|
||||
"Opacity (0.0 = transparent, 1.0 = opaque)", NULL}
|
||||
|
||||
// UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
#include "PyColor.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyFont.h"
|
||||
#include "PythonObjectCache.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
#include <algorithm>
|
||||
|
||||
|
|
@ -273,16 +272,8 @@ PyGetSetDef UICaption::getsetters[] = {
|
|||
//{"children", (getter)PyUIFrame_get_children, NULL, "UICollection of objects on top of this one", NULL},
|
||||
{"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL},
|
||||
{"font_size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Font size (integer) in points", (void*)5},
|
||||
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||
MCRF_PROPERTY(click,
|
||||
"Callable executed when object is clicked. "
|
||||
"Function receives (x, y) coordinates of click."
|
||||
), (void*)PyObjectsEnum::UICAPTION},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
||||
MCRF_PROPERTY(z_index,
|
||||
"Z-order for rendering (lower values rendered first). "
|
||||
"Automatically triggers scene resort when changed."
|
||||
), (void*)PyObjectsEnum::UICAPTION},
|
||||
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UICAPTION},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION},
|
||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UICAPTION},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
{NULL}
|
||||
|
|
@ -448,19 +439,6 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
|||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
// Initialize weak reference list
|
||||
self->weakreflist = NULL;
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref); // Cache owns the reference now
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,10 +54,6 @@ namespace mcrfpydef {
|
|||
.tp_dealloc = (destructor)[](PyObject* self)
|
||||
{
|
||||
PyUICaptionObject* obj = (PyUICaptionObject*)self;
|
||||
// Clear weak references
|
||||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
// TODO - reevaluate with PyFont usage; UICaption does not own the font
|
||||
// release reference to font object
|
||||
if (obj->font) Py_DECREF(obj->font);
|
||||
|
|
@ -68,7 +64,7 @@ namespace mcrfpydef {
|
|||
//.tp_hash = NULL,
|
||||
//.tp_iter
|
||||
//.tp_iternext
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.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"
|
||||
|
|
@ -110,11 +106,7 @@ namespace mcrfpydef {
|
|||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
|
||||
{
|
||||
PyUICaptionObject* self = (PyUICaptionObject*)type->tp_alloc(type, 0);
|
||||
if (self) {
|
||||
self->data = std::make_shared<UICaption>();
|
||||
self->font = nullptr;
|
||||
self->weakreflist = nullptr;
|
||||
}
|
||||
if (self) self->data = std::make_shared<UICaption>();
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
#include "UIGrid.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PyObjectUtils.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include <climits>
|
||||
#include <algorithm>
|
||||
|
||||
|
|
@ -18,14 +17,6 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
if (drawable->serial_number != 0) {
|
||||
PyObject* cached = PythonObjectCache::getInstance().lookup(drawable->serial_number);
|
||||
if (cached) {
|
||||
return cached; // Already INCREF'd by lookup
|
||||
}
|
||||
}
|
||||
|
||||
PyTypeObject* type = nullptr;
|
||||
PyObject* obj = nullptr;
|
||||
|
||||
|
|
@ -37,7 +28,6 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
auto pyObj = (PyUIFrameObject*)type->tp_alloc(type, 0);
|
||||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UIFrame>(drawable);
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
|
|
@ -50,7 +40,6 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UICaption>(drawable);
|
||||
pyObj->font = nullptr;
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
|
|
@ -62,7 +51,6 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
auto pyObj = (PyUISpriteObject*)type->tp_alloc(type, 0);
|
||||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UISprite>(drawable);
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
|
|
@ -74,7 +62,6 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
auto pyObj = (PyUIGridObject*)type->tp_alloc(type, 0);
|
||||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UIGrid>(drawable);
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -5,113 +5,9 @@
|
|||
#include "UIGrid.h"
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PythonObjectCache.h"
|
||||
|
||||
UIDrawable::UIDrawable() : position(0.0f, 0.0f) { click_callable = NULL; }
|
||||
|
||||
UIDrawable::UIDrawable(const UIDrawable& other)
|
||||
: z_index(other.z_index),
|
||||
name(other.name),
|
||||
position(other.position),
|
||||
visible(other.visible),
|
||||
opacity(other.opacity),
|
||||
serial_number(0), // Don't copy serial number
|
||||
use_render_texture(other.use_render_texture),
|
||||
render_dirty(true) // Force redraw after copy
|
||||
{
|
||||
// Deep copy click_callable if it exists
|
||||
if (other.click_callable) {
|
||||
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
|
||||
}
|
||||
|
||||
// Deep copy render texture if needed
|
||||
if (other.render_texture && other.use_render_texture) {
|
||||
auto size = other.render_texture->getSize();
|
||||
enableRenderTexture(size.x, size.y);
|
||||
}
|
||||
}
|
||||
|
||||
UIDrawable& UIDrawable::operator=(const UIDrawable& other) {
|
||||
if (this != &other) {
|
||||
// Copy basic members
|
||||
z_index = other.z_index;
|
||||
name = other.name;
|
||||
position = other.position;
|
||||
visible = other.visible;
|
||||
opacity = other.opacity;
|
||||
use_render_texture = other.use_render_texture;
|
||||
render_dirty = true; // Force redraw after copy
|
||||
|
||||
// Deep copy click_callable
|
||||
if (other.click_callable) {
|
||||
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
|
||||
} else {
|
||||
click_callable.reset();
|
||||
}
|
||||
|
||||
// Deep copy render texture if needed
|
||||
if (other.render_texture && other.use_render_texture) {
|
||||
auto size = other.render_texture->getSize();
|
||||
enableRenderTexture(size.x, size.y);
|
||||
} else {
|
||||
render_texture.reset();
|
||||
use_render_texture = false;
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
UIDrawable::UIDrawable(UIDrawable&& other) noexcept
|
||||
: z_index(other.z_index),
|
||||
name(std::move(other.name)),
|
||||
position(other.position),
|
||||
visible(other.visible),
|
||||
opacity(other.opacity),
|
||||
serial_number(other.serial_number),
|
||||
click_callable(std::move(other.click_callable)),
|
||||
render_texture(std::move(other.render_texture)),
|
||||
render_sprite(std::move(other.render_sprite)),
|
||||
use_render_texture(other.use_render_texture),
|
||||
render_dirty(other.render_dirty)
|
||||
{
|
||||
// Clear the moved-from object's serial number to avoid cache issues
|
||||
other.serial_number = 0;
|
||||
}
|
||||
|
||||
UIDrawable& UIDrawable::operator=(UIDrawable&& other) noexcept {
|
||||
if (this != &other) {
|
||||
// Clear our own cache entry if we have one
|
||||
if (serial_number != 0) {
|
||||
PythonObjectCache::getInstance().remove(serial_number);
|
||||
}
|
||||
|
||||
// Move basic members
|
||||
z_index = other.z_index;
|
||||
name = std::move(other.name);
|
||||
position = other.position;
|
||||
visible = other.visible;
|
||||
opacity = other.opacity;
|
||||
serial_number = other.serial_number;
|
||||
use_render_texture = other.use_render_texture;
|
||||
render_dirty = other.render_dirty;
|
||||
|
||||
// Move unique_ptr members
|
||||
click_callable = std::move(other.click_callable);
|
||||
render_texture = std::move(other.render_texture);
|
||||
render_sprite = std::move(other.render_sprite);
|
||||
|
||||
// Clear the moved-from object's serial number
|
||||
other.serial_number = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
UIDrawable::~UIDrawable() {
|
||||
if (serial_number != 0) {
|
||||
PythonObjectCache::getInstance().remove(serial_number);
|
||||
}
|
||||
}
|
||||
|
||||
void UIDrawable::click_unregister()
|
||||
{
|
||||
click_callable.reset();
|
||||
|
|
|
|||
|
|
@ -39,15 +39,6 @@ public:
|
|||
void click_unregister();
|
||||
|
||||
UIDrawable();
|
||||
virtual ~UIDrawable();
|
||||
|
||||
// Copy constructor and assignment operator
|
||||
UIDrawable(const UIDrawable& other);
|
||||
UIDrawable& operator=(const UIDrawable& other);
|
||||
|
||||
// Move constructor and assignment operator
|
||||
UIDrawable(UIDrawable&& other) noexcept;
|
||||
UIDrawable& operator=(UIDrawable&& other) noexcept;
|
||||
|
||||
static PyObject* get_click(PyObject* self, void* closure);
|
||||
static int set_click(PyObject* self, PyObject* value, void* closure);
|
||||
|
|
@ -99,9 +90,6 @@ public:
|
|||
virtual bool getProperty(const std::string& name, sf::Vector2f& value) const { return false; }
|
||||
virtual bool getProperty(const std::string& name, std::string& value) const { return false; }
|
||||
|
||||
// Python object cache support
|
||||
uint64_t serial_number = 0;
|
||||
|
||||
protected:
|
||||
// RenderTexture support (opt-in)
|
||||
std::unique_ptr<sf::RenderTexture> render_texture;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
#include <algorithm>
|
||||
#include "PyObjectUtils.h"
|
||||
#include "PyVector.h"
|
||||
#include "PythonObjectCache.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
#include "UIEntityPyMethods.h"
|
||||
|
||||
|
|
@ -17,12 +16,6 @@ UIEntity::UIEntity()
|
|||
// gridstate vector starts empty - will be lazily initialized when needed
|
||||
}
|
||||
|
||||
UIEntity::~UIEntity() {
|
||||
if (serial_number != 0) {
|
||||
PythonObjectCache::getInstance().remove(serial_number);
|
||||
}
|
||||
}
|
||||
|
||||
// Removed UIEntity(UIGrid&) constructor - using lazy initialization instead
|
||||
|
||||
void UIEntity::updateVisibility()
|
||||
|
|
@ -193,21 +186,8 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|||
|
||||
// Create the entity
|
||||
self->data = std::make_shared<UIEntity>();
|
||||
|
||||
// Initialize weak reference list
|
||||
self->weakreflist = NULL;
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref); // Cache owns the reference now
|
||||
}
|
||||
}
|
||||
|
||||
// Store reference to Python object (legacy - to be removed)
|
||||
// Store reference to Python object
|
||||
self->data->self = (PyObject*)self;
|
||||
Py_INCREF(self);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,37 +14,12 @@
|
|||
#include "PyFont.h"
|
||||
|
||||
#include "UIGridPoint.h"
|
||||
#include "UIDrawable.h"
|
||||
#include "UIBase.h"
|
||||
#include "UISprite.h"
|
||||
|
||||
class UIGrid;
|
||||
|
||||
// UIEntity
|
||||
/*
|
||||
|
||||
****************************************
|
||||
* say it with me: *
|
||||
* ✨ UIEntity is not a UIDrawable ✨ *
|
||||
****************************************
|
||||
|
||||
**Why Not, John?**
|
||||
Doesn't it say "UI" on the front of it?
|
||||
It sure does. Probably should have called it "GridEntity", but it's a bit late now.
|
||||
|
||||
UIDrawables have positions in **screen pixel coordinates**. Their position is an offset from their parent's position, and they are deeply nestable (Scene -> Frame -> Frame -> ...)
|
||||
|
||||
However:
|
||||
UIEntity has a position in **Grid tile coordinates**.
|
||||
UIEntity is not nestable at all. Grid -> Entity.
|
||||
UIEntity has a strict one/none relationship with a Grid: if you add it to another grid, it will have itself removed from the losing grid's collection.
|
||||
UIEntity originally only allowed a single-tile sprite, but around mid-July 2025, I'm working to change that to allow any UIDrawable to go there, or multi-tile sprites.
|
||||
UIEntity is, at its core, the container for *a perspective of map data*.
|
||||
The Grid should contain the true, complete contents of the game's space, and the Entity should use pathfinding, field of view, and game logic to interact with the Grid's layer data.
|
||||
|
||||
In Conclusion, because UIEntity cannot be drawn on a Frame or Scene, and has the unique role of serving as a filter of the data contained in a Grid, UIEntity will not become a UIDrawable.
|
||||
|
||||
*/
|
||||
|
||||
//class UIEntity;
|
||||
//typedef struct {
|
||||
// PyObject_HEAD
|
||||
|
|
@ -57,11 +32,11 @@ sf::Vector2f PyObject_to_sfVector2f(PyObject* obj);
|
|||
PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state);
|
||||
PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec);
|
||||
|
||||
class UIEntity
|
||||
// TODO: make UIEntity a drawable
|
||||
class UIEntity//: public UIDrawable
|
||||
{
|
||||
public:
|
||||
PyObject* self = nullptr; // Reference to the Python object (if created from Python)
|
||||
uint64_t serial_number = 0; // For Python object cache
|
||||
std::shared_ptr<UIGrid> grid;
|
||||
std::vector<UIGridPointState> gridstate;
|
||||
UISprite sprite;
|
||||
|
|
@ -69,7 +44,6 @@ public:
|
|||
//void render(sf::Vector2f); //override final;
|
||||
|
||||
UIEntity();
|
||||
~UIEntity();
|
||||
|
||||
// Visibility methods
|
||||
void updateVisibility(); // Update gridstate from current FOV
|
||||
|
|
@ -138,7 +112,7 @@ namespace mcrfpydef {
|
|||
" name (str): Element name"),
|
||||
.tp_methods = UIEntity_all_methods,
|
||||
.tp_getset = UIEntity::getsetters,
|
||||
.tp_base = NULL,
|
||||
.tp_base = &mcrfpydef::PyDrawableType,
|
||||
.tp_init = (initproc)UIEntity::init,
|
||||
.tp_new = PyType_GenericNew,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PythonObjectCache.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
|
||||
UIDrawable* UIFrame::click_at(sf::Vector2f point)
|
||||
|
|
@ -398,16 +397,8 @@ PyGetSetDef UIFrame::getsetters[] = {
|
|||
{"fill_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Fill color of the rectangle", (void*)0},
|
||||
{"outline_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Outline color of the rectangle", (void*)1},
|
||||
{"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL},
|
||||
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||
MCRF_PROPERTY(click,
|
||||
"Callable executed when object is clicked. "
|
||||
"Function receives (x, y) coordinates of click."
|
||||
), (void*)PyObjectsEnum::UIFRAME},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
||||
MCRF_PROPERTY(z_index,
|
||||
"Z-order for rendering (lower values rendered first). "
|
||||
"Automatically triggers scene resort when changed."
|
||||
), (void*)PyObjectsEnum::UIFRAME},
|
||||
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIFRAME},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIFRAME},
|
||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIFRAME},
|
||||
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UIFRAME},
|
||||
{"clip_children", (getter)UIFrame::get_clip_children, (setter)UIFrame::set_clip_children, "Whether to clip children to frame bounds", NULL},
|
||||
|
|
@ -440,9 +431,6 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
// Initialize children first
|
||||
self->data->children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
|
||||
|
||||
// Initialize weak reference list
|
||||
self->weakreflist = NULL;
|
||||
|
||||
// Define all parameters with defaults
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* size_obj = nullptr;
|
||||
|
|
@ -636,16 +624,6 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref); // Cache owns the reference now
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,10 +78,6 @@ namespace mcrfpydef {
|
|||
.tp_dealloc = (destructor)[](PyObject* self)
|
||||
{
|
||||
PyUIFrameObject* obj = (PyUIFrameObject*)self;
|
||||
// Clear weak references
|
||||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
obj->data.reset();
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
|
|
@ -89,7 +85,7 @@ namespace mcrfpydef {
|
|||
//.tp_hash = NULL,
|
||||
//.tp_iter
|
||||
//.tp_iternext
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.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"
|
||||
|
|
@ -131,10 +127,7 @@ namespace mcrfpydef {
|
|||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
|
||||
{
|
||||
PyUIFrameObject* self = (PyUIFrameObject*)type->tp_alloc(type, 0);
|
||||
if (self) {
|
||||
self->data = std::make_shared<UIFrame>();
|
||||
self->weakreflist = nullptr;
|
||||
}
|
||||
if (self) self->data = std::make_shared<UIFrame>();
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
409
src/UIGrid.cpp
409
src/UIGrid.cpp
|
|
@ -1,16 +1,13 @@
|
|||
#include "UIGrid.h"
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "UIEntity.h"
|
||||
#include "Profiler.h"
|
||||
#include <algorithm>
|
||||
// UIDrawable methods now in UIBase.h
|
||||
|
||||
UIGrid::UIGrid()
|
||||
: grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr),
|
||||
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
||||
perspective_enabled(false) // Default to omniscient view
|
||||
perspective(-1) // Default to omniscient view
|
||||
{
|
||||
// Initialize entities list
|
||||
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
|
||||
|
|
@ -38,7 +35,7 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
|
|||
zoom(1.0f),
|
||||
ptex(_ptex), points(gx * gy),
|
||||
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
||||
perspective_enabled(false) // Default to omniscient view
|
||||
perspective(-1) // Default to omniscient view
|
||||
{
|
||||
// Use texture dimensions if available, otherwise use defaults
|
||||
int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||
|
|
@ -96,14 +93,11 @@ void UIGrid::update() {}
|
|||
|
||||
void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||
{
|
||||
// Profile total grid rendering time
|
||||
ScopedTimer gridTimer(Resources::game->metrics.gridRenderTime);
|
||||
|
||||
// Check visibility
|
||||
if (!visible) return;
|
||||
|
||||
|
||||
// TODO: Apply opacity to output sprite
|
||||
|
||||
|
||||
output.setPosition(box.getPosition() + offset); // output sprite can move; update position when drawing
|
||||
// output size can change; update size when drawing
|
||||
output.setTextureRect(
|
||||
|
|
@ -139,12 +133,11 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
if (y_limit > grid_y) y_limit = grid_y;
|
||||
|
||||
// base layer - bottom color, tile sprite ("ground")
|
||||
int cellsRendered = 0;
|
||||
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
||||
x < x_limit; //x < view_width;
|
||||
x < x_limit; //x < view_width;
|
||||
x+=1)
|
||||
{
|
||||
//for (float y = (top_edge >= 0 ? top_edge : 0);
|
||||
//for (float y = (top_edge >= 0 ? top_edge : 0);
|
||||
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
||||
y < y_limit; //y < view_height;
|
||||
y+=1)
|
||||
|
|
@ -168,123 +161,81 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
sprite = ptex->sprite(gridpoint.tilesprite, pixel_pos, sf::Vector2f(zoom, zoom)); //setSprite(gridpoint.tilesprite);;
|
||||
renderTexture.draw(sprite);
|
||||
}
|
||||
|
||||
cellsRendered++;
|
||||
}
|
||||
}
|
||||
|
||||
// Record how many cells were rendered
|
||||
Resources::game->metrics.gridCellsRendered += cellsRendered;
|
||||
|
||||
// middle layer - entities
|
||||
// disabling entity rendering until I can render their UISprite inside the rendertexture (not directly to window)
|
||||
{
|
||||
ScopedTimer entityTimer(Resources::game->metrics.entityRenderTime);
|
||||
int entitiesRendered = 0;
|
||||
int totalEntities = entities->size();
|
||||
|
||||
for (auto e : *entities) {
|
||||
// Skip out-of-bounds entities for performance
|
||||
// Check if entity is within visible bounds (with 1 cell margin for partially visible entities)
|
||||
if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 ||
|
||||
e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) {
|
||||
continue; // Skip this entity as it's not visible
|
||||
}
|
||||
|
||||
//auto drawent = e->cGrid->indexsprite.drawable();
|
||||
auto& drawent = e->sprite;
|
||||
//drawent.setScale(zoom, zoom);
|
||||
drawent.setScale(sf::Vector2f(zoom, zoom));
|
||||
auto pixel_pos = sf::Vector2f(
|
||||
(e->position.x*cell_width - left_spritepixels) * zoom,
|
||||
(e->position.y*cell_height - top_spritepixels) * zoom );
|
||||
//drawent.setPosition(pixel_pos);
|
||||
//renderTexture.draw(drawent);
|
||||
drawent.render(pixel_pos, renderTexture);
|
||||
|
||||
entitiesRendered++;
|
||||
for (auto e : *entities) {
|
||||
// Skip out-of-bounds entities for performance
|
||||
// Check if entity is within visible bounds (with 1 cell margin for partially visible entities)
|
||||
if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 ||
|
||||
e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) {
|
||||
continue; // Skip this entity as it's not visible
|
||||
}
|
||||
|
||||
// Record entity rendering stats
|
||||
Resources::game->metrics.entitiesRendered += entitiesRendered;
|
||||
Resources::game->metrics.totalEntities += totalEntities;
|
||||
|
||||
//auto drawent = e->cGrid->indexsprite.drawable();
|
||||
auto& drawent = e->sprite;
|
||||
//drawent.setScale(zoom, zoom);
|
||||
drawent.setScale(sf::Vector2f(zoom, zoom));
|
||||
auto pixel_pos = sf::Vector2f(
|
||||
(e->position.x*cell_width - left_spritepixels) * zoom,
|
||||
(e->position.y*cell_height - top_spritepixels) * zoom );
|
||||
//drawent.setPosition(pixel_pos);
|
||||
//renderTexture.draw(drawent);
|
||||
drawent.render(pixel_pos, renderTexture);
|
||||
}
|
||||
|
||||
|
||||
// top layer - opacity for discovered / visible status based on perspective
|
||||
// Only render visibility overlay if perspective is enabled
|
||||
if (perspective_enabled) {
|
||||
ScopedTimer fovTimer(Resources::game->metrics.fovOverlayTime);
|
||||
auto entity = perspective_entity.lock();
|
||||
// Only render visibility overlay if perspective is set (not omniscient)
|
||||
if (perspective >= 0 && perspective < static_cast<int>(entities->size())) {
|
||||
// Get the entity whose perspective we're using
|
||||
auto it = entities->begin();
|
||||
std::advance(it, perspective);
|
||||
auto& entity = *it;
|
||||
|
||||
// Create rectangle for overlays
|
||||
sf::RectangleShape overlay;
|
||||
overlay.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
|
||||
|
||||
if (entity) {
|
||||
// Valid entity - use its gridstate for visibility
|
||||
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
||||
x < x_limit;
|
||||
x+=1)
|
||||
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
||||
x < x_limit;
|
||||
x+=1)
|
||||
{
|
||||
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
||||
y < y_limit;
|
||||
y+=1)
|
||||
{
|
||||
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
||||
y < y_limit;
|
||||
y+=1)
|
||||
{
|
||||
// Skip out-of-bounds cells
|
||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
|
||||
|
||||
auto pixel_pos = sf::Vector2f(
|
||||
(x*cell_width - left_spritepixels) * zoom,
|
||||
(y*cell_height - top_spritepixels) * zoom );
|
||||
// Skip out-of-bounds cells
|
||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
|
||||
|
||||
auto pixel_pos = sf::Vector2f(
|
||||
(x*cell_width - left_spritepixels) * zoom,
|
||||
(y*cell_height - top_spritepixels) * zoom );
|
||||
|
||||
// Get visibility state from entity's perspective
|
||||
int idx = y * grid_x + x;
|
||||
if (idx >= 0 && idx < static_cast<int>(entity->gridstate.size())) {
|
||||
const auto& state = entity->gridstate[idx];
|
||||
|
||||
overlay.setPosition(pixel_pos);
|
||||
|
||||
// Three overlay colors as specified:
|
||||
if (!state.discovered) {
|
||||
// Never seen - black
|
||||
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
||||
renderTexture.draw(overlay);
|
||||
} else if (!state.visible) {
|
||||
// Discovered but not currently visible - dark gray
|
||||
overlay.setFillColor(sf::Color(32, 32, 40, 192));
|
||||
renderTexture.draw(overlay);
|
||||
}
|
||||
// If visible and discovered, no overlay (fully visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Invalid/destroyed entity with perspective_enabled = true
|
||||
// Show all cells as undiscovered (black)
|
||||
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
||||
x < x_limit;
|
||||
x+=1)
|
||||
{
|
||||
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
||||
y < y_limit;
|
||||
y+=1)
|
||||
{
|
||||
// Skip out-of-bounds cells
|
||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
|
||||
|
||||
auto pixel_pos = sf::Vector2f(
|
||||
(x*cell_width - left_spritepixels) * zoom,
|
||||
(y*cell_height - top_spritepixels) * zoom );
|
||||
// Get visibility state from entity's perspective
|
||||
int idx = y * grid_x + x;
|
||||
if (idx >= 0 && idx < static_cast<int>(entity->gridstate.size())) {
|
||||
const auto& state = entity->gridstate[idx];
|
||||
|
||||
overlay.setPosition(pixel_pos);
|
||||
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
||||
renderTexture.draw(overlay);
|
||||
|
||||
// Three overlay colors as specified:
|
||||
if (!state.discovered) {
|
||||
// Never seen - black
|
||||
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
||||
renderTexture.draw(overlay);
|
||||
} else if (!state.visible) {
|
||||
// Discovered but not currently visible - dark gray
|
||||
overlay.setFillColor(sf::Color(32, 32, 40, 192));
|
||||
renderTexture.draw(overlay);
|
||||
}
|
||||
// If visible and discovered, no overlay (fully visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// else: omniscient view (no overlays)
|
||||
|
||||
// grid lines for testing & validation
|
||||
/*
|
||||
|
|
@ -364,7 +315,6 @@ void UIGrid::computeFOV(int x, int y, int radius, bool light_walls, TCOD_fov_alg
|
|||
{
|
||||
if (!tcod_map || x < 0 || x >= grid_x || y < 0 || y >= grid_y) return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(fov_mutex);
|
||||
tcod_map->computeFov(x, y, radius, light_walls, algo);
|
||||
}
|
||||
|
||||
|
|
@ -372,7 +322,6 @@ bool UIGrid::isInFOV(int x, int y) const
|
|||
{
|
||||
if (!tcod_map || x < 0 || x >= grid_x || y < 0 || y >= grid_y) return false;
|
||||
|
||||
std::lock_guard<std::mutex> lock(fov_mutex);
|
||||
return tcod_map->isInFov(x, y);
|
||||
}
|
||||
|
||||
|
|
@ -577,7 +526,7 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
PyObject* click_handler = nullptr;
|
||||
float center_x = 0.0f, center_y = 0.0f;
|
||||
float zoom = 1.0f;
|
||||
// perspective is now handled via properties, not init args
|
||||
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;
|
||||
|
|
@ -589,15 +538,15 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
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",
|
||||
"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, "|OOOOOOfffifizffffii", const_cast<char**>(kwlist),
|
||||
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,
|
||||
&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;
|
||||
}
|
||||
|
|
@ -703,8 +652,7 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
self->data->center_x = center_x;
|
||||
self->data->center_y = center_y;
|
||||
self->data->zoom = zoom;
|
||||
// perspective is now handled by perspective_entity and perspective_enabled
|
||||
// self->data->perspective = perspective;
|
||||
self->data->perspective = perspective;
|
||||
self->data->visible = visible;
|
||||
self->data->opacity = opacity;
|
||||
self->data->z_index = z_index;
|
||||
|
|
@ -732,19 +680,6 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
// Initialize weak reference list
|
||||
self->weakreflist = NULL;
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref); // Cache owns the reference now
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // Success
|
||||
}
|
||||
|
||||
|
|
@ -992,77 +927,33 @@ int UIGrid::set_fill_color(PyUIGridObject* self, PyObject* value, void* closure)
|
|||
|
||||
PyObject* UIGrid::get_perspective(PyUIGridObject* self, void* closure)
|
||||
{
|
||||
auto locked = self->data->perspective_entity.lock();
|
||||
if (locked) {
|
||||
// Check cache first to preserve derived class
|
||||
if (locked->serial_number != 0) {
|
||||
PyObject* cached = PythonObjectCache::getInstance().lookup(locked->serial_number);
|
||||
if (cached) {
|
||||
return cached; // Already INCREF'd by lookup
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy: If the entity has a stored Python object reference
|
||||
if (locked->self != nullptr) {
|
||||
Py_INCREF(locked->self);
|
||||
return locked->self;
|
||||
}
|
||||
|
||||
// Otherwise, create a new base Entity object
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
||||
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
||||
if (o) {
|
||||
o->data = locked;
|
||||
o->weakreflist = NULL;
|
||||
Py_DECREF(type);
|
||||
return (PyObject*)o;
|
||||
}
|
||||
Py_XDECREF(type);
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
return PyLong_FromLong(self->data->perspective);
|
||||
}
|
||||
|
||||
int UIGrid::set_perspective(PyUIGridObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (value == Py_None) {
|
||||
// Clear perspective but keep perspective_enabled unchanged
|
||||
self->data->perspective_entity.reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Extract UIEntity from PyObject
|
||||
// Get the Entity type from the module
|
||||
auto entity_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
||||
if (!entity_type) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Could not get Entity type from mcrfpy module");
|
||||
long perspective = PyLong_AsLong(value);
|
||||
if (PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyObject_IsInstance(value, entity_type)) {
|
||||
Py_DECREF(entity_type);
|
||||
PyErr_SetString(PyExc_TypeError, "perspective must be a UIEntity or None");
|
||||
// Validate perspective (-1 for omniscient, or valid entity index)
|
||||
if (perspective < -1) {
|
||||
PyErr_SetString(PyExc_ValueError, "perspective must be -1 (omniscient) or a valid entity index");
|
||||
return -1;
|
||||
}
|
||||
Py_DECREF(entity_type);
|
||||
|
||||
PyUIEntityObject* entity_obj = (PyUIEntityObject*)value;
|
||||
self->data->perspective_entity = entity_obj->data;
|
||||
self->data->perspective_enabled = true; // Enable perspective when entity assigned
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::get_perspective_enabled(PyUIGridObject* self, void* closure)
|
||||
{
|
||||
return PyBool_FromLong(self->data->perspective_enabled);
|
||||
}
|
||||
|
||||
int UIGrid::set_perspective_enabled(PyUIGridObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
int enabled = PyObject_IsTrue(value);
|
||||
if (enabled == -1) {
|
||||
return -1; // Error occurred
|
||||
// Check if entity index is valid (if not omniscient)
|
||||
if (perspective >= 0 && self->data->entities) {
|
||||
int entity_count = self->data->entities->size();
|
||||
if (perspective >= entity_count) {
|
||||
PyErr_Format(PyExc_IndexError, "perspective index %ld out of range (grid has %d entities)",
|
||||
perspective, entity_count);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
self->data->perspective_enabled = enabled;
|
||||
|
||||
self->data->perspective = perspective;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1079,43 +970,8 @@ PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject*
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// Compute FOV
|
||||
self->data->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
|
||||
|
||||
// Build list of visible cells as tuples (x, y, visible, discovered)
|
||||
PyObject* result_list = PyList_New(0);
|
||||
if (!result_list) return NULL;
|
||||
|
||||
// Iterate through grid and collect visible cells
|
||||
for (int gy = 0; gy < self->data->grid_y; gy++) {
|
||||
for (int gx = 0; gx < self->data->grid_x; gx++) {
|
||||
if (self->data->isInFOV(gx, gy)) {
|
||||
// Create tuple (x, y, visible, discovered)
|
||||
PyObject* cell_tuple = PyTuple_New(4);
|
||||
if (!cell_tuple) {
|
||||
Py_DECREF(result_list);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyTuple_SET_ITEM(cell_tuple, 0, PyLong_FromLong(gx));
|
||||
PyTuple_SET_ITEM(cell_tuple, 1, PyLong_FromLong(gy));
|
||||
PyTuple_SET_ITEM(cell_tuple, 2, Py_True); // visible
|
||||
PyTuple_SET_ITEM(cell_tuple, 3, Py_True); // discovered
|
||||
Py_INCREF(Py_True); // Need to increment ref count for True
|
||||
Py_INCREF(Py_True);
|
||||
|
||||
// Append to list
|
||||
if (PyList_Append(result_list, cell_tuple) < 0) {
|
||||
Py_DECREF(cell_tuple);
|
||||
Py_DECREF(result_list);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(cell_tuple); // List now owns the reference
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result_list;
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args)
|
||||
|
|
@ -1233,20 +1089,16 @@ PyObject* UIGrid::py_compute_astar_path(PyUIGridObject* self, PyObject* args, Py
|
|||
PyMethodDef UIGrid::methods[] = {
|
||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
||||
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> List[Tuple[int, int, bool, bool]]\n\n"
|
||||
"Compute field of view from a position and return visible cells.\n\n"
|
||||
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
|
||||
"Compute field of view from a position.\n\n"
|
||||
"Args:\n"
|
||||
" x: X coordinate of the viewer\n"
|
||||
" y: Y coordinate of the viewer\n"
|
||||
" radius: Maximum view distance (0 = unlimited)\n"
|
||||
" light_walls: Whether walls are lit when visible\n"
|
||||
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
|
||||
"Returns:\n"
|
||||
" List of tuples (x, y, visible, discovered) for all visible cells:\n"
|
||||
" - x, y: Grid coordinates\n"
|
||||
" - visible: True (all returned cells are visible)\n"
|
||||
" - discovered: True (FOV implies discovery)\n\n"
|
||||
"Also updates the internal FOV state for use with is_in_fov()."},
|
||||
"Updates the internal FOV state. Use is_in_fov() to check visibility after calling this.\n"
|
||||
"When perspective is set, this also updates visibility overlays automatically."},
|
||||
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
|
||||
"is_in_fov(x: int, y: int) -> bool\n\n"
|
||||
"Check if a cell is in the field of view.\n\n"
|
||||
|
|
@ -1319,20 +1171,16 @@ PyMethodDef UIGrid_all_methods[] = {
|
|||
UIDRAWABLE_METHODS,
|
||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
||||
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> List[Tuple[int, int, bool, bool]]\n\n"
|
||||
"Compute field of view from a position and return visible cells.\n\n"
|
||||
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
|
||||
"Compute field of view from a position.\n\n"
|
||||
"Args:\n"
|
||||
" x: X coordinate of the viewer\n"
|
||||
" y: Y coordinate of the viewer\n"
|
||||
" radius: Maximum view distance (0 = unlimited)\n"
|
||||
" light_walls: Whether walls are lit when visible\n"
|
||||
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
|
||||
"Returns:\n"
|
||||
" List of tuples (x, y, visible, discovered) for all visible cells:\n"
|
||||
" - x, y: Grid coordinates\n"
|
||||
" - visible: True (all returned cells are visible)\n"
|
||||
" - discovered: True (FOV implies discovery)\n\n"
|
||||
"Also updates the internal FOV state for use with is_in_fov()."},
|
||||
"Updates the internal FOV state. Use is_in_fov() to check visibility after calling this.\n"
|
||||
"When perspective is set, this also updates visibility overlays automatically."},
|
||||
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
|
||||
"is_in_fov(x: int, y: int) -> bool\n\n"
|
||||
"Check if a cell is in the field of view.\n\n"
|
||||
|
|
@ -1418,25 +1266,15 @@ PyGetSetDef UIGrid::getsetters[] = {
|
|||
{"center_y", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "center of the view Y-coordinate", (void*)5},
|
||||
{"zoom", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "zoom factor for displaying the Grid", (void*)6},
|
||||
|
||||
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||
MCRF_PROPERTY(click,
|
||||
"Callable executed when object is clicked. "
|
||||
"Function receives (x, y) coordinates of click."
|
||||
), (void*)PyObjectsEnum::UIGRID},
|
||||
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIGRID},
|
||||
|
||||
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5
|
||||
{"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color, "Background fill color of the grid", NULL},
|
||||
{"perspective", (getter)UIGrid::get_perspective, (setter)UIGrid::set_perspective,
|
||||
"Entity whose perspective to use for FOV rendering (None for omniscient view). "
|
||||
"Setting an entity automatically enables perspective mode.", NULL},
|
||||
{"perspective_enabled", (getter)UIGrid::get_perspective_enabled, (setter)UIGrid::set_perspective_enabled,
|
||||
"Whether to use perspective-based FOV rendering. When True with no valid entity, "
|
||||
"all cells appear undiscovered.", NULL},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
||||
MCRF_PROPERTY(z_index,
|
||||
"Z-order for rendering (lower values rendered first). "
|
||||
"Automatically triggers scene resort when changed."
|
||||
), (void*)PyObjectsEnum::UIGRID},
|
||||
"Entity perspective index for FOV rendering (-1 for omniscient view, 0+ for entity index). "
|
||||
"When set to an entity index, only cells visible to that entity are rendered normally; "
|
||||
"explored but not visible cells are darkened, and unexplored cells are black.", NULL},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID},
|
||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
{NULL} /* Sentinel */
|
||||
|
|
@ -1556,15 +1394,7 @@ PyObject* UIEntityCollection::getitem(PyUIEntityCollectionObject* self, Py_ssize
|
|||
std::advance(l_begin, index);
|
||||
auto target = *l_begin; //auto target = (*vec)[index];
|
||||
|
||||
// Check cache first to preserve derived class
|
||||
if (target->serial_number != 0) {
|
||||
PyObject* cached = PythonObjectCache::getInstance().lookup(target->serial_number);
|
||||
if (cached) {
|
||||
return cached; // Already INCREF'd by lookup
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy: If the entity has a stored Python object reference, return that to preserve derived class
|
||||
// If the entity has a stored Python object reference, return that to preserve derived class
|
||||
if (target->self != nullptr) {
|
||||
Py_INCREF(target->self);
|
||||
return target->self;
|
||||
|
|
@ -1803,46 +1633,33 @@ PyObject* UIEntityCollection::append(PyUIEntityCollectionObject* self, PyObject*
|
|||
|
||||
PyObject* UIEntityCollection::remove(PyUIEntityCollectionObject* self, PyObject* o)
|
||||
{
|
||||
// Type checking - must be an Entity
|
||||
if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity")))
|
||||
if (!PyLong_Check(o))
|
||||
{
|
||||
PyErr_SetString(PyExc_TypeError, "EntityCollection.remove requires an Entity object");
|
||||
PyErr_SetString(PyExc_TypeError, "EntityCollection.remove requires an integer index to remove");
|
||||
return NULL;
|
||||
}
|
||||
long index = PyLong_AsLong(o);
|
||||
|
||||
// Get the C++ object from the Python object
|
||||
PyUIEntityObject* entity = (PyUIEntityObject*)o;
|
||||
if (!entity->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object");
|
||||
// Handle negative indexing
|
||||
while (index < 0) index += self->data->size();
|
||||
|
||||
if (index >= self->data->size())
|
||||
{
|
||||
PyErr_SetString(PyExc_ValueError, "Index out of range");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get iterator to the entity to remove
|
||||
auto it = self->data->begin();
|
||||
std::advance(it, index);
|
||||
|
||||
// Get the underlying list
|
||||
auto list = self->data.get();
|
||||
if (!list) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "The collection store returned a null pointer");
|
||||
return NULL;
|
||||
}
|
||||
// Clear grid reference before removing
|
||||
(*it)->grid = nullptr;
|
||||
|
||||
// Search for the entity by comparing C++ pointers
|
||||
auto it = list->begin();
|
||||
while (it != list->end()) {
|
||||
if (it->get() == entity->data.get()) {
|
||||
// Found it - clear grid reference before removing
|
||||
(*it)->grid = nullptr;
|
||||
|
||||
// Remove from the list
|
||||
self->data->erase(it);
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
|
||||
// Entity not found - raise ValueError
|
||||
PyErr_SetString(PyExc_ValueError, "Entity not in EntityCollection");
|
||||
return NULL;
|
||||
// release the shared pointer at correct part of the list
|
||||
self->data->erase(it);
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
PyObject* UIEntityCollection::extend(PyUIEntityCollectionObject* self, PyObject* o)
|
||||
|
|
|
|||
27
src/UIGrid.h
27
src/UIGrid.h
|
|
@ -6,7 +6,6 @@
|
|||
#include "Resources.h"
|
||||
#include <list>
|
||||
#include <libtcod.h>
|
||||
#include <mutex>
|
||||
|
||||
#include "PyCallable.h"
|
||||
#include "PyTexture.h"
|
||||
|
|
@ -30,7 +29,6 @@ private:
|
|||
TCODMap* tcod_map; // TCOD map for FOV and pathfinding
|
||||
TCODDijkstra* tcod_dijkstra; // Dijkstra pathfinding
|
||||
TCODPath* tcod_path; // A* pathfinding
|
||||
mutable std::mutex fov_mutex; // Mutex for thread-safe FOV operations
|
||||
|
||||
public:
|
||||
UIGrid();
|
||||
|
|
@ -79,9 +77,8 @@ public:
|
|||
// Background rendering
|
||||
sf::Color fill_color;
|
||||
|
||||
// Perspective system - entity whose view to render
|
||||
std::weak_ptr<UIEntity> perspective_entity; // Weak reference to perspective entity
|
||||
bool perspective_enabled; // Whether to use perspective rendering
|
||||
// Perspective system - which entity's view to render (-1 = omniscient/default)
|
||||
int perspective;
|
||||
|
||||
// Property system for animations
|
||||
bool setProperty(const std::string& name, float value) override;
|
||||
|
|
@ -106,8 +103,6 @@ public:
|
|||
static int set_fill_color(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_perspective(PyUIGridObject* self, void* closure);
|
||||
static int set_perspective(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_perspective_enabled(PyUIGridObject* self, void* closure);
|
||||
static int set_perspective_enabled(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args);
|
||||
|
|
@ -177,22 +172,18 @@ namespace mcrfpydef {
|
|||
.tp_name = "mcrfpy.Grid",
|
||||
.tp_basicsize = sizeof(PyUIGridObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)[](PyObject* self)
|
||||
{
|
||||
PyUIGridObject* obj = (PyUIGridObject*)self;
|
||||
// Clear weak references
|
||||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
obj->data.reset();
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
//.tp_dealloc = (destructor)[](PyObject* self)
|
||||
//{
|
||||
// PyUIGridObject* obj = (PyUIGridObject*)self;
|
||||
// obj->data.reset();
|
||||
// Py_TYPE(self)->tp_free(self);
|
||||
//},
|
||||
//TODO - PyUIGrid REPR def:
|
||||
.tp_repr = (reprfunc)UIGrid::repr,
|
||||
//.tp_hash = NULL,
|
||||
//.tp_iter
|
||||
//.tp_iternext
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n"
|
||||
"A grid-based UI element for tile-based rendering and entity management.\n\n"
|
||||
"Args:\n"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#include "UISprite.h"
|
||||
#include "GameEngine.h"
|
||||
#include "PyVector.h"
|
||||
#include "PythonObjectCache.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
|
||||
UIDrawable* UISprite::click_at(sf::Vector2f point)
|
||||
|
|
@ -29,42 +28,6 @@ UISprite::UISprite(std::shared_ptr<PyTexture> _ptex, int _sprite_index, sf::Vect
|
|||
sprite = ptex->sprite(sprite_index, position, sf::Vector2f(_scale, _scale));
|
||||
}
|
||||
|
||||
UISprite::UISprite(const UISprite& other)
|
||||
: UIDrawable(other),
|
||||
sprite_index(other.sprite_index),
|
||||
sprite(other.sprite),
|
||||
ptex(other.ptex)
|
||||
{
|
||||
}
|
||||
|
||||
UISprite& UISprite::operator=(const UISprite& other) {
|
||||
if (this != &other) {
|
||||
UIDrawable::operator=(other);
|
||||
sprite_index = other.sprite_index;
|
||||
sprite = other.sprite;
|
||||
ptex = other.ptex;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
UISprite::UISprite(UISprite&& other) noexcept
|
||||
: UIDrawable(std::move(other)),
|
||||
sprite_index(other.sprite_index),
|
||||
sprite(std::move(other.sprite)),
|
||||
ptex(std::move(other.ptex))
|
||||
{
|
||||
}
|
||||
|
||||
UISprite& UISprite::operator=(UISprite&& other) noexcept {
|
||||
if (this != &other) {
|
||||
UIDrawable::operator=(std::move(other));
|
||||
sprite_index = other.sprite_index;
|
||||
sprite = std::move(other.sprite);
|
||||
ptex = std::move(other.ptex);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/*
|
||||
void UISprite::render(sf::Vector2f offset)
|
||||
{
|
||||
|
|
@ -339,16 +302,8 @@ PyGetSetDef UISprite::getsetters[] = {
|
|||
{"sprite_index", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Which sprite on the texture is shown", NULL},
|
||||
{"sprite_number", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Sprite index (DEPRECATED: use sprite_index instead)", NULL},
|
||||
{"texture", (getter)UISprite::get_texture, (setter)UISprite::set_texture, "Texture object", NULL},
|
||||
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||
MCRF_PROPERTY(click,
|
||||
"Callable executed when object is clicked. "
|
||||
"Function receives (x, y) coordinates of click."
|
||||
), (void*)PyObjectsEnum::UISPRITE},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
||||
MCRF_PROPERTY(z_index,
|
||||
"Z-order for rendering (lower values rendered first). "
|
||||
"Automatically triggers scene resort when changed."
|
||||
), (void*)PyObjectsEnum::UISPRITE},
|
||||
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UISPRITE},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UISPRITE},
|
||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UISPRITE},
|
||||
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UISPRITE},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
|
|
@ -477,19 +432,6 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
|||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
// Initialize weak reference list
|
||||
self->weakreflist = NULL;
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref); // Cache owns the reference now
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,14 +25,6 @@ protected:
|
|||
public:
|
||||
UISprite();
|
||||
UISprite(std::shared_ptr<PyTexture>, int, sf::Vector2f, float);
|
||||
|
||||
// Copy constructor and assignment operator
|
||||
UISprite(const UISprite& other);
|
||||
UISprite& operator=(const UISprite& other);
|
||||
|
||||
// Move constructor and assignment operator
|
||||
UISprite(UISprite&& other) noexcept;
|
||||
UISprite& operator=(UISprite&& other) noexcept;
|
||||
void update();
|
||||
void render(sf::Vector2f, sf::RenderTarget&) override final;
|
||||
virtual UIDrawable* click_at(sf::Vector2f point) override final;
|
||||
|
|
@ -90,10 +82,6 @@ namespace mcrfpydef {
|
|||
.tp_dealloc = (destructor)[](PyObject* self)
|
||||
{
|
||||
PyUISpriteObject* obj = (PyUISpriteObject*)self;
|
||||
// Clear weak references
|
||||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
// release reference to font object
|
||||
//if (obj->texture) Py_DECREF(obj->texture);
|
||||
obj->data.reset();
|
||||
|
|
@ -103,7 +91,7 @@ namespace mcrfpydef {
|
|||
//.tp_hash = NULL,
|
||||
//.tp_iter
|
||||
//.tp_iternext
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.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"
|
||||
|
|
@ -142,10 +130,7 @@ namespace mcrfpydef {
|
|||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
|
||||
{
|
||||
PyUISpriteObject* self = (PyUISpriteObject*)type->tp_alloc(type, 0);
|
||||
if (self) {
|
||||
self->data = std::make_shared<UISprite>();
|
||||
self->weakreflist = nullptr;
|
||||
}
|
||||
//if (self) self->data = std::make_shared<UICaption>();
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -196,14 +196,6 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
|
|||
}
|
||||
else if (!config.exec_scripts.empty()) {
|
||||
// With --exec, run the game engine after scripts execute
|
||||
// In headless mode, auto-exit when no timers remain
|
||||
McRogueFaceConfig mutable_config = config;
|
||||
if (mutable_config.headless) {
|
||||
mutable_config.auto_exit_after_exec = true;
|
||||
}
|
||||
delete engine;
|
||||
engine = new GameEngine(mutable_config);
|
||||
McRFPy_API::game = engine;
|
||||
engine->run();
|
||||
McRFPy_API::api_shutdown();
|
||||
delete engine;
|
||||
|
|
|
|||
532
stubs/mcrfpy.pyi
532
stubs/mcrfpy.pyi
|
|
@ -1,532 +0,0 @@
|
|||
"""Type stubs for McRogueFace Python API.
|
||||
|
||||
Core game engine interface for creating roguelike games with Python.
|
||||
"""
|
||||
|
||||
from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
|
||||
|
||||
# Type aliases
|
||||
UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid']
|
||||
Transition = Union[str, None]
|
||||
|
||||
# Classes
|
||||
|
||||
class Color:
|
||||
"""SFML Color Object for RGBA colors."""
|
||||
|
||||
r: int
|
||||
g: int
|
||||
b: int
|
||||
a: int
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ...
|
||||
|
||||
def from_hex(self, hex_string: str) -> 'Color':
|
||||
"""Create color from hex string (e.g., '#FF0000' or 'FF0000')."""
|
||||
...
|
||||
|
||||
def to_hex(self) -> str:
|
||||
"""Convert color to hex string format."""
|
||||
...
|
||||
|
||||
def lerp(self, other: 'Color', t: float) -> 'Color':
|
||||
"""Linear interpolation between two colors."""
|
||||
...
|
||||
|
||||
class Vector:
|
||||
"""SFML Vector Object for 2D coordinates."""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float, y: float) -> None: ...
|
||||
|
||||
def add(self, other: 'Vector') -> 'Vector': ...
|
||||
def subtract(self, other: 'Vector') -> 'Vector': ...
|
||||
def multiply(self, scalar: float) -> 'Vector': ...
|
||||
def divide(self, scalar: float) -> 'Vector': ...
|
||||
def distance(self, other: 'Vector') -> float: ...
|
||||
def normalize(self) -> 'Vector': ...
|
||||
def dot(self, other: 'Vector') -> float: ...
|
||||
|
||||
class Texture:
|
||||
"""SFML Texture Object for images."""
|
||||
|
||||
def __init__(self, filename: str) -> None: ...
|
||||
|
||||
filename: str
|
||||
width: int
|
||||
height: int
|
||||
sprite_count: int
|
||||
|
||||
class Font:
|
||||
"""SFML Font Object for text rendering."""
|
||||
|
||||
def __init__(self, filename: str) -> None: ...
|
||||
|
||||
filename: str
|
||||
family: str
|
||||
|
||||
class Drawable:
|
||||
"""Base class for all drawable UI elements."""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
visible: bool
|
||||
z_index: int
|
||||
name: str
|
||||
pos: Vector
|
||||
|
||||
def get_bounds(self) -> Tuple[float, float, float, float]:
|
||||
"""Get bounding box as (x, y, width, height)."""
|
||||
...
|
||||
|
||||
def move(self, dx: float, dy: float) -> None:
|
||||
"""Move by relative offset (dx, dy)."""
|
||||
...
|
||||
|
||||
def resize(self, width: float, height: float) -> None:
|
||||
"""Resize to new dimensions (width, height)."""
|
||||
...
|
||||
|
||||
class Frame(Drawable):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0,
|
||||
fill_color: Optional[Color] = None, outline_color: Optional[Color] = None,
|
||||
outline: float = 0, click: Optional[Callable] = None,
|
||||
children: Optional[List[UIElement]] = None) -> None: ...
|
||||
|
||||
w: float
|
||||
h: float
|
||||
fill_color: Color
|
||||
outline_color: Color
|
||||
outline: float
|
||||
click: Optional[Callable[[float, float, int], None]]
|
||||
children: 'UICollection'
|
||||
clip_children: bool
|
||||
|
||||
class Caption(Drawable):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, text: str = '', x: float = 0, y: float = 0,
|
||||
font: Optional[Font] = None, fill_color: Optional[Color] = None,
|
||||
outline_color: Optional[Color] = None, outline: float = 0,
|
||||
click: Optional[Callable] = None) -> None: ...
|
||||
|
||||
text: str
|
||||
font: Font
|
||||
fill_color: Color
|
||||
outline_color: Color
|
||||
outline: float
|
||||
click: Optional[Callable[[float, float, int], None]]
|
||||
w: float # Read-only, computed from text
|
||||
h: float # Read-only, computed from text
|
||||
|
||||
class Sprite(Drawable):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None,
|
||||
sprite_index: int = 0, scale: float = 1.0,
|
||||
click: Optional[Callable] = None) -> None: ...
|
||||
|
||||
texture: Texture
|
||||
sprite_index: int
|
||||
scale: float
|
||||
click: Optional[Callable[[float, float, int], None]]
|
||||
w: float # Read-only, computed from texture
|
||||
h: float # Read-only, computed from texture
|
||||
|
||||
class Grid(Drawable):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20),
|
||||
texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16,
|
||||
scale: float = 1.0, click: Optional[Callable] = None) -> None: ...
|
||||
|
||||
grid_size: Tuple[int, int]
|
||||
tile_width: int
|
||||
tile_height: int
|
||||
texture: Texture
|
||||
scale: float
|
||||
points: List[List['GridPoint']]
|
||||
entities: 'EntityCollection'
|
||||
background_color: Color
|
||||
click: Optional[Callable[[int, int, int], None]]
|
||||
|
||||
def at(self, x: int, y: int) -> 'GridPoint':
|
||||
"""Get grid point at tile coordinates."""
|
||||
...
|
||||
|
||||
class GridPoint:
|
||||
"""Grid point representing a single tile."""
|
||||
|
||||
texture_index: int
|
||||
solid: bool
|
||||
color: Color
|
||||
|
||||
class GridPointState:
|
||||
"""State information for a grid point."""
|
||||
|
||||
texture_index: int
|
||||
color: Color
|
||||
|
||||
class Entity(Drawable):
|
||||
"""Entity(grid_x=0, grid_y=0, texture=None, sprite_index=0, name='')
|
||||
|
||||
Game entity that lives within a Grid.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, grid_x: float = 0, grid_y: float = 0, texture: Optional[Texture] = None,
|
||||
sprite_index: int = 0, name: str = '') -> None: ...
|
||||
|
||||
grid_x: float
|
||||
grid_y: float
|
||||
texture: Texture
|
||||
sprite_index: int
|
||||
grid: Optional[Grid]
|
||||
|
||||
def at(self, grid_x: float, grid_y: float) -> None:
|
||||
"""Move entity to grid position."""
|
||||
...
|
||||
|
||||
def die(self) -> None:
|
||||
"""Remove entity from its grid."""
|
||||
...
|
||||
|
||||
def index(self) -> int:
|
||||
"""Get index in parent grid's entity collection."""
|
||||
...
|
||||
|
||||
class UICollection:
|
||||
"""Collection of UI drawable elements (Frame, Caption, Sprite, Grid)."""
|
||||
|
||||
def __len__(self) -> int: ...
|
||||
def __getitem__(self, index: int) -> UIElement: ...
|
||||
def __setitem__(self, index: int, value: UIElement) -> None: ...
|
||||
def __delitem__(self, index: int) -> None: ...
|
||||
def __contains__(self, item: UIElement) -> bool: ...
|
||||
def __iter__(self) -> Any: ...
|
||||
def __add__(self, other: 'UICollection') -> 'UICollection': ...
|
||||
def __iadd__(self, other: 'UICollection') -> 'UICollection': ...
|
||||
|
||||
def append(self, item: UIElement) -> None: ...
|
||||
def extend(self, items: List[UIElement]) -> None: ...
|
||||
def remove(self, item: UIElement) -> None: ...
|
||||
def index(self, item: UIElement) -> int: ...
|
||||
def count(self, item: UIElement) -> int: ...
|
||||
|
||||
class EntityCollection:
|
||||
"""Collection of Entity objects."""
|
||||
|
||||
def __len__(self) -> int: ...
|
||||
def __getitem__(self, index: int) -> Entity: ...
|
||||
def __setitem__(self, index: int, value: Entity) -> None: ...
|
||||
def __delitem__(self, index: int) -> None: ...
|
||||
def __contains__(self, item: Entity) -> bool: ...
|
||||
def __iter__(self) -> Any: ...
|
||||
def __add__(self, other: 'EntityCollection') -> 'EntityCollection': ...
|
||||
def __iadd__(self, other: 'EntityCollection') -> 'EntityCollection': ...
|
||||
|
||||
def append(self, item: Entity) -> None: ...
|
||||
def extend(self, items: List[Entity]) -> None: ...
|
||||
def remove(self, item: Entity) -> None: ...
|
||||
def index(self, item: Entity) -> int: ...
|
||||
def count(self, item: Entity) -> int: ...
|
||||
|
||||
class Scene:
|
||||
"""Base class for object-oriented scenes."""
|
||||
|
||||
name: str
|
||||
|
||||
def __init__(self, name: str) -> None: ...
|
||||
|
||||
def activate(self) -> None:
|
||||
"""Called when scene becomes active."""
|
||||
...
|
||||
|
||||
def deactivate(self) -> None:
|
||||
"""Called when scene becomes inactive."""
|
||||
...
|
||||
|
||||
def get_ui(self) -> UICollection:
|
||||
"""Get UI elements collection."""
|
||||
...
|
||||
|
||||
def on_keypress(self, key: str, pressed: bool) -> None:
|
||||
"""Handle keyboard events."""
|
||||
...
|
||||
|
||||
def on_click(self, x: float, y: float, button: int) -> None:
|
||||
"""Handle mouse clicks."""
|
||||
...
|
||||
|
||||
def on_enter(self) -> None:
|
||||
"""Called when entering the scene."""
|
||||
...
|
||||
|
||||
def on_exit(self) -> None:
|
||||
"""Called when leaving the scene."""
|
||||
...
|
||||
|
||||
def on_resize(self, width: int, height: int) -> None:
|
||||
"""Handle window resize events."""
|
||||
...
|
||||
|
||||
def update(self, dt: float) -> None:
|
||||
"""Update scene logic."""
|
||||
...
|
||||
|
||||
class Timer:
|
||||
"""Timer object for scheduled callbacks."""
|
||||
|
||||
name: str
|
||||
interval: int
|
||||
active: bool
|
||||
|
||||
def __init__(self, name: str, callback: Callable[[float], None], interval: int) -> None: ...
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pause the timer."""
|
||||
...
|
||||
|
||||
def resume(self) -> None:
|
||||
"""Resume the timer."""
|
||||
...
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""Cancel and remove the timer."""
|
||||
...
|
||||
|
||||
class Window:
|
||||
"""Window singleton for managing the game window."""
|
||||
|
||||
resolution: Tuple[int, int]
|
||||
fullscreen: bool
|
||||
vsync: bool
|
||||
title: str
|
||||
fps_limit: int
|
||||
game_resolution: Tuple[int, int]
|
||||
scaling_mode: str
|
||||
|
||||
@staticmethod
|
||||
def get() -> 'Window':
|
||||
"""Get the window singleton instance."""
|
||||
...
|
||||
|
||||
class Animation:
|
||||
"""Animation object for animating UI properties."""
|
||||
|
||||
target: Any
|
||||
property: str
|
||||
duration: float
|
||||
easing: str
|
||||
loop: bool
|
||||
on_complete: Optional[Callable]
|
||||
|
||||
def __init__(self, target: Any, property: str, start_value: Any, end_value: Any,
|
||||
duration: float, easing: str = 'linear', loop: bool = False,
|
||||
on_complete: Optional[Callable] = None) -> None: ...
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the animation."""
|
||||
...
|
||||
|
||||
def update(self, dt: float) -> bool:
|
||||
"""Update animation, returns True if still running."""
|
||||
...
|
||||
|
||||
def get_current_value(self) -> Any:
|
||||
"""Get the current interpolated value."""
|
||||
...
|
||||
|
||||
# Module functions
|
||||
|
||||
def createSoundBuffer(filename: str) -> int:
|
||||
"""Load a sound effect from a file and return its buffer ID."""
|
||||
...
|
||||
|
||||
def loadMusic(filename: str) -> None:
|
||||
"""Load and immediately play background music from a file."""
|
||||
...
|
||||
|
||||
def setMusicVolume(volume: int) -> None:
|
||||
"""Set the global music volume (0-100)."""
|
||||
...
|
||||
|
||||
def setSoundVolume(volume: int) -> None:
|
||||
"""Set the global sound effects volume (0-100)."""
|
||||
...
|
||||
|
||||
def playSound(buffer_id: int) -> None:
|
||||
"""Play a sound effect using a previously loaded buffer."""
|
||||
...
|
||||
|
||||
def getMusicVolume() -> int:
|
||||
"""Get the current music volume level (0-100)."""
|
||||
...
|
||||
|
||||
def getSoundVolume() -> int:
|
||||
"""Get the current sound effects volume level (0-100)."""
|
||||
...
|
||||
|
||||
def sceneUI(scene: Optional[str] = None) -> UICollection:
|
||||
"""Get all UI elements for a scene."""
|
||||
...
|
||||
|
||||
def currentScene() -> str:
|
||||
"""Get the name of the currently active scene."""
|
||||
...
|
||||
|
||||
def setScene(scene: str, transition: Optional[str] = None, duration: float = 0.0) -> None:
|
||||
"""Switch to a different scene with optional transition effect."""
|
||||
...
|
||||
|
||||
def createScene(name: str) -> None:
|
||||
"""Create a new empty scene."""
|
||||
...
|
||||
|
||||
def keypressScene(handler: Callable[[str, bool], None]) -> None:
|
||||
"""Set the keyboard event handler for the current scene."""
|
||||
...
|
||||
|
||||
def setTimer(name: str, handler: Callable[[float], None], interval: int) -> None:
|
||||
"""Create or update a recurring timer."""
|
||||
...
|
||||
|
||||
def delTimer(name: str) -> None:
|
||||
"""Stop and remove a timer."""
|
||||
...
|
||||
|
||||
def exit() -> None:
|
||||
"""Cleanly shut down the game engine and exit the application."""
|
||||
...
|
||||
|
||||
def setScale(multiplier: float) -> None:
|
||||
"""Scale the game window size (deprecated - use Window.resolution)."""
|
||||
...
|
||||
|
||||
def find(name: str, scene: Optional[str] = None) -> Optional[UIElement]:
|
||||
"""Find the first UI element with the specified name."""
|
||||
...
|
||||
|
||||
def findAll(pattern: str, scene: Optional[str] = None) -> List[UIElement]:
|
||||
"""Find all UI elements matching a name pattern (supports * wildcards)."""
|
||||
...
|
||||
|
||||
def getMetrics() -> Dict[str, Union[int, float]]:
|
||||
"""Get current performance metrics."""
|
||||
...
|
||||
|
||||
# Submodule
|
||||
class automation:
|
||||
"""Automation API for testing and scripting."""
|
||||
|
||||
@staticmethod
|
||||
def screenshot(filename: str) -> bool:
|
||||
"""Save a screenshot to the specified file."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def position() -> Tuple[int, int]:
|
||||
"""Get current mouse position as (x, y) tuple."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def size() -> Tuple[int, int]:
|
||||
"""Get screen size as (width, height) tuple."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def onScreen(x: int, y: int) -> bool:
|
||||
"""Check if coordinates are within screen bounds."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def moveTo(x: int, y: int, duration: float = 0.0) -> None:
|
||||
"""Move mouse to absolute position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def moveRel(xOffset: int, yOffset: int, duration: float = 0.0) -> None:
|
||||
"""Move mouse relative to current position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def dragTo(x: int, y: int, duration: float = 0.0, button: str = 'left') -> None:
|
||||
"""Drag mouse to position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def dragRel(xOffset: int, yOffset: int, duration: float = 0.0, button: str = 'left') -> None:
|
||||
"""Drag mouse relative to current position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def click(x: Optional[int] = None, y: Optional[int] = None, clicks: int = 1,
|
||||
interval: float = 0.0, button: str = 'left') -> None:
|
||||
"""Click mouse at position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def mouseDown(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
|
||||
"""Press mouse button down."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def mouseUp(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
|
||||
"""Release mouse button."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def keyDown(key: str) -> None:
|
||||
"""Press key down."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def keyUp(key: str) -> None:
|
||||
"""Release key."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def press(key: str) -> None:
|
||||
"""Press and release a key."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def typewrite(text: str, interval: float = 0.0) -> None:
|
||||
"""Type text with optional interval between characters."""
|
||||
...
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
"""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
|
||||
#
|
||||
# Core game engine interface for creating roguelike games with Python.
|
||||
|
||||
# Classes
|
||||
|
||||
class Animation:
|
||||
"""Animation object for animating UI properties"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def complete(self) -> None: ...
|
||||
def get_current_value(self) -> Any: ...
|
||||
def hasValidTarget(self) -> bool: ...
|
||||
def start(selftarget: UIDrawable) -> None: ...
|
||||
def update(selfdelta_time: float) -> bool: ...
|
||||
|
||||
class Caption:
|
||||
"""Caption(pos=None, font=None, text='', **kwargs)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(self) -> tuple: ...
|
||||
def move(selfdx: float, dy: float) -> None: ...
|
||||
def resize(selfwidth: float, height: float) -> None: ...
|
||||
|
||||
class Color:
|
||||
"""SFML Color Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def from_hex(selfhex_string: str) -> Color: ...
|
||||
def lerp(selfother: Color, t: float) -> Color: ...
|
||||
def to_hex(self) -> str: ...
|
||||
|
||||
class Drawable:
|
||||
"""Base class for all drawable UI elements"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(self) -> tuple: ...
|
||||
def move(selfdx: float, dy: float) -> None: ...
|
||||
def resize(selfwidth: float, height: float) -> None: ...
|
||||
|
||||
class Entity:
|
||||
"""Entity(grid_pos=None, texture=None, sprite_index=0, **kwargs)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def at(self, *args, **kwargs) -> Any: ...
|
||||
def die(self, *args, **kwargs) -> Any: ...
|
||||
def get_bounds(self) -> tuple: ...
|
||||
def index(self, *args, **kwargs) -> Any: ...
|
||||
def move(selfdx: float, dy: float) -> None: ...
|
||||
def path_to(selfx: int, y: int) -> bool: ...
|
||||
def resize(selfwidth: float, height: float) -> None: ...
|
||||
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(pos=None, size=None, **kwargs)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(self) -> tuple: ...
|
||||
def move(selfdx: float, dy: float) -> None: ...
|
||||
def resize(selfwidth: float, height: float) -> None: ...
|
||||
|
||||
class Grid:
|
||||
"""Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)"""
|
||||
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) -> List[Tuple[int, int, bool, bool]]: ...
|
||||
def find_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
|
||||
def get_bounds(self) -> tuple: ...
|
||||
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: float, dy: float) -> None: ...
|
||||
def resize(selfwidth: float, height: float) -> None: ...
|
||||
|
||||
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) -> None: ...
|
||||
def get_ui(self) -> UICollection: ...
|
||||
def register_keyboard(selfcallback: callable) -> None: ...
|
||||
|
||||
class Sprite:
|
||||
"""Sprite(pos=None, texture=None, sprite_index=0, **kwargs)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(self) -> tuple: ...
|
||||
def move(selfdx: float, dy: float) -> None: ...
|
||||
def resize(selfwidth: float, height: float) -> None: ...
|
||||
|
||||
class Texture:
|
||||
"""SFML Texture Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Timer:
|
||||
"""Timer(name, callback, interval, once=False)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def cancel(self) -> None: ...
|
||||
def pause(self) -> None: ...
|
||||
def restart(self) -> None: ...
|
||||
def resume(self) -> None: ...
|
||||
|
||||
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) -> float: ...
|
||||
def copy(self) -> Vector: ...
|
||||
def distance_to(selfother: Vector) -> float: ...
|
||||
def dot(selfother: Vector) -> float: ...
|
||||
def magnitude(self) -> float: ...
|
||||
def magnitude_squared(self) -> float: ...
|
||||
def normalize(self) -> Vector: ...
|
||||
|
||||
class Window:
|
||||
"""Window singleton for accessing and modifying the game window properties"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def center(self) -> None: ...
|
||||
def get(self) -> Window: ...
|
||||
def screenshot(selffilename: str = None) -> bytes | None: ...
|
||||
|
||||
# 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
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
"""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: ...
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
"""
|
||||
Benchmark: Moving Entities Performance Test
|
||||
|
||||
This benchmark measures McRogueFace's performance with 50 randomly moving
|
||||
entities on a 100x100 grid.
|
||||
|
||||
Expected results:
|
||||
- Should maintain 60 FPS
|
||||
- Entity render time should be <3ms
|
||||
- Grid render time will be higher due to constant updates (no dirty flag benefit)
|
||||
|
||||
Usage:
|
||||
./build/mcrogueface --exec tests/benchmark_moving_entities.py
|
||||
|
||||
Press F3 to toggle performance overlay
|
||||
Press ESC to exit
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
import random
|
||||
|
||||
# Create the benchmark scene
|
||||
mcrfpy.createScene("benchmark")
|
||||
mcrfpy.setScene("benchmark")
|
||||
|
||||
# Get scene UI
|
||||
ui = mcrfpy.sceneUI("benchmark")
|
||||
|
||||
# Create a 100x100 grid
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(100, 100),
|
||||
pos=(0, 0),
|
||||
size=(1024, 768)
|
||||
)
|
||||
|
||||
# Simple floor pattern
|
||||
for x in range(100):
|
||||
for y in range(100):
|
||||
cell = grid.at((x, y))
|
||||
cell.tilesprite = 0
|
||||
cell.color = (40, 40, 40, 255)
|
||||
|
||||
# Create 50 entities with random positions and velocities
|
||||
entities = []
|
||||
ENTITY_COUNT = 50
|
||||
|
||||
for i in range(ENTITY_COUNT):
|
||||
entity = mcrfpy.Entity(
|
||||
grid_pos=(random.randint(0, 99), random.randint(0, 99)),
|
||||
sprite_index=random.randint(10, 20) # Use varied sprites
|
||||
)
|
||||
|
||||
# Give each entity a random velocity
|
||||
entity.velocity_x = random.uniform(-0.5, 0.5)
|
||||
entity.velocity_y = random.uniform(-0.5, 0.5)
|
||||
|
||||
grid.entities.append(entity)
|
||||
entities.append(entity)
|
||||
|
||||
ui.append(grid)
|
||||
|
||||
# Instructions caption
|
||||
instructions = mcrfpy.Caption(
|
||||
text=f"Moving Entities Benchmark ({ENTITY_COUNT} entities)\n"
|
||||
"Press F3 for performance overlay\n"
|
||||
"Press ESC to exit\n"
|
||||
"Goal: 60 FPS with entities moving",
|
||||
pos=(10, 10),
|
||||
fill_color=(255, 255, 0, 255)
|
||||
)
|
||||
ui.append(instructions)
|
||||
|
||||
# Benchmark info
|
||||
print("=" * 60)
|
||||
print("MOVING ENTITIES BENCHMARK")
|
||||
print("=" * 60)
|
||||
print(f"Entity count: {ENTITY_COUNT}")
|
||||
print("Grid size: 100x100 cells")
|
||||
print("Expected FPS: 60")
|
||||
print("")
|
||||
print("Entities move randomly and bounce off walls.")
|
||||
print("This tests entity rendering performance and position updates.")
|
||||
print("")
|
||||
print("Press F3 in-game to see real-time performance metrics.")
|
||||
print("=" * 60)
|
||||
|
||||
# Exit handler
|
||||
def handle_key(key, state):
|
||||
if key == "Escape" and state:
|
||||
print("\nBenchmark ended by user")
|
||||
sys.exit(0)
|
||||
|
||||
mcrfpy.keypressScene(handle_key)
|
||||
|
||||
# Update entity positions
|
||||
def update_entities(ms):
|
||||
dt = ms / 1000.0 # Convert to seconds
|
||||
|
||||
for entity in entities:
|
||||
# Update position
|
||||
new_x = entity.x + entity.velocity_x
|
||||
new_y = entity.y + entity.velocity_y
|
||||
|
||||
# Bounce off walls
|
||||
if new_x < 0 or new_x >= 100:
|
||||
entity.velocity_x = -entity.velocity_x
|
||||
new_x = max(0, min(99, new_x))
|
||||
|
||||
if new_y < 0 or new_y >= 100:
|
||||
entity.velocity_y = -entity.velocity_y
|
||||
new_y = max(0, min(99, new_y))
|
||||
|
||||
# Update entity position
|
||||
entity.x = new_x
|
||||
entity.y = new_y
|
||||
|
||||
# Run movement update every frame (16ms)
|
||||
mcrfpy.setTimer("movement", update_entities, 16)
|
||||
|
||||
# Benchmark statistics
|
||||
frame_count = 0
|
||||
start_time = None
|
||||
|
||||
def benchmark_timer(ms):
|
||||
global frame_count, start_time
|
||||
|
||||
if start_time is None:
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
frame_count += 1
|
||||
|
||||
# After 10 seconds, print summary
|
||||
import time
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
if elapsed >= 10.0:
|
||||
print("\n" + "=" * 60)
|
||||
print("BENCHMARK COMPLETE")
|
||||
print("=" * 60)
|
||||
print(f"Frames rendered: {frame_count}")
|
||||
print(f"Time elapsed: {elapsed:.2f}s")
|
||||
print(f"Average FPS: {frame_count / elapsed:.1f}")
|
||||
print(f"Entities: {ENTITY_COUNT}")
|
||||
print("")
|
||||
print("Check profiler overlay (F3) for detailed timing breakdown.")
|
||||
print("Entity render time and total frame time are key metrics.")
|
||||
print("=" * 60)
|
||||
# Don't exit - let user review
|
||||
|
||||
mcrfpy.setTimer("benchmark", benchmark_timer, 100)
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
"""
|
||||
Benchmark: Static Grid Performance Test
|
||||
|
||||
This benchmark measures McRogueFace's grid rendering performance with a static
|
||||
100x100 grid. The goal is 60 FPS with minimal CPU usage.
|
||||
|
||||
Expected results:
|
||||
- 60 FPS (16.6ms per frame)
|
||||
- Grid render time should be <2ms after dirty flag optimization
|
||||
- Currently will be higher (likely 8-12ms) - this establishes baseline
|
||||
|
||||
Usage:
|
||||
./build/mcrogueface --exec tests/benchmark_static_grid.py
|
||||
|
||||
Press F3 to toggle performance overlay
|
||||
Press ESC to exit
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Create the benchmark scene
|
||||
mcrfpy.createScene("benchmark")
|
||||
mcrfpy.setScene("benchmark")
|
||||
|
||||
# Get scene UI
|
||||
ui = mcrfpy.sceneUI("benchmark")
|
||||
|
||||
# Create a 100x100 grid with default texture
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(100, 100),
|
||||
pos=(0, 0),
|
||||
size=(1024, 768)
|
||||
)
|
||||
|
||||
# Fill grid with varied tile patterns to ensure realistic rendering
|
||||
for x in range(100):
|
||||
for y in range(100):
|
||||
cell = grid.at((x, y))
|
||||
# Checkerboard pattern with different sprites
|
||||
if (x + y) % 2 == 0:
|
||||
cell.tilesprite = 0
|
||||
cell.color = (50, 50, 50, 255)
|
||||
else:
|
||||
cell.tilesprite = 1
|
||||
cell.color = (70, 70, 70, 255)
|
||||
|
||||
# Add some variation
|
||||
if x % 10 == 0 or y % 10 == 0:
|
||||
cell.tilesprite = 2
|
||||
cell.color = (100, 100, 100, 255)
|
||||
|
||||
# Add grid to scene
|
||||
ui.append(grid)
|
||||
|
||||
# Instructions caption
|
||||
instructions = mcrfpy.Caption(
|
||||
text="Static Grid Benchmark (100x100)\n"
|
||||
"Press F3 for performance overlay\n"
|
||||
"Press ESC to exit\n"
|
||||
"Goal: 60 FPS with low grid render time",
|
||||
pos=(10, 10),
|
||||
fill_color=(255, 255, 0, 255)
|
||||
)
|
||||
ui.append(instructions)
|
||||
|
||||
# Benchmark info
|
||||
print("=" * 60)
|
||||
print("STATIC GRID BENCHMARK")
|
||||
print("=" * 60)
|
||||
print("Grid size: 100x100 cells")
|
||||
print("Expected FPS: 60")
|
||||
print("Tiles rendered: ~1024 visible cells per frame")
|
||||
print("")
|
||||
print("This benchmark establishes baseline grid rendering performance.")
|
||||
print("After dirty flag optimization, grid render time should drop")
|
||||
print("significantly for static content.")
|
||||
print("")
|
||||
print("Press F3 in-game to see real-time performance metrics.")
|
||||
print("=" * 60)
|
||||
|
||||
# Exit handler
|
||||
def handle_key(key, state):
|
||||
if key == "Escape" and state:
|
||||
print("\nBenchmark ended by user")
|
||||
sys.exit(0)
|
||||
|
||||
mcrfpy.keypressScene(handle_key)
|
||||
|
||||
# Run for 10 seconds then provide summary
|
||||
frame_count = 0
|
||||
start_time = None
|
||||
|
||||
def benchmark_timer(ms):
|
||||
global frame_count, start_time
|
||||
|
||||
if start_time is None:
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
frame_count += 1
|
||||
|
||||
# After 10 seconds, print summary and exit
|
||||
import time
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
if elapsed >= 10.0:
|
||||
print("\n" + "=" * 60)
|
||||
print("BENCHMARK COMPLETE")
|
||||
print("=" * 60)
|
||||
print(f"Frames rendered: {frame_count}")
|
||||
print(f"Time elapsed: {elapsed:.2f}s")
|
||||
print(f"Average FPS: {frame_count / elapsed:.1f}")
|
||||
print("")
|
||||
print("Check profiler overlay (F3) for detailed timing breakdown.")
|
||||
print("Grid render time is the key metric for optimization.")
|
||||
print("=" * 60)
|
||||
# Don't exit automatically - let user review with F3
|
||||
# sys.exit(0)
|
||||
|
||||
# Update every 100ms
|
||||
mcrfpy.setTimer("benchmark", benchmark_timer, 100)
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Audit current constructor argument handling for all UI classes"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def audit_constructors():
|
||||
"""Test current state of all UI constructors"""
|
||||
|
||||
print("=== CONSTRUCTOR AUDIT ===\n")
|
||||
|
||||
# Create test scene and texture
|
||||
mcrfpy.createScene("audit")
|
||||
texture = mcrfpy.Texture("assets/test_portraits.png", 32, 32)
|
||||
|
||||
# Test Frame
|
||||
print("1. Frame Constructor Tests:")
|
||||
print("-" * 40)
|
||||
|
||||
# No args
|
||||
try:
|
||||
f = mcrfpy.Frame()
|
||||
print("✓ Frame() - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Frame() - {e}")
|
||||
|
||||
# Traditional 4 args (x, y, w, h)
|
||||
try:
|
||||
f = mcrfpy.Frame(10, 20, 100, 50)
|
||||
print("✓ Frame(10, 20, 100, 50) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Frame(10, 20, 100, 50) - {e}")
|
||||
|
||||
# Tuple pos + size
|
||||
try:
|
||||
f = mcrfpy.Frame((10, 20), (100, 50))
|
||||
print("✓ Frame((10, 20), (100, 50)) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Frame((10, 20), (100, 50)) - {e}")
|
||||
|
||||
# Keywords
|
||||
try:
|
||||
f = mcrfpy.Frame(pos=(10, 20), size=(100, 50))
|
||||
print("✓ Frame(pos=(10, 20), size=(100, 50)) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Frame(pos=(10, 20), size=(100, 50)) - {e}")
|
||||
|
||||
# Test Grid
|
||||
print("\n2. Grid Constructor Tests:")
|
||||
print("-" * 40)
|
||||
|
||||
# No args
|
||||
try:
|
||||
g = mcrfpy.Grid()
|
||||
print("✓ Grid() - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Grid() - {e}")
|
||||
|
||||
# Grid size only
|
||||
try:
|
||||
g = mcrfpy.Grid((10, 10))
|
||||
print("✓ Grid((10, 10)) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Grid((10, 10)) - {e}")
|
||||
|
||||
# Grid size + texture
|
||||
try:
|
||||
g = mcrfpy.Grid((10, 10), texture)
|
||||
print("✓ Grid((10, 10), texture) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Grid((10, 10), texture) - {e}")
|
||||
|
||||
# Full positional (expected: pos, size, grid_size, texture)
|
||||
try:
|
||||
g = mcrfpy.Grid((0, 0), (320, 320), (10, 10), texture)
|
||||
print("✓ Grid((0, 0), (320, 320), (10, 10), texture) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Grid((0, 0), (320, 320), (10, 10), texture) - {e}")
|
||||
|
||||
# Keywords
|
||||
try:
|
||||
g = mcrfpy.Grid(pos=(0, 0), size=(320, 320), grid_size=(10, 10), texture=texture)
|
||||
print("✓ Grid(pos=..., size=..., grid_size=..., texture=...) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Grid(pos=..., size=..., grid_size=..., texture=...) - {e}")
|
||||
|
||||
# Test Sprite
|
||||
print("\n3. Sprite Constructor Tests:")
|
||||
print("-" * 40)
|
||||
|
||||
# No args
|
||||
try:
|
||||
s = mcrfpy.Sprite()
|
||||
print("✓ Sprite() - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Sprite() - {e}")
|
||||
|
||||
# Position only
|
||||
try:
|
||||
s = mcrfpy.Sprite((10, 20))
|
||||
print("✓ Sprite((10, 20)) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Sprite((10, 20)) - {e}")
|
||||
|
||||
# Position + texture
|
||||
try:
|
||||
s = mcrfpy.Sprite((10, 20), texture)
|
||||
print("✓ Sprite((10, 20), texture) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Sprite((10, 20), texture) - {e}")
|
||||
|
||||
# Position + texture + sprite_index
|
||||
try:
|
||||
s = mcrfpy.Sprite((10, 20), texture, 5)
|
||||
print("✓ Sprite((10, 20), texture, 5) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Sprite((10, 20), texture, 5) - {e}")
|
||||
|
||||
# Keywords
|
||||
try:
|
||||
s = mcrfpy.Sprite(pos=(10, 20), texture=texture, sprite_index=5)
|
||||
print("✓ Sprite(pos=..., texture=..., sprite_index=...) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Sprite(pos=..., texture=..., sprite_index=...) - {e}")
|
||||
|
||||
# Test Caption
|
||||
print("\n4. Caption Constructor Tests:")
|
||||
print("-" * 40)
|
||||
|
||||
# No args
|
||||
try:
|
||||
c = mcrfpy.Caption()
|
||||
print("✓ Caption() - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Caption() - {e}")
|
||||
|
||||
# Text only
|
||||
try:
|
||||
c = mcrfpy.Caption("Hello")
|
||||
print("✓ Caption('Hello') - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Caption('Hello') - {e}")
|
||||
|
||||
# Position + text (expected order: pos, font, text)
|
||||
try:
|
||||
c = mcrfpy.Caption((10, 20), "Hello")
|
||||
print("✓ Caption((10, 20), 'Hello') - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Caption((10, 20), 'Hello') - {e}")
|
||||
|
||||
# Position + font + text
|
||||
try:
|
||||
c = mcrfpy.Caption((10, 20), 16, "Hello")
|
||||
print("✓ Caption((10, 20), 16, 'Hello') - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Caption((10, 20), 16, 'Hello') - {e}")
|
||||
|
||||
# Keywords
|
||||
try:
|
||||
c = mcrfpy.Caption(pos=(10, 20), font=16, text="Hello")
|
||||
print("✓ Caption(pos=..., font=..., text=...) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Caption(pos=..., font=..., text=...) - {e}")
|
||||
|
||||
# Test Entity
|
||||
print("\n5. Entity Constructor Tests:")
|
||||
print("-" * 40)
|
||||
|
||||
# No args
|
||||
try:
|
||||
e = mcrfpy.Entity()
|
||||
print("✓ Entity() - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Entity() - {e}")
|
||||
|
||||
# Grid position only
|
||||
try:
|
||||
e = mcrfpy.Entity((5.0, 6.0))
|
||||
print("✓ Entity((5.0, 6.0)) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Entity((5.0, 6.0)) - {e}")
|
||||
|
||||
# Grid position + texture
|
||||
try:
|
||||
e = mcrfpy.Entity((5.0, 6.0), texture)
|
||||
print("✓ Entity((5.0, 6.0), texture) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Entity((5.0, 6.0), texture) - {e}")
|
||||
|
||||
# Grid position + texture + sprite_index
|
||||
try:
|
||||
e = mcrfpy.Entity((5.0, 6.0), texture, 3)
|
||||
print("✓ Entity((5.0, 6.0), texture, 3) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Entity((5.0, 6.0), texture, 3) - {e}")
|
||||
|
||||
# Keywords
|
||||
try:
|
||||
e = mcrfpy.Entity(grid_pos=(5.0, 6.0), texture=texture, sprite_index=3)
|
||||
print("✓ Entity(grid_pos=..., texture=..., sprite_index=...) - works")
|
||||
except Exception as e:
|
||||
print(f"✗ Entity(grid_pos=..., texture=..., sprite_index=...) - {e}")
|
||||
|
||||
print("\n=== AUDIT COMPLETE ===")
|
||||
|
||||
# Run audit
|
||||
try:
|
||||
audit_constructors()
|
||||
print("\nPASS")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\nFAIL: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Count format string characters
|
||||
|
||||
fmt = "|OOOOfOOifizfffi"
|
||||
print(f"Format string: {fmt}")
|
||||
|
||||
# Remove the | prefix
|
||||
fmt_chars = fmt[1:]
|
||||
print(f"Format chars after |: {fmt_chars}")
|
||||
print(f"Length: {len(fmt_chars)}")
|
||||
|
||||
# Count each type
|
||||
o_count = fmt_chars.count('O')
|
||||
f_count = fmt_chars.count('f')
|
||||
i_count = fmt_chars.count('i')
|
||||
z_count = fmt_chars.count('z')
|
||||
s_count = fmt_chars.count('s')
|
||||
|
||||
print(f"\nCounts:")
|
||||
print(f"O (objects): {o_count}")
|
||||
print(f"f (floats): {f_count}")
|
||||
print(f"i (ints): {i_count}")
|
||||
print(f"z (strings): {z_count}")
|
||||
print(f"s (strings): {s_count}")
|
||||
print(f"Total: {o_count + f_count + i_count + z_count + s_count}")
|
||||
|
||||
# List out each position
|
||||
print("\nPosition by position:")
|
||||
for i, c in enumerate(fmt_chars):
|
||||
print(f"{i+1}: {c}")
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e # Exit on any error
|
||||
|
||||
echo "=== McRogueFace Documentation Generation ==="
|
||||
|
||||
# Verify build exists
|
||||
if [ ! -f "./build/mcrogueface" ]; then
|
||||
echo "ERROR: build/mcrogueface not found. Run 'make' first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate API docs (HTML + Markdown)
|
||||
echo "Generating API documentation..."
|
||||
./build/mcrogueface --headless --exec tools/generate_dynamic_docs.py
|
||||
|
||||
# Generate type stubs (using v2 - manually maintained high-quality stubs)
|
||||
echo "Generating type stubs..."
|
||||
./build/mcrogueface --headless --exec tools/generate_stubs_v2.py
|
||||
|
||||
# Generate man page
|
||||
echo "Generating man page..."
|
||||
./tools/generate_man_page.sh
|
||||
|
||||
echo "=== Documentation generation complete ==="
|
||||
echo " HTML: docs/api_reference_dynamic.html"
|
||||
echo " Markdown: docs/API_REFERENCE_DYNAMIC.md"
|
||||
echo " Man page: docs/mcrfpy.3"
|
||||
echo " Stubs: stubs/mcrfpy.pyi"
|
||||
|
|
@ -0,0 +1,482 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate API reference documentation for McRogueFace.
|
||||
|
||||
This script generates comprehensive API documentation in multiple formats:
|
||||
- Markdown for GitHub/documentation sites
|
||||
- HTML for local browsing
|
||||
- RST for Sphinx integration (future)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import inspect
|
||||
import datetime
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pathlib import Path
|
||||
|
||||
# We need to run this with McRogueFace as the interpreter
|
||||
# so mcrfpy is available
|
||||
import mcrfpy
|
||||
|
||||
def escape_markdown(text: str) -> str:
|
||||
"""Escape special markdown characters."""
|
||||
if not text:
|
||||
return ""
|
||||
# Escape backticks in inline code
|
||||
return text.replace("`", "\\`")
|
||||
|
||||
def format_signature(name: str, doc: str) -> str:
|
||||
"""Extract and format function signature from docstring."""
|
||||
if not doc:
|
||||
return f"{name}(...)"
|
||||
|
||||
lines = doc.strip().split('\n')
|
||||
if lines and '(' in lines[0]:
|
||||
# First line contains signature
|
||||
return lines[0].split('->')[0].strip()
|
||||
|
||||
return f"{name}(...)"
|
||||
|
||||
def get_class_info(cls: type) -> Dict[str, Any]:
|
||||
"""Extract comprehensive information about a class."""
|
||||
info = {
|
||||
'name': cls.__name__,
|
||||
'doc': cls.__doc__ or "",
|
||||
'methods': [],
|
||||
'properties': [],
|
||||
'bases': [base.__name__ for base in cls.__bases__ if base.__name__ != 'object'],
|
||||
}
|
||||
|
||||
# Get all attributes
|
||||
for attr_name in sorted(dir(cls)):
|
||||
if attr_name.startswith('_') and not attr_name.startswith('__'):
|
||||
continue
|
||||
|
||||
try:
|
||||
attr = getattr(cls, attr_name)
|
||||
|
||||
if isinstance(attr, property):
|
||||
prop_info = {
|
||||
'name': attr_name,
|
||||
'doc': (attr.fget.__doc__ if attr.fget else "") or "",
|
||||
'readonly': attr.fset is None
|
||||
}
|
||||
info['properties'].append(prop_info)
|
||||
elif callable(attr) and not attr_name.startswith('__'):
|
||||
method_info = {
|
||||
'name': attr_name,
|
||||
'doc': attr.__doc__ or "",
|
||||
'signature': format_signature(attr_name, attr.__doc__)
|
||||
}
|
||||
info['methods'].append(method_info)
|
||||
except:
|
||||
pass
|
||||
|
||||
return info
|
||||
|
||||
def get_function_info(func: Any, name: str) -> Dict[str, Any]:
|
||||
"""Extract information about a function."""
|
||||
return {
|
||||
'name': name,
|
||||
'doc': func.__doc__ or "",
|
||||
'signature': format_signature(name, func.__doc__)
|
||||
}
|
||||
|
||||
def generate_markdown_class(cls_info: Dict[str, Any]) -> List[str]:
|
||||
"""Generate markdown documentation for a class."""
|
||||
lines = []
|
||||
|
||||
# Class header
|
||||
lines.append(f"### class `{cls_info['name']}`")
|
||||
if cls_info['bases']:
|
||||
lines.append(f"*Inherits from: {', '.join(cls_info['bases'])}*")
|
||||
lines.append("")
|
||||
|
||||
# Class description
|
||||
if cls_info['doc']:
|
||||
doc_lines = cls_info['doc'].strip().split('\n')
|
||||
# First line is usually the constructor signature
|
||||
if doc_lines and '(' in doc_lines[0]:
|
||||
lines.append(f"```python")
|
||||
lines.append(doc_lines[0])
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
# Rest is description
|
||||
if len(doc_lines) > 2:
|
||||
lines.extend(doc_lines[2:])
|
||||
lines.append("")
|
||||
else:
|
||||
lines.extend(doc_lines)
|
||||
lines.append("")
|
||||
|
||||
# Properties
|
||||
if cls_info['properties']:
|
||||
lines.append("#### Properties")
|
||||
lines.append("")
|
||||
for prop in cls_info['properties']:
|
||||
readonly = " *(readonly)*" if prop['readonly'] else ""
|
||||
lines.append(f"- **`{prop['name']}`**{readonly}")
|
||||
if prop['doc']:
|
||||
lines.append(f" - {prop['doc'].strip()}")
|
||||
lines.append("")
|
||||
|
||||
# Methods
|
||||
if cls_info['methods']:
|
||||
lines.append("#### Methods")
|
||||
lines.append("")
|
||||
for method in cls_info['methods']:
|
||||
lines.append(f"##### `{method['signature']}`")
|
||||
if method['doc']:
|
||||
# Parse docstring for better formatting
|
||||
doc_lines = method['doc'].strip().split('\n')
|
||||
# Skip the signature line if it's repeated
|
||||
start = 1 if doc_lines and method['name'] in doc_lines[0] else 0
|
||||
for line in doc_lines[start:]:
|
||||
lines.append(line)
|
||||
lines.append("")
|
||||
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
def generate_markdown_function(func_info: Dict[str, Any]) -> List[str]:
|
||||
"""Generate markdown documentation for a function."""
|
||||
lines = []
|
||||
|
||||
lines.append(f"### `{func_info['signature']}`")
|
||||
lines.append("")
|
||||
|
||||
if func_info['doc']:
|
||||
doc_lines = func_info['doc'].strip().split('\n')
|
||||
# Skip signature line if present
|
||||
start = 1 if doc_lines and func_info['name'] in doc_lines[0] else 0
|
||||
|
||||
# Process documentation sections
|
||||
in_section = None
|
||||
for line in doc_lines[start:]:
|
||||
if line.strip() in ['Args:', 'Returns:', 'Raises:', 'Note:', 'Example:']:
|
||||
in_section = line.strip()
|
||||
lines.append(f"**{in_section}**")
|
||||
elif in_section and line.strip():
|
||||
# Indent content under sections
|
||||
lines.append(f"{line}")
|
||||
else:
|
||||
lines.append(line)
|
||||
lines.append("")
|
||||
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
def generate_markdown_docs() -> str:
|
||||
"""Generate complete markdown API documentation."""
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
lines.append("# McRogueFace API Reference")
|
||||
lines.append("")
|
||||
lines.append(f"*Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
|
||||
lines.append("")
|
||||
|
||||
# Module description
|
||||
if mcrfpy.__doc__:
|
||||
lines.append("## Overview")
|
||||
lines.append("")
|
||||
lines.extend(mcrfpy.__doc__.strip().split('\n'))
|
||||
lines.append("")
|
||||
|
||||
# Table of contents
|
||||
lines.append("## Table of Contents")
|
||||
lines.append("")
|
||||
lines.append("- [Classes](#classes)")
|
||||
lines.append("- [Functions](#functions)")
|
||||
lines.append("- [Automation Module](#automation-module)")
|
||||
lines.append("")
|
||||
|
||||
# Collect all components
|
||||
classes = []
|
||||
functions = []
|
||||
constants = []
|
||||
|
||||
for name in sorted(dir(mcrfpy)):
|
||||
if name.startswith('_'):
|
||||
continue
|
||||
|
||||
obj = getattr(mcrfpy, name)
|
||||
|
||||
if isinstance(obj, type):
|
||||
classes.append((name, obj))
|
||||
elif callable(obj):
|
||||
functions.append((name, obj))
|
||||
elif not inspect.ismodule(obj):
|
||||
constants.append((name, obj))
|
||||
|
||||
# Document classes
|
||||
lines.append("## Classes")
|
||||
lines.append("")
|
||||
|
||||
# Group classes by category
|
||||
ui_classes = []
|
||||
collection_classes = []
|
||||
system_classes = []
|
||||
other_classes = []
|
||||
|
||||
for name, cls in classes:
|
||||
if name in ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity']:
|
||||
ui_classes.append((name, cls))
|
||||
elif 'Collection' in name:
|
||||
collection_classes.append((name, cls))
|
||||
elif name in ['Color', 'Vector', 'Texture', 'Font']:
|
||||
system_classes.append((name, cls))
|
||||
else:
|
||||
other_classes.append((name, cls))
|
||||
|
||||
# UI Classes
|
||||
if ui_classes:
|
||||
lines.append("### UI Components")
|
||||
lines.append("")
|
||||
for name, cls in ui_classes:
|
||||
lines.extend(generate_markdown_class(get_class_info(cls)))
|
||||
|
||||
# Collections
|
||||
if collection_classes:
|
||||
lines.append("### Collections")
|
||||
lines.append("")
|
||||
for name, cls in collection_classes:
|
||||
lines.extend(generate_markdown_class(get_class_info(cls)))
|
||||
|
||||
# System Classes
|
||||
if system_classes:
|
||||
lines.append("### System Types")
|
||||
lines.append("")
|
||||
for name, cls in system_classes:
|
||||
lines.extend(generate_markdown_class(get_class_info(cls)))
|
||||
|
||||
# Other Classes
|
||||
if other_classes:
|
||||
lines.append("### Other Classes")
|
||||
lines.append("")
|
||||
for name, cls in other_classes:
|
||||
lines.extend(generate_markdown_class(get_class_info(cls)))
|
||||
|
||||
# Document functions
|
||||
lines.append("## Functions")
|
||||
lines.append("")
|
||||
|
||||
# Group functions by category
|
||||
scene_funcs = []
|
||||
audio_funcs = []
|
||||
ui_funcs = []
|
||||
system_funcs = []
|
||||
|
||||
for name, func in functions:
|
||||
if 'scene' in name.lower() or name in ['createScene', 'setScene']:
|
||||
scene_funcs.append((name, func))
|
||||
elif any(x in name.lower() for x in ['sound', 'music', 'volume']):
|
||||
audio_funcs.append((name, func))
|
||||
elif name in ['find', 'findAll']:
|
||||
ui_funcs.append((name, func))
|
||||
else:
|
||||
system_funcs.append((name, func))
|
||||
|
||||
# Scene Management
|
||||
if scene_funcs:
|
||||
lines.append("### Scene Management")
|
||||
lines.append("")
|
||||
for name, func in scene_funcs:
|
||||
lines.extend(generate_markdown_function(get_function_info(func, name)))
|
||||
|
||||
# Audio
|
||||
if audio_funcs:
|
||||
lines.append("### Audio")
|
||||
lines.append("")
|
||||
for name, func in audio_funcs:
|
||||
lines.extend(generate_markdown_function(get_function_info(func, name)))
|
||||
|
||||
# UI Utilities
|
||||
if ui_funcs:
|
||||
lines.append("### UI Utilities")
|
||||
lines.append("")
|
||||
for name, func in ui_funcs:
|
||||
lines.extend(generate_markdown_function(get_function_info(func, name)))
|
||||
|
||||
# System
|
||||
if system_funcs:
|
||||
lines.append("### System")
|
||||
lines.append("")
|
||||
for name, func in system_funcs:
|
||||
lines.extend(generate_markdown_function(get_function_info(func, name)))
|
||||
|
||||
# Automation module
|
||||
if hasattr(mcrfpy, 'automation'):
|
||||
lines.append("## Automation Module")
|
||||
lines.append("")
|
||||
lines.append("The `mcrfpy.automation` module provides testing and automation capabilities.")
|
||||
lines.append("")
|
||||
|
||||
automation = mcrfpy.automation
|
||||
auto_funcs = []
|
||||
|
||||
for name in sorted(dir(automation)):
|
||||
if not name.startswith('_'):
|
||||
obj = getattr(automation, name)
|
||||
if callable(obj):
|
||||
auto_funcs.append((name, obj))
|
||||
|
||||
for name, func in auto_funcs:
|
||||
# Format as static method
|
||||
func_info = get_function_info(func, name)
|
||||
lines.append(f"### `automation.{func_info['signature']}`")
|
||||
lines.append("")
|
||||
if func_info['doc']:
|
||||
lines.append(func_info['doc'])
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def generate_html_docs(markdown_content: str) -> str:
|
||||
"""Convert markdown to HTML."""
|
||||
# Simple conversion - in production use a proper markdown parser
|
||||
html = ['<!DOCTYPE html>']
|
||||
html.append('<html><head>')
|
||||
html.append('<meta charset="UTF-8">')
|
||||
html.append('<title>McRogueFace API Reference</title>')
|
||||
html.append('<style>')
|
||||
html.append('''
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
line-height: 1.6; color: #333; max-width: 900px; margin: 0 auto; padding: 20px; }
|
||||
h1, h2, h3, h4, h5 { color: #2c3e50; margin-top: 24px; }
|
||||
h1 { border-bottom: 2px solid #3498db; padding-bottom: 10px; }
|
||||
h2 { border-bottom: 1px solid #ecf0f1; padding-bottom: 8px; }
|
||||
code { background: #f4f4f4; padding: 2px 4px; border-radius: 3px; font-size: 90%; }
|
||||
pre { background: #f4f4f4; padding: 12px; border-radius: 5px; overflow-x: auto; }
|
||||
pre code { background: none; padding: 0; }
|
||||
blockquote { border-left: 4px solid #3498db; margin: 0; padding-left: 16px; color: #7f8c8d; }
|
||||
hr { border: none; border-top: 1px solid #ecf0f1; margin: 24px 0; }
|
||||
a { color: #3498db; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.property { color: #27ae60; }
|
||||
.method { color: #2980b9; }
|
||||
.class-name { color: #8e44ad; font-weight: bold; }
|
||||
ul { padding-left: 24px; }
|
||||
li { margin: 4px 0; }
|
||||
''')
|
||||
html.append('</style>')
|
||||
html.append('</head><body>')
|
||||
|
||||
# Very basic markdown to HTML conversion
|
||||
lines = markdown_content.split('\n')
|
||||
in_code_block = False
|
||||
in_list = False
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
|
||||
if stripped.startswith('```'):
|
||||
if in_code_block:
|
||||
html.append('</code></pre>')
|
||||
in_code_block = False
|
||||
else:
|
||||
lang = stripped[3:] or 'python'
|
||||
html.append(f'<pre><code class="language-{lang}">')
|
||||
in_code_block = True
|
||||
continue
|
||||
|
||||
if in_code_block:
|
||||
html.append(line)
|
||||
continue
|
||||
|
||||
# Headers
|
||||
if stripped.startswith('#'):
|
||||
level = len(stripped.split()[0])
|
||||
text = stripped[level:].strip()
|
||||
html.append(f'<h{level}>{text}</h{level}>')
|
||||
# Lists
|
||||
elif stripped.startswith('- '):
|
||||
if not in_list:
|
||||
html.append('<ul>')
|
||||
in_list = True
|
||||
html.append(f'<li>{stripped[2:]}</li>')
|
||||
# Horizontal rule
|
||||
elif stripped == '---':
|
||||
if in_list:
|
||||
html.append('</ul>')
|
||||
in_list = False
|
||||
html.append('<hr>')
|
||||
# Emphasis
|
||||
elif stripped.startswith('*') and stripped.endswith('*') and len(stripped) > 2:
|
||||
html.append(f'<em>{stripped[1:-1]}</em>')
|
||||
# Bold
|
||||
elif stripped.startswith('**') and stripped.endswith('**'):
|
||||
html.append(f'<strong>{stripped[2:-2]}</strong>')
|
||||
# Regular paragraph
|
||||
elif stripped:
|
||||
if in_list:
|
||||
html.append('</ul>')
|
||||
in_list = False
|
||||
# Convert inline code
|
||||
text = stripped
|
||||
if '`' in text:
|
||||
import re
|
||||
text = re.sub(r'`([^`]+)`', r'<code>\1</code>', text)
|
||||
html.append(f'<p>{text}</p>')
|
||||
else:
|
||||
if in_list:
|
||||
html.append('</ul>')
|
||||
in_list = False
|
||||
# Empty line
|
||||
html.append('')
|
||||
|
||||
if in_list:
|
||||
html.append('</ul>')
|
||||
if in_code_block:
|
||||
html.append('</code></pre>')
|
||||
|
||||
html.append('</body></html>')
|
||||
return '\n'.join(html)
|
||||
|
||||
def main():
|
||||
"""Generate API documentation in multiple formats."""
|
||||
print("Generating McRogueFace API Documentation...")
|
||||
|
||||
# Create docs directory
|
||||
docs_dir = Path("docs")
|
||||
docs_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Generate markdown documentation
|
||||
print("- Generating Markdown documentation...")
|
||||
markdown_content = generate_markdown_docs()
|
||||
|
||||
# Write markdown
|
||||
md_path = docs_dir / "API_REFERENCE.md"
|
||||
with open(md_path, 'w') as f:
|
||||
f.write(markdown_content)
|
||||
print(f" ✓ Written to {md_path}")
|
||||
|
||||
# Generate HTML
|
||||
print("- Generating HTML documentation...")
|
||||
html_content = generate_html_docs(markdown_content)
|
||||
|
||||
# Write HTML
|
||||
html_path = docs_dir / "api_reference.html"
|
||||
with open(html_path, 'w') as f:
|
||||
f.write(html_content)
|
||||
print(f" ✓ Written to {html_path}")
|
||||
|
||||
# Summary statistics
|
||||
lines = markdown_content.split('\n')
|
||||
class_count = markdown_content.count('### class')
|
||||
func_count = len([l for l in lines if l.strip().startswith('### `') and 'class' not in l])
|
||||
|
||||
print("\nDocumentation Statistics:")
|
||||
print(f"- Classes documented: {class_count}")
|
||||
print(f"- Functions documented: {func_count}")
|
||||
print(f"- Total lines: {len(lines)}")
|
||||
print(f"- File size: {len(markdown_content):,} bytes")
|
||||
|
||||
print("\nAPI documentation generated successfully!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate API reference documentation for McRogueFace - Simple version."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import mcrfpy
|
||||
|
||||
def generate_markdown_docs():
|
||||
"""Generate markdown API documentation."""
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
lines.append("# McRogueFace API Reference")
|
||||
lines.append("")
|
||||
lines.append("*Generated on {}*".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
||||
lines.append("")
|
||||
|
||||
# Module description
|
||||
if mcrfpy.__doc__:
|
||||
lines.append("## Overview")
|
||||
lines.append("")
|
||||
lines.extend(mcrfpy.__doc__.strip().split('\n'))
|
||||
lines.append("")
|
||||
|
||||
# Collect all components
|
||||
classes = []
|
||||
functions = []
|
||||
|
||||
for name in sorted(dir(mcrfpy)):
|
||||
if name.startswith('_'):
|
||||
continue
|
||||
|
||||
obj = getattr(mcrfpy, name)
|
||||
|
||||
if isinstance(obj, type):
|
||||
classes.append((name, obj))
|
||||
elif callable(obj):
|
||||
functions.append((name, obj))
|
||||
|
||||
# Document classes
|
||||
lines.append("## Classes")
|
||||
lines.append("")
|
||||
|
||||
for name, cls in classes:
|
||||
lines.append("### class {}".format(name))
|
||||
if cls.__doc__:
|
||||
doc_lines = cls.__doc__.strip().split('\n')
|
||||
for line in doc_lines[:5]: # First 5 lines
|
||||
lines.append(line)
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
# Document functions
|
||||
lines.append("## Functions")
|
||||
lines.append("")
|
||||
|
||||
for name, func in functions:
|
||||
lines.append("### {}".format(name))
|
||||
if func.__doc__:
|
||||
doc_lines = func.__doc__.strip().split('\n')
|
||||
for line in doc_lines[:5]: # First 5 lines
|
||||
lines.append(line)
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
# Automation module
|
||||
if hasattr(mcrfpy, 'automation'):
|
||||
lines.append("## Automation Module")
|
||||
lines.append("")
|
||||
|
||||
automation = mcrfpy.automation
|
||||
for name in sorted(dir(automation)):
|
||||
if not name.startswith('_'):
|
||||
obj = getattr(automation, name)
|
||||
if callable(obj):
|
||||
lines.append("### automation.{}".format(name))
|
||||
if obj.__doc__:
|
||||
lines.append(obj.__doc__.strip().split('\n')[0])
|
||||
lines.append("")
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def main():
|
||||
"""Generate API documentation."""
|
||||
print("Generating McRogueFace API Documentation...")
|
||||
|
||||
# Create docs directory
|
||||
docs_dir = Path("docs")
|
||||
docs_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Generate markdown
|
||||
markdown_content = generate_markdown_docs()
|
||||
|
||||
# Write markdown
|
||||
md_path = docs_dir / "API_REFERENCE.md"
|
||||
with open(md_path, 'w') as f:
|
||||
f.write(markdown_content)
|
||||
print("Written to {}".format(md_path))
|
||||
|
||||
# Summary
|
||||
lines = markdown_content.split('\n')
|
||||
class_count = markdown_content.count('### class')
|
||||
func_count = markdown_content.count('### ') - class_count - markdown_content.count('### automation.')
|
||||
|
||||
print("\nDocumentation Statistics:")
|
||||
print("- Classes documented: {}".format(class_count))
|
||||
print("- Functions documented: {}".format(func_count))
|
||||
print("- Total lines: {}".format(len(lines)))
|
||||
|
||||
print("\nAPI documentation generated successfully!")
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,960 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate COMPLETE HTML API reference documentation for McRogueFace with NO missing methods."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
import html
|
||||
from pathlib import Path
|
||||
import mcrfpy
|
||||
|
||||
def escape_html(text: str) -> str:
|
||||
"""Escape HTML special characters."""
|
||||
return html.escape(text) if text else ""
|
||||
|
||||
def get_complete_method_documentation():
|
||||
"""Return complete documentation for ALL methods across all classes."""
|
||||
return {
|
||||
# Base Drawable methods (inherited by all UI elements)
|
||||
'Drawable': {
|
||||
'get_bounds': {
|
||||
'signature': 'get_bounds()',
|
||||
'description': 'Get the bounding rectangle of this drawable element.',
|
||||
'returns': 'tuple: (x, y, width, height) representing the element\'s bounds',
|
||||
'note': 'The bounds are in screen coordinates and account for current position and size.'
|
||||
},
|
||||
'move': {
|
||||
'signature': 'move(dx, dy)',
|
||||
'description': 'Move the element by a relative offset.',
|
||||
'args': [
|
||||
('dx', 'float', 'Horizontal offset in pixels'),
|
||||
('dy', 'float', 'Vertical offset in pixels')
|
||||
],
|
||||
'note': 'This modifies the x and y position properties by the given amounts.'
|
||||
},
|
||||
'resize': {
|
||||
'signature': 'resize(width, height)',
|
||||
'description': 'Resize the element to new dimensions.',
|
||||
'args': [
|
||||
('width', 'float', 'New width in pixels'),
|
||||
('height', 'float', 'New height in pixels')
|
||||
],
|
||||
'note': 'For Caption and Sprite, this may not change actual size if determined by content.'
|
||||
}
|
||||
},
|
||||
|
||||
# Entity-specific methods
|
||||
'Entity': {
|
||||
'at': {
|
||||
'signature': 'at(x, y)',
|
||||
'description': 'Check if this entity is at the specified grid coordinates.',
|
||||
'args': [
|
||||
('x', 'int', 'Grid x coordinate to check'),
|
||||
('y', 'int', 'Grid y coordinate to check')
|
||||
],
|
||||
'returns': 'bool: True if entity is at position (x, y), False otherwise'
|
||||
},
|
||||
'die': {
|
||||
'signature': 'die()',
|
||||
'description': 'Remove this entity from its parent grid.',
|
||||
'note': 'The entity object remains valid but is no longer rendered or updated.'
|
||||
},
|
||||
'index': {
|
||||
'signature': 'index()',
|
||||
'description': 'Get the index of this entity in its parent grid\'s entity list.',
|
||||
'returns': 'int: Index position, or -1 if not in a grid'
|
||||
}
|
||||
},
|
||||
|
||||
# Grid-specific methods
|
||||
'Grid': {
|
||||
'at': {
|
||||
'signature': 'at(x, y)',
|
||||
'description': 'Get the GridPoint at the specified grid coordinates.',
|
||||
'args': [
|
||||
('x', 'int', 'Grid x coordinate'),
|
||||
('y', 'int', 'Grid y coordinate')
|
||||
],
|
||||
'returns': 'GridPoint or None: The grid point at (x, y), or None if out of bounds'
|
||||
}
|
||||
},
|
||||
|
||||
# Collection methods
|
||||
'EntityCollection': {
|
||||
'append': {
|
||||
'signature': 'append(entity)',
|
||||
'description': 'Add an entity to the end of the collection.',
|
||||
'args': [('entity', 'Entity', 'The entity to add')]
|
||||
},
|
||||
'remove': {
|
||||
'signature': 'remove(entity)',
|
||||
'description': 'Remove the first occurrence of an entity from the collection.',
|
||||
'args': [('entity', 'Entity', 'The entity to remove')],
|
||||
'raises': 'ValueError: If entity is not in collection'
|
||||
},
|
||||
'extend': {
|
||||
'signature': 'extend(iterable)',
|
||||
'description': 'Add all entities from an iterable to the collection.',
|
||||
'args': [('iterable', 'Iterable[Entity]', 'Entities to add')]
|
||||
},
|
||||
'count': {
|
||||
'signature': 'count(entity)',
|
||||
'description': 'Count the number of occurrences of an entity in the collection.',
|
||||
'args': [('entity', 'Entity', 'The entity to count')],
|
||||
'returns': 'int: Number of times entity appears in collection'
|
||||
},
|
||||
'index': {
|
||||
'signature': 'index(entity)',
|
||||
'description': 'Find the index of the first occurrence of an entity.',
|
||||
'args': [('entity', 'Entity', 'The entity to find')],
|
||||
'returns': 'int: Index of entity in collection',
|
||||
'raises': 'ValueError: If entity is not in collection'
|
||||
}
|
||||
},
|
||||
|
||||
'UICollection': {
|
||||
'append': {
|
||||
'signature': 'append(drawable)',
|
||||
'description': 'Add a drawable element to the end of the collection.',
|
||||
'args': [('drawable', 'UIDrawable', 'The drawable element to add')]
|
||||
},
|
||||
'remove': {
|
||||
'signature': 'remove(drawable)',
|
||||
'description': 'Remove the first occurrence of a drawable from the collection.',
|
||||
'args': [('drawable', 'UIDrawable', 'The drawable to remove')],
|
||||
'raises': 'ValueError: If drawable is not in collection'
|
||||
},
|
||||
'extend': {
|
||||
'signature': 'extend(iterable)',
|
||||
'description': 'Add all drawables from an iterable to the collection.',
|
||||
'args': [('iterable', 'Iterable[UIDrawable]', 'Drawables to add')]
|
||||
},
|
||||
'count': {
|
||||
'signature': 'count(drawable)',
|
||||
'description': 'Count the number of occurrences of a drawable in the collection.',
|
||||
'args': [('drawable', 'UIDrawable', 'The drawable to count')],
|
||||
'returns': 'int: Number of times drawable appears in collection'
|
||||
},
|
||||
'index': {
|
||||
'signature': 'index(drawable)',
|
||||
'description': 'Find the index of the first occurrence of a drawable.',
|
||||
'args': [('drawable', 'UIDrawable', 'The drawable to find')],
|
||||
'returns': 'int: Index of drawable in collection',
|
||||
'raises': 'ValueError: If drawable is not in collection'
|
||||
}
|
||||
},
|
||||
|
||||
# Animation methods
|
||||
'Animation': {
|
||||
'get_current_value': {
|
||||
'signature': 'get_current_value()',
|
||||
'description': 'Get the current interpolated value of the animation.',
|
||||
'returns': 'float: Current animation value between start and end'
|
||||
},
|
||||
'start': {
|
||||
'signature': 'start(target)',
|
||||
'description': 'Start the animation on a target UI element.',
|
||||
'args': [('target', 'UIDrawable', 'The UI element to animate')],
|
||||
'note': 'The target must have the property specified in the animation constructor.'
|
||||
},
|
||||
'update': {
|
||||
'signature': 'update(delta_time)',
|
||||
'description': 'Update the animation by the given time delta.',
|
||||
'args': [('delta_time', 'float', 'Time elapsed since last update in seconds')],
|
||||
'returns': 'bool: True if animation is still running, False if finished'
|
||||
}
|
||||
},
|
||||
|
||||
# Color methods
|
||||
'Color': {
|
||||
'from_hex': {
|
||||
'signature': 'from_hex(hex_string)',
|
||||
'description': 'Create a Color from a hexadecimal color string.',
|
||||
'args': [('hex_string', 'str', 'Hex color string (e.g., "#FF0000" or "FF0000")')],
|
||||
'returns': 'Color: New Color object from hex string',
|
||||
'example': 'red = Color.from_hex("#FF0000")'
|
||||
},
|
||||
'to_hex': {
|
||||
'signature': 'to_hex()',
|
||||
'description': 'Convert this Color to a hexadecimal string.',
|
||||
'returns': 'str: Hex color string in format "#RRGGBB"',
|
||||
'example': 'hex_str = color.to_hex() # Returns "#FF0000"'
|
||||
},
|
||||
'lerp': {
|
||||
'signature': 'lerp(other, t)',
|
||||
'description': 'Linearly interpolate between this color and another.',
|
||||
'args': [
|
||||
('other', 'Color', 'The color to interpolate towards'),
|
||||
('t', 'float', 'Interpolation factor from 0.0 to 1.0')
|
||||
],
|
||||
'returns': 'Color: New interpolated Color object',
|
||||
'example': 'mixed = red.lerp(blue, 0.5) # 50% between red and blue'
|
||||
}
|
||||
},
|
||||
|
||||
# Vector methods
|
||||
'Vector': {
|
||||
'magnitude': {
|
||||
'signature': 'magnitude()',
|
||||
'description': 'Calculate the length/magnitude of this vector.',
|
||||
'returns': 'float: The magnitude of the vector',
|
||||
'example': 'length = vector.magnitude()'
|
||||
},
|
||||
'magnitude_squared': {
|
||||
'signature': 'magnitude_squared()',
|
||||
'description': 'Calculate the squared magnitude of this vector.',
|
||||
'returns': 'float: The squared magnitude (faster than magnitude())',
|
||||
'note': 'Use this for comparisons to avoid expensive square root calculation.'
|
||||
},
|
||||
'normalize': {
|
||||
'signature': 'normalize()',
|
||||
'description': 'Return a unit vector in the same direction.',
|
||||
'returns': 'Vector: New normalized vector with magnitude 1.0',
|
||||
'raises': 'ValueError: If vector has zero magnitude'
|
||||
},
|
||||
'dot': {
|
||||
'signature': 'dot(other)',
|
||||
'description': 'Calculate the dot product with another vector.',
|
||||
'args': [('other', 'Vector', 'The other vector')],
|
||||
'returns': 'float: Dot product of the two vectors'
|
||||
},
|
||||
'distance_to': {
|
||||
'signature': 'distance_to(other)',
|
||||
'description': 'Calculate the distance to another vector.',
|
||||
'args': [('other', 'Vector', 'The other vector')],
|
||||
'returns': 'float: Distance between the two vectors'
|
||||
},
|
||||
'angle': {
|
||||
'signature': 'angle()',
|
||||
'description': 'Get the angle of this vector in radians.',
|
||||
'returns': 'float: Angle in radians from positive x-axis'
|
||||
},
|
||||
'copy': {
|
||||
'signature': 'copy()',
|
||||
'description': 'Create a copy of this vector.',
|
||||
'returns': 'Vector: New Vector object with same x and y values'
|
||||
}
|
||||
},
|
||||
|
||||
# Scene methods
|
||||
'Scene': {
|
||||
'activate': {
|
||||
'signature': 'activate()',
|
||||
'description': 'Make this scene the active scene.',
|
||||
'note': 'Equivalent to calling setScene() with this scene\'s name.'
|
||||
},
|
||||
'get_ui': {
|
||||
'signature': 'get_ui()',
|
||||
'description': 'Get the UI element collection for this scene.',
|
||||
'returns': 'UICollection: Collection of all UI elements in this scene'
|
||||
},
|
||||
'keypress': {
|
||||
'signature': 'keypress(handler)',
|
||||
'description': 'Register a keyboard handler function for this scene.',
|
||||
'args': [('handler', 'callable', 'Function that takes (key_name: str, is_pressed: bool)')],
|
||||
'note': 'Alternative to overriding the on_keypress method.'
|
||||
},
|
||||
'register_keyboard': {
|
||||
'signature': 'register_keyboard(callable)',
|
||||
'description': 'Register a keyboard event handler function for the scene.',
|
||||
'args': [('callable', 'callable', 'Function that takes (key: str, action: str) parameters')],
|
||||
'note': 'Alternative to overriding the on_keypress method when subclassing Scene objects.',
|
||||
'example': '''def handle_keyboard(key, action):
|
||||
print(f"Key '{key}' was {action}")
|
||||
if key == "q" and action == "press":
|
||||
# Handle quit
|
||||
pass
|
||||
scene.register_keyboard(handle_keyboard)'''
|
||||
}
|
||||
},
|
||||
|
||||
# Timer methods
|
||||
'Timer': {
|
||||
'pause': {
|
||||
'signature': 'pause()',
|
||||
'description': 'Pause the timer, stopping its callback execution.',
|
||||
'note': 'Use resume() to continue the timer from where it was paused.'
|
||||
},
|
||||
'resume': {
|
||||
'signature': 'resume()',
|
||||
'description': 'Resume a paused timer.',
|
||||
'note': 'Has no effect if timer is not paused.'
|
||||
},
|
||||
'cancel': {
|
||||
'signature': 'cancel()',
|
||||
'description': 'Cancel the timer and remove it from the system.',
|
||||
'note': 'After cancelling, the timer object cannot be reused.'
|
||||
},
|
||||
'restart': {
|
||||
'signature': 'restart()',
|
||||
'description': 'Restart the timer from the beginning.',
|
||||
'note': 'Resets the timer\'s internal clock to zero.'
|
||||
}
|
||||
},
|
||||
|
||||
# Window methods
|
||||
'Window': {
|
||||
'get': {
|
||||
'signature': 'get()',
|
||||
'description': 'Get the Window singleton instance.',
|
||||
'returns': 'Window: The singleton window object',
|
||||
'note': 'This is a static method that returns the same instance every time.'
|
||||
},
|
||||
'center': {
|
||||
'signature': 'center()',
|
||||
'description': 'Center the window on the screen.',
|
||||
'note': 'Only works if the window is not fullscreen.'
|
||||
},
|
||||
'screenshot': {
|
||||
'signature': 'screenshot(filename)',
|
||||
'description': 'Take a screenshot and save it to a file.',
|
||||
'args': [('filename', 'str', 'Path where to save the screenshot')],
|
||||
'note': 'Supports PNG, JPG, and BMP formats based on file extension.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def get_complete_function_documentation():
|
||||
"""Return complete documentation for ALL module functions."""
|
||||
return {
|
||||
# Scene Management
|
||||
'createScene': {
|
||||
'signature': 'createScene(name: str) -> None',
|
||||
'description': 'Create a new empty scene with the given name.',
|
||||
'args': [('name', 'str', 'Unique name for the new scene')],
|
||||
'raises': 'ValueError: If a scene with this name already exists',
|
||||
'note': 'The scene is created but not made active. Use setScene() to switch to it.',
|
||||
'example': 'mcrfpy.createScene("game_over")'
|
||||
},
|
||||
'setScene': {
|
||||
'signature': 'setScene(scene: str, transition: str = None, duration: float = 0.0) -> None',
|
||||
'description': 'Switch to a different scene with optional transition effect.',
|
||||
'args': [
|
||||
('scene', 'str', 'Name of the scene to switch to'),
|
||||
('transition', 'str', 'Transition type: "fade", "slide_left", "slide_right", "slide_up", "slide_down"'),
|
||||
('duration', 'float', 'Transition duration in seconds (default: 0.0 for instant)')
|
||||
],
|
||||
'raises': 'KeyError: If the scene doesn\'t exist',
|
||||
'example': 'mcrfpy.setScene("game", "fade", 0.5)'
|
||||
},
|
||||
'currentScene': {
|
||||
'signature': 'currentScene() -> str',
|
||||
'description': 'Get the name of the currently active scene.',
|
||||
'returns': 'str: Name of the current scene',
|
||||
'example': 'scene_name = mcrfpy.currentScene()'
|
||||
},
|
||||
'sceneUI': {
|
||||
'signature': 'sceneUI(scene: str = None) -> UICollection',
|
||||
'description': 'Get all UI elements for a scene.',
|
||||
'args': [('scene', 'str', 'Scene name. If None, uses current scene')],
|
||||
'returns': 'UICollection: All UI elements in the scene',
|
||||
'raises': 'KeyError: If the specified scene doesn\'t exist',
|
||||
'example': 'ui_elements = mcrfpy.sceneUI("game")'
|
||||
},
|
||||
'keypressScene': {
|
||||
'signature': 'keypressScene(handler: callable) -> None',
|
||||
'description': 'Set the keyboard event handler for the current scene.',
|
||||
'args': [('handler', 'callable', 'Function that receives (key_name: str, is_pressed: bool)')],
|
||||
'example': '''def on_key(key, pressed):
|
||||
if key == "SPACE" and pressed:
|
||||
player.jump()
|
||||
mcrfpy.keypressScene(on_key)'''
|
||||
},
|
||||
|
||||
# Audio Functions
|
||||
'createSoundBuffer': {
|
||||
'signature': 'createSoundBuffer(filename: str) -> int',
|
||||
'description': 'Load a sound effect from a file and return its buffer ID.',
|
||||
'args': [('filename', 'str', 'Path to the sound file (WAV, OGG, FLAC)')],
|
||||
'returns': 'int: Buffer ID for use with playSound()',
|
||||
'raises': 'RuntimeError: If the file cannot be loaded',
|
||||
'example': 'jump_sound = mcrfpy.createSoundBuffer("assets/jump.wav")'
|
||||
},
|
||||
'loadMusic': {
|
||||
'signature': 'loadMusic(filename: str, loop: bool = True) -> None',
|
||||
'description': 'Load and immediately play background music from a file.',
|
||||
'args': [
|
||||
('filename', 'str', 'Path to the music file (WAV, OGG, FLAC)'),
|
||||
('loop', 'bool', 'Whether to loop the music (default: True)')
|
||||
],
|
||||
'note': 'Only one music track can play at a time. Loading new music stops the current track.',
|
||||
'example': 'mcrfpy.loadMusic("assets/background.ogg", True)'
|
||||
},
|
||||
'playSound': {
|
||||
'signature': 'playSound(buffer_id: int) -> None',
|
||||
'description': 'Play a sound effect using a previously loaded buffer.',
|
||||
'args': [('buffer_id', 'int', 'Sound buffer ID returned by createSoundBuffer()')],
|
||||
'raises': 'RuntimeError: If the buffer ID is invalid',
|
||||
'example': 'mcrfpy.playSound(jump_sound)'
|
||||
},
|
||||
'getMusicVolume': {
|
||||
'signature': 'getMusicVolume() -> int',
|
||||
'description': 'Get the current music volume level.',
|
||||
'returns': 'int: Current volume (0-100)',
|
||||
'example': 'current_volume = mcrfpy.getMusicVolume()'
|
||||
},
|
||||
'getSoundVolume': {
|
||||
'signature': 'getSoundVolume() -> int',
|
||||
'description': 'Get the current sound effects volume level.',
|
||||
'returns': 'int: Current volume (0-100)',
|
||||
'example': 'current_volume = mcrfpy.getSoundVolume()'
|
||||
},
|
||||
'setMusicVolume': {
|
||||
'signature': 'setMusicVolume(volume: int) -> None',
|
||||
'description': 'Set the global music volume.',
|
||||
'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')],
|
||||
'example': 'mcrfpy.setMusicVolume(50) # Set to 50% volume'
|
||||
},
|
||||
'setSoundVolume': {
|
||||
'signature': 'setSoundVolume(volume: int) -> None',
|
||||
'description': 'Set the global sound effects volume.',
|
||||
'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')],
|
||||
'example': 'mcrfpy.setSoundVolume(75) # Set to 75% volume'
|
||||
},
|
||||
|
||||
# UI Utilities
|
||||
'find': {
|
||||
'signature': 'find(name: str, scene: str = None) -> UIDrawable | None',
|
||||
'description': 'Find the first UI element with the specified name.',
|
||||
'args': [
|
||||
('name', 'str', 'Exact name to search for'),
|
||||
('scene', 'str', 'Scene to search in (default: current scene)')
|
||||
],
|
||||
'returns': 'UIDrawable or None: The found element, or None if not found',
|
||||
'note': 'Searches scene UI elements and entities within grids.',
|
||||
'example': 'button = mcrfpy.find("start_button")'
|
||||
},
|
||||
'findAll': {
|
||||
'signature': 'findAll(pattern: str, scene: str = None) -> list',
|
||||
'description': 'Find all UI elements matching a name pattern.',
|
||||
'args': [
|
||||
('pattern', 'str', 'Name pattern with optional wildcards (* matches any characters)'),
|
||||
('scene', 'str', 'Scene to search in (default: current scene)')
|
||||
],
|
||||
'returns': 'list: All matching UI elements and entities',
|
||||
'example': 'enemies = mcrfpy.findAll("enemy_*")'
|
||||
},
|
||||
|
||||
# System Functions
|
||||
'exit': {
|
||||
'signature': 'exit() -> None',
|
||||
'description': 'Cleanly shut down the game engine and exit the application.',
|
||||
'note': 'This immediately closes the window and terminates the program.',
|
||||
'example': 'mcrfpy.exit()'
|
||||
},
|
||||
'getMetrics': {
|
||||
'signature': 'getMetrics() -> dict',
|
||||
'description': 'Get current performance metrics.',
|
||||
'returns': '''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''',
|
||||
'example': 'metrics = mcrfpy.getMetrics()'
|
||||
},
|
||||
'setTimer': {
|
||||
'signature': 'setTimer(name: str, handler: callable, interval: int) -> None',
|
||||
'description': 'Create or update a recurring timer.',
|
||||
'args': [
|
||||
('name', 'str', 'Unique identifier for the timer'),
|
||||
('handler', 'callable', 'Function called with (runtime: float) parameter'),
|
||||
('interval', 'int', 'Time between calls in milliseconds')
|
||||
],
|
||||
'note': 'If a timer with this name exists, it will be replaced.',
|
||||
'example': '''def update_score(runtime):
|
||||
score += 1
|
||||
mcrfpy.setTimer("score_update", update_score, 1000)'''
|
||||
},
|
||||
'delTimer': {
|
||||
'signature': 'delTimer(name: str) -> None',
|
||||
'description': 'Stop and remove a timer.',
|
||||
'args': [('name', 'str', 'Timer identifier to remove')],
|
||||
'note': 'No error is raised if the timer doesn\'t exist.',
|
||||
'example': 'mcrfpy.delTimer("score_update")'
|
||||
},
|
||||
'setScale': {
|
||||
'signature': 'setScale(multiplier: float) -> None',
|
||||
'description': 'Scale the game window size.',
|
||||
'args': [('multiplier', 'float', 'Scale factor (e.g., 2.0 for double size)')],
|
||||
'note': 'The internal resolution remains 1024x768, but the window is scaled.',
|
||||
'example': 'mcrfpy.setScale(2.0) # Double the window size'
|
||||
}
|
||||
}
|
||||
|
||||
def get_complete_property_documentation():
|
||||
"""Return complete documentation for ALL properties."""
|
||||
return {
|
||||
'Animation': {
|
||||
'property': 'str: Name of the property being animated (e.g., "x", "y", "scale")',
|
||||
'duration': 'float: Total duration of the animation in seconds',
|
||||
'elapsed_time': 'float: Time elapsed since animation started (read-only)',
|
||||
'current_value': 'float: Current interpolated value of the animation (read-only)',
|
||||
'is_running': 'bool: True if animation is currently running (read-only)',
|
||||
'is_finished': 'bool: True if animation has completed (read-only)'
|
||||
},
|
||||
'GridPoint': {
|
||||
'x': 'int: Grid x coordinate of this point',
|
||||
'y': 'int: Grid y coordinate of this point',
|
||||
'texture_index': 'int: Index of the texture/sprite to display at this point',
|
||||
'solid': 'bool: Whether this point blocks movement',
|
||||
'transparent': 'bool: Whether this point allows light/vision through',
|
||||
'color': 'Color: Color tint applied to the texture at this point'
|
||||
},
|
||||
'GridPointState': {
|
||||
'visible': 'bool: Whether this point is currently visible to the player',
|
||||
'discovered': 'bool: Whether this point has been discovered/explored',
|
||||
'custom_flags': 'int: Bitfield for custom game-specific flags'
|
||||
}
|
||||
}
|
||||
|
||||
def generate_complete_html_documentation():
|
||||
"""Generate complete HTML documentation with NO missing methods."""
|
||||
|
||||
# Get all documentation data
|
||||
method_docs = get_complete_method_documentation()
|
||||
function_docs = get_complete_function_documentation()
|
||||
property_docs = get_complete_property_documentation()
|
||||
|
||||
html_parts = []
|
||||
|
||||
# HTML header with enhanced styling
|
||||
html_parts.append('''<!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 - Complete Documentation</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
border-bottom: 3px solid #3498db;
|
||||
padding-bottom: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #34495e;
|
||||
border-bottom: 2px solid #ecf0f1;
|
||||
padding-bottom: 10px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #2c3e50;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: #34495e;
|
||||
margin-top: 20px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
color: #555;
|
||||
margin-top: 15px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f4f4f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f8f8f8;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: 0.875em;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.class-name {
|
||||
color: #8e44ad;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.property {
|
||||
color: #27ae60;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.method {
|
||||
color: #2980b9;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.function-signature {
|
||||
color: #d73a49;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.method-section {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
|
||||
.arg-list {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.arg-item {
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e1e4e8;
|
||||
}
|
||||
|
||||
.arg-name {
|
||||
color: #d73a49;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.arg-type {
|
||||
color: #6f42c1;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.returns {
|
||||
background: #e8f5e8;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #28a745;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.note {
|
||||
background: #fff3cd;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #ffc107;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.example {
|
||||
background: #e7f3ff;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #0366d6;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.toc {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.toc ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.toc li {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.toc a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toc a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
''')
|
||||
|
||||
# Title and overview
|
||||
html_parts.append('<h1>McRogueFace API Reference - Complete Documentation</h1>')
|
||||
html_parts.append(f'<p><em>Generated on {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</em></p>')
|
||||
|
||||
# Table of contents
|
||||
html_parts.append('<div class="toc">')
|
||||
html_parts.append('<h2>Table of Contents</h2>')
|
||||
html_parts.append('<ul>')
|
||||
html_parts.append('<li><a href="#functions">Functions</a></li>')
|
||||
html_parts.append('<li><a href="#classes">Classes</a></li>')
|
||||
html_parts.append('<li><a href="#automation">Automation Module</a></li>')
|
||||
html_parts.append('</ul>')
|
||||
html_parts.append('</div>')
|
||||
|
||||
# Functions section
|
||||
html_parts.append('<h2 id="functions">Functions</h2>')
|
||||
|
||||
# Group functions by category
|
||||
categories = {
|
||||
'Scene Management': ['createScene', 'setScene', 'currentScene', 'sceneUI', 'keypressScene'],
|
||||
'Audio': ['createSoundBuffer', 'loadMusic', 'playSound', 'getMusicVolume', 'getSoundVolume', 'setMusicVolume', 'setSoundVolume'],
|
||||
'UI Utilities': ['find', 'findAll'],
|
||||
'System': ['exit', 'getMetrics', 'setTimer', 'delTimer', 'setScale']
|
||||
}
|
||||
|
||||
for category, functions in categories.items():
|
||||
html_parts.append(f'<h3>{category}</h3>')
|
||||
for func_name in functions:
|
||||
if func_name in function_docs:
|
||||
html_parts.append(format_function_html(func_name, function_docs[func_name]))
|
||||
|
||||
# Classes section
|
||||
html_parts.append('<h2 id="classes">Classes</h2>')
|
||||
|
||||
# Get all classes from mcrfpy
|
||||
classes = []
|
||||
for name in sorted(dir(mcrfpy)):
|
||||
if not name.startswith('_'):
|
||||
obj = getattr(mcrfpy, name)
|
||||
if isinstance(obj, type):
|
||||
classes.append((name, obj))
|
||||
|
||||
# Generate class documentation
|
||||
for class_name, cls in classes:
|
||||
html_parts.append(format_class_html_complete(class_name, cls, method_docs, property_docs))
|
||||
|
||||
# Automation section
|
||||
if hasattr(mcrfpy, 'automation'):
|
||||
html_parts.append('<h2 id="automation">Automation Module</h2>')
|
||||
html_parts.append('<p>The <code>mcrfpy.automation</code> module provides testing and automation capabilities.</p>')
|
||||
|
||||
automation = mcrfpy.automation
|
||||
for name in sorted(dir(automation)):
|
||||
if not name.startswith('_'):
|
||||
obj = getattr(automation, name)
|
||||
if callable(obj):
|
||||
html_parts.append(f'<div class="method-section">')
|
||||
html_parts.append(f'<h4><code class="function-signature">automation.{name}</code></h4>')
|
||||
if obj.__doc__:
|
||||
doc_parts = obj.__doc__.split(' - ')
|
||||
if len(doc_parts) > 1:
|
||||
html_parts.append(f'<p>{escape_html(doc_parts[1])}</p>')
|
||||
else:
|
||||
html_parts.append(f'<p>{escape_html(obj.__doc__)}</p>')
|
||||
html_parts.append('</div>')
|
||||
|
||||
html_parts.append('</div>')
|
||||
html_parts.append('</body>')
|
||||
html_parts.append('</html>')
|
||||
|
||||
return '\n'.join(html_parts)
|
||||
|
||||
def format_function_html(func_name, func_doc):
|
||||
"""Format a function with complete documentation."""
|
||||
html_parts = []
|
||||
|
||||
html_parts.append('<div class="method-section">')
|
||||
html_parts.append(f'<h4><code class="function-signature">{func_doc["signature"]}</code></h4>')
|
||||
html_parts.append(f'<p>{escape_html(func_doc["description"])}</p>')
|
||||
|
||||
# Arguments
|
||||
if 'args' in func_doc:
|
||||
html_parts.append('<div class="arg-list">')
|
||||
html_parts.append('<h5>Arguments:</h5>')
|
||||
for arg in func_doc['args']:
|
||||
html_parts.append('<div class="arg-item">')
|
||||
html_parts.append(f'<span class="arg-name">{arg[0]}</span> ')
|
||||
html_parts.append(f'<span class="arg-type">({arg[1]})</span>: ')
|
||||
html_parts.append(f'{escape_html(arg[2])}')
|
||||
html_parts.append('</div>')
|
||||
html_parts.append('</div>')
|
||||
|
||||
# Returns
|
||||
if 'returns' in func_doc:
|
||||
html_parts.append('<div class="returns">')
|
||||
html_parts.append(f'<strong>Returns:</strong> {escape_html(func_doc["returns"])}')
|
||||
html_parts.append('</div>')
|
||||
|
||||
# Raises
|
||||
if 'raises' in func_doc:
|
||||
html_parts.append('<div class="note">')
|
||||
html_parts.append(f'<strong>Raises:</strong> {escape_html(func_doc["raises"])}')
|
||||
html_parts.append('</div>')
|
||||
|
||||
# Note
|
||||
if 'note' in func_doc:
|
||||
html_parts.append('<div class="note">')
|
||||
html_parts.append(f'<strong>Note:</strong> {escape_html(func_doc["note"])}')
|
||||
html_parts.append('</div>')
|
||||
|
||||
# Example
|
||||
if 'example' in func_doc:
|
||||
html_parts.append('<div class="example">')
|
||||
html_parts.append('<h5>Example:</h5>')
|
||||
html_parts.append('<pre><code>')
|
||||
html_parts.append(escape_html(func_doc['example']))
|
||||
html_parts.append('</code></pre>')
|
||||
html_parts.append('</div>')
|
||||
|
||||
html_parts.append('</div>')
|
||||
|
||||
return '\n'.join(html_parts)
|
||||
|
||||
def format_class_html_complete(class_name, cls, method_docs, property_docs):
|
||||
"""Format a class with complete documentation."""
|
||||
html_parts = []
|
||||
|
||||
html_parts.append('<div class="method-section">')
|
||||
html_parts.append(f'<h3><span class="class-name">{class_name}</span></h3>')
|
||||
|
||||
# Class description
|
||||
if cls.__doc__:
|
||||
html_parts.append(f'<p>{escape_html(cls.__doc__)}</p>')
|
||||
|
||||
# Properties
|
||||
if class_name in property_docs:
|
||||
html_parts.append('<h4>Properties:</h4>')
|
||||
for prop_name, prop_desc in property_docs[class_name].items():
|
||||
html_parts.append(f'<div class="arg-item">')
|
||||
html_parts.append(f'<span class="property">{prop_name}</span>: {escape_html(prop_desc)}')
|
||||
html_parts.append('</div>')
|
||||
|
||||
# Methods
|
||||
methods_to_document = []
|
||||
|
||||
# Add inherited methods for UI classes
|
||||
if any(base.__name__ == 'Drawable' for base in cls.__bases__ if hasattr(base, '__name__')):
|
||||
methods_to_document.extend(['get_bounds', 'move', 'resize'])
|
||||
|
||||
# Add class-specific methods
|
||||
if class_name in method_docs:
|
||||
methods_to_document.extend(method_docs[class_name].keys())
|
||||
|
||||
# Add methods from introspection
|
||||
for attr_name in dir(cls):
|
||||
if not attr_name.startswith('_') and callable(getattr(cls, attr_name)):
|
||||
if attr_name not in methods_to_document:
|
||||
methods_to_document.append(attr_name)
|
||||
|
||||
if methods_to_document:
|
||||
html_parts.append('<h4>Methods:</h4>')
|
||||
for method_name in set(methods_to_document):
|
||||
# Get method documentation
|
||||
method_doc = None
|
||||
if class_name in method_docs and method_name in method_docs[class_name]:
|
||||
method_doc = method_docs[class_name][method_name]
|
||||
elif method_name in method_docs.get('Drawable', {}):
|
||||
method_doc = method_docs['Drawable'][method_name]
|
||||
|
||||
if method_doc:
|
||||
html_parts.append(format_method_html(method_name, method_doc))
|
||||
else:
|
||||
# Basic method with no documentation
|
||||
html_parts.append(f'<div class="arg-item">')
|
||||
html_parts.append(f'<span class="method">{method_name}(...)</span>')
|
||||
html_parts.append('</div>')
|
||||
|
||||
html_parts.append('</div>')
|
||||
|
||||
return '\n'.join(html_parts)
|
||||
|
||||
def format_method_html(method_name, method_doc):
|
||||
"""Format a method with complete documentation."""
|
||||
html_parts = []
|
||||
|
||||
html_parts.append('<div style="margin-left: 20px; margin-bottom: 15px;">')
|
||||
html_parts.append(f'<h5><code class="method">{method_doc["signature"]}</code></h5>')
|
||||
html_parts.append(f'<p>{escape_html(method_doc["description"])}</p>')
|
||||
|
||||
# Arguments
|
||||
if 'args' in method_doc:
|
||||
for arg in method_doc['args']:
|
||||
html_parts.append(f'<div style="margin-left: 20px;">')
|
||||
html_parts.append(f'<span class="arg-name">{arg[0]}</span> ')
|
||||
html_parts.append(f'<span class="arg-type">({arg[1]})</span>: ')
|
||||
html_parts.append(f'{escape_html(arg[2])}')
|
||||
html_parts.append('</div>')
|
||||
|
||||
# Returns
|
||||
if 'returns' in method_doc:
|
||||
html_parts.append(f'<div style="margin-left: 20px; color: #28a745;">')
|
||||
html_parts.append(f'<strong>Returns:</strong> {escape_html(method_doc["returns"])}')
|
||||
html_parts.append('</div>')
|
||||
|
||||
# Note
|
||||
if 'note' in method_doc:
|
||||
html_parts.append(f'<div style="margin-left: 20px; color: #856404;">')
|
||||
html_parts.append(f'<strong>Note:</strong> {escape_html(method_doc["note"])}')
|
||||
html_parts.append('</div>')
|
||||
|
||||
# Example
|
||||
if 'example' in method_doc:
|
||||
html_parts.append(f'<div style="margin-left: 20px;">')
|
||||
html_parts.append('<strong>Example:</strong>')
|
||||
html_parts.append('<pre><code>')
|
||||
html_parts.append(escape_html(method_doc['example']))
|
||||
html_parts.append('</code></pre>')
|
||||
html_parts.append('</div>')
|
||||
|
||||
html_parts.append('</div>')
|
||||
|
||||
return '\n'.join(html_parts)
|
||||
|
||||
def main():
|
||||
"""Generate complete HTML documentation with zero missing methods."""
|
||||
print("Generating COMPLETE HTML API documentation...")
|
||||
|
||||
# Generate HTML
|
||||
html_content = generate_complete_html_documentation()
|
||||
|
||||
# Write to file
|
||||
output_path = Path("docs/api_reference_complete.html")
|
||||
output_path.parent.mkdir(exist_ok=True)
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
|
||||
print(f"✓ Generated {output_path}")
|
||||
print(f" File size: {len(html_content):,} bytes")
|
||||
|
||||
# Count "..." instances
|
||||
ellipsis_count = html_content.count('...')
|
||||
print(f" Ellipsis instances: {ellipsis_count}")
|
||||
|
||||
if ellipsis_count == 0:
|
||||
print("✅ SUCCESS: No missing documentation found!")
|
||||
else:
|
||||
print(f"❌ WARNING: {ellipsis_count} methods still need documentation")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,821 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate COMPLETE Markdown API reference documentation for McRogueFace with NO missing methods."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
import mcrfpy
|
||||
|
||||
def get_complete_method_documentation():
|
||||
"""Return complete documentation for ALL methods across all classes."""
|
||||
return {
|
||||
# Base Drawable methods (inherited by all UI elements)
|
||||
'Drawable': {
|
||||
'get_bounds': {
|
||||
'signature': 'get_bounds()',
|
||||
'description': 'Get the bounding rectangle of this drawable element.',
|
||||
'returns': 'tuple: (x, y, width, height) representing the element\'s bounds',
|
||||
'note': 'The bounds are in screen coordinates and account for current position and size.'
|
||||
},
|
||||
'move': {
|
||||
'signature': 'move(dx, dy)',
|
||||
'description': 'Move the element by a relative offset.',
|
||||
'args': [
|
||||
('dx', 'float', 'Horizontal offset in pixels'),
|
||||
('dy', 'float', 'Vertical offset in pixels')
|
||||
],
|
||||
'note': 'This modifies the x and y position properties by the given amounts.'
|
||||
},
|
||||
'resize': {
|
||||
'signature': 'resize(width, height)',
|
||||
'description': 'Resize the element to new dimensions.',
|
||||
'args': [
|
||||
('width', 'float', 'New width in pixels'),
|
||||
('height', 'float', 'New height in pixels')
|
||||
],
|
||||
'note': 'For Caption and Sprite, this may not change actual size if determined by content.'
|
||||
}
|
||||
},
|
||||
|
||||
# Entity-specific methods
|
||||
'Entity': {
|
||||
'at': {
|
||||
'signature': 'at(x, y)',
|
||||
'description': 'Check if this entity is at the specified grid coordinates.',
|
||||
'args': [
|
||||
('x', 'int', 'Grid x coordinate to check'),
|
||||
('y', 'int', 'Grid y coordinate to check')
|
||||
],
|
||||
'returns': 'bool: True if entity is at position (x, y), False otherwise'
|
||||
},
|
||||
'die': {
|
||||
'signature': 'die()',
|
||||
'description': 'Remove this entity from its parent grid.',
|
||||
'note': 'The entity object remains valid but is no longer rendered or updated.'
|
||||
},
|
||||
'index': {
|
||||
'signature': 'index()',
|
||||
'description': 'Get the index of this entity in its parent grid\'s entity list.',
|
||||
'returns': 'int: Index position, or -1 if not in a grid'
|
||||
}
|
||||
},
|
||||
|
||||
# Grid-specific methods
|
||||
'Grid': {
|
||||
'at': {
|
||||
'signature': 'at(x, y)',
|
||||
'description': 'Get the GridPoint at the specified grid coordinates.',
|
||||
'args': [
|
||||
('x', 'int', 'Grid x coordinate'),
|
||||
('y', 'int', 'Grid y coordinate')
|
||||
],
|
||||
'returns': 'GridPoint or None: The grid point at (x, y), or None if out of bounds'
|
||||
}
|
||||
},
|
||||
|
||||
# Collection methods
|
||||
'EntityCollection': {
|
||||
'append': {
|
||||
'signature': 'append(entity)',
|
||||
'description': 'Add an entity to the end of the collection.',
|
||||
'args': [('entity', 'Entity', 'The entity to add')]
|
||||
},
|
||||
'remove': {
|
||||
'signature': 'remove(entity)',
|
||||
'description': 'Remove the first occurrence of an entity from the collection.',
|
||||
'args': [('entity', 'Entity', 'The entity to remove')],
|
||||
'raises': 'ValueError: If entity is not in collection'
|
||||
},
|
||||
'extend': {
|
||||
'signature': 'extend(iterable)',
|
||||
'description': 'Add all entities from an iterable to the collection.',
|
||||
'args': [('iterable', 'Iterable[Entity]', 'Entities to add')]
|
||||
},
|
||||
'count': {
|
||||
'signature': 'count(entity)',
|
||||
'description': 'Count the number of occurrences of an entity in the collection.',
|
||||
'args': [('entity', 'Entity', 'The entity to count')],
|
||||
'returns': 'int: Number of times entity appears in collection'
|
||||
},
|
||||
'index': {
|
||||
'signature': 'index(entity)',
|
||||
'description': 'Find the index of the first occurrence of an entity.',
|
||||
'args': [('entity', 'Entity', 'The entity to find')],
|
||||
'returns': 'int: Index of entity in collection',
|
||||
'raises': 'ValueError: If entity is not in collection'
|
||||
}
|
||||
},
|
||||
|
||||
'UICollection': {
|
||||
'append': {
|
||||
'signature': 'append(drawable)',
|
||||
'description': 'Add a drawable element to the end of the collection.',
|
||||
'args': [('drawable', 'UIDrawable', 'The drawable element to add')]
|
||||
},
|
||||
'remove': {
|
||||
'signature': 'remove(drawable)',
|
||||
'description': 'Remove the first occurrence of a drawable from the collection.',
|
||||
'args': [('drawable', 'UIDrawable', 'The drawable to remove')],
|
||||
'raises': 'ValueError: If drawable is not in collection'
|
||||
},
|
||||
'extend': {
|
||||
'signature': 'extend(iterable)',
|
||||
'description': 'Add all drawables from an iterable to the collection.',
|
||||
'args': [('iterable', 'Iterable[UIDrawable]', 'Drawables to add')]
|
||||
},
|
||||
'count': {
|
||||
'signature': 'count(drawable)',
|
||||
'description': 'Count the number of occurrences of a drawable in the collection.',
|
||||
'args': [('drawable', 'UIDrawable', 'The drawable to count')],
|
||||
'returns': 'int: Number of times drawable appears in collection'
|
||||
},
|
||||
'index': {
|
||||
'signature': 'index(drawable)',
|
||||
'description': 'Find the index of the first occurrence of a drawable.',
|
||||
'args': [('drawable', 'UIDrawable', 'The drawable to find')],
|
||||
'returns': 'int: Index of drawable in collection',
|
||||
'raises': 'ValueError: If drawable is not in collection'
|
||||
}
|
||||
},
|
||||
|
||||
# Animation methods
|
||||
'Animation': {
|
||||
'get_current_value': {
|
||||
'signature': 'get_current_value()',
|
||||
'description': 'Get the current interpolated value of the animation.',
|
||||
'returns': 'float: Current animation value between start and end'
|
||||
},
|
||||
'start': {
|
||||
'signature': 'start(target)',
|
||||
'description': 'Start the animation on a target UI element.',
|
||||
'args': [('target', 'UIDrawable', 'The UI element to animate')],
|
||||
'note': 'The target must have the property specified in the animation constructor.'
|
||||
},
|
||||
'update': {
|
||||
'signature': 'update(delta_time)',
|
||||
'description': 'Update the animation by the given time delta.',
|
||||
'args': [('delta_time', 'float', 'Time elapsed since last update in seconds')],
|
||||
'returns': 'bool: True if animation is still running, False if finished'
|
||||
}
|
||||
},
|
||||
|
||||
# Color methods
|
||||
'Color': {
|
||||
'from_hex': {
|
||||
'signature': 'from_hex(hex_string)',
|
||||
'description': 'Create a Color from a hexadecimal color string.',
|
||||
'args': [('hex_string', 'str', 'Hex color string (e.g., "#FF0000" or "FF0000")')],
|
||||
'returns': 'Color: New Color object from hex string',
|
||||
'example': 'red = Color.from_hex("#FF0000")'
|
||||
},
|
||||
'to_hex': {
|
||||
'signature': 'to_hex()',
|
||||
'description': 'Convert this Color to a hexadecimal string.',
|
||||
'returns': 'str: Hex color string in format "#RRGGBB"',
|
||||
'example': 'hex_str = color.to_hex() # Returns "#FF0000"'
|
||||
},
|
||||
'lerp': {
|
||||
'signature': 'lerp(other, t)',
|
||||
'description': 'Linearly interpolate between this color and another.',
|
||||
'args': [
|
||||
('other', 'Color', 'The color to interpolate towards'),
|
||||
('t', 'float', 'Interpolation factor from 0.0 to 1.0')
|
||||
],
|
||||
'returns': 'Color: New interpolated Color object',
|
||||
'example': 'mixed = red.lerp(blue, 0.5) # 50% between red and blue'
|
||||
}
|
||||
},
|
||||
|
||||
# Vector methods
|
||||
'Vector': {
|
||||
'magnitude': {
|
||||
'signature': 'magnitude()',
|
||||
'description': 'Calculate the length/magnitude of this vector.',
|
||||
'returns': 'float: The magnitude of the vector'
|
||||
},
|
||||
'magnitude_squared': {
|
||||
'signature': 'magnitude_squared()',
|
||||
'description': 'Calculate the squared magnitude of this vector.',
|
||||
'returns': 'float: The squared magnitude (faster than magnitude())',
|
||||
'note': 'Use this for comparisons to avoid expensive square root calculation.'
|
||||
},
|
||||
'normalize': {
|
||||
'signature': 'normalize()',
|
||||
'description': 'Return a unit vector in the same direction.',
|
||||
'returns': 'Vector: New normalized vector with magnitude 1.0',
|
||||
'raises': 'ValueError: If vector has zero magnitude'
|
||||
},
|
||||
'dot': {
|
||||
'signature': 'dot(other)',
|
||||
'description': 'Calculate the dot product with another vector.',
|
||||
'args': [('other', 'Vector', 'The other vector')],
|
||||
'returns': 'float: Dot product of the two vectors'
|
||||
},
|
||||
'distance_to': {
|
||||
'signature': 'distance_to(other)',
|
||||
'description': 'Calculate the distance to another vector.',
|
||||
'args': [('other', 'Vector', 'The other vector')],
|
||||
'returns': 'float: Distance between the two vectors'
|
||||
},
|
||||
'angle': {
|
||||
'signature': 'angle()',
|
||||
'description': 'Get the angle of this vector in radians.',
|
||||
'returns': 'float: Angle in radians from positive x-axis'
|
||||
},
|
||||
'copy': {
|
||||
'signature': 'copy()',
|
||||
'description': 'Create a copy of this vector.',
|
||||
'returns': 'Vector: New Vector object with same x and y values'
|
||||
}
|
||||
},
|
||||
|
||||
# Scene methods
|
||||
'Scene': {
|
||||
'activate': {
|
||||
'signature': 'activate()',
|
||||
'description': 'Make this scene the active scene.',
|
||||
'note': 'Equivalent to calling setScene() with this scene\'s name.'
|
||||
},
|
||||
'get_ui': {
|
||||
'signature': 'get_ui()',
|
||||
'description': 'Get the UI element collection for this scene.',
|
||||
'returns': 'UICollection: Collection of all UI elements in this scene'
|
||||
},
|
||||
'keypress': {
|
||||
'signature': 'keypress(handler)',
|
||||
'description': 'Register a keyboard handler function for this scene.',
|
||||
'args': [('handler', 'callable', 'Function that takes (key_name: str, is_pressed: bool)')],
|
||||
'note': 'Alternative to overriding the on_keypress method.'
|
||||
},
|
||||
'register_keyboard': {
|
||||
'signature': 'register_keyboard(callable)',
|
||||
'description': 'Register a keyboard event handler function for the scene.',
|
||||
'args': [('callable', 'callable', 'Function that takes (key: str, action: str) parameters')],
|
||||
'note': 'Alternative to overriding the on_keypress method when subclassing Scene objects.',
|
||||
'example': '''def handle_keyboard(key, action):
|
||||
print(f"Key '{key}' was {action}")
|
||||
scene.register_keyboard(handle_keyboard)'''
|
||||
}
|
||||
},
|
||||
|
||||
# Timer methods
|
||||
'Timer': {
|
||||
'pause': {
|
||||
'signature': 'pause()',
|
||||
'description': 'Pause the timer, stopping its callback execution.',
|
||||
'note': 'Use resume() to continue the timer from where it was paused.'
|
||||
},
|
||||
'resume': {
|
||||
'signature': 'resume()',
|
||||
'description': 'Resume a paused timer.',
|
||||
'note': 'Has no effect if timer is not paused.'
|
||||
},
|
||||
'cancel': {
|
||||
'signature': 'cancel()',
|
||||
'description': 'Cancel the timer and remove it from the system.',
|
||||
'note': 'After cancelling, the timer object cannot be reused.'
|
||||
},
|
||||
'restart': {
|
||||
'signature': 'restart()',
|
||||
'description': 'Restart the timer from the beginning.',
|
||||
'note': 'Resets the timer\'s internal clock to zero.'
|
||||
}
|
||||
},
|
||||
|
||||
# Window methods
|
||||
'Window': {
|
||||
'get': {
|
||||
'signature': 'get()',
|
||||
'description': 'Get the Window singleton instance.',
|
||||
'returns': 'Window: The singleton window object',
|
||||
'note': 'This is a static method that returns the same instance every time.'
|
||||
},
|
||||
'center': {
|
||||
'signature': 'center()',
|
||||
'description': 'Center the window on the screen.',
|
||||
'note': 'Only works if the window is not fullscreen.'
|
||||
},
|
||||
'screenshot': {
|
||||
'signature': 'screenshot(filename)',
|
||||
'description': 'Take a screenshot and save it to a file.',
|
||||
'args': [('filename', 'str', 'Path where to save the screenshot')],
|
||||
'note': 'Supports PNG, JPG, and BMP formats based on file extension.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def get_complete_function_documentation():
|
||||
"""Return complete documentation for ALL module functions."""
|
||||
return {
|
||||
# Scene Management
|
||||
'createScene': {
|
||||
'signature': 'createScene(name: str) -> None',
|
||||
'description': 'Create a new empty scene with the given name.',
|
||||
'args': [('name', 'str', 'Unique name for the new scene')],
|
||||
'raises': 'ValueError: If a scene with this name already exists',
|
||||
'note': 'The scene is created but not made active. Use setScene() to switch to it.',
|
||||
'example': 'mcrfpy.createScene("game_over")'
|
||||
},
|
||||
'setScene': {
|
||||
'signature': 'setScene(scene: str, transition: str = None, duration: float = 0.0) -> None',
|
||||
'description': 'Switch to a different scene with optional transition effect.',
|
||||
'args': [
|
||||
('scene', 'str', 'Name of the scene to switch to'),
|
||||
('transition', 'str', 'Transition type: "fade", "slide_left", "slide_right", "slide_up", "slide_down"'),
|
||||
('duration', 'float', 'Transition duration in seconds (default: 0.0 for instant)')
|
||||
],
|
||||
'raises': 'KeyError: If the scene doesn\'t exist',
|
||||
'example': 'mcrfpy.setScene("game", "fade", 0.5)'
|
||||
},
|
||||
'currentScene': {
|
||||
'signature': 'currentScene() -> str',
|
||||
'description': 'Get the name of the currently active scene.',
|
||||
'returns': 'str: Name of the current scene',
|
||||
'example': 'scene_name = mcrfpy.currentScene()'
|
||||
},
|
||||
'sceneUI': {
|
||||
'signature': 'sceneUI(scene: str = None) -> UICollection',
|
||||
'description': 'Get all UI elements for a scene.',
|
||||
'args': [('scene', 'str', 'Scene name. If None, uses current scene')],
|
||||
'returns': 'UICollection: All UI elements in the scene',
|
||||
'raises': 'KeyError: If the specified scene doesn\'t exist',
|
||||
'example': 'ui_elements = mcrfpy.sceneUI("game")'
|
||||
},
|
||||
'keypressScene': {
|
||||
'signature': 'keypressScene(handler: callable) -> None',
|
||||
'description': 'Set the keyboard event handler for the current scene.',
|
||||
'args': [('handler', 'callable', 'Function that receives (key_name: str, is_pressed: bool)')],
|
||||
'example': '''def on_key(key, pressed):
|
||||
if key == "SPACE" and pressed:
|
||||
player.jump()
|
||||
mcrfpy.keypressScene(on_key)'''
|
||||
},
|
||||
|
||||
# Audio Functions
|
||||
'createSoundBuffer': {
|
||||
'signature': 'createSoundBuffer(filename: str) -> int',
|
||||
'description': 'Load a sound effect from a file and return its buffer ID.',
|
||||
'args': [('filename', 'str', 'Path to the sound file (WAV, OGG, FLAC)')],
|
||||
'returns': 'int: Buffer ID for use with playSound()',
|
||||
'raises': 'RuntimeError: If the file cannot be loaded',
|
||||
'example': 'jump_sound = mcrfpy.createSoundBuffer("assets/jump.wav")'
|
||||
},
|
||||
'loadMusic': {
|
||||
'signature': 'loadMusic(filename: str, loop: bool = True) -> None',
|
||||
'description': 'Load and immediately play background music from a file.',
|
||||
'args': [
|
||||
('filename', 'str', 'Path to the music file (WAV, OGG, FLAC)'),
|
||||
('loop', 'bool', 'Whether to loop the music (default: True)')
|
||||
],
|
||||
'note': 'Only one music track can play at a time. Loading new music stops the current track.',
|
||||
'example': 'mcrfpy.loadMusic("assets/background.ogg", True)'
|
||||
},
|
||||
'playSound': {
|
||||
'signature': 'playSound(buffer_id: int) -> None',
|
||||
'description': 'Play a sound effect using a previously loaded buffer.',
|
||||
'args': [('buffer_id', 'int', 'Sound buffer ID returned by createSoundBuffer()')],
|
||||
'raises': 'RuntimeError: If the buffer ID is invalid',
|
||||
'example': 'mcrfpy.playSound(jump_sound)'
|
||||
},
|
||||
'getMusicVolume': {
|
||||
'signature': 'getMusicVolume() -> int',
|
||||
'description': 'Get the current music volume level.',
|
||||
'returns': 'int: Current volume (0-100)',
|
||||
'example': 'current_volume = mcrfpy.getMusicVolume()'
|
||||
},
|
||||
'getSoundVolume': {
|
||||
'signature': 'getSoundVolume() -> int',
|
||||
'description': 'Get the current sound effects volume level.',
|
||||
'returns': 'int: Current volume (0-100)',
|
||||
'example': 'current_volume = mcrfpy.getSoundVolume()'
|
||||
},
|
||||
'setMusicVolume': {
|
||||
'signature': 'setMusicVolume(volume: int) -> None',
|
||||
'description': 'Set the global music volume.',
|
||||
'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')],
|
||||
'example': 'mcrfpy.setMusicVolume(50) # Set to 50% volume'
|
||||
},
|
||||
'setSoundVolume': {
|
||||
'signature': 'setSoundVolume(volume: int) -> None',
|
||||
'description': 'Set the global sound effects volume.',
|
||||
'args': [('volume', 'int', 'Volume level from 0 (silent) to 100 (full volume)')],
|
||||
'example': 'mcrfpy.setSoundVolume(75) # Set to 75% volume'
|
||||
},
|
||||
|
||||
# UI Utilities
|
||||
'find': {
|
||||
'signature': 'find(name: str, scene: str = None) -> UIDrawable | None',
|
||||
'description': 'Find the first UI element with the specified name.',
|
||||
'args': [
|
||||
('name', 'str', 'Exact name to search for'),
|
||||
('scene', 'str', 'Scene to search in (default: current scene)')
|
||||
],
|
||||
'returns': 'UIDrawable or None: The found element, or None if not found',
|
||||
'note': 'Searches scene UI elements and entities within grids.',
|
||||
'example': 'button = mcrfpy.find("start_button")'
|
||||
},
|
||||
'findAll': {
|
||||
'signature': 'findAll(pattern: str, scene: str = None) -> list',
|
||||
'description': 'Find all UI elements matching a name pattern.',
|
||||
'args': [
|
||||
('pattern', 'str', 'Name pattern with optional wildcards (* matches any characters)'),
|
||||
('scene', 'str', 'Scene to search in (default: current scene)')
|
||||
],
|
||||
'returns': 'list: All matching UI elements and entities',
|
||||
'example': 'enemies = mcrfpy.findAll("enemy_*")'
|
||||
},
|
||||
|
||||
# System Functions
|
||||
'exit': {
|
||||
'signature': 'exit() -> None',
|
||||
'description': 'Cleanly shut down the game engine and exit the application.',
|
||||
'note': 'This immediately closes the window and terminates the program.',
|
||||
'example': 'mcrfpy.exit()'
|
||||
},
|
||||
'getMetrics': {
|
||||
'signature': 'getMetrics() -> dict',
|
||||
'description': 'Get current performance metrics.',
|
||||
'returns': '''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''',
|
||||
'example': 'metrics = mcrfpy.getMetrics()'
|
||||
},
|
||||
'setTimer': {
|
||||
'signature': 'setTimer(name: str, handler: callable, interval: int) -> None',
|
||||
'description': 'Create or update a recurring timer.',
|
||||
'args': [
|
||||
('name', 'str', 'Unique identifier for the timer'),
|
||||
('handler', 'callable', 'Function called with (runtime: float) parameter'),
|
||||
('interval', 'int', 'Time between calls in milliseconds')
|
||||
],
|
||||
'note': 'If a timer with this name exists, it will be replaced.',
|
||||
'example': '''def update_score(runtime):
|
||||
score += 1
|
||||
mcrfpy.setTimer("score_update", update_score, 1000)'''
|
||||
},
|
||||
'delTimer': {
|
||||
'signature': 'delTimer(name: str) -> None',
|
||||
'description': 'Stop and remove a timer.',
|
||||
'args': [('name', 'str', 'Timer identifier to remove')],
|
||||
'note': 'No error is raised if the timer doesn\'t exist.',
|
||||
'example': 'mcrfpy.delTimer("score_update")'
|
||||
},
|
||||
'setScale': {
|
||||
'signature': 'setScale(multiplier: float) -> None',
|
||||
'description': 'Scale the game window size.',
|
||||
'args': [('multiplier', 'float', 'Scale factor (e.g., 2.0 for double size)')],
|
||||
'note': 'The internal resolution remains 1024x768, but the window is scaled.',
|
||||
'example': 'mcrfpy.setScale(2.0) # Double the window size'
|
||||
}
|
||||
}
|
||||
|
||||
def get_complete_property_documentation():
|
||||
"""Return complete documentation for ALL properties."""
|
||||
return {
|
||||
'Animation': {
|
||||
'property': 'str: Name of the property being animated (e.g., "x", "y", "scale")',
|
||||
'duration': 'float: Total duration of the animation in seconds',
|
||||
'elapsed_time': 'float: Time elapsed since animation started (read-only)',
|
||||
'current_value': 'float: Current interpolated value of the animation (read-only)',
|
||||
'is_running': 'bool: True if animation is currently running (read-only)',
|
||||
'is_finished': 'bool: True if animation has completed (read-only)'
|
||||
},
|
||||
'GridPoint': {
|
||||
'x': 'int: Grid x coordinate of this point',
|
||||
'y': 'int: Grid y coordinate of this point',
|
||||
'texture_index': 'int: Index of the texture/sprite to display at this point',
|
||||
'solid': 'bool: Whether this point blocks movement',
|
||||
'transparent': 'bool: Whether this point allows light/vision through',
|
||||
'color': 'Color: Color tint applied to the texture at this point'
|
||||
},
|
||||
'GridPointState': {
|
||||
'visible': 'bool: Whether this point is currently visible to the player',
|
||||
'discovered': 'bool: Whether this point has been discovered/explored',
|
||||
'custom_flags': 'int: Bitfield for custom game-specific flags'
|
||||
}
|
||||
}
|
||||
|
||||
def format_method_markdown(method_name, method_doc):
|
||||
"""Format a method as markdown."""
|
||||
lines = []
|
||||
|
||||
lines.append(f"#### `{method_doc['signature']}`")
|
||||
lines.append("")
|
||||
lines.append(method_doc['description'])
|
||||
lines.append("")
|
||||
|
||||
# Arguments
|
||||
if 'args' in method_doc:
|
||||
lines.append("**Arguments:**")
|
||||
for arg in method_doc['args']:
|
||||
lines.append(f"- `{arg[0]}` (*{arg[1]}*): {arg[2]}")
|
||||
lines.append("")
|
||||
|
||||
# Returns
|
||||
if 'returns' in method_doc:
|
||||
lines.append(f"**Returns:** {method_doc['returns']}")
|
||||
lines.append("")
|
||||
|
||||
# Raises
|
||||
if 'raises' in method_doc:
|
||||
lines.append(f"**Raises:** {method_doc['raises']}")
|
||||
lines.append("")
|
||||
|
||||
# Note
|
||||
if 'note' in method_doc:
|
||||
lines.append(f"**Note:** {method_doc['note']}")
|
||||
lines.append("")
|
||||
|
||||
# Example
|
||||
if 'example' in method_doc:
|
||||
lines.append("**Example:**")
|
||||
lines.append("```python")
|
||||
lines.append(method_doc['example'])
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
|
||||
return lines
|
||||
|
||||
def format_function_markdown(func_name, func_doc):
|
||||
"""Format a function as markdown."""
|
||||
lines = []
|
||||
|
||||
lines.append(f"### `{func_doc['signature']}`")
|
||||
lines.append("")
|
||||
lines.append(func_doc['description'])
|
||||
lines.append("")
|
||||
|
||||
# Arguments
|
||||
if 'args' in func_doc:
|
||||
lines.append("**Arguments:**")
|
||||
for arg in func_doc['args']:
|
||||
lines.append(f"- `{arg[0]}` (*{arg[1]}*): {arg[2]}")
|
||||
lines.append("")
|
||||
|
||||
# Returns
|
||||
if 'returns' in func_doc:
|
||||
lines.append(f"**Returns:** {func_doc['returns']}")
|
||||
lines.append("")
|
||||
|
||||
# Raises
|
||||
if 'raises' in func_doc:
|
||||
lines.append(f"**Raises:** {func_doc['raises']}")
|
||||
lines.append("")
|
||||
|
||||
# Note
|
||||
if 'note' in func_doc:
|
||||
lines.append(f"**Note:** {func_doc['note']}")
|
||||
lines.append("")
|
||||
|
||||
# Example
|
||||
if 'example' in func_doc:
|
||||
lines.append("**Example:**")
|
||||
lines.append("```python")
|
||||
lines.append(func_doc['example'])
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
return lines
|
||||
|
||||
def generate_complete_markdown_documentation():
|
||||
"""Generate complete markdown documentation with NO missing methods."""
|
||||
|
||||
# Get all documentation data
|
||||
method_docs = get_complete_method_documentation()
|
||||
function_docs = get_complete_function_documentation()
|
||||
property_docs = get_complete_property_documentation()
|
||||
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
lines.append("# McRogueFace API Reference")
|
||||
lines.append("")
|
||||
lines.append(f"*Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
|
||||
lines.append("")
|
||||
|
||||
# Overview
|
||||
if mcrfpy.__doc__:
|
||||
lines.append("## Overview")
|
||||
lines.append("")
|
||||
# Process the docstring properly
|
||||
doc_text = mcrfpy.__doc__.replace('\\n', '\n')
|
||||
lines.append(doc_text)
|
||||
lines.append("")
|
||||
|
||||
# Table of Contents
|
||||
lines.append("## Table of Contents")
|
||||
lines.append("")
|
||||
lines.append("- [Functions](#functions)")
|
||||
lines.append(" - [Scene Management](#scene-management)")
|
||||
lines.append(" - [Audio](#audio)")
|
||||
lines.append(" - [UI Utilities](#ui-utilities)")
|
||||
lines.append(" - [System](#system)")
|
||||
lines.append("- [Classes](#classes)")
|
||||
lines.append(" - [UI Components](#ui-components)")
|
||||
lines.append(" - [Collections](#collections)")
|
||||
lines.append(" - [System Types](#system-types)")
|
||||
lines.append(" - [Other Classes](#other-classes)")
|
||||
lines.append("- [Automation Module](#automation-module)")
|
||||
lines.append("")
|
||||
|
||||
# Functions section
|
||||
lines.append("## Functions")
|
||||
lines.append("")
|
||||
|
||||
# Group functions by category
|
||||
categories = {
|
||||
'Scene Management': ['createScene', 'setScene', 'currentScene', 'sceneUI', 'keypressScene'],
|
||||
'Audio': ['createSoundBuffer', 'loadMusic', 'playSound', 'getMusicVolume', 'getSoundVolume', 'setMusicVolume', 'setSoundVolume'],
|
||||
'UI Utilities': ['find', 'findAll'],
|
||||
'System': ['exit', 'getMetrics', 'setTimer', 'delTimer', 'setScale']
|
||||
}
|
||||
|
||||
for category, functions in categories.items():
|
||||
lines.append(f"### {category}")
|
||||
lines.append("")
|
||||
for func_name in functions:
|
||||
if func_name in function_docs:
|
||||
lines.extend(format_function_markdown(func_name, function_docs[func_name]))
|
||||
|
||||
# Classes section
|
||||
lines.append("## Classes")
|
||||
lines.append("")
|
||||
|
||||
# Get all classes from mcrfpy
|
||||
classes = []
|
||||
for name in sorted(dir(mcrfpy)):
|
||||
if not name.startswith('_'):
|
||||
obj = getattr(mcrfpy, name)
|
||||
if isinstance(obj, type):
|
||||
classes.append((name, obj))
|
||||
|
||||
# Group classes
|
||||
ui_classes = ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity']
|
||||
collection_classes = ['EntityCollection', 'UICollection', 'UICollectionIter', 'UIEntityCollectionIter']
|
||||
system_classes = ['Color', 'Vector', 'Texture', 'Font']
|
||||
other_classes = [name for name, _ in classes if name not in ui_classes + collection_classes + system_classes]
|
||||
|
||||
# UI Components
|
||||
lines.append("### UI Components")
|
||||
lines.append("")
|
||||
for class_name in ui_classes:
|
||||
if any(name == class_name for name, _ in classes):
|
||||
lines.extend(format_class_markdown(class_name, method_docs, property_docs))
|
||||
|
||||
# Collections
|
||||
lines.append("### Collections")
|
||||
lines.append("")
|
||||
for class_name in collection_classes:
|
||||
if any(name == class_name for name, _ in classes):
|
||||
lines.extend(format_class_markdown(class_name, method_docs, property_docs))
|
||||
|
||||
# System Types
|
||||
lines.append("### System Types")
|
||||
lines.append("")
|
||||
for class_name in system_classes:
|
||||
if any(name == class_name for name, _ in classes):
|
||||
lines.extend(format_class_markdown(class_name, method_docs, property_docs))
|
||||
|
||||
# Other Classes
|
||||
lines.append("### Other Classes")
|
||||
lines.append("")
|
||||
for class_name in other_classes:
|
||||
lines.extend(format_class_markdown(class_name, method_docs, property_docs))
|
||||
|
||||
# Automation section
|
||||
if hasattr(mcrfpy, 'automation'):
|
||||
lines.append("## Automation Module")
|
||||
lines.append("")
|
||||
lines.append("The `mcrfpy.automation` module provides testing and automation capabilities.")
|
||||
lines.append("")
|
||||
|
||||
automation = mcrfpy.automation
|
||||
for name in sorted(dir(automation)):
|
||||
if not name.startswith('_'):
|
||||
obj = getattr(automation, name)
|
||||
if callable(obj):
|
||||
lines.append(f"### `automation.{name}`")
|
||||
lines.append("")
|
||||
if obj.__doc__:
|
||||
doc_parts = obj.__doc__.split(' - ')
|
||||
if len(doc_parts) > 1:
|
||||
lines.append(doc_parts[1])
|
||||
else:
|
||||
lines.append(obj.__doc__)
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def format_class_markdown(class_name, method_docs, property_docs):
|
||||
"""Format a class as markdown."""
|
||||
lines = []
|
||||
|
||||
lines.append(f"### class `{class_name}`")
|
||||
lines.append("")
|
||||
|
||||
# Class description from known info
|
||||
class_descriptions = {
|
||||
'Frame': 'A rectangular frame UI element that can contain other drawable elements.',
|
||||
'Caption': 'A text display UI element with customizable font and styling.',
|
||||
'Sprite': 'A sprite UI element that displays a texture or portion of a texture atlas.',
|
||||
'Grid': 'A grid-based tilemap UI element for rendering tile-based levels and game worlds.',
|
||||
'Entity': 'Game entity that can be placed in a Grid.',
|
||||
'EntityCollection': 'Container for Entity objects in a Grid. Supports iteration and indexing.',
|
||||
'UICollection': 'Container for UI drawable elements. Supports iteration and indexing.',
|
||||
'UICollectionIter': 'Iterator for UICollection. Automatically created when iterating over a UICollection.',
|
||||
'UIEntityCollectionIter': 'Iterator for EntityCollection. Automatically created when iterating over an EntityCollection.',
|
||||
'Color': 'RGBA color representation.',
|
||||
'Vector': '2D vector for positions and directions.',
|
||||
'Font': 'Font object for text rendering.',
|
||||
'Texture': 'Texture object for image data.',
|
||||
'Animation': 'Animate UI element properties over time.',
|
||||
'GridPoint': 'Represents a single tile in a Grid.',
|
||||
'GridPointState': 'State information for a GridPoint.',
|
||||
'Scene': 'Base class for object-oriented scenes.',
|
||||
'Timer': 'Timer object for scheduled callbacks.',
|
||||
'Window': 'Window singleton for accessing and modifying the game window properties.',
|
||||
'Drawable': 'Base class for all drawable UI elements.'
|
||||
}
|
||||
|
||||
if class_name in class_descriptions:
|
||||
lines.append(class_descriptions[class_name])
|
||||
lines.append("")
|
||||
|
||||
# Properties
|
||||
if class_name in property_docs:
|
||||
lines.append("#### Properties")
|
||||
lines.append("")
|
||||
for prop_name, prop_desc in property_docs[class_name].items():
|
||||
lines.append(f"- **`{prop_name}`**: {prop_desc}")
|
||||
lines.append("")
|
||||
|
||||
# Methods
|
||||
methods_to_document = []
|
||||
|
||||
# Add inherited methods for UI classes
|
||||
if class_name in ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity']:
|
||||
methods_to_document.extend(['get_bounds', 'move', 'resize'])
|
||||
|
||||
# Add class-specific methods
|
||||
if class_name in method_docs:
|
||||
methods_to_document.extend(method_docs[class_name].keys())
|
||||
|
||||
if methods_to_document:
|
||||
lines.append("#### Methods")
|
||||
lines.append("")
|
||||
for method_name in set(methods_to_document):
|
||||
# Get method documentation
|
||||
method_doc = None
|
||||
if class_name in method_docs and method_name in method_docs[class_name]:
|
||||
method_doc = method_docs[class_name][method_name]
|
||||
elif method_name in method_docs.get('Drawable', {}):
|
||||
method_doc = method_docs['Drawable'][method_name]
|
||||
|
||||
if method_doc:
|
||||
lines.extend(format_method_markdown(method_name, method_doc))
|
||||
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
return lines
|
||||
|
||||
def main():
|
||||
"""Generate complete markdown documentation with zero missing methods."""
|
||||
print("Generating COMPLETE Markdown API documentation...")
|
||||
|
||||
# Generate markdown
|
||||
markdown_content = generate_complete_markdown_documentation()
|
||||
|
||||
# Write to file
|
||||
output_path = Path("docs/API_REFERENCE_COMPLETE.md")
|
||||
output_path.parent.mkdir(exist_ok=True)
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
print(f"✓ Generated {output_path}")
|
||||
print(f" File size: {len(markdown_content):,} bytes")
|
||||
|
||||
# Count "..." instances
|
||||
ellipsis_count = markdown_content.count('...')
|
||||
print(f" Ellipsis instances: {ellipsis_count}")
|
||||
|
||||
if ellipsis_count == 0:
|
||||
print("✅ SUCCESS: No missing documentation found!")
|
||||
else:
|
||||
print(f"❌ WARNING: {ellipsis_count} methods still need documentation")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -12,67 +12,6 @@ import html
|
|||
import re
|
||||
from pathlib import Path
|
||||
|
||||
def transform_doc_links(docstring, format='html', base_url=''):
|
||||
"""Transform MCRF_LINK patterns based on output format.
|
||||
|
||||
Detects pattern: "See also: TEXT (docs/path.md)"
|
||||
Transforms to appropriate format for output type.
|
||||
|
||||
For HTML/web formats, properly escapes content before inserting HTML tags.
|
||||
"""
|
||||
if not docstring:
|
||||
return docstring
|
||||
|
||||
link_pattern = r'See also: ([^(]+) \(([^)]+)\)'
|
||||
|
||||
def replace_link(match):
|
||||
text, ref = match.group(1).strip(), match.group(2).strip()
|
||||
|
||||
if format == 'html':
|
||||
# Convert docs/foo.md → foo.html and escape for safe HTML
|
||||
href = html.escape(ref.replace('docs/', '').replace('.md', '.html'), quote=True)
|
||||
text_escaped = html.escape(text)
|
||||
return f'<p class="see-also">See also: <a href="{href}">{text_escaped}</a></p>'
|
||||
|
||||
elif format == 'web':
|
||||
# Link to hosted docs and escape for safe HTML
|
||||
web_path = ref.replace('docs/', '').replace('.md', '')
|
||||
href = html.escape(f"{base_url}/{web_path}", quote=True)
|
||||
text_escaped = html.escape(text)
|
||||
return f'<p class="see-also">See also: <a href="{href}">{text_escaped}</a></p>'
|
||||
|
||||
elif format == 'markdown':
|
||||
# Markdown link
|
||||
return f'\n**See also:** [{text}]({ref})'
|
||||
|
||||
else: # 'python' or default
|
||||
# Keep as plain text for Python docstrings
|
||||
return match.group(0)
|
||||
|
||||
# For HTML formats, escape the entire docstring first, then process links
|
||||
if format in ('html', 'web'):
|
||||
# Split by the link pattern, escape non-link parts, then reassemble
|
||||
parts = []
|
||||
last_end = 0
|
||||
|
||||
for match in re.finditer(link_pattern, docstring):
|
||||
# Escape the text before this match
|
||||
if match.start() > last_end:
|
||||
parts.append(html.escape(docstring[last_end:match.start()]))
|
||||
|
||||
# Process the link (replace_link handles escaping internally)
|
||||
parts.append(replace_link(match))
|
||||
last_end = match.end()
|
||||
|
||||
# Escape any remaining text after the last match
|
||||
if last_end < len(docstring):
|
||||
parts.append(html.escape(docstring[last_end:]))
|
||||
|
||||
return ''.join(parts)
|
||||
else:
|
||||
# For non-HTML formats, just do simple replacement
|
||||
return re.sub(link_pattern, replace_link, docstring)
|
||||
|
||||
# Must be run with McRogueFace as interpreter
|
||||
try:
|
||||
import mcrfpy
|
||||
|
|
@ -82,90 +21,67 @@ except ImportError:
|
|||
sys.exit(1)
|
||||
|
||||
def parse_docstring(docstring):
|
||||
"""Parse a docstring to extract signature, description, args, returns, and raises."""
|
||||
"""Parse a docstring to extract signature, description, args, and returns."""
|
||||
if not docstring:
|
||||
return {"signature": "", "description": "", "args": [], "returns": "", "raises": "", "example": ""}
|
||||
|
||||
return {"signature": "", "description": "", "args": [], "returns": "", "example": ""}
|
||||
|
||||
lines = docstring.strip().split('\n')
|
||||
result = {
|
||||
"signature": "",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"returns": "",
|
||||
"raises": "",
|
||||
"example": ""
|
||||
}
|
||||
|
||||
|
||||
# First line often contains the signature
|
||||
if lines and '(' in lines[0] and ')' in lines[0]:
|
||||
result["signature"] = lines[0].strip()
|
||||
lines = lines[1:] if len(lines) > 1 else []
|
||||
|
||||
|
||||
# Parse the rest
|
||||
current_section = "description"
|
||||
description_lines = []
|
||||
returns_lines = []
|
||||
raises_lines = []
|
||||
example_lines = []
|
||||
|
||||
in_example = False
|
||||
|
||||
for line in lines:
|
||||
line_lower = line.strip().lower()
|
||||
|
||||
# Detect section headers
|
||||
|
||||
if line_lower.startswith("args:") or line_lower.startswith("arguments:"):
|
||||
current_section = "args"
|
||||
continue
|
||||
elif line_lower.startswith("returns:") or line_lower.startswith("return:"):
|
||||
current_section = "returns"
|
||||
# Capture any text on the same line as "Returns:"
|
||||
content_after_colon = line[line.find(':')+1:].strip()
|
||||
if content_after_colon:
|
||||
returns_lines.append(content_after_colon)
|
||||
continue
|
||||
elif line_lower.startswith("raises:") or line_lower.startswith("raise:"):
|
||||
current_section = "raises"
|
||||
# Capture any text on the same line as "Raises:"
|
||||
content_after_colon = line[line.find(':')+1:].strip()
|
||||
if content_after_colon:
|
||||
raises_lines.append(content_after_colon)
|
||||
result["returns"] = line[line.find(':')+1:].strip()
|
||||
continue
|
||||
elif line_lower.startswith("example:") or line_lower.startswith("examples:"):
|
||||
current_section = "example"
|
||||
in_example = True
|
||||
continue
|
||||
elif line_lower.startswith("note:"):
|
||||
# Notes go into description
|
||||
if description_lines:
|
||||
description_lines.append("")
|
||||
description_lines.append(line)
|
||||
continue
|
||||
|
||||
# Skip blank lines unless we're in example section
|
||||
if not line.strip() and current_section != "example":
|
||||
continue
|
||||
|
||||
# Add content to appropriate section
|
||||
if current_section == "description":
|
||||
|
||||
if in_example:
|
||||
example_lines.append(line)
|
||||
elif current_section == "description" and not line.startswith(" "):
|
||||
description_lines.append(line)
|
||||
elif current_section == "args" and line.strip():
|
||||
# Parse argument lines like " filename: Path to file"
|
||||
# Parse argument lines like " x: X coordinate"
|
||||
match = re.match(r'\s+(\w+):\s*(.+)', line)
|
||||
if match:
|
||||
result["args"].append({
|
||||
"name": match.group(1),
|
||||
"description": match.group(2).strip()
|
||||
})
|
||||
elif current_section == "returns" and line.strip():
|
||||
returns_lines.append(line.strip())
|
||||
elif current_section == "raises" and line.strip():
|
||||
raises_lines.append(line.strip())
|
||||
elif current_section == "example":
|
||||
example_lines.append(line)
|
||||
|
||||
elif current_section == "returns" and line.strip() and line.startswith(" "):
|
||||
result["returns"] += " " + line.strip()
|
||||
|
||||
result["description"] = '\n'.join(description_lines).strip()
|
||||
result["returns"] = ' '.join(returns_lines).strip()
|
||||
result["raises"] = ' '.join(raises_lines).strip()
|
||||
result["example"] = '\n'.join(example_lines).strip()
|
||||
|
||||
|
||||
return result
|
||||
|
||||
def get_all_functions():
|
||||
|
|
@ -384,32 +300,25 @@ def generate_html_docs():
|
|||
for func_name in sorted(functions.keys()):
|
||||
func_info = functions[func_name]
|
||||
parsed = func_info["parsed"]
|
||||
|
||||
# Use signature if available (already includes name), otherwise use just name
|
||||
heading = parsed['signature'] if parsed['signature'] else f"{func_name}(...)"
|
||||
|
||||
html_content += f"""
|
||||
<div class="method-section">
|
||||
<h3><code class="function-signature">{heading}</code></h3>
|
||||
<h3><code class="function-signature">{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}</code></h3>
|
||||
<p>{html.escape(parsed['description'])}</p>
|
||||
"""
|
||||
if parsed['description']:
|
||||
description = transform_doc_links(parsed['description'], format='html')
|
||||
html_content += f" <p>{description}</p>\n"
|
||||
|
||||
|
||||
if parsed['args']:
|
||||
html_content += " <h4>Arguments:</h4>\n <ul>\n"
|
||||
for arg in parsed['args']:
|
||||
html_content += f" <li><span class='arg-name'>{arg['name']}</span>: {html.escape(arg['description'])}</li>\n"
|
||||
html_content += " </ul>\n"
|
||||
|
||||
|
||||
if parsed['returns']:
|
||||
html_content += f" <p><span class='returns'>Returns:</span> {html.escape(parsed['returns'])}</p>\n"
|
||||
|
||||
if parsed['raises']:
|
||||
html_content += f" <p><span class='raises'>Raises:</span> {html.escape(parsed['raises'])}</p>\n"
|
||||
|
||||
|
||||
if parsed['example']:
|
||||
html_content += f" <h4>Example:</h4>\n <pre><code>{html.escape(parsed['example'])}</code></pre>\n"
|
||||
|
||||
|
||||
html_content += " </div>\n"
|
||||
|
||||
# Generate class documentation
|
||||
|
|
@ -447,30 +356,24 @@ def generate_html_docs():
|
|||
if method_name == '__init__':
|
||||
continue
|
||||
parsed = method_info['parsed']
|
||||
|
||||
# Use signature if available (already includes name), otherwise use just name
|
||||
heading = parsed['signature'] if parsed['signature'] else f"{method_name}(...)"
|
||||
|
||||
html_content += f"""
|
||||
<div style="margin-left: 20px; margin-bottom: 15px;">
|
||||
<h5><code class="method-name">{heading}</code></h5>
|
||||
<h5><code class="method-name">{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}</code></h5>
|
||||
"""
|
||||
|
||||
|
||||
if parsed['description']:
|
||||
description = transform_doc_links(parsed['description'], format='html')
|
||||
html_content += f" <p>{description}</p>\n"
|
||||
|
||||
html_content += f" <p>{html.escape(parsed['description'])}</p>\n"
|
||||
|
||||
if parsed['args']:
|
||||
html_content += " <div style='margin-left: 20px;'>\n"
|
||||
for arg in parsed['args']:
|
||||
html_content += f" <div><span class='arg-name'>{arg['name']}</span>: {html.escape(arg['description'])}</div>\n"
|
||||
html_content += " </div>\n"
|
||||
|
||||
|
||||
if parsed['returns']:
|
||||
html_content += f" <p style='margin-left: 20px;'><span class='returns'>Returns:</span> {html.escape(parsed['returns'])}</p>\n"
|
||||
|
||||
if parsed['raises']:
|
||||
html_content += f" <p style='margin-left: 20px;'><span class='raises'>Raises:</span> {html.escape(parsed['raises'])}</p>\n"
|
||||
|
||||
|
||||
html_content += " </div>\n"
|
||||
|
||||
html_content += " </div>\n"
|
||||
|
|
@ -524,27 +427,21 @@ def generate_markdown_docs():
|
|||
for func_name in sorted(functions.keys()):
|
||||
func_info = functions[func_name]
|
||||
parsed = func_info["parsed"]
|
||||
|
||||
# Use signature if available (already includes name), otherwise use just name
|
||||
heading = parsed['signature'] if parsed['signature'] else f"{func_name}(...)"
|
||||
md_content += f"### `{heading}`\n\n"
|
||||
|
||||
|
||||
md_content += f"### `{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n"
|
||||
|
||||
if parsed['description']:
|
||||
description = transform_doc_links(parsed['description'], format='markdown')
|
||||
md_content += f"{description}\n\n"
|
||||
|
||||
md_content += f"{parsed['description']}\n\n"
|
||||
|
||||
if parsed['args']:
|
||||
md_content += "**Arguments:**\n"
|
||||
for arg in parsed['args']:
|
||||
md_content += f"- `{arg['name']}`: {arg['description']}\n"
|
||||
md_content += "\n"
|
||||
|
||||
|
||||
if parsed['returns']:
|
||||
md_content += f"**Returns:** {parsed['returns']}\n\n"
|
||||
|
||||
if parsed['raises']:
|
||||
md_content += f"**Raises:** {parsed['raises']}\n\n"
|
||||
|
||||
|
||||
if parsed['example']:
|
||||
md_content += f"**Example:**\n```python\n{parsed['example']}\n```\n\n"
|
||||
|
||||
|
|
@ -580,26 +477,20 @@ def generate_markdown_docs():
|
|||
if method_name == '__init__':
|
||||
continue
|
||||
parsed = method_info['parsed']
|
||||
|
||||
# Use signature if available (already includes name), otherwise use just name
|
||||
heading = parsed['signature'] if parsed['signature'] else f"{method_name}(...)"
|
||||
md_content += f"#### `{heading}`\n\n"
|
||||
|
||||
|
||||
md_content += f"#### `{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n"
|
||||
|
||||
if parsed['description']:
|
||||
description = transform_doc_links(parsed['description'], format='markdown')
|
||||
md_content += f"{description}\n\n"
|
||||
|
||||
md_content += f"{parsed['description']}\n\n"
|
||||
|
||||
if parsed['args']:
|
||||
md_content += "**Arguments:**\n"
|
||||
for arg in parsed['args']:
|
||||
md_content += f"- `{arg['name']}`: {arg['description']}\n"
|
||||
md_content += "\n"
|
||||
|
||||
|
||||
if parsed['returns']:
|
||||
md_content += f"**Returns:** {parsed['returns']}\n\n"
|
||||
|
||||
if parsed['raises']:
|
||||
md_content += f"**Raises:** {parsed['raises']}\n\n"
|
||||
|
||||
# Constants
|
||||
md_content += "## Constants\n\n"
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Convert markdown docs to man page format
|
||||
|
||||
pandoc docs/API_REFERENCE_DYNAMIC.md \
|
||||
-s -t man \
|
||||
--metadata title="MCRFPY" \
|
||||
--metadata section=3 \
|
||||
--metadata date="$(date +%Y-%m-%d)" \
|
||||
--metadata footer="McRogueFace $(git describe --tags 2>/dev/null || echo 'dev')" \
|
||||
-o docs/mcrfpy.3
|
||||
|
||||
echo "Generated docs/mcrfpy.3"
|
||||
|
|
@ -8,7 +8,7 @@ from time import time
|
|||
print("Fetching issues...", end='')
|
||||
start = time()
|
||||
from gitea import Gitea, Repository, Issue
|
||||
g = Gitea("https://gamedev.ffwf.net/gitea", token_text="febad52bd50f87fb17691c5e972597d6fff73452")
|
||||
g = Gitea("https://gamedev.ffwf.net/gitea", token_text="3b450f66e21d62c22bb9fa1c8b975049a5d0c38d")
|
||||
repo = Repository.request(g, "john", "McRogueFace")
|
||||
issues = repo.get_issues()
|
||||
dur = time() - start
|
||||
|
|
@ -99,4 +99,4 @@ nx.draw_networkx_edges(relations, pos,
|
|||
edgelist = relations.edges()
|
||||
)
|
||||
nx.draw_networkx_labels(relations, pos, {i: str(i) for i in relations.nodes()})
|
||||
plt.show()
|
||||
plt.show()
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test script for link transformation function."""
|
||||
|
||||
import re
|
||||
|
||||
def transform_doc_links(docstring, format='html', base_url=''):
|
||||
"""Transform MCRF_LINK patterns based on output format.
|
||||
|
||||
Detects pattern: "See also: TEXT (docs/path.md)"
|
||||
Transforms to appropriate format for output type.
|
||||
"""
|
||||
if not docstring:
|
||||
return docstring
|
||||
|
||||
link_pattern = r'See also: ([^(]+) \(([^)]+)\)'
|
||||
|
||||
def replace_link(match):
|
||||
text, ref = match.group(1).strip(), match.group(2).strip()
|
||||
|
||||
if format == 'html':
|
||||
# Convert docs/foo.md → foo.html
|
||||
href = ref.replace('docs/', '').replace('.md', '.html')
|
||||
return f'<p class="see-also">See also: <a href="{href}">{text}</a></p>'
|
||||
|
||||
elif format == 'web':
|
||||
# Link to hosted docs
|
||||
web_path = ref.replace('docs/', '').replace('.md', '')
|
||||
return f'<p class="see-also">See also: <a href="{base_url}/{web_path}">{text}</a></p>'
|
||||
|
||||
elif format == 'markdown':
|
||||
# Markdown link
|
||||
return f'\n**See also:** [{text}]({ref})'
|
||||
|
||||
else: # 'python' or default
|
||||
# Keep as plain text for Python docstrings
|
||||
return match.group(0)
|
||||
|
||||
return re.sub(link_pattern, replace_link, docstring)
|
||||
|
||||
# Test cases
|
||||
test_doc = "Description text.\n\nSee also: Tutorial Guide (docs/guide.md)\n\nMore text."
|
||||
|
||||
html_result = transform_doc_links(test_doc, format='html')
|
||||
print("HTML:", html_result)
|
||||
assert '<a href="guide.html">Tutorial Guide</a>' in html_result
|
||||
|
||||
md_result = transform_doc_links(test_doc, format='markdown')
|
||||
print("Markdown:", md_result)
|
||||
assert '[Tutorial Guide](docs/guide.md)' in md_result
|
||||
|
||||
plain_result = transform_doc_links(test_doc, format='python')
|
||||
print("Python:", plain_result)
|
||||
assert 'See also: Tutorial Guide (docs/guide.md)' in plain_result
|
||||
|
||||
print("\nSUCCESS: All transformations work correctly")
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Check Vector.magnitude docstring
|
||||
mag_doc = mcrfpy.Vector.magnitude.__doc__
|
||||
print("magnitude doc:", mag_doc)
|
||||
assert "magnitude()" in mag_doc
|
||||
assert "Calculate the length/magnitude" in mag_doc
|
||||
assert "Returns:" in mag_doc
|
||||
|
||||
# Check Vector.dot docstring
|
||||
dot_doc = mcrfpy.Vector.dot.__doc__
|
||||
print("dot doc:", dot_doc)
|
||||
assert "dot(other: Vector)" in dot_doc
|
||||
assert "Args:" in dot_doc
|
||||
assert "other:" in dot_doc
|
||||
|
||||
# Check Vector.normalize docstring
|
||||
normalize_doc = mcrfpy.Vector.normalize.__doc__
|
||||
print("normalize doc:", normalize_doc)
|
||||
assert "normalize()" in normalize_doc
|
||||
assert "Return a unit vector" in normalize_doc
|
||||
assert "Returns:" in normalize_doc
|
||||
assert "Note:" in normalize_doc
|
||||
|
||||
# Check Vector.x property docstring
|
||||
x_doc = mcrfpy.Vector.x.__doc__
|
||||
print("x property doc:", x_doc)
|
||||
assert "X coordinate of the vector" in x_doc
|
||||
assert "float" in x_doc
|
||||
|
||||
# Check Vector.y property docstring
|
||||
y_doc = mcrfpy.Vector.y.__doc__
|
||||
print("y property doc:", y_doc)
|
||||
assert "Y coordinate of the vector" in y_doc
|
||||
assert "float" in y_doc
|
||||
|
||||
print("SUCCESS: All docstrings present and complete")
|
||||
sys.exit(0)
|
||||
Loading…
Reference in New Issue