Compare commits
12 Commits
master
...
alpha_pres
| Author | SHA1 | Date |
|---|---|---|
|
|
c5e7e8e298 | |
|
|
6d29652ae7 | |
|
|
a010e5fa96 | |
|
|
9c8d6c4591 | |
|
|
dcd1b0ca33 | |
|
|
6813fb5129 | |
|
|
6f67fbb51e | |
|
|
eb88c7b3aa | |
|
|
9fb428dd01 | |
|
|
bde82028b5 | |
|
|
062e4dadc4 | |
|
|
98fc49a978 |
|
|
@ -8,26 +8,26 @@ PCbuild
|
|||
obj
|
||||
build
|
||||
lib
|
||||
obj
|
||||
__pycache__
|
||||
|
||||
.cache/
|
||||
7DRL2025 Release/
|
||||
CMakeFiles/
|
||||
Makefile
|
||||
*.md
|
||||
*.zip
|
||||
__lib/
|
||||
_oldscripts/
|
||||
assets/
|
||||
cellular_automata_fire/
|
||||
*.txt
|
||||
deps/
|
||||
fetch_issues_txt.py
|
||||
forest_fire_CA.py
|
||||
mcrogueface.github.io
|
||||
scripts/
|
||||
test_*
|
||||
|
||||
tcod_reference
|
||||
.archive
|
||||
|
||||
# Keep important documentation and tests
|
||||
!CLAUDE.md
|
||||
!README.md
|
||||
!tests/
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
[submodule "modules/SFML"]
|
||||
path = modules/SFML
|
||||
url = git@github.com:SFML/SFML.git
|
||||
[submodule "modules/libtcod-headless"]
|
||||
path = modules/libtcod-headless
|
||||
url = git@github.com:jmccardle/libtcod-headless.git
|
||||
branch = 2.2.1-headless
|
||||
[submodule "modules/libtcod"]
|
||||
path = modules/libtcod
|
||||
url = git@github.com:libtcod/libtcod.git
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
# Building McRogueFace from Source
|
||||
|
||||
This document describes how to build McRogueFace from a fresh clone.
|
||||
|
||||
## Build Options
|
||||
|
||||
There are two ways to build McRogueFace:
|
||||
|
||||
1. **Quick Build** (recommended): Use pre-built dependency libraries from a `build_deps` archive
|
||||
2. **Full Build**: Compile all dependencies from submodules
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### System Dependencies
|
||||
|
||||
Install these packages before building:
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install \
|
||||
build-essential \
|
||||
cmake \
|
||||
git \
|
||||
zlib1g-dev \
|
||||
libx11-dev \
|
||||
libxrandr-dev \
|
||||
libxcursor-dev \
|
||||
libfreetype-dev \
|
||||
libudev-dev \
|
||||
libvorbis-dev \
|
||||
libflac-dev \
|
||||
libgl-dev \
|
||||
libopenal-dev
|
||||
```
|
||||
|
||||
**Note:** SDL is NOT required - McRogueFace uses libtcod-headless which has no SDL dependency.
|
||||
|
||||
---
|
||||
|
||||
## Option 1: Quick Build (Using Pre-built Dependencies)
|
||||
|
||||
If you have a `build_deps.tar.gz` or `build_deps.zip` archive:
|
||||
|
||||
```bash
|
||||
# Clone McRogueFace (no submodules needed)
|
||||
git clone <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
|
||||
# Extract pre-built dependencies
|
||||
tar -xzf /path/to/build_deps.tar.gz
|
||||
# Or for zip: unzip /path/to/build_deps.zip
|
||||
|
||||
# Build McRogueFace
|
||||
mkdir -p build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# Run
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
The `build_deps` archive contains:
|
||||
- `__lib/` - Pre-built shared libraries (Python, SFML, libtcod-headless)
|
||||
- `deps/` - Header symlinks for compilation
|
||||
|
||||
**Total build time: ~30 seconds**
|
||||
|
||||
---
|
||||
|
||||
## Option 2: Full Build (Compiling All Dependencies)
|
||||
|
||||
### 1. Clone with Submodules
|
||||
|
||||
```bash
|
||||
git clone --recursive <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
```
|
||||
|
||||
If submodules weren't cloned:
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
**Note:** imgui/imgui-sfml submodules may fail - this is fine, they're not used.
|
||||
|
||||
### 2. Create Dependency Symlinks
|
||||
|
||||
```bash
|
||||
cd deps
|
||||
ln -sf ../modules/cpython cpython
|
||||
ln -sf ../modules/libtcod-headless/src/libtcod libtcod
|
||||
ln -sf ../modules/cpython/Include Python
|
||||
ln -sf ../modules/SFML/include/SFML SFML
|
||||
cd ..
|
||||
```
|
||||
|
||||
### 3. Build libtcod-headless
|
||||
|
||||
libtcod-headless is our SDL-free fork with vendored dependencies:
|
||||
|
||||
```bash
|
||||
cd modules/libtcod-headless
|
||||
mkdir build && cd build
|
||||
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=ON
|
||||
|
||||
make -j$(nproc)
|
||||
cd ../../..
|
||||
```
|
||||
|
||||
That's it! No special flags needed - libtcod-headless defaults to:
|
||||
- `LIBTCOD_SDL3=disable` (no SDL dependency)
|
||||
- Vendored lodepng, utf8proc, stb
|
||||
|
||||
### 4. Build Python 3.12
|
||||
|
||||
```bash
|
||||
cd modules/cpython
|
||||
./configure --enable-shared
|
||||
make -j$(nproc)
|
||||
cd ../..
|
||||
```
|
||||
|
||||
### 5. Build SFML 2.6
|
||||
|
||||
```bash
|
||||
cd modules/SFML
|
||||
mkdir build && cd build
|
||||
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=ON
|
||||
|
||||
make -j$(nproc)
|
||||
cd ../../..
|
||||
```
|
||||
|
||||
### 6. Copy Libraries
|
||||
|
||||
```bash
|
||||
mkdir -p __lib
|
||||
|
||||
# Python
|
||||
cp modules/cpython/libpython3.12.so* __lib/
|
||||
|
||||
# SFML
|
||||
cp modules/SFML/build/lib/libsfml-*.so* __lib/
|
||||
|
||||
# libtcod-headless
|
||||
cp modules/libtcod-headless/build/bin/libtcod.so* __lib/
|
||||
|
||||
# Python standard library
|
||||
cp -r modules/cpython/Lib __lib/Python
|
||||
```
|
||||
|
||||
### 7. Build McRogueFace
|
||||
|
||||
```bash
|
||||
mkdir -p build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
```
|
||||
|
||||
### 8. Run
|
||||
|
||||
```bash
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Submodule Versions
|
||||
|
||||
| Submodule | Version | Notes |
|
||||
|-----------|---------|-------|
|
||||
| SFML | 2.6.1 | Graphics, audio, windowing |
|
||||
| cpython | 3.12.2 | Embedded Python interpreter |
|
||||
| libtcod-headless | 2.2.1 | SDL-free fork for FOV, pathfinding |
|
||||
|
||||
---
|
||||
|
||||
## Creating a build_deps Archive
|
||||
|
||||
To create a `build_deps` archive for distribution:
|
||||
|
||||
```bash
|
||||
cd McRogueFace
|
||||
|
||||
# Create archive directory
|
||||
mkdir -p build_deps_staging
|
||||
|
||||
# Copy libraries
|
||||
cp -r __lib build_deps_staging/
|
||||
|
||||
# Copy/create deps symlinks as actual directories with only needed headers
|
||||
mkdir -p build_deps_staging/deps
|
||||
cp -rL deps/libtcod build_deps_staging/deps/ # Follow symlink
|
||||
cp -rL deps/Python build_deps_staging/deps/
|
||||
cp -rL deps/SFML build_deps_staging/deps/
|
||||
cp -r deps/platform build_deps_staging/deps/
|
||||
|
||||
# Create archives
|
||||
cd build_deps_staging
|
||||
tar -czf ../build_deps.tar.gz __lib deps
|
||||
zip -r ../build_deps.zip __lib deps
|
||||
cd ..
|
||||
|
||||
# Cleanup
|
||||
rm -rf build_deps_staging
|
||||
```
|
||||
|
||||
The resulting archive can be distributed alongside releases for users who want to build McRogueFace without compiling dependencies.
|
||||
|
||||
**Archive contents:**
|
||||
```
|
||||
build_deps.tar.gz
|
||||
├── __lib/
|
||||
│ ├── libpython3.12.so*
|
||||
│ ├── libsfml-*.so*
|
||||
│ ├── libtcod.so*
|
||||
│ └── Python/ # Python standard library
|
||||
└── deps/
|
||||
├── libtcod/ # libtcod headers
|
||||
├── Python/ # Python headers
|
||||
├── SFML/ # SFML headers
|
||||
└── platform/ # Platform-specific configs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verify the Build
|
||||
|
||||
```bash
|
||||
cd build
|
||||
|
||||
# Check version
|
||||
./mcrogueface --version
|
||||
|
||||
# Test headless mode
|
||||
./mcrogueface --headless -c "import mcrfpy; print('Success')"
|
||||
|
||||
# Verify no SDL dependencies
|
||||
ldd mcrogueface | grep -i sdl # Should output nothing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### OpenAL not found
|
||||
```bash
|
||||
sudo apt install libopenal-dev
|
||||
```
|
||||
|
||||
### FreeType not found
|
||||
```bash
|
||||
sudo apt install libfreetype-dev
|
||||
```
|
||||
|
||||
### X11/Xrandr not found
|
||||
```bash
|
||||
sudo apt install libx11-dev libxrandr-dev
|
||||
```
|
||||
|
||||
### Python standard library missing
|
||||
Ensure `__lib/Python` contains the standard library:
|
||||
```bash
|
||||
ls __lib/Python/os.py # Should exist
|
||||
```
|
||||
|
||||
### libtcod symbols not found
|
||||
Ensure libtcod.so is in `__lib/` with correct version:
|
||||
```bash
|
||||
ls -la __lib/libtcod.so*
|
||||
# Should show libtcod.so -> libtcod.so.2 -> libtcod.so.2.2.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Times (approximate)
|
||||
|
||||
On a typical 4-core system:
|
||||
|
||||
| Component | Time |
|
||||
|-----------|------|
|
||||
| libtcod-headless | ~30 seconds |
|
||||
| Python 3.12 | ~3-5 minutes |
|
||||
| SFML 2.6 | ~1 minute |
|
||||
| McRogueFace | ~30 seconds |
|
||||
| **Full build total** | **~5-7 minutes** |
|
||||
| **Quick build (pre-built deps)** | **~30 seconds** |
|
||||
|
||||
---
|
||||
|
||||
## Runtime Dependencies
|
||||
|
||||
The built executable requires these system libraries:
|
||||
- `libz.so.1` (zlib)
|
||||
- `libopenal.so.1` (OpenAL)
|
||||
- `libX11.so.6`, `libXrandr.so.2` (X11)
|
||||
- `libfreetype.so.6` (FreeType)
|
||||
- `libGL.so.1` (OpenGL)
|
||||
|
||||
All other dependencies (Python, SFML, libtcod) are bundled in `lib/`.
|
||||
628
CLAUDE.md
628
CLAUDE.md
|
|
@ -1,628 +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
|
||||
- Before implementing: Read relevant wiki pages per the [Development Workflow](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Development-Workflow) consultation table
|
||||
- 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: Note it for wiki update
|
||||
- When documentation misleads you: Note it for wiki correction
|
||||
- After committing code changes: Update relevant wiki pages (with user permission)
|
||||
- Follow the [Development Workflow](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Development-Workflow) for wiki update procedures
|
||||
|
||||
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`
|
||||
|
||||
**⚠️ CRITICAL BUG**: The Gitea MCP tool (v0.07) has a label application bug documented in `GITEA_MCP_LABEL_BUG_REPORT.md`:
|
||||
- `add_issue_labels` and `replace_issue_labels` behave inconsistently
|
||||
- Single ID arrays produce different results than multi-ID arrays for the SAME IDs
|
||||
- Label IDs do not map reliably to actual labels
|
||||
|
||||
**Workaround Options:**
|
||||
1. **Best**: Apply labels manually via web interface: `https://gamedev.ffwf.net/gitea/john/McRogueFace/issues/<number>`
|
||||
2. **Automated**: Apply labels ONE AT A TIME using single-element arrays (slower but more reliable)
|
||||
3. **Use single-ID mapping** (documented below)
|
||||
|
||||
**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
|
||||
|
||||
### Test Suite Structure
|
||||
|
||||
The `tests/` directory contains the comprehensive test suite:
|
||||
|
||||
```
|
||||
tests/
|
||||
├── run_tests.py # Test runner - executes all tests with timeout
|
||||
├── unit/ # Unit tests for individual components (105+ tests)
|
||||
├── integration/ # Integration tests for system interactions
|
||||
├── regression/ # Bug regression tests (issue_XX_*.py)
|
||||
├── benchmarks/ # Performance benchmarks
|
||||
├── demo/ # Feature demonstration system
|
||||
│ ├── demo_main.py # Interactive demo runner
|
||||
│ ├── screens/ # Per-feature demo screens
|
||||
│ └── screenshots/ # Generated demo screenshots
|
||||
└── notes/ # Analysis files and documentation
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run the full test suite (from tests/ directory)
|
||||
cd tests && python3 run_tests.py
|
||||
|
||||
# Run a specific test
|
||||
cd build && ./mcrogueface --headless --exec ../tests/unit/some_test.py
|
||||
|
||||
# Run the demo system interactively
|
||||
cd build && ./mcrogueface ../tests/demo/demo_main.py
|
||||
|
||||
# Generate demo screenshots (headless)
|
||||
cd build && ./mcrogueface --headless --exec ../tests/demo/demo_main.py
|
||||
```
|
||||
|
||||
### Reading Tests as Examples
|
||||
|
||||
**IMPORTANT**: Before implementing a feature or fixing a bug, check existing tests for API usage examples:
|
||||
|
||||
- `tests/unit/` - Shows correct usage of individual mcrfpy classes and functions
|
||||
- `tests/demo/screens/` - Complete working examples of UI components
|
||||
- `tests/regression/` - Documents edge cases and bug scenarios
|
||||
|
||||
Example: To understand Animation API:
|
||||
```bash
|
||||
grep -r "Animation" tests/unit/
|
||||
cat tests/demo/screens/animation_demo.py
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
**Always write tests when adding features or fixing bugs:**
|
||||
|
||||
1. **For new features**: Create `tests/unit/feature_name_test.py`
|
||||
2. **For bug fixes**: Create `tests/regression/issue_XX_description_test.py`
|
||||
3. **For demos**: Add to `tests/demo/screens/` if it showcases a feature
|
||||
|
||||
### Quick Testing Commands
|
||||
```bash
|
||||
# Test headless mode with inline Python
|
||||
cd build
|
||||
./mcrogueface --headless -c "import mcrfpy; print('Headless test')"
|
||||
|
||||
# Run specific test with output
|
||||
./mcrogueface --headless --exec ../tests/unit/my_test.py 2>&1
|
||||
```
|
||||
|
||||
## 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 tests in `./tests/` for all bugs and new features
|
||||
- **Practice TDD**: Write tests that fail to demonstrate the issue, then pass after the fix
|
||||
- **Read existing tests**: Check `tests/unit/` and `tests/demo/screens/` for API examples before writing code
|
||||
- **Close the loop**: Reproduce issue → change code → recompile → run test → verify
|
||||
|
||||
### Two Types of Tests
|
||||
|
||||
#### 1. Direct Execution Tests (No Game Loop)
|
||||
For tests that only need class initialization or direct code execution:
|
||||
```python
|
||||
# tests/unit/my_feature_test.py
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Test code - runs immediately
|
||||
frame = mcrfpy.Frame(pos=(0,0), size=(100,100))
|
||||
assert frame.x == 0
|
||||
assert frame.w == 100
|
||||
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
```
|
||||
|
||||
#### 2. Game Loop Tests (Timer-Based)
|
||||
For tests requiring rendering, screenshots, or elapsed time:
|
||||
```python
|
||||
# tests/unit/my_visual_test.py
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
def run_test(runtime):
|
||||
"""Timer callback - runs after game loop starts"""
|
||||
automation.screenshot("test_result.png")
|
||||
# Validate results...
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
|
||||
mcrfpy.createScene("test")
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
ui.append(mcrfpy.Frame(pos=(50,50), size=(100,100)))
|
||||
mcrfpy.setScene("test")
|
||||
mcrfpy.setTimer("test", run_test, 100)
|
||||
```
|
||||
|
||||
### Key Testing Principles
|
||||
- **Timer callbacks are essential**: Screenshots only work after the render loop starts
|
||||
- **Use automation API**: `automation.screenshot()`, `automation.click()` for visual testing
|
||||
- **Exit properly**: Always call `sys.exit(0)` for PASS or `sys.exit(1)` for FAIL
|
||||
- **Headless mode**: Use `--headless --exec` for CI/automated testing
|
||||
- **Check examples first**: Read `tests/demo/screens/*.py` for correct API usage
|
||||
|
||||
### API Quick Reference (from tests)
|
||||
```python
|
||||
# Animation: (property, target_value, duration, easing)
|
||||
anim = mcrfpy.Animation("x", 500.0, 2.0, "easeInOut")
|
||||
anim.start(frame)
|
||||
|
||||
# Caption: use keyword arguments to avoid positional conflicts
|
||||
cap = mcrfpy.Caption(text="Hello", pos=(100, 100))
|
||||
|
||||
# Grid center: uses pixel coordinates, not cell coordinates
|
||||
grid = mcrfpy.Grid(grid_size=(15, 10), pos=(50, 50), size=(400, 300))
|
||||
grid.center = (120, 80) # pixels: (cells * cell_size / 2)
|
||||
|
||||
# Keyboard handler: key names are "Num1", "Num2", "Escape", "Q", etc.
|
||||
def on_key(key, state):
|
||||
if key == "Num1" and state == "start":
|
||||
mcrfpy.setScene("demo_1")
|
||||
```
|
||||
|
||||
## Development Best Practices
|
||||
|
||||
### Testing and Deployment
|
||||
- **Keep tests in ./tests, not ./build/tests** - ./build gets shipped, tests shouldn't be included
|
||||
- **Run full suite before commits**: `cd tests && python3 run_tests.py`
|
||||
|
||||
## 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).
|
||||
|
|
@ -17,41 +17,21 @@ include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
|
|||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
|
||||
|
||||
# ImGui and ImGui-SFML include directories
|
||||
include_directories(${CMAKE_SOURCE_DIR}/modules/imgui)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/modules/imgui-sfml)
|
||||
|
||||
# ImGui source files
|
||||
set(IMGUI_SOURCES
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui/imgui.cpp
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_draw.cpp
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_tables.cpp
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_widgets.cpp
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui-sfml/imgui-SFML.cpp
|
||||
)
|
||||
|
||||
# Collect all the source files
|
||||
file(GLOB_RECURSE SOURCES "src/*.cpp")
|
||||
|
||||
# Add ImGui sources to the build
|
||||
list(APPEND SOURCES ${IMGUI_SOURCES})
|
||||
|
||||
# Find OpenGL (required by ImGui-SFML)
|
||||
find_package(OpenGL REQUIRED)
|
||||
|
||||
# Create a list of libraries to link against
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod
|
||||
OpenGL::GL)
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod)
|
||||
|
||||
# On Windows, add any additional libs and include directories
|
||||
if(WIN32)
|
||||
# Windows-specific Python library name (no dots)
|
||||
list(APPEND LINK_LIBS python314)
|
||||
list(APPEND LINK_LIBS python312)
|
||||
# Add the necessary Windows-specific libraries and include directories
|
||||
# include_directories(path_to_additional_includes)
|
||||
# link_directories(path_to_additional_libs)
|
||||
|
|
@ -59,7 +39,7 @@ if(WIN32)
|
|||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
else()
|
||||
# Unix/Linux specific libraries
|
||||
list(APPEND LINK_LIBS python3.14 m dl util pthread)
|
||||
list(APPEND LINK_LIBS python3.12 m dl util pthread)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||
endif()
|
||||
|
||||
|
|
@ -70,9 +50,6 @@ link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
|||
# Define the executable target before linking libraries
|
||||
add_executable(mcrogueface ${SOURCES})
|
||||
|
||||
# Define NO_SDL for libtcod-headless headers (excludes SDL-dependent code)
|
||||
target_compile_definitions(mcrogueface PRIVATE NO_SDL)
|
||||
|
||||
# On Windows, set subsystem to WINDOWS to hide console
|
||||
if(WIN32)
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
|
|
|
|||
75
README.md
75
README.md
|
|
@ -35,43 +35,6 @@ cd build
|
|||
./mcrogueface
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
|
||||
For most users, pre-built releases are available. If you need to build from source:
|
||||
|
||||
### Quick Build (with pre-built dependencies)
|
||||
|
||||
Download `build_deps.tar.gz` from the releases page, then:
|
||||
|
||||
```bash
|
||||
git clone <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
tar -xzf /path/to/build_deps.tar.gz
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
```
|
||||
|
||||
### Full Build (compiling all dependencies)
|
||||
|
||||
```bash
|
||||
git clone --recursive <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
# See BUILD_FROM_SOURCE.md for complete instructions
|
||||
```
|
||||
|
||||
**[BUILD_FROM_SOURCE.md](BUILD_FROM_SOURCE.md)** - Complete build guide including:
|
||||
- System dependency installation
|
||||
- Compiling SFML, Python, and libtcod-headless from source
|
||||
- Creating `build_deps` archives for distribution
|
||||
- Troubleshooting common build issues
|
||||
|
||||
### System Requirements
|
||||
|
||||
- **Linux**: Debian/Ubuntu tested; other distros should work
|
||||
- **Windows**: Supported (see build guide for details)
|
||||
- **macOS**: Untested
|
||||
|
||||
## Example: Creating a Simple Scene
|
||||
|
||||
```python
|
||||
|
|
@ -94,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
|
||||
|
||||
|
|
@ -161,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
1438
docs/mcrfpy.3
1438
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 +1 @@
|
|||
Subproject commit ebf955df7a89ed0c7968f79faec1de49f61ed7cb
|
||||
Subproject commit 6abddd9f6afdddc09031989e0deb25e301ecf315
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit c6e0284ac58b3f205c95365478888f7b53b077e2
|
||||
Subproject commit 313676d200f093e2694b5cfca574f72a2b116c85
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit bf9023d1bc6ec422769559a5eff60bd00597354f
|
||||
Subproject commit de565ac8f2b795dedc0307b60830cb006afd2ecd
|
||||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 34ae258a863c4f6446effee28ca8ecae51b1519f
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 3b4b65dc9aae7d21a98d3578e3e9433728b118bb
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
# Part 0 - Setting Up McRogueFace
|
||||
|
||||
Welcome to the McRogueFace Roguelike Tutorial! This tutorial will teach you how to create a complete roguelike game using the McRogueFace game engine. Unlike traditional Python libraries, McRogueFace is a complete, portable game engine that includes everything you need to make and distribute games.
|
||||
|
||||
## What is McRogueFace?
|
||||
|
||||
McRogueFace is a high-performance game engine with Python scripting support. Think of it like Unity or Godot, but specifically designed for roguelikes and 2D games. It includes:
|
||||
|
||||
- A complete Python 3.12 runtime (no installation needed!)
|
||||
- High-performance C++ rendering and entity management
|
||||
- Built-in UI components and scene management
|
||||
- Integrated audio system
|
||||
- Professional sprite-based graphics
|
||||
- Easy distribution - your players don't need Python installed!
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting this tutorial, you should:
|
||||
|
||||
- Have basic Python knowledge (variables, functions, classes)
|
||||
- Be comfortable editing text files
|
||||
- Have a text editor (VS Code, Sublime Text, Notepad++, etc.)
|
||||
|
||||
That's it! Unlike other roguelike tutorials, you don't need Python installed - McRogueFace includes everything.
|
||||
|
||||
## Getting McRogueFace
|
||||
|
||||
### Step 1: Download the Engine
|
||||
|
||||
1. Visit the McRogueFace releases page
|
||||
2. Download the version for your operating system:
|
||||
- `McRogueFace-Windows.zip` for Windows
|
||||
- `McRogueFace-MacOS.zip` for macOS
|
||||
- `McRogueFace-Linux.zip` for Linux
|
||||
|
||||
### Step 2: Extract the Archive
|
||||
|
||||
Extract the downloaded archive to a folder where you want to develop your game. You should see this structure:
|
||||
|
||||
```
|
||||
McRogueFace/
|
||||
├── mcrogueface (or mcrogueface.exe on Windows)
|
||||
├── scripts/
|
||||
│ └── game.py
|
||||
├── assets/
|
||||
│ ├── sprites/
|
||||
│ ├── fonts/
|
||||
│ └── audio/
|
||||
└── lib/
|
||||
```
|
||||
|
||||
### Step 3: Run the Engine
|
||||
|
||||
Run the McRogueFace executable:
|
||||
|
||||
- **Windows**: Double-click `mcrogueface.exe`
|
||||
- **Mac/Linux**: Open a terminal in the folder and run `./mcrogueface`
|
||||
|
||||
You should see a window open with the default McRogueFace demo. This shows the engine is working correctly!
|
||||
|
||||
## Your First McRogueFace Script
|
||||
|
||||
Let's modify the engine to display "Hello Roguelike!" instead of the default demo.
|
||||
|
||||
### Step 1: Open game.py
|
||||
|
||||
Open `scripts/game.py` in your text editor. You'll see the default demo code. Replace it entirely with:
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Create a new scene called "hello"
|
||||
mcrfpy.createScene("hello")
|
||||
|
||||
# Switch to our new scene
|
||||
mcrfpy.setScene("hello")
|
||||
|
||||
# Get the UI container for our scene
|
||||
ui = mcrfpy.sceneUI("hello")
|
||||
|
||||
# Create a text caption
|
||||
caption = mcrfpy.Caption("Hello Roguelike!", 400, 300)
|
||||
caption.font_size = 32
|
||||
caption.fill_color = mcrfpy.Color(255, 255, 255) # White text
|
||||
|
||||
# Add the caption to our scene
|
||||
ui.append(caption)
|
||||
|
||||
# Create a smaller instruction caption
|
||||
instruction = mcrfpy.Caption("Press ESC to exit", 400, 350)
|
||||
instruction.font_size = 16
|
||||
instruction.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
|
||||
ui.append(instruction)
|
||||
|
||||
# Set up a simple key handler
|
||||
def handle_keys(key, state):
|
||||
if state == "start" and key == "Escape":
|
||||
mcrfpy.setScene(None) # This exits the game
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
print("Hello Roguelike is running!")
|
||||
```
|
||||
|
||||
### Step 2: Save and Run
|
||||
|
||||
1. Save the file
|
||||
2. If McRogueFace is still running, it will automatically reload!
|
||||
3. If not, run the engine again
|
||||
|
||||
You should now see "Hello Roguelike!" displayed in the window.
|
||||
|
||||
### Step 3: Understanding the Code
|
||||
|
||||
Let's break down what we just wrote:
|
||||
|
||||
1. **Import mcrfpy**: This is McRogueFace's Python API
|
||||
2. **Create a scene**: Scenes are like game states (menu, gameplay, inventory, etc.)
|
||||
3. **UI elements**: We create Caption objects for text display
|
||||
4. **Colors**: McRogueFace uses RGB colors (0-255 for each component)
|
||||
5. **Input handling**: We set up a callback for keyboard input
|
||||
6. **Scene switching**: Setting the scene to None exits the game
|
||||
|
||||
## Key Differences from Pure Python Development
|
||||
|
||||
### The Game Loop
|
||||
|
||||
Unlike typical Python scripts, McRogueFace runs your code inside its game loop:
|
||||
|
||||
1. The engine starts and loads `scripts/game.py`
|
||||
2. Your script sets up scenes, UI elements, and callbacks
|
||||
3. The engine runs at 60 FPS, handling rendering and input
|
||||
4. Your callbacks are triggered by game events
|
||||
|
||||
### Hot Reloading
|
||||
|
||||
McRogueFace can reload your scripts while running! Just save your changes and the engine will reload automatically. This makes development incredibly fast.
|
||||
|
||||
### Asset Pipeline
|
||||
|
||||
McRogueFace includes a complete asset system:
|
||||
|
||||
- **Sprites**: Place images in `assets/sprites/`
|
||||
- **Fonts**: TrueType fonts in `assets/fonts/`
|
||||
- **Audio**: Sound effects and music in `assets/audio/`
|
||||
|
||||
We'll explore these in later lessons.
|
||||
|
||||
## Testing Your Setup
|
||||
|
||||
Let's create a more interactive test to ensure everything is working properly:
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Create our test scene
|
||||
mcrfpy.createScene("test")
|
||||
mcrfpy.setScene("test")
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
|
||||
# Create a background frame
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(20, 20, 30) # Dark blue-gray
|
||||
ui.append(background)
|
||||
|
||||
# Title text
|
||||
title = mcrfpy.Caption("McRogueFace Setup Test", 512, 100)
|
||||
title.font_size = 36
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
|
||||
ui.append(title)
|
||||
|
||||
# Status text that will update
|
||||
status_text = mcrfpy.Caption("Press any key to test input...", 512, 300)
|
||||
status_text.font_size = 20
|
||||
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(status_text)
|
||||
|
||||
# Instructions
|
||||
instructions = [
|
||||
"Arrow Keys: Test movement input",
|
||||
"Space: Test action input",
|
||||
"Mouse Click: Test mouse input",
|
||||
"ESC: Exit"
|
||||
]
|
||||
|
||||
y_offset = 400
|
||||
for instruction in instructions:
|
||||
inst_caption = mcrfpy.Caption(instruction, 512, y_offset)
|
||||
inst_caption.font_size = 16
|
||||
inst_caption.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(inst_caption)
|
||||
y_offset += 30
|
||||
|
||||
# Input handler
|
||||
def handle_input(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
else:
|
||||
status_text.text = f"You pressed: {key}"
|
||||
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green
|
||||
|
||||
# Set up input handling
|
||||
mcrfpy.keypressScene(handle_input)
|
||||
|
||||
print("Setup test is running! Try pressing different keys.")
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Engine Won't Start
|
||||
|
||||
- **Windows**: Make sure you extracted all files, not just the .exe
|
||||
- **Mac**: You may need to right-click and select "Open" the first time
|
||||
- **Linux**: Make sure the file is executable: `chmod +x mcrogueface`
|
||||
|
||||
### Scripts Not Loading
|
||||
|
||||
- Ensure your script is named exactly `game.py` in the `scripts/` folder
|
||||
- Check the console output for Python errors
|
||||
- Make sure you're using Python 3 syntax
|
||||
|
||||
### Performance Issues
|
||||
|
||||
- McRogueFace should run smoothly at 60 FPS
|
||||
- If not, check if your graphics drivers are updated
|
||||
- The engine shows FPS in the window title
|
||||
|
||||
## What's Next?
|
||||
|
||||
Congratulations! You now have McRogueFace set up and running. You've learned:
|
||||
|
||||
- How to download and run the McRogueFace engine
|
||||
- The basic structure of a McRogueFace project
|
||||
- How to create scenes and UI elements
|
||||
- How to handle keyboard input
|
||||
- The development workflow with hot reloading
|
||||
|
||||
In Part 1, we'll create our player character and implement movement. We'll explore McRogueFace's entity system and learn how to create a game world.
|
||||
|
||||
## Why McRogueFace?
|
||||
|
||||
Before we continue, let's highlight why McRogueFace is excellent for roguelike development:
|
||||
|
||||
1. **No Installation Hassles**: Your players just download and run - no Python needed!
|
||||
2. **Professional Performance**: C++ engine core means smooth gameplay even with hundreds of entities
|
||||
3. **Built-in Features**: UI, audio, scenes, and animations are already there
|
||||
4. **Easy Distribution**: Just zip your game folder and share it
|
||||
5. **Rapid Development**: Hot reloading and Python scripting for quick iteration
|
||||
|
||||
Ready to make a roguelike? Let's continue to Part 1!
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import mcrfpy
|
||||
|
||||
# Create a new scene called "hello"
|
||||
mcrfpy.createScene("hello")
|
||||
|
||||
# Switch to our new scene
|
||||
mcrfpy.setScene("hello")
|
||||
|
||||
# Get the UI container for our scene
|
||||
ui = mcrfpy.sceneUI("hello")
|
||||
|
||||
# Create a text caption
|
||||
caption = mcrfpy.Caption("Hello Roguelike!", 400, 300)
|
||||
caption.font_size = 32
|
||||
caption.fill_color = mcrfpy.Color(255, 255, 255) # White text
|
||||
|
||||
# Add the caption to our scene
|
||||
ui.append(caption)
|
||||
|
||||
# Create a smaller instruction caption
|
||||
instruction = mcrfpy.Caption("Press ESC to exit", 400, 350)
|
||||
instruction.font_size = 16
|
||||
instruction.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
|
||||
ui.append(instruction)
|
||||
|
||||
# Set up a simple key handler
|
||||
def handle_keys(key, state):
|
||||
if state == "start" and key == "Escape":
|
||||
mcrfpy.setScene(None) # This exits the game
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
print("Hello Roguelike is running!")
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import mcrfpy
|
||||
|
||||
# Create our test scene
|
||||
mcrfpy.createScene("test")
|
||||
mcrfpy.setScene("test")
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
|
||||
# Create a background frame
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(20, 20, 30) # Dark blue-gray
|
||||
ui.append(background)
|
||||
|
||||
# Title text
|
||||
title = mcrfpy.Caption("McRogueFace Setup Test", 512, 100)
|
||||
title.font_size = 36
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
|
||||
ui.append(title)
|
||||
|
||||
# Status text that will update
|
||||
status_text = mcrfpy.Caption("Press any key to test input...", 512, 300)
|
||||
status_text.font_size = 20
|
||||
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(status_text)
|
||||
|
||||
# Instructions
|
||||
instructions = [
|
||||
"Arrow Keys: Test movement input",
|
||||
"Space: Test action input",
|
||||
"Mouse Click: Test mouse input",
|
||||
"ESC: Exit"
|
||||
]
|
||||
|
||||
y_offset = 400
|
||||
for instruction in instructions:
|
||||
inst_caption = mcrfpy.Caption(instruction, 512, y_offset)
|
||||
inst_caption.font_size = 16
|
||||
inst_caption.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
ui.append(inst_caption)
|
||||
y_offset += 30
|
||||
|
||||
# Input handler
|
||||
def handle_input(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
else:
|
||||
status_text.text = f"You pressed: {key}"
|
||||
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green
|
||||
|
||||
# Set up input handling
|
||||
mcrfpy.keypressScene(handle_input)
|
||||
|
||||
print("Setup test is running! Try pressing different keys.")
|
||||
|
|
@ -0,0 +1,457 @@
|
|||
# Part 1 - Drawing the '@' Symbol and Moving It Around
|
||||
|
||||
In Part 0, we set up McRogueFace and created a simple "Hello Roguelike" scene. Now it's time to create the foundation of our game: a player character that can move around the screen.
|
||||
|
||||
In traditional roguelikes, the player is represented by the '@' symbol. We'll honor that tradition while taking advantage of McRogueFace's powerful sprite-based rendering system.
|
||||
|
||||
## Understanding McRogueFace's Architecture
|
||||
|
||||
Before we dive into code, let's understand two key concepts in McRogueFace:
|
||||
|
||||
### Grid - The Game World
|
||||
|
||||
A `Grid` represents your game world. It's a 2D array of tiles where each tile can be:
|
||||
- **Walkable or not** (for collision detection)
|
||||
- **Transparent or not** (for field of view, which we'll cover later)
|
||||
- **Have a visual appearance** (sprite index and color)
|
||||
|
||||
Think of the Grid as the dungeon floor, walls, and other static elements.
|
||||
|
||||
### Entity - Things That Move
|
||||
|
||||
An `Entity` represents anything that can move around on the Grid:
|
||||
- The player character
|
||||
- Monsters
|
||||
- Items (if you want them to be thrown or moved)
|
||||
- Projectiles
|
||||
|
||||
Entities exist "on top of" the Grid and automatically handle smooth movement animation between tiles.
|
||||
|
||||
## Creating Our Game World
|
||||
|
||||
Let's start by creating a simple room for our player to move around in. Create a new `game.py`:
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Define some constants for our tile types
|
||||
FLOOR_TILE = 0
|
||||
WALL_TILE = 1
|
||||
PLAYER_SPRITE = 2
|
||||
|
||||
# Window configuration
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
# Configure window properties
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 1"
|
||||
|
||||
# Get the UI container for our scene
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
|
||||
# Create a dark background
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
ui.append(background)
|
||||
```
|
||||
|
||||
Now we need to set up our tileset. For this tutorial, we'll use ASCII-style sprites. McRogueFace comes with a built-in ASCII tileset:
|
||||
|
||||
```python
|
||||
# Load the ASCII tileset
|
||||
# This tileset has characters mapped to sprite indices
|
||||
# For example: @ = 64, # = 35, . = 46
|
||||
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
# Create the game grid
|
||||
# 50x30 tiles is a good size for a roguelike
|
||||
GRID_WIDTH = 50
|
||||
GRID_HEIGHT = 30
|
||||
|
||||
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
|
||||
grid.position = (100, 100) # Position on screen
|
||||
grid.size = (800, 480) # Size in pixels
|
||||
|
||||
# Add the grid to our UI
|
||||
ui.append(grid)
|
||||
```
|
||||
|
||||
## Initializing the Game World
|
||||
|
||||
Now let's fill our grid with a simple room:
|
||||
|
||||
```python
|
||||
def create_room():
|
||||
"""Create a room with walls around the edges"""
|
||||
# Fill everything with floor tiles first
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.sprite_index = 46 # '.' character
|
||||
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
|
||||
|
||||
# Create walls around the edges
|
||||
for x in range(GRID_WIDTH):
|
||||
# Top wall
|
||||
cell = grid.at(x, 0)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.sprite_index = 35 # '#' character
|
||||
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
|
||||
|
||||
# Bottom wall
|
||||
cell = grid.at(x, GRID_HEIGHT - 1)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.sprite_index = 35 # '#' character
|
||||
cell.color = mcrfpy.Color(100, 100, 100)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
# Left wall
|
||||
cell = grid.at(0, y)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.sprite_index = 35 # '#' character
|
||||
cell.color = mcrfpy.Color(100, 100, 100)
|
||||
|
||||
# Right wall
|
||||
cell = grid.at(GRID_WIDTH - 1, y)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.sprite_index = 35 # '#' character
|
||||
cell.color = mcrfpy.Color(100, 100, 100)
|
||||
|
||||
# Create the room
|
||||
create_room()
|
||||
```
|
||||
|
||||
## Creating the Player
|
||||
|
||||
Now let's add our player character:
|
||||
|
||||
```python
|
||||
# Create the player entity
|
||||
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
|
||||
player.sprite_index = 64 # '@' character
|
||||
player.color = mcrfpy.Color(255, 255, 255) # White
|
||||
|
||||
# The entity is automatically added to the grid when we pass grid= parameter
|
||||
# This is equivalent to: grid.entities.append(player)
|
||||
```
|
||||
|
||||
## Handling Input
|
||||
|
||||
McRogueFace uses a callback system for input. For a turn-based roguelike, we only care about key presses, not releases:
|
||||
|
||||
```python
|
||||
def handle_input(key, state):
|
||||
"""Handle keyboard input for player movement"""
|
||||
# Only process key presses, not releases
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
# Movement deltas
|
||||
dx, dy = 0, 0
|
||||
|
||||
# Arrow keys
|
||||
if key == "Up":
|
||||
dy = -1
|
||||
elif key == "Down":
|
||||
dy = 1
|
||||
elif key == "Left":
|
||||
dx = -1
|
||||
elif key == "Right":
|
||||
dx = 1
|
||||
|
||||
# Numpad movement (for true roguelike feel!)
|
||||
elif key == "Num7": # Northwest
|
||||
dx, dy = -1, -1
|
||||
elif key == "Num8": # North
|
||||
dy = -1
|
||||
elif key == "Num9": # Northeast
|
||||
dx, dy = 1, -1
|
||||
elif key == "Num4": # West
|
||||
dx = -1
|
||||
elif key == "Num6": # East
|
||||
dx = 1
|
||||
elif key == "Num1": # Southwest
|
||||
dx, dy = -1, 1
|
||||
elif key == "Num2": # South
|
||||
dy = 1
|
||||
elif key == "Num3": # Southeast
|
||||
dx, dy = 1, 1
|
||||
|
||||
# Escape to quit
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
return
|
||||
|
||||
# If there's movement, try to move the player
|
||||
if dx != 0 or dy != 0:
|
||||
move_player(dx, dy)
|
||||
|
||||
# Register the input handler
|
||||
mcrfpy.keypressScene(handle_input)
|
||||
```
|
||||
|
||||
## Implementing Movement with Collision Detection
|
||||
|
||||
Now let's implement the movement function with proper collision detection:
|
||||
|
||||
```python
|
||||
def move_player(dx, dy):
|
||||
"""Move the player if the destination is walkable"""
|
||||
# Calculate new position
|
||||
new_x = player.x + dx
|
||||
new_y = player.y + dy
|
||||
|
||||
# Check bounds
|
||||
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
|
||||
return
|
||||
|
||||
# Check if the destination is walkable
|
||||
destination = grid.at(new_x, new_y)
|
||||
if destination.walkable:
|
||||
# Move the player
|
||||
player.x = new_x
|
||||
player.y = new_y
|
||||
# The entity will automatically animate to the new position!
|
||||
```
|
||||
|
||||
## Adding Visual Polish
|
||||
|
||||
Let's add some UI elements to make our game look more polished:
|
||||
|
||||
```python
|
||||
# Add a title
|
||||
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
|
||||
ui.append(title)
|
||||
|
||||
# Add instructions
|
||||
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
|
||||
ui.append(instructions)
|
||||
|
||||
# Add a status line at the bottom
|
||||
status = mcrfpy.Caption("@ You", 100, 600)
|
||||
status.font_size = 18
|
||||
status.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(status)
|
||||
```
|
||||
|
||||
## Complete Code
|
||||
|
||||
Here's the complete `game.py` for Part 1:
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Window configuration
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 1"
|
||||
|
||||
# Get the UI container for our scene
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
|
||||
# Create a dark background
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
ui.append(background)
|
||||
|
||||
# Load the ASCII tileset
|
||||
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
# Create the game grid
|
||||
GRID_WIDTH = 50
|
||||
GRID_HEIGHT = 30
|
||||
|
||||
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
|
||||
grid.position = (100, 100)
|
||||
grid.size = (800, 480)
|
||||
ui.append(grid)
|
||||
|
||||
def create_room():
|
||||
"""Create a room with walls around the edges"""
|
||||
# Fill everything with floor tiles first
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.sprite_index = 46 # '.' character
|
||||
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
|
||||
|
||||
# Create walls around the edges
|
||||
for x in range(GRID_WIDTH):
|
||||
# Top wall
|
||||
cell = grid.at(x, 0)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.sprite_index = 35 # '#' character
|
||||
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
|
||||
|
||||
# Bottom wall
|
||||
cell = grid.at(x, GRID_HEIGHT - 1)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.sprite_index = 35 # '#' character
|
||||
cell.color = mcrfpy.Color(100, 100, 100)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
# Left wall
|
||||
cell = grid.at(0, y)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.sprite_index = 35 # '#' character
|
||||
cell.color = mcrfpy.Color(100, 100, 100)
|
||||
|
||||
# Right wall
|
||||
cell = grid.at(GRID_WIDTH - 1, y)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.sprite_index = 35 # '#' character
|
||||
cell.color = mcrfpy.Color(100, 100, 100)
|
||||
|
||||
# Create the room
|
||||
create_room()
|
||||
|
||||
# Create the player entity
|
||||
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
|
||||
player.sprite_index = 64 # '@' character
|
||||
player.color = mcrfpy.Color(255, 255, 255) # White
|
||||
|
||||
def move_player(dx, dy):
|
||||
"""Move the player if the destination is walkable"""
|
||||
# Calculate new position
|
||||
new_x = player.x + dx
|
||||
new_y = player.y + dy
|
||||
|
||||
# Check bounds
|
||||
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
|
||||
return
|
||||
|
||||
# Check if the destination is walkable
|
||||
destination = grid.at(new_x, new_y)
|
||||
if destination.walkable:
|
||||
# Move the player
|
||||
player.x = new_x
|
||||
player.y = new_y
|
||||
|
||||
def handle_input(key, state):
|
||||
"""Handle keyboard input for player movement"""
|
||||
# Only process key presses, not releases
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
# Movement deltas
|
||||
dx, dy = 0, 0
|
||||
|
||||
# Arrow keys
|
||||
if key == "Up":
|
||||
dy = -1
|
||||
elif key == "Down":
|
||||
dy = 1
|
||||
elif key == "Left":
|
||||
dx = -1
|
||||
elif key == "Right":
|
||||
dx = 1
|
||||
|
||||
# Numpad movement (for true roguelike feel!)
|
||||
elif key == "Num7": # Northwest
|
||||
dx, dy = -1, -1
|
||||
elif key == "Num8": # North
|
||||
dy = -1
|
||||
elif key == "Num9": # Northeast
|
||||
dx, dy = 1, -1
|
||||
elif key == "Num4": # West
|
||||
dx = -1
|
||||
elif key == "Num6": # East
|
||||
dx = 1
|
||||
elif key == "Num1": # Southwest
|
||||
dx, dy = -1, 1
|
||||
elif key == "Num2": # South
|
||||
dy = 1
|
||||
elif key == "Num3": # Southeast
|
||||
dx, dy = 1, 1
|
||||
|
||||
# Escape to quit
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
return
|
||||
|
||||
# If there's movement, try to move the player
|
||||
if dx != 0 or dy != 0:
|
||||
move_player(dx, dy)
|
||||
|
||||
# Register the input handler
|
||||
mcrfpy.keypressScene(handle_input)
|
||||
|
||||
# Add UI elements
|
||||
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
ui.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(instructions)
|
||||
|
||||
status = mcrfpy.Caption("@ You", 100, 600)
|
||||
status.font_size = 18
|
||||
status.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(status)
|
||||
|
||||
print("Part 1: The @ symbol moves!")
|
||||
```
|
||||
|
||||
## Understanding What We've Built
|
||||
|
||||
Let's review the key concepts we've implemented:
|
||||
|
||||
1. **Grid-Entity Architecture**: The Grid represents our static world (floors and walls), while the Entity (player) moves on top of it.
|
||||
|
||||
2. **Collision Detection**: By checking the `walkable` property of grid cells, we prevent the player from walking through walls.
|
||||
|
||||
3. **Turn-Based Input**: By only responding to key presses (not releases), we've created true turn-based movement.
|
||||
|
||||
4. **Visual Feedback**: The Entity system automatically animates movement between tiles, giving smooth visual feedback.
|
||||
|
||||
## Exercises
|
||||
|
||||
Try these modifications to deepen your understanding:
|
||||
|
||||
1. **Add More Rooms**: Create multiple rooms connected by corridors
|
||||
2. **Different Tile Types**: Add doors (walkable but different appearance)
|
||||
3. **Sprint Movement**: Hold Shift to move multiple tiles at once
|
||||
4. **Mouse Support**: Click a tile to pathfind to it (we'll cover pathfinding properly later)
|
||||
|
||||
## ASCII Sprite Reference
|
||||
|
||||
Here are some useful ASCII character indices for the default tileset:
|
||||
- @ (player): 64
|
||||
- # (wall): 35
|
||||
- . (floor): 46
|
||||
- + (door): 43
|
||||
- ~ (water): 126
|
||||
- % (item): 37
|
||||
- ! (potion): 33
|
||||
|
||||
## What's Next?
|
||||
|
||||
In Part 2, we'll expand our world with:
|
||||
- A proper Entity system for managing multiple objects
|
||||
- NPCs that can also move around
|
||||
- A more interesting map layout
|
||||
- The beginning of our game architecture
|
||||
|
||||
The foundation is set - you have a player character that can move around a world with collision detection. This is the core of any roguelike game!
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
import mcrfpy
|
||||
|
||||
# Window configuration
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 1"
|
||||
|
||||
# Get the UI container for our scene
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
|
||||
# Create a dark background
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
ui.append(background)
|
||||
|
||||
# Load the ASCII tileset
|
||||
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
# Create the game grid
|
||||
GRID_WIDTH = 50
|
||||
GRID_HEIGHT = 30
|
||||
|
||||
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
|
||||
grid.position = (100, 100)
|
||||
grid.size = (800, 480)
|
||||
ui.append(grid)
|
||||
|
||||
def create_room():
|
||||
"""Create a room with walls around the edges"""
|
||||
# Fill everything with floor tiles first
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.sprite_index = 46 # '.' character
|
||||
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
|
||||
|
||||
# Create walls around the edges
|
||||
for x in range(GRID_WIDTH):
|
||||
# Top wall
|
||||
cell = grid.at(x, 0)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.sprite_index = 35 # '#' character
|
||||
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
|
||||
|
||||
# Bottom wall
|
||||
cell = grid.at(x, GRID_HEIGHT - 1)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.sprite_index = 35 # '#' character
|
||||
cell.color = mcrfpy.Color(100, 100, 100)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
# Left wall
|
||||
cell = grid.at(0, y)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.sprite_index = 35 # '#' character
|
||||
cell.color = mcrfpy.Color(100, 100, 100)
|
||||
|
||||
# Right wall
|
||||
cell = grid.at(GRID_WIDTH - 1, y)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.sprite_index = 35 # '#' character
|
||||
cell.color = mcrfpy.Color(100, 100, 100)
|
||||
|
||||
# Create the room
|
||||
create_room()
|
||||
|
||||
# Create the player entity
|
||||
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
|
||||
player.sprite_index = 64 # '@' character
|
||||
player.color = mcrfpy.Color(255, 255, 255) # White
|
||||
|
||||
def move_player(dx, dy):
|
||||
"""Move the player if the destination is walkable"""
|
||||
# Calculate new position
|
||||
new_x = player.x + dx
|
||||
new_y = player.y + dy
|
||||
|
||||
# Check bounds
|
||||
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
|
||||
return
|
||||
|
||||
# Check if the destination is walkable
|
||||
destination = grid.at(new_x, new_y)
|
||||
if destination.walkable:
|
||||
# Move the player
|
||||
player.x = new_x
|
||||
player.y = new_y
|
||||
|
||||
def handle_input(key, state):
|
||||
"""Handle keyboard input for player movement"""
|
||||
# Only process key presses, not releases
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
# Movement deltas
|
||||
dx, dy = 0, 0
|
||||
|
||||
# Arrow keys
|
||||
if key == "Up":
|
||||
dy = -1
|
||||
elif key == "Down":
|
||||
dy = 1
|
||||
elif key == "Left":
|
||||
dx = -1
|
||||
elif key == "Right":
|
||||
dx = 1
|
||||
|
||||
# Numpad movement (for true roguelike feel!)
|
||||
elif key == "Num7": # Northwest
|
||||
dx, dy = -1, -1
|
||||
elif key == "Num8": # North
|
||||
dy = -1
|
||||
elif key == "Num9": # Northeast
|
||||
dx, dy = 1, -1
|
||||
elif key == "Num4": # West
|
||||
dx = -1
|
||||
elif key == "Num6": # East
|
||||
dx = 1
|
||||
elif key == "Num1": # Southwest
|
||||
dx, dy = -1, 1
|
||||
elif key == "Num2": # South
|
||||
dy = 1
|
||||
elif key == "Num3": # Southeast
|
||||
dx, dy = 1, 1
|
||||
|
||||
# Escape to quit
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
return
|
||||
|
||||
# If there's movement, try to move the player
|
||||
if dx != 0 or dy != 0:
|
||||
move_player(dx, dy)
|
||||
|
||||
# Register the input handler
|
||||
mcrfpy.keypressScene(handle_input)
|
||||
|
||||
# Add UI elements
|
||||
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
ui.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(instructions)
|
||||
|
||||
status = mcrfpy.Caption("@ You", 100, 600)
|
||||
status.font_size = 18
|
||||
status.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(status)
|
||||
|
||||
print("Part 1: The @ symbol moves!")
|
||||
|
|
@ -0,0 +1,562 @@
|
|||
# Part 2 - The Generic Entity, the Render Functions, and the Map
|
||||
|
||||
In Part 1, we created a player character that could move around a simple room. Now it's time to build a proper architecture for our roguelike. We'll create a flexible entity system, a proper map structure, and organize our code for future expansion.
|
||||
|
||||
## Understanding Game Architecture
|
||||
|
||||
Before diving into code, let's understand the architecture we're building:
|
||||
|
||||
1. **Entities**: Anything that can exist in the game world (player, monsters, items)
|
||||
2. **Game Map**: The dungeon structure with tiles that can be walls or floors
|
||||
3. **Game Engine**: Coordinates everything - entities, map, input, and rendering
|
||||
|
||||
In McRogueFace, we'll adapt these concepts to work with the engine's scene-based architecture.
|
||||
|
||||
## Creating a Flexible Entity System
|
||||
|
||||
While McRogueFace provides a built-in `Entity` class, we'll create a wrapper to add game-specific functionality:
|
||||
|
||||
```python
|
||||
class GameObject:
|
||||
"""Base class for all game objects (player, monsters, items)"""
|
||||
|
||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.sprite_index = sprite_index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.blocks = blocks # Does this entity block movement?
|
||||
self._entity = None # The McRogueFace entity
|
||||
self.grid = None # Reference to the grid
|
||||
|
||||
def attach_to_grid(self, grid):
|
||||
"""Attach this game object to a McRogueFace grid"""
|
||||
self.grid = grid
|
||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||
self._entity.sprite_index = self.sprite_index
|
||||
self._entity.color = self.color
|
||||
|
||||
def move(self, dx, dy):
|
||||
"""Move by the given amount if possible"""
|
||||
if not self.grid:
|
||||
return
|
||||
|
||||
new_x = self.x + dx
|
||||
new_y = self.y + dy
|
||||
|
||||
# Update our position
|
||||
self.x = new_x
|
||||
self.y = new_y
|
||||
|
||||
# Update the visual entity
|
||||
if self._entity:
|
||||
self._entity.x = new_x
|
||||
self._entity.y = new_y
|
||||
|
||||
def destroy(self):
|
||||
"""Remove this entity from the game"""
|
||||
if self._entity and self.grid:
|
||||
# Find and remove from grid's entity list
|
||||
for i, entity in enumerate(self.grid.entities):
|
||||
if entity == self._entity:
|
||||
del self.grid.entities[i]
|
||||
break
|
||||
self._entity = None
|
||||
```
|
||||
|
||||
## Building the Game Map
|
||||
|
||||
Let's create a proper map class that manages our dungeon:
|
||||
|
||||
```python
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.grid = None
|
||||
self.entities = [] # List of GameObjects
|
||||
|
||||
def create_grid(self, tileset):
|
||||
"""Create the McRogueFace grid"""
|
||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||
self.grid.position = (100, 100)
|
||||
self.grid.size = (800, 480)
|
||||
|
||||
# Initialize all tiles as walls
|
||||
self.fill_with_walls()
|
||||
|
||||
return self.grid
|
||||
|
||||
def fill_with_walls(self):
|
||||
"""Fill the entire map with wall tiles"""
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
self.set_tile(x, y, walkable=False, transparent=False,
|
||||
sprite_index=35, color=(100, 100, 100))
|
||||
|
||||
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
||||
"""Set properties for a specific tile"""
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
cell = self.grid.at(x, y)
|
||||
cell.walkable = walkable
|
||||
cell.transparent = transparent
|
||||
cell.sprite_index = sprite_index
|
||||
cell.color = mcrfpy.Color(*color)
|
||||
|
||||
def create_room(self, x1, y1, x2, y2):
|
||||
"""Carve out a room in the map"""
|
||||
# Make sure coordinates are in the right order
|
||||
x1, x2 = min(x1, x2), max(x1, x2)
|
||||
y1, y2 = min(y1, y2), max(y1, y2)
|
||||
|
||||
# Carve out floor tiles
|
||||
for y in range(y1, y2 + 1):
|
||||
for x in range(x1, x2 + 1):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(50, 50, 50))
|
||||
|
||||
def create_tunnel_h(self, x1, x2, y):
|
||||
"""Create a horizontal tunnel"""
|
||||
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(50, 50, 50))
|
||||
|
||||
def create_tunnel_v(self, y1, y2, x):
|
||||
"""Create a vertical tunnel"""
|
||||
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(50, 50, 50))
|
||||
|
||||
def is_blocked(self, x, y):
|
||||
"""Check if a tile blocks movement"""
|
||||
# Check map boundaries
|
||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||
return True
|
||||
|
||||
# Check if tile is walkable
|
||||
if not self.grid.at(x, y).walkable:
|
||||
return True
|
||||
|
||||
# Check if any blocking entity is at this position
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_entity(self, entity):
|
||||
"""Add a GameObject to the map"""
|
||||
self.entities.append(entity)
|
||||
entity.attach_to_grid(self.grid)
|
||||
|
||||
def get_blocking_entity_at(self, x, y):
|
||||
"""Return any blocking entity at the given position"""
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return entity
|
||||
return None
|
||||
```
|
||||
|
||||
## Creating the Game Engine
|
||||
|
||||
Now let's build our game engine to tie everything together:
|
||||
|
||||
```python
|
||||
class Engine:
|
||||
"""Main game engine that manages game state"""
|
||||
|
||||
def __init__(self):
|
||||
self.game_map = None
|
||||
self.player = None
|
||||
self.entities = []
|
||||
|
||||
# Create the game scene
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
# Configure window
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 2"
|
||||
|
||||
# Get UI container
|
||||
self.ui = mcrfpy.sceneUI("game")
|
||||
|
||||
# Add background
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
self.ui.append(background)
|
||||
|
||||
# Load tileset
|
||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
# Create the game world
|
||||
self.setup_game()
|
||||
|
||||
# Setup input handling
|
||||
self.setup_input()
|
||||
|
||||
# Add UI elements
|
||||
self.setup_ui()
|
||||
|
||||
def setup_game(self):
|
||||
"""Initialize the game world"""
|
||||
# Create the map
|
||||
self.game_map = GameMap(50, 30)
|
||||
grid = self.game_map.create_grid(self.tileset)
|
||||
self.ui.append(grid)
|
||||
|
||||
# Create some rooms
|
||||
self.game_map.create_room(10, 10, 20, 20)
|
||||
self.game_map.create_room(30, 15, 40, 25)
|
||||
self.game_map.create_room(15, 22, 25, 28)
|
||||
|
||||
# Connect rooms with tunnels
|
||||
self.game_map.create_tunnel_h(20, 30, 15)
|
||||
self.game_map.create_tunnel_v(20, 22, 20)
|
||||
|
||||
# Create player
|
||||
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
|
||||
self.game_map.add_entity(self.player)
|
||||
|
||||
# Create an NPC
|
||||
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
|
||||
self.game_map.add_entity(npc)
|
||||
self.entities.append(npc)
|
||||
|
||||
# Create some items (non-blocking)
|
||||
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
|
||||
self.game_map.add_entity(potion)
|
||||
self.entities.append(potion)
|
||||
|
||||
def handle_movement(self, dx, dy):
|
||||
"""Handle player movement"""
|
||||
new_x = self.player.x + dx
|
||||
new_y = self.player.y + dy
|
||||
|
||||
# Check if movement is blocked
|
||||
if not self.game_map.is_blocked(new_x, new_y):
|
||||
self.player.move(dx, dy)
|
||||
else:
|
||||
# Check if we bumped into an entity
|
||||
target = self.game_map.get_blocking_entity_at(new_x, new_y)
|
||||
if target:
|
||||
print(f"You bump into the {target.name}!")
|
||||
|
||||
def setup_input(self):
|
||||
"""Setup keyboard input handling"""
|
||||
def handle_keys(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
# Movement keys
|
||||
movement = {
|
||||
"Up": (0, -1),
|
||||
"Down": (0, 1),
|
||||
"Left": (-1, 0),
|
||||
"Right": (1, 0),
|
||||
"Num7": (-1, -1),
|
||||
"Num8": (0, -1),
|
||||
"Num9": (1, -1),
|
||||
"Num4": (-1, 0),
|
||||
"Num6": (1, 0),
|
||||
"Num1": (-1, 1),
|
||||
"Num2": (0, 1),
|
||||
"Num3": (1, 1),
|
||||
}
|
||||
|
||||
if key in movement:
|
||||
dx, dy = movement[key]
|
||||
self.handle_movement(dx, dy)
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup UI elements"""
|
||||
# Title
|
||||
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
self.ui.append(title)
|
||||
|
||||
# Instructions
|
||||
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.ui.append(instructions)
|
||||
```
|
||||
|
||||
## Putting It All Together
|
||||
|
||||
Here's the complete `game.py` file:
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
class GameObject:
|
||||
"""Base class for all game objects (player, monsters, items)"""
|
||||
|
||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.sprite_index = sprite_index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.blocks = blocks
|
||||
self._entity = None
|
||||
self.grid = None
|
||||
|
||||
def attach_to_grid(self, grid):
|
||||
"""Attach this game object to a McRogueFace grid"""
|
||||
self.grid = grid
|
||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||
self._entity.sprite_index = self.sprite_index
|
||||
self._entity.color = mcrfpy.Color(*self.color)
|
||||
|
||||
def move(self, dx, dy):
|
||||
"""Move by the given amount if possible"""
|
||||
if not self.grid:
|
||||
return
|
||||
|
||||
new_x = self.x + dx
|
||||
new_y = self.y + dy
|
||||
|
||||
self.x = new_x
|
||||
self.y = new_y
|
||||
|
||||
if self._entity:
|
||||
self._entity.x = new_x
|
||||
self._entity.y = new_y
|
||||
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.grid = None
|
||||
self.entities = []
|
||||
|
||||
def create_grid(self, tileset):
|
||||
"""Create the McRogueFace grid"""
|
||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||
self.grid.position = (100, 100)
|
||||
self.grid.size = (800, 480)
|
||||
self.fill_with_walls()
|
||||
return self.grid
|
||||
|
||||
def fill_with_walls(self):
|
||||
"""Fill the entire map with wall tiles"""
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
self.set_tile(x, y, walkable=False, transparent=False,
|
||||
sprite_index=35, color=(100, 100, 100))
|
||||
|
||||
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
||||
"""Set properties for a specific tile"""
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
cell = self.grid.at(x, y)
|
||||
cell.walkable = walkable
|
||||
cell.transparent = transparent
|
||||
cell.sprite_index = sprite_index
|
||||
cell.color = mcrfpy.Color(*color)
|
||||
|
||||
def create_room(self, x1, y1, x2, y2):
|
||||
"""Carve out a room in the map"""
|
||||
x1, x2 = min(x1, x2), max(x1, x2)
|
||||
y1, y2 = min(y1, y2), max(y1, y2)
|
||||
|
||||
for y in range(y1, y2 + 1):
|
||||
for x in range(x1, x2 + 1):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(50, 50, 50))
|
||||
|
||||
def create_tunnel_h(self, x1, x2, y):
|
||||
"""Create a horizontal tunnel"""
|
||||
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(50, 50, 50))
|
||||
|
||||
def create_tunnel_v(self, y1, y2, x):
|
||||
"""Create a vertical tunnel"""
|
||||
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(50, 50, 50))
|
||||
|
||||
def is_blocked(self, x, y):
|
||||
"""Check if a tile blocks movement"""
|
||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||
return True
|
||||
|
||||
if not self.grid.at(x, y).walkable:
|
||||
return True
|
||||
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_entity(self, entity):
|
||||
"""Add a GameObject to the map"""
|
||||
self.entities.append(entity)
|
||||
entity.attach_to_grid(self.grid)
|
||||
|
||||
def get_blocking_entity_at(self, x, y):
|
||||
"""Return any blocking entity at the given position"""
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return entity
|
||||
return None
|
||||
|
||||
class Engine:
|
||||
"""Main game engine that manages game state"""
|
||||
|
||||
def __init__(self):
|
||||
self.game_map = None
|
||||
self.player = None
|
||||
self.entities = []
|
||||
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 2"
|
||||
|
||||
self.ui = mcrfpy.sceneUI("game")
|
||||
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
self.ui.append(background)
|
||||
|
||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
self.setup_game()
|
||||
self.setup_input()
|
||||
self.setup_ui()
|
||||
|
||||
def setup_game(self):
|
||||
"""Initialize the game world"""
|
||||
self.game_map = GameMap(50, 30)
|
||||
grid = self.game_map.create_grid(self.tileset)
|
||||
self.ui.append(grid)
|
||||
|
||||
self.game_map.create_room(10, 10, 20, 20)
|
||||
self.game_map.create_room(30, 15, 40, 25)
|
||||
self.game_map.create_room(15, 22, 25, 28)
|
||||
|
||||
self.game_map.create_tunnel_h(20, 30, 15)
|
||||
self.game_map.create_tunnel_v(20, 22, 20)
|
||||
|
||||
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
|
||||
self.game_map.add_entity(self.player)
|
||||
|
||||
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
|
||||
self.game_map.add_entity(npc)
|
||||
self.entities.append(npc)
|
||||
|
||||
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
|
||||
self.game_map.add_entity(potion)
|
||||
self.entities.append(potion)
|
||||
|
||||
def handle_movement(self, dx, dy):
|
||||
"""Handle player movement"""
|
||||
new_x = self.player.x + dx
|
||||
new_y = self.player.y + dy
|
||||
|
||||
if not self.game_map.is_blocked(new_x, new_y):
|
||||
self.player.move(dx, dy)
|
||||
else:
|
||||
target = self.game_map.get_blocking_entity_at(new_x, new_y)
|
||||
if target:
|
||||
print(f"You bump into the {target.name}!")
|
||||
|
||||
def setup_input(self):
|
||||
"""Setup keyboard input handling"""
|
||||
def handle_keys(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
movement = {
|
||||
"Up": (0, -1), "Down": (0, 1),
|
||||
"Left": (-1, 0), "Right": (1, 0),
|
||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||
"Num4": (-1, 0), "Num6": (1, 0),
|
||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||
}
|
||||
|
||||
if key in movement:
|
||||
dx, dy = movement[key]
|
||||
self.handle_movement(dx, dy)
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup UI elements"""
|
||||
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
self.ui.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.ui.append(instructions)
|
||||
|
||||
# Create and run the game
|
||||
engine = Engine()
|
||||
print("Part 2: Entities and Maps!")
|
||||
```
|
||||
|
||||
## Understanding the Architecture
|
||||
|
||||
### GameObject Class
|
||||
Our `GameObject` class wraps McRogueFace's `Entity` and adds:
|
||||
- Game logic properties (name, blocking)
|
||||
- Position tracking independent of the visual entity
|
||||
- Easy attachment/detachment from grids
|
||||
|
||||
### GameMap Class
|
||||
The `GameMap` manages:
|
||||
- The McRogueFace `Grid` for visual representation
|
||||
- A list of all entities in the map
|
||||
- Collision detection including entity blocking
|
||||
- Map generation utilities (rooms, tunnels)
|
||||
|
||||
### Engine Class
|
||||
The `Engine` coordinates everything:
|
||||
- Scene and UI setup
|
||||
- Game state management
|
||||
- Input handling
|
||||
- Entity-map interactions
|
||||
|
||||
## Key Improvements from Part 1
|
||||
|
||||
1. **Proper Entity Management**: Multiple entities can exist and interact
|
||||
2. **Blocking Entities**: Some entities block movement, others don't
|
||||
3. **Map Generation**: Tools for creating rooms and tunnels
|
||||
4. **Collision System**: Checks both tiles and entities
|
||||
5. **Organized Code**: Clear separation of concerns
|
||||
|
||||
## Exercises
|
||||
|
||||
1. **Add More Entity Types**: Create different sprites for monsters, items, and NPCs
|
||||
2. **Entity Interactions**: Make items disappear when walked over
|
||||
3. **Random Map Generation**: Place rooms and tunnels randomly
|
||||
4. **Entity Properties**: Add health, damage, or other attributes to GameObjects
|
||||
|
||||
## What's Next?
|
||||
|
||||
In Part 3, we'll implement proper dungeon generation with:
|
||||
- Procedurally generated rooms
|
||||
- Smart tunnel routing
|
||||
- Entity spawning
|
||||
- The beginning of a real roguelike dungeon!
|
||||
|
||||
We now have a solid foundation with proper entity management and map structure. This architecture will serve us well as we add more complex features to our roguelike!
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
import mcrfpy
|
||||
|
||||
class GameObject:
|
||||
"""Base class for all game objects (player, monsters, items)"""
|
||||
|
||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.sprite_index = sprite_index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.blocks = blocks
|
||||
self._entity = None
|
||||
self.grid = None
|
||||
|
||||
def attach_to_grid(self, grid):
|
||||
"""Attach this game object to a McRogueFace grid"""
|
||||
self.grid = grid
|
||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||
self._entity.sprite_index = self.sprite_index
|
||||
self._entity.color = mcrfpy.Color(*self.color)
|
||||
|
||||
def move(self, dx, dy):
|
||||
"""Move by the given amount if possible"""
|
||||
if not self.grid:
|
||||
return
|
||||
|
||||
new_x = self.x + dx
|
||||
new_y = self.y + dy
|
||||
|
||||
self.x = new_x
|
||||
self.y = new_y
|
||||
|
||||
if self._entity:
|
||||
self._entity.x = new_x
|
||||
self._entity.y = new_y
|
||||
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.grid = None
|
||||
self.entities = []
|
||||
|
||||
def create_grid(self, tileset):
|
||||
"""Create the McRogueFace grid"""
|
||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||
self.grid.position = (100, 100)
|
||||
self.grid.size = (800, 480)
|
||||
self.fill_with_walls()
|
||||
return self.grid
|
||||
|
||||
def fill_with_walls(self):
|
||||
"""Fill the entire map with wall tiles"""
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
self.set_tile(x, y, walkable=False, transparent=False,
|
||||
sprite_index=35, color=(100, 100, 100))
|
||||
|
||||
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
||||
"""Set properties for a specific tile"""
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
cell = self.grid.at(x, y)
|
||||
cell.walkable = walkable
|
||||
cell.transparent = transparent
|
||||
cell.sprite_index = sprite_index
|
||||
cell.color = mcrfpy.Color(*color)
|
||||
|
||||
def create_room(self, x1, y1, x2, y2):
|
||||
"""Carve out a room in the map"""
|
||||
x1, x2 = min(x1, x2), max(x1, x2)
|
||||
y1, y2 = min(y1, y2), max(y1, y2)
|
||||
|
||||
for y in range(y1, y2 + 1):
|
||||
for x in range(x1, x2 + 1):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(50, 50, 50))
|
||||
|
||||
def create_tunnel_h(self, x1, x2, y):
|
||||
"""Create a horizontal tunnel"""
|
||||
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(50, 50, 50))
|
||||
|
||||
def create_tunnel_v(self, y1, y2, x):
|
||||
"""Create a vertical tunnel"""
|
||||
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(50, 50, 50))
|
||||
|
||||
def is_blocked(self, x, y):
|
||||
"""Check if a tile blocks movement"""
|
||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||
return True
|
||||
|
||||
if not self.grid.at(x, y).walkable:
|
||||
return True
|
||||
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_entity(self, entity):
|
||||
"""Add a GameObject to the map"""
|
||||
self.entities.append(entity)
|
||||
entity.attach_to_grid(self.grid)
|
||||
|
||||
def get_blocking_entity_at(self, x, y):
|
||||
"""Return any blocking entity at the given position"""
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return entity
|
||||
return None
|
||||
|
||||
class Engine:
|
||||
"""Main game engine that manages game state"""
|
||||
|
||||
def __init__(self):
|
||||
self.game_map = None
|
||||
self.player = None
|
||||
self.entities = []
|
||||
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 2"
|
||||
|
||||
self.ui = mcrfpy.sceneUI("game")
|
||||
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
self.ui.append(background)
|
||||
|
||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
self.setup_game()
|
||||
self.setup_input()
|
||||
self.setup_ui()
|
||||
|
||||
def setup_game(self):
|
||||
"""Initialize the game world"""
|
||||
self.game_map = GameMap(50, 30)
|
||||
grid = self.game_map.create_grid(self.tileset)
|
||||
self.ui.append(grid)
|
||||
|
||||
self.game_map.create_room(10, 10, 20, 20)
|
||||
self.game_map.create_room(30, 15, 40, 25)
|
||||
self.game_map.create_room(15, 22, 25, 28)
|
||||
|
||||
self.game_map.create_tunnel_h(20, 30, 15)
|
||||
self.game_map.create_tunnel_v(20, 22, 20)
|
||||
|
||||
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
|
||||
self.game_map.add_entity(self.player)
|
||||
|
||||
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
|
||||
self.game_map.add_entity(npc)
|
||||
self.entities.append(npc)
|
||||
|
||||
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
|
||||
self.game_map.add_entity(potion)
|
||||
self.entities.append(potion)
|
||||
|
||||
def handle_movement(self, dx, dy):
|
||||
"""Handle player movement"""
|
||||
new_x = self.player.x + dx
|
||||
new_y = self.player.y + dy
|
||||
|
||||
if not self.game_map.is_blocked(new_x, new_y):
|
||||
self.player.move(dx, dy)
|
||||
else:
|
||||
target = self.game_map.get_blocking_entity_at(new_x, new_y)
|
||||
if target:
|
||||
print(f"You bump into the {target.name}!")
|
||||
|
||||
def setup_input(self):
|
||||
"""Setup keyboard input handling"""
|
||||
def handle_keys(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
movement = {
|
||||
"Up": (0, -1), "Down": (0, 1),
|
||||
"Left": (-1, 0), "Right": (1, 0),
|
||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||
"Num4": (-1, 0), "Num6": (1, 0),
|
||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||
}
|
||||
|
||||
if key in movement:
|
||||
dx, dy = movement[key]
|
||||
self.handle_movement(dx, dy)
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup UI elements"""
|
||||
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
self.ui.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.ui.append(instructions)
|
||||
|
||||
# Create and run the game
|
||||
engine = Engine()
|
||||
print("Part 2: Entities and Maps!")
|
||||
|
|
@ -0,0 +1,548 @@
|
|||
# Part 3 - Generating a Dungeon
|
||||
|
||||
In Parts 1 and 2, we created a player that could move around and interact with a hand-crafted dungeon. Now it's time to generate dungeons procedurally - a core feature of any roguelike game!
|
||||
|
||||
## The Plan
|
||||
|
||||
We'll create a dungeon generator that:
|
||||
1. Places rectangular rooms randomly
|
||||
2. Ensures rooms don't overlap
|
||||
3. Connects rooms with tunnels
|
||||
4. Places the player in the first room
|
||||
|
||||
This is a classic approach used by many roguelikes, and it creates interesting, playable dungeons.
|
||||
|
||||
## Creating a Room Class
|
||||
|
||||
First, let's create a class to represent rectangular rooms:
|
||||
|
||||
```python
|
||||
class RectangularRoom:
|
||||
"""A rectangular room with its position and size"""
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
@property
|
||||
def inner(self):
|
||||
"""Return the inner area of the room as a tuple of slices
|
||||
|
||||
This property returns the area inside the walls.
|
||||
We'll add 1 to min coordinates and subtract 1 from max coordinates.
|
||||
"""
|
||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||
|
||||
def intersects(self, other):
|
||||
"""Return True if this room overlaps with another RectangularRoom"""
|
||||
return (
|
||||
self.x1 <= other.x2
|
||||
and self.x2 >= other.x1
|
||||
and self.y1 <= other.y2
|
||||
and self.y2 >= other.y1
|
||||
)
|
||||
```
|
||||
|
||||
## Implementing Tunnel Generation
|
||||
|
||||
Since McRogueFace doesn't include line-drawing algorithms, let's implement simple L-shaped tunnels:
|
||||
|
||||
```python
|
||||
def tunnel_between(start, end):
|
||||
"""Return an L-shaped tunnel between two points"""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
# Randomly decide whether to go horizontal first or vertical first
|
||||
if random.random() < 0.5:
|
||||
# Horizontal, then vertical
|
||||
corner_x = x2
|
||||
corner_y = y1
|
||||
else:
|
||||
# Vertical, then horizontal
|
||||
corner_x = x1
|
||||
corner_y = y2
|
||||
|
||||
# Generate the coordinates
|
||||
# First line: from start to corner
|
||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||
yield x, y1
|
||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||
yield corner_x, y
|
||||
|
||||
# Second line: from corner to end
|
||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||
yield x, corner_y
|
||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||
yield x2, y
|
||||
```
|
||||
|
||||
## The Dungeon Generator
|
||||
|
||||
Now let's update our GameMap class to generate dungeons:
|
||||
|
||||
```python
|
||||
import random
|
||||
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.grid = None
|
||||
self.entities = []
|
||||
self.rooms = [] # Keep track of rooms for game logic
|
||||
|
||||
def generate_dungeon(
|
||||
self,
|
||||
max_rooms,
|
||||
room_min_size,
|
||||
room_max_size,
|
||||
player
|
||||
):
|
||||
"""Generate a new dungeon map"""
|
||||
# Start with everything as walls
|
||||
self.fill_with_walls()
|
||||
|
||||
for r in range(max_rooms):
|
||||
# Random width and height
|
||||
room_width = random.randint(room_min_size, room_max_size)
|
||||
room_height = random.randint(room_min_size, room_max_size)
|
||||
|
||||
# Random position without going out of bounds
|
||||
x = random.randint(0, self.width - room_width - 1)
|
||||
y = random.randint(0, self.height - room_height - 1)
|
||||
|
||||
# Create the room
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
# Check if it intersects with any existing room
|
||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||
continue # This room intersects, so go to the next attempt
|
||||
|
||||
# If we get here, it's a valid room
|
||||
|
||||
# Carve out this room
|
||||
self.carve_room(new_room)
|
||||
|
||||
# Place the player in the center of the first room
|
||||
if len(self.rooms) == 0:
|
||||
player.x, player.y = new_room.center
|
||||
if player._entity:
|
||||
player._entity.x, player._entity.y = new_room.center
|
||||
else:
|
||||
# All rooms after the first:
|
||||
# Tunnel between this room and the previous one
|
||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||
|
||||
# Finally, append the new room to the list
|
||||
self.rooms.append(new_room)
|
||||
|
||||
def carve_room(self, room):
|
||||
"""Carve out a room"""
|
||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||
|
||||
for y in range(inner_y1, inner_y2):
|
||||
for x in range(inner_x1, inner_x2):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(50, 50, 50))
|
||||
|
||||
def carve_tunnel(self, start, end):
|
||||
"""Carve a tunnel between two points"""
|
||||
for x, y in tunnel_between(start, end):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(30, 30, 40)) # Slightly different color for tunnels
|
||||
```
|
||||
|
||||
## Complete Code
|
||||
|
||||
Here's the complete `game.py` with procedural dungeon generation:
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
class GameObject:
|
||||
"""Base class for all game objects"""
|
||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.sprite_index = sprite_index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.blocks = blocks
|
||||
self._entity = None
|
||||
self.grid = None
|
||||
|
||||
def attach_to_grid(self, grid):
|
||||
"""Attach this game object to a McRogueFace grid"""
|
||||
self.grid = grid
|
||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||
self._entity.sprite_index = self.sprite_index
|
||||
self._entity.color = mcrfpy.Color(*self.color)
|
||||
|
||||
def move(self, dx, dy):
|
||||
"""Move by the given amount"""
|
||||
if not self.grid:
|
||||
return
|
||||
self.x += dx
|
||||
self.y += dy
|
||||
if self._entity:
|
||||
self._entity.x = self.x
|
||||
self._entity.y = self.y
|
||||
|
||||
class RectangularRoom:
|
||||
"""A rectangular room with its position and size"""
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
@property
|
||||
def inner(self):
|
||||
"""Return the inner area of the room"""
|
||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
def tunnel_between(start, end):
|
||||
"""Return an L-shaped tunnel between two points"""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
if random.random() < 0.5:
|
||||
corner_x = x2
|
||||
corner_y = y1
|
||||
else:
|
||||
corner_x = x1
|
||||
corner_y = y2
|
||||
|
||||
# Generate the coordinates
|
||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||
yield x, y1
|
||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||
yield corner_x, y
|
||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||
yield x, corner_y
|
||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||
yield x2, y
|
||||
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.grid = None
|
||||
self.entities = []
|
||||
self.rooms = []
|
||||
|
||||
def create_grid(self, tileset):
|
||||
"""Create the McRogueFace grid"""
|
||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||
self.grid.position = (100, 100)
|
||||
self.grid.size = (800, 480)
|
||||
return self.grid
|
||||
|
||||
def fill_with_walls(self):
|
||||
"""Fill the entire map with wall tiles"""
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
self.set_tile(x, y, walkable=False, transparent=False,
|
||||
sprite_index=35, color=(100, 100, 100))
|
||||
|
||||
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
||||
"""Set properties for a specific tile"""
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
cell = self.grid.at(x, y)
|
||||
cell.walkable = walkable
|
||||
cell.transparent = transparent
|
||||
cell.sprite_index = sprite_index
|
||||
cell.color = mcrfpy.Color(*color)
|
||||
|
||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
|
||||
"""Generate a new dungeon map"""
|
||||
self.fill_with_walls()
|
||||
|
||||
for r in range(max_rooms):
|
||||
room_width = random.randint(room_min_size, room_max_size)
|
||||
room_height = random.randint(room_min_size, room_max_size)
|
||||
|
||||
x = random.randint(0, self.width - room_width - 1)
|
||||
y = random.randint(0, self.height - room_height - 1)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||
continue
|
||||
|
||||
self.carve_room(new_room)
|
||||
|
||||
if len(self.rooms) == 0:
|
||||
player.x, player.y = new_room.center
|
||||
if player._entity:
|
||||
player._entity.x, player._entity.y = new_room.center
|
||||
else:
|
||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||
|
||||
self.rooms.append(new_room)
|
||||
|
||||
def carve_room(self, room):
|
||||
"""Carve out a room"""
|
||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||
|
||||
for y in range(inner_y1, inner_y2):
|
||||
for x in range(inner_x1, inner_x2):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(50, 50, 50))
|
||||
|
||||
def carve_tunnel(self, start, end):
|
||||
"""Carve a tunnel between two points"""
|
||||
for x, y in tunnel_between(start, end):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(30, 30, 40))
|
||||
|
||||
def is_blocked(self, x, y):
|
||||
"""Check if a tile blocks movement"""
|
||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||
return True
|
||||
if not self.grid.at(x, y).walkable:
|
||||
return True
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_entity(self, entity):
|
||||
"""Add a GameObject to the map"""
|
||||
self.entities.append(entity)
|
||||
entity.attach_to_grid(self.grid)
|
||||
|
||||
class Engine:
|
||||
"""Main game engine"""
|
||||
|
||||
def __init__(self):
|
||||
self.game_map = None
|
||||
self.player = None
|
||||
self.entities = []
|
||||
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 3"
|
||||
|
||||
self.ui = mcrfpy.sceneUI("game")
|
||||
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
self.ui.append(background)
|
||||
|
||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
self.setup_game()
|
||||
self.setup_input()
|
||||
self.setup_ui()
|
||||
|
||||
def setup_game(self):
|
||||
"""Initialize the game world"""
|
||||
self.game_map = GameMap(80, 45)
|
||||
grid = self.game_map.create_grid(self.tileset)
|
||||
self.ui.append(grid)
|
||||
|
||||
# Create player (before dungeon generation)
|
||||
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
||||
|
||||
# Generate the dungeon
|
||||
self.game_map.generate_dungeon(
|
||||
max_rooms=30,
|
||||
room_min_size=6,
|
||||
room_max_size=10,
|
||||
player=self.player
|
||||
)
|
||||
|
||||
# Add player to map
|
||||
self.game_map.add_entity(self.player)
|
||||
|
||||
# Add some monsters in random rooms
|
||||
for i in range(5):
|
||||
if i < len(self.game_map.rooms) - 1: # Don't spawn in first room
|
||||
room = self.game_map.rooms[i + 1]
|
||||
x, y = room.center
|
||||
|
||||
# Create an orc
|
||||
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||
self.game_map.add_entity(orc)
|
||||
self.entities.append(orc)
|
||||
|
||||
def handle_movement(self, dx, dy):
|
||||
"""Handle player movement"""
|
||||
new_x = self.player.x + dx
|
||||
new_y = self.player.y + dy
|
||||
|
||||
if not self.game_map.is_blocked(new_x, new_y):
|
||||
self.player.move(dx, dy)
|
||||
|
||||
def setup_input(self):
|
||||
"""Setup keyboard input handling"""
|
||||
def handle_keys(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
movement = {
|
||||
"Up": (0, -1), "Down": (0, 1),
|
||||
"Left": (-1, 0), "Right": (1, 0),
|
||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||
"Num4": (-1, 0), "Num6": (1, 0),
|
||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||
}
|
||||
|
||||
if key in movement:
|
||||
dx, dy = movement[key]
|
||||
self.handle_movement(dx, dy)
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
elif key == "Space":
|
||||
# Regenerate the dungeon
|
||||
self.regenerate_dungeon()
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
def regenerate_dungeon(self):
|
||||
"""Generate a new dungeon"""
|
||||
# Clear existing entities
|
||||
self.game_map.entities.clear()
|
||||
self.game_map.rooms.clear()
|
||||
self.entities.clear()
|
||||
|
||||
# Clear the entity list in the grid
|
||||
if self.game_map.grid:
|
||||
self.game_map.grid.entities.clear()
|
||||
|
||||
# Regenerate
|
||||
self.game_map.generate_dungeon(
|
||||
max_rooms=30,
|
||||
room_min_size=6,
|
||||
room_max_size=10,
|
||||
player=self.player
|
||||
)
|
||||
|
||||
# Re-add player
|
||||
self.game_map.add_entity(self.player)
|
||||
|
||||
# Add new monsters
|
||||
for i in range(5):
|
||||
if i < len(self.game_map.rooms) - 1:
|
||||
room = self.game_map.rooms[i + 1]
|
||||
x, y = room.center
|
||||
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||
self.game_map.add_entity(orc)
|
||||
self.entities.append(orc)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup UI elements"""
|
||||
title = mcrfpy.Caption("Procedural Dungeon Generation", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
self.ui.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption("Arrow keys to move, SPACE to regenerate, ESC to quit", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.ui.append(instructions)
|
||||
|
||||
# Create and run the game
|
||||
engine = Engine()
|
||||
print("Part 3: Procedural Dungeon Generation!")
|
||||
print("Press SPACE to generate a new dungeon")
|
||||
```
|
||||
|
||||
## Understanding the Algorithm
|
||||
|
||||
Our dungeon generation algorithm is simple but effective:
|
||||
|
||||
1. **Start with solid walls** - The entire map begins filled with wall tiles
|
||||
2. **Try to place rooms** - Generate random rooms and check for overlaps
|
||||
3. **Connect with tunnels** - Each new room connects to the previous one
|
||||
4. **Place entities** - The player starts in the first room, monsters in others
|
||||
|
||||
### Room Placement
|
||||
|
||||
The algorithm attempts to place `max_rooms` rooms, but may place fewer if many attempts result in overlapping rooms. This is called "rejection sampling" - we generate random rooms and reject ones that don't fit.
|
||||
|
||||
### Tunnel Design
|
||||
|
||||
Our L-shaped tunnels are simple but effective. They either go:
|
||||
- Horizontal first, then vertical
|
||||
- Vertical first, then horizontal
|
||||
|
||||
This creates variety while ensuring all rooms are connected.
|
||||
|
||||
## Experimenting with Parameters
|
||||
|
||||
Try adjusting these parameters to create different dungeon styles:
|
||||
|
||||
```python
|
||||
# Sparse dungeon with large rooms
|
||||
self.game_map.generate_dungeon(
|
||||
max_rooms=10,
|
||||
room_min_size=10,
|
||||
room_max_size=15,
|
||||
player=self.player
|
||||
)
|
||||
|
||||
# Dense dungeon with small rooms
|
||||
self.game_map.generate_dungeon(
|
||||
max_rooms=50,
|
||||
room_min_size=4,
|
||||
room_max_size=6,
|
||||
player=self.player
|
||||
)
|
||||
```
|
||||
|
||||
## Visual Enhancements
|
||||
|
||||
Notice how we gave tunnels a slightly different color:
|
||||
- Rooms: `color=(50, 50, 50)` - Medium gray
|
||||
- Tunnels: `color=(30, 30, 40)` - Darker with blue tint
|
||||
|
||||
This subtle difference helps players understand the dungeon layout.
|
||||
|
||||
## Exercises
|
||||
|
||||
1. **Different Room Shapes**: Create circular or cross-shaped rooms
|
||||
2. **Better Tunnel Routing**: Implement A* pathfinding for more natural tunnels
|
||||
3. **Room Types**: Create special rooms (treasure rooms, trap rooms)
|
||||
4. **Dungeon Themes**: Use different tile sets and colors for different dungeon levels
|
||||
|
||||
## What's Next?
|
||||
|
||||
In Part 4, we'll implement Field of View (FOV) so the player can only see parts of the dungeon they've explored. This will add mystery and atmosphere to our procedurally generated dungeons!
|
||||
|
||||
Our dungeon generator is now creating unique, playable levels every time. The foundation of a true roguelike is taking shape!
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
import mcrfpy
|
||||
import random
|
||||
|
||||
class GameObject:
|
||||
"""Base class for all game objects"""
|
||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.sprite_index = sprite_index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.blocks = blocks
|
||||
self._entity = None
|
||||
self.grid = None
|
||||
|
||||
def attach_to_grid(self, grid):
|
||||
"""Attach this game object to a McRogueFace grid"""
|
||||
self.grid = grid
|
||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||
self._entity.sprite_index = self.sprite_index
|
||||
self._entity.color = mcrfpy.Color(*self.color)
|
||||
|
||||
def move(self, dx, dy):
|
||||
"""Move by the given amount"""
|
||||
if not self.grid:
|
||||
return
|
||||
self.x += dx
|
||||
self.y += dy
|
||||
if self._entity:
|
||||
self._entity.x = self.x
|
||||
self._entity.y = self.y
|
||||
|
||||
class RectangularRoom:
|
||||
"""A rectangular room with its position and size"""
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
@property
|
||||
def inner(self):
|
||||
"""Return the inner area of the room"""
|
||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
def tunnel_between(start, end):
|
||||
"""Return an L-shaped tunnel between two points"""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
if random.random() < 0.5:
|
||||
corner_x = x2
|
||||
corner_y = y1
|
||||
else:
|
||||
corner_x = x1
|
||||
corner_y = y2
|
||||
|
||||
# Generate the coordinates
|
||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||
yield x, y1
|
||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||
yield corner_x, y
|
||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||
yield x, corner_y
|
||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||
yield x2, y
|
||||
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.grid = None
|
||||
self.entities = []
|
||||
self.rooms = []
|
||||
|
||||
def create_grid(self, tileset):
|
||||
"""Create the McRogueFace grid"""
|
||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||
self.grid.position = (100, 100)
|
||||
self.grid.size = (800, 480)
|
||||
return self.grid
|
||||
|
||||
def fill_with_walls(self):
|
||||
"""Fill the entire map with wall tiles"""
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
self.set_tile(x, y, walkable=False, transparent=False,
|
||||
sprite_index=35, color=(100, 100, 100))
|
||||
|
||||
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
||||
"""Set properties for a specific tile"""
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
cell = self.grid.at(x, y)
|
||||
cell.walkable = walkable
|
||||
cell.transparent = transparent
|
||||
cell.sprite_index = sprite_index
|
||||
cell.color = mcrfpy.Color(*color)
|
||||
|
||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
|
||||
"""Generate a new dungeon map"""
|
||||
self.fill_with_walls()
|
||||
|
||||
for r in range(max_rooms):
|
||||
room_width = random.randint(room_min_size, room_max_size)
|
||||
room_height = random.randint(room_min_size, room_max_size)
|
||||
|
||||
x = random.randint(0, self.width - room_width - 1)
|
||||
y = random.randint(0, self.height - room_height - 1)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||
continue
|
||||
|
||||
self.carve_room(new_room)
|
||||
|
||||
if len(self.rooms) == 0:
|
||||
player.x, player.y = new_room.center
|
||||
if player._entity:
|
||||
player._entity.x, player._entity.y = new_room.center
|
||||
else:
|
||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||
|
||||
self.rooms.append(new_room)
|
||||
|
||||
def carve_room(self, room):
|
||||
"""Carve out a room"""
|
||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||
|
||||
for y in range(inner_y1, inner_y2):
|
||||
for x in range(inner_x1, inner_x2):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(50, 50, 50))
|
||||
|
||||
def carve_tunnel(self, start, end):
|
||||
"""Carve a tunnel between two points"""
|
||||
for x, y in tunnel_between(start, end):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, color=(30, 30, 40))
|
||||
|
||||
def is_blocked(self, x, y):
|
||||
"""Check if a tile blocks movement"""
|
||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||
return True
|
||||
if not self.grid.at(x, y).walkable:
|
||||
return True
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_entity(self, entity):
|
||||
"""Add a GameObject to the map"""
|
||||
self.entities.append(entity)
|
||||
entity.attach_to_grid(self.grid)
|
||||
|
||||
class Engine:
|
||||
"""Main game engine"""
|
||||
|
||||
def __init__(self):
|
||||
self.game_map = None
|
||||
self.player = None
|
||||
self.entities = []
|
||||
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 3"
|
||||
|
||||
self.ui = mcrfpy.sceneUI("game")
|
||||
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
self.ui.append(background)
|
||||
|
||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
self.setup_game()
|
||||
self.setup_input()
|
||||
self.setup_ui()
|
||||
|
||||
def setup_game(self):
|
||||
"""Initialize the game world"""
|
||||
self.game_map = GameMap(80, 45)
|
||||
grid = self.game_map.create_grid(self.tileset)
|
||||
self.ui.append(grid)
|
||||
|
||||
# Create player (before dungeon generation)
|
||||
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
||||
|
||||
# Generate the dungeon
|
||||
self.game_map.generate_dungeon(
|
||||
max_rooms=30,
|
||||
room_min_size=6,
|
||||
room_max_size=10,
|
||||
player=self.player
|
||||
)
|
||||
|
||||
# Add player to map
|
||||
self.game_map.add_entity(self.player)
|
||||
|
||||
# Add some monsters in random rooms
|
||||
for i in range(5):
|
||||
if i < len(self.game_map.rooms) - 1: # Don't spawn in first room
|
||||
room = self.game_map.rooms[i + 1]
|
||||
x, y = room.center
|
||||
|
||||
# Create an orc
|
||||
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||
self.game_map.add_entity(orc)
|
||||
self.entities.append(orc)
|
||||
|
||||
def handle_movement(self, dx, dy):
|
||||
"""Handle player movement"""
|
||||
new_x = self.player.x + dx
|
||||
new_y = self.player.y + dy
|
||||
|
||||
if not self.game_map.is_blocked(new_x, new_y):
|
||||
self.player.move(dx, dy)
|
||||
|
||||
def setup_input(self):
|
||||
"""Setup keyboard input handling"""
|
||||
def handle_keys(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
movement = {
|
||||
"Up": (0, -1), "Down": (0, 1),
|
||||
"Left": (-1, 0), "Right": (1, 0),
|
||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||
"Num4": (-1, 0), "Num6": (1, 0),
|
||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||
}
|
||||
|
||||
if key in movement:
|
||||
dx, dy = movement[key]
|
||||
self.handle_movement(dx, dy)
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
elif key == "Space":
|
||||
# Regenerate the dungeon
|
||||
self.regenerate_dungeon()
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
def regenerate_dungeon(self):
|
||||
"""Generate a new dungeon"""
|
||||
# Clear existing entities
|
||||
self.game_map.entities.clear()
|
||||
self.game_map.rooms.clear()
|
||||
self.entities.clear()
|
||||
|
||||
# Clear the entity list in the grid
|
||||
if self.game_map.grid:
|
||||
self.game_map.grid.entities.clear()
|
||||
|
||||
# Regenerate
|
||||
self.game_map.generate_dungeon(
|
||||
max_rooms=30,
|
||||
room_min_size=6,
|
||||
room_max_size=10,
|
||||
player=self.player
|
||||
)
|
||||
|
||||
# Re-add player
|
||||
self.game_map.add_entity(self.player)
|
||||
|
||||
# Add new monsters
|
||||
for i in range(5):
|
||||
if i < len(self.game_map.rooms) - 1:
|
||||
room = self.game_map.rooms[i + 1]
|
||||
x, y = room.center
|
||||
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||
self.game_map.add_entity(orc)
|
||||
self.entities.append(orc)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup UI elements"""
|
||||
title = mcrfpy.Caption("Procedural Dungeon Generation", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
self.ui.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption("Arrow keys to move, SPACE to regenerate, ESC to quit", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.ui.append(instructions)
|
||||
|
||||
# Create and run the game
|
||||
engine = Engine()
|
||||
print("Part 3: Procedural Dungeon Generation!")
|
||||
print("Press SPACE to generate a new dungeon")
|
||||
|
|
@ -0,0 +1,520 @@
|
|||
# Part 4 - Field of View
|
||||
|
||||
One of the defining features of roguelikes is exploration and discovery. In Part 3, we could see the entire dungeon at once. Now we'll implement Field of View (FOV) so players can only see what their character can actually see, adding mystery and tactical depth to our game.
|
||||
|
||||
## Understanding Field of View
|
||||
|
||||
Field of View creates three distinct visibility states for each tile:
|
||||
|
||||
1. **Visible**: Currently in the player's line of sight
|
||||
2. **Explored**: Previously seen but not currently visible
|
||||
3. **Unexplored**: Never seen (completely hidden)
|
||||
|
||||
This creates the classic "fog of war" effect where you remember the layout of areas you've explored, but can't see current enemy positions unless they're in your view.
|
||||
|
||||
## McRogueFace's FOV System
|
||||
|
||||
Good news! McRogueFace includes built-in FOV support through its C++ engine. We just need to enable and configure it. The engine uses an efficient shadowcasting algorithm that provides smooth, realistic line-of-sight calculations.
|
||||
|
||||
Let's update our code to use FOV:
|
||||
|
||||
```python
|
||||
class GameObject:
|
||||
"""Base class for all game objects"""
|
||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.sprite_index = sprite_index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.blocks = blocks
|
||||
self._entity = None
|
||||
self.grid = None
|
||||
|
||||
def attach_to_grid(self, grid):
|
||||
"""Attach this game object to a McRogueFace grid"""
|
||||
self.grid = grid
|
||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||
self._entity.sprite_index = self.sprite_index
|
||||
self._entity.color = mcrfpy.Color(*self.color)
|
||||
|
||||
def move(self, dx, dy):
|
||||
"""Move by the given amount"""
|
||||
if not self.grid:
|
||||
return
|
||||
self.x += dx
|
||||
self.y += dy
|
||||
if self._entity:
|
||||
self._entity.x = self.x
|
||||
self._entity.y = self.y
|
||||
# Update FOV when player moves
|
||||
if self.name == "Player":
|
||||
self.update_fov()
|
||||
|
||||
def update_fov(self):
|
||||
"""Update field of view from this entity's position"""
|
||||
if self._entity and self.grid:
|
||||
self._entity.update_fov(radius=8)
|
||||
```
|
||||
|
||||
## Configuring Visibility Rendering
|
||||
|
||||
McRogueFace automatically handles the rendering of visible/explored/unexplored tiles. We need to set up our grid to use perspective-based rendering:
|
||||
|
||||
```python
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def create_grid(self, tileset):
|
||||
"""Create the McRogueFace grid"""
|
||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||
self.grid.position = (100, 100)
|
||||
self.grid.size = (800, 480)
|
||||
|
||||
# Enable perspective rendering (0 = first entity = player)
|
||||
self.grid.perspective = 0
|
||||
|
||||
return self.grid
|
||||
```
|
||||
|
||||
## Visual Appearance Configuration
|
||||
|
||||
Let's define how our tiles look in different visibility states:
|
||||
|
||||
```python
|
||||
# Color configurations for visibility states
|
||||
COLORS_VISIBLE = {
|
||||
'wall': (100, 100, 100), # Light gray
|
||||
'floor': (50, 50, 50), # Dark gray
|
||||
'tunnel': (30, 30, 40), # Dark blue-gray
|
||||
}
|
||||
|
||||
COLORS_EXPLORED = {
|
||||
'wall': (50, 50, 70), # Darker, bluish
|
||||
'floor': (20, 20, 30), # Very dark
|
||||
'tunnel': (15, 15, 25), # Almost black
|
||||
}
|
||||
|
||||
# Update the tile-setting methods to store the tile type
|
||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||
"""Set properties for a specific tile"""
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
cell = self.grid.at(x, y)
|
||||
cell.walkable = walkable
|
||||
cell.transparent = transparent
|
||||
cell.sprite_index = sprite_index
|
||||
# Store both visible and explored colors
|
||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||
# The engine will automatically darken explored tiles
|
||||
```
|
||||
|
||||
## Complete Implementation
|
||||
|
||||
Here's the complete updated `game.py` with FOV:
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Color configurations for visibility
|
||||
COLORS_VISIBLE = {
|
||||
'wall': (100, 100, 100),
|
||||
'floor': (50, 50, 50),
|
||||
'tunnel': (30, 30, 40),
|
||||
}
|
||||
|
||||
class GameObject:
|
||||
"""Base class for all game objects"""
|
||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.sprite_index = sprite_index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.blocks = blocks
|
||||
self._entity = None
|
||||
self.grid = None
|
||||
|
||||
def attach_to_grid(self, grid):
|
||||
"""Attach this game object to a McRogueFace grid"""
|
||||
self.grid = grid
|
||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||
self._entity.sprite_index = self.sprite_index
|
||||
self._entity.color = mcrfpy.Color(*self.color)
|
||||
|
||||
def move(self, dx, dy):
|
||||
"""Move by the given amount"""
|
||||
if not self.grid:
|
||||
return
|
||||
self.x += dx
|
||||
self.y += dy
|
||||
if self._entity:
|
||||
self._entity.x = self.x
|
||||
self._entity.y = self.y
|
||||
# Update FOV when player moves
|
||||
if self.name == "Player":
|
||||
self.update_fov()
|
||||
|
||||
def update_fov(self):
|
||||
"""Update field of view from this entity's position"""
|
||||
if self._entity and self.grid:
|
||||
self._entity.update_fov(radius=8)
|
||||
|
||||
class RectangularRoom:
|
||||
"""A rectangular room with its position and size"""
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return center_x, center_y
|
||||
|
||||
@property
|
||||
def inner(self):
|
||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||
|
||||
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 tunnel_between(start, end):
|
||||
"""Return an L-shaped tunnel between two points"""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
if random.random() < 0.5:
|
||||
corner_x = x2
|
||||
corner_y = y1
|
||||
else:
|
||||
corner_x = x1
|
||||
corner_y = y2
|
||||
|
||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||
yield x, y1
|
||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||
yield corner_x, y
|
||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||
yield x, corner_y
|
||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||
yield x2, y
|
||||
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.grid = None
|
||||
self.entities = []
|
||||
self.rooms = []
|
||||
|
||||
def create_grid(self, tileset):
|
||||
"""Create the McRogueFace grid"""
|
||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||
self.grid.position = (100, 100)
|
||||
self.grid.size = (800, 480)
|
||||
|
||||
# Enable perspective rendering (0 = first entity = player)
|
||||
self.grid.perspective = 0
|
||||
|
||||
return self.grid
|
||||
|
||||
def fill_with_walls(self):
|
||||
"""Fill the entire map with wall tiles"""
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
self.set_tile(x, y, walkable=False, transparent=False,
|
||||
sprite_index=35, tile_type='wall')
|
||||
|
||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||
"""Set properties for a specific tile"""
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
cell = self.grid.at(x, y)
|
||||
cell.walkable = walkable
|
||||
cell.transparent = transparent
|
||||
cell.sprite_index = sprite_index
|
||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||
|
||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
|
||||
"""Generate a new dungeon map"""
|
||||
self.fill_with_walls()
|
||||
|
||||
for r in range(max_rooms):
|
||||
room_width = random.randint(room_min_size, room_max_size)
|
||||
room_height = random.randint(room_min_size, room_max_size)
|
||||
|
||||
x = random.randint(0, self.width - room_width - 1)
|
||||
y = random.randint(0, self.height - room_height - 1)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||
continue
|
||||
|
||||
self.carve_room(new_room)
|
||||
|
||||
if len(self.rooms) == 0:
|
||||
player.x, player.y = new_room.center
|
||||
if player._entity:
|
||||
player._entity.x, player._entity.y = new_room.center
|
||||
else:
|
||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||
|
||||
self.rooms.append(new_room)
|
||||
|
||||
def carve_room(self, room):
|
||||
"""Carve out a room"""
|
||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||
|
||||
for y in range(inner_y1, inner_y2):
|
||||
for x in range(inner_x1, inner_x2):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, tile_type='floor')
|
||||
|
||||
def carve_tunnel(self, start, end):
|
||||
"""Carve a tunnel between two points"""
|
||||
for x, y in tunnel_between(start, end):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, tile_type='tunnel')
|
||||
|
||||
def is_blocked(self, x, y):
|
||||
"""Check if a tile blocks movement"""
|
||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||
return True
|
||||
if not self.grid.at(x, y).walkable:
|
||||
return True
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_entity(self, entity):
|
||||
"""Add a GameObject to the map"""
|
||||
self.entities.append(entity)
|
||||
entity.attach_to_grid(self.grid)
|
||||
|
||||
class Engine:
|
||||
"""Main game engine"""
|
||||
|
||||
def __init__(self):
|
||||
self.game_map = None
|
||||
self.player = None
|
||||
self.entities = []
|
||||
self.fov_radius = 8
|
||||
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 4"
|
||||
|
||||
self.ui = mcrfpy.sceneUI("game")
|
||||
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
self.ui.append(background)
|
||||
|
||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
self.setup_game()
|
||||
self.setup_input()
|
||||
self.setup_ui()
|
||||
|
||||
def setup_game(self):
|
||||
"""Initialize the game world"""
|
||||
self.game_map = GameMap(80, 45)
|
||||
grid = self.game_map.create_grid(self.tileset)
|
||||
self.ui.append(grid)
|
||||
|
||||
# Create player
|
||||
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
||||
|
||||
# Generate the dungeon
|
||||
self.game_map.generate_dungeon(
|
||||
max_rooms=30,
|
||||
room_min_size=6,
|
||||
room_max_size=10,
|
||||
player=self.player
|
||||
)
|
||||
|
||||
# Add player to map
|
||||
self.game_map.add_entity(self.player)
|
||||
|
||||
# Add monsters in random rooms
|
||||
for i in range(10):
|
||||
if i < len(self.game_map.rooms) - 1:
|
||||
room = self.game_map.rooms[i + 1]
|
||||
x, y = room.center
|
||||
|
||||
# Randomly offset from center
|
||||
x += random.randint(-2, 2)
|
||||
y += random.randint(-2, 2)
|
||||
|
||||
# Make sure position is walkable
|
||||
if self.game_map.grid.at(x, y).walkable:
|
||||
if i % 2 == 0:
|
||||
# Create an orc
|
||||
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||
self.game_map.add_entity(orc)
|
||||
self.entities.append(orc)
|
||||
else:
|
||||
# Create a troll
|
||||
troll = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
||||
self.game_map.add_entity(troll)
|
||||
self.entities.append(troll)
|
||||
|
||||
# Initial FOV calculation
|
||||
self.player.update_fov()
|
||||
|
||||
def handle_movement(self, dx, dy):
|
||||
"""Handle player movement"""
|
||||
new_x = self.player.x + dx
|
||||
new_y = self.player.y + dy
|
||||
|
||||
if not self.game_map.is_blocked(new_x, new_y):
|
||||
self.player.move(dx, dy)
|
||||
|
||||
def setup_input(self):
|
||||
"""Setup keyboard input handling"""
|
||||
def handle_keys(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
movement = {
|
||||
"Up": (0, -1), "Down": (0, 1),
|
||||
"Left": (-1, 0), "Right": (1, 0),
|
||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||
"Num4": (-1, 0), "Num6": (1, 0),
|
||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||
}
|
||||
|
||||
if key in movement:
|
||||
dx, dy = movement[key]
|
||||
self.handle_movement(dx, dy)
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
elif key == "v":
|
||||
# Toggle FOV on/off
|
||||
if self.game_map.grid.perspective == 0:
|
||||
self.game_map.grid.perspective = -1 # Omniscient
|
||||
print("FOV disabled - omniscient view")
|
||||
else:
|
||||
self.game_map.grid.perspective = 0 # Player perspective
|
||||
print("FOV enabled - player perspective")
|
||||
elif key == "Plus" or key == "Equals":
|
||||
# Increase FOV radius
|
||||
self.fov_radius = min(self.fov_radius + 1, 20)
|
||||
self.player._entity.update_fov(radius=self.fov_radius)
|
||||
print(f"FOV radius: {self.fov_radius}")
|
||||
elif key == "Minus":
|
||||
# Decrease FOV radius
|
||||
self.fov_radius = max(self.fov_radius - 1, 3)
|
||||
self.player._entity.update_fov(radius=self.fov_radius)
|
||||
print(f"FOV radius: {self.fov_radius}")
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup UI elements"""
|
||||
title = mcrfpy.Caption("Field of View", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
self.ui.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption("Arrow keys to move | V to toggle FOV | +/- to adjust radius | ESC to quit", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.ui.append(instructions)
|
||||
|
||||
# FOV indicator
|
||||
self.fov_text = mcrfpy.Caption(f"FOV Radius: {self.fov_radius}", 900, 100)
|
||||
self.fov_text.font_size = 14
|
||||
self.fov_text.fill_color = mcrfpy.Color(150, 200, 255)
|
||||
self.ui.append(self.fov_text)
|
||||
|
||||
# Create and run the game
|
||||
engine = Engine()
|
||||
print("Part 4: Field of View!")
|
||||
print("Press V to toggle FOV on/off")
|
||||
print("Press +/- to adjust FOV radius")
|
||||
```
|
||||
|
||||
## How FOV Works
|
||||
|
||||
McRogueFace's built-in FOV system uses a shadowcasting algorithm that:
|
||||
|
||||
1. **Casts rays** from the player's position to tiles within the radius
|
||||
2. **Checks transparency** along each ray path
|
||||
3. **Marks tiles as visible** if the ray reaches them unobstructed
|
||||
4. **Remembers explored tiles** automatically
|
||||
|
||||
The engine handles all the complex calculations in C++ for optimal performance.
|
||||
|
||||
## Visibility States in Detail
|
||||
|
||||
### Visible Tiles
|
||||
- Currently in the player's line of sight
|
||||
- Rendered at full brightness
|
||||
- Show current entity positions
|
||||
|
||||
### Explored Tiles
|
||||
- Previously seen but not currently visible
|
||||
- Rendered darker/muted
|
||||
- Show remembered terrain but not entities
|
||||
|
||||
### Unexplored Tiles
|
||||
- Never been in the player's FOV
|
||||
- Rendered as black/invisible
|
||||
- Complete mystery to the player
|
||||
|
||||
## FOV Parameters
|
||||
|
||||
You can customize FOV behavior:
|
||||
|
||||
```python
|
||||
# Basic FOV update
|
||||
entity.update_fov(radius=8)
|
||||
|
||||
# The grid's perspective property controls rendering:
|
||||
grid.perspective = 0 # Use first entity's FOV (player)
|
||||
grid.perspective = 1 # Use second entity's FOV
|
||||
grid.perspective = -1 # Omniscient (no FOV, see everything)
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
McRogueFace's C++ FOV implementation is highly optimized:
|
||||
- Uses efficient shadowcasting algorithm
|
||||
- Only recalculates when needed
|
||||
- Handles large maps smoothly
|
||||
- Automatically culls entities outside FOV
|
||||
|
||||
## Visual Polish
|
||||
|
||||
The engine automatically handles visual transitions:
|
||||
- Smooth color changes between visibility states
|
||||
- Entities fade in/out of view
|
||||
- Explored areas remain visible but dimmed
|
||||
|
||||
## Exercises
|
||||
|
||||
1. **Variable Vision**: Give different entities different FOV radii
|
||||
2. **Light Sources**: Create torches that expand local FOV
|
||||
3. **Blind Spots**: Add pillars that create interesting shadows
|
||||
4. **X-Ray Vision**: Temporary power-up to see through walls
|
||||
|
||||
## What's Next?
|
||||
|
||||
In Part 5, we'll place enemies throughout the dungeon and implement basic interactions. With FOV in place, enemies will appear and disappear as you explore, creating tension and surprise!
|
||||
|
||||
Field of View transforms our dungeon from a tactical puzzle into a mysterious world to explore. The fog of war adds atmosphere and gameplay depth that's essential to the roguelike experience.
|
||||
|
|
@ -0,0 +1,334 @@
|
|||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Color configurations for visibility
|
||||
COLORS_VISIBLE = {
|
||||
'wall': (100, 100, 100),
|
||||
'floor': (50, 50, 50),
|
||||
'tunnel': (30, 30, 40),
|
||||
}
|
||||
|
||||
class GameObject:
|
||||
"""Base class for all game objects"""
|
||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.sprite_index = sprite_index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.blocks = blocks
|
||||
self._entity = None
|
||||
self.grid = None
|
||||
|
||||
def attach_to_grid(self, grid):
|
||||
"""Attach this game object to a McRogueFace grid"""
|
||||
self.grid = grid
|
||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||
self._entity.sprite_index = self.sprite_index
|
||||
self._entity.color = mcrfpy.Color(*self.color)
|
||||
|
||||
def move(self, dx, dy):
|
||||
"""Move by the given amount"""
|
||||
if not self.grid:
|
||||
return
|
||||
self.x += dx
|
||||
self.y += dy
|
||||
if self._entity:
|
||||
self._entity.x = self.x
|
||||
self._entity.y = self.y
|
||||
# Update FOV when player moves
|
||||
if self.name == "Player":
|
||||
self.update_fov()
|
||||
|
||||
def update_fov(self):
|
||||
"""Update field of view from this entity's position"""
|
||||
if self._entity and self.grid:
|
||||
self._entity.update_fov(radius=8)
|
||||
|
||||
class RectangularRoom:
|
||||
"""A rectangular room with its position and size"""
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return center_x, center_y
|
||||
|
||||
@property
|
||||
def inner(self):
|
||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||
|
||||
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 tunnel_between(start, end):
|
||||
"""Return an L-shaped tunnel between two points"""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
if random.random() < 0.5:
|
||||
corner_x = x2
|
||||
corner_y = y1
|
||||
else:
|
||||
corner_x = x1
|
||||
corner_y = y2
|
||||
|
||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||
yield x, y1
|
||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||
yield corner_x, y
|
||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||
yield x, corner_y
|
||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||
yield x2, y
|
||||
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.grid = None
|
||||
self.entities = []
|
||||
self.rooms = []
|
||||
|
||||
def create_grid(self, tileset):
|
||||
"""Create the McRogueFace grid"""
|
||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||
self.grid.position = (100, 100)
|
||||
self.grid.size = (800, 480)
|
||||
|
||||
# Enable perspective rendering (0 = first entity = player)
|
||||
self.grid.perspective = 0
|
||||
|
||||
return self.grid
|
||||
|
||||
def fill_with_walls(self):
|
||||
"""Fill the entire map with wall tiles"""
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
self.set_tile(x, y, walkable=False, transparent=False,
|
||||
sprite_index=35, tile_type='wall')
|
||||
|
||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||
"""Set properties for a specific tile"""
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
cell = self.grid.at(x, y)
|
||||
cell.walkable = walkable
|
||||
cell.transparent = transparent
|
||||
cell.sprite_index = sprite_index
|
||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||
|
||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
|
||||
"""Generate a new dungeon map"""
|
||||
self.fill_with_walls()
|
||||
|
||||
for r in range(max_rooms):
|
||||
room_width = random.randint(room_min_size, room_max_size)
|
||||
room_height = random.randint(room_min_size, room_max_size)
|
||||
|
||||
x = random.randint(0, self.width - room_width - 1)
|
||||
y = random.randint(0, self.height - room_height - 1)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||
continue
|
||||
|
||||
self.carve_room(new_room)
|
||||
|
||||
if len(self.rooms) == 0:
|
||||
player.x, player.y = new_room.center
|
||||
if player._entity:
|
||||
player._entity.x, player._entity.y = new_room.center
|
||||
else:
|
||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||
|
||||
self.rooms.append(new_room)
|
||||
|
||||
def carve_room(self, room):
|
||||
"""Carve out a room"""
|
||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||
|
||||
for y in range(inner_y1, inner_y2):
|
||||
for x in range(inner_x1, inner_x2):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, tile_type='floor')
|
||||
|
||||
def carve_tunnel(self, start, end):
|
||||
"""Carve a tunnel between two points"""
|
||||
for x, y in tunnel_between(start, end):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, tile_type='tunnel')
|
||||
|
||||
def is_blocked(self, x, y):
|
||||
"""Check if a tile blocks movement"""
|
||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||
return True
|
||||
if not self.grid.at(x, y).walkable:
|
||||
return True
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_entity(self, entity):
|
||||
"""Add a GameObject to the map"""
|
||||
self.entities.append(entity)
|
||||
entity.attach_to_grid(self.grid)
|
||||
|
||||
class Engine:
|
||||
"""Main game engine"""
|
||||
|
||||
def __init__(self):
|
||||
self.game_map = None
|
||||
self.player = None
|
||||
self.entities = []
|
||||
self.fov_radius = 8
|
||||
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 4"
|
||||
|
||||
self.ui = mcrfpy.sceneUI("game")
|
||||
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
self.ui.append(background)
|
||||
|
||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
self.setup_game()
|
||||
self.setup_input()
|
||||
self.setup_ui()
|
||||
|
||||
def setup_game(self):
|
||||
"""Initialize the game world"""
|
||||
self.game_map = GameMap(80, 45)
|
||||
grid = self.game_map.create_grid(self.tileset)
|
||||
self.ui.append(grid)
|
||||
|
||||
# Create player
|
||||
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
||||
|
||||
# Generate the dungeon
|
||||
self.game_map.generate_dungeon(
|
||||
max_rooms=30,
|
||||
room_min_size=6,
|
||||
room_max_size=10,
|
||||
player=self.player
|
||||
)
|
||||
|
||||
# Add player to map
|
||||
self.game_map.add_entity(self.player)
|
||||
|
||||
# Add monsters in random rooms
|
||||
for i in range(10):
|
||||
if i < len(self.game_map.rooms) - 1:
|
||||
room = self.game_map.rooms[i + 1]
|
||||
x, y = room.center
|
||||
|
||||
# Randomly offset from center
|
||||
x += random.randint(-2, 2)
|
||||
y += random.randint(-2, 2)
|
||||
|
||||
# Make sure position is walkable
|
||||
if self.game_map.grid.at(x, y).walkable:
|
||||
if i % 2 == 0:
|
||||
# Create an orc
|
||||
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||
self.game_map.add_entity(orc)
|
||||
self.entities.append(orc)
|
||||
else:
|
||||
# Create a troll
|
||||
troll = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
||||
self.game_map.add_entity(troll)
|
||||
self.entities.append(troll)
|
||||
|
||||
# Initial FOV calculation
|
||||
self.player.update_fov()
|
||||
|
||||
def handle_movement(self, dx, dy):
|
||||
"""Handle player movement"""
|
||||
new_x = self.player.x + dx
|
||||
new_y = self.player.y + dy
|
||||
|
||||
if not self.game_map.is_blocked(new_x, new_y):
|
||||
self.player.move(dx, dy)
|
||||
|
||||
def setup_input(self):
|
||||
"""Setup keyboard input handling"""
|
||||
def handle_keys(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
movement = {
|
||||
"Up": (0, -1), "Down": (0, 1),
|
||||
"Left": (-1, 0), "Right": (1, 0),
|
||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||
"Num4": (-1, 0), "Num6": (1, 0),
|
||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||
}
|
||||
|
||||
if key in movement:
|
||||
dx, dy = movement[key]
|
||||
self.handle_movement(dx, dy)
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
elif key == "v":
|
||||
# Toggle FOV on/off
|
||||
if self.game_map.grid.perspective == 0:
|
||||
self.game_map.grid.perspective = -1 # Omniscient
|
||||
print("FOV disabled - omniscient view")
|
||||
else:
|
||||
self.game_map.grid.perspective = 0 # Player perspective
|
||||
print("FOV enabled - player perspective")
|
||||
elif key == "Plus" or key == "Equals":
|
||||
# Increase FOV radius
|
||||
self.fov_radius = min(self.fov_radius + 1, 20)
|
||||
self.player._entity.update_fov(radius=self.fov_radius)
|
||||
print(f"FOV radius: {self.fov_radius}")
|
||||
elif key == "Minus":
|
||||
# Decrease FOV radius
|
||||
self.fov_radius = max(self.fov_radius - 1, 3)
|
||||
self.player._entity.update_fov(radius=self.fov_radius)
|
||||
print(f"FOV radius: {self.fov_radius}")
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup UI elements"""
|
||||
title = mcrfpy.Caption("Field of View", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
self.ui.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption("Arrow keys to move | V to toggle FOV | +/- to adjust radius | ESC to quit", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.ui.append(instructions)
|
||||
|
||||
# FOV indicator
|
||||
self.fov_text = mcrfpy.Caption(f"FOV Radius: {self.fov_radius}", 900, 100)
|
||||
self.fov_text.font_size = 14
|
||||
self.fov_text.fill_color = mcrfpy.Color(150, 200, 255)
|
||||
self.ui.append(self.fov_text)
|
||||
|
||||
# Create and run the game
|
||||
engine = Engine()
|
||||
print("Part 4: Field of View!")
|
||||
print("Press V to toggle FOV on/off")
|
||||
print("Press +/- to adjust FOV radius")
|
||||
|
|
@ -0,0 +1,570 @@
|
|||
# Part 5 - Placing Enemies and Kicking Them (Harmlessly)
|
||||
|
||||
Now that we have Field of View working, it's time to populate our dungeon with enemies! In this part, we'll:
|
||||
- Place enemies randomly in rooms
|
||||
- Implement entity-to-entity collision detection
|
||||
- Create basic interactions (bumping into enemies)
|
||||
- Set the stage for combat in Part 6
|
||||
|
||||
## Enemy Spawning System
|
||||
|
||||
First, let's create a system to spawn enemies in our dungeon rooms. We'll avoid placing them in the first room (where the player starts) to give players a safe starting area.
|
||||
|
||||
```python
|
||||
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
||||
"""Spawn between 0 and max_enemies in a room"""
|
||||
import random
|
||||
|
||||
number_of_enemies = random.randint(0, max_enemies)
|
||||
|
||||
for i in range(number_of_enemies):
|
||||
# Try to find a valid position
|
||||
attempts = 10
|
||||
while attempts > 0:
|
||||
# Random position within room bounds
|
||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||||
|
||||
# Check if position is valid
|
||||
if not game_map.is_blocked(x, y):
|
||||
# 80% chance for orc, 20% for troll
|
||||
if random.random() < 0.8:
|
||||
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||
else:
|
||||
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
||||
|
||||
game_map.add_entity(enemy)
|
||||
break
|
||||
|
||||
attempts -= 1
|
||||
```
|
||||
|
||||
## Enhanced Collision Detection
|
||||
|
||||
We need to improve our collision detection to check for entities, not just walls:
|
||||
|
||||
```python
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def get_blocking_entity_at(self, x, y):
|
||||
"""Return any blocking entity at the given position"""
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def is_blocked(self, x, y):
|
||||
"""Check if a tile blocks movement"""
|
||||
# Check boundaries
|
||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||
return True
|
||||
|
||||
# Check walls
|
||||
if not self.grid.at(x, y).walkable:
|
||||
return True
|
||||
|
||||
# Check entities
|
||||
if self.get_blocking_entity_at(x, y):
|
||||
return True
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
## Action System Introduction
|
||||
|
||||
Let's create a simple action system to handle different types of interactions:
|
||||
|
||||
```python
|
||||
class Action:
|
||||
"""Base class for all actions"""
|
||||
pass
|
||||
|
||||
class MovementAction(Action):
|
||||
"""Action for moving an entity"""
|
||||
def __init__(self, dx, dy):
|
||||
self.dx = dx
|
||||
self.dy = dy
|
||||
|
||||
class BumpAction(Action):
|
||||
"""Action for bumping into something"""
|
||||
def __init__(self, dx, dy, target=None):
|
||||
self.dx = dx
|
||||
self.dy = dy
|
||||
self.target = target
|
||||
|
||||
class WaitAction(Action):
|
||||
"""Action for waiting/skipping turn"""
|
||||
pass
|
||||
```
|
||||
|
||||
## Handling Player Actions
|
||||
|
||||
Now let's update our movement handling to support bumping into enemies:
|
||||
|
||||
```python
|
||||
def handle_player_turn(self, action):
|
||||
"""Process the player's action"""
|
||||
if isinstance(action, MovementAction):
|
||||
dest_x = self.player.x + action.dx
|
||||
dest_y = self.player.y + action.dy
|
||||
|
||||
# Check what's at the destination
|
||||
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
||||
|
||||
if target:
|
||||
# We bumped into something!
|
||||
print(f"You kick the {target.name} in the shins, much to its annoyance!")
|
||||
elif not self.game_map.is_blocked(dest_x, dest_y):
|
||||
# Move the player
|
||||
self.player.move(action.dx, action.dy)
|
||||
# Update message
|
||||
self.status_text.text = "Exploring the dungeon..."
|
||||
else:
|
||||
# Bumped into a wall
|
||||
self.status_text.text = "Ouch! You bump into a wall."
|
||||
|
||||
elif isinstance(action, WaitAction):
|
||||
self.status_text.text = "You wait..."
|
||||
```
|
||||
|
||||
## Complete Updated Code
|
||||
|
||||
Here's the complete `game.py` with enemy placement and interactions:
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Color configurations
|
||||
COLORS_VISIBLE = {
|
||||
'wall': (100, 100, 100),
|
||||
'floor': (50, 50, 50),
|
||||
'tunnel': (30, 30, 40),
|
||||
}
|
||||
|
||||
# Actions
|
||||
class Action:
|
||||
"""Base class for all actions"""
|
||||
pass
|
||||
|
||||
class MovementAction(Action):
|
||||
"""Action for moving an entity"""
|
||||
def __init__(self, dx, dy):
|
||||
self.dx = dx
|
||||
self.dy = dy
|
||||
|
||||
class WaitAction(Action):
|
||||
"""Action for waiting/skipping turn"""
|
||||
pass
|
||||
|
||||
class GameObject:
|
||||
"""Base class for all game objects"""
|
||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.sprite_index = sprite_index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.blocks = blocks
|
||||
self._entity = None
|
||||
self.grid = None
|
||||
|
||||
def attach_to_grid(self, grid):
|
||||
"""Attach this game object to a McRogueFace grid"""
|
||||
self.grid = grid
|
||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||
self._entity.sprite_index = self.sprite_index
|
||||
self._entity.color = mcrfpy.Color(*self.color)
|
||||
|
||||
def move(self, dx, dy):
|
||||
"""Move by the given amount"""
|
||||
if not self.grid:
|
||||
return
|
||||
self.x += dx
|
||||
self.y += dy
|
||||
if self._entity:
|
||||
self._entity.x = self.x
|
||||
self._entity.y = self.y
|
||||
# Update FOV when player moves
|
||||
if self.name == "Player":
|
||||
self.update_fov()
|
||||
|
||||
def update_fov(self):
|
||||
"""Update field of view from this entity's position"""
|
||||
if self._entity and self.grid:
|
||||
self._entity.update_fov(radius=8)
|
||||
|
||||
class RectangularRoom:
|
||||
"""A rectangular room with its position and size"""
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return center_x, center_y
|
||||
|
||||
@property
|
||||
def inner(self):
|
||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||
|
||||
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 tunnel_between(start, end):
|
||||
"""Return an L-shaped tunnel between two points"""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
if random.random() < 0.5:
|
||||
corner_x = x2
|
||||
corner_y = y1
|
||||
else:
|
||||
corner_x = x1
|
||||
corner_y = y2
|
||||
|
||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||
yield x, y1
|
||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||
yield corner_x, y
|
||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||
yield x, corner_y
|
||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||
yield x2, y
|
||||
|
||||
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
||||
"""Spawn between 0 and max_enemies in a room"""
|
||||
number_of_enemies = random.randint(0, max_enemies)
|
||||
|
||||
enemies_spawned = []
|
||||
|
||||
for i in range(number_of_enemies):
|
||||
# Try to find a valid position
|
||||
attempts = 10
|
||||
while attempts > 0:
|
||||
# Random position within room bounds
|
||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||||
|
||||
# Check if position is valid
|
||||
if not game_map.is_blocked(x, y):
|
||||
# 80% chance for orc, 20% for troll
|
||||
if random.random() < 0.8:
|
||||
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||
else:
|
||||
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
||||
|
||||
game_map.add_entity(enemy)
|
||||
enemies_spawned.append(enemy)
|
||||
break
|
||||
|
||||
attempts -= 1
|
||||
|
||||
return enemies_spawned
|
||||
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.grid = None
|
||||
self.entities = []
|
||||
self.rooms = []
|
||||
|
||||
def create_grid(self, tileset):
|
||||
"""Create the McRogueFace grid"""
|
||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||
self.grid.position = (100, 100)
|
||||
self.grid.size = (800, 480)
|
||||
|
||||
# Enable perspective rendering
|
||||
self.grid.perspective = 0
|
||||
|
||||
return self.grid
|
||||
|
||||
def fill_with_walls(self):
|
||||
"""Fill the entire map with wall tiles"""
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
self.set_tile(x, y, walkable=False, transparent=False,
|
||||
sprite_index=35, tile_type='wall')
|
||||
|
||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||
"""Set properties for a specific tile"""
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
cell = self.grid.at(x, y)
|
||||
cell.walkable = walkable
|
||||
cell.transparent = transparent
|
||||
cell.sprite_index = sprite_index
|
||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||
|
||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
|
||||
"""Generate a new dungeon map"""
|
||||
self.fill_with_walls()
|
||||
|
||||
for r in range(max_rooms):
|
||||
room_width = random.randint(room_min_size, room_max_size)
|
||||
room_height = random.randint(room_min_size, room_max_size)
|
||||
|
||||
x = random.randint(0, self.width - room_width - 1)
|
||||
y = random.randint(0, self.height - room_height - 1)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||
continue
|
||||
|
||||
self.carve_room(new_room)
|
||||
|
||||
if len(self.rooms) == 0:
|
||||
# First room - place player
|
||||
player.x, player.y = new_room.center
|
||||
if player._entity:
|
||||
player._entity.x, player._entity.y = new_room.center
|
||||
else:
|
||||
# All other rooms - add tunnel and enemies
|
||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
|
||||
|
||||
self.rooms.append(new_room)
|
||||
|
||||
def carve_room(self, room):
|
||||
"""Carve out a room"""
|
||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||
|
||||
for y in range(inner_y1, inner_y2):
|
||||
for x in range(inner_x1, inner_x2):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, tile_type='floor')
|
||||
|
||||
def carve_tunnel(self, start, end):
|
||||
"""Carve a tunnel between two points"""
|
||||
for x, y in tunnel_between(start, end):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, tile_type='tunnel')
|
||||
|
||||
def get_blocking_entity_at(self, x, y):
|
||||
"""Return any blocking entity at the given position"""
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def is_blocked(self, x, y):
|
||||
"""Check if a tile blocks movement"""
|
||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||
return True
|
||||
|
||||
if not self.grid.at(x, y).walkable:
|
||||
return True
|
||||
|
||||
if self.get_blocking_entity_at(x, y):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_entity(self, entity):
|
||||
"""Add a GameObject to the map"""
|
||||
self.entities.append(entity)
|
||||
entity.attach_to_grid(self.grid)
|
||||
|
||||
class Engine:
|
||||
"""Main game engine"""
|
||||
|
||||
def __init__(self):
|
||||
self.game_map = None
|
||||
self.player = None
|
||||
self.entities = []
|
||||
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 5"
|
||||
|
||||
self.ui = mcrfpy.sceneUI("game")
|
||||
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
self.ui.append(background)
|
||||
|
||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
self.setup_game()
|
||||
self.setup_input()
|
||||
self.setup_ui()
|
||||
|
||||
def setup_game(self):
|
||||
"""Initialize the game world"""
|
||||
self.game_map = GameMap(80, 45)
|
||||
grid = self.game_map.create_grid(self.tileset)
|
||||
self.ui.append(grid)
|
||||
|
||||
# Create player
|
||||
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
||||
|
||||
# Generate the dungeon
|
||||
self.game_map.generate_dungeon(
|
||||
max_rooms=30,
|
||||
room_min_size=6,
|
||||
room_max_size=10,
|
||||
player=self.player,
|
||||
max_enemies_per_room=2
|
||||
)
|
||||
|
||||
# Add player to map
|
||||
self.game_map.add_entity(self.player)
|
||||
|
||||
# Store reference to all entities
|
||||
self.entities = [e for e in self.game_map.entities if e != self.player]
|
||||
|
||||
# Initial FOV calculation
|
||||
self.player.update_fov()
|
||||
|
||||
def handle_player_turn(self, action):
|
||||
"""Process the player's action"""
|
||||
if isinstance(action, MovementAction):
|
||||
dest_x = self.player.x + action.dx
|
||||
dest_y = self.player.y + action.dy
|
||||
|
||||
# Check what's at the destination
|
||||
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
||||
|
||||
if target:
|
||||
# We bumped into something!
|
||||
print(f"You kick the {target.name} in the shins, much to its annoyance!")
|
||||
self.status_text.text = f"You kick the {target.name}!"
|
||||
elif not self.game_map.is_blocked(dest_x, dest_y):
|
||||
# Move the player
|
||||
self.player.move(action.dx, action.dy)
|
||||
self.status_text.text = ""
|
||||
else:
|
||||
# Bumped into a wall
|
||||
self.status_text.text = "Blocked!"
|
||||
|
||||
elif isinstance(action, WaitAction):
|
||||
self.status_text.text = "You wait..."
|
||||
|
||||
def setup_input(self):
|
||||
"""Setup keyboard input handling"""
|
||||
def handle_keys(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
action = None
|
||||
|
||||
# Movement keys
|
||||
movement = {
|
||||
"Up": (0, -1), "Down": (0, 1),
|
||||
"Left": (-1, 0), "Right": (1, 0),
|
||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
|
||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||
}
|
||||
|
||||
if key in movement:
|
||||
dx, dy = movement[key]
|
||||
if dx == 0 and dy == 0:
|
||||
action = WaitAction()
|
||||
else:
|
||||
action = MovementAction(dx, dy)
|
||||
elif key == "Period":
|
||||
action = WaitAction()
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
return
|
||||
|
||||
# Process the action
|
||||
if action:
|
||||
self.handle_player_turn(action)
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup UI elements"""
|
||||
title = mcrfpy.Caption("Placing Enemies", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
self.ui.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption("Arrow keys to move | . to wait | Bump into enemies! | ESC to quit", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.ui.append(instructions)
|
||||
|
||||
# Status text
|
||||
self.status_text = mcrfpy.Caption("", 512, 600)
|
||||
self.status_text.font_size = 18
|
||||
self.status_text.fill_color = mcrfpy.Color(255, 200, 200)
|
||||
self.ui.append(self.status_text)
|
||||
|
||||
# Entity count
|
||||
entity_count = len(self.entities)
|
||||
count_text = mcrfpy.Caption(f"Enemies: {entity_count}", 900, 100)
|
||||
count_text.font_size = 14
|
||||
count_text.fill_color = mcrfpy.Color(150, 150, 255)
|
||||
self.ui.append(count_text)
|
||||
|
||||
# Create and run the game
|
||||
engine = Engine()
|
||||
print("Part 5: Placing Enemies!")
|
||||
print("Try bumping into enemies - combat coming in Part 6!")
|
||||
```
|
||||
|
||||
## Understanding Entity Interactions
|
||||
|
||||
### Collision Detection
|
||||
Our system now checks three things when the player tries to move:
|
||||
1. **Map boundaries** - Can't move outside the map
|
||||
2. **Wall tiles** - Can't walk through walls
|
||||
3. **Blocking entities** - Can't walk through enemies
|
||||
|
||||
### The Action System
|
||||
We've introduced a simple action system that will grow in Part 6:
|
||||
- `Action` - Base class for all actions
|
||||
- `MovementAction` - Represents attempted movement
|
||||
- `WaitAction` - Skip a turn (important for turn-based games)
|
||||
|
||||
### Entity Spawning
|
||||
Enemies are placed randomly in rooms with these rules:
|
||||
- Never in the first room (player's starting room)
|
||||
- Random number between 0 and max per room
|
||||
- 80% orcs, 20% trolls
|
||||
- Must be placed on walkable, unoccupied tiles
|
||||
|
||||
## Visual Feedback
|
||||
|
||||
With FOV enabled, enemies will appear and disappear as you explore:
|
||||
- Enemies in sight are fully visible
|
||||
- Enemies in explored but dark areas are hidden
|
||||
- Creates tension and surprise encounters
|
||||
|
||||
## Exercises
|
||||
|
||||
1. **More Enemy Types**: Add different sprites and names (goblins, skeletons)
|
||||
2. **Enemy Density**: Adjust spawn rates based on dungeon depth
|
||||
3. **Special Rooms**: Create rooms with guaranteed enemies or treasures
|
||||
4. **Better Feedback**: Add sound effects or visual effects for bumping
|
||||
|
||||
## What's Next?
|
||||
|
||||
In Part 6, we'll transform those harmless kicks into a real combat system! We'll add:
|
||||
- Health points for all entities
|
||||
- Damage calculations
|
||||
- Death and corpses
|
||||
- Combat messages
|
||||
- The beginning of a real roguelike!
|
||||
|
||||
Right now our enemies are just obstacles. Soon they'll fight back!
|
||||
|
|
@ -0,0 +1,388 @@
|
|||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Color configurations
|
||||
COLORS_VISIBLE = {
|
||||
'wall': (100, 100, 100),
|
||||
'floor': (50, 50, 50),
|
||||
'tunnel': (30, 30, 40),
|
||||
}
|
||||
|
||||
# Actions
|
||||
class Action:
|
||||
"""Base class for all actions"""
|
||||
pass
|
||||
|
||||
class MovementAction(Action):
|
||||
"""Action for moving an entity"""
|
||||
def __init__(self, dx, dy):
|
||||
self.dx = dx
|
||||
self.dy = dy
|
||||
|
||||
class WaitAction(Action):
|
||||
"""Action for waiting/skipping turn"""
|
||||
pass
|
||||
|
||||
class GameObject:
|
||||
"""Base class for all game objects"""
|
||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.sprite_index = sprite_index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.blocks = blocks
|
||||
self._entity = None
|
||||
self.grid = None
|
||||
|
||||
def attach_to_grid(self, grid):
|
||||
"""Attach this game object to a McRogueFace grid"""
|
||||
self.grid = grid
|
||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||
self._entity.sprite_index = self.sprite_index
|
||||
self._entity.color = mcrfpy.Color(*self.color)
|
||||
|
||||
def move(self, dx, dy):
|
||||
"""Move by the given amount"""
|
||||
if not self.grid:
|
||||
return
|
||||
self.x += dx
|
||||
self.y += dy
|
||||
if self._entity:
|
||||
self._entity.x = self.x
|
||||
self._entity.y = self.y
|
||||
# Update FOV when player moves
|
||||
if self.name == "Player":
|
||||
self.update_fov()
|
||||
|
||||
def update_fov(self):
|
||||
"""Update field of view from this entity's position"""
|
||||
if self._entity and self.grid:
|
||||
self._entity.update_fov(radius=8)
|
||||
|
||||
class RectangularRoom:
|
||||
"""A rectangular room with its position and size"""
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return center_x, center_y
|
||||
|
||||
@property
|
||||
def inner(self):
|
||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||
|
||||
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 tunnel_between(start, end):
|
||||
"""Return an L-shaped tunnel between two points"""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
if random.random() < 0.5:
|
||||
corner_x = x2
|
||||
corner_y = y1
|
||||
else:
|
||||
corner_x = x1
|
||||
corner_y = y2
|
||||
|
||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||
yield x, y1
|
||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||
yield corner_x, y
|
||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||
yield x, corner_y
|
||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||
yield x2, y
|
||||
|
||||
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
||||
"""Spawn between 0 and max_enemies in a room"""
|
||||
number_of_enemies = random.randint(0, max_enemies)
|
||||
|
||||
enemies_spawned = []
|
||||
|
||||
for i in range(number_of_enemies):
|
||||
# Try to find a valid position
|
||||
attempts = 10
|
||||
while attempts > 0:
|
||||
# Random position within room bounds
|
||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||||
|
||||
# Check if position is valid
|
||||
if not game_map.is_blocked(x, y):
|
||||
# 80% chance for orc, 20% for troll
|
||||
if random.random() < 0.8:
|
||||
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||
else:
|
||||
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
||||
|
||||
game_map.add_entity(enemy)
|
||||
enemies_spawned.append(enemy)
|
||||
break
|
||||
|
||||
attempts -= 1
|
||||
|
||||
return enemies_spawned
|
||||
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.grid = None
|
||||
self.entities = []
|
||||
self.rooms = []
|
||||
|
||||
def create_grid(self, tileset):
|
||||
"""Create the McRogueFace grid"""
|
||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||
self.grid.position = (100, 100)
|
||||
self.grid.size = (800, 480)
|
||||
|
||||
# Enable perspective rendering
|
||||
self.grid.perspective = 0
|
||||
|
||||
return self.grid
|
||||
|
||||
def fill_with_walls(self):
|
||||
"""Fill the entire map with wall tiles"""
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
self.set_tile(x, y, walkable=False, transparent=False,
|
||||
sprite_index=35, tile_type='wall')
|
||||
|
||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||
"""Set properties for a specific tile"""
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
cell = self.grid.at(x, y)
|
||||
cell.walkable = walkable
|
||||
cell.transparent = transparent
|
||||
cell.sprite_index = sprite_index
|
||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||
|
||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
|
||||
"""Generate a new dungeon map"""
|
||||
self.fill_with_walls()
|
||||
|
||||
for r in range(max_rooms):
|
||||
room_width = random.randint(room_min_size, room_max_size)
|
||||
room_height = random.randint(room_min_size, room_max_size)
|
||||
|
||||
x = random.randint(0, self.width - room_width - 1)
|
||||
y = random.randint(0, self.height - room_height - 1)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||
continue
|
||||
|
||||
self.carve_room(new_room)
|
||||
|
||||
if len(self.rooms) == 0:
|
||||
# First room - place player
|
||||
player.x, player.y = new_room.center
|
||||
if player._entity:
|
||||
player._entity.x, player._entity.y = new_room.center
|
||||
else:
|
||||
# All other rooms - add tunnel and enemies
|
||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
|
||||
|
||||
self.rooms.append(new_room)
|
||||
|
||||
def carve_room(self, room):
|
||||
"""Carve out a room"""
|
||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||
|
||||
for y in range(inner_y1, inner_y2):
|
||||
for x in range(inner_x1, inner_x2):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, tile_type='floor')
|
||||
|
||||
def carve_tunnel(self, start, end):
|
||||
"""Carve a tunnel between two points"""
|
||||
for x, y in tunnel_between(start, end):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, tile_type='tunnel')
|
||||
|
||||
def get_blocking_entity_at(self, x, y):
|
||||
"""Return any blocking entity at the given position"""
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def is_blocked(self, x, y):
|
||||
"""Check if a tile blocks movement"""
|
||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||
return True
|
||||
|
||||
if not self.grid.at(x, y).walkable:
|
||||
return True
|
||||
|
||||
if self.get_blocking_entity_at(x, y):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_entity(self, entity):
|
||||
"""Add a GameObject to the map"""
|
||||
self.entities.append(entity)
|
||||
entity.attach_to_grid(self.grid)
|
||||
|
||||
class Engine:
|
||||
"""Main game engine"""
|
||||
|
||||
def __init__(self):
|
||||
self.game_map = None
|
||||
self.player = None
|
||||
self.entities = []
|
||||
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 5"
|
||||
|
||||
self.ui = mcrfpy.sceneUI("game")
|
||||
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
self.ui.append(background)
|
||||
|
||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
self.setup_game()
|
||||
self.setup_input()
|
||||
self.setup_ui()
|
||||
|
||||
def setup_game(self):
|
||||
"""Initialize the game world"""
|
||||
self.game_map = GameMap(80, 45)
|
||||
grid = self.game_map.create_grid(self.tileset)
|
||||
self.ui.append(grid)
|
||||
|
||||
# Create player
|
||||
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
||||
|
||||
# Generate the dungeon
|
||||
self.game_map.generate_dungeon(
|
||||
max_rooms=30,
|
||||
room_min_size=6,
|
||||
room_max_size=10,
|
||||
player=self.player,
|
||||
max_enemies_per_room=2
|
||||
)
|
||||
|
||||
# Add player to map
|
||||
self.game_map.add_entity(self.player)
|
||||
|
||||
# Store reference to all entities
|
||||
self.entities = [e for e in self.game_map.entities if e != self.player]
|
||||
|
||||
# Initial FOV calculation
|
||||
self.player.update_fov()
|
||||
|
||||
def handle_player_turn(self, action):
|
||||
"""Process the player's action"""
|
||||
if isinstance(action, MovementAction):
|
||||
dest_x = self.player.x + action.dx
|
||||
dest_y = self.player.y + action.dy
|
||||
|
||||
# Check what's at the destination
|
||||
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
||||
|
||||
if target:
|
||||
# We bumped into something!
|
||||
print(f"You kick the {target.name} in the shins, much to its annoyance!")
|
||||
self.status_text.text = f"You kick the {target.name}!"
|
||||
elif not self.game_map.is_blocked(dest_x, dest_y):
|
||||
# Move the player
|
||||
self.player.move(action.dx, action.dy)
|
||||
self.status_text.text = ""
|
||||
else:
|
||||
# Bumped into a wall
|
||||
self.status_text.text = "Blocked!"
|
||||
|
||||
elif isinstance(action, WaitAction):
|
||||
self.status_text.text = "You wait..."
|
||||
|
||||
def setup_input(self):
|
||||
"""Setup keyboard input handling"""
|
||||
def handle_keys(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
action = None
|
||||
|
||||
# Movement keys
|
||||
movement = {
|
||||
"Up": (0, -1), "Down": (0, 1),
|
||||
"Left": (-1, 0), "Right": (1, 0),
|
||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
|
||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||
}
|
||||
|
||||
if key in movement:
|
||||
dx, dy = movement[key]
|
||||
if dx == 0 and dy == 0:
|
||||
action = WaitAction()
|
||||
else:
|
||||
action = MovementAction(dx, dy)
|
||||
elif key == "Period":
|
||||
action = WaitAction()
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
return
|
||||
|
||||
# Process the action
|
||||
if action:
|
||||
self.handle_player_turn(action)
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup UI elements"""
|
||||
title = mcrfpy.Caption("Placing Enemies", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
self.ui.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption("Arrow keys to move | . to wait | Bump into enemies! | ESC to quit", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.ui.append(instructions)
|
||||
|
||||
# Status text
|
||||
self.status_text = mcrfpy.Caption("", 512, 600)
|
||||
self.status_text.font_size = 18
|
||||
self.status_text.fill_color = mcrfpy.Color(255, 200, 200)
|
||||
self.ui.append(self.status_text)
|
||||
|
||||
# Entity count
|
||||
entity_count = len(self.entities)
|
||||
count_text = mcrfpy.Caption(f"Enemies: {entity_count}", 900, 100)
|
||||
count_text.font_size = 14
|
||||
count_text.fill_color = mcrfpy.Color(150, 150, 255)
|
||||
self.ui.append(count_text)
|
||||
|
||||
# Create and run the game
|
||||
engine = Engine()
|
||||
print("Part 5: Placing Enemies!")
|
||||
print("Try bumping into enemies - combat coming in Part 6!")
|
||||
|
|
@ -0,0 +1,743 @@
|
|||
# Part 6 - Doing (and Taking) Some Damage
|
||||
|
||||
It's time to turn our harmless kicks into real combat! In this part, we'll implement:
|
||||
- Health points for all entities
|
||||
- A damage calculation system
|
||||
- Death and corpse mechanics
|
||||
- Combat feedback messages
|
||||
- The foundation of tactical roguelike combat
|
||||
|
||||
## Adding Combat Stats
|
||||
|
||||
First, let's enhance our GameObject class with combat capabilities:
|
||||
|
||||
```python
|
||||
class GameObject:
|
||||
"""Base class for all game objects"""
|
||||
def __init__(self, x, y, sprite_index, color, name,
|
||||
blocks=False, hp=0, defense=0, power=0):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.sprite_index = sprite_index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.blocks = blocks
|
||||
self._entity = None
|
||||
self.grid = None
|
||||
|
||||
# Combat stats
|
||||
self.max_hp = hp
|
||||
self.hp = hp
|
||||
self.defense = defense
|
||||
self.power = power
|
||||
|
||||
@property
|
||||
def is_alive(self):
|
||||
"""Returns True if this entity can act"""
|
||||
return self.hp > 0
|
||||
|
||||
def take_damage(self, amount):
|
||||
"""Apply damage to this entity"""
|
||||
damage = amount - self.defense
|
||||
if damage > 0:
|
||||
self.hp -= damage
|
||||
|
||||
# Check for death
|
||||
if self.hp <= 0 and self.hp + damage > 0:
|
||||
self.die()
|
||||
|
||||
return damage
|
||||
|
||||
def die(self):
|
||||
"""Handle entity death"""
|
||||
if self.name == "Player":
|
||||
# Player death is special - we'll handle it differently
|
||||
self.sprite_index = 64 # Stay as @ but change color
|
||||
self.color = (127, 0, 0) # Dark red
|
||||
if self._entity:
|
||||
self._entity.color = mcrfpy.Color(127, 0, 0)
|
||||
print("You have died!")
|
||||
else:
|
||||
# Enemy death
|
||||
self.sprite_index = 37 # % character for corpse
|
||||
self.color = (127, 0, 0) # Dark red
|
||||
self.blocks = False # Corpses don't block
|
||||
self.name = f"remains of {self.name}"
|
||||
|
||||
if self._entity:
|
||||
self._entity.sprite_index = 37
|
||||
self._entity.color = mcrfpy.Color(127, 0, 0)
|
||||
```
|
||||
|
||||
## The Combat System
|
||||
|
||||
Now let's implement actual combat when entities bump into each other:
|
||||
|
||||
```python
|
||||
class MeleeAction(Action):
|
||||
"""Action for melee attacks"""
|
||||
def __init__(self, attacker, target):
|
||||
self.attacker = attacker
|
||||
self.target = target
|
||||
|
||||
def perform(self):
|
||||
"""Execute the attack"""
|
||||
if not self.target.is_alive:
|
||||
return # Can't attack the dead
|
||||
|
||||
damage = self.attacker.power - self.target.defense
|
||||
|
||||
if damage > 0:
|
||||
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
|
||||
self.target.take_damage(damage)
|
||||
else:
|
||||
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
|
||||
|
||||
return attack_desc
|
||||
```
|
||||
|
||||
## Entity Factories
|
||||
|
||||
Let's create factory functions for consistent entity creation:
|
||||
|
||||
```python
|
||||
def create_player(x, y):
|
||||
"""Create the player entity"""
|
||||
return GameObject(
|
||||
x=x, y=y,
|
||||
sprite_index=64, # @
|
||||
color=(255, 255, 255),
|
||||
name="Player",
|
||||
blocks=True,
|
||||
hp=30,
|
||||
defense=2,
|
||||
power=5
|
||||
)
|
||||
|
||||
def create_orc(x, y):
|
||||
"""Create an orc enemy"""
|
||||
return GameObject(
|
||||
x=x, y=y,
|
||||
sprite_index=111, # o
|
||||
color=(63, 127, 63),
|
||||
name="Orc",
|
||||
blocks=True,
|
||||
hp=10,
|
||||
defense=0,
|
||||
power=3
|
||||
)
|
||||
|
||||
def create_troll(x, y):
|
||||
"""Create a troll enemy"""
|
||||
return GameObject(
|
||||
x=x, y=y,
|
||||
sprite_index=84, # T
|
||||
color=(0, 127, 0),
|
||||
name="Troll",
|
||||
blocks=True,
|
||||
hp=16,
|
||||
defense=1,
|
||||
power=4
|
||||
)
|
||||
```
|
||||
|
||||
## The Message Log
|
||||
|
||||
Combat needs feedback! Let's create a simple message log:
|
||||
|
||||
```python
|
||||
class MessageLog:
|
||||
"""Manages game messages"""
|
||||
def __init__(self, max_messages=5):
|
||||
self.messages = []
|
||||
self.max_messages = max_messages
|
||||
|
||||
def add_message(self, text, color=(255, 255, 255)):
|
||||
"""Add a message to the log"""
|
||||
self.messages.append((text, color))
|
||||
# Keep only recent messages
|
||||
if len(self.messages) > self.max_messages:
|
||||
self.messages.pop(0)
|
||||
|
||||
def render(self, ui, x, y, line_height=20):
|
||||
"""Render messages to the UI"""
|
||||
for i, (text, color) in enumerate(self.messages):
|
||||
caption = mcrfpy.Caption(text, x, y + i * line_height)
|
||||
caption.font_size = 14
|
||||
caption.fill_color = mcrfpy.Color(*color)
|
||||
ui.append(caption)
|
||||
```
|
||||
|
||||
## Complete Implementation
|
||||
|
||||
Here's the complete `game.py` with combat:
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Color configurations
|
||||
COLORS_VISIBLE = {
|
||||
'wall': (100, 100, 100),
|
||||
'floor': (50, 50, 50),
|
||||
'tunnel': (30, 30, 40),
|
||||
}
|
||||
|
||||
# Message colors
|
||||
COLOR_PLAYER_ATK = (230, 230, 230)
|
||||
COLOR_ENEMY_ATK = (255, 200, 200)
|
||||
COLOR_PLAYER_DIE = (255, 100, 100)
|
||||
COLOR_ENEMY_DIE = (255, 165, 0)
|
||||
|
||||
# Actions
|
||||
class Action:
|
||||
"""Base class for all actions"""
|
||||
pass
|
||||
|
||||
class MovementAction(Action):
|
||||
"""Action for moving an entity"""
|
||||
def __init__(self, dx, dy):
|
||||
self.dx = dx
|
||||
self.dy = dy
|
||||
|
||||
class MeleeAction(Action):
|
||||
"""Action for melee attacks"""
|
||||
def __init__(self, attacker, target):
|
||||
self.attacker = attacker
|
||||
self.target = target
|
||||
|
||||
def perform(self):
|
||||
"""Execute the attack"""
|
||||
if not self.target.is_alive:
|
||||
return None
|
||||
|
||||
damage = self.attacker.power - self.target.defense
|
||||
|
||||
if damage > 0:
|
||||
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
|
||||
self.target.take_damage(damage)
|
||||
|
||||
# Choose color based on attacker
|
||||
if self.attacker.name == "Player":
|
||||
color = COLOR_PLAYER_ATK
|
||||
else:
|
||||
color = COLOR_ENEMY_ATK
|
||||
|
||||
return attack_desc, color
|
||||
else:
|
||||
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
|
||||
return attack_desc, (150, 150, 150)
|
||||
|
||||
class WaitAction(Action):
|
||||
"""Action for waiting/skipping turn"""
|
||||
pass
|
||||
|
||||
class GameObject:
|
||||
"""Base class for all game objects"""
|
||||
def __init__(self, x, y, sprite_index, color, name,
|
||||
blocks=False, hp=0, defense=0, power=0):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.sprite_index = sprite_index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.blocks = blocks
|
||||
self._entity = None
|
||||
self.grid = None
|
||||
|
||||
# Combat stats
|
||||
self.max_hp = hp
|
||||
self.hp = hp
|
||||
self.defense = defense
|
||||
self.power = power
|
||||
|
||||
@property
|
||||
def is_alive(self):
|
||||
"""Returns True if this entity can act"""
|
||||
return self.hp > 0
|
||||
|
||||
def attach_to_grid(self, grid):
|
||||
"""Attach this game object to a McRogueFace grid"""
|
||||
self.grid = grid
|
||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||
self._entity.sprite_index = self.sprite_index
|
||||
self._entity.color = mcrfpy.Color(*self.color)
|
||||
|
||||
def move(self, dx, dy):
|
||||
"""Move by the given amount"""
|
||||
if not self.grid:
|
||||
return
|
||||
self.x += dx
|
||||
self.y += dy
|
||||
if self._entity:
|
||||
self._entity.x = self.x
|
||||
self._entity.y = self.y
|
||||
# Update FOV when player moves
|
||||
if self.name == "Player":
|
||||
self.update_fov()
|
||||
|
||||
def update_fov(self):
|
||||
"""Update field of view from this entity's position"""
|
||||
if self._entity and self.grid:
|
||||
self._entity.update_fov(radius=8)
|
||||
|
||||
def take_damage(self, amount):
|
||||
"""Apply damage to this entity"""
|
||||
self.hp -= amount
|
||||
|
||||
# Check for death
|
||||
if self.hp <= 0:
|
||||
self.die()
|
||||
|
||||
def die(self):
|
||||
"""Handle entity death"""
|
||||
if self.name == "Player":
|
||||
# Player death
|
||||
self.sprite_index = 64 # Stay as @
|
||||
self.color = (127, 0, 0) # Dark red
|
||||
if self._entity:
|
||||
self._entity.color = mcrfpy.Color(127, 0, 0)
|
||||
else:
|
||||
# Enemy death
|
||||
self.sprite_index = 37 # % character for corpse
|
||||
self.color = (127, 0, 0) # Dark red
|
||||
self.blocks = False # Corpses don't block
|
||||
self.name = f"remains of {self.name}"
|
||||
|
||||
if self._entity:
|
||||
self._entity.sprite_index = 37
|
||||
self._entity.color = mcrfpy.Color(127, 0, 0)
|
||||
|
||||
# Entity factories
|
||||
def create_player(x, y):
|
||||
"""Create the player entity"""
|
||||
return GameObject(
|
||||
x=x, y=y,
|
||||
sprite_index=64, # @
|
||||
color=(255, 255, 255),
|
||||
name="Player",
|
||||
blocks=True,
|
||||
hp=30,
|
||||
defense=2,
|
||||
power=5
|
||||
)
|
||||
|
||||
def create_orc(x, y):
|
||||
"""Create an orc enemy"""
|
||||
return GameObject(
|
||||
x=x, y=y,
|
||||
sprite_index=111, # o
|
||||
color=(63, 127, 63),
|
||||
name="Orc",
|
||||
blocks=True,
|
||||
hp=10,
|
||||
defense=0,
|
||||
power=3
|
||||
)
|
||||
|
||||
def create_troll(x, y):
|
||||
"""Create a troll enemy"""
|
||||
return GameObject(
|
||||
x=x, y=y,
|
||||
sprite_index=84, # T
|
||||
color=(0, 127, 0),
|
||||
name="Troll",
|
||||
blocks=True,
|
||||
hp=16,
|
||||
defense=1,
|
||||
power=4
|
||||
)
|
||||
|
||||
class RectangularRoom:
|
||||
"""A rectangular room with its position and size"""
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return center_x, center_y
|
||||
|
||||
@property
|
||||
def inner(self):
|
||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||
|
||||
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 tunnel_between(start, end):
|
||||
"""Return an L-shaped tunnel between two points"""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
if random.random() < 0.5:
|
||||
corner_x = x2
|
||||
corner_y = y1
|
||||
else:
|
||||
corner_x = x1
|
||||
corner_y = y2
|
||||
|
||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||
yield x, y1
|
||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||
yield corner_x, y
|
||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||
yield x, corner_y
|
||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||
yield x2, y
|
||||
|
||||
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
||||
"""Spawn between 0 and max_enemies in a room"""
|
||||
number_of_enemies = random.randint(0, max_enemies)
|
||||
|
||||
enemies_spawned = []
|
||||
|
||||
for i in range(number_of_enemies):
|
||||
attempts = 10
|
||||
while attempts > 0:
|
||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||||
|
||||
if not game_map.is_blocked(x, y):
|
||||
# 80% chance for orc, 20% for troll
|
||||
if random.random() < 0.8:
|
||||
enemy = create_orc(x, y)
|
||||
else:
|
||||
enemy = create_troll(x, y)
|
||||
|
||||
game_map.add_entity(enemy)
|
||||
enemies_spawned.append(enemy)
|
||||
break
|
||||
|
||||
attempts -= 1
|
||||
|
||||
return enemies_spawned
|
||||
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.grid = None
|
||||
self.entities = []
|
||||
self.rooms = []
|
||||
|
||||
def create_grid(self, tileset):
|
||||
"""Create the McRogueFace grid"""
|
||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||
self.grid.position = (100, 100)
|
||||
self.grid.size = (800, 480)
|
||||
|
||||
# Enable perspective rendering
|
||||
self.grid.perspective = 0
|
||||
|
||||
return self.grid
|
||||
|
||||
def fill_with_walls(self):
|
||||
"""Fill the entire map with wall tiles"""
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
self.set_tile(x, y, walkable=False, transparent=False,
|
||||
sprite_index=35, tile_type='wall')
|
||||
|
||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||
"""Set properties for a specific tile"""
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
cell = self.grid.at(x, y)
|
||||
cell.walkable = walkable
|
||||
cell.transparent = transparent
|
||||
cell.sprite_index = sprite_index
|
||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||
|
||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
|
||||
"""Generate a new dungeon map"""
|
||||
self.fill_with_walls()
|
||||
|
||||
for r in range(max_rooms):
|
||||
room_width = random.randint(room_min_size, room_max_size)
|
||||
room_height = random.randint(room_min_size, room_max_size)
|
||||
|
||||
x = random.randint(0, self.width - room_width - 1)
|
||||
y = random.randint(0, self.height - room_height - 1)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||
continue
|
||||
|
||||
self.carve_room(new_room)
|
||||
|
||||
if len(self.rooms) == 0:
|
||||
# First room - place player
|
||||
player.x, player.y = new_room.center
|
||||
if player._entity:
|
||||
player._entity.x, player._entity.y = new_room.center
|
||||
else:
|
||||
# All other rooms - add tunnel and enemies
|
||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
|
||||
|
||||
self.rooms.append(new_room)
|
||||
|
||||
def carve_room(self, room):
|
||||
"""Carve out a room"""
|
||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||
|
||||
for y in range(inner_y1, inner_y2):
|
||||
for x in range(inner_x1, inner_x2):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, tile_type='floor')
|
||||
|
||||
def carve_tunnel(self, start, end):
|
||||
"""Carve a tunnel between two points"""
|
||||
for x, y in tunnel_between(start, end):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, tile_type='tunnel')
|
||||
|
||||
def get_blocking_entity_at(self, x, y):
|
||||
"""Return any blocking entity at the given position"""
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def is_blocked(self, x, y):
|
||||
"""Check if a tile blocks movement"""
|
||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||
return True
|
||||
|
||||
if not self.grid.at(x, y).walkable:
|
||||
return True
|
||||
|
||||
if self.get_blocking_entity_at(x, y):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_entity(self, entity):
|
||||
"""Add a GameObject to the map"""
|
||||
self.entities.append(entity)
|
||||
entity.attach_to_grid(self.grid)
|
||||
|
||||
class Engine:
|
||||
"""Main game engine"""
|
||||
|
||||
def __init__(self):
|
||||
self.game_map = None
|
||||
self.player = None
|
||||
self.entities = []
|
||||
self.messages = [] # Simple message log
|
||||
self.max_messages = 5
|
||||
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 6"
|
||||
|
||||
self.ui = mcrfpy.sceneUI("game")
|
||||
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
self.ui.append(background)
|
||||
|
||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
self.setup_game()
|
||||
self.setup_input()
|
||||
self.setup_ui()
|
||||
|
||||
def add_message(self, text, color=(255, 255, 255)):
|
||||
"""Add a message to the log"""
|
||||
self.messages.append((text, color))
|
||||
if len(self.messages) > self.max_messages:
|
||||
self.messages.pop(0)
|
||||
self.update_message_display()
|
||||
|
||||
def update_message_display(self):
|
||||
"""Update the message display"""
|
||||
# Clear old messages
|
||||
for caption in self.message_captions:
|
||||
# Remove from UI (McRogueFace doesn't have remove, so we hide it)
|
||||
caption.text = ""
|
||||
|
||||
# Display current messages
|
||||
for i, (text, color) in enumerate(self.messages):
|
||||
if i < len(self.message_captions):
|
||||
self.message_captions[i].text = text
|
||||
self.message_captions[i].fill_color = mcrfpy.Color(*color)
|
||||
|
||||
def setup_game(self):
|
||||
"""Initialize the game world"""
|
||||
self.game_map = GameMap(80, 45)
|
||||
grid = self.game_map.create_grid(self.tileset)
|
||||
self.ui.append(grid)
|
||||
|
||||
# Create player
|
||||
self.player = create_player(0, 0)
|
||||
|
||||
# Generate the dungeon
|
||||
self.game_map.generate_dungeon(
|
||||
max_rooms=30,
|
||||
room_min_size=6,
|
||||
room_max_size=10,
|
||||
player=self.player,
|
||||
max_enemies_per_room=2
|
||||
)
|
||||
|
||||
# Add player to map
|
||||
self.game_map.add_entity(self.player)
|
||||
|
||||
# Store reference to all entities
|
||||
self.entities = [e for e in self.game_map.entities if e != self.player]
|
||||
|
||||
# Initial FOV calculation
|
||||
self.player.update_fov()
|
||||
|
||||
# Welcome message
|
||||
self.add_message("Welcome to the dungeon!", (100, 100, 255))
|
||||
|
||||
def handle_player_turn(self, action):
|
||||
"""Process the player's action"""
|
||||
if not self.player.is_alive:
|
||||
return
|
||||
|
||||
if isinstance(action, MovementAction):
|
||||
dest_x = self.player.x + action.dx
|
||||
dest_y = self.player.y + action.dy
|
||||
|
||||
# Check what's at the destination
|
||||
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
||||
|
||||
if target:
|
||||
# Attack!
|
||||
attack = MeleeAction(self.player, target)
|
||||
result = attack.perform()
|
||||
if result:
|
||||
text, color = result
|
||||
self.add_message(text, color)
|
||||
|
||||
# Check if target died
|
||||
if not target.is_alive:
|
||||
death_msg = f"The {target.name.replace('remains of ', '')} is dead!"
|
||||
self.add_message(death_msg, COLOR_ENEMY_DIE)
|
||||
|
||||
elif not self.game_map.is_blocked(dest_x, dest_y):
|
||||
# Move the player
|
||||
self.player.move(action.dx, action.dy)
|
||||
|
||||
elif isinstance(action, WaitAction):
|
||||
pass # Do nothing
|
||||
|
||||
# Enemy turns
|
||||
self.handle_enemy_turns()
|
||||
|
||||
def handle_enemy_turns(self):
|
||||
"""Let all enemies take their turn"""
|
||||
for entity in self.entities:
|
||||
if entity.is_alive:
|
||||
# Simple AI: if player is adjacent, attack. Otherwise, do nothing.
|
||||
dx = entity.x - self.player.x
|
||||
dy = entity.y - self.player.y
|
||||
distance = abs(dx) + abs(dy)
|
||||
|
||||
if distance == 1: # Adjacent to player
|
||||
attack = MeleeAction(entity, self.player)
|
||||
result = attack.perform()
|
||||
if result:
|
||||
text, color = result
|
||||
self.add_message(text, color)
|
||||
|
||||
# Check if player died
|
||||
if not self.player.is_alive:
|
||||
self.add_message("You have died!", COLOR_PLAYER_DIE)
|
||||
|
||||
def setup_input(self):
|
||||
"""Setup keyboard input handling"""
|
||||
def handle_keys(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
action = None
|
||||
|
||||
# Movement keys
|
||||
movement = {
|
||||
"Up": (0, -1), "Down": (0, 1),
|
||||
"Left": (-1, 0), "Right": (1, 0),
|
||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
|
||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||
}
|
||||
|
||||
if key in movement:
|
||||
dx, dy = movement[key]
|
||||
if dx == 0 and dy == 0:
|
||||
action = WaitAction()
|
||||
else:
|
||||
action = MovementAction(dx, dy)
|
||||
elif key == "Period":
|
||||
action = WaitAction()
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
return
|
||||
|
||||
# Process the action
|
||||
if action:
|
||||
self.handle_player_turn(action)
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup UI elements"""
|
||||
title = mcrfpy.Caption("Combat System", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
self.ui.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption("Attack enemies by bumping into them!", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.ui.append(instructions)
|
||||
|
||||
# Player stats
|
||||
self.hp_text = mcrfpy.Caption(f"HP: {self.player.hp}/{self.player.max_hp}", 50, 100)
|
||||
self.hp_text.font_size = 18
|
||||
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
self.ui.append(self.hp_text)
|
||||
|
||||
# Message log
|
||||
self.message_captions = []
|
||||
for i in range(self.max_messages):
|
||||
caption = mcrfpy.Caption("", 50, 620 + i * 20)
|
||||
caption.font_size = 14
|
||||
caption.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.ui.append(caption)
|
||||
self.message_captions.append(caption)
|
||||
|
||||
# Timer to update HP display
|
||||
def update_stats(dt):
|
||||
self.hp_text.text = f"HP: {self.player.hp}/{self.player.max_hp}"
|
||||
if self.player.hp <= 0:
|
||||
self.hp_text.fill_color = mcrfpy.Color(127, 0, 0)
|
||||
elif self.player.hp < self.player.max_hp // 3:
|
||||
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
else:
|
||||
self.hp_text.fill_color = mcrfpy.Color(0, 255, 0)
|
||||
|
||||
mcrfpy.setTimer("update_stats", update_stats, 100)
|
||||
|
||||
# Create and run the game
|
||||
engine = Engine()
|
||||
print("Part 6: Combat System!")
|
||||
print("Attack enemies to defeat them, but watch your HP!")
|
||||
|
|
@ -0,0 +1,568 @@
|
|||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Color configurations
|
||||
COLORS_VISIBLE = {
|
||||
'wall': (100, 100, 100),
|
||||
'floor': (50, 50, 50),
|
||||
'tunnel': (30, 30, 40),
|
||||
}
|
||||
|
||||
# Message colors
|
||||
COLOR_PLAYER_ATK = (230, 230, 230)
|
||||
COLOR_ENEMY_ATK = (255, 200, 200)
|
||||
COLOR_PLAYER_DIE = (255, 100, 100)
|
||||
COLOR_ENEMY_DIE = (255, 165, 0)
|
||||
|
||||
# Actions
|
||||
class Action:
|
||||
"""Base class for all actions"""
|
||||
pass
|
||||
|
||||
class MovementAction(Action):
|
||||
"""Action for moving an entity"""
|
||||
def __init__(self, dx, dy):
|
||||
self.dx = dx
|
||||
self.dy = dy
|
||||
|
||||
class MeleeAction(Action):
|
||||
"""Action for melee attacks"""
|
||||
def __init__(self, attacker, target):
|
||||
self.attacker = attacker
|
||||
self.target = target
|
||||
|
||||
def perform(self):
|
||||
"""Execute the attack"""
|
||||
if not self.target.is_alive:
|
||||
return None
|
||||
|
||||
damage = self.attacker.power - self.target.defense
|
||||
|
||||
if damage > 0:
|
||||
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
|
||||
self.target.take_damage(damage)
|
||||
|
||||
# Choose color based on attacker
|
||||
if self.attacker.name == "Player":
|
||||
color = COLOR_PLAYER_ATK
|
||||
else:
|
||||
color = COLOR_ENEMY_ATK
|
||||
|
||||
return attack_desc, color
|
||||
else:
|
||||
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
|
||||
return attack_desc, (150, 150, 150)
|
||||
|
||||
class WaitAction(Action):
|
||||
"""Action for waiting/skipping turn"""
|
||||
pass
|
||||
|
||||
class GameObject:
|
||||
"""Base class for all game objects"""
|
||||
def __init__(self, x, y, sprite_index, color, name,
|
||||
blocks=False, hp=0, defense=0, power=0):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.sprite_index = sprite_index
|
||||
self.color = color
|
||||
self.name = name
|
||||
self.blocks = blocks
|
||||
self._entity = None
|
||||
self.grid = None
|
||||
|
||||
# Combat stats
|
||||
self.max_hp = hp
|
||||
self.hp = hp
|
||||
self.defense = defense
|
||||
self.power = power
|
||||
|
||||
@property
|
||||
def is_alive(self):
|
||||
"""Returns True if this entity can act"""
|
||||
return self.hp > 0
|
||||
|
||||
def attach_to_grid(self, grid):
|
||||
"""Attach this game object to a McRogueFace grid"""
|
||||
self.grid = grid
|
||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||
self._entity.sprite_index = self.sprite_index
|
||||
self._entity.color = mcrfpy.Color(*self.color)
|
||||
|
||||
def move(self, dx, dy):
|
||||
"""Move by the given amount"""
|
||||
if not self.grid:
|
||||
return
|
||||
self.x += dx
|
||||
self.y += dy
|
||||
if self._entity:
|
||||
self._entity.x = self.x
|
||||
self._entity.y = self.y
|
||||
# Update FOV when player moves
|
||||
if self.name == "Player":
|
||||
self.update_fov()
|
||||
|
||||
def update_fov(self):
|
||||
"""Update field of view from this entity's position"""
|
||||
if self._entity and self.grid:
|
||||
self._entity.update_fov(radius=8)
|
||||
|
||||
def take_damage(self, amount):
|
||||
"""Apply damage to this entity"""
|
||||
self.hp -= amount
|
||||
|
||||
# Check for death
|
||||
if self.hp <= 0:
|
||||
self.die()
|
||||
|
||||
def die(self):
|
||||
"""Handle entity death"""
|
||||
if self.name == "Player":
|
||||
# Player death
|
||||
self.sprite_index = 64 # Stay as @
|
||||
self.color = (127, 0, 0) # Dark red
|
||||
if self._entity:
|
||||
self._entity.color = mcrfpy.Color(127, 0, 0)
|
||||
else:
|
||||
# Enemy death
|
||||
self.sprite_index = 37 # % character for corpse
|
||||
self.color = (127, 0, 0) # Dark red
|
||||
self.blocks = False # Corpses don't block
|
||||
self.name = f"remains of {self.name}"
|
||||
|
||||
if self._entity:
|
||||
self._entity.sprite_index = 37
|
||||
self._entity.color = mcrfpy.Color(127, 0, 0)
|
||||
|
||||
# Entity factories
|
||||
def create_player(x, y):
|
||||
"""Create the player entity"""
|
||||
return GameObject(
|
||||
x=x, y=y,
|
||||
sprite_index=64, # @
|
||||
color=(255, 255, 255),
|
||||
name="Player",
|
||||
blocks=True,
|
||||
hp=30,
|
||||
defense=2,
|
||||
power=5
|
||||
)
|
||||
|
||||
def create_orc(x, y):
|
||||
"""Create an orc enemy"""
|
||||
return GameObject(
|
||||
x=x, y=y,
|
||||
sprite_index=111, # o
|
||||
color=(63, 127, 63),
|
||||
name="Orc",
|
||||
blocks=True,
|
||||
hp=10,
|
||||
defense=0,
|
||||
power=3
|
||||
)
|
||||
|
||||
def create_troll(x, y):
|
||||
"""Create a troll enemy"""
|
||||
return GameObject(
|
||||
x=x, y=y,
|
||||
sprite_index=84, # T
|
||||
color=(0, 127, 0),
|
||||
name="Troll",
|
||||
blocks=True,
|
||||
hp=16,
|
||||
defense=1,
|
||||
power=4
|
||||
)
|
||||
|
||||
class RectangularRoom:
|
||||
"""A rectangular room with its position and size"""
|
||||
|
||||
def __init__(self, x, y, width, height):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return center_x, center_y
|
||||
|
||||
@property
|
||||
def inner(self):
|
||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||
|
||||
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 tunnel_between(start, end):
|
||||
"""Return an L-shaped tunnel between two points"""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
if random.random() < 0.5:
|
||||
corner_x = x2
|
||||
corner_y = y1
|
||||
else:
|
||||
corner_x = x1
|
||||
corner_y = y2
|
||||
|
||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||
yield x, y1
|
||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||
yield corner_x, y
|
||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||
yield x, corner_y
|
||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||
yield x2, y
|
||||
|
||||
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
||||
"""Spawn between 0 and max_enemies in a room"""
|
||||
number_of_enemies = random.randint(0, max_enemies)
|
||||
|
||||
enemies_spawned = []
|
||||
|
||||
for i in range(number_of_enemies):
|
||||
attempts = 10
|
||||
while attempts > 0:
|
||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||||
|
||||
if not game_map.is_blocked(x, y):
|
||||
# 80% chance for orc, 20% for troll
|
||||
if random.random() < 0.8:
|
||||
enemy = create_orc(x, y)
|
||||
else:
|
||||
enemy = create_troll(x, y)
|
||||
|
||||
game_map.add_entity(enemy)
|
||||
enemies_spawned.append(enemy)
|
||||
break
|
||||
|
||||
attempts -= 1
|
||||
|
||||
return enemies_spawned
|
||||
|
||||
class GameMap:
|
||||
"""Manages the game world"""
|
||||
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.grid = None
|
||||
self.entities = []
|
||||
self.rooms = []
|
||||
|
||||
def create_grid(self, tileset):
|
||||
"""Create the McRogueFace grid"""
|
||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||
self.grid.position = (100, 100)
|
||||
self.grid.size = (800, 480)
|
||||
|
||||
# Enable perspective rendering
|
||||
self.grid.perspective = 0
|
||||
|
||||
return self.grid
|
||||
|
||||
def fill_with_walls(self):
|
||||
"""Fill the entire map with wall tiles"""
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
self.set_tile(x, y, walkable=False, transparent=False,
|
||||
sprite_index=35, tile_type='wall')
|
||||
|
||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||
"""Set properties for a specific tile"""
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
cell = self.grid.at(x, y)
|
||||
cell.walkable = walkable
|
||||
cell.transparent = transparent
|
||||
cell.sprite_index = sprite_index
|
||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||
|
||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
|
||||
"""Generate a new dungeon map"""
|
||||
self.fill_with_walls()
|
||||
|
||||
for r in range(max_rooms):
|
||||
room_width = random.randint(room_min_size, room_max_size)
|
||||
room_height = random.randint(room_min_size, room_max_size)
|
||||
|
||||
x = random.randint(0, self.width - room_width - 1)
|
||||
y = random.randint(0, self.height - room_height - 1)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||
continue
|
||||
|
||||
self.carve_room(new_room)
|
||||
|
||||
if len(self.rooms) == 0:
|
||||
# First room - place player
|
||||
player.x, player.y = new_room.center
|
||||
if player._entity:
|
||||
player._entity.x, player._entity.y = new_room.center
|
||||
else:
|
||||
# All other rooms - add tunnel and enemies
|
||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
|
||||
|
||||
self.rooms.append(new_room)
|
||||
|
||||
def carve_room(self, room):
|
||||
"""Carve out a room"""
|
||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||
|
||||
for y in range(inner_y1, inner_y2):
|
||||
for x in range(inner_x1, inner_x2):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, tile_type='floor')
|
||||
|
||||
def carve_tunnel(self, start, end):
|
||||
"""Carve a tunnel between two points"""
|
||||
for x, y in tunnel_between(start, end):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite_index=46, tile_type='tunnel')
|
||||
|
||||
def get_blocking_entity_at(self, x, y):
|
||||
"""Return any blocking entity at the given position"""
|
||||
for entity in self.entities:
|
||||
if entity.blocks and entity.x == x and entity.y == y:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def is_blocked(self, x, y):
|
||||
"""Check if a tile blocks movement"""
|
||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||
return True
|
||||
|
||||
if not self.grid.at(x, y).walkable:
|
||||
return True
|
||||
|
||||
if self.get_blocking_entity_at(x, y):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_entity(self, entity):
|
||||
"""Add a GameObject to the map"""
|
||||
self.entities.append(entity)
|
||||
entity.attach_to_grid(self.grid)
|
||||
|
||||
class Engine:
|
||||
"""Main game engine"""
|
||||
|
||||
def __init__(self):
|
||||
self.game_map = None
|
||||
self.player = None
|
||||
self.entities = []
|
||||
self.messages = [] # Simple message log
|
||||
self.max_messages = 5
|
||||
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
window = mcrfpy.Window.get()
|
||||
window.title = "McRogueFace Roguelike - Part 6"
|
||||
|
||||
self.ui = mcrfpy.sceneUI("game")
|
||||
|
||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
self.ui.append(background)
|
||||
|
||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||
|
||||
self.setup_game()
|
||||
self.setup_input()
|
||||
self.setup_ui()
|
||||
|
||||
def add_message(self, text, color=(255, 255, 255)):
|
||||
"""Add a message to the log"""
|
||||
self.messages.append((text, color))
|
||||
if len(self.messages) > self.max_messages:
|
||||
self.messages.pop(0)
|
||||
self.update_message_display()
|
||||
|
||||
def update_message_display(self):
|
||||
"""Update the message display"""
|
||||
# Clear old messages
|
||||
for caption in self.message_captions:
|
||||
# Remove from UI (McRogueFace doesn't have remove, so we hide it)
|
||||
caption.text = ""
|
||||
|
||||
# Display current messages
|
||||
for i, (text, color) in enumerate(self.messages):
|
||||
if i < len(self.message_captions):
|
||||
self.message_captions[i].text = text
|
||||
self.message_captions[i].fill_color = mcrfpy.Color(*color)
|
||||
|
||||
def setup_game(self):
|
||||
"""Initialize the game world"""
|
||||
self.game_map = GameMap(80, 45)
|
||||
grid = self.game_map.create_grid(self.tileset)
|
||||
self.ui.append(grid)
|
||||
|
||||
# Create player
|
||||
self.player = create_player(0, 0)
|
||||
|
||||
# Generate the dungeon
|
||||
self.game_map.generate_dungeon(
|
||||
max_rooms=30,
|
||||
room_min_size=6,
|
||||
room_max_size=10,
|
||||
player=self.player,
|
||||
max_enemies_per_room=2
|
||||
)
|
||||
|
||||
# Add player to map
|
||||
self.game_map.add_entity(self.player)
|
||||
|
||||
# Store reference to all entities
|
||||
self.entities = [e for e in self.game_map.entities if e != self.player]
|
||||
|
||||
# Initial FOV calculation
|
||||
self.player.update_fov()
|
||||
|
||||
# Welcome message
|
||||
self.add_message("Welcome to the dungeon!", (100, 100, 255))
|
||||
|
||||
def handle_player_turn(self, action):
|
||||
"""Process the player's action"""
|
||||
if not self.player.is_alive:
|
||||
return
|
||||
|
||||
if isinstance(action, MovementAction):
|
||||
dest_x = self.player.x + action.dx
|
||||
dest_y = self.player.y + action.dy
|
||||
|
||||
# Check what's at the destination
|
||||
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
||||
|
||||
if target:
|
||||
# Attack!
|
||||
attack = MeleeAction(self.player, target)
|
||||
result = attack.perform()
|
||||
if result:
|
||||
text, color = result
|
||||
self.add_message(text, color)
|
||||
|
||||
# Check if target died
|
||||
if not target.is_alive:
|
||||
death_msg = f"The {target.name.replace('remains of ', '')} is dead!"
|
||||
self.add_message(death_msg, COLOR_ENEMY_DIE)
|
||||
|
||||
elif not self.game_map.is_blocked(dest_x, dest_y):
|
||||
# Move the player
|
||||
self.player.move(action.dx, action.dy)
|
||||
|
||||
elif isinstance(action, WaitAction):
|
||||
pass # Do nothing
|
||||
|
||||
# Enemy turns
|
||||
self.handle_enemy_turns()
|
||||
|
||||
def handle_enemy_turns(self):
|
||||
"""Let all enemies take their turn"""
|
||||
for entity in self.entities:
|
||||
if entity.is_alive:
|
||||
# Simple AI: if player is adjacent, attack. Otherwise, do nothing.
|
||||
dx = entity.x - self.player.x
|
||||
dy = entity.y - self.player.y
|
||||
distance = abs(dx) + abs(dy)
|
||||
|
||||
if distance == 1: # Adjacent to player
|
||||
attack = MeleeAction(entity, self.player)
|
||||
result = attack.perform()
|
||||
if result:
|
||||
text, color = result
|
||||
self.add_message(text, color)
|
||||
|
||||
# Check if player died
|
||||
if not self.player.is_alive:
|
||||
self.add_message("You have died!", COLOR_PLAYER_DIE)
|
||||
|
||||
def setup_input(self):
|
||||
"""Setup keyboard input handling"""
|
||||
def handle_keys(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
action = None
|
||||
|
||||
# Movement keys
|
||||
movement = {
|
||||
"Up": (0, -1), "Down": (0, 1),
|
||||
"Left": (-1, 0), "Right": (1, 0),
|
||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
|
||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||
}
|
||||
|
||||
if key in movement:
|
||||
dx, dy = movement[key]
|
||||
if dx == 0 and dy == 0:
|
||||
action = WaitAction()
|
||||
else:
|
||||
action = MovementAction(dx, dy)
|
||||
elif key == "Period":
|
||||
action = WaitAction()
|
||||
elif key == "Escape":
|
||||
mcrfpy.setScene(None)
|
||||
return
|
||||
|
||||
# Process the action
|
||||
if action:
|
||||
self.handle_player_turn(action)
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup UI elements"""
|
||||
title = mcrfpy.Caption("Combat System", 512, 30)
|
||||
title.font_size = 24
|
||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
self.ui.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption("Attack enemies by bumping into them!", 512, 60)
|
||||
instructions.font_size = 16
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.ui.append(instructions)
|
||||
|
||||
# Player stats
|
||||
self.hp_text = mcrfpy.Caption(f"HP: {self.player.hp}/{self.player.max_hp}", 50, 100)
|
||||
self.hp_text.font_size = 18
|
||||
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
self.ui.append(self.hp_text)
|
||||
|
||||
# Message log
|
||||
self.message_captions = []
|
||||
for i in range(self.max_messages):
|
||||
caption = mcrfpy.Caption("", 50, 620 + i * 20)
|
||||
caption.font_size = 14
|
||||
caption.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.ui.append(caption)
|
||||
self.message_captions.append(caption)
|
||||
|
||||
# Timer to update HP display
|
||||
def update_stats(dt):
|
||||
self.hp_text.text = f"HP: {self.player.hp}/{self.player.max_hp}"
|
||||
if self.player.hp <= 0:
|
||||
self.hp_text.fill_color = mcrfpy.Color(127, 0, 0)
|
||||
elif self.player.hp < self.player.max_hp // 3:
|
||||
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
else:
|
||||
self.hp_text.fill_color = mcrfpy.Color(0, 255, 0)
|
||||
|
||||
mcrfpy.setTimer("update_stats", update_stats, 100)
|
||||
|
||||
# Create and run the game
|
||||
engine = Engine()
|
||||
print("Part 6: Combat System!")
|
||||
print("Attack enemies to defeat them, but watch your HP!")
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 0: Introduction to Scene, Texture, and Grid
|
||||
|
||||
This tutorial introduces the basic building blocks:
|
||||
- Scene: A container for UI elements and game state
|
||||
- Texture: Loading image assets for use in the game
|
||||
- Grid: A tilemap component for rendering tile-based worlds
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Create and activate a new scene
|
||||
mcrfpy.createScene("tutorial")
|
||||
mcrfpy.setScene("tutorial")
|
||||
|
||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||
|
||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||
|
||||
# calculating the size in pixels to fit the entire grid on-screen
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size, # height and width on screen
|
||||
)
|
||||
|
||||
grid.zoom = zoom
|
||||
grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Fill the grid with a simple pattern
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
# Create walls around the edges
|
||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||
tile_index = random.choice(WALL_TILES)
|
||||
else:
|
||||
# Fill interior with floor tiles
|
||||
tile_index = random.choice(FLOOR_TILES)
|
||||
|
||||
# Set the tile at this position
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = tile_index
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Add a title caption
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 0",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
# Add instructions
|
||||
instructions = mcrfpy.Caption((280, 750),
|
||||
text="Scene + Texture + Grid = Tilemap!",
|
||||
)
|
||||
instructions.font_size=18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
print("Tutorial Part 0 loaded!")
|
||||
print(f"Created a {grid.grid_size[0]}x{grid.grid_size[1]} grid")
|
||||
print(f"Grid positioned at ({grid.x}, {grid.y})")
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 1: Entities and Keyboard Input
|
||||
|
||||
This tutorial builds on Part 0 by adding:
|
||||
- Entity: A game object that can be placed in a grid
|
||||
- Keyboard handling: Responding to key presses to move the entity
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Create and activate a new scene
|
||||
mcrfpy.createScene("tutorial")
|
||||
mcrfpy.setScene("tutorial")
|
||||
|
||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Load the hero sprite texture (32x32 sprite sheet)
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||
|
||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||
|
||||
# calculating the size in pixels to fit the entire grid on-screen
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size, # height and width on screen
|
||||
)
|
||||
|
||||
grid.zoom = zoom
|
||||
grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Fill the grid with a simple pattern
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
# Create walls around the edges
|
||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||
tile_index = random.choice(WALL_TILES)
|
||||
else:
|
||||
# Fill interior with floor tiles
|
||||
tile_index = random.choice(FLOOR_TILES)
|
||||
|
||||
# Set the tile at this position
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = tile_index
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Create a player entity at position (4, 4)
|
||||
player = mcrfpy.Entity(
|
||||
(4, 4), # Entity positions are tile coordinates
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Use the first sprite in the texture
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
|
||||
# Define keyboard handler
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input to move the player"""
|
||||
if state == "start": # Only respond to key press, not release
|
||||
# Get current player position in grid coordinates
|
||||
px, py = player.x, player.y
|
||||
|
||||
# Calculate new position based on key press
|
||||
if key == "W" or key == "Up":
|
||||
py -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
py += 1
|
||||
elif key == "A" or key == "Left":
|
||||
px -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
px += 1
|
||||
|
||||
# Update player position (no collision checking yet)
|
||||
player.x = px
|
||||
player.y = py
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add a title caption
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 1",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
# Add instructions
|
||||
instructions = mcrfpy.Caption((200, 750),
|
||||
text="Use WASD or Arrow Keys to move the hero!",
|
||||
)
|
||||
instructions.font_size=18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
print("Tutorial Part 1 loaded!")
|
||||
print(f"Player entity created at grid position (4, 4)")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 1: Entities and Keyboard Input
|
||||
|
||||
This tutorial builds on Part 0 by adding:
|
||||
- Entity: A game object that can be placed in a grid
|
||||
- Keyboard handling: Responding to key presses to move the entity
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Create and activate a new scene
|
||||
mcrfpy.createScene("tutorial")
|
||||
mcrfpy.setScene("tutorial")
|
||||
|
||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Load the hero sprite texture (32x32 sprite sheet)
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||
|
||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||
|
||||
# calculating the size in pixels to fit the entire grid on-screen
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size, # height and width on screen
|
||||
)
|
||||
|
||||
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Fill the grid with a simple pattern
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
# Create walls around the edges
|
||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||
tile_index = random.choice(WALL_TILES)
|
||||
else:
|
||||
# Fill interior with floor tiles
|
||||
tile_index = random.choice(FLOOR_TILES)
|
||||
|
||||
# Set the tile at this position
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = tile_index
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Create a player entity at position (4, 4)
|
||||
player = mcrfpy.Entity(
|
||||
(4, 4), # Entity positions are tile coordinates
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Use the first sprite in the texture
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||
|
||||
# Define keyboard handler
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input to move the player"""
|
||||
if state == "start": # Only respond to key press, not release
|
||||
# Get current player position in grid coordinates
|
||||
px, py = player.x, player.y
|
||||
|
||||
# Calculate new position based on key press
|
||||
if key == "W" or key == "Up":
|
||||
py -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
py += 1
|
||||
elif key == "A" or key == "Left":
|
||||
px -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
px += 1
|
||||
|
||||
# Update player position (no collision checking yet)
|
||||
player.x = px
|
||||
player.y = py
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add a title caption
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 1",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
# Add instructions
|
||||
instructions = mcrfpy.Caption((200, 750),
|
||||
text="Use WASD or Arrow Keys to move the hero!",
|
||||
)
|
||||
instructions.font_size=18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
print("Tutorial Part 1 loaded!")
|
||||
print(f"Player entity created at grid position (4, 4)")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 2: Animated Movement
|
||||
|
||||
This tutorial builds on Part 1 by adding:
|
||||
- Animation system for smooth movement
|
||||
- Movement that takes 0.5 seconds per tile
|
||||
- Input blocking during movement animation
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Create and activate a new scene
|
||||
mcrfpy.createScene("tutorial")
|
||||
mcrfpy.setScene("tutorial")
|
||||
|
||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Load the hero sprite texture (32x32 sprite sheet)
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||
|
||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||
|
||||
# calculating the size in pixels to fit the entire grid on-screen
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size, # height and width on screen
|
||||
)
|
||||
|
||||
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Fill the grid with a simple pattern
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
# Create walls around the edges
|
||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||
tile_index = random.choice(WALL_TILES)
|
||||
else:
|
||||
# Fill interior with floor tiles
|
||||
tile_index = random.choice(FLOOR_TILES)
|
||||
|
||||
# Set the tile at this position
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = tile_index
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Create a player entity at position (4, 4)
|
||||
player = mcrfpy.Entity(
|
||||
(4, 4), # Entity positions are tile coordinates
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Use the first sprite in the texture
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||
|
||||
# Movement state tracking
|
||||
is_moving = False
|
||||
move_animations = [] # Track active animations
|
||||
|
||||
# Animation completion callback
|
||||
def movement_complete(runtime):
|
||||
"""Called when movement animation completes"""
|
||||
global is_moving
|
||||
is_moving = False
|
||||
# Ensure grid is centered on final position
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||
|
||||
motion_speed = 0.30 # seconds per tile
|
||||
# Define keyboard handler
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input to move the player"""
|
||||
global is_moving, move_animations
|
||||
|
||||
if state == "start" and not is_moving: # Only respond to key press when not moving
|
||||
# Get current player position in grid coordinates
|
||||
px, py = player.x, player.y
|
||||
new_x, new_y = px, py
|
||||
|
||||
# Calculate new position based on key press
|
||||
if key == "W" or key == "Up":
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
new_x += 1
|
||||
|
||||
# If position changed, start movement animation
|
||||
if new_x != px or new_y != py:
|
||||
is_moving = True
|
||||
|
||||
# Create animations for player position
|
||||
anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad")
|
||||
anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
||||
anim_x.start(player)
|
||||
anim_y.start(player)
|
||||
|
||||
# Animate grid center to follow player
|
||||
center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
||||
center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
||||
center_x.start(grid)
|
||||
center_y.start(grid)
|
||||
|
||||
# Set a timer to mark movement as complete
|
||||
mcrfpy.setTimer("move_complete", movement_complete, 500)
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add a title caption
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 2",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
# Add instructions
|
||||
instructions = mcrfpy.Caption((150, 750),
|
||||
text="Smooth movement! Each step takes 0.5 seconds.",
|
||||
)
|
||||
instructions.font_size=18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
print("Tutorial Part 2 loaded!")
|
||||
print(f"Player entity created at grid position (4, 4)")
|
||||
print("Movement is now animated over 0.5 seconds per tile!")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 2: Enhanced with Single Move Queue
|
||||
|
||||
This tutorial builds on Part 2 by adding:
|
||||
- Single queued move system for responsive input
|
||||
- Debug display showing position and queue status
|
||||
- Smooth continuous movement when keys are held
|
||||
- Animation callbacks to prevent race conditions
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Create and activate a new scene
|
||||
mcrfpy.createScene("tutorial")
|
||||
mcrfpy.setScene("tutorial")
|
||||
|
||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Load the hero sprite texture (32x32 sprite sheet)
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||
|
||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||
|
||||
# calculating the size in pixels to fit the entire grid on-screen
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size, # height and width on screen
|
||||
)
|
||||
|
||||
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Fill the grid with a simple pattern
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
# Create walls around the edges
|
||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||
tile_index = random.choice(WALL_TILES)
|
||||
else:
|
||||
# Fill interior with floor tiles
|
||||
tile_index = random.choice(FLOOR_TILES)
|
||||
|
||||
# Set the tile at this position
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = tile_index
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Create a player entity at position (4, 4)
|
||||
player = mcrfpy.Entity(
|
||||
(4, 4), # Entity positions are tile coordinates
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Use the first sprite in the texture
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||
|
||||
# Movement state tracking
|
||||
is_moving = False
|
||||
move_queue = [] # List to store queued moves (max 1 item)
|
||||
#last_position = (4, 4) # Track last position
|
||||
current_destination = None # Track where we're currently moving to
|
||||
current_move = None # Track current move direction
|
||||
|
||||
# Store animation references
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
grid_anim_x = None
|
||||
grid_anim_y = None
|
||||
|
||||
# Debug display caption
|
||||
debug_caption = mcrfpy.Caption((10, 40),
|
||||
text="Last: (4, 4) | Queue: 0 | Dest: None",
|
||||
)
|
||||
debug_caption.font_size = 16
|
||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
||||
|
||||
# Additional debug caption for movement state
|
||||
move_debug_caption = mcrfpy.Caption((10, 60),
|
||||
text="Moving: False | Current: None | Queued: None",
|
||||
)
|
||||
move_debug_caption.font_size = 16
|
||||
move_debug_caption.fill_color = mcrfpy.Color(255, 200, 0, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(move_debug_caption)
|
||||
|
||||
def key_to_direction(key):
|
||||
"""Convert key to direction string"""
|
||||
if key == "W" or key == "Up":
|
||||
return "Up"
|
||||
elif key == "S" or key == "Down":
|
||||
return "Down"
|
||||
elif key == "A" or key == "Left":
|
||||
return "Left"
|
||||
elif key == "D" or key == "Right":
|
||||
return "Right"
|
||||
return None
|
||||
|
||||
def update_debug_display():
|
||||
"""Update the debug caption with current state"""
|
||||
queue_count = len(move_queue)
|
||||
dest_text = f"({current_destination[0]}, {current_destination[1]})" if current_destination else "None"
|
||||
debug_caption.text = f"Last: ({player.x}, {player.y}) | Queue: {queue_count} | Dest: {dest_text}"
|
||||
|
||||
# Update movement state debug
|
||||
current_dir = key_to_direction(current_move) if current_move else "None"
|
||||
queued_dir = key_to_direction(move_queue[0]) if move_queue else "None"
|
||||
move_debug_caption.text = f"Moving: {is_moving} | Current: {current_dir} | Queued: {queued_dir}"
|
||||
|
||||
# Animation completion callback
|
||||
def movement_complete(anim, target):
|
||||
"""Called when movement animation completes"""
|
||||
global is_moving, move_queue, current_destination, current_move
|
||||
global player_anim_x, player_anim_y
|
||||
print(f"In callback for animation: {anim=} {target=}")
|
||||
# Clear movement state
|
||||
is_moving = False
|
||||
current_move = None
|
||||
current_destination = None
|
||||
# Clear animation references
|
||||
player_anim_x = None
|
||||
player_anim_y = None
|
||||
|
||||
# Update last position to where we actually are now
|
||||
#last_position = (int(player.x), int(player.y))
|
||||
|
||||
# Ensure grid is centered on final position
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||
|
||||
# Check if there's a queued move
|
||||
if move_queue:
|
||||
# Pop the next move from the queue
|
||||
next_move = move_queue.pop(0)
|
||||
print(f"Processing queued move: {next_move}")
|
||||
# Process it like a fresh input
|
||||
process_move(next_move)
|
||||
|
||||
update_debug_display()
|
||||
|
||||
motion_speed = 0.30 # seconds per tile
|
||||
|
||||
def process_move(key):
|
||||
"""Process a move based on the key"""
|
||||
global is_moving, current_move, current_destination, move_queue
|
||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
||||
|
||||
# If already moving, just update the queue
|
||||
if is_moving:
|
||||
print(f"process_move processing {key=} as a queued move (is_moving = True)")
|
||||
# Clear queue and add new move (only keep 1 queued move)
|
||||
move_queue.clear()
|
||||
move_queue.append(key)
|
||||
update_debug_display()
|
||||
return
|
||||
print(f"process_move processing {key=} as a new, immediate animation (is_moving = False)")
|
||||
# Calculate new position from current position
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
# Calculate new position based on key press (only one tile movement)
|
||||
if key == "W" or key == "Up":
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
new_x += 1
|
||||
|
||||
# Start the move if position changed
|
||||
if new_x != px or new_y != py:
|
||||
is_moving = True
|
||||
current_move = key
|
||||
current_destination = (new_x, new_y)
|
||||
# only animate a single axis, same callback from either
|
||||
if new_x != px:
|
||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
||||
player_anim_x.start(player)
|
||||
elif new_y != py:
|
||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad", callback=movement_complete)
|
||||
player_anim_y.start(player)
|
||||
|
||||
# Animate grid center to follow player
|
||||
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
||||
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
||||
grid_anim_x.start(grid)
|
||||
grid_anim_y.start(grid)
|
||||
|
||||
update_debug_display()
|
||||
|
||||
# Define keyboard handler
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input to move the player"""
|
||||
if state == "start":
|
||||
# Only process movement keys
|
||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
||||
print(f"handle_keys producing actual input: {key=}")
|
||||
process_move(key)
|
||||
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add a title caption
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 2 Enhanced",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
# Add instructions
|
||||
instructions = mcrfpy.Caption((150, 750),
|
||||
text="One-move queue system with animation callbacks!",
|
||||
)
|
||||
instructions.font_size=18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
print("Tutorial Part 2 Enhanced loaded!")
|
||||
print(f"Player entity created at grid position (4, 4)")
|
||||
print("Movement now uses animation callbacks to prevent race conditions!")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
"""
|
||||
McRogueFace Tutorial - Part 2: Animated Movement
|
||||
|
||||
This tutorial builds on Part 1 by adding:
|
||||
- Animation system for smooth movement
|
||||
- Movement that takes 0.5 seconds per tile
|
||||
- Input blocking during movement animation
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Create and activate a new scene
|
||||
mcrfpy.createScene("tutorial")
|
||||
mcrfpy.setScene("tutorial")
|
||||
|
||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
||||
|
||||
# Load the hero sprite texture (32x32 sprite sheet)
|
||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
||||
|
||||
# Create a grid of tiles
|
||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
||||
|
||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
||||
|
||||
# calculating the size in pixels to fit the entire grid on-screen
|
||||
zoom = 2.0
|
||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
||||
|
||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
pos=grid_position,
|
||||
grid_size=(grid_width, grid_height),
|
||||
texture=texture,
|
||||
size=grid_size, # height and width on screen
|
||||
)
|
||||
|
||||
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
||||
|
||||
# Define tile types
|
||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
||||
WALL_TILES = [3, 7, 11]
|
||||
|
||||
# Fill the grid with a simple pattern
|
||||
for y in range(grid_height):
|
||||
for x in range(grid_width):
|
||||
# Create walls around the edges
|
||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
||||
tile_index = random.choice(WALL_TILES)
|
||||
else:
|
||||
# Fill interior with floor tiles
|
||||
tile_index = random.choice(FLOOR_TILES)
|
||||
|
||||
# Set the tile at this position
|
||||
point = grid.at(x, y)
|
||||
if point:
|
||||
point.tilesprite = tile_index
|
||||
|
||||
# Add the grid to the scene
|
||||
mcrfpy.sceneUI("tutorial").append(grid)
|
||||
|
||||
# Create a player entity at position (4, 4)
|
||||
player = mcrfpy.Entity(
|
||||
(4, 4), # Entity positions are tile coordinates
|
||||
texture=hero_texture,
|
||||
sprite_index=0 # Use the first sprite in the texture
|
||||
)
|
||||
|
||||
# Add the player entity to the grid
|
||||
grid.entities.append(player)
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
||||
|
||||
# Movement state tracking
|
||||
is_moving = False
|
||||
move_animations = [] # Track active animations
|
||||
|
||||
# Animation completion callback
|
||||
def movement_complete(runtime):
|
||||
"""Called when movement animation completes"""
|
||||
global is_moving
|
||||
is_moving = False
|
||||
# Ensure grid is centered on final position
|
||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
||||
|
||||
motion_speed = 0.30 # seconds per tile
|
||||
# Define keyboard handler
|
||||
def handle_keys(key, state):
|
||||
"""Handle keyboard input to move the player"""
|
||||
global is_moving, move_animations
|
||||
|
||||
if state == "start" and not is_moving: # Only respond to key press when not moving
|
||||
# Get current player position in grid coordinates
|
||||
px, py = player.x, player.y
|
||||
new_x, new_y = px, py
|
||||
|
||||
# Calculate new position based on key press
|
||||
if key == "W" or key == "Up":
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
new_x += 1
|
||||
|
||||
# If position changed, start movement animation
|
||||
if new_x != px or new_y != py:
|
||||
is_moving = True
|
||||
|
||||
# Create animations for player position
|
||||
anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad")
|
||||
anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
||||
anim_x.start(player)
|
||||
anim_y.start(player)
|
||||
|
||||
# Animate grid center to follow player
|
||||
center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
||||
center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
||||
center_x.start(grid)
|
||||
center_y.start(grid)
|
||||
|
||||
# Set a timer to mark movement as complete
|
||||
mcrfpy.setTimer("move_complete", movement_complete, 500)
|
||||
|
||||
# Register the keyboard handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Add a title caption
|
||||
title = mcrfpy.Caption((320, 10),
|
||||
text="McRogueFace Tutorial - Part 2",
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(title)
|
||||
|
||||
# Add instructions
|
||||
instructions = mcrfpy.Caption((150, 750),
|
||||
"Smooth movement! Each step takes 0.5 seconds.",
|
||||
)
|
||||
instructions.font_size=18
|
||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
||||
|
||||
print("Tutorial Part 2 loaded!")
|
||||
print(f"Player entity created at grid position (4, 4)")
|
||||
print("Movement is now animated over 0.5 seconds per tile!")
|
||||
print("Use WASD or Arrow keys to move!")
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -3,8 +3,6 @@
|
|||
#include "UIEntity.h"
|
||||
#include "PyAnimation.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "GameEngine.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
|
|
@ -48,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) {
|
||||
|
|
@ -369,14 +362,9 @@ void Animation::triggerCallback() {
|
|||
Py_DECREF(args);
|
||||
|
||||
if (!result) {
|
||||
std::cerr << "Animation callback raised an exception:" << std::endl;
|
||||
// Print error but don't crash
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
|
||||
// Check if we should exit on exception
|
||||
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
|
||||
McRFPy_API::signalPythonException();
|
||||
}
|
||||
PyErr_Clear(); // Clear the error state
|
||||
} else {
|
||||
Py_DECREF(result);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
#include "BenchmarkLogger.h"
|
||||
#include "GameEngine.h"
|
||||
|
||||
// Global benchmark logger instance
|
||||
BenchmarkLogger g_benchmarkLogger;
|
||||
|
||||
void BenchmarkLogger::recordFrame(const ProfilingMetrics& metrics) {
|
||||
if (!running) return;
|
||||
|
||||
auto now = std::chrono::high_resolution_clock::now();
|
||||
double timestamp_ms = std::chrono::duration<double, std::milli>(now - start_time).count();
|
||||
|
||||
BenchmarkFrame frame;
|
||||
frame.frame_number = ++frame_counter;
|
||||
frame.timestamp_ms = timestamp_ms;
|
||||
frame.frame_time_ms = metrics.frameTime;
|
||||
frame.fps = metrics.fps;
|
||||
|
||||
frame.work_time_ms = metrics.workTime;
|
||||
frame.grid_render_ms = metrics.gridRenderTime;
|
||||
frame.entity_render_ms = metrics.entityRenderTime;
|
||||
frame.python_time_ms = metrics.pythonScriptTime;
|
||||
frame.animation_time_ms = metrics.animationTime;
|
||||
frame.fov_overlay_ms = metrics.fovOverlayTime;
|
||||
|
||||
frame.draw_calls = metrics.drawCalls;
|
||||
frame.ui_elements = metrics.uiElements;
|
||||
frame.visible_elements = metrics.visibleElements;
|
||||
frame.grid_cells_rendered = metrics.gridCellsRendered;
|
||||
frame.entities_rendered = metrics.entitiesRendered;
|
||||
frame.total_entities = metrics.totalEntities;
|
||||
|
||||
// Move pending logs to this frame
|
||||
frame.logs = std::move(pending_logs);
|
||||
pending_logs.clear();
|
||||
|
||||
frames.push_back(std::move(frame));
|
||||
}
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <stdexcept>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <process.h>
|
||||
#define getpid _getpid
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
// Forward declaration
|
||||
struct ProfilingMetrics;
|
||||
|
||||
/**
|
||||
* @brief Frame data captured during benchmarking
|
||||
*/
|
||||
struct BenchmarkFrame {
|
||||
int frame_number;
|
||||
double timestamp_ms; // Time since benchmark start
|
||||
float frame_time_ms;
|
||||
int fps;
|
||||
|
||||
// Detailed timing breakdown
|
||||
float work_time_ms; // Actual work time (frame_time - sleep_time)
|
||||
float grid_render_ms;
|
||||
float entity_render_ms;
|
||||
float python_time_ms;
|
||||
float animation_time_ms;
|
||||
float fov_overlay_ms;
|
||||
|
||||
// Counts
|
||||
int draw_calls;
|
||||
int ui_elements;
|
||||
int visible_elements;
|
||||
int grid_cells_rendered;
|
||||
int entities_rendered;
|
||||
int total_entities;
|
||||
|
||||
// User-provided log messages for this frame
|
||||
std::vector<std::string> logs;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Benchmark logging system for capturing performance data to JSON files
|
||||
*
|
||||
* Usage from Python:
|
||||
* mcrfpy.start_benchmark() # Start capturing
|
||||
* mcrfpy.log_benchmark("msg") # Add comment to current frame
|
||||
* filename = mcrfpy.end_benchmark() # Stop and get filename
|
||||
*/
|
||||
class BenchmarkLogger {
|
||||
private:
|
||||
bool running;
|
||||
std::string filename;
|
||||
std::chrono::high_resolution_clock::time_point start_time;
|
||||
std::vector<BenchmarkFrame> frames;
|
||||
std::vector<std::string> pending_logs; // Logs for current frame (before it's recorded)
|
||||
int frame_counter;
|
||||
|
||||
// Generate filename based on PID and timestamp
|
||||
std::string generateFilename() {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto time_t = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm tm = *std::localtime(&time_t);
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "benchmark_" << getpid() << "_"
|
||||
<< std::put_time(&tm, "%Y%m%d_%H%M%S") << ".json";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// Get current timestamp as ISO 8601 string
|
||||
std::string getCurrentTimestamp() {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto time_t = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm tm = *std::localtime(&time_t);
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S");
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// Escape string for JSON
|
||||
std::string escapeJson(const std::string& str) {
|
||||
std::ostringstream oss;
|
||||
for (char c : str) {
|
||||
switch (c) {
|
||||
case '"': oss << "\\\""; break;
|
||||
case '\\': oss << "\\\\"; break;
|
||||
case '\b': oss << "\\b"; break;
|
||||
case '\f': oss << "\\f"; break;
|
||||
case '\n': oss << "\\n"; break;
|
||||
case '\r': oss << "\\r"; break;
|
||||
case '\t': oss << "\\t"; break;
|
||||
default:
|
||||
if ('\x00' <= c && c <= '\x1f') {
|
||||
oss << "\\u" << std::hex << std::setw(4) << std::setfill('0') << (int)c;
|
||||
} else {
|
||||
oss << c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string start_timestamp;
|
||||
|
||||
public:
|
||||
BenchmarkLogger() : running(false), frame_counter(0) {}
|
||||
|
||||
/**
|
||||
* @brief Start benchmark logging
|
||||
* @throws std::runtime_error if already running
|
||||
*/
|
||||
void start() {
|
||||
if (running) {
|
||||
throw std::runtime_error("Benchmark already running. Call end_benchmark() first.");
|
||||
}
|
||||
|
||||
running = true;
|
||||
filename = generateFilename();
|
||||
start_time = std::chrono::high_resolution_clock::now();
|
||||
start_timestamp = getCurrentTimestamp();
|
||||
frames.clear();
|
||||
pending_logs.clear();
|
||||
frame_counter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop benchmark logging and write to file
|
||||
* @return The filename that was written
|
||||
* @throws std::runtime_error if not running
|
||||
*/
|
||||
std::string end() {
|
||||
if (!running) {
|
||||
throw std::runtime_error("No benchmark running. Call start_benchmark() first.");
|
||||
}
|
||||
|
||||
running = false;
|
||||
|
||||
// Calculate duration
|
||||
auto end_time = std::chrono::high_resolution_clock::now();
|
||||
double duration_seconds = std::chrono::duration<double>(end_time - start_time).count();
|
||||
std::string end_timestamp = getCurrentTimestamp();
|
||||
|
||||
// Write JSON file
|
||||
std::ofstream file(filename);
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("Failed to open benchmark file for writing: " + filename);
|
||||
}
|
||||
|
||||
file << "{\n";
|
||||
file << " \"benchmark\": {\n";
|
||||
file << " \"pid\": " << getpid() << ",\n";
|
||||
file << " \"start_time\": \"" << start_timestamp << "\",\n";
|
||||
file << " \"end_time\": \"" << end_timestamp << "\",\n";
|
||||
file << " \"total_frames\": " << frames.size() << ",\n";
|
||||
file << " \"duration_seconds\": " << std::fixed << std::setprecision(3) << duration_seconds << "\n";
|
||||
file << " },\n";
|
||||
|
||||
file << " \"frames\": [\n";
|
||||
for (size_t i = 0; i < frames.size(); ++i) {
|
||||
const auto& f = frames[i];
|
||||
file << " {\n";
|
||||
file << " \"frame_number\": " << f.frame_number << ",\n";
|
||||
file << " \"timestamp_ms\": " << std::fixed << std::setprecision(3) << f.timestamp_ms << ",\n";
|
||||
file << " \"frame_time_ms\": " << std::setprecision(3) << f.frame_time_ms << ",\n";
|
||||
file << " \"fps\": " << f.fps << ",\n";
|
||||
file << " \"work_time_ms\": " << std::setprecision(3) << f.work_time_ms << ",\n";
|
||||
file << " \"grid_render_ms\": " << std::setprecision(3) << f.grid_render_ms << ",\n";
|
||||
file << " \"entity_render_ms\": " << std::setprecision(3) << f.entity_render_ms << ",\n";
|
||||
file << " \"python_time_ms\": " << std::setprecision(3) << f.python_time_ms << ",\n";
|
||||
file << " \"animation_time_ms\": " << std::setprecision(3) << f.animation_time_ms << ",\n";
|
||||
file << " \"fov_overlay_ms\": " << std::setprecision(3) << f.fov_overlay_ms << ",\n";
|
||||
file << " \"draw_calls\": " << f.draw_calls << ",\n";
|
||||
file << " \"ui_elements\": " << f.ui_elements << ",\n";
|
||||
file << " \"visible_elements\": " << f.visible_elements << ",\n";
|
||||
file << " \"grid_cells_rendered\": " << f.grid_cells_rendered << ",\n";
|
||||
file << " \"entities_rendered\": " << f.entities_rendered << ",\n";
|
||||
file << " \"total_entities\": " << f.total_entities << ",\n";
|
||||
|
||||
// Write logs array
|
||||
file << " \"logs\": [";
|
||||
for (size_t j = 0; j < f.logs.size(); ++j) {
|
||||
file << "\"" << escapeJson(f.logs[j]) << "\"";
|
||||
if (j < f.logs.size() - 1) file << ", ";
|
||||
}
|
||||
file << "]\n";
|
||||
|
||||
file << " }";
|
||||
if (i < frames.size() - 1) file << ",";
|
||||
file << "\n";
|
||||
}
|
||||
file << " ]\n";
|
||||
file << "}\n";
|
||||
|
||||
file.close();
|
||||
|
||||
std::string result = filename;
|
||||
filename.clear();
|
||||
frames.clear();
|
||||
pending_logs.clear();
|
||||
frame_counter = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add a log message to the current frame
|
||||
* @param message The message to log
|
||||
* @throws std::runtime_error if not running
|
||||
*/
|
||||
void log(const std::string& message) {
|
||||
if (!running) {
|
||||
throw std::runtime_error("No benchmark running. Call start_benchmark() first.");
|
||||
}
|
||||
pending_logs.push_back(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Record frame data (called by game loop at end of each frame)
|
||||
* @param metrics The current frame's profiling metrics
|
||||
*/
|
||||
void recordFrame(const ProfilingMetrics& metrics);
|
||||
|
||||
/**
|
||||
* @brief Check if benchmark is currently running
|
||||
*/
|
||||
bool isRunning() const { return running; }
|
||||
|
||||
/**
|
||||
* @brief Get current frame count
|
||||
*/
|
||||
int getFrameCount() const { return frame_counter; }
|
||||
};
|
||||
|
||||
// Global benchmark logger instance
|
||||
extern BenchmarkLogger g_benchmarkLogger;
|
||||
|
|
@ -121,12 +121,6 @@ CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& confi
|
|||
current_arg++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--continue-after-exceptions") {
|
||||
config.exit_on_exception = false;
|
||||
current_arg++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If no flags matched, treat as positional argument (script name)
|
||||
if (arg[0] != '-') {
|
||||
|
|
@ -166,8 +160,6 @@ void CommandLineParser::print_help() {
|
|||
<< " --audio-off : disable audio\n"
|
||||
<< " --audio-on : enable audio (even in headless mode)\n"
|
||||
<< " --screenshot [path] : take a screenshot in headless mode\n"
|
||||
<< " --continue-after-exceptions : don't exit on Python callback exceptions\n"
|
||||
<< " (default: exit on first exception)\n"
|
||||
<< "\n"
|
||||
<< "Arguments:\n"
|
||||
<< " file : program read from script file\n"
|
||||
|
|
@ -176,5 +168,5 @@ void CommandLineParser::print_help() {
|
|||
}
|
||||
|
||||
void CommandLineParser::print_version() {
|
||||
std::cout << "Python 3.14.0 (McRogueFace embedded)\n";
|
||||
std::cout << "Python 3.12.0 (McRogueFace embedded)\n";
|
||||
}
|
||||
|
|
@ -5,10 +5,6 @@
|
|||
#include "UITestScene.h"
|
||||
#include "Resources.h"
|
||||
#include "Animation.h"
|
||||
#include "Timer.h"
|
||||
#include "BenchmarkLogger.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui-SFML.h"
|
||||
#include <cmath>
|
||||
|
||||
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
|
||||
|
|
@ -34,13 +30,8 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
|||
window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close | sf::Style::Resize);
|
||||
window->setFramerateLimit(60);
|
||||
render_target = window.get();
|
||||
|
||||
// Initialize ImGui for the window
|
||||
if (ImGui::SFML::Init(*window)) {
|
||||
imguiInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
visible = render_target->getDefaultView();
|
||||
|
||||
// Initialize the game view
|
||||
|
|
@ -50,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() &&
|
||||
|
|
@ -71,31 +59,23 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
|||
McRFPy_API::executePyString("import mcrfpy");
|
||||
McRFPy_API::executeScript("scripts/game.py");
|
||||
}
|
||||
|
||||
// Note: --exec scripts are NOT executed here.
|
||||
// They are executed via executeStartupScripts() after the final engine is set up.
|
||||
// This prevents double-execution when main.cpp creates multiple GameEngine instances.
|
||||
|
||||
clock.restart();
|
||||
runtime.restart();
|
||||
}
|
||||
|
||||
void GameEngine::executeStartupScripts()
|
||||
{
|
||||
|
||||
// Execute any --exec scripts in order
|
||||
// This is called ONCE from main.cpp after the final engine is set up
|
||||
if (!config.exec_scripts.empty()) {
|
||||
if (!Py_IsInitialized()) {
|
||||
McRFPy_API::api_init();
|
||||
}
|
||||
McRFPy_API::executePyString("import mcrfpy");
|
||||
|
||||
|
||||
for (const auto& exec_script : config.exec_scripts) {
|
||||
std::cout << "Executing script: " << exec_script << std::endl;
|
||||
McRFPy_API::executeScript(exec_script.string());
|
||||
}
|
||||
std::cout << "All --exec scripts completed" << std::endl;
|
||||
}
|
||||
|
||||
clock.restart();
|
||||
runtime.restart();
|
||||
}
|
||||
|
||||
GameEngine::~GameEngine()
|
||||
|
|
@ -104,7 +84,6 @@ GameEngine::~GameEngine()
|
|||
for (auto& [name, scene] : scenes) {
|
||||
delete scene;
|
||||
}
|
||||
delete profilerOverlay;
|
||||
}
|
||||
|
||||
void GameEngine::cleanup()
|
||||
|
|
@ -124,12 +103,6 @@ void GameEngine::cleanup()
|
|||
McRFPy_API::game = nullptr;
|
||||
}
|
||||
|
||||
// Shutdown ImGui before closing window
|
||||
if (imguiInitialized) {
|
||||
ImGui::SFML::Shutdown();
|
||||
imguiInitialized = false;
|
||||
}
|
||||
|
||||
// Force close the window if it's still open
|
||||
if (window && window->isOpen()) {
|
||||
window->close();
|
||||
|
|
@ -137,10 +110,6 @@ void GameEngine::cleanup()
|
|||
}
|
||||
|
||||
Scene* GameEngine::currentScene() { return scenes[scene]; }
|
||||
Scene* GameEngine::getScene(const std::string& name) {
|
||||
auto it = scenes.find(name);
|
||||
return (it != scenes.end()) ? it->second : nullptr;
|
||||
}
|
||||
void GameEngine::changeScene(std::string s)
|
||||
{
|
||||
changeScene(s, TransitionType::None, 0.0f);
|
||||
|
|
@ -229,24 +198,15 @@ 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);
|
||||
}
|
||||
|
||||
if (!headless) {
|
||||
sUserInput();
|
||||
|
||||
// Update ImGui
|
||||
if (imguiInitialized) {
|
||||
ImGui::SFML::Update(*window, clock.getElapsedTime());
|
||||
}
|
||||
}
|
||||
if (!paused)
|
||||
{
|
||||
|
|
@ -279,21 +239,6 @@ void GameEngine::run()
|
|||
currentScene()->render();
|
||||
}
|
||||
|
||||
// Update and render profiler overlay (if enabled)
|
||||
if (profilerOverlay && !headless) {
|
||||
profilerOverlay->update(metrics);
|
||||
profilerOverlay->render(*render_target);
|
||||
}
|
||||
|
||||
// Render ImGui console overlay
|
||||
if (imguiInitialized && !headless) {
|
||||
console.render();
|
||||
ImGui::SFML::Render(*window);
|
||||
}
|
||||
|
||||
// Record work time before display (which may block for vsync/framerate limit)
|
||||
metrics.workTime = clock.getElapsedTime().asSeconds() * 1000.0f;
|
||||
|
||||
// Display the frame
|
||||
if (headless) {
|
||||
headless_renderer->display();
|
||||
|
|
@ -312,10 +257,7 @@ void GameEngine::run()
|
|||
|
||||
// Update profiling metrics
|
||||
metrics.updateFrameTime(frameTime * 1000.0f); // Convert to milliseconds
|
||||
|
||||
// Record frame data for benchmark logging (if running)
|
||||
g_benchmarkLogger.recordFrame(metrics);
|
||||
|
||||
|
||||
int whole_fps = metrics.fps;
|
||||
int tenth_fps = (metrics.fps * 10) % 10;
|
||||
|
||||
|
|
@ -327,28 +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;
|
||||
}
|
||||
|
||||
// Check if a Python exception has signaled exit
|
||||
if (McRFPy_API::shouldExit()) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Clean up before exiting the run loop
|
||||
cleanup();
|
||||
|
||||
// #144: Quick exit to avoid cleanup segfaults in Python/C++ destructor ordering
|
||||
// This is a pragmatic workaround - proper cleanup would require careful
|
||||
// attention to shared_ptr cycles and Python GC interaction
|
||||
std::_Exit(0);
|
||||
}
|
||||
|
||||
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()) {
|
||||
|
|
@ -360,17 +287,13 @@ std::shared_ptr<Timer> GameEngine::getTimer(const std::string& name)
|
|||
void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
|
||||
{
|
||||
auto it = timers.find(name);
|
||||
|
||||
// #153 - In headless mode, use simulation_time instead of real-time clock
|
||||
int now = headless ? simulation_time : runtime.getElapsedTime().asMilliseconds();
|
||||
|
||||
if (it != timers.end()) // overwrite existing
|
||||
{
|
||||
if (target == NULL || target == Py_None)
|
||||
{
|
||||
// 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, now);
|
||||
timers[name] = std::make_shared<PyTimerCallable>(Py_None, 1000, runtime.getElapsedTime().asMilliseconds());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -379,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, now);
|
||||
timers[name] = std::make_shared<PyTimerCallable>(target, interval, runtime.getElapsedTime().asMilliseconds());
|
||||
}
|
||||
|
||||
void GameEngine::testTimers()
|
||||
|
|
@ -388,15 +311,9 @@ void GameEngine::testTimers()
|
|||
auto it = timers.begin();
|
||||
while (it != timers.end())
|
||||
{
|
||||
// Keep a local copy of the timer to prevent use-after-free.
|
||||
// If the callback calls delTimer(), the map entry gets replaced,
|
||||
// but we need the Timer object to survive until test() returns.
|
||||
auto timer = it->second;
|
||||
timer->test(now);
|
||||
|
||||
// Remove timers that have been cancelled or are one-shot and fired.
|
||||
// Note: Check it->second (current map value) in case callback replaced it.
|
||||
if (!it->second->getCallback() || it->second->getCallback() == Py_None)
|
||||
it->second->test(now);
|
||||
|
||||
if (it->second->isNone())
|
||||
{
|
||||
it = timers.erase(it);
|
||||
}
|
||||
|
|
@ -411,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
|
||||
|
|
@ -444,15 +353,6 @@ void GameEngine::processEvent(const sf::Event& event)
|
|||
actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta );
|
||||
}
|
||||
}
|
||||
// #140 - Handle mouse movement for hover detection
|
||||
else if (event.type == sf::Event::MouseMoved)
|
||||
{
|
||||
// Cast to PyScene to call do_mouse_hover
|
||||
if (auto* pyscene = dynamic_cast<PyScene*>(currentScene())) {
|
||||
pyscene->do_mouse_hover(event.mouseMove.x, event.mouseMove.y);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
return;
|
||||
|
||||
|
|
@ -473,26 +373,6 @@ void GameEngine::sUserInput()
|
|||
sf::Event event;
|
||||
while (window && window->pollEvent(event))
|
||||
{
|
||||
// Process event through ImGui first
|
||||
if (imguiInitialized) {
|
||||
ImGui::SFML::ProcessEvent(*window, event);
|
||||
}
|
||||
|
||||
// Handle grave/tilde key for console toggle (before other processing)
|
||||
if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Grave) {
|
||||
console.toggle();
|
||||
continue; // Don't pass grave key to game
|
||||
}
|
||||
|
||||
// If console wants keyboard, don't pass keyboard events to game
|
||||
if (console.wantsKeyboardInput()) {
|
||||
// Still process non-keyboard events (mouse, window close, etc.)
|
||||
if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased ||
|
||||
event.type == sf::Event::TextEntered) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
processEvent(event);
|
||||
}
|
||||
}
|
||||
|
|
@ -630,92 +510,7 @@ void GameEngine::updateViewport() {
|
|||
|
||||
sf::Vector2f GameEngine::windowToGameCoords(const sf::Vector2f& windowPos) const {
|
||||
if (!render_target) return windowPos;
|
||||
|
||||
|
||||
// Convert window coordinates to game coordinates using the view
|
||||
return render_target->mapPixelToCoords(sf::Vector2i(windowPos), gameView);
|
||||
}
|
||||
|
||||
// #153 - Headless simulation control: step() advances simulation time
|
||||
float GameEngine::step(float dt) {
|
||||
// In windowed mode, step() is a no-op
|
||||
if (!headless) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float actual_dt;
|
||||
|
||||
if (dt < 0) {
|
||||
// dt < 0 means "advance to next event"
|
||||
// Find the minimum time until next timer fires
|
||||
int min_remaining = INT_MAX;
|
||||
|
||||
for (auto& [name, timer] : timers) {
|
||||
if (timer && timer->isActive()) {
|
||||
int remaining = timer->getRemaining(simulation_time);
|
||||
if (remaining > 0 && remaining < min_remaining) {
|
||||
min_remaining = remaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also consider animations - find minimum time to completion
|
||||
// AnimationManager doesn't expose this, so we'll just step by 1ms if no timers
|
||||
if (min_remaining == INT_MAX) {
|
||||
// No pending timers - check if there are active animations
|
||||
// Step by a small amount to advance any running animations
|
||||
min_remaining = 1; // 1ms minimum step
|
||||
}
|
||||
|
||||
actual_dt = static_cast<float>(min_remaining) / 1000.0f; // Convert to seconds
|
||||
simulation_time += min_remaining;
|
||||
} else {
|
||||
// Advance by specified amount
|
||||
actual_dt = dt;
|
||||
simulation_time += static_cast<int>(dt * 1000.0f); // Convert seconds to ms
|
||||
}
|
||||
|
||||
// Update animations with the dt in seconds
|
||||
if (actual_dt > 0.0f && actual_dt < 10.0f) { // Sanity check
|
||||
AnimationManager::getInstance().update(actual_dt);
|
||||
}
|
||||
|
||||
// Test timers with the new simulation time
|
||||
auto it = timers.begin();
|
||||
while (it != timers.end()) {
|
||||
auto timer = it->second;
|
||||
|
||||
// Custom timer test using simulation time instead of runtime
|
||||
if (timer && timer->isActive() && timer->hasElapsed(simulation_time)) {
|
||||
timer->test(simulation_time);
|
||||
}
|
||||
|
||||
// Remove cancelled timers
|
||||
if (!it->second->getCallback() || it->second->getCallback() == Py_None) {
|
||||
it = timers.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
|
||||
return actual_dt;
|
||||
}
|
||||
|
||||
// #153 - Force render the current scene (for synchronous screenshots)
|
||||
void GameEngine::renderScene() {
|
||||
if (!render_target) return;
|
||||
|
||||
// Handle scene transitions
|
||||
if (transition.type != TransitionType::None) {
|
||||
transition.update(0); // Don't advance transition time, just render current state
|
||||
render_target->clear();
|
||||
transition.render(*render_target);
|
||||
} else {
|
||||
// Normal scene rendering
|
||||
currentScene()->render();
|
||||
}
|
||||
|
||||
// For RenderTexture (headless), we need to call display()
|
||||
if (headless && headless_renderer) {
|
||||
headless_renderer->display();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
164
src/GameEngine.h
164
src/GameEngine.h
|
|
@ -9,82 +9,11 @@
|
|||
#include "McRogueFaceConfig.h"
|
||||
#include "HeadlessRenderer.h"
|
||||
#include "SceneTransition.h"
|
||||
#include "Profiler.h"
|
||||
#include "ImGuiConsole.h"
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
|
||||
/**
|
||||
* @brief Performance profiling metrics structure
|
||||
*
|
||||
* Tracks frame timing, render counts, and detailed timing breakdowns.
|
||||
* Used by GameEngine, ProfilerOverlay (F3), and BenchmarkLogger.
|
||||
*/
|
||||
struct ProfilingMetrics {
|
||||
float frameTime = 0.0f; // Current frame time in milliseconds
|
||||
float avgFrameTime = 0.0f; // Average frame time over last N frames
|
||||
int fps = 0; // Frames per second
|
||||
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)
|
||||
float workTime = 0.0f; // Total work time before display/sleep (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) {
|
||||
sum += frameTimeHistory[i];
|
||||
}
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
|
|
@ -110,10 +39,6 @@ private:
|
|||
bool headless = false;
|
||||
McRogueFaceConfig config;
|
||||
bool cleaned_up = false;
|
||||
|
||||
// #153 - Headless simulation control
|
||||
int simulation_time = 0; // Simulated time in milliseconds (for headless mode)
|
||||
bool simulation_clock_paused = false; // True when simulation is paused (waiting for step())
|
||||
|
||||
// Window state tracking
|
||||
bool vsync_enabled = false;
|
||||
|
|
@ -126,33 +51,55 @@ 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
|
||||
|
||||
// ImGui console overlay
|
||||
ImGuiConsole console;
|
||||
bool imguiInitialized = false;
|
||||
|
||||
|
||||
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 (struct defined above class)
|
||||
ProfilingMetrics metrics;
|
||||
|
||||
// Profiling metrics
|
||||
struct ProfilingMetrics {
|
||||
float frameTime = 0.0f; // Current frame time in milliseconds
|
||||
float avgFrameTime = 0.0f; // Average frame time over last N frames
|
||||
int fps = 0; // Frames per second
|
||||
int drawCalls = 0; // Draw calls per frame
|
||||
int uiElements = 0; // Number of UI elements rendered
|
||||
int visibleElements = 0; // Number of visible elements
|
||||
|
||||
// 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) {
|
||||
sum += frameTimeHistory[i];
|
||||
}
|
||||
avgFrameTime = sum / HISTORY_SIZE;
|
||||
fps = avgFrameTime > 0 ? static_cast<int>(1000.0f / avgFrameTime) : 0;
|
||||
}
|
||||
|
||||
void resetPerFrame() {
|
||||
drawCalls = 0;
|
||||
uiElements = 0;
|
||||
visibleElements = 0;
|
||||
}
|
||||
} metrics;
|
||||
GameEngine();
|
||||
GameEngine(const McRogueFaceConfig& cfg);
|
||||
~GameEngine();
|
||||
Scene* currentScene();
|
||||
Scene* getScene(const std::string& name); // #118: Get scene by name
|
||||
void changeScene(std::string);
|
||||
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
|
||||
void createScene(std::string);
|
||||
|
|
@ -165,16 +112,13 @@ public:
|
|||
void run();
|
||||
void sUserInput();
|
||||
void cleanup(); // Clean up Python references before destruction
|
||||
void executeStartupScripts(); // Execute --exec scripts (called once after final engine setup)
|
||||
int getFrame() { return currentFrame; }
|
||||
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; }
|
||||
const McRogueFaceConfig& getConfig() const { return config; }
|
||||
void setAutoExitAfterExec(bool enabled) { config.auto_exit_after_exec = enabled; }
|
||||
void processEvent(const sf::Event& event);
|
||||
|
||||
// Window property accessors
|
||||
|
|
@ -193,11 +137,6 @@ public:
|
|||
std::string getViewportModeString() const;
|
||||
sf::Vector2f windowToGameCoords(const sf::Vector2f& windowPos) const;
|
||||
|
||||
// #153 - Headless simulation control
|
||||
float step(float dt = -1.0f); // Advance simulation; dt<0 means advance to next event
|
||||
int getSimulationTime() const { return simulation_time; }
|
||||
void renderScene(); // Force render current scene (for synchronous screenshot)
|
||||
|
||||
// global textures for scripts to access
|
||||
std::vector<IndexTexture> textures;
|
||||
|
||||
|
|
@ -206,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,201 +0,0 @@
|
|||
#include "GridChunk.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyTexture.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
// =============================================================================
|
||||
// GridChunk implementation
|
||||
// =============================================================================
|
||||
|
||||
GridChunk::GridChunk(int chunk_x, int chunk_y, int width, int height,
|
||||
int world_x, int world_y, UIGrid* parent)
|
||||
: chunk_x(chunk_x), chunk_y(chunk_y),
|
||||
width(width), height(height),
|
||||
world_x(world_x), world_y(world_y),
|
||||
cells(width * height),
|
||||
dirty(true),
|
||||
parent_grid(parent)
|
||||
{}
|
||||
|
||||
UIGridPoint& GridChunk::at(int local_x, int local_y) {
|
||||
return cells[local_y * width + local_x];
|
||||
}
|
||||
|
||||
const UIGridPoint& GridChunk::at(int local_x, int local_y) const {
|
||||
return cells[local_y * width + local_x];
|
||||
}
|
||||
|
||||
void GridChunk::markDirty() {
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// #150 - Removed ensureTexture/renderToTexture - base layer rendering removed
|
||||
// GridChunk now only provides data storage for GridPoints
|
||||
|
||||
sf::FloatRect GridChunk::getWorldBounds(int cell_width, int cell_height) const {
|
||||
return sf::FloatRect(
|
||||
sf::Vector2f(world_x * cell_width, world_y * cell_height),
|
||||
sf::Vector2f(width * cell_width, height * cell_height)
|
||||
);
|
||||
}
|
||||
|
||||
bool GridChunk::isVisible(float left_edge, float top_edge,
|
||||
float right_edge, float bottom_edge) const {
|
||||
// Check if chunk's cell range overlaps with viewport's cell range
|
||||
float chunk_right = world_x + width;
|
||||
float chunk_bottom = world_y + height;
|
||||
|
||||
return !(world_x >= right_edge || chunk_right <= left_edge ||
|
||||
world_y >= bottom_edge || chunk_bottom <= top_edge);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ChunkManager implementation
|
||||
// =============================================================================
|
||||
|
||||
ChunkManager::ChunkManager(int grid_x, int grid_y, UIGrid* parent)
|
||||
: grid_x(grid_x), grid_y(grid_y), parent_grid(parent)
|
||||
{
|
||||
// Calculate number of chunks needed
|
||||
chunks_x = (grid_x + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
|
||||
chunks_y = (grid_y + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
|
||||
|
||||
chunks.reserve(chunks_x * chunks_y);
|
||||
|
||||
// Create chunks
|
||||
for (int cy = 0; cy < chunks_y; ++cy) {
|
||||
for (int cx = 0; cx < chunks_x; ++cx) {
|
||||
// Calculate world position
|
||||
int world_x = cx * GridChunk::CHUNK_SIZE;
|
||||
int world_y = cy * GridChunk::CHUNK_SIZE;
|
||||
|
||||
// Calculate actual size (may be smaller at edges)
|
||||
int chunk_width = std::min(GridChunk::CHUNK_SIZE, grid_x - world_x);
|
||||
int chunk_height = std::min(GridChunk::CHUNK_SIZE, grid_y - world_y);
|
||||
|
||||
chunks.push_back(std::make_unique<GridChunk>(
|
||||
cx, cy, chunk_width, chunk_height, world_x, world_y, parent
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridChunk* ChunkManager::getChunkForCell(int x, int y) {
|
||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int chunk_x = x / GridChunk::CHUNK_SIZE;
|
||||
int chunk_y = y / GridChunk::CHUNK_SIZE;
|
||||
return getChunk(chunk_x, chunk_y);
|
||||
}
|
||||
|
||||
const GridChunk* ChunkManager::getChunkForCell(int x, int y) const {
|
||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int chunk_x = x / GridChunk::CHUNK_SIZE;
|
||||
int chunk_y = y / GridChunk::CHUNK_SIZE;
|
||||
return getChunk(chunk_x, chunk_y);
|
||||
}
|
||||
|
||||
GridChunk* ChunkManager::getChunk(int chunk_x, int chunk_y) {
|
||||
if (chunk_x < 0 || chunk_x >= chunks_x || chunk_y < 0 || chunk_y >= chunks_y) {
|
||||
return nullptr;
|
||||
}
|
||||
return chunks[chunk_y * chunks_x + chunk_x].get();
|
||||
}
|
||||
|
||||
const GridChunk* ChunkManager::getChunk(int chunk_x, int chunk_y) const {
|
||||
if (chunk_x < 0 || chunk_x >= chunks_x || chunk_y < 0 || chunk_y >= chunks_y) {
|
||||
return nullptr;
|
||||
}
|
||||
return chunks[chunk_y * chunks_x + chunk_x].get();
|
||||
}
|
||||
|
||||
UIGridPoint& ChunkManager::at(int x, int y) {
|
||||
GridChunk* chunk = getChunkForCell(x, y);
|
||||
if (!chunk) {
|
||||
// Return a static dummy point for out-of-bounds access
|
||||
// This matches the original behavior of UIGrid::at()
|
||||
static UIGridPoint dummy;
|
||||
return dummy;
|
||||
}
|
||||
|
||||
// Convert to local coordinates within chunk
|
||||
int local_x = x % GridChunk::CHUNK_SIZE;
|
||||
int local_y = y % GridChunk::CHUNK_SIZE;
|
||||
|
||||
// Mark chunk dirty when accessed for modification
|
||||
chunk->markDirty();
|
||||
|
||||
return chunk->at(local_x, local_y);
|
||||
}
|
||||
|
||||
const UIGridPoint& ChunkManager::at(int x, int y) const {
|
||||
const GridChunk* chunk = getChunkForCell(x, y);
|
||||
if (!chunk) {
|
||||
static UIGridPoint dummy;
|
||||
return dummy;
|
||||
}
|
||||
|
||||
int local_x = x % GridChunk::CHUNK_SIZE;
|
||||
int local_y = y % GridChunk::CHUNK_SIZE;
|
||||
|
||||
return chunk->at(local_x, local_y);
|
||||
}
|
||||
|
||||
void ChunkManager::markAllDirty() {
|
||||
for (auto& chunk : chunks) {
|
||||
chunk->markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GridChunk*> ChunkManager::getVisibleChunks(float left_edge, float top_edge,
|
||||
float right_edge, float bottom_edge) {
|
||||
std::vector<GridChunk*> visible;
|
||||
visible.reserve(chunks.size()); // Pre-allocate for worst case
|
||||
|
||||
for (auto& chunk : chunks) {
|
||||
if (chunk->isVisible(left_edge, top_edge, right_edge, bottom_edge)) {
|
||||
visible.push_back(chunk.get());
|
||||
}
|
||||
}
|
||||
|
||||
return visible;
|
||||
}
|
||||
|
||||
void ChunkManager::resize(int new_grid_x, int new_grid_y) {
|
||||
// For now, simple rebuild - could be optimized to preserve data
|
||||
grid_x = new_grid_x;
|
||||
grid_y = new_grid_y;
|
||||
|
||||
chunks_x = (grid_x + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
|
||||
chunks_y = (grid_y + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
|
||||
|
||||
chunks.clear();
|
||||
chunks.reserve(chunks_x * chunks_y);
|
||||
|
||||
for (int cy = 0; cy < chunks_y; ++cy) {
|
||||
for (int cx = 0; cx < chunks_x; ++cx) {
|
||||
int world_x = cx * GridChunk::CHUNK_SIZE;
|
||||
int world_y = cy * GridChunk::CHUNK_SIZE;
|
||||
int chunk_width = std::min(GridChunk::CHUNK_SIZE, grid_x - world_x);
|
||||
int chunk_height = std::min(GridChunk::CHUNK_SIZE, grid_y - world_y);
|
||||
|
||||
chunks.push_back(std::make_unique<GridChunk>(
|
||||
cx, cy, chunk_width, chunk_height, world_x, world_y, parent_grid
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int ChunkManager::dirtyChunks() const {
|
||||
int count = 0;
|
||||
for (const auto& chunk : chunks) {
|
||||
if (chunk->dirty) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
108
src/GridChunk.h
108
src/GridChunk.h
|
|
@ -1,108 +0,0 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include "UIGridPoint.h"
|
||||
|
||||
// Forward declarations
|
||||
class UIGrid;
|
||||
class PyTexture;
|
||||
|
||||
/**
|
||||
* #123 - Grid chunk for sub-grid data storage
|
||||
* #150 - Rendering removed; layers now handle all rendering
|
||||
*
|
||||
* Each chunk represents a CHUNK_SIZE x CHUNK_SIZE portion of the grid.
|
||||
* Chunks store GridPoint data for pathfinding and game logic.
|
||||
*/
|
||||
class GridChunk {
|
||||
public:
|
||||
// Compile-time configurable chunk size (power of 2 recommended)
|
||||
static constexpr int CHUNK_SIZE = 64;
|
||||
|
||||
// Position of this chunk in chunk coordinates
|
||||
int chunk_x, chunk_y;
|
||||
|
||||
// Actual dimensions (may be less than CHUNK_SIZE at grid edges)
|
||||
int width, height;
|
||||
|
||||
// World position (in cell coordinates)
|
||||
int world_x, world_y;
|
||||
|
||||
// Cell data for this chunk (pathfinding properties only)
|
||||
std::vector<UIGridPoint> cells;
|
||||
|
||||
// Dirty flag (for layer sync if needed)
|
||||
bool dirty;
|
||||
|
||||
// Parent grid reference
|
||||
UIGrid* parent_grid;
|
||||
|
||||
// Constructor
|
||||
GridChunk(int chunk_x, int chunk_y, int width, int height,
|
||||
int world_x, int world_y, UIGrid* parent);
|
||||
|
||||
// Access cell at local chunk coordinates
|
||||
UIGridPoint& at(int local_x, int local_y);
|
||||
const UIGridPoint& at(int local_x, int local_y) const;
|
||||
|
||||
// Mark chunk as dirty
|
||||
void markDirty();
|
||||
|
||||
// Get pixel bounds of this chunk in world coordinates
|
||||
sf::FloatRect getWorldBounds(int cell_width, int cell_height) const;
|
||||
|
||||
// Check if chunk overlaps with viewport
|
||||
bool isVisible(float left_edge, float top_edge,
|
||||
float right_edge, float bottom_edge) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages a 2D array of chunks for a grid
|
||||
*/
|
||||
class ChunkManager {
|
||||
public:
|
||||
// Dimensions in chunks
|
||||
int chunks_x, chunks_y;
|
||||
|
||||
// Grid dimensions in cells
|
||||
int grid_x, grid_y;
|
||||
|
||||
// All chunks (row-major order)
|
||||
std::vector<std::unique_ptr<GridChunk>> chunks;
|
||||
|
||||
// Parent grid
|
||||
UIGrid* parent_grid;
|
||||
|
||||
// Constructor - creates chunks for given grid dimensions
|
||||
ChunkManager(int grid_x, int grid_y, UIGrid* parent);
|
||||
|
||||
// Get chunk containing cell (x, y)
|
||||
GridChunk* getChunkForCell(int x, int y);
|
||||
const GridChunk* getChunkForCell(int x, int y) const;
|
||||
|
||||
// Get chunk at chunk coordinates
|
||||
GridChunk* getChunk(int chunk_x, int chunk_y);
|
||||
const GridChunk* getChunk(int chunk_x, int chunk_y) const;
|
||||
|
||||
// Access cell at grid coordinates (routes through chunk)
|
||||
UIGridPoint& at(int x, int y);
|
||||
const UIGridPoint& at(int x, int y) const;
|
||||
|
||||
// Mark all chunks dirty (for full rebuild)
|
||||
void markAllDirty();
|
||||
|
||||
// Get chunks that overlap with viewport
|
||||
std::vector<GridChunk*> getVisibleChunks(float left_edge, float top_edge,
|
||||
float right_edge, float bottom_edge);
|
||||
|
||||
// Resize grid (rebuilds chunks)
|
||||
void resize(int new_grid_x, int new_grid_y);
|
||||
|
||||
// Get total number of chunks
|
||||
int totalChunks() const { return chunks_x * chunks_y; }
|
||||
|
||||
// Get number of dirty chunks
|
||||
int dirtyChunks() const;
|
||||
};
|
||||
1401
src/GridLayers.cpp
1401
src/GridLayers.cpp
File diff suppressed because it is too large
Load Diff
314
src/GridLayers.h
314
src/GridLayers.h
|
|
@ -1,314 +0,0 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include "structmember.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <libtcod.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
// Forward declarations
|
||||
class UIGrid;
|
||||
class PyTexture;
|
||||
class UIEntity;
|
||||
|
||||
// Include PyTexture.h for PyTextureObject (typedef, not struct)
|
||||
#include "PyTexture.h"
|
||||
|
||||
// Layer type enumeration
|
||||
enum class GridLayerType {
|
||||
Color,
|
||||
Tile
|
||||
};
|
||||
|
||||
// Abstract base class for grid layers
|
||||
class GridLayer {
|
||||
public:
|
||||
// Chunk size for per-chunk dirty tracking (matches GridChunk::CHUNK_SIZE)
|
||||
static constexpr int CHUNK_SIZE = 64;
|
||||
|
||||
GridLayerType type;
|
||||
std::string name; // #150 - Layer name for GridPoint property access
|
||||
int z_index; // Negative = below entities, >= 0 = above entities
|
||||
int grid_x, grid_y; // Dimensions
|
||||
UIGrid* parent_grid; // Parent grid reference
|
||||
bool visible; // Visibility flag
|
||||
|
||||
// Chunk dimensions
|
||||
int chunks_x, chunks_y;
|
||||
|
||||
// Per-chunk dirty flags and RenderTextures
|
||||
std::vector<bool> chunk_dirty; // One flag per chunk
|
||||
std::vector<std::unique_ptr<sf::RenderTexture>> chunk_textures; // One texture per chunk
|
||||
std::vector<bool> chunk_texture_initialized; // Track which textures are created
|
||||
int cached_cell_width, cached_cell_height; // Cell size used for cached textures
|
||||
|
||||
GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent);
|
||||
virtual ~GridLayer() = default;
|
||||
|
||||
// Mark entire layer as needing re-render
|
||||
void markDirty();
|
||||
|
||||
// Mark specific cell's chunk as dirty
|
||||
void markDirty(int cell_x, int cell_y);
|
||||
|
||||
// Get chunk index for a cell
|
||||
int getChunkIndex(int cell_x, int cell_y) const;
|
||||
|
||||
// Get chunk coordinates for a cell
|
||||
void getChunkCoords(int cell_x, int cell_y, int& chunk_x, int& chunk_y) const;
|
||||
|
||||
// Get cell bounds for a chunk
|
||||
void getChunkBounds(int chunk_x, int chunk_y, int& start_x, int& start_y, int& end_x, int& end_y) const;
|
||||
|
||||
// Ensure a specific chunk's texture is properly sized
|
||||
void ensureChunkTexture(int chunk_idx, int cell_width, int cell_height);
|
||||
|
||||
// Initialize chunk tracking arrays
|
||||
void initChunks();
|
||||
|
||||
// Render a specific chunk to its cached texture (called when chunk is dirty)
|
||||
virtual void renderChunkToTexture(int chunk_x, int chunk_y, int cell_width, int cell_height) = 0;
|
||||
|
||||
// Render the layer content to the cached texture (legacy - marks all dirty)
|
||||
virtual void renderToTexture(int cell_width, int cell_height) = 0;
|
||||
|
||||
// Render the layer to a RenderTarget with the given transformation parameters
|
||||
// Uses cached chunk textures, only re-renders visible dirty chunks
|
||||
virtual void render(sf::RenderTarget& target,
|
||||
float left_spritepixels, float top_spritepixels,
|
||||
int left_edge, int top_edge, int x_limit, int y_limit,
|
||||
float zoom, int cell_width, int cell_height) = 0;
|
||||
|
||||
// Resize the layer (reallocates storage and reinitializes chunks)
|
||||
virtual void resize(int new_grid_x, int new_grid_y) = 0;
|
||||
};
|
||||
|
||||
// Color layer - stores RGBA color per cell
|
||||
class ColorLayer : public GridLayer {
|
||||
public:
|
||||
std::vector<sf::Color> colors;
|
||||
|
||||
// Perspective binding (#113) - binds layer to entity for automatic FOV updates
|
||||
std::weak_ptr<UIEntity> perspective_entity;
|
||||
sf::Color perspective_visible;
|
||||
sf::Color perspective_discovered;
|
||||
sf::Color perspective_unknown;
|
||||
bool has_perspective;
|
||||
|
||||
ColorLayer(int z_index, int grid_x, int grid_y, UIGrid* parent);
|
||||
|
||||
// Access color at position
|
||||
sf::Color& at(int x, int y);
|
||||
const sf::Color& at(int x, int y) const;
|
||||
|
||||
// Fill entire layer with a color
|
||||
void fill(const sf::Color& color);
|
||||
|
||||
// Fill a rectangular region with a color (#113)
|
||||
void fillRect(int x, int y, int width, int height, const sf::Color& color);
|
||||
|
||||
// Draw FOV-based visibility (#113)
|
||||
// Paints cells based on current FOV state from parent grid
|
||||
void drawFOV(int source_x, int source_y, int radius,
|
||||
TCOD_fov_algorithm_t algorithm,
|
||||
const sf::Color& visible,
|
||||
const sf::Color& discovered,
|
||||
const sf::Color& unknown);
|
||||
|
||||
// Perspective binding (#113) - bind layer to entity for automatic updates
|
||||
void applyPerspective(std::shared_ptr<UIEntity> entity,
|
||||
const sf::Color& visible,
|
||||
const sf::Color& discovered,
|
||||
const sf::Color& unknown);
|
||||
|
||||
// Update perspective - redraws based on bound entity's current position
|
||||
void updatePerspective();
|
||||
|
||||
// Clear perspective binding
|
||||
void clearPerspective();
|
||||
|
||||
// Render a specific chunk to its texture (called when chunk is dirty AND visible)
|
||||
void renderChunkToTexture(int chunk_x, int chunk_y, int cell_width, int cell_height) override;
|
||||
|
||||
// #148 - Render all content to cached texture (legacy - calls renderChunkToTexture for all)
|
||||
void renderToTexture(int cell_width, int cell_height) override;
|
||||
|
||||
void render(sf::RenderTarget& target,
|
||||
float left_spritepixels, float top_spritepixels,
|
||||
int left_edge, int top_edge, int x_limit, int y_limit,
|
||||
float zoom, int cell_width, int cell_height) override;
|
||||
|
||||
void resize(int new_grid_x, int new_grid_y) override;
|
||||
};
|
||||
|
||||
// Tile layer - stores sprite index per cell with texture reference
|
||||
class TileLayer : public GridLayer {
|
||||
public:
|
||||
std::vector<int> tiles; // Sprite indices (-1 = no tile)
|
||||
std::shared_ptr<PyTexture> texture;
|
||||
|
||||
TileLayer(int z_index, int grid_x, int grid_y, UIGrid* parent,
|
||||
std::shared_ptr<PyTexture> texture = nullptr);
|
||||
|
||||
// Access tile index at position
|
||||
int& at(int x, int y);
|
||||
int at(int x, int y) const;
|
||||
|
||||
// Fill entire layer with a tile index
|
||||
void fill(int tile_index);
|
||||
|
||||
// Fill a rectangular region with a tile index (#113)
|
||||
void fillRect(int x, int y, int width, int height, int tile_index);
|
||||
|
||||
// Render a specific chunk to its texture (called when chunk is dirty AND visible)
|
||||
void renderChunkToTexture(int chunk_x, int chunk_y, int cell_width, int cell_height) override;
|
||||
|
||||
// #148 - Render all content to cached texture (legacy - calls renderChunkToTexture for all)
|
||||
void renderToTexture(int cell_width, int cell_height) override;
|
||||
|
||||
void render(sf::RenderTarget& target,
|
||||
float left_spritepixels, float top_spritepixels,
|
||||
int left_edge, int top_edge, int x_limit, int y_limit,
|
||||
float zoom, int cell_width, int cell_height) override;
|
||||
|
||||
void resize(int new_grid_x, int new_grid_y) override;
|
||||
};
|
||||
|
||||
// Python wrapper types
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<GridLayer> data;
|
||||
std::shared_ptr<UIGrid> grid; // Parent grid reference
|
||||
} PyGridLayerObject;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<ColorLayer> data;
|
||||
std::shared_ptr<UIGrid> grid;
|
||||
} PyColorLayerObject;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<TileLayer> data;
|
||||
std::shared_ptr<UIGrid> grid;
|
||||
} PyTileLayerObject;
|
||||
|
||||
// Python API classes
|
||||
class PyGridLayerAPI {
|
||||
public:
|
||||
// ColorLayer methods
|
||||
static int ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* ColorLayer_at(PyColorLayerObject* self, PyObject* args);
|
||||
static PyObject* ColorLayer_set(PyColorLayerObject* self, PyObject* args);
|
||||
static PyObject* ColorLayer_fill(PyColorLayerObject* self, PyObject* args);
|
||||
static PyObject* ColorLayer_fill_rect(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* ColorLayer_draw_fov(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* ColorLayer_apply_perspective(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* ColorLayer_update_perspective(PyColorLayerObject* self, PyObject* args);
|
||||
static PyObject* ColorLayer_clear_perspective(PyColorLayerObject* self, PyObject* args);
|
||||
static PyObject* ColorLayer_get_z_index(PyColorLayerObject* self, void* closure);
|
||||
static int ColorLayer_set_z_index(PyColorLayerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* ColorLayer_get_visible(PyColorLayerObject* self, void* closure);
|
||||
static int ColorLayer_set_visible(PyColorLayerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* ColorLayer_get_grid_size(PyColorLayerObject* self, void* closure);
|
||||
static PyObject* ColorLayer_repr(PyColorLayerObject* self);
|
||||
|
||||
// TileLayer methods
|
||||
static int TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args);
|
||||
static PyObject* TileLayer_set(PyTileLayerObject* self, PyObject* args);
|
||||
static PyObject* TileLayer_fill(PyTileLayerObject* self, PyObject* args);
|
||||
static PyObject* TileLayer_fill_rect(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* TileLayer_get_z_index(PyTileLayerObject* self, void* closure);
|
||||
static int TileLayer_set_z_index(PyTileLayerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* TileLayer_get_visible(PyTileLayerObject* self, void* closure);
|
||||
static int TileLayer_set_visible(PyTileLayerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* TileLayer_get_texture(PyTileLayerObject* self, void* closure);
|
||||
static int TileLayer_set_texture(PyTileLayerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* TileLayer_get_grid_size(PyTileLayerObject* self, void* closure);
|
||||
static PyObject* TileLayer_repr(PyTileLayerObject* self);
|
||||
|
||||
// Method and getset arrays
|
||||
static PyMethodDef ColorLayer_methods[];
|
||||
static PyGetSetDef ColorLayer_getsetters[];
|
||||
static PyMethodDef TileLayer_methods[];
|
||||
static PyGetSetDef TileLayer_getsetters[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
// ColorLayer type
|
||||
static PyTypeObject PyColorLayerType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.ColorLayer",
|
||||
.tp_basicsize = sizeof(PyColorLayerObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)[](PyObject* self) {
|
||||
PyColorLayerObject* obj = (PyColorLayerObject*)self;
|
||||
obj->data.reset();
|
||||
obj->grid.reset();
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
.tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("ColorLayer(z_index=-1, grid_size=None)\n\n"
|
||||
"A grid layer that stores RGBA colors per cell.\n\n"
|
||||
"Args:\n"
|
||||
" z_index (int): Render order. Negative = below entities. Default: -1\n"
|
||||
" grid_size (tuple): Dimensions as (width, height). Default: parent grid size\n\n"
|
||||
"Attributes:\n"
|
||||
" z_index (int): Layer z-order relative to entities\n"
|
||||
" visible (bool): Whether layer is rendered\n"
|
||||
" grid_size (tuple): Layer dimensions (read-only)\n\n"
|
||||
"Methods:\n"
|
||||
" at(x, y): Get color at cell position\n"
|
||||
" set(x, y, color): Set color at cell position\n"
|
||||
" fill(color): Fill entire layer with color"),
|
||||
.tp_methods = PyGridLayerAPI::ColorLayer_methods,
|
||||
.tp_getset = PyGridLayerAPI::ColorLayer_getsetters,
|
||||
.tp_init = (initproc)PyGridLayerAPI::ColorLayer_init,
|
||||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
|
||||
PyColorLayerObject* self = (PyColorLayerObject*)type->tp_alloc(type, 0);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
|
||||
// TileLayer type
|
||||
static PyTypeObject PyTileLayerType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.TileLayer",
|
||||
.tp_basicsize = sizeof(PyTileLayerObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)[](PyObject* self) {
|
||||
PyTileLayerObject* obj = (PyTileLayerObject*)self;
|
||||
obj->data.reset();
|
||||
obj->grid.reset();
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
.tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("TileLayer(z_index=-1, texture=None, grid_size=None)\n\n"
|
||||
"A grid layer that stores sprite indices per cell.\n\n"
|
||||
"Args:\n"
|
||||
" z_index (int): Render order. Negative = below entities. Default: -1\n"
|
||||
" texture (Texture): Sprite atlas for tile rendering. Default: None\n"
|
||||
" grid_size (tuple): Dimensions as (width, height). Default: parent grid size\n\n"
|
||||
"Attributes:\n"
|
||||
" z_index (int): Layer z-order relative to entities\n"
|
||||
" visible (bool): Whether layer is rendered\n"
|
||||
" texture (Texture): Tile sprite atlas\n"
|
||||
" grid_size (tuple): Layer dimensions (read-only)\n\n"
|
||||
"Methods:\n"
|
||||
" at(x, y): Get tile index at cell position\n"
|
||||
" set(x, y, index): Set tile index at cell position\n"
|
||||
" fill(index): Fill entire layer with tile index"),
|
||||
.tp_methods = PyGridLayerAPI::TileLayer_methods,
|
||||
.tp_getset = PyGridLayerAPI::TileLayer_getsetters,
|
||||
.tp_init = (initproc)PyGridLayerAPI::TileLayer_init,
|
||||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
|
||||
PyTileLayerObject* self = (PyTileLayerObject*)type->tp_alloc(type, 0);
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
#include "ImGuiConsole.h"
|
||||
#include "imgui.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include <Python.h>
|
||||
#include <sstream>
|
||||
|
||||
// Static member initialization
|
||||
bool ImGuiConsole::enabled = true;
|
||||
|
||||
ImGuiConsole::ImGuiConsole() {
|
||||
addOutput("McRogueFace Python Console", false);
|
||||
addOutput("Type Python commands and press Enter to execute.", false);
|
||||
addOutput("", false);
|
||||
}
|
||||
|
||||
void ImGuiConsole::toggle() {
|
||||
if (enabled) {
|
||||
visible = !visible;
|
||||
if (visible) {
|
||||
// Focus input when opening
|
||||
ImGui::SetWindowFocus("Console");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ImGuiConsole::wantsKeyboardInput() const {
|
||||
return visible && enabled;
|
||||
}
|
||||
|
||||
void ImGuiConsole::addOutput(const std::string& text, bool isError) {
|
||||
// Split text by newlines and add each line separately
|
||||
std::istringstream stream(text);
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
outputHistory.push_back({line, isError, false});
|
||||
}
|
||||
|
||||
// Trim history if too long
|
||||
while (outputHistory.size() > MAX_HISTORY) {
|
||||
outputHistory.pop_front();
|
||||
}
|
||||
|
||||
scrollToBottom = true;
|
||||
}
|
||||
|
||||
void ImGuiConsole::executeCommand(const std::string& command) {
|
||||
if (command.empty()) return;
|
||||
|
||||
// Add command to output with >>> prefix
|
||||
outputHistory.push_back({">>> " + command, false, true});
|
||||
|
||||
// Add to command history
|
||||
commandHistory.push_back(command);
|
||||
historyIndex = -1;
|
||||
|
||||
// Capture Python output
|
||||
// Redirect stdout/stderr to capture output
|
||||
std::string captureCode = R"(
|
||||
import sys
|
||||
import io
|
||||
_console_stdout = io.StringIO()
|
||||
_console_stderr = io.StringIO()
|
||||
_old_stdout = sys.stdout
|
||||
_old_stderr = sys.stderr
|
||||
sys.stdout = _console_stdout
|
||||
sys.stderr = _console_stderr
|
||||
)";
|
||||
|
||||
std::string restoreCode = R"(
|
||||
sys.stdout = _old_stdout
|
||||
sys.stderr = _old_stderr
|
||||
_stdout_val = _console_stdout.getvalue()
|
||||
_stderr_val = _console_stderr.getvalue()
|
||||
)";
|
||||
|
||||
// Set up capture
|
||||
PyRun_SimpleString(captureCode.c_str());
|
||||
|
||||
// Try to evaluate as expression first (for things like "2+2")
|
||||
PyObject* main_module = PyImport_AddModule("__main__");
|
||||
PyObject* main_dict = PyModule_GetDict(main_module);
|
||||
|
||||
// First try eval (for expressions that return values)
|
||||
PyObject* result = PyRun_String(command.c_str(), Py_eval_input, main_dict, main_dict);
|
||||
bool showedResult = false;
|
||||
|
||||
if (result == nullptr) {
|
||||
// Clear the error from eval attempt
|
||||
PyErr_Clear();
|
||||
|
||||
// Try exec (for statements)
|
||||
result = PyRun_String(command.c_str(), Py_file_input, main_dict, main_dict);
|
||||
|
||||
if (result == nullptr) {
|
||||
// Real error - capture it
|
||||
PyErr_Print(); // This prints to stderr which we're capturing
|
||||
}
|
||||
} else if (result != Py_None) {
|
||||
// Expression returned a non-None value - show its repr
|
||||
PyObject* repr = PyObject_Repr(result);
|
||||
if (repr) {
|
||||
const char* repr_str = PyUnicode_AsUTF8(repr);
|
||||
if (repr_str) {
|
||||
addOutput(repr_str, false);
|
||||
showedResult = true;
|
||||
}
|
||||
Py_DECREF(repr);
|
||||
}
|
||||
}
|
||||
Py_XDECREF(result);
|
||||
|
||||
// Restore stdout/stderr
|
||||
PyRun_SimpleString(restoreCode.c_str());
|
||||
|
||||
// Get captured stdout (only if we didn't already show a result)
|
||||
PyObject* stdout_val = PyObject_GetAttrString(main_module, "_stdout_val");
|
||||
if (stdout_val && PyUnicode_Check(stdout_val)) {
|
||||
const char* stdout_str = PyUnicode_AsUTF8(stdout_val);
|
||||
if (stdout_str && strlen(stdout_str) > 0) {
|
||||
addOutput(stdout_str, false);
|
||||
}
|
||||
}
|
||||
Py_XDECREF(stdout_val);
|
||||
|
||||
// Get captured stderr
|
||||
PyObject* stderr_val = PyObject_GetAttrString(main_module, "_stderr_val");
|
||||
if (stderr_val && PyUnicode_Check(stderr_val)) {
|
||||
const char* stderr_str = PyUnicode_AsUTF8(stderr_val);
|
||||
if (stderr_str && strlen(stderr_str) > 0) {
|
||||
addOutput(stderr_str, true);
|
||||
}
|
||||
}
|
||||
Py_XDECREF(stderr_val);
|
||||
|
||||
// Clean up temporary variables
|
||||
PyRun_SimpleString("del _console_stdout, _console_stderr, _old_stdout, _old_stderr, _stdout_val, _stderr_val");
|
||||
|
||||
scrollToBottom = true;
|
||||
}
|
||||
|
||||
void ImGuiConsole::render() {
|
||||
if (!visible || !enabled) return;
|
||||
|
||||
// Set up console window
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y * 0.4f), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_FirstUseEver);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||
|
||||
if (!ImGui::Begin("Console", &visible, flags)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Output area (scrollable, no horizontal scrollbar - use word wrap)
|
||||
float footerHeight = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing();
|
||||
ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footerHeight), false, ImGuiWindowFlags_None);
|
||||
|
||||
// Render output lines with word wrap
|
||||
for (const auto& line : outputHistory) {
|
||||
if (line.isInput) {
|
||||
// User input - yellow/gold color
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.9f, 0.4f, 1.0f));
|
||||
} else if (line.isError) {
|
||||
// Error - red color
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
|
||||
} else {
|
||||
// Normal output - default color
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.8f, 0.8f, 0.8f, 1.0f));
|
||||
}
|
||||
|
||||
ImGui::TextWrapped("%s", line.text.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom when new content is added
|
||||
if (scrollToBottom || ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
}
|
||||
scrollToBottom = false;
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
// Input line
|
||||
ImGui::Separator();
|
||||
|
||||
// Input field
|
||||
ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_EnterReturnsTrue |
|
||||
ImGuiInputTextFlags_CallbackHistory |
|
||||
ImGuiInputTextFlags_CallbackCompletion;
|
||||
|
||||
bool reclaimFocus = false;
|
||||
|
||||
// Custom callback for history navigation
|
||||
auto callback = [](ImGuiInputTextCallbackData* data) -> int {
|
||||
ImGuiConsole* console = static_cast<ImGuiConsole*>(data->UserData);
|
||||
|
||||
if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) {
|
||||
if (console->commandHistory.empty()) return 0;
|
||||
|
||||
if (data->EventKey == ImGuiKey_UpArrow) {
|
||||
if (console->historyIndex < 0) {
|
||||
console->historyIndex = static_cast<int>(console->commandHistory.size()) - 1;
|
||||
} else if (console->historyIndex > 0) {
|
||||
console->historyIndex--;
|
||||
}
|
||||
} else if (data->EventKey == ImGuiKey_DownArrow) {
|
||||
if (console->historyIndex >= 0) {
|
||||
console->historyIndex++;
|
||||
if (console->historyIndex >= static_cast<int>(console->commandHistory.size())) {
|
||||
console->historyIndex = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update input buffer
|
||||
if (console->historyIndex >= 0 && console->historyIndex < static_cast<int>(console->commandHistory.size())) {
|
||||
const std::string& historyEntry = console->commandHistory[console->historyIndex];
|
||||
data->DeleteChars(0, data->BufTextLen);
|
||||
data->InsertChars(0, historyEntry.c_str());
|
||||
} else {
|
||||
data->DeleteChars(0, data->BufTextLen);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
ImGui::PushItemWidth(-1); // Full width
|
||||
if (ImGui::InputText("##Input", inputBuffer, sizeof(inputBuffer), inputFlags, callback, this)) {
|
||||
std::string command(inputBuffer);
|
||||
inputBuffer[0] = '\0';
|
||||
executeCommand(command);
|
||||
reclaimFocus = true;
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
// Keep focus on input
|
||||
ImGui::SetItemDefaultFocus();
|
||||
if (reclaimFocus || (visible && !ImGui::IsAnyItemActive())) {
|
||||
ImGui::SetKeyboardFocusHere(-1);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
|
||||
/**
|
||||
* @brief ImGui-based debug console for Python REPL
|
||||
*
|
||||
* Provides an overlay console that can execute Python code
|
||||
* without blocking the main game loop. Activated by grave/tilde key.
|
||||
*/
|
||||
class ImGuiConsole {
|
||||
public:
|
||||
ImGuiConsole();
|
||||
|
||||
// Core functionality
|
||||
void render(); // Render the console UI
|
||||
void toggle(); // Toggle visibility
|
||||
bool isVisible() const { return visible; }
|
||||
void setVisible(bool v) { visible = v; }
|
||||
|
||||
// Configuration (for Python API)
|
||||
static bool isEnabled() { return enabled; }
|
||||
static void setEnabled(bool e) { enabled = e; }
|
||||
|
||||
// Input handling
|
||||
bool wantsKeyboardInput() const; // Returns true if ImGui wants keyboard
|
||||
|
||||
private:
|
||||
void executeCommand(const std::string& command);
|
||||
void addOutput(const std::string& text, bool isError = false);
|
||||
|
||||
// State
|
||||
bool visible = false;
|
||||
static bool enabled; // Global enable/disable (for shipping games)
|
||||
|
||||
// Input buffer
|
||||
char inputBuffer[1024] = {0};
|
||||
|
||||
// Output history
|
||||
struct OutputLine {
|
||||
std::string text;
|
||||
bool isError;
|
||||
bool isInput; // True if this was user input (for styling)
|
||||
};
|
||||
std::deque<OutputLine> outputHistory;
|
||||
static constexpr size_t MAX_HISTORY = 500;
|
||||
|
||||
// Command history for up/down navigation
|
||||
std::vector<std::string> commandHistory;
|
||||
int historyIndex = -1;
|
||||
|
||||
// Scroll state
|
||||
bool scrollToBottom = true;
|
||||
};
|
||||
|
|
@ -1,22 +1,14 @@
|
|||
#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"
|
||||
#include "PyTimer.h"
|
||||
#include "PyWindow.h"
|
||||
#include "PySceneObject.h"
|
||||
#include "PyFOV.h"
|
||||
#include "GameEngine.h"
|
||||
#include "ImGuiConsole.h"
|
||||
#include "BenchmarkLogger.h"
|
||||
#include "UI.h"
|
||||
#include "UILine.h"
|
||||
#include "UICircle.h"
|
||||
#include "UIArc.h"
|
||||
#include "GridLayers.h"
|
||||
#include "Resources.h"
|
||||
#include "PyScene.h"
|
||||
#include <filesystem>
|
||||
|
|
@ -31,256 +23,192 @@ std::shared_ptr<PyFont> McRFPy_API::default_font;
|
|||
std::shared_ptr<PyTexture> McRFPy_API::default_texture;
|
||||
PyObject* McRFPy_API::mcrf_module;
|
||||
|
||||
// Exception handling state
|
||||
std::atomic<bool> McRFPy_API::exception_occurred{false};
|
||||
std::atomic<int> McRFPy_API::exit_code{0};
|
||||
|
||||
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.")
|
||||
)},
|
||||
{"step", McRFPy_API::_step, METH_VARARGS,
|
||||
MCRF_FUNCTION(step,
|
||||
MCRF_SIG("(dt: float = None)", "float"),
|
||||
MCRF_DESC("Advance simulation time (headless mode only)."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("dt", "Time to advance in seconds. If None, advances to the next scheduled event (timer/animation).")
|
||||
MCRF_RETURNS("float: Actual time advanced in seconds. Returns 0.0 in windowed mode.")
|
||||
MCRF_NOTE("In windowed mode, this is a no-op and returns 0.0. Use this for deterministic simulation control in headless/testing scenarios.")
|
||||
)},
|
||||
"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)")
|
||||
)},
|
||||
|
||||
{"setDevConsole", McRFPy_API::_setDevConsole, METH_VARARGS,
|
||||
MCRF_FUNCTION(setDevConsole,
|
||||
MCRF_SIG("(enabled: bool)", "None"),
|
||||
MCRF_DESC("Enable or disable the developer console overlay."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("enabled", "True to enable the console (default), False to disable")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("When disabled, the grave/tilde key will not open the console. Use this to ship games without debug features.")
|
||||
)},
|
||||
|
||||
{"start_benchmark", McRFPy_API::_startBenchmark, METH_NOARGS,
|
||||
MCRF_FUNCTION(start_benchmark,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Start capturing benchmark data to a file."),
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_RAISES("RuntimeError", "If a benchmark is already running")
|
||||
MCRF_NOTE("Benchmark filename is auto-generated from PID and timestamp. Use end_benchmark() to stop and get filename.")
|
||||
)},
|
||||
|
||||
{"end_benchmark", McRFPy_API::_endBenchmark, METH_NOARGS,
|
||||
MCRF_FUNCTION(end_benchmark,
|
||||
MCRF_SIG("()", "str"),
|
||||
MCRF_DESC("Stop benchmark capture and write data to JSON file."),
|
||||
MCRF_RETURNS("str: The filename of the written benchmark data")
|
||||
MCRF_RAISES("RuntimeError", "If no benchmark is currently running")
|
||||
MCRF_NOTE("Returns the auto-generated filename (e.g., 'benchmark_12345_20250528_143022.json')")
|
||||
)},
|
||||
|
||||
{"log_benchmark", McRFPy_API::_logBenchmark, METH_VARARGS,
|
||||
MCRF_FUNCTION(log_benchmark,
|
||||
MCRF_SIG("(message: str)", "None"),
|
||||
MCRF_DESC("Add a log message to the current benchmark frame."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("message", "Text to associate with the current frame")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_RAISES("RuntimeError", "If no benchmark is currently running")
|
||||
MCRF_NOTE("Messages appear in the 'logs' array of each frame in the output JSON.")
|
||||
)},
|
||||
|
||||
"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. */
|
||||
|
|
@ -309,30 +237,26 @@ PyObject* PyInit_mcrfpy()
|
|||
|
||||
/*UI widgets*/
|
||||
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
|
||||
&PyUILineType, &PyUICircleType, &PyUIArcType,
|
||||
|
||||
/*game map & perspective data*/
|
||||
&PyUIGridPointType, &PyUIGridPointStateType,
|
||||
|
||||
/*grid layers (#147)*/
|
||||
&PyColorLayerType, &PyTileLayerType,
|
||||
|
||||
/*collections & iterators*/
|
||||
&PyUICollectionType, &PyUICollectionIterType,
|
||||
&PyUIEntityCollectionType, &PyUIEntityCollectionIterType,
|
||||
|
||||
|
||||
/*animation*/
|
||||
&PyAnimationType,
|
||||
|
||||
|
||||
/*timer*/
|
||||
&PyTimerType,
|
||||
|
||||
|
||||
/*window singleton*/
|
||||
&PyWindowType,
|
||||
|
||||
|
||||
/*scene class*/
|
||||
&PySceneType,
|
||||
|
||||
|
||||
nullptr};
|
||||
|
||||
// Set up PyWindowType methods and getsetters before PyType_Ready
|
||||
|
|
@ -343,17 +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);
|
||||
PyUILineType.tp_weaklistoffset = offsetof(PyUILineObject, weakreflist);
|
||||
PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist);
|
||||
PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist);
|
||||
|
||||
int i = 0;
|
||||
auto t = pytypes[i];
|
||||
while (t != nullptr)
|
||||
|
|
@ -376,28 +289,20 @@ PyObject* PyInit_mcrfpy()
|
|||
PyModule_AddObject(m, "default_font", Py_None);
|
||||
PyModule_AddObject(m, "default_texture", Py_None);
|
||||
|
||||
// Add FOV enum class (uses Python's IntEnum) (#114)
|
||||
PyObject* fov_class = PyFOV::create_enum_class(m);
|
||||
if (!fov_class) {
|
||||
// If enum creation fails, continue without it (non-fatal)
|
||||
PyErr_Clear();
|
||||
}
|
||||
|
||||
// Add default_fov module property - defaults to FOV.BASIC
|
||||
// New grids copy this value at creation time
|
||||
if (fov_class) {
|
||||
PyObject* default_fov = PyObject_GetAttrString(fov_class, "BASIC");
|
||||
if (default_fov) {
|
||||
PyModule_AddObject(m, "default_fov", default_fov);
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
// Fallback to integer
|
||||
PyModule_AddIntConstant(m, "default_fov", FOV_BASIC);
|
||||
}
|
||||
} else {
|
||||
// Fallback to integer if enum failed
|
||||
PyModule_AddIntConstant(m, "default_fov", FOV_BASIC);
|
||||
}
|
||||
// Add TCOD FOV algorithm constants
|
||||
PyModule_AddIntConstant(m, "FOV_BASIC", FOV_BASIC);
|
||||
PyModule_AddIntConstant(m, "FOV_DIAMOND", FOV_DIAMOND);
|
||||
PyModule_AddIntConstant(m, "FOV_SHADOW", FOV_SHADOW);
|
||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_0", FOV_PERMISSIVE_0);
|
||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_1", FOV_PERMISSIVE_1);
|
||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_2", FOV_PERMISSIVE_2);
|
||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_3", FOV_PERMISSIVE_3);
|
||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_4", FOV_PERMISSIVE_4);
|
||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_5", FOV_PERMISSIVE_5);
|
||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_6", FOV_PERMISSIVE_6);
|
||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_7", FOV_PERMISSIVE_7);
|
||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_8", FOV_PERMISSIVE_8);
|
||||
PyModule_AddIntConstant(m, "FOV_RESTRICTIVE", FOV_RESTRICTIVE);
|
||||
|
||||
// Add automation submodule
|
||||
PyObject* automation_module = McRFPy_Automation::init_automation_module();
|
||||
|
|
@ -447,49 +352,25 @@ PyStatus init_python(const char *program_name)
|
|||
PyConfig_SetString(&config, &config.stdio_errors, L"surrogateescape");
|
||||
config.configure_c_stdio = 1;
|
||||
|
||||
// Set sys.executable to the McRogueFace binary path
|
||||
auto exe_filename = executable_filename();
|
||||
PyConfig_SetString(&config, &config.executable, exe_filename.c_str());
|
||||
|
||||
PyConfig_SetBytesString(&config, &config.home,
|
||||
PyConfig_SetBytesString(&config, &config.home,
|
||||
narrow_string(executable_path() + L"/lib/Python").c_str());
|
||||
|
||||
status = PyConfig_SetBytesString(&config, &config.program_name,
|
||||
program_name);
|
||||
|
||||
// Check for sibling venv/ directory (self-contained deployment)
|
||||
auto exe_dir = std::filesystem::path(executable_path());
|
||||
auto sibling_venv = exe_dir / "venv";
|
||||
if (std::filesystem::exists(sibling_venv)) {
|
||||
// Platform-specific site-packages path
|
||||
#ifdef _WIN32
|
||||
auto site_packages = sibling_venv / "Lib" / "site-packages";
|
||||
#else
|
||||
auto site_packages = sibling_venv / "lib" / "python3.14" / "site-packages";
|
||||
#endif
|
||||
if (std::filesystem::exists(site_packages)) {
|
||||
// Prepend so venv packages take priority over bundled
|
||||
PyWideStringList_Insert(&config.module_search_paths, 0,
|
||||
site_packages.wstring().c_str());
|
||||
config.module_search_paths_set = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// under Windows, the search paths are correct; under Linux, they need manual insertion
|
||||
#if __PLATFORM_SET_PYTHON_SEARCH_PATHS == 1
|
||||
if (!config.module_search_paths_set) {
|
||||
config.module_search_paths_set = 1;
|
||||
}
|
||||
|
||||
// search paths for python libs/modules/scripts
|
||||
config.module_search_paths_set = 1;
|
||||
|
||||
// search paths for python libs/modules/scripts
|
||||
const wchar_t* str_arr[] = {
|
||||
L"/scripts",
|
||||
L"/lib/Python/lib.linux-x86_64-3.14",
|
||||
L"/lib/Python",
|
||||
L"/lib/Python/Lib"
|
||||
// Note: venv site-packages handled above via sibling_venv detection
|
||||
L"/lib/Python/lib.linux-x86_64-3.12",
|
||||
L"/lib/Python",
|
||||
L"/lib/Python/Lib",
|
||||
L"/venv/lib/python3.12/site-packages"
|
||||
};
|
||||
|
||||
|
||||
|
||||
for(auto s : str_arr) {
|
||||
status = PyWideStringList_Append(&config.module_search_paths, (executable_path() + s).c_str());
|
||||
|
|
@ -506,128 +387,61 @@ PyStatus init_python(const char *program_name)
|
|||
return status;
|
||||
}
|
||||
|
||||
PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config)
|
||||
PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv)
|
||||
{
|
||||
// If Python is already initialized, just return success
|
||||
if (Py_IsInitialized()) {
|
||||
return PyStatus_Ok();
|
||||
}
|
||||
|
||||
|
||||
PyStatus status;
|
||||
PyConfig pyconfig;
|
||||
PyConfig_InitIsolatedConfig(&pyconfig);
|
||||
|
||||
|
||||
// Configure UTF-8 for stdio
|
||||
PyConfig_SetString(&pyconfig, &pyconfig.stdio_encoding, L"UTF-8");
|
||||
PyConfig_SetString(&pyconfig, &pyconfig.stdio_errors, L"surrogateescape");
|
||||
pyconfig.configure_c_stdio = 1;
|
||||
|
||||
// Set sys.executable to the McRogueFace binary path
|
||||
auto exe_path = executable_filename();
|
||||
PyConfig_SetString(&pyconfig, &pyconfig.executable, exe_path.c_str());
|
||||
|
||||
// Set interactive mode (replaces deprecated Py_InspectFlag)
|
||||
if (config.interactive_mode) {
|
||||
pyconfig.inspect = 1;
|
||||
}
|
||||
|
||||
// Don't modify sys.path based on script location (replaces PySys_SetArgvEx updatepath=0)
|
||||
pyconfig.safe_path = 1;
|
||||
|
||||
// Construct Python argv from config (replaces deprecated PySys_SetArgvEx)
|
||||
// Python convention:
|
||||
// - Script mode: argv[0] = script_path, argv[1:] = script_args
|
||||
// - -c mode: argv[0] = "-c"
|
||||
// - -m mode: argv[0] = module_name, argv[1:] = script_args
|
||||
// - Interactive only: argv[0] = ""
|
||||
std::vector<std::wstring> argv_storage;
|
||||
|
||||
if (!config.script_path.empty()) {
|
||||
// Script execution: argv[0] = script path
|
||||
argv_storage.push_back(config.script_path.wstring());
|
||||
for (const auto& arg : config.script_args) {
|
||||
std::wstring warg(arg.begin(), arg.end());
|
||||
argv_storage.push_back(warg);
|
||||
}
|
||||
} else if (!config.python_command.empty()) {
|
||||
// -c command: argv[0] = "-c"
|
||||
argv_storage.push_back(L"-c");
|
||||
} else if (!config.python_module.empty()) {
|
||||
// -m module: argv[0] = module name
|
||||
std::wstring wmodule(config.python_module.begin(), config.python_module.end());
|
||||
argv_storage.push_back(wmodule);
|
||||
for (const auto& arg : config.script_args) {
|
||||
std::wstring warg(arg.begin(), arg.end());
|
||||
argv_storage.push_back(warg);
|
||||
}
|
||||
} else {
|
||||
// Interactive mode or no script: argv[0] = ""
|
||||
argv_storage.push_back(L"");
|
||||
}
|
||||
|
||||
// Build wchar_t* array for PyConfig
|
||||
std::vector<wchar_t*> argv_ptrs;
|
||||
for (auto& ws : argv_storage) {
|
||||
argv_ptrs.push_back(const_cast<wchar_t*>(ws.c_str()));
|
||||
}
|
||||
|
||||
status = PyConfig_SetWideStringList(&pyconfig, &pyconfig.argv,
|
||||
argv_ptrs.size(), argv_ptrs.data());
|
||||
|
||||
// CRITICAL: Pass actual command line arguments to Python
|
||||
status = PyConfig_SetBytesArgv(&pyconfig, argc, argv);
|
||||
if (PyStatus_Exception(status)) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Check if we're in a virtual environment (symlinked into a venv)
|
||||
auto exe_wpath = executable_filename();
|
||||
auto exe_path_fs = std::filesystem::path(exe_wpath);
|
||||
auto exe_dir = exe_path_fs.parent_path();
|
||||
|
||||
// Check if we're in a virtual environment
|
||||
auto exe_path = std::filesystem::path(argv[0]);
|
||||
auto exe_dir = exe_path.parent_path();
|
||||
auto venv_root = exe_dir.parent_path();
|
||||
|
||||
|
||||
if (std::filesystem::exists(venv_root / "pyvenv.cfg")) {
|
||||
// We're running from within a venv!
|
||||
// Add venv's site-packages to module search paths
|
||||
auto site_packages = venv_root / "lib" / "python3.14" / "site-packages";
|
||||
auto site_packages = venv_root / "lib" / "python3.12" / "site-packages";
|
||||
PyWideStringList_Append(&pyconfig.module_search_paths,
|
||||
site_packages.wstring().c_str());
|
||||
pyconfig.module_search_paths_set = 1;
|
||||
}
|
||||
|
||||
// Check for sibling venv/ directory (self-contained deployment)
|
||||
auto sibling_venv = exe_dir / "venv";
|
||||
if (std::filesystem::exists(sibling_venv)) {
|
||||
// Platform-specific site-packages path
|
||||
#ifdef _WIN32
|
||||
auto site_packages = sibling_venv / "Lib" / "site-packages";
|
||||
#else
|
||||
auto site_packages = sibling_venv / "lib" / "python3.14" / "site-packages";
|
||||
#endif
|
||||
if (std::filesystem::exists(site_packages)) {
|
||||
// Prepend so venv packages take priority over bundled
|
||||
PyWideStringList_Insert(&pyconfig.module_search_paths, 0,
|
||||
site_packages.wstring().c_str());
|
||||
pyconfig.module_search_paths_set = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set Python home to our bundled Python
|
||||
auto python_home = executable_path() + L"/lib/Python";
|
||||
PyConfig_SetString(&pyconfig, &pyconfig.home, python_home.c_str());
|
||||
|
||||
|
||||
// Set up module search paths
|
||||
#if __PLATFORM_SET_PYTHON_SEARCH_PATHS == 1
|
||||
if (!pyconfig.module_search_paths_set) {
|
||||
pyconfig.module_search_paths_set = 1;
|
||||
}
|
||||
|
||||
|
||||
// search paths for python libs/modules/scripts
|
||||
const wchar_t* str_arr[] = {
|
||||
L"/scripts",
|
||||
L"/lib/Python/lib.linux-x86_64-3.14",
|
||||
L"/lib/Python/lib.linux-x86_64-3.12",
|
||||
L"/lib/Python",
|
||||
L"/lib/Python/Lib"
|
||||
// Note: venv site-packages handled above via sibling_venv detection
|
||||
L"/lib/Python/Lib",
|
||||
L"/venv/lib/python3.12/site-packages"
|
||||
};
|
||||
|
||||
|
||||
for(auto s : str_arr) {
|
||||
status = PyWideStringList_Append(&pyconfig.module_search_paths, (executable_path() + s).c_str());
|
||||
if (PyStatus_Exception(status)) {
|
||||
|
|
@ -635,13 +449,15 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config)
|
|||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// Register mcrfpy module before initialization
|
||||
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
|
||||
|
||||
if (!Py_IsInitialized()) {
|
||||
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
|
||||
}
|
||||
|
||||
status = Py_InitializeFromConfig(&pyconfig);
|
||||
PyConfig_Clear(&pyconfig);
|
||||
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
|
|
@ -687,9 +503,9 @@ void McRFPy_API::api_init() {
|
|||
//setSpriteTexture(0);
|
||||
}
|
||||
|
||||
void McRFPy_API::api_init(const McRogueFaceConfig& config) {
|
||||
// Initialize Python with proper argv constructed from config
|
||||
PyStatus status = init_python_with_config(config);
|
||||
void McRFPy_API::api_init(const McRogueFaceConfig& config, int argc, char** argv) {
|
||||
// Initialize Python with proper argv - this is CRITICAL
|
||||
PyStatus status = init_python_with_config(config, argc, argv);
|
||||
if (PyStatus_Exception(status)) {
|
||||
Py_ExitStatusException(status);
|
||||
}
|
||||
|
|
@ -992,33 +808,6 @@ PyObject* McRFPy_API::_delTimer(PyObject* self, PyObject* args) {
|
|||
return Py_None;
|
||||
}
|
||||
|
||||
// #153 - Headless simulation control
|
||||
PyObject* McRFPy_API::_step(PyObject* self, PyObject* args) {
|
||||
PyObject* dt_obj = Py_None;
|
||||
if (!PyArg_ParseTuple(args, "|O", &dt_obj)) return NULL;
|
||||
|
||||
float dt;
|
||||
if (dt_obj == Py_None) {
|
||||
// None means "advance to next event"
|
||||
dt = -1.0f;
|
||||
} else if (PyFloat_Check(dt_obj)) {
|
||||
dt = static_cast<float>(PyFloat_AsDouble(dt_obj));
|
||||
} else if (PyLong_Check(dt_obj)) {
|
||||
dt = static_cast<float>(PyLong_AsLong(dt_obj));
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "step() argument must be a float, int, or None");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Game engine not initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
float actual_dt = game->step(dt);
|
||||
return PyFloat_FromDouble(actual_dt);
|
||||
}
|
||||
|
||||
PyObject* McRFPy_API::_exit(PyObject* self, PyObject* args) {
|
||||
game->quit();
|
||||
Py_INCREF(Py_None);
|
||||
|
|
@ -1312,103 +1101,20 @@ PyObject* McRFPy_API::_getMetrics(PyObject* self, PyObject* args) {
|
|||
// Create a dictionary with metrics
|
||||
PyObject* dict = PyDict_New();
|
||||
if (!dict) return NULL;
|
||||
|
||||
|
||||
// Add frame time metrics
|
||||
PyDict_SetItemString(dict, "frame_time", PyFloat_FromDouble(game->metrics.frameTime));
|
||||
PyDict_SetItemString(dict, "avg_frame_time", PyFloat_FromDouble(game->metrics.avgFrameTime));
|
||||
PyDict_SetItemString(dict, "fps", PyLong_FromLong(game->metrics.fps));
|
||||
|
||||
|
||||
// Add draw call metrics
|
||||
PyDict_SetItemString(dict, "draw_calls", PyLong_FromLong(game->metrics.drawCalls));
|
||||
PyDict_SetItemString(dict, "ui_elements", PyLong_FromLong(game->metrics.uiElements));
|
||||
PyDict_SetItemString(dict, "visible_elements", PyLong_FromLong(game->metrics.visibleElements));
|
||||
|
||||
// #144 - Add detailed timing breakdown (in milliseconds)
|
||||
PyDict_SetItemString(dict, "grid_render_time", PyFloat_FromDouble(game->metrics.gridRenderTime));
|
||||
PyDict_SetItemString(dict, "entity_render_time", PyFloat_FromDouble(game->metrics.entityRenderTime));
|
||||
PyDict_SetItemString(dict, "fov_overlay_time", PyFloat_FromDouble(game->metrics.fovOverlayTime));
|
||||
PyDict_SetItemString(dict, "python_time", PyFloat_FromDouble(game->metrics.pythonScriptTime));
|
||||
PyDict_SetItemString(dict, "animation_time", PyFloat_FromDouble(game->metrics.animationTime));
|
||||
|
||||
// #144 - Add grid-specific metrics
|
||||
PyDict_SetItemString(dict, "grid_cells_rendered", PyLong_FromLong(game->metrics.gridCellsRendered));
|
||||
PyDict_SetItemString(dict, "entities_rendered", PyLong_FromLong(game->metrics.entitiesRendered));
|
||||
PyDict_SetItemString(dict, "total_entities", PyLong_FromLong(game->metrics.totalEntities));
|
||||
|
||||
|
||||
// Add general metrics
|
||||
PyDict_SetItemString(dict, "current_frame", PyLong_FromLong(game->getFrame()));
|
||||
PyDict_SetItemString(dict, "runtime", PyFloat_FromDouble(game->runtime.getElapsedTime().asSeconds()));
|
||||
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
PyObject* McRFPy_API::_setDevConsole(PyObject* self, PyObject* args) {
|
||||
int enabled;
|
||||
if (!PyArg_ParseTuple(args, "p", &enabled)) { // "p" for boolean predicate
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ImGuiConsole::setEnabled(enabled);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Benchmark logging implementation (#104)
|
||||
PyObject* McRFPy_API::_startBenchmark(PyObject* self, PyObject* args) {
|
||||
try {
|
||||
// Warn if in headless mode - benchmark frames are only recorded by the game loop
|
||||
if (game && game->isHeadless()) {
|
||||
PyErr_WarnEx(PyExc_UserWarning,
|
||||
"Benchmark started in headless mode. Note: step() and screenshot() do not "
|
||||
"record benchmark frames. The benchmark API captures per-frame data from the "
|
||||
"game loop, which is bypassed when using step()-based simulation control. "
|
||||
"For headless performance measurement, use Python's time module instead.", 1);
|
||||
}
|
||||
g_benchmarkLogger.start();
|
||||
Py_RETURN_NONE;
|
||||
} catch (const std::runtime_error& e) {
|
||||
PyErr_SetString(PyExc_RuntimeError, e.what());
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
PyObject* McRFPy_API::_endBenchmark(PyObject* self, PyObject* args) {
|
||||
try {
|
||||
std::string filename = g_benchmarkLogger.end();
|
||||
return PyUnicode_FromString(filename.c_str());
|
||||
} catch (const std::runtime_error& e) {
|
||||
PyErr_SetString(PyExc_RuntimeError, e.what());
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
PyObject* McRFPy_API::_logBenchmark(PyObject* self, PyObject* args) {
|
||||
const char* message;
|
||||
if (!PyArg_ParseTuple(args, "s", &message)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
try {
|
||||
g_benchmarkLogger.log(message);
|
||||
Py_RETURN_NONE;
|
||||
} catch (const std::runtime_error& e) {
|
||||
PyErr_SetString(PyExc_RuntimeError, e.what());
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Exception handling implementation
|
||||
void McRFPy_API::signalPythonException() {
|
||||
// Check if we should exit on exception (consult config via game)
|
||||
if (game && !game->isHeadless()) {
|
||||
// In windowed mode, respect the config setting
|
||||
// Access config through game engine - but we need to check the config
|
||||
}
|
||||
|
||||
// For now, always signal - the game loop will check the config
|
||||
exception_occurred.store(true);
|
||||
exit_code.store(1);
|
||||
}
|
||||
|
||||
bool McRFPy_API::shouldExit() {
|
||||
return exception_occurred.load();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <list>
|
||||
#include <atomic>
|
||||
|
||||
#include "PyFont.h"
|
||||
#include "PyTexture.h"
|
||||
|
|
@ -29,8 +28,8 @@ public:
|
|||
//static void setSpriteTexture(int);
|
||||
inline static GameEngine* game;
|
||||
static void api_init();
|
||||
static void api_init(const McRogueFaceConfig& config);
|
||||
static PyStatus init_python_with_config(const McRogueFaceConfig& config);
|
||||
static void api_init(const McRogueFaceConfig& config, int argc, char** argv);
|
||||
static PyStatus init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv);
|
||||
static void api_shutdown();
|
||||
// Python API functionality - use mcrfpy.* in scripts
|
||||
//static PyObject* _drawSprite(PyObject*, PyObject*);
|
||||
|
|
@ -62,9 +61,6 @@ public:
|
|||
static PyObject* _setTimer(PyObject*, PyObject*);
|
||||
static PyObject* _delTimer(PyObject*, PyObject*);
|
||||
|
||||
// #153 - Headless simulation control
|
||||
static PyObject* _step(PyObject*, PyObject*);
|
||||
|
||||
static PyObject* _exit(PyObject*, PyObject*);
|
||||
static PyObject* _setScale(PyObject*, PyObject*);
|
||||
|
||||
|
|
@ -84,23 +80,9 @@ public:
|
|||
|
||||
// Profiling/metrics
|
||||
static PyObject* _getMetrics(PyObject*, PyObject*);
|
||||
|
||||
// Benchmark logging (#104)
|
||||
static PyObject* _startBenchmark(PyObject*, PyObject*);
|
||||
static PyObject* _endBenchmark(PyObject*, PyObject*);
|
||||
static PyObject* _logBenchmark(PyObject*, PyObject*);
|
||||
|
||||
// Developer console
|
||||
static PyObject* _setDevConsole(PyObject*, PyObject*);
|
||||
|
||||
|
||||
// Scene lifecycle management for Python Scene objects
|
||||
static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene);
|
||||
static void updatePythonScenes(float dt);
|
||||
static void triggerResize(int width, int height);
|
||||
|
||||
// Exception handling - signal game loop to exit on unhandled Python exceptions
|
||||
static std::atomic<bool> exception_occurred;
|
||||
static std::atomic<int> exit_code;
|
||||
static void signalPythonException(); // Called by exception handlers
|
||||
static bool shouldExit(); // Checked by game loop
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,14 +6,6 @@
|
|||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
|
||||
// #111 - Static member for simulated mouse position in headless mode
|
||||
sf::Vector2i McRFPy_Automation::simulated_mouse_pos(0, 0);
|
||||
|
||||
// #111 - Get simulated mouse position for headless mode
|
||||
sf::Vector2i McRFPy_Automation::getSimulatedMousePosition() {
|
||||
return simulated_mouse_pos;
|
||||
}
|
||||
|
||||
// Helper function to get game engine
|
||||
GameEngine* McRFPy_Automation::getGameEngine() {
|
||||
return McRFPy_API::game;
|
||||
|
|
@ -114,17 +106,10 @@ sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) {
|
|||
void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button) {
|
||||
auto engine = getGameEngine();
|
||||
if (!engine) return;
|
||||
|
||||
// #111 - Track simulated mouse position for headless mode
|
||||
if (type == sf::Event::MouseMoved ||
|
||||
type == sf::Event::MouseButtonPressed ||
|
||||
type == sf::Event::MouseButtonReleased) {
|
||||
simulated_mouse_pos = sf::Vector2i(x, y);
|
||||
}
|
||||
|
||||
|
||||
sf::Event event;
|
||||
event.type = type;
|
||||
|
||||
|
||||
switch (type) {
|
||||
case sf::Event::MouseMoved:
|
||||
event.mouseMove.x = x;
|
||||
|
|
@ -145,7 +130,7 @@ void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y
|
|||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
engine->processEvent(event);
|
||||
}
|
||||
|
||||
|
|
@ -185,52 +170,47 @@ void McRFPy_Automation::injectTextEvent(sf::Uint32 unicode) {
|
|||
}
|
||||
|
||||
// Screenshot implementation
|
||||
// #153 - In headless mode, this is now SYNCHRONOUS: renders scene then captures
|
||||
PyObject* McRFPy_Automation::_screenshot(PyObject* self, PyObject* args) {
|
||||
const char* filename;
|
||||
if (!PyArg_ParseTuple(args, "s", &filename)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
auto engine = getGameEngine();
|
||||
if (!engine) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Game engine not initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// Get the render target
|
||||
sf::RenderTarget* target = engine->getRenderTargetPtr();
|
||||
if (!target) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No render target available");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// For RenderWindow (windowed mode), capture the current buffer
|
||||
|
||||
// For RenderWindow, we can get a screenshot directly
|
||||
if (auto* window = dynamic_cast<sf::RenderWindow*>(target)) {
|
||||
sf::Vector2u windowSize = window->getSize();
|
||||
sf::Texture texture;
|
||||
texture.create(windowSize.x, windowSize.y);
|
||||
texture.update(*window);
|
||||
|
||||
|
||||
if (texture.copyToImage().saveToFile(filename)) {
|
||||
Py_RETURN_TRUE;
|
||||
} else {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
}
|
||||
// For RenderTexture (headless mode) - SYNCHRONOUS render then capture
|
||||
// For RenderTexture (headless mode)
|
||||
else if (auto* renderTexture = dynamic_cast<sf::RenderTexture*>(target)) {
|
||||
// #153 - Force a synchronous render before capturing
|
||||
// This ensures we capture the CURRENT state, not the previous frame
|
||||
engine->renderScene();
|
||||
|
||||
if (renderTexture->getTexture().copyToImage().saveToFile(filename)) {
|
||||
Py_RETURN_TRUE;
|
||||
} else {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
PyErr_SetString(PyExc_RuntimeError, "Unknown render target type");
|
||||
return NULL;
|
||||
}
|
||||
|
|
@ -239,22 +219,18 @@ PyObject* McRFPy_Automation::_screenshot(PyObject* self, PyObject* args) {
|
|||
PyObject* McRFPy_Automation::_position(PyObject* self, PyObject* args) {
|
||||
auto engine = getGameEngine();
|
||||
if (!engine || !engine->getRenderTargetPtr()) {
|
||||
return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y);
|
||||
return Py_BuildValue("(ii)", 0, 0);
|
||||
}
|
||||
|
||||
// In headless mode, return the simulated mouse position (#111)
|
||||
if (engine->isHeadless()) {
|
||||
return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y);
|
||||
}
|
||||
|
||||
// In windowed mode, return the actual mouse position relative to window
|
||||
|
||||
// In headless mode, we'd need to track the simulated mouse position
|
||||
// For now, return the actual mouse position relative to window if available
|
||||
if (auto* window = dynamic_cast<sf::RenderWindow*>(engine->getRenderTargetPtr())) {
|
||||
sf::Vector2i pos = sf::Mouse::getPosition(*window);
|
||||
return Py_BuildValue("(ii)", pos.x, pos.y);
|
||||
}
|
||||
|
||||
// Fallback to simulated position
|
||||
return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y);
|
||||
|
||||
// In headless mode, return simulated position (TODO: track this)
|
||||
return Py_BuildValue("(ii)", 0, 0);
|
||||
}
|
||||
|
||||
// Get screen size
|
||||
|
|
|
|||
|
|
@ -51,12 +51,6 @@ public:
|
|||
static sf::Keyboard::Key stringToKey(const std::string& keyName);
|
||||
static void sleep_ms(int milliseconds);
|
||||
|
||||
// #111 - Simulated mouse position for headless mode
|
||||
static sf::Vector2i getSimulatedMousePosition();
|
||||
|
||||
private:
|
||||
static GameEngine* getGameEngine();
|
||||
|
||||
// #111 - Track simulated mouse position for headless mode
|
||||
static sf::Vector2i simulated_mouse_pos;
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
@ -185,19 +185,38 @@ static PyObject* McRFPy_Libtcod::dijkstra_path_to(PyObject* self, PyObject* args
|
|||
return path_list;
|
||||
}
|
||||
|
||||
// FOV algorithm constants removed - use mcrfpy.FOV enum instead (#114)
|
||||
// Add FOV algorithm constants to module
|
||||
static PyObject* McRFPy_Libtcod::add_fov_constants(PyObject* module) {
|
||||
// FOV algorithms
|
||||
PyModule_AddIntConstant(module, "FOV_BASIC", FOV_BASIC);
|
||||
PyModule_AddIntConstant(module, "FOV_DIAMOND", FOV_DIAMOND);
|
||||
PyModule_AddIntConstant(module, "FOV_SHADOW", FOV_SHADOW);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_0", FOV_PERMISSIVE_0);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_1", FOV_PERMISSIVE_1);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_2", FOV_PERMISSIVE_2);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_3", FOV_PERMISSIVE_3);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_4", FOV_PERMISSIVE_4);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_5", FOV_PERMISSIVE_5);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_6", FOV_PERMISSIVE_6);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_7", FOV_PERMISSIVE_7);
|
||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_8", FOV_PERMISSIVE_8);
|
||||
PyModule_AddIntConstant(module, "FOV_RESTRICTIVE", FOV_RESTRICTIVE);
|
||||
PyModule_AddIntConstant(module, "FOV_SYMMETRIC_SHADOWCAST", FOV_SYMMETRIC_SHADOWCAST);
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
// Method definitions
|
||||
static PyMethodDef libtcodMethods[] = {
|
||||
{"compute_fov", McRFPy_Libtcod::compute_fov, METH_VARARGS,
|
||||
"compute_fov(grid, x, y, radius, light_walls=True, algorithm=mcrfpy.FOV.BASIC)\n\n"
|
||||
{"compute_fov", McRFPy_Libtcod::compute_fov, METH_VARARGS,
|
||||
"compute_fov(grid, x, y, radius, light_walls=True, algorithm=FOV_BASIC)\n\n"
|
||||
"Compute field of view from a position.\n\n"
|
||||
"Args:\n"
|
||||
" grid: Grid object to compute FOV on\n"
|
||||
" x, y: Origin position\n"
|
||||
" radius: Maximum sight radius\n"
|
||||
" light_walls: Whether walls are lit when in FOV\n"
|
||||
" algorithm: FOV algorithm (mcrfpy.FOV.BASIC, mcrfpy.FOV.SHADOW, etc.)\n\n"
|
||||
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_SHADOW, etc.)\n\n"
|
||||
"Returns:\n"
|
||||
" List of (x, y) tuples for visible cells"},
|
||||
|
||||
|
|
@ -274,13 +293,13 @@ static PyModuleDef libtcodModule = {
|
|||
"TCOD-compatible algorithms for field of view, pathfinding, and line drawing.\n\n"
|
||||
"This module provides access to TCOD's algorithms integrated with McRogueFace grids.\n"
|
||||
"Unlike the original TCOD, these functions work directly with Grid objects.\n\n"
|
||||
"FOV Algorithms (use mcrfpy.FOV enum):\n"
|
||||
" mcrfpy.FOV.BASIC - Basic circular FOV\n"
|
||||
" mcrfpy.FOV.SHADOW - Shadow casting (recommended)\n"
|
||||
" mcrfpy.FOV.DIAMOND - Diamond-shaped FOV\n"
|
||||
" mcrfpy.FOV.PERMISSIVE_0 through PERMISSIVE_8 - Permissive variants\n"
|
||||
" mcrfpy.FOV.RESTRICTIVE - Most restrictive FOV\n"
|
||||
" mcrfpy.FOV.SYMMETRIC_SHADOWCAST - Symmetric shadow casting\n\n"
|
||||
"FOV Algorithms:\n"
|
||||
" FOV_BASIC - Basic circular FOV\n"
|
||||
" FOV_SHADOW - Shadow casting (recommended)\n"
|
||||
" FOV_DIAMOND - Diamond-shaped FOV\n"
|
||||
" FOV_PERMISSIVE_0 through FOV_PERMISSIVE_8 - Permissive variants\n"
|
||||
" FOV_RESTRICTIVE - Most restrictive FOV\n"
|
||||
" FOV_SYMMETRIC_SHADOWCAST - Symmetric shadow casting\n\n"
|
||||
"Example:\n"
|
||||
" import mcrfpy\n"
|
||||
" from mcrfpy import libtcod\n\n"
|
||||
|
|
@ -298,7 +317,8 @@ PyObject* McRFPy_Libtcod::init_libtcod_module() {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// FOV algorithm constants now provided by mcrfpy.FOV enum (#114)
|
||||
|
||||
// Add FOV algorithm constants
|
||||
add_fov_constants(m);
|
||||
|
||||
return m;
|
||||
}
|
||||
|
|
@ -18,7 +18,10 @@ namespace McRFPy_Libtcod
|
|||
// Line algorithms
|
||||
static PyObject* line(PyObject* self, PyObject* args);
|
||||
static PyObject* line_iter(PyObject* self, PyObject* args);
|
||||
|
||||
|
||||
// FOV algorithm constants
|
||||
static PyObject* add_fov_constants(PyObject* module);
|
||||
|
||||
// Module initialization
|
||||
PyObject* init_libtcod_module();
|
||||
}
|
||||
|
|
@ -28,13 +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;
|
||||
|
||||
// Exception handling: exit on first Python callback exception (default: true)
|
||||
// Use --continue-after-exceptions to disable
|
||||
bool exit_on_exception = true;
|
||||
};
|
||||
|
||||
#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}
|
||||
};
|
||||
|
|
@ -1,27 +1,10 @@
|
|||
#include "PyCallable.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "GameEngine.h"
|
||||
|
||||
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)
|
||||
|
|
@ -38,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)
|
||||
|
|
@ -53,14 +133,9 @@ void PyClickCallable::call(sf::Vector2f mousepos, std::string button, std::strin
|
|||
PyObject* retval = PyCallable::call(args, NULL);
|
||||
if (!retval)
|
||||
{
|
||||
std::cerr << "Click callback raised an exception:" << std::endl;
|
||||
std::cout << "ClickCallable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
|
||||
// Check if we should exit on exception
|
||||
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
|
||||
McRFPy_API::signalPythonException();
|
||||
}
|
||||
} else if (retval != Py_None)
|
||||
{
|
||||
std::cout << "ClickCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
|
|
@ -88,14 +163,9 @@ void PyKeyCallable::call(std::string key, std::string action)
|
|||
PyObject* retval = PyCallable::call(args, NULL);
|
||||
if (!retval)
|
||||
{
|
||||
std::cerr << "Key callback raised an exception:" << std::endl;
|
||||
std::cout << "KeyCallable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
|
||||
// Check if we should exit on exception
|
||||
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
|
||||
McRFPy_API::signalPythonException();
|
||||
}
|
||||
} else if (retval != Py_None)
|
||||
{
|
||||
std::cout << "KeyCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
};
|
||||
|
||||
|
|
@ -236,7 +207,6 @@ PyColorObject* PyColor::from_arg(PyObject* args)
|
|||
|
||||
// Check if args is already a Color instance
|
||||
if (PyObject_IsInstance(args, (PyObject*)type.get())) {
|
||||
Py_INCREF(args); // Return new reference so caller can safely DECREF
|
||||
return (PyColorObject*)args;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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[] = {
|
||||
{"on_click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click,
|
||||
MCRF_PROPERTY(on_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
|
||||
};
|
||||
|
||||
|
|
|
|||
148
src/PyFOV.cpp
148
src/PyFOV.cpp
|
|
@ -1,148 +0,0 @@
|
|||
#include "PyFOV.h"
|
||||
#include "McRFPy_API.h"
|
||||
|
||||
// Static storage for cached enum class reference
|
||||
PyObject* PyFOV::fov_enum_class = nullptr;
|
||||
|
||||
PyObject* PyFOV::create_enum_class(PyObject* module) {
|
||||
// Import IntEnum from enum module
|
||||
PyObject* enum_module = PyImport_ImportModule("enum");
|
||||
if (!enum_module) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* int_enum = PyObject_GetAttrString(enum_module, "IntEnum");
|
||||
Py_DECREF(enum_module);
|
||||
if (!int_enum) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Create dict of enum members
|
||||
PyObject* members = PyDict_New();
|
||||
if (!members) {
|
||||
Py_DECREF(int_enum);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Add all FOV algorithm members
|
||||
struct {
|
||||
const char* name;
|
||||
int value;
|
||||
} fov_members[] = {
|
||||
{"BASIC", FOV_BASIC},
|
||||
{"DIAMOND", FOV_DIAMOND},
|
||||
{"SHADOW", FOV_SHADOW},
|
||||
{"PERMISSIVE_0", FOV_PERMISSIVE_0},
|
||||
{"PERMISSIVE_1", FOV_PERMISSIVE_1},
|
||||
{"PERMISSIVE_2", FOV_PERMISSIVE_2},
|
||||
{"PERMISSIVE_3", FOV_PERMISSIVE_3},
|
||||
{"PERMISSIVE_4", FOV_PERMISSIVE_4},
|
||||
{"PERMISSIVE_5", FOV_PERMISSIVE_5},
|
||||
{"PERMISSIVE_6", FOV_PERMISSIVE_6},
|
||||
{"PERMISSIVE_7", FOV_PERMISSIVE_7},
|
||||
{"PERMISSIVE_8", FOV_PERMISSIVE_8},
|
||||
{"RESTRICTIVE", FOV_RESTRICTIVE},
|
||||
{"SYMMETRIC_SHADOWCAST", FOV_SYMMETRIC_SHADOWCAST},
|
||||
};
|
||||
|
||||
for (const auto& m : fov_members) {
|
||||
PyObject* value = PyLong_FromLong(m.value);
|
||||
if (!value) {
|
||||
Py_DECREF(members);
|
||||
Py_DECREF(int_enum);
|
||||
return NULL;
|
||||
}
|
||||
if (PyDict_SetItemString(members, m.name, value) < 0) {
|
||||
Py_DECREF(value);
|
||||
Py_DECREF(members);
|
||||
Py_DECREF(int_enum);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(value);
|
||||
}
|
||||
|
||||
// Call IntEnum("FOV", members) to create the enum class
|
||||
PyObject* name = PyUnicode_FromString("FOV");
|
||||
if (!name) {
|
||||
Py_DECREF(members);
|
||||
Py_DECREF(int_enum);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// IntEnum(name, members) using functional API
|
||||
PyObject* args = PyTuple_Pack(2, name, members);
|
||||
Py_DECREF(name);
|
||||
Py_DECREF(members);
|
||||
if (!args) {
|
||||
Py_DECREF(int_enum);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* fov_class = PyObject_Call(int_enum, args, NULL);
|
||||
Py_DECREF(args);
|
||||
Py_DECREF(int_enum);
|
||||
|
||||
if (!fov_class) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Cache the reference for fast type checking
|
||||
fov_enum_class = fov_class;
|
||||
Py_INCREF(fov_enum_class);
|
||||
|
||||
// Add to module
|
||||
if (PyModule_AddObject(module, "FOV", fov_class) < 0) {
|
||||
Py_DECREF(fov_class);
|
||||
fov_enum_class = nullptr;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return fov_class;
|
||||
}
|
||||
|
||||
int PyFOV::from_arg(PyObject* arg, TCOD_fov_algorithm_t* out_algo, bool* was_none) {
|
||||
if (was_none) *was_none = false;
|
||||
|
||||
// Accept None -> caller should use default
|
||||
if (arg == Py_None) {
|
||||
if (was_none) *was_none = true;
|
||||
*out_algo = FOV_BASIC;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Accept FOV enum member (check if it's an instance of our enum)
|
||||
if (fov_enum_class && PyObject_IsInstance(arg, fov_enum_class)) {
|
||||
// IntEnum members have a 'value' attribute
|
||||
PyObject* value = PyObject_GetAttrString(arg, "value");
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
long val = PyLong_AsLong(value);
|
||||
Py_DECREF(value);
|
||||
if (val == -1 && PyErr_Occurred()) {
|
||||
return 0;
|
||||
}
|
||||
*out_algo = (TCOD_fov_algorithm_t)val;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Accept int (for backwards compatibility)
|
||||
if (PyLong_Check(arg)) {
|
||||
long val = PyLong_AsLong(arg);
|
||||
if (val == -1 && PyErr_Occurred()) {
|
||||
return 0;
|
||||
}
|
||||
if (val < 0 || val >= NB_FOV_ALGORITHMS) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"Invalid FOV algorithm value: %ld. Must be 0-%d or use mcrfpy.FOV enum.",
|
||||
val, NB_FOV_ALGORITHMS - 1);
|
||||
return 0;
|
||||
}
|
||||
*out_algo = (TCOD_fov_algorithm_t)val;
|
||||
return 1;
|
||||
}
|
||||
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"FOV algorithm must be mcrfpy.FOV enum member, int, or None");
|
||||
return 0;
|
||||
}
|
||||
22
src/PyFOV.h
22
src/PyFOV.h
|
|
@ -1,22 +0,0 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <libtcod.h>
|
||||
|
||||
// Module-level FOV enum class (created at runtime using Python's IntEnum)
|
||||
// Stored as a module attribute: mcrfpy.FOV
|
||||
|
||||
class PyFOV {
|
||||
public:
|
||||
// Create the FOV enum class and add to module
|
||||
// Returns the enum class (new reference), or NULL on error
|
||||
static PyObject* create_enum_class(PyObject* module);
|
||||
|
||||
// Helper to extract algorithm from Python arg (accepts FOV enum, int, or None)
|
||||
// Returns 1 on success, 0 on error (with exception set)
|
||||
// If arg is None, sets *out_algo to the default (FOV_BASIC) and sets *was_none to true
|
||||
static int from_arg(PyObject* arg, TCOD_fov_algorithm_t* out_algo, bool* was_none = nullptr);
|
||||
|
||||
// Cached reference to the FOV enum class for fast type checking
|
||||
static PyObject* fov_enum_class;
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
119
src/PyScene.cpp
119
src/PyScene.cpp
|
|
@ -2,11 +2,7 @@
|
|||
#include "ActionCode.h"
|
||||
#include "Resources.h"
|
||||
#include "PyCallable.h"
|
||||
#include "UIFrame.h"
|
||||
#include "UIGrid.h"
|
||||
#include "McRFPy_Automation.h" // #111 - For simulated mouse position
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
|
||||
PyScene::PyScene(GameEngine* g) : Scene(g)
|
||||
{
|
||||
|
|
@ -26,18 +22,15 @@ void PyScene::update()
|
|||
|
||||
void PyScene::do_mouse_input(std::string button, std::string type)
|
||||
{
|
||||
sf::Vector2f mousepos;
|
||||
|
||||
// #111 - In headless mode, use simulated mouse position
|
||||
// In headless mode, mouse input is not available
|
||||
if (game->isHeadless()) {
|
||||
sf::Vector2i simPos = McRFPy_Automation::getSimulatedMousePosition();
|
||||
mousepos = sf::Vector2f(static_cast<float>(simPos.x), static_cast<float>(simPos.y));
|
||||
} else {
|
||||
auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow());
|
||||
// Convert window coordinates to game coordinates using the viewport
|
||||
mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos));
|
||||
return;
|
||||
}
|
||||
|
||||
auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow());
|
||||
// Convert window coordinates to game coordinates using the viewport
|
||||
auto mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos));
|
||||
|
||||
// Only sort if z_index values have changed
|
||||
if (ui_elements_need_sort) {
|
||||
// Sort in ascending order (same as render)
|
||||
|
|
@ -61,7 +54,7 @@ void PyScene::do_mouse_input(std::string button, std::string type)
|
|||
|
||||
void PyScene::doAction(std::string name, std::string type)
|
||||
{
|
||||
if (name.compare("left") == 0 || name.compare("right") == 0 || name.compare("wheel_up") == 0 || name.compare("wheel_down") == 0) {
|
||||
if (name.compare("left") == 0 || name.compare("rclick") == 0 || name.compare("wheel_up") == 0 || name.compare("wheel_down") == 0) {
|
||||
do_mouse_input(name, type);
|
||||
}
|
||||
else if ACTIONONCE("debug_menu") {
|
||||
|
|
@ -69,93 +62,20 @@ void PyScene::doAction(std::string name, std::string type)
|
|||
}
|
||||
}
|
||||
|
||||
// #140 - Mouse enter/exit tracking
|
||||
void PyScene::do_mouse_hover(int x, int y)
|
||||
{
|
||||
// In headless mode, use the coordinates directly (already in game space)
|
||||
sf::Vector2f mousepos;
|
||||
if (game->isHeadless()) {
|
||||
mousepos = sf::Vector2f(static_cast<float>(x), static_cast<float>(y));
|
||||
} else {
|
||||
// Convert window coordinates to game coordinates using the viewport
|
||||
mousepos = game->windowToGameCoords(sf::Vector2f(static_cast<float>(x), static_cast<float>(y)));
|
||||
}
|
||||
|
||||
// Helper function to process hover for a single drawable and its children
|
||||
std::function<void(UIDrawable*)> processHover = [&](UIDrawable* drawable) {
|
||||
if (!drawable || !drawable->visible) return;
|
||||
|
||||
bool is_inside = drawable->contains_point(mousepos.x, mousepos.y);
|
||||
bool was_hovered = drawable->hovered;
|
||||
|
||||
if (is_inside && !was_hovered) {
|
||||
// Mouse entered
|
||||
drawable->hovered = true;
|
||||
if (drawable->on_enter_callable) {
|
||||
drawable->on_enter_callable->call(mousepos, "enter", "start");
|
||||
}
|
||||
} else if (!is_inside && was_hovered) {
|
||||
// Mouse exited
|
||||
drawable->hovered = false;
|
||||
if (drawable->on_exit_callable) {
|
||||
drawable->on_exit_callable->call(mousepos, "exit", "start");
|
||||
}
|
||||
}
|
||||
|
||||
// #141 - Fire on_move if mouse is inside and has a move callback
|
||||
if (is_inside && drawable->on_move_callable) {
|
||||
drawable->on_move_callable->call(mousepos, "move", "start");
|
||||
}
|
||||
|
||||
// Process children for Frame elements
|
||||
if (drawable->derived_type() == PyObjectsEnum::UIFRAME) {
|
||||
auto frame = static_cast<UIFrame*>(drawable);
|
||||
if (frame->children) {
|
||||
for (auto& child : *frame->children) {
|
||||
processHover(child.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Process children for Grid elements
|
||||
else if (drawable->derived_type() == PyObjectsEnum::UIGRID) {
|
||||
auto grid = static_cast<UIGrid*>(drawable);
|
||||
|
||||
// #142 - Update cell hover tracking for grid
|
||||
grid->updateCellHover(mousepos);
|
||||
|
||||
if (grid->children) {
|
||||
for (auto& child : *grid->children) {
|
||||
processHover(child.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Process all top-level UI elements
|
||||
for (auto& element : *ui_elements) {
|
||||
processHover(element.get());
|
||||
}
|
||||
}
|
||||
|
||||
void PyScene::render()
|
||||
{
|
||||
// #118: Skip rendering if scene is not visible
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
game->getRenderTarget().clear();
|
||||
|
||||
|
||||
// Only sort if z_index values have changed
|
||||
if (ui_elements_need_sort) {
|
||||
std::sort(ui_elements->begin(), ui_elements->end(),
|
||||
std::sort(ui_elements->begin(), ui_elements->end(),
|
||||
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
|
||||
return a->z_index < b->z_index;
|
||||
});
|
||||
ui_elements_need_sort = false;
|
||||
}
|
||||
|
||||
// Render in sorted order with scene-level transformations
|
||||
|
||||
// Render in sorted order (no need to copy anymore)
|
||||
for (auto e: *ui_elements)
|
||||
{
|
||||
if (e) {
|
||||
|
|
@ -166,22 +86,9 @@ void PyScene::render()
|
|||
// Count this as a draw call (each visible element = 1+ draw calls)
|
||||
game->metrics.drawCalls++;
|
||||
}
|
||||
|
||||
// #118: Apply scene-level opacity to element
|
||||
float original_opacity = e->opacity;
|
||||
if (opacity < 1.0f) {
|
||||
e->opacity = original_opacity * opacity;
|
||||
}
|
||||
|
||||
// #118: Render with scene position offset
|
||||
e->render(position, game->getRenderTarget());
|
||||
|
||||
// #118: Restore original opacity
|
||||
if (opacity < 1.0f) {
|
||||
e->opacity = original_opacity;
|
||||
}
|
||||
e->render();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Display is handled by GameEngine
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ public:
|
|||
void render() override final;
|
||||
|
||||
void do_mouse_input(std::string, std::string);
|
||||
void do_mouse_hover(int x, int y); // #140 - Mouse enter/exit tracking
|
||||
|
||||
|
||||
// Dirty flag for z_index sorting optimization
|
||||
bool ui_elements_need_sort = true;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -133,159 +132,10 @@ PyObject* PySceneClass::get_active(PySceneObject* self, void* closure)
|
|||
if (!game) {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
|
||||
return PyBool_FromLong(game->scene == self->name);
|
||||
}
|
||||
|
||||
// #118: Scene position getter
|
||||
static PyObject* PySceneClass_get_pos(PySceneObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Get the scene by name using the public accessor
|
||||
auto scene = game->getScene(self->name);
|
||||
if (!scene) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Create a Vector object
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (!type) return NULL;
|
||||
PyObject* args = Py_BuildValue("(ff)", scene->position.x, scene->position.y);
|
||||
PyObject* result = PyObject_CallObject((PyObject*)type, args);
|
||||
Py_DECREF(type);
|
||||
Py_DECREF(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
// #118: Scene position setter
|
||||
static int PySceneClass_set_pos(PySceneObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine");
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto scene = game->getScene(self->name);
|
||||
if (!scene) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Scene not found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Accept tuple or Vector
|
||||
float x, y;
|
||||
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
|
||||
x = PyFloat_AsDouble(PyTuple_GetItem(value, 0));
|
||||
y = PyFloat_AsDouble(PyTuple_GetItem(value, 1));
|
||||
} else if (PyObject_HasAttrString(value, "x") && PyObject_HasAttrString(value, "y")) {
|
||||
PyObject* xobj = PyObject_GetAttrString(value, "x");
|
||||
PyObject* yobj = PyObject_GetAttrString(value, "y");
|
||||
x = PyFloat_AsDouble(xobj);
|
||||
y = PyFloat_AsDouble(yobj);
|
||||
Py_DECREF(xobj);
|
||||
Py_DECREF(yobj);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
|
||||
scene->position = sf::Vector2f(x, y);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// #118: Scene visible getter
|
||||
static PyObject* PySceneClass_get_visible(PySceneObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
Py_RETURN_TRUE;
|
||||
}
|
||||
|
||||
auto scene = game->getScene(self->name);
|
||||
if (!scene) {
|
||||
Py_RETURN_TRUE;
|
||||
}
|
||||
|
||||
return PyBool_FromLong(scene->visible);
|
||||
}
|
||||
|
||||
// #118: Scene visible setter
|
||||
static int PySceneClass_set_visible(PySceneObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine");
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto scene = game->getScene(self->name);
|
||||
if (!scene) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Scene not found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyBool_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
|
||||
return -1;
|
||||
}
|
||||
|
||||
scene->visible = PyObject_IsTrue(value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// #118: Scene opacity getter
|
||||
static PyObject* PySceneClass_get_opacity(PySceneObject* self, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
return PyFloat_FromDouble(1.0);
|
||||
}
|
||||
|
||||
auto scene = game->getScene(self->name);
|
||||
if (!scene) {
|
||||
return PyFloat_FromDouble(1.0);
|
||||
}
|
||||
|
||||
return PyFloat_FromDouble(scene->opacity);
|
||||
}
|
||||
|
||||
// #118: Scene opacity setter
|
||||
static int PySceneClass_set_opacity(PySceneObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
GameEngine* game = McRFPy_API::game;
|
||||
if (!game) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No game engine");
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto scene = game->getScene(self->name);
|
||||
if (!scene) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Scene not found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
double opacity;
|
||||
if (PyFloat_Check(value)) {
|
||||
opacity = PyFloat_AsDouble(value);
|
||||
} else if (PyLong_Check(value)) {
|
||||
opacity = PyLong_AsDouble(value);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "opacity must be a number");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Clamp to valid range
|
||||
if (opacity < 0.0) opacity = 0.0;
|
||||
if (opacity > 1.0) opacity = 1.0;
|
||||
|
||||
scene->opacity = opacity;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Lifecycle callbacks
|
||||
void PySceneClass::call_on_enter(PySceneObject* self)
|
||||
{
|
||||
|
|
@ -297,12 +147,8 @@ void PySceneClass::call_on_enter(PySceneObject* self)
|
|||
} else {
|
||||
PyErr_Print();
|
||||
}
|
||||
Py_DECREF(method);
|
||||
} else {
|
||||
// Clear AttributeError if method doesn't exist
|
||||
PyErr_Clear();
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
|
||||
void PySceneClass::call_on_exit(PySceneObject* self)
|
||||
|
|
@ -315,18 +161,14 @@ void PySceneClass::call_on_exit(PySceneObject* self)
|
|||
} else {
|
||||
PyErr_Print();
|
||||
}
|
||||
Py_DECREF(method);
|
||||
} else {
|
||||
// Clear AttributeError if method doesn't exist
|
||||
PyErr_Clear();
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
|
||||
void PySceneClass::call_on_keypress(PySceneObject* self, std::string key, std::string action)
|
||||
{
|
||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||
|
||||
|
||||
PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_keypress");
|
||||
if (method && PyCallable_Check(method)) {
|
||||
PyObject* result = PyObject_CallFunction(method, "ss", key.c_str(), action.c_str());
|
||||
|
|
@ -335,13 +177,9 @@ void PySceneClass::call_on_keypress(PySceneObject* self, std::string key, std::s
|
|||
} else {
|
||||
PyErr_Print();
|
||||
}
|
||||
Py_DECREF(method);
|
||||
} else {
|
||||
// Clear AttributeError if method doesn't exist
|
||||
PyErr_Clear();
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
|
||||
Py_XDECREF(method);
|
||||
|
||||
PyGILState_Release(gstate);
|
||||
}
|
||||
|
||||
|
|
@ -355,12 +193,8 @@ void PySceneClass::call_update(PySceneObject* self, float dt)
|
|||
} else {
|
||||
PyErr_Print();
|
||||
}
|
||||
Py_DECREF(method);
|
||||
} else {
|
||||
// Clear AttributeError if method doesn't exist
|
||||
PyErr_Clear();
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
|
||||
void PySceneClass::call_on_resize(PySceneObject* self, int width, int height)
|
||||
|
|
@ -373,55 +207,25 @@ void PySceneClass::call_on_resize(PySceneObject* self, int width, int height)
|
|||
} else {
|
||||
PyErr_Print();
|
||||
}
|
||||
Py_DECREF(method);
|
||||
} else {
|
||||
// Clear AttributeError if method doesn't exist
|
||||
PyErr_Clear();
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
Py_XDECREF(method);
|
||||
}
|
||||
|
||||
// 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},
|
||||
// #118: Scene-level UIDrawable-like properties
|
||||
{"pos", (getter)PySceneClass_get_pos, (setter)PySceneClass_set_pos,
|
||||
MCRF_PROPERTY(pos, "Scene position offset (Vector). Applied to all UI elements during rendering."), NULL},
|
||||
{"visible", (getter)PySceneClass_get_visible, (setter)PySceneClass_set_visible,
|
||||
MCRF_PROPERTY(visible, "Scene visibility (bool). If False, scene is not rendered."), NULL},
|
||||
{"opacity", (getter)PySceneClass_get_opacity, (setter)PySceneClass_set_opacity,
|
||||
MCRF_PROPERTY(opacity, "Scene opacity (0.0-1.0). Applied to all UI elements during rendering."), 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)
|
||||
|
|
@ -17,37 +16,11 @@ PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h)
|
|||
sheet_height = (size.y / sprite_height);
|
||||
if (size.x % sprite_width != 0 || size.y % sprite_height != 0)
|
||||
{
|
||||
std::cout << "Warning: Texture `" << source << "` is not an even number of sprite widths or heights across." << std::endl
|
||||
std::cout << "Warning: Texture `" << source << "` is not an even number of sprite widths or heights across." << std::endl
|
||||
<< "Sprite size given was " << sprite_w << "x" << sprite_h << "px but the file has a resolution of " << sheet_width << "x" << sheet_height << "px." << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// #144: Factory method to create texture from rendered content (snapshot)
|
||||
std::shared_ptr<PyTexture> PyTexture::from_rendered(sf::RenderTexture& render_tex)
|
||||
{
|
||||
// Use a custom shared_ptr construction to access private default constructor
|
||||
struct MakeSharedEnabler : public PyTexture {
|
||||
MakeSharedEnabler() : PyTexture() {}
|
||||
};
|
||||
auto ptex = std::make_shared<MakeSharedEnabler>();
|
||||
|
||||
// Copy the rendered texture data
|
||||
ptex->texture = render_tex.getTexture();
|
||||
ptex->texture.setSmooth(false); // Maintain pixel art aesthetic
|
||||
|
||||
// Set source to indicate this is a snapshot
|
||||
ptex->source = "<snapshot>";
|
||||
|
||||
// Treat entire texture as single sprite
|
||||
auto size = ptex->texture.getSize();
|
||||
ptex->sprite_width = size.x;
|
||||
ptex->sprite_height = size.y;
|
||||
ptex->sheet_width = 1;
|
||||
ptex->sheet_height = 1;
|
||||
|
||||
return ptex;
|
||||
}
|
||||
|
||||
sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s)
|
||||
{
|
||||
// Protect against division by zero if texture failed to load
|
||||
|
|
@ -158,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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,16 +15,9 @@ private:
|
|||
sf::Texture texture;
|
||||
std::string source;
|
||||
int sheet_width, sheet_height;
|
||||
|
||||
// Private default constructor for factory methods
|
||||
PyTexture() : source("<uninitialized>"), sprite_width(0), sprite_height(0), sheet_width(0), sheet_height(0) {}
|
||||
|
||||
public:
|
||||
int sprite_width, sprite_height; // just use them read only, OK?
|
||||
PyTexture(std::string filename, int sprite_w, int sprite_h);
|
||||
|
||||
// #144: Factory method to create texture from rendered content (snapshot)
|
||||
static std::shared_ptr<PyTexture> from_rendered(sf::RenderTexture& render_tex);
|
||||
sf::Sprite sprite(int index, sf::Vector2f pos = sf::Vector2f(0, 0), sf::Vector2f s = sf::Vector2f(1.0, 1.0));
|
||||
int getSpriteCount() const { return sheet_width * sheet_height; }
|
||||
|
||||
|
|
|
|||
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,
|
||||
|
|
|
|||
228
src/PyVector.cpp
228
src/PyVector.cpp
|
|
@ -1,75 +1,21 @@
|
|||
#include "PyVector.h"
|
||||
#include "PyObjectUtils.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include "PyRAII.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},
|
||||
{"int", (getter)PyVector::get_int, NULL,
|
||||
MCRF_PROPERTY(int, "Integer tuple (floor of x and y) for use as dict keys. Read-only."), NULL},
|
||||
{"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")
|
||||
)},
|
||||
{"floor", (PyCFunction)PyVector::floor, METH_NOARGS,
|
||||
MCRF_METHOD(Vector, floor,
|
||||
MCRF_SIG("()", "Vector"),
|
||||
MCRF_DESC("Return a new vector with floored (integer) coordinates."),
|
||||
MCRF_RETURNS("Vector: New Vector with floor(x) and floor(y)")
|
||||
MCRF_NOTE("Useful for grid-based positioning. For a hashable tuple, use the .int property instead.")
|
||||
)},
|
||||
{"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}
|
||||
};
|
||||
|
||||
|
|
@ -112,19 +58,6 @@ namespace mcrfpydef {
|
|||
.nb_matrix_multiply = 0,
|
||||
.nb_inplace_matrix_multiply = 0
|
||||
};
|
||||
|
||||
PySequenceMethods PyVector_as_sequence = {
|
||||
.sq_length = PyVector::sequence_length,
|
||||
.sq_concat = 0,
|
||||
.sq_repeat = 0,
|
||||
.sq_item = PyVector::sequence_item,
|
||||
.was_sq_slice = 0,
|
||||
.sq_ass_item = 0,
|
||||
.was_sq_ass_slice = 0,
|
||||
.sq_contains = 0,
|
||||
.sq_inplace_concat = 0,
|
||||
.sq_inplace_repeat = 0
|
||||
};
|
||||
}
|
||||
|
||||
PyVector::PyVector(sf::Vector2f target)
|
||||
|
|
@ -262,46 +195,35 @@ int PyVector::set_member(PyObject* obj, PyObject* value, void* closure)
|
|||
|
||||
PyVectorObject* PyVector::from_arg(PyObject* args)
|
||||
{
|
||||
// Use RAII for type reference management
|
||||
PyRAII::PyTypeRef type("Vector", McRFPy_API::mcrf_module);
|
||||
if (!type) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Check if args is already a Vector instance
|
||||
if (PyObject_IsInstance(args, (PyObject*)type.get())) {
|
||||
Py_INCREF(args); // Return new reference so caller can safely DECREF
|
||||
return (PyVectorObject*)args;
|
||||
}
|
||||
|
||||
// Create new Vector object using RAII
|
||||
PyRAII::PyObjectRef obj(type->tp_alloc(type.get(), 0), true);
|
||||
if (!obj) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (PyObject_IsInstance(args, (PyObject*)type)) return (PyVectorObject*)args;
|
||||
|
||||
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
|
||||
|
||||
// Handle different input types
|
||||
if (PyTuple_Check(args)) {
|
||||
// It's already a tuple, pass it directly to init
|
||||
int err = init((PyVectorObject*)obj.get(), args, NULL);
|
||||
int err = init(obj, args, NULL);
|
||||
if (err) {
|
||||
// obj will be automatically cleaned up when it goes out of scope
|
||||
Py_DECREF(obj);
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
// Wrap single argument in a tuple for init
|
||||
PyRAII::PyObjectRef tuple(PyTuple_Pack(1, args), true);
|
||||
PyObject* tuple = PyTuple_Pack(1, args);
|
||||
if (!tuple) {
|
||||
Py_DECREF(obj);
|
||||
return NULL;
|
||||
}
|
||||
int err = init((PyVectorObject*)obj.get(), tuple.get(), NULL);
|
||||
int err = init(obj, tuple, NULL);
|
||||
Py_DECREF(tuple);
|
||||
if (err) {
|
||||
Py_DECREF(obj);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Release ownership and return
|
||||
return (PyVectorObject*)obj.release();
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Arithmetic operations
|
||||
|
|
@ -431,65 +353,29 @@ int PyVector::bool_check(PyObject* self)
|
|||
PyObject* PyVector::richcompare(PyObject* left, PyObject* right, int op)
|
||||
{
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
|
||||
float left_x, left_y, right_x, right_y;
|
||||
|
||||
// Extract left operand values
|
||||
if (PyObject_IsInstance(left, (PyObject*)type)) {
|
||||
PyVectorObject* vec = (PyVectorObject*)left;
|
||||
left_x = vec->data.x;
|
||||
left_y = vec->data.y;
|
||||
} else if (PyTuple_Check(left) && PyTuple_Size(left) == 2) {
|
||||
PyObject* x_obj = PyTuple_GetItem(left, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(left, 1);
|
||||
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
|
||||
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
|
||||
left_x = (float)PyFloat_AsDouble(x_obj);
|
||||
left_y = (float)PyFloat_AsDouble(y_obj);
|
||||
} else {
|
||||
Py_INCREF(Py_NotImplemented);
|
||||
return Py_NotImplemented;
|
||||
}
|
||||
} else {
|
||||
|
||||
if (!PyObject_IsInstance(left, (PyObject*)type) || !PyObject_IsInstance(right, (PyObject*)type)) {
|
||||
Py_INCREF(Py_NotImplemented);
|
||||
return Py_NotImplemented;
|
||||
}
|
||||
|
||||
// Extract right operand values
|
||||
if (PyObject_IsInstance(right, (PyObject*)type)) {
|
||||
PyVectorObject* vec = (PyVectorObject*)right;
|
||||
right_x = vec->data.x;
|
||||
right_y = vec->data.y;
|
||||
} else if (PyTuple_Check(right) && PyTuple_Size(right) == 2) {
|
||||
PyObject* x_obj = PyTuple_GetItem(right, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(right, 1);
|
||||
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
|
||||
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
|
||||
right_x = (float)PyFloat_AsDouble(x_obj);
|
||||
right_y = (float)PyFloat_AsDouble(y_obj);
|
||||
} else {
|
||||
Py_INCREF(Py_NotImplemented);
|
||||
return Py_NotImplemented;
|
||||
}
|
||||
} else {
|
||||
Py_INCREF(Py_NotImplemented);
|
||||
return Py_NotImplemented;
|
||||
}
|
||||
|
||||
|
||||
PyVectorObject* vec1 = (PyVectorObject*)left;
|
||||
PyVectorObject* vec2 = (PyVectorObject*)right;
|
||||
|
||||
bool result = false;
|
||||
|
||||
|
||||
switch (op) {
|
||||
case Py_EQ:
|
||||
result = (left_x == right_x && left_y == right_y);
|
||||
result = (vec1->data.x == vec2->data.x && vec1->data.y == vec2->data.y);
|
||||
break;
|
||||
case Py_NE:
|
||||
result = (left_x != right_x || left_y != right_y);
|
||||
result = (vec1->data.x != vec2->data.x || vec1->data.y != vec2->data.y);
|
||||
break;
|
||||
default:
|
||||
Py_INCREF(Py_NotImplemented);
|
||||
return Py_NotImplemented;
|
||||
}
|
||||
|
||||
|
||||
if (result)
|
||||
Py_RETURN_TRUE;
|
||||
else
|
||||
|
|
@ -570,54 +456,10 @@ PyObject* PyVector::copy(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
|
|||
{
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
|
||||
|
||||
|
||||
if (result) {
|
||||
result->data = self->data;
|
||||
}
|
||||
|
||||
|
||||
return (PyObject*)result;
|
||||
}
|
||||
|
||||
PyObject* PyVector::floor(PyVectorObject* self, PyObject* Py_UNUSED(ignored))
|
||||
{
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
auto result = (PyVectorObject*)type->tp_alloc(type, 0);
|
||||
|
||||
if (result) {
|
||||
result->data = sf::Vector2f(std::floor(self->data.x), std::floor(self->data.y));
|
||||
}
|
||||
|
||||
return (PyObject*)result;
|
||||
}
|
||||
|
||||
// Sequence protocol implementation
|
||||
Py_ssize_t PyVector::sequence_length(PyObject* self)
|
||||
{
|
||||
return 2; // Vectors always have exactly 2 elements
|
||||
}
|
||||
|
||||
PyObject* PyVector::sequence_item(PyObject* obj, Py_ssize_t index)
|
||||
{
|
||||
PyVectorObject* self = (PyVectorObject*)obj;
|
||||
|
||||
// Note: Python already handles negative index normalization when sq_length is defined
|
||||
// So v[-1] arrives here as index=1, v[-2] as index=0
|
||||
// Out-of-range negative indices (like v[-3]) arrive as negative values (e.g., -1)
|
||||
if (index == 0) {
|
||||
return PyFloat_FromDouble(self->data.x);
|
||||
} else if (index == 1) {
|
||||
return PyFloat_FromDouble(self->data.y);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_IndexError, "Vector index out of range (must be 0 or 1)");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Property: .int - returns integer tuple for use as dict keys
|
||||
PyObject* PyVector::get_int(PyObject* obj, void* closure)
|
||||
{
|
||||
PyVectorObject* self = (PyVectorObject*)obj;
|
||||
long ix = (long)std::floor(self->data.x);
|
||||
long iy = (long)std::floor(self->data.y);
|
||||
return Py_BuildValue("(ll)", ix, iy);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,24 +45,15 @@ public:
|
|||
static PyObject* distance_to(PyVectorObject*, PyObject*);
|
||||
static PyObject* angle(PyVectorObject*, PyObject*);
|
||||
static PyObject* copy(PyVectorObject*, PyObject*);
|
||||
static PyObject* floor(PyVectorObject*, PyObject*);
|
||||
|
||||
// Sequence protocol
|
||||
static Py_ssize_t sequence_length(PyObject*);
|
||||
static PyObject* sequence_item(PyObject*, Py_ssize_t);
|
||||
|
||||
// Additional properties
|
||||
static PyObject* get_int(PyObject*, void*);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyMethodDef methods[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
// Forward declare the PyNumberMethods and PySequenceMethods structures
|
||||
// Forward declare the PyNumberMethods structure
|
||||
extern PyNumberMethods PyVector_as_number;
|
||||
extern PySequenceMethods PyVector_as_sequence;
|
||||
|
||||
|
||||
static PyTypeObject PyVectorType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Vector",
|
||||
|
|
@ -70,7 +61,6 @@ namespace mcrfpydef {
|
|||
.tp_itemsize = 0,
|
||||
.tp_repr = PyVector::repr,
|
||||
.tp_as_number = &PyVector_as_number,
|
||||
.tp_as_sequence = &PyVector_as_sequence,
|
||||
.tp_hash = PyVector::hash,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("SFML Vector Object"),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
@ -54,43 +54,3 @@ void Scene::key_unregister()
|
|||
*/
|
||||
key_callable.reset();
|
||||
}
|
||||
|
||||
// #118: Scene animation property support
|
||||
bool Scene::setProperty(const std::string& name, float value)
|
||||
{
|
||||
if (name == "x") {
|
||||
position.x = value;
|
||||
return true;
|
||||
}
|
||||
if (name == "y") {
|
||||
position.y = value;
|
||||
return true;
|
||||
}
|
||||
if (name == "opacity") {
|
||||
opacity = std::max(0.0f, std::min(1.0f, value));
|
||||
return true;
|
||||
}
|
||||
if (name == "visible") {
|
||||
visible = (value != 0.0f);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Scene::setProperty(const std::string& name, const sf::Vector2f& value)
|
||||
{
|
||||
if (name == "pos" || name == "position") {
|
||||
position = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
float Scene::getProperty(const std::string& name) const
|
||||
{
|
||||
if (name == "x") return position.x;
|
||||
if (name == "y") return position.y;
|
||||
if (name == "opacity") return opacity;
|
||||
if (name == "visible") return visible ? 1.0f : 0.0f;
|
||||
return 0.0f;
|
||||
}
|
||||
|
|
|
|||
14
src/Scene.h
14
src/Scene.h
|
|
@ -35,22 +35,12 @@ public:
|
|||
bool hasAction(std::string);
|
||||
bool hasAction(int);
|
||||
std::string action(int);
|
||||
|
||||
|
||||
|
||||
|
||||
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> ui_elements;
|
||||
|
||||
//PyObject* key_callable;
|
||||
std::unique_ptr<PyKeyCallable> key_callable;
|
||||
void key_register(PyObject*);
|
||||
void key_unregister();
|
||||
|
||||
// #118: Scene-level UIDrawable-like properties for animations/transitions
|
||||
sf::Vector2f position{0.0f, 0.0f}; // Offset applied to all ui_elements
|
||||
bool visible = true; // Controls rendering of scene
|
||||
float opacity = 1.0f; // Applied to all ui_elements (0.0-1.0)
|
||||
|
||||
// Animation support for scene properties
|
||||
bool setProperty(const std::string& name, float value);
|
||||
bool setProperty(const std::string& name, const sf::Vector2f& value);
|
||||
float getProperty(const std::string& name) const;
|
||||
};
|
||||
|
|
|
|||
136
src/Timer.cpp
136
src/Timer.cpp
|
|
@ -1,147 +1,31 @@
|
|||
#include "Timer.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "PyCallable.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "GameEngine.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::cerr << "Timer callback raised an exception:" << std::endl;
|
||||
{
|
||||
std::cout << "timer has raised an exception. It's going to STDERR and being dropped:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
|
||||
// Check if we should exit on exception
|
||||
if (McRFPy_API::game && McRFPy_API::game->getConfig().exit_on_exception) {
|
||||
McRFPy_API::signalPythonException();
|
||||
}
|
||||
} 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);
|
||||
};
|
||||
|
|
|
|||
529
src/UIArc.cpp
529
src/UIArc.cpp
|
|
@ -1,529 +0,0 @@
|
|||
#include "UIArc.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
UIArc::UIArc()
|
||||
: center(0.0f, 0.0f), radius(0.0f), start_angle(0.0f), end_angle(90.0f),
|
||||
color(sf::Color::White), thickness(1.0f), vertices_dirty(true)
|
||||
{
|
||||
position = center;
|
||||
}
|
||||
|
||||
UIArc::UIArc(sf::Vector2f center, float radius, float startAngle, float endAngle,
|
||||
sf::Color color, float thickness)
|
||||
: center(center), radius(radius), start_angle(startAngle), end_angle(endAngle),
|
||||
color(color), thickness(thickness), vertices_dirty(true)
|
||||
{
|
||||
position = center;
|
||||
}
|
||||
|
||||
UIArc::UIArc(const UIArc& other)
|
||||
: UIDrawable(other),
|
||||
center(other.center),
|
||||
radius(other.radius),
|
||||
start_angle(other.start_angle),
|
||||
end_angle(other.end_angle),
|
||||
color(other.color),
|
||||
thickness(other.thickness),
|
||||
vertices_dirty(true)
|
||||
{
|
||||
}
|
||||
|
||||
UIArc& UIArc::operator=(const UIArc& other) {
|
||||
if (this != &other) {
|
||||
UIDrawable::operator=(other);
|
||||
center = other.center;
|
||||
radius = other.radius;
|
||||
start_angle = other.start_angle;
|
||||
end_angle = other.end_angle;
|
||||
color = other.color;
|
||||
thickness = other.thickness;
|
||||
vertices_dirty = true;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
UIArc::UIArc(UIArc&& other) noexcept
|
||||
: UIDrawable(std::move(other)),
|
||||
center(other.center),
|
||||
radius(other.radius),
|
||||
start_angle(other.start_angle),
|
||||
end_angle(other.end_angle),
|
||||
color(other.color),
|
||||
thickness(other.thickness),
|
||||
vertices(std::move(other.vertices)),
|
||||
vertices_dirty(other.vertices_dirty)
|
||||
{
|
||||
}
|
||||
|
||||
UIArc& UIArc::operator=(UIArc&& other) noexcept {
|
||||
if (this != &other) {
|
||||
UIDrawable::operator=(std::move(other));
|
||||
center = other.center;
|
||||
radius = other.radius;
|
||||
start_angle = other.start_angle;
|
||||
end_angle = other.end_angle;
|
||||
color = other.color;
|
||||
thickness = other.thickness;
|
||||
vertices = std::move(other.vertices);
|
||||
vertices_dirty = other.vertices_dirty;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void UIArc::rebuildVertices() {
|
||||
vertices.clear();
|
||||
vertices.setPrimitiveType(sf::TriangleStrip);
|
||||
|
||||
// Calculate the arc parameters
|
||||
float inner_radius = radius - thickness / 2.0f;
|
||||
float outer_radius = radius + thickness / 2.0f;
|
||||
|
||||
if (inner_radius < 0) inner_radius = 0;
|
||||
|
||||
// Normalize angles
|
||||
float start_rad = start_angle * M_PI / 180.0f;
|
||||
float end_rad = end_angle * M_PI / 180.0f;
|
||||
|
||||
// Calculate number of segments based on arc length
|
||||
float angle_span = end_rad - start_rad;
|
||||
int num_segments = std::max(3, static_cast<int>(std::abs(angle_span * radius) / 5.0f));
|
||||
num_segments = std::min(num_segments, 100); // Cap at 100 segments
|
||||
|
||||
float angle_step = angle_span / num_segments;
|
||||
|
||||
// Apply opacity to color
|
||||
sf::Color render_color = color;
|
||||
render_color.a = static_cast<sf::Uint8>(render_color.a * opacity);
|
||||
|
||||
// Build the triangle strip
|
||||
for (int i = 0; i <= num_segments; ++i) {
|
||||
float angle = start_rad + i * angle_step;
|
||||
float cos_a = std::cos(angle);
|
||||
float sin_a = std::sin(angle);
|
||||
|
||||
// Inner vertex
|
||||
sf::Vector2f inner_pos(
|
||||
center.x + inner_radius * cos_a,
|
||||
center.y + inner_radius * sin_a
|
||||
);
|
||||
vertices.append(sf::Vertex(inner_pos, render_color));
|
||||
|
||||
// Outer vertex
|
||||
sf::Vector2f outer_pos(
|
||||
center.x + outer_radius * cos_a,
|
||||
center.y + outer_radius * sin_a
|
||||
);
|
||||
vertices.append(sf::Vertex(outer_pos, render_color));
|
||||
}
|
||||
|
||||
vertices_dirty = false;
|
||||
}
|
||||
|
||||
void UIArc::render(sf::Vector2f offset, sf::RenderTarget& target) {
|
||||
if (!visible) return;
|
||||
|
||||
if (vertices_dirty) {
|
||||
rebuildVertices();
|
||||
}
|
||||
|
||||
// Apply offset by creating a transformed copy
|
||||
sf::Transform transform;
|
||||
transform.translate(offset);
|
||||
target.draw(vertices, transform);
|
||||
}
|
||||
|
||||
UIDrawable* UIArc::click_at(sf::Vector2f point) {
|
||||
if (!visible) return nullptr;
|
||||
|
||||
// Calculate distance from center
|
||||
float dx = point.x - center.x;
|
||||
float dy = point.y - center.y;
|
||||
float dist = std::sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Check if within the arc's radial range
|
||||
float inner_radius = radius - thickness / 2.0f;
|
||||
float outer_radius = radius + thickness / 2.0f;
|
||||
if (inner_radius < 0) inner_radius = 0;
|
||||
|
||||
if (dist < inner_radius || dist > outer_radius) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check if within the angle range
|
||||
float angle = std::atan2(dy, dx) * 180.0f / M_PI;
|
||||
|
||||
// Normalize angle to match start/end angle range
|
||||
float start = start_angle;
|
||||
float end = end_angle;
|
||||
|
||||
// Handle angle wrapping
|
||||
while (angle < start - 180.0f) angle += 360.0f;
|
||||
while (angle > start + 180.0f) angle -= 360.0f;
|
||||
|
||||
if ((start <= end && angle >= start && angle <= end) ||
|
||||
(start > end && (angle >= start || angle <= end))) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObjectsEnum UIArc::derived_type() {
|
||||
return PyObjectsEnum::UIARC;
|
||||
}
|
||||
|
||||
sf::FloatRect UIArc::get_bounds() const {
|
||||
float outer_radius = radius + thickness / 2.0f;
|
||||
return sf::FloatRect(
|
||||
center.x - outer_radius,
|
||||
center.y - outer_radius,
|
||||
outer_radius * 2,
|
||||
outer_radius * 2
|
||||
);
|
||||
}
|
||||
|
||||
void UIArc::move(float dx, float dy) {
|
||||
center.x += dx;
|
||||
center.y += dy;
|
||||
position = center;
|
||||
vertices_dirty = true;
|
||||
}
|
||||
|
||||
void UIArc::resize(float w, float h) {
|
||||
// Resize by adjusting radius to fit in the given dimensions
|
||||
radius = std::min(w, h) / 2.0f - thickness / 2.0f;
|
||||
if (radius < 0) radius = 0;
|
||||
vertices_dirty = true;
|
||||
}
|
||||
|
||||
// Property setters
|
||||
bool UIArc::setProperty(const std::string& name, float value) {
|
||||
if (name == "radius") {
|
||||
setRadius(value);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "start_angle") {
|
||||
setStartAngle(value);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "end_angle") {
|
||||
setEndAngle(value);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "thickness") {
|
||||
setThickness(value);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "x") {
|
||||
center.x = value;
|
||||
position = center;
|
||||
vertices_dirty = true;
|
||||
markCompositeDirty(); // #144 - Position change, texture still valid
|
||||
return true;
|
||||
}
|
||||
else if (name == "y") {
|
||||
center.y = value;
|
||||
position = center;
|
||||
vertices_dirty = true;
|
||||
markCompositeDirty(); // #144 - Position change, texture still valid
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UIArc::setProperty(const std::string& name, const sf::Color& value) {
|
||||
if (name == "color") {
|
||||
setColor(value);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UIArc::setProperty(const std::string& name, const sf::Vector2f& value) {
|
||||
if (name == "center") {
|
||||
setCenter(value);
|
||||
markCompositeDirty(); // #144 - Position change, texture still valid
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UIArc::getProperty(const std::string& name, float& value) const {
|
||||
if (name == "radius") {
|
||||
value = radius;
|
||||
return true;
|
||||
}
|
||||
else if (name == "start_angle") {
|
||||
value = start_angle;
|
||||
return true;
|
||||
}
|
||||
else if (name == "end_angle") {
|
||||
value = end_angle;
|
||||
return true;
|
||||
}
|
||||
else if (name == "thickness") {
|
||||
value = thickness;
|
||||
return true;
|
||||
}
|
||||
else if (name == "x") {
|
||||
value = center.x;
|
||||
return true;
|
||||
}
|
||||
else if (name == "y") {
|
||||
value = center.y;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UIArc::getProperty(const std::string& name, sf::Color& value) const {
|
||||
if (name == "color") {
|
||||
value = color;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UIArc::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||
if (name == "center") {
|
||||
value = center;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Python API implementation
|
||||
PyObject* UIArc::get_center(PyUIArcObject* self, void* closure) {
|
||||
auto center = self->data->getCenter();
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (!type) return NULL;
|
||||
PyObject* result = PyObject_CallFunction((PyObject*)type, "ff", center.x, center.y);
|
||||
Py_DECREF(type);
|
||||
return result;
|
||||
}
|
||||
|
||||
int UIArc::set_center(PyUIArcObject* self, PyObject* value, void* closure) {
|
||||
PyVectorObject* vec = PyVector::from_arg(value);
|
||||
if (!vec) {
|
||||
PyErr_Clear();
|
||||
PyErr_SetString(PyExc_TypeError, "center must be a Vector or tuple (x, y)");
|
||||
return -1;
|
||||
}
|
||||
self->data->setCenter(vec->data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIArc::get_radius(PyUIArcObject* self, void* closure) {
|
||||
return PyFloat_FromDouble(self->data->getRadius());
|
||||
}
|
||||
|
||||
int UIArc::set_radius(PyUIArcObject* self, PyObject* value, void* closure) {
|
||||
if (!PyNumber_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "radius must be a number");
|
||||
return -1;
|
||||
}
|
||||
self->data->setRadius(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIArc::get_start_angle(PyUIArcObject* self, void* closure) {
|
||||
return PyFloat_FromDouble(self->data->getStartAngle());
|
||||
}
|
||||
|
||||
int UIArc::set_start_angle(PyUIArcObject* self, PyObject* value, void* closure) {
|
||||
if (!PyNumber_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "start_angle must be a number");
|
||||
return -1;
|
||||
}
|
||||
self->data->setStartAngle(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIArc::get_end_angle(PyUIArcObject* self, void* closure) {
|
||||
return PyFloat_FromDouble(self->data->getEndAngle());
|
||||
}
|
||||
|
||||
int UIArc::set_end_angle(PyUIArcObject* self, PyObject* value, void* closure) {
|
||||
if (!PyNumber_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "end_angle must be a number");
|
||||
return -1;
|
||||
}
|
||||
self->data->setEndAngle(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIArc::get_color(PyUIArcObject* self, void* closure) {
|
||||
auto color = self->data->getColor();
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
|
||||
PyObject* args = Py_BuildValue("(iiii)", color.r, color.g, color.b, color.a);
|
||||
PyObject* obj = PyObject_CallObject((PyObject*)type, args);
|
||||
Py_DECREF(args);
|
||||
Py_DECREF(type);
|
||||
return obj;
|
||||
}
|
||||
|
||||
int UIArc::set_color(PyUIArcObject* self, PyObject* value, void* closure) {
|
||||
auto color = PyColor::from_arg(value);
|
||||
if (!color) {
|
||||
PyErr_SetString(PyExc_TypeError, "color must be a Color or tuple (r, g, b) or (r, g, b, a)");
|
||||
return -1;
|
||||
}
|
||||
self->data->setColor(color->data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIArc::get_thickness(PyUIArcObject* self, void* closure) {
|
||||
return PyFloat_FromDouble(self->data->getThickness());
|
||||
}
|
||||
|
||||
int UIArc::set_thickness(PyUIArcObject* self, PyObject* value, void* closure) {
|
||||
if (!PyNumber_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "thickness must be a number");
|
||||
return -1;
|
||||
}
|
||||
self->data->setThickness(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Required typedef for UIDRAWABLE_GETSETTERS and UIDRAWABLE_METHODS macro templates
|
||||
typedef PyUIArcObject PyObjectType;
|
||||
|
||||
PyGetSetDef UIArc::getsetters[] = {
|
||||
{"center", (getter)UIArc::get_center, (setter)UIArc::set_center,
|
||||
"Center position of the arc", NULL},
|
||||
{"radius", (getter)UIArc::get_radius, (setter)UIArc::set_radius,
|
||||
"Arc radius in pixels", NULL},
|
||||
{"start_angle", (getter)UIArc::get_start_angle, (setter)UIArc::set_start_angle,
|
||||
"Starting angle in degrees", NULL},
|
||||
{"end_angle", (getter)UIArc::get_end_angle, (setter)UIArc::set_end_angle,
|
||||
"Ending angle in degrees", NULL},
|
||||
{"color", (getter)UIArc::get_color, (setter)UIArc::set_color,
|
||||
"Arc color", NULL},
|
||||
{"thickness", (getter)UIArc::get_thickness, (setter)UIArc::set_thickness,
|
||||
"Line thickness", NULL},
|
||||
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||
"Callable executed when arc is clicked.", (void*)PyObjectsEnum::UIARC},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
||||
"Z-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UIARC},
|
||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name,
|
||||
"Name for finding this element.", (void*)PyObjectsEnum::UIARC},
|
||||
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos,
|
||||
"Position as a Vector (same as center).", (void*)PyObjectsEnum::UIARC},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIARC),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyMethodDef UIArc_methods[] = {
|
||||
UIDRAWABLE_METHODS,
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyObject* UIArc::repr(PyUIArcObject* self) {
|
||||
std::ostringstream oss;
|
||||
if (!self->data) {
|
||||
oss << "<Arc (invalid internal object)>";
|
||||
} else {
|
||||
auto center = self->data->getCenter();
|
||||
auto color = self->data->getColor();
|
||||
oss << "<Arc center=(" << center.x << ", " << center.y << ") "
|
||||
<< "radius=" << self->data->getRadius() << " "
|
||||
<< "angles=(" << self->data->getStartAngle() << ", " << self->data->getEndAngle() << ") "
|
||||
<< "color=(" << (int)color.r << ", " << (int)color.g << ", "
|
||||
<< (int)color.b << ", " << (int)color.a << ")>";
|
||||
}
|
||||
std::string repr_str = oss.str();
|
||||
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
||||
}
|
||||
|
||||
int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) {
|
||||
// Arguments
|
||||
PyObject* center_obj = nullptr;
|
||||
float radius = 0.0f;
|
||||
float start_angle = 0.0f;
|
||||
float end_angle = 90.0f;
|
||||
PyObject* color_obj = nullptr;
|
||||
float thickness = 1.0f;
|
||||
PyObject* click_handler = nullptr;
|
||||
int visible = 1;
|
||||
float opacity = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = nullptr;
|
||||
|
||||
static const char* kwlist[] = {
|
||||
"center", "radius", "start_angle", "end_angle", "color", "thickness",
|
||||
"click", "visible", "opacity", "z_index", "name",
|
||||
nullptr
|
||||
};
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifiz", const_cast<char**>(kwlist),
|
||||
¢er_obj, &radius, &start_angle, &end_angle,
|
||||
&color_obj, &thickness,
|
||||
&click_handler, &visible, &opacity, &z_index, &name)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Parse center position
|
||||
sf::Vector2f center(0.0f, 0.0f);
|
||||
if (center_obj) {
|
||||
PyVectorObject* vec = PyVector::from_arg(center_obj);
|
||||
if (vec) {
|
||||
center = vec->data;
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
PyErr_SetString(PyExc_TypeError, "center must be a Vector or tuple (x, y)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse color
|
||||
sf::Color color = sf::Color::White;
|
||||
if (color_obj) {
|
||||
auto pycolor = PyColor::from_arg(color_obj);
|
||||
if (pycolor) {
|
||||
color = pycolor->data;
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
PyErr_SetString(PyExc_TypeError, "color must be a Color or tuple (r, g, b) or (r, g, b, a)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Set values
|
||||
self->data->setCenter(center);
|
||||
self->data->setRadius(radius);
|
||||
self->data->setStartAngle(start_angle);
|
||||
self->data->setEndAngle(end_angle);
|
||||
self->data->setColor(color);
|
||||
self->data->setThickness(thickness);
|
||||
|
||||
// Handle common UIDrawable properties
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
if (!PyCallable_Check(click_handler)) {
|
||||
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
||||
return -1;
|
||||
}
|
||||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
self->data->visible = (visible != 0);
|
||||
self->data->opacity = opacity;
|
||||
self->data->z_index = z_index;
|
||||
|
||||
if (name) {
|
||||
self->data->name = name;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
167
src/UIArc.h
167
src/UIArc.h
|
|
@ -1,167 +0,0 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include "structmember.h"
|
||||
#include "UIDrawable.h"
|
||||
#include "UIBase.h"
|
||||
#include "PyDrawable.h"
|
||||
#include "PyColor.h"
|
||||
#include "PyVector.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
|
||||
// Forward declaration
|
||||
class UIArc;
|
||||
|
||||
// Python object structure
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UIArc> data;
|
||||
PyObject* weakreflist;
|
||||
} PyUIArcObject;
|
||||
|
||||
class UIArc : public UIDrawable
|
||||
{
|
||||
private:
|
||||
sf::Vector2f center;
|
||||
float radius;
|
||||
float start_angle; // in degrees
|
||||
float end_angle; // in degrees
|
||||
sf::Color color;
|
||||
float thickness;
|
||||
|
||||
// Cached vertex array for rendering
|
||||
sf::VertexArray vertices;
|
||||
bool vertices_dirty;
|
||||
|
||||
void rebuildVertices();
|
||||
|
||||
public:
|
||||
UIArc();
|
||||
UIArc(sf::Vector2f center, float radius, float startAngle, float endAngle,
|
||||
sf::Color color = sf::Color::White, float thickness = 1.0f);
|
||||
|
||||
// Copy constructor and assignment
|
||||
UIArc(const UIArc& other);
|
||||
UIArc& operator=(const UIArc& other);
|
||||
|
||||
// Move constructor and assignment
|
||||
UIArc(UIArc&& other) noexcept;
|
||||
UIArc& operator=(UIArc&& other) noexcept;
|
||||
|
||||
// UIDrawable interface
|
||||
void render(sf::Vector2f offset, sf::RenderTarget& target) override;
|
||||
UIDrawable* click_at(sf::Vector2f point) override;
|
||||
PyObjectsEnum derived_type() override;
|
||||
|
||||
// Getters and setters
|
||||
sf::Vector2f getCenter() const { return center; }
|
||||
void setCenter(sf::Vector2f c) { center = c; position = c; vertices_dirty = true; }
|
||||
|
||||
float getRadius() const { return radius; }
|
||||
void setRadius(float r) { radius = r; vertices_dirty = true; }
|
||||
|
||||
float getStartAngle() const { return start_angle; }
|
||||
void setStartAngle(float a) { start_angle = a; vertices_dirty = true; }
|
||||
|
||||
float getEndAngle() const { return end_angle; }
|
||||
void setEndAngle(float a) { end_angle = a; vertices_dirty = true; }
|
||||
|
||||
sf::Color getColor() const { return color; }
|
||||
void setColor(sf::Color c) { color = c; vertices_dirty = true; }
|
||||
|
||||
float getThickness() const { return thickness; }
|
||||
void setThickness(float t) { thickness = t; vertices_dirty = true; }
|
||||
|
||||
// Phase 1 virtual method implementations
|
||||
sf::FloatRect get_bounds() const override;
|
||||
void move(float dx, float dy) override;
|
||||
void resize(float w, float h) override;
|
||||
|
||||
// Property system for animations
|
||||
bool setProperty(const std::string& name, float value) override;
|
||||
bool setProperty(const std::string& name, const sf::Color& value) override;
|
||||
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
|
||||
bool getProperty(const std::string& name, float& value) const override;
|
||||
bool getProperty(const std::string& name, sf::Color& value) const override;
|
||||
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
||||
|
||||
// Python API
|
||||
static PyObject* get_center(PyUIArcObject* self, void* closure);
|
||||
static int set_center(PyUIArcObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_radius(PyUIArcObject* self, void* closure);
|
||||
static int set_radius(PyUIArcObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_start_angle(PyUIArcObject* self, void* closure);
|
||||
static int set_start_angle(PyUIArcObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_end_angle(PyUIArcObject* self, void* closure);
|
||||
static int set_end_angle(PyUIArcObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_color(PyUIArcObject* self, void* closure);
|
||||
static int set_color(PyUIArcObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_thickness(PyUIArcObject* self, void* closure);
|
||||
static int set_thickness(PyUIArcObject* self, PyObject* value, void* closure);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyObject* repr(PyUIArcObject* self);
|
||||
static int init(PyUIArcObject* self, PyObject* args, PyObject* kwds);
|
||||
};
|
||||
|
||||
// Method definitions
|
||||
extern PyMethodDef UIArc_methods[];
|
||||
|
||||
namespace mcrfpydef {
|
||||
static PyTypeObject PyUIArcType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Arc",
|
||||
.tp_basicsize = sizeof(PyUIArcObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)[](PyObject* self) {
|
||||
PyUIArcObject* obj = (PyUIArcObject*)self;
|
||||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
obj->data.reset();
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
.tp_repr = (reprfunc)UIArc::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_doc = PyDoc_STR(
|
||||
"Arc(center=None, radius=0, start_angle=0, end_angle=90, color=None, thickness=1, **kwargs)\n\n"
|
||||
"An arc UI element for drawing curved line segments.\n\n"
|
||||
"Args:\n"
|
||||
" center (tuple, optional): Center position as (x, y). Default: (0, 0)\n"
|
||||
" radius (float, optional): Arc radius in pixels. Default: 0\n"
|
||||
" start_angle (float, optional): Starting angle in degrees. Default: 0\n"
|
||||
" end_angle (float, optional): Ending angle in degrees. Default: 90\n"
|
||||
" color (Color, optional): Arc color. Default: White\n"
|
||||
" thickness (float, optional): Line thickness. Default: 1.0\n\n"
|
||||
"Keyword Args:\n"
|
||||
" click (callable): Click handler. Default: None\n"
|
||||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n\n"
|
||||
"Attributes:\n"
|
||||
" center (Vector): Center position\n"
|
||||
" radius (float): Arc radius\n"
|
||||
" start_angle (float): Starting angle in degrees\n"
|
||||
" end_angle (float): Ending angle in degrees\n"
|
||||
" color (Color): Arc color\n"
|
||||
" thickness (float): Line thickness\n"
|
||||
" visible (bool): Visibility state\n"
|
||||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
),
|
||||
.tp_methods = UIArc_methods,
|
||||
.tp_getset = UIArc::getsetters,
|
||||
.tp_base = &mcrfpydef::PyDrawableType,
|
||||
.tp_init = (initproc)UIArc::init,
|
||||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
|
||||
PyUIArcObject* self = (PyUIArcObject*)type->tp_alloc(type, 0);
|
||||
if (self) {
|
||||
self->data = std::make_shared<UIArc>();
|
||||
self->weakreflist = nullptr;
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
}
|
||||
85
src/UIBase.h
85
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,58 +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}
|
||||
|
||||
// #122 & #102: Macro for parent/global_position properties (requires closure with type enum)
|
||||
// These need the PyObjectsEnum value in closure, so they're added separately in each class
|
||||
#define UIDRAWABLE_PARENT_GETSETTERS(type_enum) \
|
||||
{"parent", (getter)UIDrawable::get_parent, (setter)UIDrawable::set_parent, \
|
||||
MCRF_PROPERTY(parent, \
|
||||
"Parent drawable. " \
|
||||
"Get: Returns the parent Frame/Grid if nested, or None if at scene level. " \
|
||||
"Set: Assign a Frame/Grid to reparent, or None to remove from parent." \
|
||||
), (void*)type_enum}, \
|
||||
{"global_position", (getter)UIDrawable::get_global_pos, NULL, \
|
||||
MCRF_PROPERTY(global_position, \
|
||||
"Global screen position (read-only). " \
|
||||
"Calculates absolute position by walking up the parent chain." \
|
||||
), (void*)type_enum}, \
|
||||
{"bounds", (getter)UIDrawable::get_bounds_py, NULL, \
|
||||
MCRF_PROPERTY(bounds, \
|
||||
"Bounding rectangle (x, y, width, height) in local coordinates." \
|
||||
), (void*)type_enum}, \
|
||||
{"global_bounds", (getter)UIDrawable::get_global_bounds_py, NULL, \
|
||||
MCRF_PROPERTY(global_bounds, \
|
||||
"Bounding rectangle (x, y, width, height) in screen coordinates." \
|
||||
), (void*)type_enum}, \
|
||||
{"on_enter", (getter)UIDrawable::get_on_enter, (setter)UIDrawable::set_on_enter, \
|
||||
MCRF_PROPERTY(on_enter, \
|
||||
"Callback for mouse enter events. " \
|
||||
"Called with (x, y, button, action) when mouse enters this element's bounds." \
|
||||
), (void*)type_enum}, \
|
||||
{"on_exit", (getter)UIDrawable::get_on_exit, (setter)UIDrawable::set_on_exit, \
|
||||
MCRF_PROPERTY(on_exit, \
|
||||
"Callback for mouse exit events. " \
|
||||
"Called with (x, y, button, action) when mouse leaves this element's bounds." \
|
||||
), (void*)type_enum}, \
|
||||
{"hovered", (getter)UIDrawable::get_hovered, NULL, \
|
||||
MCRF_PROPERTY(hovered, \
|
||||
"Whether mouse is currently over this element (read-only). " \
|
||||
"Updated automatically by the engine during mouse movement." \
|
||||
), (void*)type_enum}, \
|
||||
{"on_move", (getter)UIDrawable::get_on_move, (setter)UIDrawable::set_on_move, \
|
||||
MCRF_PROPERTY(on_move, \
|
||||
"Callback for mouse movement within bounds. " \
|
||||
"Called with (x, y, button, action) for each mouse movement while inside. " \
|
||||
"Performance note: Called frequently during movement - keep handlers fast." \
|
||||
), (void*)type_enum}
|
||||
"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,19 +272,10 @@ 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},
|
||||
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||
MCRF_PROPERTY(on_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,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICAPTION),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -449,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;
|
||||
}
|
||||
|
||||
|
|
@ -471,84 +448,71 @@ bool UICaption::setProperty(const std::string& name, float value) {
|
|||
if (name == "x") {
|
||||
position.x = value;
|
||||
text.setPosition(position); // Keep text in sync
|
||||
markCompositeDirty(); // #144 - Position change, texture still valid
|
||||
return true;
|
||||
}
|
||||
else if (name == "y") {
|
||||
position.y = value;
|
||||
text.setPosition(position); // Keep text in sync
|
||||
markCompositeDirty(); // #144 - Position change, texture still valid
|
||||
return true;
|
||||
}
|
||||
else if (name == "font_size" || name == "size") { // Support both for backward compatibility
|
||||
text.setCharacterSize(static_cast<unsigned int>(value));
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline") {
|
||||
text.setOutlineThickness(value);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "fill_color.r") {
|
||||
auto color = text.getFillColor();
|
||||
color.r = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setFillColor(color);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "fill_color.g") {
|
||||
auto color = text.getFillColor();
|
||||
color.g = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setFillColor(color);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "fill_color.b") {
|
||||
auto color = text.getFillColor();
|
||||
color.b = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setFillColor(color);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "fill_color.a") {
|
||||
auto color = text.getFillColor();
|
||||
color.a = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setFillColor(color);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color.r") {
|
||||
auto color = text.getOutlineColor();
|
||||
color.r = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setOutlineColor(color);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color.g") {
|
||||
auto color = text.getOutlineColor();
|
||||
color.g = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setOutlineColor(color);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color.b") {
|
||||
auto color = text.getOutlineColor();
|
||||
color.b = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setOutlineColor(color);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color.a") {
|
||||
auto color = text.getOutlineColor();
|
||||
color.a = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setOutlineColor(color);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "z_index") {
|
||||
z_index = static_cast<int>(value);
|
||||
markDirty(); // #144 - Z-order change affects parent
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -557,12 +521,10 @@ bool UICaption::setProperty(const std::string& name, float value) {
|
|||
bool UICaption::setProperty(const std::string& name, const sf::Color& value) {
|
||||
if (name == "fill_color") {
|
||||
text.setFillColor(value);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color") {
|
||||
text.setOutlineColor(value);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -571,7 +533,6 @@ bool UICaption::setProperty(const std::string& name, const sf::Color& value) {
|
|||
bool UICaption::setProperty(const std::string& name, const std::string& value) {
|
||||
if (name == "text") {
|
||||
text.setString(value);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
498
src/UICircle.cpp
498
src/UICircle.cpp
|
|
@ -1,498 +0,0 @@
|
|||
#include "UICircle.h"
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyColor.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include <cmath>
|
||||
|
||||
UICircle::UICircle()
|
||||
: radius(10.0f),
|
||||
fill_color(sf::Color::White),
|
||||
outline_color(sf::Color::Transparent),
|
||||
outline_thickness(0.0f)
|
||||
{
|
||||
position = sf::Vector2f(0.0f, 0.0f);
|
||||
shape.setRadius(radius);
|
||||
shape.setFillColor(fill_color);
|
||||
shape.setOutlineColor(outline_color);
|
||||
shape.setOutlineThickness(outline_thickness);
|
||||
shape.setOrigin(radius, radius); // Center the origin
|
||||
}
|
||||
|
||||
UICircle::UICircle(float radius, sf::Vector2f center, sf::Color fillColor,
|
||||
sf::Color outlineColor, float outlineThickness)
|
||||
: radius(radius),
|
||||
fill_color(fillColor),
|
||||
outline_color(outlineColor),
|
||||
outline_thickness(outlineThickness)
|
||||
{
|
||||
position = center;
|
||||
shape.setRadius(radius);
|
||||
shape.setFillColor(fill_color);
|
||||
shape.setOutlineColor(outline_color);
|
||||
shape.setOutlineThickness(outline_thickness);
|
||||
shape.setOrigin(radius, radius); // Center the origin
|
||||
}
|
||||
|
||||
UICircle::UICircle(const UICircle& other)
|
||||
: UIDrawable(other),
|
||||
radius(other.radius),
|
||||
fill_color(other.fill_color),
|
||||
outline_color(other.outline_color),
|
||||
outline_thickness(other.outline_thickness)
|
||||
{
|
||||
shape.setRadius(radius);
|
||||
shape.setFillColor(fill_color);
|
||||
shape.setOutlineColor(outline_color);
|
||||
shape.setOutlineThickness(outline_thickness);
|
||||
shape.setOrigin(radius, radius);
|
||||
}
|
||||
|
||||
UICircle& UICircle::operator=(const UICircle& other) {
|
||||
if (this != &other) {
|
||||
UIDrawable::operator=(other);
|
||||
radius = other.radius;
|
||||
fill_color = other.fill_color;
|
||||
outline_color = other.outline_color;
|
||||
outline_thickness = other.outline_thickness;
|
||||
shape.setRadius(radius);
|
||||
shape.setFillColor(fill_color);
|
||||
shape.setOutlineColor(outline_color);
|
||||
shape.setOutlineThickness(outline_thickness);
|
||||
shape.setOrigin(radius, radius);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
UICircle::UICircle(UICircle&& other) noexcept
|
||||
: UIDrawable(std::move(other)),
|
||||
shape(std::move(other.shape)),
|
||||
radius(other.radius),
|
||||
fill_color(other.fill_color),
|
||||
outline_color(other.outline_color),
|
||||
outline_thickness(other.outline_thickness)
|
||||
{
|
||||
}
|
||||
|
||||
UICircle& UICircle::operator=(UICircle&& other) noexcept {
|
||||
if (this != &other) {
|
||||
UIDrawable::operator=(std::move(other));
|
||||
shape = std::move(other.shape);
|
||||
radius = other.radius;
|
||||
fill_color = other.fill_color;
|
||||
outline_color = other.outline_color;
|
||||
outline_thickness = other.outline_thickness;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void UICircle::setRadius(float r) {
|
||||
radius = r;
|
||||
shape.setRadius(r);
|
||||
shape.setOrigin(r, r); // Keep origin at center
|
||||
}
|
||||
|
||||
void UICircle::setFillColor(sf::Color c) {
|
||||
fill_color = c;
|
||||
shape.setFillColor(c);
|
||||
}
|
||||
|
||||
void UICircle::setOutlineColor(sf::Color c) {
|
||||
outline_color = c;
|
||||
shape.setOutlineColor(c);
|
||||
}
|
||||
|
||||
void UICircle::setOutline(float t) {
|
||||
outline_thickness = t;
|
||||
shape.setOutlineThickness(t);
|
||||
}
|
||||
|
||||
void UICircle::render(sf::Vector2f offset, sf::RenderTarget& target) {
|
||||
if (!visible) return;
|
||||
|
||||
// Apply position and offset
|
||||
shape.setPosition(position + offset);
|
||||
|
||||
// Apply opacity to colors
|
||||
sf::Color render_fill = fill_color;
|
||||
render_fill.a = static_cast<sf::Uint8>(fill_color.a * opacity);
|
||||
shape.setFillColor(render_fill);
|
||||
|
||||
sf::Color render_outline = outline_color;
|
||||
render_outline.a = static_cast<sf::Uint8>(outline_color.a * opacity);
|
||||
shape.setOutlineColor(render_outline);
|
||||
|
||||
target.draw(shape);
|
||||
}
|
||||
|
||||
UIDrawable* UICircle::click_at(sf::Vector2f point) {
|
||||
if (!click_callable) return nullptr;
|
||||
|
||||
// Check if point is within the circle (including outline)
|
||||
float dx = point.x - position.x;
|
||||
float dy = point.y - position.y;
|
||||
float distance = std::sqrt(dx * dx + dy * dy);
|
||||
|
||||
float effective_radius = radius + outline_thickness;
|
||||
if (distance <= effective_radius) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObjectsEnum UICircle::derived_type() {
|
||||
return PyObjectsEnum::UICIRCLE;
|
||||
}
|
||||
|
||||
sf::FloatRect UICircle::get_bounds() const {
|
||||
float effective_radius = radius + outline_thickness;
|
||||
return sf::FloatRect(
|
||||
position.x - effective_radius,
|
||||
position.y - effective_radius,
|
||||
effective_radius * 2,
|
||||
effective_radius * 2
|
||||
);
|
||||
}
|
||||
|
||||
void UICircle::move(float dx, float dy) {
|
||||
position.x += dx;
|
||||
position.y += dy;
|
||||
}
|
||||
|
||||
void UICircle::resize(float w, float h) {
|
||||
// For circles, use the average of w and h as diameter
|
||||
radius = (w + h) / 4.0f; // Average of w and h, then divide by 2 for radius
|
||||
shape.setRadius(radius);
|
||||
shape.setOrigin(radius, radius);
|
||||
}
|
||||
|
||||
// Property system for animations
|
||||
bool UICircle::setProperty(const std::string& name, float value) {
|
||||
if (name == "radius") {
|
||||
setRadius(value);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
} else if (name == "outline") {
|
||||
setOutline(value);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
} else if (name == "x") {
|
||||
position.x = value;
|
||||
markCompositeDirty(); // #144 - Position change, texture still valid
|
||||
return true;
|
||||
} else if (name == "y") {
|
||||
position.y = value;
|
||||
markCompositeDirty(); // #144 - Position change, texture still valid
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UICircle::setProperty(const std::string& name, const sf::Color& value) {
|
||||
if (name == "fill_color") {
|
||||
setFillColor(value);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
} else if (name == "outline_color") {
|
||||
setOutlineColor(value);
|
||||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UICircle::setProperty(const std::string& name, const sf::Vector2f& value) {
|
||||
if (name == "center" || name == "position") {
|
||||
position = value;
|
||||
markDirty(); // #144 - Propagate to parent for texture caching
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UICircle::getProperty(const std::string& name, float& value) const {
|
||||
if (name == "radius") {
|
||||
value = radius;
|
||||
return true;
|
||||
} else if (name == "outline") {
|
||||
value = outline_thickness;
|
||||
return true;
|
||||
} else if (name == "x") {
|
||||
value = position.x;
|
||||
return true;
|
||||
} else if (name == "y") {
|
||||
value = position.y;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UICircle::getProperty(const std::string& name, sf::Color& value) const {
|
||||
if (name == "fill_color") {
|
||||
value = fill_color;
|
||||
return true;
|
||||
} else if (name == "outline_color") {
|
||||
value = outline_color;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UICircle::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||
if (name == "center" || name == "position") {
|
||||
value = position;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Python API implementations
|
||||
PyObject* UICircle::get_radius(PyUICircleObject* self, void* closure) {
|
||||
return PyFloat_FromDouble(self->data->getRadius());
|
||||
}
|
||||
|
||||
int UICircle::set_radius(PyUICircleObject* self, PyObject* value, void* closure) {
|
||||
if (!PyNumber_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "radius must be a number");
|
||||
return -1;
|
||||
}
|
||||
self->data->setRadius(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UICircle::get_center(PyUICircleObject* self, void* closure) {
|
||||
sf::Vector2f center = self->data->getCenter();
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (!type) return NULL;
|
||||
PyObject* result = PyObject_CallFunction((PyObject*)type, "ff", center.x, center.y);
|
||||
Py_DECREF(type);
|
||||
return result;
|
||||
}
|
||||
|
||||
int UICircle::set_center(PyUICircleObject* self, PyObject* value, void* closure) {
|
||||
PyVectorObject* vec = PyVector::from_arg(value);
|
||||
if (!vec) {
|
||||
PyErr_Clear();
|
||||
PyErr_SetString(PyExc_TypeError, "center must be a Vector or tuple (x, y)");
|
||||
return -1;
|
||||
}
|
||||
self->data->setCenter(vec->data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UICircle::get_fill_color(PyUICircleObject* self, void* closure) {
|
||||
sf::Color c = self->data->getFillColor();
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
|
||||
if (!type) return NULL;
|
||||
PyObject* result = PyObject_CallFunction((PyObject*)type, "iiii", c.r, c.g, c.b, c.a);
|
||||
Py_DECREF(type);
|
||||
return result;
|
||||
}
|
||||
|
||||
int UICircle::set_fill_color(PyUICircleObject* self, PyObject* value, void* closure) {
|
||||
sf::Color color;
|
||||
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"))) {
|
||||
auto pyColor = (PyColorObject*)value;
|
||||
color = pyColor->data;
|
||||
} else if (PyTuple_Check(value)) {
|
||||
int r, g, b, a = 255;
|
||||
if (!PyArg_ParseTuple(value, "iii|i", &r, &g, &b, &a)) {
|
||||
PyErr_SetString(PyExc_TypeError, "color tuple must be (r, g, b) or (r, g, b, a)");
|
||||
return -1;
|
||||
}
|
||||
color = sf::Color(r, g, b, a);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->setFillColor(color);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UICircle::get_outline_color(PyUICircleObject* self, void* closure) {
|
||||
sf::Color c = self->data->getOutlineColor();
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
|
||||
if (!type) return NULL;
|
||||
PyObject* result = PyObject_CallFunction((PyObject*)type, "iiii", c.r, c.g, c.b, c.a);
|
||||
Py_DECREF(type);
|
||||
return result;
|
||||
}
|
||||
|
||||
int UICircle::set_outline_color(PyUICircleObject* self, PyObject* value, void* closure) {
|
||||
sf::Color color;
|
||||
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"))) {
|
||||
auto pyColor = (PyColorObject*)value;
|
||||
color = pyColor->data;
|
||||
} else if (PyTuple_Check(value)) {
|
||||
int r, g, b, a = 255;
|
||||
if (!PyArg_ParseTuple(value, "iii|i", &r, &g, &b, &a)) {
|
||||
PyErr_SetString(PyExc_TypeError, "color tuple must be (r, g, b) or (r, g, b, a)");
|
||||
return -1;
|
||||
}
|
||||
color = sf::Color(r, g, b, a);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->setOutlineColor(color);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UICircle::get_outline(PyUICircleObject* self, void* closure) {
|
||||
return PyFloat_FromDouble(self->data->getOutline());
|
||||
}
|
||||
|
||||
int UICircle::set_outline(PyUICircleObject* self, PyObject* value, void* closure) {
|
||||
if (!PyNumber_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "outline must be a number");
|
||||
return -1;
|
||||
}
|
||||
self->data->setOutline(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Required typedef for UIDRAWABLE_GETSETTERS and UIDRAWABLE_METHODS macro templates
|
||||
typedef PyUICircleObject PyObjectType;
|
||||
|
||||
PyGetSetDef UICircle::getsetters[] = {
|
||||
{"radius", (getter)UICircle::get_radius, (setter)UICircle::set_radius,
|
||||
"Circle radius in pixels", NULL},
|
||||
{"center", (getter)UICircle::get_center, (setter)UICircle::set_center,
|
||||
"Center position of the circle", NULL},
|
||||
{"fill_color", (getter)UICircle::get_fill_color, (setter)UICircle::set_fill_color,
|
||||
"Fill color of the circle", NULL},
|
||||
{"outline_color", (getter)UICircle::get_outline_color, (setter)UICircle::set_outline_color,
|
||||
"Outline color of the circle", NULL},
|
||||
{"outline", (getter)UICircle::get_outline, (setter)UICircle::set_outline,
|
||||
"Outline thickness (0 for no outline)", NULL},
|
||||
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||
"Callable executed when circle is clicked.", (void*)PyObjectsEnum::UICIRCLE},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
||||
"Z-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UICIRCLE},
|
||||
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name,
|
||||
"Name for finding this element.", (void*)PyObjectsEnum::UICIRCLE},
|
||||
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos,
|
||||
"Position as a Vector (same as center).", (void*)PyObjectsEnum::UICIRCLE},
|
||||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICIRCLE),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyMethodDef UICircle_methods[] = {
|
||||
UIDRAWABLE_METHODS,
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyObject* UICircle::repr(PyUICircleObject* self) {
|
||||
std::ostringstream oss;
|
||||
auto& circle = self->data;
|
||||
sf::Vector2f center = circle->getCenter();
|
||||
sf::Color fc = circle->getFillColor();
|
||||
oss << "<Circle center=(" << center.x << ", " << center.y << ") "
|
||||
<< "radius=" << circle->getRadius() << " "
|
||||
<< "fill_color=(" << (int)fc.r << ", " << (int)fc.g << ", "
|
||||
<< (int)fc.b << ", " << (int)fc.a << ")>";
|
||||
return PyUnicode_FromString(oss.str().c_str());
|
||||
}
|
||||
|
||||
int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {
|
||||
"radius", "center", "fill_color", "outline_color", "outline",
|
||||
"click", "visible", "opacity", "z_index", "name", NULL
|
||||
};
|
||||
|
||||
float radius = 10.0f;
|
||||
PyObject* center_obj = NULL;
|
||||
PyObject* fill_color_obj = NULL;
|
||||
PyObject* outline_color_obj = NULL;
|
||||
float outline = 0.0f;
|
||||
|
||||
// Common UIDrawable kwargs
|
||||
PyObject* click_obj = NULL;
|
||||
int visible = 1;
|
||||
float opacity_val = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = NULL;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fOOOfOpfis", (char**)kwlist,
|
||||
&radius, ¢er_obj, &fill_color_obj, &outline_color_obj, &outline,
|
||||
&click_obj, &visible, &opacity_val, &z_index, &name)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Set radius
|
||||
self->data->setRadius(radius);
|
||||
|
||||
// Set center if provided
|
||||
if (center_obj && center_obj != Py_None) {
|
||||
PyVectorObject* vec = PyVector::from_arg(center_obj);
|
||||
if (!vec) {
|
||||
PyErr_Clear();
|
||||
PyErr_SetString(PyExc_TypeError, "center must be a Vector or tuple (x, y)");
|
||||
return -1;
|
||||
}
|
||||
self->data->setCenter(vec->data);
|
||||
}
|
||||
|
||||
// Set fill color if provided
|
||||
if (fill_color_obj && fill_color_obj != Py_None) {
|
||||
sf::Color color;
|
||||
if (PyObject_IsInstance(fill_color_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"))) {
|
||||
color = ((PyColorObject*)fill_color_obj)->data;
|
||||
} else if (PyTuple_Check(fill_color_obj)) {
|
||||
int r, g, b, a = 255;
|
||||
if (!PyArg_ParseTuple(fill_color_obj, "iii|i", &r, &g, &b, &a)) {
|
||||
PyErr_SetString(PyExc_TypeError, "fill_color tuple must be (r, g, b) or (r, g, b, a)");
|
||||
return -1;
|
||||
}
|
||||
color = sf::Color(r, g, b, a);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->setFillColor(color);
|
||||
}
|
||||
|
||||
// Set outline color if provided
|
||||
if (outline_color_obj && outline_color_obj != Py_None) {
|
||||
sf::Color color;
|
||||
if (PyObject_IsInstance(outline_color_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"))) {
|
||||
color = ((PyColorObject*)outline_color_obj)->data;
|
||||
} else if (PyTuple_Check(outline_color_obj)) {
|
||||
int r, g, b, a = 255;
|
||||
if (!PyArg_ParseTuple(outline_color_obj, "iii|i", &r, &g, &b, &a)) {
|
||||
PyErr_SetString(PyExc_TypeError, "outline_color tuple must be (r, g, b) or (r, g, b, a)");
|
||||
return -1;
|
||||
}
|
||||
color = sf::Color(r, g, b, a);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->setOutlineColor(color);
|
||||
}
|
||||
|
||||
// Set outline thickness
|
||||
self->data->setOutline(outline);
|
||||
|
||||
// Handle common UIDrawable properties
|
||||
if (click_obj && click_obj != Py_None) {
|
||||
if (!PyCallable_Check(click_obj)) {
|
||||
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
||||
return -1;
|
||||
}
|
||||
self->data->click_register(click_obj);
|
||||
}
|
||||
|
||||
self->data->visible = (visible != 0);
|
||||
self->data->opacity = opacity_val;
|
||||
self->data->z_index = z_index;
|
||||
|
||||
if (name) {
|
||||
self->data->name = name;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
155
src/UICircle.h
155
src/UICircle.h
|
|
@ -1,155 +0,0 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include "structmember.h"
|
||||
#include "UIDrawable.h"
|
||||
#include "UIBase.h"
|
||||
#include "PyDrawable.h"
|
||||
#include "PyColor.h"
|
||||
#include "PyVector.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
|
||||
// Forward declaration
|
||||
class UICircle;
|
||||
|
||||
// Python object structure
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UICircle> data;
|
||||
PyObject* weakreflist;
|
||||
} PyUICircleObject;
|
||||
|
||||
class UICircle : public UIDrawable
|
||||
{
|
||||
private:
|
||||
sf::CircleShape shape;
|
||||
float radius;
|
||||
sf::Color fill_color;
|
||||
sf::Color outline_color;
|
||||
float outline_thickness;
|
||||
|
||||
public:
|
||||
UICircle();
|
||||
UICircle(float radius, sf::Vector2f center = sf::Vector2f(0, 0),
|
||||
sf::Color fillColor = sf::Color::White,
|
||||
sf::Color outlineColor = sf::Color::Transparent,
|
||||
float outlineThickness = 0.0f);
|
||||
|
||||
// Copy constructor and assignment
|
||||
UICircle(const UICircle& other);
|
||||
UICircle& operator=(const UICircle& other);
|
||||
|
||||
// Move constructor and assignment
|
||||
UICircle(UICircle&& other) noexcept;
|
||||
UICircle& operator=(UICircle&& other) noexcept;
|
||||
|
||||
// UIDrawable interface
|
||||
void render(sf::Vector2f offset, sf::RenderTarget& target) override;
|
||||
UIDrawable* click_at(sf::Vector2f point) override;
|
||||
PyObjectsEnum derived_type() override;
|
||||
|
||||
// Getters and setters
|
||||
float getRadius() const { return radius; }
|
||||
void setRadius(float r);
|
||||
|
||||
sf::Vector2f getCenter() const { return position; }
|
||||
void setCenter(sf::Vector2f c) { position = c; }
|
||||
|
||||
sf::Color getFillColor() const { return fill_color; }
|
||||
void setFillColor(sf::Color c);
|
||||
|
||||
sf::Color getOutlineColor() const { return outline_color; }
|
||||
void setOutlineColor(sf::Color c);
|
||||
|
||||
float getOutline() const { return outline_thickness; }
|
||||
void setOutline(float t);
|
||||
|
||||
// Phase 1 virtual method implementations
|
||||
sf::FloatRect get_bounds() const override;
|
||||
void move(float dx, float dy) override;
|
||||
void resize(float w, float h) override;
|
||||
|
||||
// Property system for animations
|
||||
bool setProperty(const std::string& name, float value) override;
|
||||
bool setProperty(const std::string& name, const sf::Color& value) override;
|
||||
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
|
||||
bool getProperty(const std::string& name, float& value) const override;
|
||||
bool getProperty(const std::string& name, sf::Color& value) const override;
|
||||
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
||||
|
||||
// Python API
|
||||
static PyObject* get_radius(PyUICircleObject* self, void* closure);
|
||||
static int set_radius(PyUICircleObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_center(PyUICircleObject* self, void* closure);
|
||||
static int set_center(PyUICircleObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_fill_color(PyUICircleObject* self, void* closure);
|
||||
static int set_fill_color(PyUICircleObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_outline_color(PyUICircleObject* self, void* closure);
|
||||
static int set_outline_color(PyUICircleObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_outline(PyUICircleObject* self, void* closure);
|
||||
static int set_outline(PyUICircleObject* self, PyObject* value, void* closure);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyObject* repr(PyUICircleObject* self);
|
||||
static int init(PyUICircleObject* self, PyObject* args, PyObject* kwds);
|
||||
};
|
||||
|
||||
// Method definitions
|
||||
extern PyMethodDef UICircle_methods[];
|
||||
|
||||
namespace mcrfpydef {
|
||||
static PyTypeObject PyUICircleType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Circle",
|
||||
.tp_basicsize = sizeof(PyUICircleObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)[](PyObject* self) {
|
||||
PyUICircleObject* obj = (PyUICircleObject*)self;
|
||||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
obj->data.reset();
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
.tp_repr = (reprfunc)UICircle::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_doc = PyDoc_STR(
|
||||
"Circle(radius=0, center=None, fill_color=None, outline_color=None, outline=0, **kwargs)\n\n"
|
||||
"A circle UI element for drawing filled or outlined circles.\n\n"
|
||||
"Args:\n"
|
||||
" radius (float, optional): Circle radius in pixels. Default: 0\n"
|
||||
" center (tuple, optional): Center position as (x, y). Default: (0, 0)\n"
|
||||
" fill_color (Color, optional): Fill color. Default: White\n"
|
||||
" outline_color (Color, optional): Outline color. Default: Transparent\n"
|
||||
" outline (float, optional): Outline thickness. Default: 0 (no outline)\n\n"
|
||||
"Keyword Args:\n"
|
||||
" click (callable): Click handler. Default: None\n"
|
||||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n\n"
|
||||
"Attributes:\n"
|
||||
" radius (float): Circle radius\n"
|
||||
" center (Vector): Center position\n"
|
||||
" fill_color (Color): Fill color\n"
|
||||
" outline_color (Color): Outline color\n"
|
||||
" outline (float): Outline thickness\n"
|
||||
" visible (bool): Visibility state\n"
|
||||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
),
|
||||
.tp_methods = UICircle_methods,
|
||||
.tp_getset = UICircle::getsetters,
|
||||
.tp_base = &mcrfpydef::PyDrawableType,
|
||||
.tp_init = (initproc)UICircle::init,
|
||||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* {
|
||||
PyUICircleObject* self = (PyUICircleObject*)type->tp_alloc(type, 0);
|
||||
if (self) {
|
||||
self->data = std::make_shared<UICircle>();
|
||||
self->weakreflist = nullptr;
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -4,12 +4,8 @@
|
|||
#include "UICaption.h"
|
||||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "UILine.h"
|
||||
#include "UICircle.h"
|
||||
#include "UIArc.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PyObjectUtils.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include <climits>
|
||||
#include <algorithm>
|
||||
|
||||
|
|
@ -21,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;
|
||||
|
||||
|
|
@ -40,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;
|
||||
|
|
@ -53,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;
|
||||
|
|
@ -65,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;
|
||||
|
|
@ -77,43 +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;
|
||||
}
|
||||
case PyObjectsEnum::UILINE:
|
||||
{
|
||||
type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line");
|
||||
if (!type) return nullptr;
|
||||
auto pyObj = (PyUILineObject*)type->tp_alloc(type, 0);
|
||||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UILine>(drawable);
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
}
|
||||
case PyObjectsEnum::UICIRCLE:
|
||||
{
|
||||
type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle");
|
||||
if (!type) return nullptr;
|
||||
auto pyObj = (PyUICircleObject*)type->tp_alloc(type, 0);
|
||||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UICircle>(drawable);
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
}
|
||||
case PyObjectsEnum::UIARC:
|
||||
{
|
||||
type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc");
|
||||
if (!type) return nullptr;
|
||||
auto pyObj = (PyUIArcObject*)type->tp_alloc(type, 0);
|
||||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UIArc>(drawable);
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
|
|
@ -220,8 +168,6 @@ int UICollection::setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject
|
|||
|
||||
// Handle deletion
|
||||
if (value == NULL) {
|
||||
// #122: Clear the parent before removing
|
||||
(*self->data)[index]->setParent(nullptr);
|
||||
self->data->erase(self->data->begin() + index);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -257,27 +203,16 @@ int UICollection::setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject
|
|||
PyErr_SetString(PyExc_RuntimeError, "Failed to extract C++ object from Python object");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// #122: Clear parent of old element
|
||||
(*vec)[index]->setParent(nullptr);
|
||||
|
||||
// #122: Remove new drawable from its old parent if it has one
|
||||
if (auto old_parent = new_drawable->getParent()) {
|
||||
new_drawable->removeFromParent();
|
||||
}
|
||||
|
||||
|
||||
// Preserve the z_index of the replaced element
|
||||
new_drawable->z_index = old_z_index;
|
||||
|
||||
// #122: Set new parent
|
||||
new_drawable->setParent(self->owner.lock());
|
||||
|
||||
|
||||
// Replace the element
|
||||
(*vec)[index] = new_drawable;
|
||||
|
||||
|
||||
// Mark scene as needing resort after replacing element
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -629,13 +564,10 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
|
|||
if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
|
||||
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
|
||||
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")) &&
|
||||
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line")) &&
|
||||
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle")) &&
|
||||
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"))
|
||||
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))
|
||||
)
|
||||
{
|
||||
PyErr_SetString(PyExc_TypeError, "Only Frame, Caption, Sprite, Grid, Line, Circle, and Arc objects can be added to UICollection");
|
||||
PyErr_SetString(PyExc_TypeError, "Only Frame, Caption, Sprite, and Grid objects can be added to UICollection");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -651,53 +583,31 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
|
|||
}
|
||||
}
|
||||
|
||||
// #122: Get the owner as parent for this drawable
|
||||
std::shared_ptr<UIDrawable> owner_ptr = self->owner.lock();
|
||||
|
||||
// Helper lambda to add drawable with parent tracking
|
||||
auto addDrawable = [&](std::shared_ptr<UIDrawable> drawable) {
|
||||
// #122: Remove from old parent if it has one
|
||||
if (auto old_parent = drawable->getParent()) {
|
||||
drawable->removeFromParent();
|
||||
}
|
||||
|
||||
drawable->z_index = new_z_index;
|
||||
|
||||
// #122: Set new parent (owner of this collection)
|
||||
drawable->setParent(owner_ptr);
|
||||
|
||||
self->data->push_back(drawable);
|
||||
};
|
||||
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")))
|
||||
{
|
||||
addDrawable(((PyUIFrameObject*)o)->data);
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)o;
|
||||
frame->data->z_index = new_z_index;
|
||||
self->data->push_back(frame->data);
|
||||
}
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")))
|
||||
{
|
||||
addDrawable(((PyUICaptionObject*)o)->data);
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)o;
|
||||
caption->data->z_index = new_z_index;
|
||||
self->data->push_back(caption->data);
|
||||
}
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")))
|
||||
{
|
||||
addDrawable(((PyUISpriteObject*)o)->data);
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)o;
|
||||
sprite->data->z_index = new_z_index;
|
||||
self->data->push_back(sprite->data);
|
||||
}
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")))
|
||||
{
|
||||
addDrawable(((PyUIGridObject*)o)->data);
|
||||
PyUIGridObject* grid = (PyUIGridObject*)o;
|
||||
grid->data->z_index = new_z_index;
|
||||
self->data->push_back(grid->data);
|
||||
}
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line")))
|
||||
{
|
||||
addDrawable(((PyUILineObject*)o)->data);
|
||||
}
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle")))
|
||||
{
|
||||
addDrawable(((PyUICircleObject*)o)->data);
|
||||
}
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc")))
|
||||
{
|
||||
addDrawable(((PyUIArcObject*)o)->data);
|
||||
}
|
||||
|
||||
|
||||
// Mark scene as needing resort after adding element
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
|
|
@ -733,14 +643,11 @@ PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable)
|
|||
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
|
||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
|
||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")) &&
|
||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line")) &&
|
||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle")) &&
|
||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc")))
|
||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")))
|
||||
{
|
||||
Py_DECREF(item);
|
||||
Py_DECREF(iterator);
|
||||
PyErr_SetString(PyExc_TypeError, "All items must be Frame, Caption, Sprite, Grid, Line, Circle, or Arc objects");
|
||||
PyErr_SetString(PyExc_TypeError, "All items must be Frame, Caption, Sprite, or Grid objects");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -751,46 +658,31 @@ PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable)
|
|||
current_z_index = INT_MAX;
|
||||
}
|
||||
|
||||
// #122: Get the owner as parent for this drawable
|
||||
std::shared_ptr<UIDrawable> owner_ptr = self->owner.lock();
|
||||
|
||||
// Helper lambda to add drawable with parent tracking
|
||||
auto addDrawable = [&](std::shared_ptr<UIDrawable> drawable) {
|
||||
// #122: Remove from old parent if it has one
|
||||
if (auto old_parent = drawable->getParent()) {
|
||||
drawable->removeFromParent();
|
||||
}
|
||||
drawable->z_index = current_z_index;
|
||||
drawable->setParent(owner_ptr);
|
||||
self->data->push_back(drawable);
|
||||
};
|
||||
|
||||
// Add the item based on its type
|
||||
if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
addDrawable(((PyUIFrameObject*)item)->data);
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)item;
|
||||
frame->data->z_index = current_z_index;
|
||||
self->data->push_back(frame->data);
|
||||
}
|
||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
addDrawable(((PyUICaptionObject*)item)->data);
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)item;
|
||||
caption->data->z_index = current_z_index;
|
||||
self->data->push_back(caption->data);
|
||||
}
|
||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
addDrawable(((PyUISpriteObject*)item)->data);
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)item;
|
||||
sprite->data->z_index = current_z_index;
|
||||
self->data->push_back(sprite->data);
|
||||
}
|
||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
addDrawable(((PyUIGridObject*)item)->data);
|
||||
PyUIGridObject* grid = (PyUIGridObject*)item;
|
||||
grid->data->z_index = current_z_index;
|
||||
self->data->push_back(grid->data);
|
||||
}
|
||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line"))) {
|
||||
addDrawable(((PyUILineObject*)item)->data);
|
||||
}
|
||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle"))) {
|
||||
addDrawable(((PyUICircleObject*)item)->data);
|
||||
}
|
||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"))) {
|
||||
addDrawable(((PyUIArcObject*)item)->data);
|
||||
}
|
||||
|
||||
|
||||
Py_DECREF(item);
|
||||
}
|
||||
|
||||
|
||||
Py_DECREF(iterator);
|
||||
|
||||
// Check if iteration ended due to an error
|
||||
|
|
@ -807,164 +699,30 @@ PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable)
|
|||
|
||||
PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o)
|
||||
{
|
||||
auto vec = self->data.get();
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Collection data is null");
|
||||
if (!PyLong_Check(o))
|
||||
{
|
||||
PyErr_SetString(PyExc_TypeError, "UICollection.remove requires an integer index to remove");
|
||||
return NULL;
|
||||
}
|
||||
long index = PyLong_AsLong(o);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Type checking - must be a UIDrawable subclass
|
||||
if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Drawable"))) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"UICollection.remove requires a UI element (Frame, Caption, Sprite, Grid)");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the C++ object from the Python object
|
||||
std::shared_ptr<UIDrawable> search_drawable = nullptr;
|
||||
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
search_drawable = ((PyUIFrameObject*)o)->data;
|
||||
} else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
search_drawable = ((PyUICaptionObject*)o)->data;
|
||||
} else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
search_drawable = ((PyUISpriteObject*)o)->data;
|
||||
} else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
search_drawable = ((PyUIGridObject*)o)->data;
|
||||
}
|
||||
|
||||
if (!search_drawable) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"UICollection.remove requires a UI element (Frame, Caption, Sprite, Grid)");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Search for the object and remove first occurrence
|
||||
for (auto it = vec->begin(); it != vec->end(); ++it) {
|
||||
if (it->get() == search_drawable.get()) {
|
||||
// #122: Clear the parent before removing
|
||||
(*it)->setParent(nullptr);
|
||||
vec->erase(it);
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
PyErr_SetString(PyExc_ValueError, "element not in UICollection");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* UICollection::pop(PyUICollectionObject* self, PyObject* args)
|
||||
{
|
||||
Py_ssize_t index = -1; // Default to last element
|
||||
|
||||
if (!PyArg_ParseTuple(args, "|n", &index)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto vec = self->data.get();
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Collection data is null");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (vec->empty()) {
|
||||
PyErr_SetString(PyExc_IndexError, "pop from empty UICollection");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Handle negative indexing
|
||||
Py_ssize_t size = static_cast<Py_ssize_t>(vec->size());
|
||||
if (index < 0) {
|
||||
index += size;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= size) {
|
||||
PyErr_SetString(PyExc_IndexError, "pop index out of range");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the element before removing
|
||||
std::shared_ptr<UIDrawable> drawable = (*vec)[index];
|
||||
|
||||
// #122: Clear the parent before removing
|
||||
drawable->setParent(nullptr);
|
||||
|
||||
// Remove from vector
|
||||
vec->erase(vec->begin() + index);
|
||||
|
||||
// release the shared pointer at self->data[index];
|
||||
self->data->erase(self->data->begin() + index);
|
||||
|
||||
// Mark scene as needing resort after removing element
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// Convert to Python object and return
|
||||
return convertDrawableToPython(drawable);
|
||||
}
|
||||
|
||||
PyObject* UICollection::insert(PyUICollectionObject* self, PyObject* args)
|
||||
{
|
||||
Py_ssize_t index;
|
||||
PyObject* o;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "nO", &index, &o)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto vec = self->data.get();
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Collection data is null");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Type checking - must be a UIDrawable subclass
|
||||
if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Drawable"))) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"UICollection.insert requires a UI element (Frame, Caption, Sprite, Grid)");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the C++ object from the Python object
|
||||
std::shared_ptr<UIDrawable> drawable = nullptr;
|
||||
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
drawable = ((PyUIFrameObject*)o)->data;
|
||||
} else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
drawable = ((PyUICaptionObject*)o)->data;
|
||||
} else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
drawable = ((PyUISpriteObject*)o)->data;
|
||||
} else if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
drawable = ((PyUIGridObject*)o)->data;
|
||||
}
|
||||
|
||||
if (!drawable) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"UICollection.insert requires a UI element (Frame, Caption, Sprite, Grid)");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Handle negative indexing and clamping (Python list.insert behavior)
|
||||
Py_ssize_t size = static_cast<Py_ssize_t>(vec->size());
|
||||
if (index < 0) {
|
||||
index += size;
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
} else if (index > size) {
|
||||
index = size;
|
||||
}
|
||||
|
||||
// #122: Remove from old parent if it has one
|
||||
if (auto old_parent = drawable->getParent()) {
|
||||
drawable->removeFromParent();
|
||||
}
|
||||
|
||||
// #122: Set new parent
|
||||
drawable->setParent(self->owner.lock());
|
||||
|
||||
// Insert at position
|
||||
vec->insert(vec->begin() + index, drawable);
|
||||
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
Py_RETURN_NONE;
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
PyObject* UICollection::index_method(PyUICollectionObject* self, PyObject* value) {
|
||||
|
|
@ -1056,173 +814,12 @@ PyObject* UICollection::count(PyUICollectionObject* self, PyObject* value) {
|
|||
return PyLong_FromSsize_t(count);
|
||||
}
|
||||
|
||||
// Helper function to match names with optional wildcard support
|
||||
static bool matchName(const std::string& name, const std::string& pattern) {
|
||||
// Check for wildcard pattern
|
||||
if (pattern.find('*') != std::string::npos) {
|
||||
// Simple wildcard matching: only support * at start, end, or both
|
||||
if (pattern == "*") {
|
||||
return true; // Match everything
|
||||
} else if (pattern.front() == '*' && pattern.back() == '*' && pattern.length() > 2) {
|
||||
// *substring* - contains match
|
||||
std::string substring = pattern.substr(1, pattern.length() - 2);
|
||||
return name.find(substring) != std::string::npos;
|
||||
} else if (pattern.front() == '*') {
|
||||
// *suffix - ends with
|
||||
std::string suffix = pattern.substr(1);
|
||||
return name.length() >= suffix.length() &&
|
||||
name.compare(name.length() - suffix.length(), suffix.length(), suffix) == 0;
|
||||
} else if (pattern.back() == '*') {
|
||||
// prefix* - starts with
|
||||
std::string prefix = pattern.substr(0, pattern.length() - 1);
|
||||
return name.compare(0, prefix.length(), prefix) == 0;
|
||||
}
|
||||
// For more complex patterns, fall back to exact match
|
||||
return name == pattern;
|
||||
}
|
||||
// Exact match
|
||||
return name == pattern;
|
||||
}
|
||||
|
||||
PyObject* UICollection::find(PyUICollectionObject* self, PyObject* args, PyObject* kwds) {
|
||||
const char* name = nullptr;
|
||||
int recursive = 0;
|
||||
|
||||
static const char* kwlist[] = {"name", "recursive", NULL};
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|p", const_cast<char**>(kwlist),
|
||||
&name, &recursive)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto vec = self->data.get();
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Collection data is null");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
std::string pattern(name);
|
||||
bool has_wildcard = (pattern.find('*') != std::string::npos);
|
||||
|
||||
if (has_wildcard) {
|
||||
// Return list of all matches
|
||||
PyObject* results = PyList_New(0);
|
||||
if (!results) return NULL;
|
||||
|
||||
for (auto& drawable : *vec) {
|
||||
if (matchName(drawable->name, pattern)) {
|
||||
PyObject* py_drawable = convertDrawableToPython(drawable);
|
||||
if (!py_drawable) {
|
||||
Py_DECREF(results);
|
||||
return NULL;
|
||||
}
|
||||
if (PyList_Append(results, py_drawable) < 0) {
|
||||
Py_DECREF(py_drawable);
|
||||
Py_DECREF(results);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(py_drawable); // PyList_Append increfs
|
||||
}
|
||||
|
||||
// Recursive search into Frame children
|
||||
if (recursive && drawable->derived_type() == PyObjectsEnum::UIFRAME) {
|
||||
auto frame = std::static_pointer_cast<UIFrame>(drawable);
|
||||
// Create temporary collection object for recursive call
|
||||
PyTypeObject* collType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollection");
|
||||
if (collType) {
|
||||
PyUICollectionObject* child_coll = (PyUICollectionObject*)collType->tp_alloc(collType, 0);
|
||||
if (child_coll) {
|
||||
child_coll->data = frame->children;
|
||||
PyObject* child_results = find(child_coll, args, kwds);
|
||||
if (child_results && PyList_Check(child_results)) {
|
||||
// Extend results with child results
|
||||
for (Py_ssize_t i = 0; i < PyList_Size(child_results); i++) {
|
||||
PyObject* item = PyList_GetItem(child_results, i);
|
||||
Py_INCREF(item);
|
||||
PyList_Append(results, item);
|
||||
Py_DECREF(item);
|
||||
}
|
||||
Py_DECREF(child_results);
|
||||
}
|
||||
Py_DECREF(child_coll);
|
||||
}
|
||||
Py_DECREF(collType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} else {
|
||||
// Return first exact match or None
|
||||
for (auto& drawable : *vec) {
|
||||
if (drawable->name == pattern) {
|
||||
return convertDrawableToPython(drawable);
|
||||
}
|
||||
|
||||
// Recursive search into Frame children
|
||||
if (recursive && drawable->derived_type() == PyObjectsEnum::UIFRAME) {
|
||||
auto frame = std::static_pointer_cast<UIFrame>(drawable);
|
||||
PyTypeObject* collType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollection");
|
||||
if (collType) {
|
||||
PyUICollectionObject* child_coll = (PyUICollectionObject*)collType->tp_alloc(collType, 0);
|
||||
if (child_coll) {
|
||||
child_coll->data = frame->children;
|
||||
PyObject* result = find(child_coll, args, kwds);
|
||||
Py_DECREF(child_coll);
|
||||
Py_DECREF(collType);
|
||||
if (result && result != Py_None) {
|
||||
return result;
|
||||
}
|
||||
Py_XDECREF(result);
|
||||
} else {
|
||||
Py_DECREF(collType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
PyMethodDef UICollection::methods[] = {
|
||||
{"append", (PyCFunction)UICollection::append, METH_O,
|
||||
"append(element)\n\n"
|
||||
"Add an element to the end of the collection."},
|
||||
{"extend", (PyCFunction)UICollection::extend, METH_O,
|
||||
"extend(iterable)\n\n"
|
||||
"Add all elements from an iterable to the collection."},
|
||||
{"insert", (PyCFunction)UICollection::insert, METH_VARARGS,
|
||||
"insert(index, element)\n\n"
|
||||
"Insert element at index. Like list.insert(), indices past the end append.\n\n"
|
||||
"Note: If using z_index for sorting, insertion order may not persist after\n"
|
||||
"the next render. Use name-based .find() for stable element access."},
|
||||
{"remove", (PyCFunction)UICollection::remove, METH_O,
|
||||
"remove(element)\n\n"
|
||||
"Remove first occurrence of element. Raises ValueError if not found."},
|
||||
{"pop", (PyCFunction)UICollection::pop, METH_VARARGS,
|
||||
"pop([index]) -> element\n\n"
|
||||
"Remove and return element at index (default: last element).\n\n"
|
||||
"Note: If using z_index for sorting, indices may shift after render.\n"
|
||||
"Use name-based .find() for stable element access."},
|
||||
{"index", (PyCFunction)UICollection::index_method, METH_O,
|
||||
"index(element) -> int\n\n"
|
||||
"Return index of first occurrence of element. Raises ValueError if not found."},
|
||||
{"count", (PyCFunction)UICollection::count, METH_O,
|
||||
"count(element) -> int\n\n"
|
||||
"Count occurrences of element in the collection."},
|
||||
{"find", (PyCFunction)UICollection::find, METH_VARARGS | METH_KEYWORDS,
|
||||
"find(name, recursive=False) -> element or list\n\n"
|
||||
"Find elements by name.\n\n"
|
||||
"Args:\n"
|
||||
" name (str): Name to search for. Supports wildcards:\n"
|
||||
" - 'exact' for exact match (returns single element or None)\n"
|
||||
" - 'prefix*' for starts-with match (returns list)\n"
|
||||
" - '*suffix' for ends-with match (returns list)\n"
|
||||
" - '*substring*' for contains match (returns list)\n"
|
||||
" recursive (bool): If True, search in Frame children recursively.\n\n"
|
||||
"Returns:\n"
|
||||
" Single element if exact match, list if wildcard, None if not found."},
|
||||
{"append", (PyCFunction)UICollection::append, METH_O},
|
||||
{"extend", (PyCFunction)UICollection::extend, METH_O},
|
||||
{"remove", (PyCFunction)UICollection::remove, METH_O},
|
||||
{"index", (PyCFunction)UICollection::index_method, METH_O},
|
||||
{"count", (PyCFunction)UICollection::count, METH_O},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -30,11 +30,8 @@ public:
|
|||
static PyObject* append(PyUICollectionObject* self, PyObject* o);
|
||||
static PyObject* extend(PyUICollectionObject* self, PyObject* iterable);
|
||||
static PyObject* remove(PyUICollectionObject* self, PyObject* o);
|
||||
static PyObject* pop(PyUICollectionObject* self, PyObject* args);
|
||||
static PyObject* insert(PyUICollectionObject* self, PyObject* args);
|
||||
static PyObject* index_method(PyUICollectionObject* self, PyObject* value);
|
||||
static PyObject* count(PyUICollectionObject* self, PyObject* value);
|
||||
static PyObject* find(PyUICollectionObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyMethodDef methods[];
|
||||
static PyObject* repr(PyUICollectionObject* self);
|
||||
static int init(PyUICollectionObject* self, PyObject* args, PyObject* kwds);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue