Compare commits
5 commits
master
...
raii_pyobj
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f060dc87b | |||
| c9d5251c71 | |||
| 0a8f67e391 | |||
| 05d9f6a882 | |||
| 972768eb26 |
42
.gitignore
vendored
|
|
@ -7,44 +7,6 @@ PCbuild
|
|||
.vs
|
||||
obj
|
||||
build
|
||||
/lib
|
||||
__pycache__
|
||||
lib
|
||||
obj
|
||||
|
||||
# unimportant files that won't pass clean dir check
|
||||
build*
|
||||
docs
|
||||
.claude
|
||||
my_games
|
||||
|
||||
# images are produced by many tests
|
||||
*.png
|
||||
|
||||
# WASM stdlib for Emscripten build
|
||||
!wasm_stdlib/
|
||||
|
||||
.cache/
|
||||
7DRL2025 Release/
|
||||
CMakeFiles/
|
||||
Makefile
|
||||
*.zip
|
||||
__lib/
|
||||
__lib_windows/
|
||||
build-windows/
|
||||
build_windows/
|
||||
_oldscripts/
|
||||
assets/
|
||||
cellular_automata_fire/
|
||||
deps/
|
||||
fetch_issues_txt.py
|
||||
forest_fire_CA.py
|
||||
mcrogueface.github.io
|
||||
scripts/
|
||||
tcod_reference
|
||||
.archive
|
||||
.mcp.json
|
||||
dist/
|
||||
|
||||
# Keep important documentation and tests
|
||||
!CLAUDE.md
|
||||
!README.md
|
||||
!tests/
|
||||
|
|
|
|||
13
.gitmodules
vendored
|
|
@ -10,13 +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/RapidXML"]
|
||||
path = modules/RapidXML
|
||||
url = https://github.com/Fe-Bell/RapidXML
|
||||
[submodule "modules/json"]
|
||||
path = modules/json
|
||||
url = git@github.com:nlohmann/json.git
|
||||
[submodule "modules/libtcod"]
|
||||
path = modules/libtcod
|
||||
url = git@github.com:libtcod/libtcod.git
|
||||
|
|
|
|||
|
|
@ -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/`.
|
||||
992
CLAUDE.md
|
|
@ -1,992 +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 System
|
||||
|
||||
McRogueFace uses a unified Makefile for both Linux native builds and Windows cross-compilation.
|
||||
|
||||
**IMPORTANT**: All `make` commands must be run from the **project root directory** (`/home/john/Development/McRogueFace/`), not from `build/` or any subdirectory.
|
||||
|
||||
### Quick Reference
|
||||
|
||||
```bash
|
||||
# Linux builds
|
||||
make # Build for Linux (default target)
|
||||
make linux # Same as above
|
||||
make run # Build and run
|
||||
make clean # Remove Linux build artifacts
|
||||
|
||||
# Windows cross-compilation (requires MinGW-w64)
|
||||
make windows # Release build for Windows
|
||||
make windows-debug # Debug build with console output
|
||||
make clean-windows # Remove Windows build artifacts
|
||||
|
||||
# Distribution packages
|
||||
make package-linux-light # Linux with minimal stdlib (~25 MB)
|
||||
make package-linux-full # Linux with full stdlib (~26 MB)
|
||||
make package-windows-light # Windows with minimal stdlib
|
||||
make package-windows-full # Windows with full stdlib
|
||||
make package-all # All platform/preset combinations
|
||||
|
||||
# Cleanup
|
||||
make clean-all # Remove all builds and packages
|
||||
make clean-dist # Remove only distribution packages
|
||||
```
|
||||
|
||||
### Build Outputs
|
||||
|
||||
| Command | Output Directory | Executable |
|
||||
|---------|------------------|------------|
|
||||
| `make` / `make linux` | `build/` | `build/mcrogueface` |
|
||||
| `make windows` | `build-windows/` | `build-windows/mcrogueface.exe` |
|
||||
| `make windows-debug` | `build-windows-debug/` | `build-windows-debug/mcrogueface.exe` |
|
||||
| `make package-*` | `dist/` | `.tar.gz` or `.zip` archives |
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**Linux build:**
|
||||
- CMake 3.14+
|
||||
- GCC/G++ with C++17 support
|
||||
- SFML 2.6 development libraries
|
||||
- Libraries in `__lib/` directory (libpython3.14, libtcod, etc.)
|
||||
|
||||
**Windows cross-compilation:**
|
||||
- MinGW-w64 (`x86_64-w64-mingw32-g++-posix`)
|
||||
- Libraries in `__lib_windows/` directory
|
||||
- Toolchain file: `cmake/toolchains/mingw-w64-x86_64.cmake`
|
||||
|
||||
### Library Dependencies
|
||||
|
||||
The build expects pre-built libraries in:
|
||||
- `__lib/` - Linux shared libraries (libpython3.14.so, libsfml-*.so, libtcod.so)
|
||||
- `__lib/Python/Lib/` - Python standard library source
|
||||
- `__lib/Python/lib.linux-x86_64-3.14/` - Python extension modules (.so)
|
||||
- `__lib_windows/` - Windows DLLs and libraries
|
||||
|
||||
### Manual CMake Build
|
||||
|
||||
If you need more control over the build:
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# Windows cross-compile
|
||||
mkdir build-windows && cd build-windows
|
||||
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# Windows debug with console
|
||||
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_WINDOWS_CONSOLE=ON
|
||||
```
|
||||
|
||||
### Distribution Packaging
|
||||
|
||||
The packaging system creates self-contained archives with:
|
||||
- Executable
|
||||
- Required shared libraries
|
||||
- Assets (sprites, fonts, audio)
|
||||
- Python scripts
|
||||
- Filtered Python stdlib (light or full variant)
|
||||
|
||||
**Light variant** (~25 MB): Core + gamedev + utility modules only
|
||||
**Full variant** (~26 MB): Includes networking, async, debugging modules
|
||||
|
||||
Packaging tools:
|
||||
- `tools/package.sh` - Main packaging orchestrator
|
||||
- `tools/package_stdlib.py` - Creates filtered stdlib archives
|
||||
- `tools/stdlib_modules.yaml` - Module categorization config
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**"No rule to make target 'linux'"**: You're in the wrong directory. Run `make` from project root.
|
||||
|
||||
**Library linking errors**: Ensure `__lib/` contains all required .so files. Check `CMakeLists.txt` for `link_directories(${CMAKE_SOURCE_DIR}/__lib)`.
|
||||
|
||||
**Windows build fails**: Verify MinGW-w64 is installed with posix thread model: `x86_64-w64-mingw32-g++-posix --version`
|
||||
|
||||
### Legacy Build Scripts
|
||||
|
||||
The following are deprecated but kept for reference:
|
||||
- `build.sh` - Original Linux build script (use `make` instead)
|
||||
- `GNUmakefile.legacy` - Old wrapper makefile (renamed to avoid conflicts)
|
||||
|
||||
## Emscripten / WebAssembly Builds
|
||||
|
||||
McRogueFace supports WebGL deployment via Emscripten with an SDL2+OpenGL ES 2 backend.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
source ~/emsdk/emsdk_env.sh # Activate Emscripten SDK
|
||||
make wasm # Full game web build
|
||||
make playground # REPL-focused web build
|
||||
make serve # Serve at http://localhost:8080
|
||||
```
|
||||
|
||||
### Build Variants
|
||||
|
||||
| Target | Output Directory | Purpose |
|
||||
|--------|------------------|---------|
|
||||
| `make wasm` | `build-emscripten/` | Full game with all scripts/assets |
|
||||
| `make playground` | `build-playground/` | Minimal REPL build for interactive testing |
|
||||
|
||||
### Rendering Backend Selection
|
||||
|
||||
The build system supports three backends via CMake defines:
|
||||
|
||||
```bash
|
||||
cmake .. # SFML (default desktop)
|
||||
cmake -DMCRF_SDL2=ON .. # SDL2 + OpenGL ES 2 (Emscripten)
|
||||
cmake -DMCRF_HEADLESS=ON .. # No graphics (CI/testing)
|
||||
```
|
||||
|
||||
Emscripten builds automatically select SDL2 mode. Backend selection happens in `Common.h`:
|
||||
|
||||
```cpp
|
||||
#ifdef MCRF_HEADLESS
|
||||
#include "platform/HeadlessTypes.h"
|
||||
#elif defined(MCRF_SDL2)
|
||||
#include "platform/SDL2Types.h"
|
||||
#else
|
||||
#include <SFML/Graphics.hpp>
|
||||
#endif
|
||||
```
|
||||
|
||||
### SDL2 Backend Architecture
|
||||
|
||||
Game code remains unchanged because `SDL2Types.h` provides SFML-compatible type stubs:
|
||||
- `sf::Vector2f`, `sf::Color`, `sf::RectangleShape`, etc. all work identically
|
||||
- Rendering uses OpenGL ES 2 shaders internally
|
||||
- Text rendering uses FreeType 2 (with outline support)
|
||||
|
||||
### Web Build Constraints
|
||||
|
||||
When developing features that must work in WebGL:
|
||||
|
||||
| Feature | Desktop (SFML) | Web (SDL2) | Notes |
|
||||
|---------|----------------|------------|-------|
|
||||
| Audio | ✅ Full | ❌ Stubbed | SoundBuffer/Sound/Music do nothing |
|
||||
| ImGui console | ✅ Full | ❌ Disabled | Debug overlay unavailable |
|
||||
| Dynamic assets | ✅ Filesystem | ❌ Preloaded | All assets bundled at build time |
|
||||
| Threading | ✅ Full | ⚠️ Limited | Single-threaded JS execution |
|
||||
| Input | ✅ Full | ✅ Full | SDL events translated to SFML enums |
|
||||
|
||||
### Development Workflow
|
||||
|
||||
For iterative Emscripten development:
|
||||
|
||||
```bash
|
||||
source ~/emsdk/emsdk_env.sh
|
||||
# Initial configure (once)
|
||||
emmake cmake -B build-emscripten -DMCRF_SDL2=ON
|
||||
|
||||
# Rebuild after code changes (fast)
|
||||
emmake make -C build-emscripten -j$(nproc)
|
||||
|
||||
# Test in browser
|
||||
make serve
|
||||
```
|
||||
|
||||
### Adding Cross-Platform Features
|
||||
|
||||
When adding rendering features:
|
||||
|
||||
1. **Check backend at compile time** if needed:
|
||||
```cpp
|
||||
#ifdef MCRF_SDL2
|
||||
// SDL2-specific implementation
|
||||
#else
|
||||
// SFML implementation
|
||||
#endif
|
||||
```
|
||||
|
||||
2. **Prefer SFML API abstractions** - SDL2Types.h mirrors the SFML interface
|
||||
|
||||
3. **For new SDL2 features**: Add to `SDL2Renderer.cpp` with GLSL ES 2.0 shaders
|
||||
|
||||
4. **Test headless mode** for CI compatibility:
|
||||
```bash
|
||||
./mcrogueface --headless --exec ../tests/unit/my_test.py
|
||||
```
|
||||
|
||||
### Asset Paths
|
||||
|
||||
Emscripten uses a virtual filesystem with preloaded assets:
|
||||
- Use absolute paths: `/assets/sprite.png`, `/scripts/game.py`
|
||||
- Assets defined in CMakeLists.txt `--preload-file` flags
|
||||
- No runtime file loading from disk
|
||||
|
||||
### Playground Mode
|
||||
|
||||
The playground build (`-DMCRF_PLAYGROUND=ON`) provides:
|
||||
- Python REPL widget in browser
|
||||
- Scene reset capability via `reset_python_environment()`
|
||||
- Minimal `src/scripts_playground/game.py` with idempotent initialization
|
||||
- JavaScript interop via `Module.ccall()` to C functions
|
||||
|
||||
### Key Files
|
||||
|
||||
- `src/platform/SDL2Types.h` - SFML-compatible type stubs
|
||||
- `src/platform/SDL2Renderer.cpp` - OpenGL ES 2 rendering implementation
|
||||
- `src/platform/HeadlessTypes.h` - No-op types for headless mode
|
||||
- `src/EmscriptenStubs.cpp` - JavaScript interop functions
|
||||
- `emscripten_pre.js` - Browser quirk fixes
|
||||
- `shell.html` - HTML template with REPL widget
|
||||
|
||||
## 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: `Scene("name")` object with `scene.on_key` for keyboard events
|
||||
- Entity Creation: `Entity()` with position and sprite properties
|
||||
- Grid Management: `Grid()` for tilemap rendering with cell callbacks
|
||||
- Input Handling: `scene.on_key = handler` receives `(Key, InputState)` enums
|
||||
- Audio: `createSoundBuffer()`, `playSound()`, `setVolume()`
|
||||
- Timers: `Timer("name", callback, interval)` object 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.14 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
|
||||
|
||||
See the [Build System](#build-system) section above for comprehensive build instructions.
|
||||
|
||||
```bash
|
||||
# Quick reference (run from project root!)
|
||||
make # Linux build
|
||||
make windows # Windows cross-compile
|
||||
make clean && make # Full rebuild
|
||||
```
|
||||
|
||||
### 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"); scene = mcrfpy.Scene("test"); scene.activate()' > 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 (or SDL2 when building for wasm) 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 2023
|
||||
- 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)
|
||||
|
||||
test_scene = mcrfpy.Scene("test")
|
||||
ui = test_scene.children
|
||||
ui.append(mcrfpy.Frame(pos=(50,50), size=(100,100)))
|
||||
mcrfpy.current_scene = test_scene
|
||||
timer = mcrfpy.Timer("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
|
||||
# Scene: create and activate a scene, or create another scene
|
||||
mcrfpy.current_scene = mcrfpy.Scene("test")
|
||||
demo_scene = mcrfpy.Scene("demo")
|
||||
|
||||
# Animation: (property, target_value, duration, easing)
|
||||
# direct use of Animation object: deprecated
|
||||
#anim = mcrfpy.Animation("x", 500.0, 2.0, "easeInOut")
|
||||
#anim.start(frame)
|
||||
|
||||
# preferred: create animations directly against the targeted object; use Enum of easing functions
|
||||
frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT)
|
||||
|
||||
# Animation callbacks (#229) receive (target, property, final_value):
|
||||
def on_anim_complete(target, prop, value):
|
||||
print(f"{type(target).__name__}.{prop} reached {value}")
|
||||
frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT, callback=on_anim_complete)
|
||||
|
||||
# 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)
|
||||
# grid center defaults to the position that puts (0, 0) in the top left corner of the grid's visible area.
|
||||
# set grid.center to focus on that position. To position the camera in tile coordinates, use grid.center_camera():
|
||||
grid.center_camera((14.5, 8.5)) # offset of 0.5 tiles to point at the middle of the tile
|
||||
|
||||
# Keyboard handler (#184): receives Key and InputState enums
|
||||
def on_key(key, action):
|
||||
if key == mcrfpy.Key.Num1 and action == mcrfpy.InputState.PRESSED:
|
||||
demo_scene.activate()
|
||||
scene.on_key = on_key
|
||||
|
||||
# Mouse callbacks (#230):
|
||||
# on_click receives (pos: Vector, button: MouseButton, action: InputState)
|
||||
def on_click(pos, button, action):
|
||||
if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED:
|
||||
print(f"Clicked at {pos.x}, {pos.y}")
|
||||
frame.on_click = on_click
|
||||
|
||||
# Hover callbacks (#230) receive only position:
|
||||
def on_enter(pos):
|
||||
print(f"Entered at {pos.x}, {pos.y}")
|
||||
frame.on_enter = on_enter # Also: on_exit, on_move
|
||||
|
||||
# Grid cell callbacks (#230):
|
||||
# on_cell_click receives (cell_pos: Vector, button: MouseButton, action: InputState)
|
||||
# on_cell_enter/on_cell_exit receive only (cell_pos: Vector)
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
### Adding Documentation for New Python Types
|
||||
|
||||
When adding a new Python class/type to the engine, follow these steps to ensure it's properly documented:
|
||||
|
||||
#### 1. Class Docstring (tp_doc)
|
||||
|
||||
In the `PyTypeObject` definition (usually in the header file), set `tp_doc` with a comprehensive docstring:
|
||||
|
||||
```cpp
|
||||
// In PyMyClass.h
|
||||
.tp_doc = PyDoc_STR(
|
||||
"MyClass(arg1: type, arg2: type)\n\n"
|
||||
"Brief description of what this class does.\n\n"
|
||||
"Args:\n"
|
||||
" arg1: Description of first argument.\n"
|
||||
" arg2: Description of second argument.\n\n"
|
||||
"Properties:\n"
|
||||
" prop1 (type, read-only): Description of property.\n"
|
||||
" prop2 (type): Description of writable property.\n\n"
|
||||
"Example:\n"
|
||||
" obj = mcrfpy.MyClass('example', 42)\n"
|
||||
" print(obj.prop1)\n"
|
||||
),
|
||||
```
|
||||
|
||||
#### 2. Method Documentation (PyMethodDef)
|
||||
|
||||
For each method in the `methods[]` array, use the MCRF_* macros:
|
||||
|
||||
```cpp
|
||||
// In PyMyClass.cpp
|
||||
PyMethodDef PyMyClass::methods[] = {
|
||||
{"do_something", (PyCFunction)do_something, METH_VARARGS,
|
||||
MCRF_METHOD(MyClass, do_something,
|
||||
MCRF_SIG("(value: int)", "bool"),
|
||||
MCRF_DESC("Does something with the value."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("value", "The value to process")
|
||||
MCRF_RETURNS("True if successful, False otherwise")
|
||||
)},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. Property Documentation (PyGetSetDef)
|
||||
|
||||
For each property in the `getsetters[]` array, include a docstring:
|
||||
|
||||
```cpp
|
||||
// In PyMyClass.cpp
|
||||
PyGetSetDef PyMyClass::getsetters[] = {
|
||||
{"property_name", (getter)get_property, (setter)set_property,
|
||||
"Property description. Include (type, read-only) if not writable.",
|
||||
NULL},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
```
|
||||
|
||||
**Important for read-only properties:** Include "read-only" in the docstring so the doc generator detects it:
|
||||
```cpp
|
||||
{"name", (getter)get_name, NULL, // NULL setter = read-only
|
||||
"Object name (str, read-only). Unique identifier.",
|
||||
NULL},
|
||||
```
|
||||
|
||||
#### 4. Register Type in Module
|
||||
|
||||
Ensure the type is properly registered in `McRFPy_API.cpp` and its methods/getsetters are assigned:
|
||||
|
||||
```cpp
|
||||
// Set methods and getsetters before PyType_Ready
|
||||
mcrfpydef::PyMyClassType.tp_methods = PyMyClass::methods;
|
||||
mcrfpydef::PyMyClassType.tp_getset = PyMyClass::getsetters;
|
||||
|
||||
// Then call PyType_Ready and add to module
|
||||
```
|
||||
|
||||
#### 5. Regenerate Documentation
|
||||
|
||||
After adding the new type, regenerate all docs:
|
||||
|
||||
```bash
|
||||
make -j4 # Rebuild with new documentation
|
||||
cd build
|
||||
./mcrogueface --headless --exec ../tools/generate_dynamic_docs.py
|
||||
cp docs/API_REFERENCE_DYNAMIC.md ../docs/
|
||||
cp docs/api_reference_dynamic.html ../docs/
|
||||
```
|
||||
|
||||
#### 6. Update Type Stubs (Optional)
|
||||
|
||||
For IDE support, update `stubs/mcrfpy.pyi` with the new class:
|
||||
|
||||
```python
|
||||
class MyClass:
|
||||
"""Brief description."""
|
||||
def __init__(self, arg1: str, arg2: int) -> None: ...
|
||||
@property
|
||||
def prop1(self) -> str: ...
|
||||
def do_something(self, value: int) -> bool: ...
|
||||
```
|
||||
|
||||
### Documentation Extraction Details
|
||||
|
||||
The doc generator (`tools/generate_dynamic_docs.py`) uses Python introspection:
|
||||
|
||||
- **Classes**: Detected via `inspect.isclass()`, docstring from `cls.__doc__`
|
||||
- **Methods**: Detected via `callable()` check on class attributes
|
||||
- **Properties**: Detected via `types.GetSetDescriptorType` (C++ extension) or `property` (Python)
|
||||
- **Read-only detection**: Checks if "read-only" appears in property docstring
|
||||
|
||||
If documentation isn't appearing, verify:
|
||||
1. The type is exported to the `mcrfpy` module
|
||||
2. Methods/getsetters arrays are properly assigned before `PyType_Ready()`
|
||||
3. Docstrings don't contain null bytes or invalid UTF-8
|
||||
|
||||
---
|
||||
|
||||
- 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).
|
||||
407
CMakeLists.txt
|
|
@ -8,361 +8,49 @@ project(McRogueFace)
|
|||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
# Headless build option (no SFML, no graphics - for server/testing/Emscripten prep)
|
||||
option(MCRF_HEADLESS "Build without graphics dependencies (SFML, ImGui)" OFF)
|
||||
|
||||
# SDL2 backend option (SDL2 + OpenGL ES 2 - for Emscripten/WebGL, Android, cross-platform)
|
||||
option(MCRF_SDL2 "Build with SDL2+OpenGL ES 2 backend instead of SFML" OFF)
|
||||
|
||||
# Playground mode - minimal scripts for web playground (REPL-focused)
|
||||
option(MCRF_PLAYGROUND "Build with minimal playground scripts instead of full game" OFF)
|
||||
|
||||
# Game shell mode - fullscreen canvas, no REPL chrome (for itch.io / standalone web games)
|
||||
option(MCRF_GAME_SHELL "Use minimal game-only HTML shell (no REPL)" OFF)
|
||||
|
||||
# Emscripten builds: use SDL2 if specified, otherwise fall back to headless
|
||||
if(EMSCRIPTEN)
|
||||
if(MCRF_SDL2)
|
||||
message(STATUS "Emscripten detected - using SDL2 backend")
|
||||
set(MCRF_HEADLESS OFF)
|
||||
else()
|
||||
set(MCRF_HEADLESS ON)
|
||||
message(STATUS "Emscripten detected - forcing HEADLESS mode (use -DMCRF_SDL2=ON for graphics)")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MCRF_SDL2)
|
||||
message(STATUS "Building with SDL2 backend - SDL2+OpenGL ES 2")
|
||||
endif()
|
||||
|
||||
if(MCRF_PLAYGROUND)
|
||||
message(STATUS "Building in PLAYGROUND mode - minimal scripts for web REPL")
|
||||
endif()
|
||||
|
||||
if(MCRF_HEADLESS)
|
||||
message(STATUS "Building in HEADLESS mode - no SFML/ImGui dependencies")
|
||||
endif()
|
||||
|
||||
# Detect cross-compilation for Windows (MinGW)
|
||||
if(CMAKE_CROSSCOMPILING AND WIN32)
|
||||
set(MCRF_CROSS_WINDOWS TRUE)
|
||||
message(STATUS "Cross-compiling for Windows using MinGW")
|
||||
endif()
|
||||
|
||||
# Add include directories
|
||||
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps)
|
||||
include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/deps/libtcod)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/3d)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/platform)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/tiled)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/ldtk)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/audio)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/modules/RapidXML)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/modules/json/single_include)
|
||||
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux/Python-3.11.1)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
|
||||
|
||||
# Python includes: use different paths for Windows vs Linux vs Emscripten
|
||||
if(EMSCRIPTEN)
|
||||
# Emscripten build: use Python headers compiled for wasm32-emscripten
|
||||
# The pyconfig.h from cross-build has correct LONG_BIT and other settings
|
||||
set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python")
|
||||
# Force-include wasm pyconfig.h BEFORE anything else to set correct platform defines
|
||||
add_compile_options(-include ${PYTHON_WASM_BUILD}/pyconfig.h)
|
||||
# Override LONG_BIT - Emscripten's limits.h incorrectly defines it as 64 for wasm32
|
||||
add_compile_definitions(LONG_BIT=32)
|
||||
# Include wasm build directory FIRST so its pyconfig.h is found by #include "pyconfig.h"
|
||||
include_directories(BEFORE ${PYTHON_WASM_BUILD})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/Include)
|
||||
message(STATUS "Using Emscripten Python from: ${PYTHON_WASM_BUILD}")
|
||||
elseif(MCRF_CROSS_WINDOWS)
|
||||
# Windows cross-compilation: use cpython headers with PC/pyconfig.h
|
||||
# Problem: Python.h uses #include "pyconfig.h" which finds Include/pyconfig.h (Linux) first
|
||||
# Solution: Use -include to force Windows pyconfig.h to be included first
|
||||
# This defines MS_WINDOWS before Python.h is processed, ensuring correct struct layouts
|
||||
add_compile_options(-include ${CMAKE_SOURCE_DIR}/deps/cpython/PC/pyconfig.h)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/Include)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/PC) # For other Windows-specific headers
|
||||
# Also include SFML and libtcod Windows headers
|
||||
include_directories(${CMAKE_SOURCE_DIR}/__lib_windows/sfml/include)
|
||||
include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/include)
|
||||
else()
|
||||
# Native builds (Linux/Windows): use existing Python setup
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
|
||||
endif()
|
||||
|
||||
# ImGui and ImGui-SFML include directories (not needed in headless or SDL2 mode)
|
||||
# SDL2 builds will use ImGui with SDL2 backend later; for now, no ImGui
|
||||
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
|
||||
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
|
||||
)
|
||||
endif()
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
|
||||
|
||||
# Collect all the source files
|
||||
file(GLOB_RECURSE SOURCES "src/*.cpp")
|
||||
|
||||
# Add ImGui sources to the build (only if using SFML)
|
||||
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
|
||||
list(APPEND SOURCES ${IMGUI_SOURCES})
|
||||
# Add GLAD for OpenGL function loading (needed for 3D rendering on SFML)
|
||||
list(APPEND SOURCES "${CMAKE_SOURCE_DIR}/src/3d/glad.c")
|
||||
endif()
|
||||
|
||||
# Find OpenGL (required by ImGui-SFML) - not needed in headless mode
|
||||
# SDL2 builds handle OpenGL ES 2 differently (via SDL2 or Emscripten)
|
||||
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
|
||||
if(MCRF_CROSS_WINDOWS)
|
||||
# For cross-compilation, OpenGL is provided by MinGW
|
||||
set(OPENGL_LIBRARIES opengl32)
|
||||
else()
|
||||
find_package(OpenGL REQUIRED)
|
||||
set(OPENGL_LIBRARIES OpenGL::GL)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Create a list of libraries to link against
|
||||
if(EMSCRIPTEN)
|
||||
# Emscripten build: link against WASM-compiled Python and libtcod
|
||||
set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python")
|
||||
set(PYTHON_WASM_PREFIX "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/prefix")
|
||||
set(LIBTCOD_WASM_BUILD "${CMAKE_SOURCE_DIR}/modules/libtcod-headless/build-emscripten")
|
||||
# Collect HACL crypto object files (not included in libpython3.14.a)
|
||||
file(GLOB PYTHON_HACL_OBJECTS "${PYTHON_WASM_BUILD}/Modules/_hacl/*.o")
|
||||
set(LINK_LIBS
|
||||
${PYTHON_WASM_BUILD}/libpython3.14.a
|
||||
${PYTHON_HACL_OBJECTS}
|
||||
${PYTHON_WASM_BUILD}/Modules/expat/libexpat.a
|
||||
${PYTHON_WASM_PREFIX}/lib/libmpdec.a
|
||||
${PYTHON_WASM_PREFIX}/lib/libffi.a
|
||||
${LIBTCOD_WASM_BUILD}/libtcod.a
|
||||
${LIBTCOD_WASM_BUILD}/_deps/lodepng-c-build/liblodepng-c.a
|
||||
${LIBTCOD_WASM_BUILD}/_deps/utf8proc-build/libutf8proc.a)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux) # Use Linux platform stubs for now
|
||||
# For SDL2 builds, add stb headers for image/font loading
|
||||
if(MCRF_SDL2)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/stb)
|
||||
endif()
|
||||
message(STATUS "Linking Emscripten Python: ${PYTHON_WASM_BUILD}/libpython3.14.a")
|
||||
message(STATUS "Linking Emscripten libtcod: ${LIBTCOD_WASM_BUILD}/libtcod.a")
|
||||
elseif(MCRF_SDL2)
|
||||
# SDL2 build (non-Emscripten): link against SDL2 and system libraries
|
||||
# Note: For desktop SDL2 builds in the future
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
set(LINK_LIBS
|
||||
SDL2::SDL2
|
||||
OpenGL::GL
|
||||
tcod
|
||||
python3.14
|
||||
m dl util pthread)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/stb) # stb_image.h, stb_truetype.h
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
message(STATUS "Building with SDL2 backend (desktop)")
|
||||
elseif(MCRF_HEADLESS)
|
||||
# Headless build: no SFML, no OpenGL
|
||||
if(WIN32 OR MCRF_CROSS_WINDOWS)
|
||||
set(LINK_LIBS
|
||||
libtcod
|
||||
python314)
|
||||
if(MCRF_CROSS_WINDOWS)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/lib)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows)
|
||||
else()
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
endif()
|
||||
else()
|
||||
# Unix/Linux headless build
|
||||
set(LINK_LIBS
|
||||
tcod
|
||||
python3.14
|
||||
m dl util pthread)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
endif()
|
||||
elseif(MCRF_CROSS_WINDOWS)
|
||||
# MinGW cross-compilation: use full library names
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
libtcod
|
||||
python314
|
||||
${OPENGL_LIBRARIES})
|
||||
|
||||
# Add Windows system libraries needed by SFML and MinGW
|
||||
list(APPEND LINK_LIBS
|
||||
winmm # Windows multimedia (for audio)
|
||||
gdi32 # Graphics Device Interface
|
||||
ws2_32 # Winsock (networking, used by some deps)
|
||||
ole32 # OLE support
|
||||
oleaut32 # OLE automation
|
||||
uuid # UUID library
|
||||
comdlg32 # Common dialogs
|
||||
imm32 # Input Method Manager
|
||||
version # Version info
|
||||
)
|
||||
set(LINK_LIBS
|
||||
m
|
||||
dl
|
||||
util
|
||||
pthread
|
||||
python3.12
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod)
|
||||
|
||||
# On Windows, add any additional libs and include directories
|
||||
if(WIN32)
|
||||
# Add the necessary Windows-specific libraries and include directories
|
||||
# include_directories(path_to_additional_includes)
|
||||
# link_directories(path_to_additional_libs)
|
||||
# list(APPEND LINK_LIBS additional_windows_libs)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
|
||||
# Link directories for cross-compiled Windows libs
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/sfml/lib)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/lib)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows)
|
||||
elseif(WIN32)
|
||||
# Native Windows build (MSVC)
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod
|
||||
python314
|
||||
${OPENGL_LIBRARIES})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
else()
|
||||
# Unix/Linux build
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod
|
||||
python3.14
|
||||
m dl util pthread
|
||||
${OPENGL_LIBRARIES})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
endif()
|
||||
|
||||
# Add the directory where the linker should look for the libraries
|
||||
#link_directories(${CMAKE_SOURCE_DIR}/deps_linux)
|
||||
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)
|
||||
# We ALWAYS need this because libtcod headers expect SDL3, not SDL2
|
||||
# Our SDL2 backend is separate from libtcod's SDL3 renderer
|
||||
target_compile_definitions(mcrogueface PRIVATE NO_SDL)
|
||||
|
||||
# Define MCRF_HEADLESS for headless builds (excludes SFML/ImGui code)
|
||||
if(MCRF_HEADLESS)
|
||||
target_compile_definitions(mcrogueface PRIVATE MCRF_HEADLESS)
|
||||
endif()
|
||||
|
||||
# Define MCRF_SDL2 for SDL2 builds (uses SDL2+OpenGL ES 2 instead of SFML)
|
||||
if(MCRF_SDL2)
|
||||
target_compile_definitions(mcrogueface PRIVATE MCRF_SDL2)
|
||||
endif()
|
||||
|
||||
# Asset/script directories for WASM preloading (game projects override these)
|
||||
set(MCRF_ASSETS_DIR "${CMAKE_SOURCE_DIR}/assets" CACHE PATH "Assets directory for WASM preloading")
|
||||
set(MCRF_SCRIPTS_DIR "${CMAKE_SOURCE_DIR}/src/scripts" CACHE PATH "Scripts directory for WASM preloading")
|
||||
set(MCRF_SCRIPTS_PLAYGROUND_DIR "${CMAKE_SOURCE_DIR}/src/scripts_playground" CACHE PATH "Playground scripts for WASM")
|
||||
|
||||
# Emscripten-specific link options (use ports for zlib, bzip2, sqlite3)
|
||||
if(EMSCRIPTEN)
|
||||
# Base Emscripten options
|
||||
set(EMSCRIPTEN_LINK_OPTIONS
|
||||
-sUSE_ZLIB=1
|
||||
-sUSE_BZIP2=1
|
||||
-sUSE_SQLITE3=1
|
||||
-sALLOW_MEMORY_GROWTH=1
|
||||
-sSTACK_SIZE=2097152
|
||||
-sEXPORTED_RUNTIME_METHODS=ccall,cwrap,FS
|
||||
-sEXPORTED_FUNCTIONS=_main,_run_python_string,_run_python_string_with_output,_reset_python_environment,_notify_canvas_resize,_sync_storage
|
||||
-lidbfs.js
|
||||
-sASSERTIONS=2
|
||||
-sSTACK_OVERFLOW_CHECK=2
|
||||
-fexceptions
|
||||
-sNO_DISABLE_EXCEPTION_CATCHING
|
||||
# Disable features that require dynamic linking support
|
||||
-sERROR_ON_UNDEFINED_SYMBOLS=0
|
||||
-sALLOW_UNIMPLEMENTED_SYSCALLS=1
|
||||
# Preload Python stdlib into virtual filesystem at /lib/python3.14
|
||||
--preload-file=${CMAKE_SOURCE_DIR}/wasm_stdlib/lib@/lib
|
||||
# Preload game scripts into /scripts (use playground scripts if MCRF_PLAYGROUND is set)
|
||||
--preload-file=$<IF:$<BOOL:${MCRF_PLAYGROUND}>,${MCRF_SCRIPTS_PLAYGROUND_DIR},${MCRF_SCRIPTS_DIR}>@/scripts
|
||||
# Preload assets
|
||||
--preload-file=${MCRF_ASSETS_DIR}@/assets
|
||||
# Use custom HTML shell - game shell (fullscreen) or playground shell (REPL)
|
||||
--shell-file=${CMAKE_SOURCE_DIR}/src/$<IF:$<BOOL:${MCRF_GAME_SHELL}>,shell_game.html,shell.html>
|
||||
# Pre-JS to fix browser zoom causing undefined values in events
|
||||
--pre-js=${CMAKE_SOURCE_DIR}/src/emscripten_pre.js
|
||||
)
|
||||
|
||||
# Add SDL2 options if using SDL2 backend
|
||||
if(MCRF_SDL2)
|
||||
list(APPEND EMSCRIPTEN_LINK_OPTIONS
|
||||
-sUSE_SDL=2
|
||||
-sUSE_SDL_MIXER=2
|
||||
-sFULL_ES2=1
|
||||
-sMIN_WEBGL_VERSION=2
|
||||
-sMAX_WEBGL_VERSION=2
|
||||
-sUSE_FREETYPE=1
|
||||
)
|
||||
# SDL2, SDL2_mixer, and FreeType flags are also needed at compile time for headers
|
||||
target_compile_options(mcrogueface PRIVATE
|
||||
-sUSE_SDL=2
|
||||
-sUSE_SDL_MIXER=2
|
||||
-sUSE_FREETYPE=1
|
||||
)
|
||||
message(STATUS "Emscripten SDL2 options enabled: -sUSE_SDL=2 -sUSE_SDL_MIXER=2 -sFULL_ES2=1 -sUSE_FREETYPE=1")
|
||||
endif()
|
||||
|
||||
target_link_options(mcrogueface PRIVATE ${EMSCRIPTEN_LINK_OPTIONS})
|
||||
|
||||
# Output as HTML to use the shell file
|
||||
set_target_properties(mcrogueface PROPERTIES SUFFIX ".html")
|
||||
|
||||
# Set Python home for the embedded interpreter
|
||||
target_compile_definitions(mcrogueface PRIVATE
|
||||
MCRF_WASM_PYTHON_HOME="/lib/python3.14"
|
||||
)
|
||||
endif()
|
||||
|
||||
# On Windows, define Py_ENABLE_SHARED for proper Python DLL imports
|
||||
# Py_PYCONFIG_H prevents Include/pyconfig.h (Linux config) from being included
|
||||
# (PC/pyconfig.h already defines HAVE_DECLSPEC_DLL and MS_WINDOWS)
|
||||
if(WIN32 OR MCRF_CROSS_WINDOWS)
|
||||
target_compile_definitions(mcrogueface PRIVATE Py_ENABLE_SHARED Py_PYCONFIG_H)
|
||||
endif()
|
||||
|
||||
# On Windows, set subsystem to WINDOWS to hide console (release builds only)
|
||||
# Use -DMCRF_WINDOWS_CONSOLE=ON for debug builds with console output
|
||||
option(MCRF_WINDOWS_CONSOLE "Keep console window visible for debugging" OFF)
|
||||
|
||||
if(WIN32 AND NOT MCRF_CROSS_WINDOWS)
|
||||
# MSVC-specific flags
|
||||
if(NOT MCRF_WINDOWS_CONSOLE)
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
WIN32_EXECUTABLE TRUE
|
||||
LINK_FLAGS "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup")
|
||||
endif()
|
||||
elseif(MCRF_CROSS_WINDOWS)
|
||||
# MinGW cross-compilation
|
||||
if(NOT MCRF_WINDOWS_CONSOLE)
|
||||
# Release: use -mwindows to hide console
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
WIN32_EXECUTABLE TRUE
|
||||
LINK_FLAGS "-mwindows")
|
||||
else()
|
||||
# Debug: keep console for stdout/stderr output
|
||||
message(STATUS "Windows console enabled for debugging")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Now the linker will find the libraries in the specified directory
|
||||
target_link_libraries(mcrogueface ${LINK_LIBS})
|
||||
|
||||
|
|
@ -379,44 +67,9 @@ add_custom_command(TARGET mcrogueface POST_BUILD
|
|||
# Copy Python standard library to build directory
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib)
|
||||
${CMAKE_SOURCE_DIR}/lib $<TARGET_FILE_DIR:mcrogueface>/lib)
|
||||
|
||||
# On Windows, copy DLLs to executable directory
|
||||
if(MCRF_CROSS_WINDOWS)
|
||||
# Cross-compilation: copy DLLs from __lib_windows
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/sfml/bin $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/bin $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/python314.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/python3.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/vcruntime140.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/vcruntime140_1.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
/usr/x86_64-w64-mingw32/lib/libwinpthread-1.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Copied Windows DLLs to executable directory")
|
||||
|
||||
# Copy Python standard library zip
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/python314.zip $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Copied Python stdlib")
|
||||
elseif(WIN32)
|
||||
# Native Windows build: copy DLLs from __lib
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Copied DLLs to executable directory")
|
||||
endif()
|
||||
|
||||
# rpath for including shared libraries (Linux/Unix only)
|
||||
if(NOT WIN32)
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
INSTALL_RPATH "$ORIGIN/./lib")
|
||||
endif()
|
||||
# rpath for including shared libraries
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
INSTALL_RPATH "./lib")
|
||||
|
||||
|
|
|
|||
242
README.md
|
|
@ -1,226 +1,30 @@
|
|||
# McRogueFace
|
||||
# McRogueFace - 2D Game Engine
|
||||
|
||||
An experimental prototype game engine built for my own use in 7DRL 2023.
|
||||
|
||||
*Blame my wife for the name*
|
||||
|
||||
A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML.
|
||||
## Tenets:
|
||||
|
||||
* Core roguelike logic from libtcod: field of view, pathfinding
|
||||
* Animate sprites with multiple frames. Smooth transitions for positions, sizes, zoom, and camera
|
||||
* Simple GUI element system allows keyboard and mouse input, composition
|
||||
* No compilation or installation necessary. The runtime is a full Python environment; "Zip And Ship"
|
||||
* C++ first, Python close behind.
|
||||
* Entity-Component system based on David Churchill's Memorial University COMP4300 course lectures available on Youtube.
|
||||
* Graphics, particles and shaders provided by SFML.
|
||||
* Pathfinding, noise generation, and other Roguelike goodness provided by TCOD.
|
||||
|
||||
📖 **[Full Documentation & Tutorials](https://mcrogueface.github.io/)** - Quickstart guide, API reference, and cookbook
|
||||
## Why?
|
||||
|
||||
## Quick Start
|
||||
I did the r/RoguelikeDev TCOD tutorial in Python. I loved it, but I did not want to be limited to ASCII. I want to be able to draw pixels on top of my tiles (like lines or circles) and eventually incorporate even more polish.
|
||||
|
||||
**Download** the [latest release](https://github.com/jmccardle/McRogueFace/releases/latest):
|
||||
- **Windows**: `McRogueFace-*-Win.zip`
|
||||
- **Linux**: `McRogueFace-*-Linux.tar.bz2`
|
||||
## To-do
|
||||
|
||||
Extract and run `mcrogueface` (or `mcrogueface.exe` on Windows) to see the demo game.
|
||||
|
||||
### Your First Game
|
||||
|
||||
Create `scripts/game.py` (or edit the existing one):
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Create and activate a scene
|
||||
scene = mcrfpy.Scene("game")
|
||||
scene.activate()
|
||||
|
||||
# Load a sprite sheet
|
||||
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
|
||||
# Create a tile grid
|
||||
grid = mcrfpy.Grid(grid_size=(20, 15), texture=texture, pos=(50, 50), size=(640, 480))
|
||||
grid.zoom = 2.0
|
||||
scene.children.append(grid)
|
||||
|
||||
# Add a player entity
|
||||
player = mcrfpy.Entity(pos=(10, 7), texture=texture, sprite_index=84)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Handle keyboard input
|
||||
def on_key(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
x, y = int(player.x), int(player.y)
|
||||
if key == "W": y -= 1
|
||||
elif key == "S": y += 1
|
||||
elif key == "A": x -= 1
|
||||
elif key == "D": x += 1
|
||||
player.x, player.y = x, y
|
||||
|
||||
scene.on_key = on_key
|
||||
```
|
||||
|
||||
Run `mcrogueface` and you have a movable character!
|
||||
|
||||
### Visual Framework
|
||||
|
||||
- **Sprite**: Single image or sprite from a shared sheet
|
||||
- **Caption**: Text rendering with fonts
|
||||
- **Frame**: Container rectangle for composing UIs
|
||||
- **Grid**: 2D tile array with zoom and camera control
|
||||
- **Entity**: Grid-based game object with sprite and pathfinding
|
||||
- **Animation**: Interpolate any property over time with easing
|
||||
|
||||
## 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: Main Menu with Buttons
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Create a scene
|
||||
scene = mcrfpy.Scene("menu")
|
||||
|
||||
# Add a background frame
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
|
||||
fill_color=mcrfpy.Color(20, 20, 40))
|
||||
scene.children.append(bg)
|
||||
|
||||
# Add a title
|
||||
title = mcrfpy.Caption(pos=(312, 100), text="My Roguelike",
|
||||
fill_color=mcrfpy.Color(255, 255, 100))
|
||||
title.font_size = 48
|
||||
scene.children.append(title)
|
||||
|
||||
# Create a button
|
||||
button = mcrfpy.Frame(pos=(362, 300), size=(300, 80),
|
||||
fill_color=mcrfpy.Color(50, 150, 50))
|
||||
button_text = mcrfpy.Caption(pos=(90, 25), text="Start Game")
|
||||
button.children.append(button_text)
|
||||
|
||||
def on_click(x, y, btn):
|
||||
print("Game starting!")
|
||||
|
||||
button.on_click = on_click
|
||||
scene.children.append(button)
|
||||
|
||||
scene.activate()
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### 📚 Developer Documentation
|
||||
|
||||
For comprehensive documentation about systems, architecture, and development workflows:
|
||||
|
||||
**[Project Wiki](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki)**
|
||||
|
||||
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 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
|
||||
|
||||
## Build Requirements
|
||||
|
||||
- C++17 compiler (GCC 7+ or Clang 5+)
|
||||
- CMake 3.14+
|
||||
- Python 3.14 (embedded)
|
||||
- SFML 2.6
|
||||
- Linux or Windows (macOS untested)
|
||||
|
||||
See [BUILD_FROM_SOURCE.md](BUILD_FROM_SOURCE.md) for detailed compilation instructions.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
McRogueFace/
|
||||
├── assets/ # Sprites, fonts, audio
|
||||
├── build/ # Build output: this is what you distribute
|
||||
│ ├── assets/ # (copied from assets/)
|
||||
│ ├── scripts/ # (copied from src/scripts/)
|
||||
│ └── lib/ # Python stdlib and extension modules
|
||||
├── docs/ # Generated HTML, markdown API docs
|
||||
├── src/ # C++ engine source
|
||||
│ └── scripts/ # Python game scripts
|
||||
├── stubs/ # .pyi type stubs for IDE integration
|
||||
├── tests/ # Automated test suite
|
||||
└── tools/ # Documentation generation scripts
|
||||
```
|
||||
|
||||
If you are building McRogueFace to implement game logic or scene configuration in C++, you'll have to compile the project.
|
||||
|
||||
If you are writing a game in Python using McRogueFace, you only need to rename and zip/distribute the `build` directory.
|
||||
|
||||
## Philosophy
|
||||
|
||||
- **C++ every frame, Python every tick**: All rendering data is handled in C++. Structure your UI and program animations in Python, and they are rendered without Python. All game logic can be written in Python.
|
||||
- **No Compiling Required; Zip And Ship**: Implement your game objects with Python, zip up McRogueFace with your "game.py" to ship
|
||||
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod
|
||||
- **Hands-Off Testing**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration
|
||||
- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter
|
||||
|
||||
## Contributing
|
||||
|
||||
PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request.
|
||||
|
||||
### 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.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see LICENSE file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Developed for 7-Day Roguelike 2023, 2024, 2025, 2026 - here's to many more
|
||||
- Built with [SFML](https://www.sfml-dev.org/), [libtcod](https://github.com/libtcod/libtcod), and Python
|
||||
- Inspired by David Churchill's COMP4300 game engine lectures
|
||||
* ✅ Initial Commit
|
||||
* ✅ Integrate scene, action, entity, component system from COMP4300 engine
|
||||
* ✅ Windows / Visual Studio project
|
||||
* ✅ Draw Sprites
|
||||
* ✅ Play Sounds
|
||||
* ✅ Draw UI, spawn entity from Python code
|
||||
* ❌ Python AI for entities (NPCs on set paths, enemies towards player)
|
||||
* ✅ Walking / Collision
|
||||
* ❌ "Boards" (stairs / doors / walk off edge of screen)
|
||||
* ❌ Cutscenes - interrupt normal controls, text scroll, character portraits
|
||||
* ❌ Mouse integration - tooltips, zoom, click to select targets, cursors
|
||||
|
|
|
|||
120
ROADMAP.md
|
|
@ -1,120 +0,0 @@
|
|||
# McRogueFace - Development Roadmap
|
||||
|
||||
**Version**: 0.2.6-prerelease | **Era**: McRogueFace (2D roguelikes)
|
||||
|
||||
For detailed architecture, philosophy, and decision framework, see the [Strategic Direction](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Strategic-Direction) wiki page. For per-issue tracking, see the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap).
|
||||
|
||||
---
|
||||
|
||||
## What Has Shipped
|
||||
|
||||
**Alpha 0.1** (2024) -- First complete release. Milestone: all datatypes behaving.
|
||||
|
||||
**0.2 series** (Jan-Feb 2026) -- Weekly updates to GitHub. Key additions:
|
||||
- 3D/Voxel pipeline (experimental): Viewport3D, Camera3D, Entity3D, VoxelGrid with greedy meshing and serialization
|
||||
- Procedural generation: HeightMap, BSP, NoiseSource, DiscreteMap
|
||||
- Tiled and LDtk import with Wang tile / AutoRule resolution
|
||||
- Emscripten/SDL2 backend for WebAssembly deployment
|
||||
- Animation callbacks, mouse event system, grid cell callbacks
|
||||
- Multi-layer grid system with chunk-based rendering and dirty-flag caching
|
||||
- Documentation macro system with auto-generated API docs, man pages, and type stubs
|
||||
- Windows cross-compilation, mobile-ish WASM support, SDL2_mixer audio
|
||||
|
||||
**Proving grounds**: Crypt of Sokoban (7DRL 2025) was the first complete game. 7DRL 2026 is the current target.
|
||||
|
||||
---
|
||||
|
||||
## Current Focus: 7DRL 2026
|
||||
|
||||
**Dates**: February 28 -- March 8, 2026
|
||||
|
||||
Engine preparation is complete. All 2D systems are production-ready. The jam will expose remaining rough edges in the workflow of building a complete game on McRogueFace.
|
||||
|
||||
Open prep items:
|
||||
- **#248** -- Crypt of Sokoban Remaster (game content for the jam)
|
||||
|
||||
---
|
||||
|
||||
## Post-7DRL: The Road to 1.0
|
||||
|
||||
After 7DRL, the priority shifts from feature development to **API stability**. 1.0 means the Python API is frozen: documented, stable, and not going to break.
|
||||
|
||||
### API Freeze Process
|
||||
1. Catalog every public Python class, method, and property
|
||||
2. Identify anything that should change before committing (naming, signatures, defaults)
|
||||
3. Make breaking changes in a single coordinated pass
|
||||
4. Document the stable API as the contract
|
||||
5. Experimental modules (3D/Voxel) get an explicit `experimental` label and are exempt from the freeze
|
||||
|
||||
### Post-Jam Priorities
|
||||
- Fix pain points discovered during actual 7DRL game development
|
||||
- Progress on the r/roguelikedev tutorial series (#167)
|
||||
- API consistency audit and freeze
|
||||
- Better pip/virtualenv integration for adding packages to McRogueFace's embedded interpreter
|
||||
|
||||
---
|
||||
|
||||
## Engine Eras
|
||||
|
||||
One engine, accumulating capabilities. Nothing is thrown away.
|
||||
|
||||
| Era | Focus | Status |
|
||||
|-----|-------|--------|
|
||||
| **McRogueFace** | 2D tiles, roguelike systems, procgen | Active -- approaching 1.0 |
|
||||
| **McVectorFace** | Sparse grids, vector graphics, physics | Planned |
|
||||
| **McVoxelFace** | Voxel terrain, 3D gameplay | Proof-of-concept complete |
|
||||
|
||||
---
|
||||
|
||||
## 3D/Voxel Pipeline (Experimental)
|
||||
|
||||
The 3D pipeline is proof-of-concept scouting for the McVoxelFace era. It works and is tested but is explicitly **not** part of the 1.0 API freeze.
|
||||
|
||||
**What exists**: Viewport3D, Camera3D, Entity3D, MeshLayer, Model3D (glTF), Billboard, Shader3D, VoxelGrid with greedy meshing, face culling, RLE serialization, and navigation projection.
|
||||
|
||||
**Known gaps**: Some Entity3D collection methods, animation stubs, shader pipeline incomplete.
|
||||
|
||||
**Maturity track**: These modules will mature on their own timeline, driven by games that need 3D. They won't block 2D stability.
|
||||
|
||||
---
|
||||
|
||||
## Future Directions
|
||||
|
||||
These are ideas on the horizon -- not yet concrete enough for issues, but worth capturing.
|
||||
|
||||
### McRogueFace Lite
|
||||
A spiritual port to MicroPython targeting the PicoCalc and other microcontrollers. Could provide a migration path to retro ROMs or compete in the Pico-8 space. The core idea: strip McRogueFace down to its essential tile/entity/scene model and run it on constrained hardware.
|
||||
|
||||
### McVectorFace Era
|
||||
The next major capability expansion. Sparse grid layers, a polygon/shape rendering class, and eventually physics integration. This would support games that aren't purely tile-based -- top-down action, strategy maps with irregular regions, or hybrid tile+vector visuals. See the [Strategic Direction](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Strategic-Direction) wiki for the full era model.
|
||||
|
||||
### McRogueFace Standard Library
|
||||
A built-in collection of reusable GUI widgets and game UI patterns: menus, dialogs, inventory screens, stat bars, text input fields, scrollable lists. These would ship with the engine as importable Python modules, saving every game from reimplementing the same UI primitives. Think of it as `mcrfpy.widgets` -- batteries included.
|
||||
|
||||
### Pip/Virtualenv Integration
|
||||
Rather than inverting the architecture to make McRogueFace a pip-installable package, the nearer-term goal is better integration in the other direction: making it easy to install and use third-party Python packages within McRogueFace's embedded interpreter. This could mean virtualenv awareness, a `mcrf install` command, or bundling pip itself.
|
||||
|
||||
---
|
||||
|
||||
## Open Issues by Area
|
||||
|
||||
30 open issues across the tracker. Key groupings:
|
||||
|
||||
- **Multi-tile entities** (#233-#237) -- Oversized sprites, composite entities, origin offsets
|
||||
- **Grid enhancements** (#152, #149, #67) -- Sparse layers, refactoring, infinite worlds
|
||||
- **Performance** (#117, #124, #145) -- Memory pools, grid point animation, texture reuse
|
||||
- **LLM agent testbed** (#154, #156, #55) -- Multi-agent simulation, turn-based orchestration
|
||||
- **Platform/distribution** (#70, #54, #62, #53) -- Packaging, Jupyter, multiple windows, input methods
|
||||
- **WASM tooling** (#238-#240) -- Debug infrastructure, automated browser testing, troubleshooting docs
|
||||
- **Rendering** (#107, #218) -- Particle system, Color/Vector animation targets
|
||||
|
||||
See the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current status.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- **Issue Tracker**: [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues)
|
||||
- **Wiki**: [Strategic Direction](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Strategic-Direction), [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap), [Development Workflow](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Development-Workflow)
|
||||
- **Build Guide**: See `CLAUDE.md` for build instructions
|
||||
- **Tutorial**: `roguelike_tutorial/` for implementation examples
|
||||
BIN
assets/Sprite-0001.ase
Normal file
BIN
assets/Sprite-0001.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/alives_other.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
assets/boom.wav
Normal file
BIN
assets/custom_player.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/gamescale_buildings.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/gamescale_decor.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/temp_logo.png
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
assets/terrain.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
assets/terrain_alpha.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/test_portraits.ase
Normal file
BIN
assets/test_portraits.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
54
build.sh
|
|
@ -1,54 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Build script for McRogueFace - compiles everything into ./build directory
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}McRogueFace Build Script${NC}"
|
||||
echo "========================="
|
||||
|
||||
# Create build directory if it doesn't exist
|
||||
if [ ! -d "build" ]; then
|
||||
echo -e "${YELLOW}Creating build directory...${NC}"
|
||||
mkdir build
|
||||
fi
|
||||
|
||||
# Change to build directory
|
||||
cd build
|
||||
|
||||
# Run CMake to generate build files
|
||||
echo -e "${YELLOW}Running CMake...${NC}"
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
# Check if CMake succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}CMake configuration failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run make with parallel jobs
|
||||
echo -e "${YELLOW}Building with make...${NC}"
|
||||
make -j$(nproc)
|
||||
|
||||
# Check if make succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Build failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Build completed successfully!${NC}"
|
||||
echo ""
|
||||
echo "The build directory contains:"
|
||||
ls -la
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}To run McRogueFace:${NC}"
|
||||
echo " cd build"
|
||||
echo " ./mcrogueface"
|
||||
echo ""
|
||||
echo -e "${GREEN}To create a distribution archive:${NC}"
|
||||
echo " cd build"
|
||||
echo " zip -r ../McRogueFace-$(date +%Y%m%d).zip ."
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
@echo off
|
||||
REM Windows build script for McRogueFace
|
||||
REM Run this over SSH without Visual Studio GUI
|
||||
|
||||
echo Building McRogueFace for Windows...
|
||||
|
||||
REM Clean previous build
|
||||
if exist build_win rmdir /s /q build_win
|
||||
mkdir build_win
|
||||
cd build_win
|
||||
|
||||
REM Generate Visual Studio project files with CMake
|
||||
REM Use -G to specify generator, -A for architecture
|
||||
REM Visual Studio 2022 = "Visual Studio 17 2022"
|
||||
REM Visual Studio 2019 = "Visual Studio 16 2019"
|
||||
cmake -G "Visual Studio 17 2022" -A x64 ..
|
||||
if errorlevel 1 (
|
||||
echo CMake configuration failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build using MSBuild (comes with Visual Studio)
|
||||
REM You can also use cmake --build . --config Release
|
||||
msbuild McRogueFace.sln /p:Configuration=Release /p:Platform=x64 /m
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Build completed successfully!
|
||||
echo Executable location: build_win\Release\mcrogueface.exe
|
||||
|
||||
REM Alternative: Using cmake to build (works with any generator)
|
||||
REM cmake --build . --config Release --parallel
|
||||
|
||||
cd ..
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
@echo off
|
||||
REM Windows build script using cmake --build (generator-agnostic)
|
||||
REM This version works with any CMake generator
|
||||
|
||||
echo Building McRogueFace for Windows using CMake...
|
||||
|
||||
REM Set build directory
|
||||
set BUILD_DIR=build_win
|
||||
set CONFIG=Release
|
||||
|
||||
REM Clean previous build
|
||||
if exist %BUILD_DIR% rmdir /s /q %BUILD_DIR%
|
||||
mkdir %BUILD_DIR%
|
||||
cd %BUILD_DIR%
|
||||
|
||||
REM Configure with CMake
|
||||
REM You can change the generator here if needed:
|
||||
REM -G "Visual Studio 17 2022" (VS 2022)
|
||||
REM -G "Visual Studio 16 2019" (VS 2019)
|
||||
REM -G "MinGW Makefiles" (MinGW)
|
||||
REM -G "Ninja" (Ninja build system)
|
||||
cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=%CONFIG% ..
|
||||
if errorlevel 1 (
|
||||
echo CMake configuration failed!
|
||||
cd ..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build using cmake (works with any generator)
|
||||
cmake --build . --config %CONFIG% --parallel
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
cd ..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Build completed successfully!
|
||||
echo Executable: %BUILD_DIR%\%CONFIG%\mcrogueface.exe
|
||||
echo.
|
||||
|
||||
cd ..
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# CMake toolchain file for cross-compiling to Windows using MinGW-w64
|
||||
# Usage: cmake -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64-x86_64.cmake ..
|
||||
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
|
||||
# Specify the cross-compiler (use posix variant for std::mutex support)
|
||||
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc-posix)
|
||||
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++-posix)
|
||||
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||
|
||||
# Target environment location
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||
|
||||
# Add MinGW system include directories for Windows headers
|
||||
include_directories(SYSTEM /usr/x86_64-w64-mingw32/include)
|
||||
|
||||
# Adjust search behavior
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
|
||||
# Static linking of libgcc and libstdc++ to avoid runtime dependency issues
|
||||
# Enable auto-import for Python DLL data symbols
|
||||
set(CMAKE_EXE_LINKER_FLAGS_INIT "-static-libgcc -static-libstdc++ -Wl,--enable-auto-import")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-static-libgcc -static-libstdc++ -Wl,--enable-auto-import")
|
||||
|
||||
# Windows-specific defines
|
||||
add_definitions(-DWIN32 -D_WIN32 -D_WINDOWS)
|
||||
add_definitions(-DMINGW_HAS_SECURE_API)
|
||||
|
||||
# Disable console window for GUI applications (optional, can be overridden)
|
||||
# set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -mwindows")
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
[
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/GameEngine.cpp.o -c /home/john/Development/McRogueFace/src/GameEngine.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/GameEngine.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/IndexTexture.cpp.o -c /home/john/Development/McRogueFace/src/IndexTexture.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/IndexTexture.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/McRFPy_API.cpp.o -c /home/john/Development/McRogueFace/src/McRFPy_API.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/McRFPy_API.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyCallable.cpp.o -c /home/john/Development/McRogueFace/src/PyCallable.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyCallable.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyColor.cpp.o -c /home/john/Development/McRogueFace/src/PyColor.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyColor.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyFont.cpp.o -c /home/john/Development/McRogueFace/src/PyFont.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyFont.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyScene.cpp.o -c /home/john/Development/McRogueFace/src/PyScene.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyScene.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyTexture.cpp.o -c /home/john/Development/McRogueFace/src/PyTexture.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyTexture.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyVector.cpp.o -c /home/john/Development/McRogueFace/src/PyVector.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyVector.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Resources.cpp.o -c /home/john/Development/McRogueFace/src/Resources.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/Resources.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Scene.cpp.o -c /home/john/Development/McRogueFace/src/Scene.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/Scene.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Timer.cpp.o -c /home/john/Development/McRogueFace/src/Timer.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/Timer.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICaption.cpp.o -c /home/john/Development/McRogueFace/src/UICaption.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UICaption.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICollection.cpp.o -c /home/john/Development/McRogueFace/src/UICollection.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UICollection.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIDrawable.cpp.o -c /home/john/Development/McRogueFace/src/UIDrawable.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIDrawable.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIEntity.cpp.o -c /home/john/Development/McRogueFace/src/UIEntity.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIEntity.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIFrame.cpp.o -c /home/john/Development/McRogueFace/src/UIFrame.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIFrame.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGrid.cpp.o -c /home/john/Development/McRogueFace/src/UIGrid.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIGrid.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGridPoint.cpp.o -c /home/john/Development/McRogueFace/src/UIGridPoint.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIGridPoint.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UISprite.cpp.o -c /home/john/Development/McRogueFace/src/UISprite.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UISprite.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UITestScene.cpp.o -c /home/john/Development/McRogueFace/src/UITestScene.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UITestScene.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/main.cpp.o -c /home/john/Development/McRogueFace/src/main.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/main.cpp"
|
||||
}
|
||||
]
|
||||
52
deps/platform/linux/platform.h
vendored
|
|
@ -1,54 +1,6 @@
|
|||
#ifndef __PLATFORM
|
||||
#define __PLATFORM
|
||||
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// WASM/Emscripten platform - no /proc filesystem, limited std::filesystem support
|
||||
|
||||
std::wstring executable_path()
|
||||
{
|
||||
// In WASM, the executable is at the root of the virtual filesystem
|
||||
return L"/";
|
||||
}
|
||||
|
||||
std::wstring executable_filename()
|
||||
{
|
||||
// In WASM, we use a fixed executable name
|
||||
return L"/mcrogueface";
|
||||
}
|
||||
|
||||
std::wstring working_path()
|
||||
{
|
||||
// In WASM, working directory is root of virtual filesystem
|
||||
return L"/";
|
||||
}
|
||||
|
||||
std::string narrow_string(std::wstring convertme)
|
||||
{
|
||||
// Simple conversion for ASCII/UTF-8 compatible strings
|
||||
std::string result;
|
||||
result.reserve(convertme.size());
|
||||
for (wchar_t wc : convertme) {
|
||||
if (wc < 128) {
|
||||
result.push_back(static_cast<char>(wc));
|
||||
} else {
|
||||
// For non-ASCII, use a simple UTF-8 encoding
|
||||
if (wc < 0x800) {
|
||||
result.push_back(static_cast<char>(0xC0 | (wc >> 6)));
|
||||
result.push_back(static_cast<char>(0x80 | (wc & 0x3F)));
|
||||
} else {
|
||||
result.push_back(static_cast<char>(0xE0 | (wc >> 12)));
|
||||
result.push_back(static_cast<char>(0x80 | ((wc >> 6) & 0x3F)));
|
||||
result.push_back(static_cast<char>(0x80 | (wc & 0x3F)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#else
|
||||
// Native Linux platform
|
||||
|
||||
std::wstring executable_path()
|
||||
{
|
||||
/*
|
||||
|
|
@ -85,6 +37,4 @@ std::string narrow_string(std::wstring convertme)
|
|||
return converter.to_bytes(convertme);
|
||||
}
|
||||
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
||||
#endif // __PLATFORM
|
||||
#endif
|
||||
|
|
|
|||
8
deps/platform/windows/platform.h
vendored
|
|
@ -1,12 +1,12 @@
|
|||
#ifndef __PLATFORM
|
||||
#define __PLATFORM
|
||||
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
|
||||
#include <windows.h>
|
||||
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 0
|
||||
#include <Windows.h>
|
||||
|
||||
std::wstring executable_path()
|
||||
{
|
||||
wchar_t buffer[MAX_PATH];
|
||||
GetModuleFileNameW(NULL, buffer, MAX_PATH); // Use explicit Unicode version
|
||||
GetModuleFileName(NULL, buffer, MAX_PATH);
|
||||
std::wstring exec_path = buffer;
|
||||
size_t path_index = exec_path.find_last_of(L"\\/");
|
||||
return exec_path.substr(0, path_index);
|
||||
|
|
@ -15,7 +15,7 @@ std::wstring executable_path()
|
|||
std::wstring executable_filename()
|
||||
{
|
||||
wchar_t buffer[MAX_PATH];
|
||||
GetModuleFileNameW(NULL, buffer, MAX_PATH); // Use explicit Unicode version
|
||||
GetModuleFileName(NULL, buffer, MAX_PATH);
|
||||
std::wstring exec_path = buffer;
|
||||
return exec_path;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,860 +0,0 @@
|
|||
# McRogueFace Emscripten & Renderer Abstraction Research
|
||||
|
||||
**Date**: 2026-01-30
|
||||
**Branch**: `emscripten-mcrogueface`
|
||||
**Related Issues**: #157 (True Headless), #158 (Emscripten/WASM)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document analyzes the technical requirements for:
|
||||
1. **SFML 2.6 → 3.0 migration** (modernization)
|
||||
2. **Emscripten/WebAssembly compilation** (browser deployment)
|
||||
|
||||
Both goals share a common prerequisite: **renderer abstraction**. The codebase already has a partial abstraction via `sf::RenderTarget*` pointer, but SFML types are pervasive (1276 occurrences across 78 files).
|
||||
|
||||
**Key Insight**: This is a **build-time configuration**, not runtime switching. The standard McRogueFace binary remains a dynamic environment; Emscripten builds bundle assets and scripts at compile time.
|
||||
|
||||
---
|
||||
|
||||
## Current Architecture Analysis
|
||||
|
||||
### Existing Abstraction Strengths
|
||||
|
||||
1. **RenderTarget Pointer Pattern** (`GameEngine.h:156`)
|
||||
```cpp
|
||||
sf::RenderTarget* render_target;
|
||||
// Points to either window.get() or headless_renderer->getRenderTarget()
|
||||
```
|
||||
This already decouples rendering logic from the specific backend.
|
||||
|
||||
2. **HeadlessRenderer** (`src/HeadlessRenderer.h`)
|
||||
- Uses `sf::RenderTexture` internally
|
||||
- Provides unified interface: `getRenderTarget()`, `display()`, `saveScreenshot()`
|
||||
- Demonstrates the pattern for additional backends
|
||||
|
||||
3. **UIDrawable Hierarchy**
|
||||
- Virtual `render(sf::Vector2f, sf::RenderTarget&)` method
|
||||
- 7 drawable types: Frame, Caption, Sprite, Entity, Grid, Line, Circle, Arc
|
||||
- Each manages its own SFML primitives internally
|
||||
|
||||
4. **Asset Wrappers**
|
||||
- `PyTexture`, `PyFont`, `PyShader` wrap SFML types
|
||||
- Python reference counting integrated
|
||||
- Single point of change for asset loading APIs
|
||||
|
||||
### Current SFML Coupling Points
|
||||
|
||||
| Area | Count | Difficulty | Notes |
|
||||
|------|-------|------------|-------|
|
||||
| `sf::Vector2f` | ~200+ | Medium | Used everywhere for positions, sizes |
|
||||
| `sf::Color` | ~100+ | Easy | Simple 4-byte struct replacement |
|
||||
| `sf::FloatRect` | ~50+ | Medium | Bounds, intersection testing |
|
||||
| `sf::RenderTexture` | ~20 | Hard | Shader effects, caching |
|
||||
| `sf::Sprite/Text` | ~30 | Hard | Core rendering primitives |
|
||||
| `sf::Event` | ~15 | Medium | Input system coupling |
|
||||
| `sf::Keyboard/Mouse` | ~50+ | Easy | Enum mappings |
|
||||
|
||||
Total: **1276 occurrences across 78 files**
|
||||
|
||||
---
|
||||
|
||||
## SFML 3.0 Migration Analysis
|
||||
|
||||
### Breaking Changes Requiring Code Updates
|
||||
|
||||
#### 1. Vector Parameters (High Impact)
|
||||
```cpp
|
||||
// SFML 2.6
|
||||
setPosition(10, 20);
|
||||
sf::VideoMode(1024, 768, 32);
|
||||
sf::FloatRect(x, y, w, h);
|
||||
|
||||
// SFML 3.0
|
||||
setPosition({10, 20});
|
||||
sf::VideoMode({1024, 768}, 32);
|
||||
sf::FloatRect({x, y}, {w, h});
|
||||
```
|
||||
|
||||
**Strategy**: Regex-based search/replace with manual verification.
|
||||
|
||||
#### 2. Rect Member Changes (Medium Impact)
|
||||
```cpp
|
||||
// SFML 2.6
|
||||
rect.left, rect.top, rect.width, rect.height
|
||||
rect.getPosition(), rect.getSize()
|
||||
|
||||
// SFML 3.0
|
||||
rect.position.x, rect.position.y, rect.size.x, rect.size.y
|
||||
rect.position, rect.size // direct access
|
||||
rect.findIntersection() -> std::optional<Rect<T>>
|
||||
```
|
||||
|
||||
#### 3. Resource Constructors (Low Impact)
|
||||
```cpp
|
||||
// SFML 2.6
|
||||
sf::Sound sound; // default constructible
|
||||
sound.setBuffer(buffer);
|
||||
|
||||
// SFML 3.0
|
||||
sf::Sound sound(buffer); // requires buffer at construction
|
||||
```
|
||||
|
||||
#### 4. Keyboard/Mouse Enum Scoping (Medium Impact)
|
||||
```cpp
|
||||
// SFML 2.6
|
||||
sf::Keyboard::A
|
||||
sf::Mouse::Left
|
||||
|
||||
// SFML 3.0
|
||||
sf::Keyboard::Key::A
|
||||
sf::Mouse::Button::Left
|
||||
```
|
||||
|
||||
#### 5. Event Handling (Medium Impact)
|
||||
```cpp
|
||||
// SFML 2.6
|
||||
sf::Event event;
|
||||
while (window.pollEvent(event)) {
|
||||
if (event.type == sf::Event::Closed) ...
|
||||
}
|
||||
|
||||
// SFML 3.0
|
||||
while (auto event = window.pollEvent()) {
|
||||
if (event->is<sf::Event::Closed>()) ...
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. CMake Target Changes
|
||||
```cmake
|
||||
# SFML 2.6
|
||||
find_package(SFML 2 REQUIRED COMPONENTS graphics audio)
|
||||
target_link_libraries(app sfml-graphics sfml-audio)
|
||||
|
||||
# SFML 3.0
|
||||
find_package(SFML 3 REQUIRED COMPONENTS Graphics Audio)
|
||||
target_link_libraries(app SFML::Graphics SFML::Audio)
|
||||
```
|
||||
|
||||
### Migration Effort Estimate
|
||||
|
||||
| Phase | Files | Changes | Effort |
|
||||
|-------|-------|---------|--------|
|
||||
| CMakeLists.txt | 1 | Target names | 1 hour |
|
||||
| Vector parameters | 30+ | ~200 calls | 4-8 hours |
|
||||
| Rect refactoring | 20+ | ~50 usages | 2-4 hours |
|
||||
| Event handling | 5 | ~15 sites | 2 hours |
|
||||
| Keyboard/Mouse | 10 | ~50 enums | 2 hours |
|
||||
| Resource constructors | 10 | ~30 sites | 2 hours |
|
||||
| **Total** | - | - | **~15-25 hours** |
|
||||
|
||||
---
|
||||
|
||||
## Emscripten/VRSFML Analysis
|
||||
|
||||
### Why VRSFML Over Waiting for SFML 4.x?
|
||||
|
||||
1. **Available Now**: VRSFML is working today with browser demos
|
||||
2. **Modern OpenGL**: Removes legacy calls, targets OpenGL ES 3.0+ (WebGL 2)
|
||||
3. **SFML_GAME_LOOP Macro**: Handles blocking vs callback loop abstraction
|
||||
4. **Performance**: 500k sprites @ 60FPS vs 3 FPS upstream (batching)
|
||||
5. **SFML 4.x Timeline**: Unknown, potentially years away
|
||||
|
||||
### VRSFML API Differences from SFML
|
||||
|
||||
| Feature | SFML 2.6/3.0 | VRSFML |
|
||||
|---------|--------------|--------|
|
||||
| Default constructors | Allowed | Not allowed for resources |
|
||||
| Texture ownership | Pointer in Sprite | Passed at draw time |
|
||||
| Context management | Hidden global | Explicit `GraphicsContext` |
|
||||
| Drawable base class | Polymorphic | Removed |
|
||||
| Loading methods | `loadFromFile()` returns bool | Returns `std::optional` |
|
||||
| Main loop | `while(running)` | `SFML_GAME_LOOP { }` |
|
||||
|
||||
### Main Loop Refactoring
|
||||
|
||||
Current blocking loop:
|
||||
```cpp
|
||||
void GameEngine::run() {
|
||||
while (running) {
|
||||
processEvents();
|
||||
update();
|
||||
render();
|
||||
display();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Emscripten-compatible pattern:
|
||||
```cpp
|
||||
// Option A: VRSFML macro
|
||||
SFML_GAME_LOOP {
|
||||
processEvents();
|
||||
update();
|
||||
render();
|
||||
display();
|
||||
}
|
||||
|
||||
// Option B: Manual Emscripten integration
|
||||
#ifdef __EMSCRIPTEN__
|
||||
void mainLoopCallback() {
|
||||
if (!game.running) {
|
||||
emscripten_cancel_main_loop();
|
||||
return;
|
||||
}
|
||||
game.doFrame();
|
||||
}
|
||||
emscripten_set_main_loop(mainLoopCallback, 0, 1);
|
||||
#else
|
||||
while (running) { doFrame(); }
|
||||
#endif
|
||||
```
|
||||
|
||||
**Recommendation**: Use preprocessor-based approach with `doFrame()` extraction for cleaner separation.
|
||||
|
||||
---
|
||||
|
||||
## Build-Time Configuration Strategy
|
||||
|
||||
### Normal Build (Desktop)
|
||||
- Dynamic loading of assets from `assets/` directory
|
||||
- Python scripts loaded from `scripts/` directory at runtime
|
||||
- Full McRogueFace environment with dynamic game loading
|
||||
|
||||
### Emscripten Build (Web)
|
||||
- Assets bundled via `--preload-file assets`
|
||||
- Scripts bundled via `--preload-file scripts`
|
||||
- Virtual filesystem (MEMFS/IDBFS)
|
||||
- Optional: Script linting with Pyodide before bundling
|
||||
- Single-purpose deployment (one game per build)
|
||||
|
||||
### CMake Configuration
|
||||
```cmake
|
||||
option(MCRF_BUILD_EMSCRIPTEN "Build for Emscripten/WebAssembly" OFF)
|
||||
|
||||
if(MCRF_BUILD_EMSCRIPTEN)
|
||||
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/cmake/toolchains/emscripten.cmake)
|
||||
add_definitions(-DMCRF_EMSCRIPTEN)
|
||||
|
||||
# Bundle assets
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} \
|
||||
--preload-file ${CMAKE_SOURCE_DIR}/assets@/assets \
|
||||
--preload-file ${CMAKE_SOURCE_DIR}/scripts@/scripts")
|
||||
endif()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phased Implementation Plan
|
||||
|
||||
### Phase 0: Preparation (This PR)
|
||||
- [ ] Create `docs/EMSCRIPTEN_RESEARCH.md` (this document)
|
||||
- [ ] Update Gitea issues #157, #158 with findings
|
||||
- [ ] Identify specific files requiring changes
|
||||
- [ ] Create test matrix for rendering features
|
||||
|
||||
### Phase 1: Type Abstraction Layer
|
||||
**Goal**: Isolate SFML types behind McRogueFace wrappers
|
||||
|
||||
```cpp
|
||||
// src/types/McrfTypes.h
|
||||
namespace mcrf {
|
||||
using Vector2f = sf::Vector2f; // Alias initially, replace later
|
||||
using Color = sf::Color;
|
||||
using FloatRect = sf::FloatRect;
|
||||
}
|
||||
```
|
||||
|
||||
Changes:
|
||||
- [ ] Create `src/types/` directory with wrapper types
|
||||
- [ ] Gradually replace `sf::` with `mcrf::` namespace
|
||||
- [ ] Update Common.h to provide both namespaces during transition
|
||||
|
||||
### Phase 2: Main Loop Extraction
|
||||
**Goal**: Make game loop callback-compatible
|
||||
|
||||
- [ ] Extract `GameEngine::doFrame()` from `run()`
|
||||
- [ ] Add `#ifdef __EMSCRIPTEN__` conditional in `run()`
|
||||
- [ ] Test that desktop behavior is unchanged
|
||||
|
||||
### Phase 3: Render Backend Interface
|
||||
**Goal**: Abstract RenderTarget operations
|
||||
|
||||
```cpp
|
||||
class RenderBackend {
|
||||
public:
|
||||
virtual ~RenderBackend() = default;
|
||||
virtual void clear(const Color& color) = 0;
|
||||
virtual void draw(const Sprite& sprite) = 0;
|
||||
virtual void draw(const Text& text) = 0;
|
||||
virtual void display() = 0;
|
||||
virtual bool isOpen() const = 0;
|
||||
virtual Vector2u getSize() const = 0;
|
||||
};
|
||||
|
||||
class SFMLBackend : public RenderBackend { ... };
|
||||
class VRSFMLBackend : public RenderBackend { ... }; // Future
|
||||
```
|
||||
|
||||
### Phase 4: SFML 3.0 Migration
|
||||
**Goal**: Update to SFML 3.0 API
|
||||
|
||||
- [ ] Update CMakeLists.txt targets
|
||||
- [ ] Fix vector parameter calls
|
||||
- [ ] Fix rect member access
|
||||
- [ ] Fix event handling
|
||||
- [ ] Fix keyboard/mouse enums
|
||||
- [ ] Test thoroughly
|
||||
|
||||
### Phase 5: VRSFML Integration (Experimental)
|
||||
**Goal**: Add VRSFML as alternative backend
|
||||
|
||||
- [ ] Add VRSFML as submodule/dependency
|
||||
- [ ] Implement VRSFMLBackend
|
||||
- [ ] Add Emscripten CMake configuration
|
||||
- [ ] Test in browser
|
||||
|
||||
### Phase 6: Python-in-WASM
|
||||
**Goal**: Get Python scripting working in browser
|
||||
|
||||
**High Risk** - This is the major unknown:
|
||||
- [ ] Build CPython for Emscripten
|
||||
- [ ] Test `McRFPy_API` binding compatibility
|
||||
- [ ] Evaluate Pyodide vs raw CPython
|
||||
- [ ] Handle filesystem virtualization
|
||||
- [ ] Test threading limitations
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Probability | Impact | Mitigation |
|
||||
|------|-------------|--------|------------|
|
||||
| SFML 3.0 breaks unexpected code | Medium | Medium | Comprehensive test suite |
|
||||
| VRSFML API too different | Low | High | Can fork/patch VRSFML |
|
||||
| Python-in-WASM fails | Medium | Critical | Evaluate Pyodide early |
|
||||
| Performance regression | Low | Medium | Benchmark before/after |
|
||||
| Binary size too large | Medium | Medium | Lazy loading, stdlib trimming |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
### SFML 3.0
|
||||
- [Migration Guide](https://www.sfml-dev.org/tutorials/3.0/getting-started/migrate/)
|
||||
- [Changelog](https://www.sfml-dev.org/development/changelog/)
|
||||
- [Release Notes](https://github.com/SFML/SFML/releases/tag/3.0.0)
|
||||
|
||||
### VRSFML/Emscripten
|
||||
- [VRSFML Blog Post](https://vittorioromeo.com/index/blog/vrsfml.html)
|
||||
- [VRSFML GitHub](https://github.com/vittorioromeo/VRSFML)
|
||||
- [Browser Demos](https://vittorioromeo.github.io/VRSFML_HTML5_Examples/)
|
||||
|
||||
### Python WASM
|
||||
- [PEP 776 - Python Emscripten Support](https://peps.python.org/pep-0776/)
|
||||
- [CPython WASM Build Guide](https://github.com/python/cpython/blob/main/Tools/wasm/README.md)
|
||||
- [Pyodide](https://github.com/pyodide/pyodide)
|
||||
|
||||
### Related Issues
|
||||
- [SFML Emscripten Discussion #1494](https://github.com/SFML/SFML/issues/1494)
|
||||
- [libtcod Emscripten #41](https://github.com/libtcod/libtcod/issues/41)
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: File-by-File SFML Usage Inventory
|
||||
|
||||
### Critical Files (Must Abstract for Emscripten)
|
||||
|
||||
| File | SFML Types Used | Role | Abstraction Difficulty |
|
||||
|------|-----------------|------|------------------------|
|
||||
| `GameEngine.h/cpp` | RenderWindow, Clock, Font, Event | Main loop, window | **CRITICAL** |
|
||||
| `HeadlessRenderer.h/cpp` | RenderTexture | Headless backend | **CRITICAL** |
|
||||
| `UIDrawable.h/cpp` | Vector2f, RenderTarget, FloatRect | Base render interface | **HARD** |
|
||||
| `UIFrame.h/cpp` | RectangleShape, Vector2f, Color | Container rendering | **HARD** |
|
||||
| `UISprite.h/cpp` | Sprite, Texture, Vector2f | Texture display | **HARD** |
|
||||
| `UICaption.h/cpp` | Text, Font, Vector2f, Color | Text rendering | **HARD** |
|
||||
| `UIGrid.h/cpp` | RenderTexture, Sprite, Vector2f | Tile grid system | **HARD** |
|
||||
| `UIEntity.h/cpp` | Sprite, Vector2f | Game entities | **HARD** |
|
||||
| `UICircle.h/cpp` | CircleShape, Vector2f, Color | Circle shape | **MEDIUM** |
|
||||
| `UILine.h/cpp` | VertexArray, Vector2f, Color | Line rendering | **MEDIUM** |
|
||||
| `UIArc.h/cpp` | CircleShape segments, Vector2f | Arc shape | **MEDIUM** |
|
||||
| `Scene.h/cpp` | Vector2f, RenderTarget | Scene management | **MEDIUM** |
|
||||
| `SceneTransition.h/cpp` | RenderTexture, Sprite | Transitions | **MEDIUM** |
|
||||
|
||||
### Wrapper Files (Already Partially Abstracted)
|
||||
|
||||
| File | SFML Types Wrapped | Python API | Notes |
|
||||
|------|-------------------|------------|-------|
|
||||
| `PyVector.h/cpp` | sf::Vector2f | Vector | Ready for backend swap |
|
||||
| `PyColor.h/cpp` | sf::Color | Color | Ready for backend swap |
|
||||
| `PyTexture.h/cpp` | sf::Texture | Texture | Asset loading needs work |
|
||||
| `PyFont.h/cpp` | sf::Font | Font | Asset loading needs work |
|
||||
| `PyShader.h/cpp` | sf::Shader | Shader | Optional feature |
|
||||
|
||||
### Input System Files
|
||||
|
||||
| File | SFML Types Used | Notes |
|
||||
|------|-----------------|-------|
|
||||
| `ActionCode.h` | Keyboard::Key, Mouse::Button | Enum encoding only |
|
||||
| `PyKey.h/cpp` | Keyboard::Key enum | 140+ key mappings |
|
||||
| `PyMouseButton.h/cpp` | Mouse::Button enum | Simple enum |
|
||||
| `PyKeyboard.h/cpp` | Keyboard::isKeyPressed | State queries |
|
||||
| `PyMouse.h/cpp` | Mouse::getPosition | Position queries |
|
||||
| `PyInputState.h/cpp` | None (pure enum) | No SFML dependency |
|
||||
|
||||
### Support Files (Low Priority)
|
||||
|
||||
| File | SFML Types Used | Notes |
|
||||
|------|-----------------|-------|
|
||||
| `Animation.h/cpp` | Vector2f, Color (as values) | Pure data animation |
|
||||
| `GridLayers.h/cpp` | RenderTexture, Color | Layer caching |
|
||||
| `IndexTexture.h/cpp` | Texture, IntRect | Legacy texture format |
|
||||
| `Resources.h/cpp` | Font | Global font storage |
|
||||
| `ProfilerOverlay.cpp` | Text, RectangleShape | Debug overlay |
|
||||
| `McRFPy_Automation.h/cpp` | Various | Testing only |
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Recommended First Steps
|
||||
|
||||
### Immediate (Non-Breaking Changes)
|
||||
|
||||
1. **Extract `GameEngine::doFrame()`**
|
||||
- Move loop body to separate method
|
||||
- No API changes, just internal refactoring
|
||||
- Enables future Emscripten callback integration
|
||||
|
||||
2. **Create type aliases in Common.h**
|
||||
```cpp
|
||||
namespace mcrf {
|
||||
using Vector2f = sf::Vector2f;
|
||||
using Vector2i = sf::Vector2i;
|
||||
using Color = sf::Color;
|
||||
using FloatRect = sf::FloatRect;
|
||||
}
|
||||
```
|
||||
- Allows gradual migration from `sf::` to `mcrf::`
|
||||
- No functional changes
|
||||
|
||||
3. **Document current render path**
|
||||
- Add comments to key rendering functions
|
||||
- Identify all `target.draw()` call sites
|
||||
- Create rendering flow diagram
|
||||
|
||||
### Short-Term (Preparation for SFML 3.0)
|
||||
|
||||
1. **Audit vector parameter calls**
|
||||
- Find all `setPosition(x, y)` style calls
|
||||
- Prepare regex patterns for migration
|
||||
|
||||
2. **Audit rect member access**
|
||||
- Find all `.left`, `.top`, `.width`, `.height` uses
|
||||
- Prepare for `.position.x`, `.size.x` style
|
||||
|
||||
3. **Test suite expansion**
|
||||
- Add rendering validation tests
|
||||
- Screenshot comparison tests
|
||||
- Animation correctness tests
|
||||
|
||||
---
|
||||
|
||||
## Appendix C: libtcod Architecture Analysis
|
||||
|
||||
**Key Finding**: libtcod uses a much simpler abstraction pattern than initially proposed.
|
||||
|
||||
### libtcod's Context Vtable Pattern
|
||||
|
||||
libtcod doesn't wrap every SDL type. Instead, it abstracts at the **context level** using a C-style vtable:
|
||||
|
||||
```c
|
||||
struct TCOD_Context {
|
||||
int type;
|
||||
void* contextdata_; // Backend-specific data (opaque pointer)
|
||||
|
||||
// Function pointers - the "vtable"
|
||||
void (*c_destructor_)(struct TCOD_Context* self);
|
||||
TCOD_Error (*c_present_)(struct TCOD_Context* self,
|
||||
const TCOD_Console* console,
|
||||
const TCOD_ViewportOptions* viewport);
|
||||
void (*c_pixel_to_tile_)(struct TCOD_Context* self, double* x, double* y);
|
||||
TCOD_Error (*c_save_screenshot_)(struct TCOD_Context* self, const char* filename);
|
||||
struct SDL_Window* (*c_get_sdl_window_)(struct TCOD_Context* self);
|
||||
TCOD_Error (*c_set_tileset_)(struct TCOD_Context* self, TCOD_Tileset* tileset);
|
||||
TCOD_Error (*c_screen_capture_)(struct TCOD_Context* self, ...);
|
||||
// ... more operations
|
||||
};
|
||||
```
|
||||
|
||||
### How Backends Implement It
|
||||
|
||||
Each renderer fills in the function pointers:
|
||||
|
||||
```c
|
||||
// In renderer_sdl2.c
|
||||
context->c_destructor_ = sdl2_destructor;
|
||||
context->c_present_ = sdl2_present;
|
||||
context->c_get_sdl_window_ = sdl2_get_window;
|
||||
// ...
|
||||
|
||||
// In renderer_xterm.c
|
||||
context->c_destructor_ = xterm_destructor;
|
||||
context->c_present_ = xterm_present;
|
||||
// ...
|
||||
```
|
||||
|
||||
### Conditional Compilation with NO_SDL
|
||||
|
||||
libtcod uses simple preprocessor guards:
|
||||
|
||||
```c
|
||||
// In CMakeLists.txt
|
||||
if(LIBTCOD_SDL3)
|
||||
target_link_libraries(${PROJECT_NAME} PUBLIC SDL3::SDL3)
|
||||
else()
|
||||
target_compile_definitions(${PROJECT_NAME} PUBLIC NO_SDL)
|
||||
endif()
|
||||
|
||||
// In source files
|
||||
#ifndef NO_SDL
|
||||
#include <SDL3/SDL.h>
|
||||
// ... SDL-dependent code ...
|
||||
#endif
|
||||
```
|
||||
|
||||
**47 files** use this pattern. When building headless, SDL code is simply excluded.
|
||||
|
||||
### Why This Pattern Works
|
||||
|
||||
1. **Core functionality is SDL-independent**: Console manipulation, pathfinding, FOV, noise, BSP, etc. don't need SDL
|
||||
2. **Only rendering needs abstraction**: The `TCOD_Context` is the single point of abstraction
|
||||
3. **Minimal API surface**: Just ~10 function pointers instead of wrapping every primitive
|
||||
4. **Backend-specific data is opaque**: `contextdata_` holds renderer-specific state
|
||||
|
||||
### Implications for McRogueFace
|
||||
|
||||
**libtcod's approach suggests we should NOT try to abstract every `sf::` type.**
|
||||
|
||||
Instead, consider:
|
||||
|
||||
1. **Keep SFML types internally** - `sf::Vector2f`, `sf::Color`, `sf::FloatRect` are fine
|
||||
2. **Abstract at the RenderContext level** - One vtable for window/rendering operations
|
||||
3. **Use `#ifndef NO_SFML` guards** - Compile-time backend selection
|
||||
4. **Create alternative backend for Emscripten** - WebGL + canvas implementation
|
||||
|
||||
### Proposed McRogueFace Context Pattern
|
||||
|
||||
```cpp
|
||||
struct McRF_RenderContext {
|
||||
void* backend_data; // SFML or WebGL specific data
|
||||
|
||||
// Function pointers
|
||||
void (*destroy)(McRF_RenderContext* self);
|
||||
void (*clear)(McRF_RenderContext* self, uint32_t color);
|
||||
void (*present)(McRF_RenderContext* self);
|
||||
void (*draw_sprite)(McRF_RenderContext* self, const Sprite* sprite);
|
||||
void (*draw_text)(McRF_RenderContext* self, const Text* text);
|
||||
void (*draw_rect)(McRF_RenderContext* self, const Rect* rect);
|
||||
bool (*poll_event)(McRF_RenderContext* self, Event* event);
|
||||
void (*screenshot)(McRF_RenderContext* self, const char* path);
|
||||
// ...
|
||||
};
|
||||
|
||||
// SFML backend
|
||||
McRF_RenderContext* mcrf_sfml_context_new(int width, int height, const char* title);
|
||||
|
||||
// Emscripten backend (future)
|
||||
McRF_RenderContext* mcrf_webgl_context_new(const char* canvas_id);
|
||||
```
|
||||
|
||||
### Comparison: Original Plan vs libtcod-Inspired Plan
|
||||
|
||||
| Aspect | Original Plan | libtcod-Inspired Plan |
|
||||
|--------|---------------|----------------------|
|
||||
| Type abstraction | Replace all `sf::*` with `mcrf::*` | Keep `sf::*` internally |
|
||||
| Abstraction point | Every primitive type | Single Context object |
|
||||
| Files affected | 78+ files | ~10 core files |
|
||||
| Compile-time switching | Complex namespace aliasing | Simple `#ifndef NO_SFML` |
|
||||
| Backend complexity | Full reimplementation | Focused vtable |
|
||||
|
||||
**Recommendation**: Adopt libtcod's simpler pattern. Focus abstraction on the rendering context, not on data types.
|
||||
|
||||
---
|
||||
|
||||
## Appendix D: Headless Build Experiment Results
|
||||
|
||||
**Experiment Date**: 2026-01-30
|
||||
**Branch**: `emscripten-mcrogueface`
|
||||
|
||||
### Objective
|
||||
|
||||
Attempt to compile McRogueFace without SFML dependencies to identify true coupling points.
|
||||
|
||||
### What We Created
|
||||
|
||||
1. **`src/platform/HeadlessTypes.h`** - Complete SFML type stubs (~600 lines):
|
||||
- Vector2f, Vector2i, Vector2u
|
||||
- Color with standard color constants
|
||||
- FloatRect, IntRect
|
||||
- Time, Clock (with chrono-based implementation)
|
||||
- Transform, Vertex, View
|
||||
- Shape hierarchy (RectangleShape, CircleShape, etc.)
|
||||
- Texture, Sprite, Font, Text stubs
|
||||
- RenderTarget, RenderTexture, RenderWindow stubs
|
||||
- Audio stubs (Sound, Music, SoundBuffer)
|
||||
- Input stubs (Keyboard, Mouse, Event)
|
||||
- Shader stub
|
||||
|
||||
2. **Modified `src/Common.h`** - Conditional include:
|
||||
```cpp
|
||||
#ifdef MCRF_HEADLESS
|
||||
#include "platform/HeadlessTypes.h"
|
||||
#else
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/Audio.hpp>
|
||||
#endif
|
||||
```
|
||||
|
||||
### Build Attempt Result
|
||||
|
||||
**SUCCESS** - Headless build compiles after consolidating includes and adding stubs.
|
||||
|
||||
### Work Completed
|
||||
|
||||
#### 1. Consolidated SFML Includes
|
||||
|
||||
**15 files** had direct SFML includes that bypassed Common.h. All were modified to use `#include "Common.h"` instead:
|
||||
|
||||
| File | Original Include | Fixed |
|
||||
|------|------------------|-------|
|
||||
| `main.cpp` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `Animation.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `GridChunk.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `GridLayers.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `HeadlessRenderer.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `SceneTransition.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `McRFPy_Automation.h` | `<SFML/Graphics.hpp>`, `<SFML/Window.hpp>` | ✓ |
|
||||
| `PyWindow.cpp` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `ActionCode.h` | `<SFML/Window/Keyboard.hpp>` | ✓ |
|
||||
| `PyKey.h` | `<SFML/Window/Keyboard.hpp>` | ✓ |
|
||||
| `PyMouseButton.h` | `<SFML/Window/Mouse.hpp>` | ✓ |
|
||||
| `PyBSP.h` | `<SFML/System/Vector2.hpp>` | ✓ |
|
||||
| `UIGridPathfinding.h` | `<SFML/System/Vector2.hpp>` | ✓ |
|
||||
|
||||
#### 2. Wrapped ImGui-SFML with Guards
|
||||
|
||||
ImGui-SFML is disabled entirely in headless builds since debug tools can't be accessed through the API:
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `GameEngine.h` | Guarded includes and member variables |
|
||||
| `GameEngine.cpp` | Guarded all ImGui::SFML calls |
|
||||
| `ImGuiConsole.h/cpp` | Entire file wrapped with `#ifndef MCRF_HEADLESS` |
|
||||
| `ImGuiSceneExplorer.h/cpp` | Entire file wrapped with `#ifndef MCRF_HEADLESS` |
|
||||
| `McRFPy_API.cpp` | Guarded ImGuiConsole include and setEnabled call |
|
||||
|
||||
#### 3. Extended HeadlessTypes.h
|
||||
|
||||
The stub file grew from ~700 lines to ~900 lines with additional types and methods:
|
||||
|
||||
**Types Added:**
|
||||
- `sf::Image` - For screenshot functionality
|
||||
- `sf::Glsl::Vec3`, `sf::Glsl::Vec4` - For shader uniforms
|
||||
- `sf::BlendMode` - For rendering states
|
||||
- `sf::CurrentTextureType` - For shader texture binding
|
||||
|
||||
**Methods Added:**
|
||||
- `Font::Info` struct and `Font::getInfo()`
|
||||
- `Texture::update()` overloads
|
||||
- `Texture::copyToImage()`
|
||||
- `Transform::getInverse()`
|
||||
- `RenderStates` constructors from Transform, BlendMode, Shader*
|
||||
- `Music::getDuration()`, `getPlayingOffset()`, `setPlayingOffset()`
|
||||
- `SoundBuffer::getDuration()`
|
||||
- `RenderWindow::setMouseCursorGrabbed()`
|
||||
- `sf::err()` stream function
|
||||
- Keyboard aliases: `BackSpace`, `BackSlash`, `SemiColon`, `Dash`
|
||||
|
||||
### Build Commands
|
||||
|
||||
```bash
|
||||
# Normal SFML build (default)
|
||||
make
|
||||
|
||||
# Headless build (no SFML/ImGui dependencies)
|
||||
mkdir build-headless && cd build-headless
|
||||
cmake .. -DMCRF_HEADLESS=ON -DCMAKE_BUILD_TYPE=Release
|
||||
make
|
||||
```
|
||||
|
||||
### Key Insight
|
||||
|
||||
The libtcod approach of `#ifndef NO_SDL` guards works when **all platform includes go through a single point**. The consolidation of 15+ bypass points into Common.h was the prerequisite that made this work.
|
||||
|
||||
### Actual Effort
|
||||
|
||||
| Task | Files | Time |
|
||||
|------|-------|------|
|
||||
| Replace direct SFML includes with Common.h | 15 | ~30 min |
|
||||
| Wrap ImGui-SFML in guards | 5 | ~20 min |
|
||||
| Extend HeadlessTypes.h with missing stubs | 1 | ~1 hour |
|
||||
| Fix compilation errors iteratively | - | ~1 hour |
|
||||
|
||||
**Total**: ~3 hours for clean headless compilation
|
||||
|
||||
### Completed Milestones
|
||||
|
||||
1. ✅ **Test Python bindings** - mcrfpy module loads and works in headless mode
|
||||
- Vector, Color, Scene, Frame, Grid all functional
|
||||
- libtcod integrations (BSP, pathfinding) available
|
||||
2. ✅ **Add CMake option** - `option(MCRF_HEADLESS "Build without graphics" OFF)`
|
||||
- Proper conditional compilation and linking
|
||||
- No SFML symbols in headless binary
|
||||
3. ✅ **Link-time validation** - `ldd` confirms zero SFML/OpenGL dependencies
|
||||
4. ✅ **Binary size reduction** - Headless is 1.6 MB vs 2.5 MB normal build (36% smaller)
|
||||
|
||||
### Python Test Results (Headless Mode)
|
||||
|
||||
```python
|
||||
# All these work in headless build:
|
||||
import mcrfpy
|
||||
v = mcrfpy.Vector(10, 20) # ✅
|
||||
c = mcrfpy.Color(255, 128, 64) # ✅
|
||||
scene = mcrfpy.Scene('test') # ✅
|
||||
frame = mcrfpy.Frame(pos=(0,0)) # ✅
|
||||
grid = mcrfpy.Grid(grid_size=(10,10)) # ✅
|
||||
```
|
||||
|
||||
### Remaining Steps for Emscripten
|
||||
|
||||
1. ✅ **Main loop extraction** - `GameEngine::doFrame()` extracted with Emscripten callback support
|
||||
- `run()` now uses `#ifdef __EMSCRIPTEN__` to choose between callback and blocking loop
|
||||
- `emscripten_set_main_loop_arg()` integration ready
|
||||
2. ✅ **Emscripten toolchain** - `emcmake cmake` works with headless mode
|
||||
3. ✅ **Python-in-WASM** - Built CPython 3.14.2 for wasm32-emscripten target
|
||||
- Uses official `Tools/wasm/emscripten build` script from CPython repo
|
||||
- Produced libpython3.14.a (47MB static library)
|
||||
- Also builds: libmpdec, libffi, libexpat for WASM
|
||||
4. ✅ **libtcod-in-WASM** - Built libtcod-headless for Emscripten
|
||||
- Uses `LIBTCOD_SDL3=OFF` to avoid SDL dependency
|
||||
- Includes lodepng and utf8proc dependencies
|
||||
5. ✅ **First successful WASM build** - mcrogueface.wasm (8.9MB) + mcrogueface.js (126KB)
|
||||
- All 68 C++ source files compile with emcc
|
||||
- Links: Python, libtcod, HACL crypto, expat, mpdec, ffi, zlib, bzip2, sqlite3
|
||||
6. 🔲 **Python stdlib bundling** - Need to package Python stdlib for WASM filesystem
|
||||
7. 🔲 **VRSFML integration** - Replace stubs with actual WebGL rendering
|
||||
|
||||
### First Emscripten Build Attempt (2026-01-31)
|
||||
|
||||
**Command:**
|
||||
```bash
|
||||
source ~/emsdk/emsdk_env.sh
|
||||
emcmake cmake .. -DMCRF_HEADLESS=ON -DCMAKE_BUILD_TYPE=Release
|
||||
emmake make -j8
|
||||
```
|
||||
|
||||
**Result:** Build failed on Python headers.
|
||||
|
||||
**Key Errors:**
|
||||
```
|
||||
deps/Python/pyport.h:429:2: error: "LONG_BIT definition appears wrong for platform"
|
||||
```
|
||||
```
|
||||
warning: shift count >= width of type [-Wshift-count-overflow]
|
||||
_Py_STATIC_FLAG_BITS << 48 // 48-bit shift on 32-bit WASM!
|
||||
```
|
||||
|
||||
**Root Cause:**
|
||||
1. Desktop Python 3.14 headers assume 64-bit Linux with glibc
|
||||
2. Emscripten targets 32-bit WASM with musl-based libc
|
||||
3. Python's immortal reference counting uses `<< 48` shifts that overflow on 32-bit
|
||||
4. `LONG_BIT` check fails because WASM's `long` is 32 bits
|
||||
|
||||
**Analysis:**
|
||||
The HeadlessTypes.h stubs and game engine code compile fine. The blocker is exclusively the Python C API integration.
|
||||
|
||||
### Python-in-WASM Options
|
||||
|
||||
| Option | Complexity | Description |
|
||||
|--------|------------|-------------|
|
||||
| **Pyodide** | Medium | Pre-built Python WASM with package ecosystem |
|
||||
| **CPython WASM** | High | Build CPython ourselves with Emscripten |
|
||||
| **No-Python mode** | Low | New CMake option to exclude Python entirely |
|
||||
|
||||
**Pyodide Approach (Recommended):**
|
||||
- Pyodide provides Python 3.12 compiled for WASM
|
||||
- Would need to replace `deps/Python` with Pyodide headers
|
||||
- `McRFPy_API` binding layer needs adaptation
|
||||
- Pyodide handles asyncio, file system virtualization
|
||||
- Active project with good documentation
|
||||
|
||||
### CPython WASM Build (Successful!)
|
||||
|
||||
**Date**: 2026-01-31
|
||||
|
||||
Used the official CPython WASM build process:
|
||||
|
||||
```bash
|
||||
# From deps/cpython directory
|
||||
./Tools/wasm/emscripten build
|
||||
|
||||
# This produces:
|
||||
# - cross-build/wasm32-emscripten/build/python/libpython3.14.a
|
||||
# - cross-build/wasm32-emscripten/prefix/lib/libmpdec.a
|
||||
# - cross-build/wasm32-emscripten/prefix/lib/libffi.a
|
||||
# - cross-build/wasm32-emscripten/build/python/Modules/expat/libexpat.a
|
||||
```
|
||||
|
||||
**CMake Integration:**
|
||||
```cmake
|
||||
if(EMSCRIPTEN)
|
||||
set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python")
|
||||
set(PYTHON_WASM_PREFIX "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/prefix")
|
||||
|
||||
# Force WASM-compatible pyconfig.h
|
||||
add_compile_options(-include ${PYTHON_WASM_BUILD}/pyconfig.h)
|
||||
|
||||
# Link all Python dependencies
|
||||
set(LINK_LIBS
|
||||
${PYTHON_WASM_BUILD}/libpython3.14.a
|
||||
${PYTHON_WASM_BUILD}/Modules/_hacl/*.o # HACL crypto not in libpython
|
||||
${PYTHON_WASM_BUILD}/Modules/expat/libexpat.a
|
||||
${PYTHON_WASM_PREFIX}/lib/libmpdec.a
|
||||
${PYTHON_WASM_PREFIX}/lib/libffi.a
|
||||
)
|
||||
|
||||
# Emscripten ports for common libraries
|
||||
target_link_options(mcrogueface PRIVATE
|
||||
-sUSE_ZLIB=1
|
||||
-sUSE_BZIP2=1
|
||||
-sUSE_SQLITE3=1
|
||||
)
|
||||
endif()
|
||||
```
|
||||
|
||||
**No-Python Mode (For Testing):**
|
||||
- Add `MCRF_NO_PYTHON` CMake option
|
||||
- Allows testing WASM build without Python complexity
|
||||
- Game engine would be pure C++ (no scripting)
|
||||
- Useful for validating rendering, input, timing first
|
||||
|
||||
### Main Loop Architecture
|
||||
|
||||
The game loop now supports both desktop (blocking) and browser (callback) modes:
|
||||
|
||||
```cpp
|
||||
// GameEngine::run() - build-time conditional
|
||||
#ifdef __EMSCRIPTEN__
|
||||
emscripten_set_main_loop_arg(emscriptenMainLoopCallback, this, 0, 1);
|
||||
#else
|
||||
while (running) { doFrame(); }
|
||||
#endif
|
||||
|
||||
// GameEngine::doFrame() - same code runs in both modes
|
||||
void GameEngine::doFrame() {
|
||||
metrics.resetPerFrame();
|
||||
currentScene()->update();
|
||||
testTimers();
|
||||
// ... animations, input, rendering ...
|
||||
currentFrame++;
|
||||
frameTime = clock.restart().asSeconds();
|
||||
}
|
||||
```
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
"""McRogueFace - Animated Movement (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_animated_movement
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_animated_movement_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
if new_x != current_x:
|
||||
anim = mcrfpy.Animation("x", float(new_x), duration, "easeInOut", callback=done)
|
||||
else:
|
||||
anim = mcrfpy.Animation("y", float(new_y), duration, "easeInOut", callback=done)
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
"""McRogueFace - Animated Movement (basic_2)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_animated_movement
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_animated_movement_basic_2.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
current_anim = mcrfpy.Animation("x", 100.0, 0.5, "linear")
|
||||
current_anim.start(entity)
|
||||
# Later: current_anim = None # Let it complete or create new one
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
"""McRogueFace - Basic Enemy AI (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
def wander(enemy, grid):
|
||||
"""Move randomly to an adjacent walkable tile."""
|
||||
ex, ey = int(enemy.x), int(enemy.y)
|
||||
|
||||
# Get valid adjacent tiles
|
||||
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
|
||||
random.shuffle(directions)
|
||||
|
||||
for dx, dy in directions:
|
||||
new_x, new_y = ex + dx, ey + dy
|
||||
|
||||
if is_walkable(grid, new_x, new_y) and not is_occupied(new_x, new_y):
|
||||
enemy.x = new_x
|
||||
enemy.y = new_y
|
||||
return
|
||||
|
||||
# No valid moves - stay in place
|
||||
|
||||
def is_walkable(grid, x, y):
|
||||
"""Check if a tile can be walked on."""
|
||||
grid_w, grid_h = grid.grid_size
|
||||
if x < 0 or x >= grid_w or y < 0 or y >= grid_h:
|
||||
return False
|
||||
return grid.at(x, y).walkable
|
||||
|
||||
def is_occupied(x, y, entities=None):
|
||||
"""Check if a tile is occupied by another entity."""
|
||||
if entities is None:
|
||||
return False
|
||||
|
||||
for entity in entities:
|
||||
if int(entity.x) == x and int(entity.y) == y:
|
||||
return True
|
||||
return False
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
"""McRogueFace - Basic Enemy AI (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
# Filter to cardinal directions only
|
||||
path = [p for p in path if abs(p[0] - ex) + abs(p[1] - ey) == 1]
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
"""McRogueFace - Basic Enemy AI (multi_2)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_multi_2.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def alert_nearby(x, y, radius, enemies):
|
||||
for enemy in enemies:
|
||||
dist = abs(enemy.entity.x - x) + abs(enemy.entity.y - y)
|
||||
if dist <= radius and hasattr(enemy.ai, 'alert'):
|
||||
enemy.ai.alert = True
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
"""McRogueFace - Melee Combat System (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class CombatLog:
|
||||
"""Scrolling combat message log."""
|
||||
|
||||
def __init__(self, x, y, width, height, max_messages=10):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.max_messages = max_messages
|
||||
self.messages = []
|
||||
self.captions = []
|
||||
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
# Background
|
||||
self.frame = mcrfpy.Frame(x, y, width, height)
|
||||
self.frame.fill_color = mcrfpy.Color(0, 0, 0, 180)
|
||||
ui.append(self.frame)
|
||||
|
||||
def add_message(self, text, color=None):
|
||||
"""Add a message to the log."""
|
||||
if color is None:
|
||||
color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
self.messages.append((text, color))
|
||||
|
||||
# Keep only recent messages
|
||||
if len(self.messages) > self.max_messages:
|
||||
self.messages.pop(0)
|
||||
|
||||
self._refresh_display()
|
||||
|
||||
def _refresh_display(self):
|
||||
"""Redraw all messages."""
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
# Remove old captions
|
||||
for caption in self.captions:
|
||||
try:
|
||||
ui.remove(caption)
|
||||
except:
|
||||
pass
|
||||
self.captions.clear()
|
||||
|
||||
# Create new captions
|
||||
line_height = 18
|
||||
for i, (text, color) in enumerate(self.messages):
|
||||
caption = mcrfpy.Caption(text, self.x + 5, self.y + 5 + i * line_height)
|
||||
caption.fill_color = color
|
||||
ui.append(caption)
|
||||
self.captions.append(caption)
|
||||
|
||||
def log_attack(self, attacker_name, defender_name, damage, killed=False, critical=False):
|
||||
"""Log an attack event."""
|
||||
if critical:
|
||||
text = f"{attacker_name} CRITS {defender_name} for {damage}!"
|
||||
color = mcrfpy.Color(255, 255, 0)
|
||||
else:
|
||||
text = f"{attacker_name} hits {defender_name} for {damage}."
|
||||
color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
self.add_message(text, color)
|
||||
|
||||
if killed:
|
||||
self.add_message(f"{defender_name} is defeated!", mcrfpy.Color(255, 100, 100))
|
||||
|
||||
|
||||
# Global combat log
|
||||
combat_log = None
|
||||
|
||||
def init_combat_log():
|
||||
global combat_log
|
||||
combat_log = CombatLog(10, 500, 400, 200)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
"""McRogueFace - Melee Combat System (complete)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_complete.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def die_with_animation(entity):
|
||||
# Play death animation
|
||||
anim = mcrfpy.Animation("opacity", 0.0, 0.5, "linear")
|
||||
anim.start(entity)
|
||||
# Remove after animation
|
||||
mcrfpy.setTimer("remove", lambda dt: remove_entity(entity), 500)
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
"""McRogueFace - Melee Combat System (complete_2)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_complete_2.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
@dataclass
|
||||
class AdvancedFighter(Fighter):
|
||||
fire_resist: float = 0.0
|
||||
ice_resist: float = 0.0
|
||||
physical_resist: float = 0.0
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
"""McRogueFace - Status Effects (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class StackableEffect(StatusEffect):
|
||||
"""Effect that stacks intensity."""
|
||||
|
||||
def __init__(self, name, duration, intensity=1, max_stacks=5, **kwargs):
|
||||
super().__init__(name, duration, **kwargs)
|
||||
self.intensity = intensity
|
||||
self.max_stacks = max_stacks
|
||||
self.stacks = 1
|
||||
|
||||
def add_stack(self):
|
||||
"""Add another stack."""
|
||||
if self.stacks < self.max_stacks:
|
||||
self.stacks += 1
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class StackingEffectManager(EffectManager):
|
||||
"""Effect manager with stacking support."""
|
||||
|
||||
def add_effect(self, effect):
|
||||
if isinstance(effect, StackableEffect):
|
||||
# Check for existing stacks
|
||||
for existing in self.effects:
|
||||
if existing.name == effect.name:
|
||||
if existing.add_stack():
|
||||
# Refresh duration
|
||||
existing.duration = max(existing.duration, effect.duration)
|
||||
return
|
||||
else:
|
||||
return # Max stacks
|
||||
|
||||
# Default behavior
|
||||
super().add_effect(effect)
|
||||
|
||||
|
||||
# Stacking poison example
|
||||
def create_stacking_poison(base_damage=1, duration=5):
|
||||
def on_tick(target):
|
||||
# Find the poison effect to get stack count
|
||||
effect = target.effects.get_effect("poison")
|
||||
if effect:
|
||||
damage = base_damage * effect.stacks
|
||||
target.hp -= damage
|
||||
print(f"{target.name} takes {damage} poison damage! ({effect.stacks} stacks)")
|
||||
|
||||
return StackableEffect("poison", duration, on_tick=on_tick, max_stacks=5)
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
"""McRogueFace - Status Effects (basic_2)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic_2.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def apply_effect(self, effect):
|
||||
if effect.name in self.immunities:
|
||||
print(f"{self.name} is immune to {effect.name}!")
|
||||
return
|
||||
if effect.name in self.resistances:
|
||||
effect.duration //= 2 # Half duration
|
||||
self.effects.add_effect(effect)
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
"""McRogueFace - Status Effects (basic_3)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic_3.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def serialize_effects(effect_manager):
|
||||
return [{"name": e.name, "duration": e.duration}
|
||||
for e in effect_manager.effects]
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
"""McRogueFace - Turn-Based Game Loop (combat_turn_system)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_turn_system
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_turn_system.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def create_turn_order_ui(turn_manager, x=800, y=50):
|
||||
"""Create a visual turn order display."""
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
# Background frame
|
||||
frame = mcrfpy.Frame(x, y, 200, 300)
|
||||
frame.fill_color = mcrfpy.Color(30, 30, 30, 200)
|
||||
frame.outline = 2
|
||||
frame.outline_color = mcrfpy.Color(100, 100, 100)
|
||||
ui.append(frame)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Turn Order", x + 10, y + 10)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
return frame
|
||||
|
||||
def update_turn_order_display(frame, turn_manager, x=800, y=50):
|
||||
"""Update the turn order display."""
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
# Clear old entries (keep frame and title)
|
||||
# In practice, store references to caption objects and update them
|
||||
|
||||
for i, actor_data in enumerate(turn_manager.actors):
|
||||
actor = actor_data["actor"]
|
||||
is_current = (i == turn_manager.current)
|
||||
|
||||
# Actor name/type
|
||||
name = getattr(actor, 'name', f"Actor {i}")
|
||||
color = mcrfpy.Color(255, 255, 0) if is_current else mcrfpy.Color(200, 200, 200)
|
||||
|
||||
caption = mcrfpy.Caption(name, x + 10, y + 40 + i * 25)
|
||||
caption.fill_color = color
|
||||
ui.append(caption)
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
"""McRogueFace - Color Pulse Effect (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_color_pulse
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_color_pulse_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class PulsingCell:
|
||||
"""A cell that continuously pulses until stopped."""
|
||||
|
||||
def __init__(self, grid, x, y, color, period=1.0, max_alpha=180):
|
||||
"""
|
||||
Args:
|
||||
grid: Grid with color layer
|
||||
x, y: Cell position
|
||||
color: RGB tuple
|
||||
period: Time for one complete pulse cycle
|
||||
max_alpha: Maximum alpha value (0-255)
|
||||
"""
|
||||
self.grid = grid
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.color = color
|
||||
self.period = period
|
||||
self.max_alpha = max_alpha
|
||||
self.is_pulsing = False
|
||||
self.pulse_id = 0
|
||||
self.cell = None
|
||||
|
||||
self._setup_layer()
|
||||
|
||||
def _setup_layer(self):
|
||||
"""Ensure color layer exists and get cell reference."""
|
||||
color_layer = None
|
||||
for layer in self.grid.layers:
|
||||
if isinstance(layer, mcrfpy.ColorLayer):
|
||||
color_layer = layer
|
||||
break
|
||||
|
||||
if not color_layer:
|
||||
self.grid.add_layer("color")
|
||||
color_layer = self.grid.layers[-1]
|
||||
|
||||
self.cell = color_layer.at(self.x, self.y)
|
||||
if self.cell:
|
||||
self.cell.color = mcrfpy.Color(self.color[0], self.color[1],
|
||||
self.color[2], 0)
|
||||
|
||||
def start(self):
|
||||
"""Start continuous pulsing."""
|
||||
if self.is_pulsing or not self.cell:
|
||||
return
|
||||
|
||||
self.is_pulsing = True
|
||||
self.pulse_id += 1
|
||||
self._pulse_up()
|
||||
|
||||
def _pulse_up(self):
|
||||
"""Animate alpha increasing."""
|
||||
if not self.is_pulsing:
|
||||
return
|
||||
|
||||
current_id = self.pulse_id
|
||||
half_period = self.period / 2
|
||||
|
||||
anim = mcrfpy.Animation("a", float(self.max_alpha), half_period, "easeInOut")
|
||||
anim.start(self.cell.color)
|
||||
|
||||
def next_phase(timer_name):
|
||||
if self.is_pulsing and self.pulse_id == current_id:
|
||||
self._pulse_down()
|
||||
|
||||
mcrfpy.Timer(f"pulse_up_{id(self)}_{current_id}",
|
||||
next_phase, int(half_period * 1000), once=True)
|
||||
|
||||
def _pulse_down(self):
|
||||
"""Animate alpha decreasing."""
|
||||
if not self.is_pulsing:
|
||||
return
|
||||
|
||||
current_id = self.pulse_id
|
||||
half_period = self.period / 2
|
||||
|
||||
anim = mcrfpy.Animation("a", 0.0, half_period, "easeInOut")
|
||||
anim.start(self.cell.color)
|
||||
|
||||
def next_phase(timer_name):
|
||||
if self.is_pulsing and self.pulse_id == current_id:
|
||||
self._pulse_up()
|
||||
|
||||
mcrfpy.Timer(f"pulse_down_{id(self)}_{current_id}",
|
||||
next_phase, int(half_period * 1000), once=True)
|
||||
|
||||
def stop(self):
|
||||
"""Stop pulsing and fade out."""
|
||||
self.is_pulsing = False
|
||||
if self.cell:
|
||||
anim = mcrfpy.Animation("a", 0.0, 0.2, "easeOut")
|
||||
anim.start(self.cell.color)
|
||||
|
||||
def set_color(self, color):
|
||||
"""Change pulse color."""
|
||||
self.color = color
|
||||
if self.cell:
|
||||
current_alpha = self.cell.color.a
|
||||
self.cell.color = mcrfpy.Color(color[0], color[1], color[2], current_alpha)
|
||||
|
||||
|
||||
# Usage
|
||||
objective_pulse = PulsingCell(grid, 10, 10, (0, 255, 100), period=1.5)
|
||||
objective_pulse.start()
|
||||
|
||||
# Later, when objective is reached:
|
||||
objective_pulse.stop()
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
"""McRogueFace - Color Pulse Effect (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_color_pulse
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_color_pulse_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
def ripple_effect(grid, center_x, center_y, color, max_radius=5, duration=1.0):
|
||||
"""
|
||||
Create an expanding ripple effect.
|
||||
|
||||
Args:
|
||||
grid: Grid with color layer
|
||||
center_x, center_y: Ripple origin
|
||||
color: RGB tuple
|
||||
max_radius: Maximum ripple size
|
||||
duration: Total animation time
|
||||
"""
|
||||
# Get color layer
|
||||
color_layer = None
|
||||
for layer in grid.layers:
|
||||
if isinstance(layer, mcrfpy.ColorLayer):
|
||||
color_layer = layer
|
||||
break
|
||||
|
||||
if not color_layer:
|
||||
grid.add_layer("color")
|
||||
color_layer = grid.layers[-1]
|
||||
|
||||
step_duration = duration / max_radius
|
||||
|
||||
for radius in range(max_radius + 1):
|
||||
# Get cells at this radius (ring, not filled)
|
||||
ring_cells = []
|
||||
for dy in range(-radius, radius + 1):
|
||||
for dx in range(-radius, radius + 1):
|
||||
dist_sq = dx * dx + dy * dy
|
||||
# Include cells approximately on the ring edge
|
||||
if radius * radius - radius <= dist_sq <= radius * radius + radius:
|
||||
cell = color_layer.at(center_x + dx, center_y + dy)
|
||||
if cell:
|
||||
ring_cells.append(cell)
|
||||
|
||||
# Schedule this ring to animate
|
||||
def animate_ring(timer_name, cells=ring_cells, c=color):
|
||||
for cell in cells:
|
||||
cell.color = mcrfpy.Color(c[0], c[1], c[2], 200)
|
||||
# Fade out
|
||||
anim = mcrfpy.Animation("a", 0.0, step_duration * 2, "easeOut")
|
||||
anim.start(cell.color)
|
||||
|
||||
delay = int(radius * step_duration * 1000)
|
||||
mcrfpy.Timer(f"ripple_{radius}", animate_ring, delay, once=True)
|
||||
|
||||
|
||||
# Usage
|
||||
ripple_effect(grid, 10, 10, (100, 200, 255), max_radius=6, duration=0.8)
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
"""McRogueFace - Damage Flash Effect (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Add a color layer to your grid (do this once during setup)
|
||||
grid.add_layer("color")
|
||||
color_layer = grid.layers[-1] # Get the color layer
|
||||
|
||||
def flash_cell(grid, x, y, color, duration=0.3):
|
||||
"""Flash a grid cell with a color overlay."""
|
||||
# Get the color layer (assumes it's the last layer added)
|
||||
color_layer = None
|
||||
for layer in grid.layers:
|
||||
if isinstance(layer, mcrfpy.ColorLayer):
|
||||
color_layer = layer
|
||||
break
|
||||
|
||||
if not color_layer:
|
||||
return
|
||||
|
||||
# Set cell to flash color
|
||||
cell = color_layer.at(x, y)
|
||||
cell.color = mcrfpy.Color(color[0], color[1], color[2], 200)
|
||||
|
||||
# Animate alpha back to 0
|
||||
anim = mcrfpy.Animation("a", 0.0, duration, "easeOut")
|
||||
anim.start(cell.color)
|
||||
|
||||
def damage_at_position(grid, x, y, duration=0.3):
|
||||
"""Flash red at a grid position when damage occurs."""
|
||||
flash_cell(grid, x, y, (255, 0, 0), duration)
|
||||
|
||||
# Usage when entity takes damage
|
||||
damage_at_position(grid, int(enemy.x), int(enemy.y))
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
"""McRogueFace - Damage Flash Effect (complete)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_complete.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class DamageEffects:
|
||||
"""Manages visual damage feedback effects."""
|
||||
|
||||
# Color presets
|
||||
DAMAGE_RED = (255, 50, 50)
|
||||
HEAL_GREEN = (50, 255, 50)
|
||||
POISON_PURPLE = (150, 50, 200)
|
||||
FIRE_ORANGE = (255, 150, 50)
|
||||
ICE_BLUE = (100, 200, 255)
|
||||
|
||||
def __init__(self, grid):
|
||||
self.grid = grid
|
||||
self.color_layer = None
|
||||
self._setup_color_layer()
|
||||
|
||||
def _setup_color_layer(self):
|
||||
"""Ensure grid has a color layer for effects."""
|
||||
self.grid.add_layer("color")
|
||||
self.color_layer = self.grid.layers[-1]
|
||||
|
||||
def flash_entity(self, entity, color, duration=0.3):
|
||||
"""Flash an entity with a color tint."""
|
||||
# Flash at entity's grid position
|
||||
x, y = int(entity.x), int(entity.y)
|
||||
self.flash_cell(x, y, color, duration)
|
||||
|
||||
def flash_cell(self, x, y, color, duration=0.3):
|
||||
"""Flash a specific grid cell."""
|
||||
if not self.color_layer:
|
||||
return
|
||||
|
||||
cell = self.color_layer.at(x, y)
|
||||
if cell:
|
||||
cell.color = mcrfpy.Color(color[0], color[1], color[2], 180)
|
||||
|
||||
# Fade out
|
||||
anim = mcrfpy.Animation("a", 0.0, duration, "easeOut")
|
||||
anim.start(cell.color)
|
||||
|
||||
def damage(self, entity, amount, duration=0.3):
|
||||
"""Standard damage flash."""
|
||||
self.flash_entity(entity, self.DAMAGE_RED, duration)
|
||||
|
||||
def heal(self, entity, amount, duration=0.4):
|
||||
"""Healing effect - green flash."""
|
||||
self.flash_entity(entity, self.HEAL_GREEN, duration)
|
||||
|
||||
def poison(self, entity, duration=0.5):
|
||||
"""Poison damage - purple flash."""
|
||||
self.flash_entity(entity, self.POISON_PURPLE, duration)
|
||||
|
||||
def fire(self, entity, duration=0.3):
|
||||
"""Fire damage - orange flash."""
|
||||
self.flash_entity(entity, self.FIRE_ORANGE, duration)
|
||||
|
||||
def ice(self, entity, duration=0.4):
|
||||
"""Ice damage - blue flash."""
|
||||
self.flash_entity(entity, self.ICE_BLUE, duration)
|
||||
|
||||
def area_damage(self, center_x, center_y, radius, color, duration=0.4):
|
||||
"""Flash all cells in a radius."""
|
||||
for dy in range(-radius, radius + 1):
|
||||
for dx in range(-radius, radius + 1):
|
||||
if dx * dx + dy * dy <= radius * radius:
|
||||
self.flash_cell(center_x + dx, center_y + dy, color, duration)
|
||||
|
||||
# Setup
|
||||
effects = DamageEffects(grid)
|
||||
|
||||
# Usage examples
|
||||
effects.damage(player, 10) # Red flash
|
||||
effects.heal(player, 5) # Green flash
|
||||
effects.poison(enemy) # Purple flash
|
||||
effects.area_damage(5, 5, 3, effects.FIRE_ORANGE) # Area effect
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
"""McRogueFace - Damage Flash Effect (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
def multi_flash(grid, x, y, color, flashes=3, flash_duration=0.1):
|
||||
"""Flash a cell multiple times for emphasis."""
|
||||
delay = 0
|
||||
|
||||
for i in range(flashes):
|
||||
# Schedule each flash with increasing delay
|
||||
def do_flash(timer_name, fx=x, fy=y, fc=color, fd=flash_duration):
|
||||
flash_cell(grid, fx, fy, fc, fd)
|
||||
|
||||
mcrfpy.Timer(f"flash_{x}_{y}_{i}", do_flash, int(delay * 1000), once=True)
|
||||
delay += flash_duration * 1.5 # Gap between flashes
|
||||
|
||||
# Usage for critical hit
|
||||
multi_flash(grid, int(enemy.x), int(enemy.y), (255, 255, 0), flashes=3)
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
"""McRogueFace - Floating Damage Numbers (effects_floating_text)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_floating_text
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_floating_text.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class StackedFloatingText:
|
||||
"""Prevents overlapping text by stacking vertically."""
|
||||
|
||||
def __init__(self, scene_name, grid=None):
|
||||
self.manager = FloatingTextManager(scene_name, grid)
|
||||
self.position_stack = {} # Track recent spawns per position
|
||||
|
||||
def spawn_stacked(self, x, y, text, color, **kwargs):
|
||||
"""Spawn with automatic vertical stacking."""
|
||||
key = (int(x), int(y))
|
||||
|
||||
# Calculate offset based on recent spawns at this position
|
||||
offset = self.position_stack.get(key, 0)
|
||||
actual_y = y - (offset * 20) # 20 pixels between stacked texts
|
||||
|
||||
self.manager.spawn(x, actual_y, text, color, **kwargs)
|
||||
|
||||
# Increment stack counter
|
||||
self.position_stack[key] = offset + 1
|
||||
|
||||
# Reset stack after delay
|
||||
def reset_stack(timer_name, k=key):
|
||||
if k in self.position_stack:
|
||||
self.position_stack[k] = max(0, self.position_stack[k] - 1)
|
||||
|
||||
mcrfpy.Timer(f"stack_reset_{x}_{y}_{offset}", reset_stack, 300, once=True)
|
||||
|
||||
# Usage
|
||||
stacked = StackedFloatingText("game", grid)
|
||||
# Rapid hits will stack vertically instead of overlapping
|
||||
stacked.spawn_stacked(5, 5, "-10", (255, 0, 0), is_grid_pos=True)
|
||||
stacked.spawn_stacked(5, 5, "-8", (255, 0, 0), is_grid_pos=True)
|
||||
stacked.spawn_stacked(5, 5, "-12", (255, 0, 0), is_grid_pos=True)
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
"""McRogueFace - Path Animation (Multi-Step Movement) (effects_path_animation)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_path_animation
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_path_animation.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class CameraFollowingPath:
|
||||
"""Path animator that also moves the camera."""
|
||||
|
||||
def __init__(self, entity, grid, path, step_duration=0.2):
|
||||
self.entity = entity
|
||||
self.grid = grid
|
||||
self.path = path
|
||||
self.step_duration = step_duration
|
||||
self.index = 0
|
||||
self.on_complete = None
|
||||
|
||||
def start(self):
|
||||
self.index = 0
|
||||
self._next()
|
||||
|
||||
def _next(self):
|
||||
if self.index >= len(self.path):
|
||||
if self.on_complete:
|
||||
self.on_complete(self)
|
||||
return
|
||||
|
||||
x, y = self.path[self.index]
|
||||
|
||||
def done(anim, target):
|
||||
self.index += 1
|
||||
self._next()
|
||||
|
||||
# Animate entity
|
||||
if self.entity.x != x:
|
||||
anim = mcrfpy.Animation("x", float(x), self.step_duration,
|
||||
"easeInOut", callback=done)
|
||||
anim.start(self.entity)
|
||||
elif self.entity.y != y:
|
||||
anim = mcrfpy.Animation("y", float(y), self.step_duration,
|
||||
"easeInOut", callback=done)
|
||||
anim.start(self.entity)
|
||||
else:
|
||||
done(None, None)
|
||||
return
|
||||
|
||||
# Animate camera to follow
|
||||
cam_x = mcrfpy.Animation("center_x", (x + 0.5) * 16,
|
||||
self.step_duration, "easeInOut")
|
||||
cam_y = mcrfpy.Animation("center_y", (y + 0.5) * 16,
|
||||
self.step_duration, "easeInOut")
|
||||
cam_x.start(self.grid)
|
||||
cam_y.start(self.grid)
|
||||
|
||||
|
||||
# Usage
|
||||
path = [(5, 5), (5, 10), (10, 10)]
|
||||
mover = CameraFollowingPath(player, grid, path)
|
||||
mover.on_complete = lambda m: print("Journey complete!")
|
||||
mover.start()
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
"""McRogueFace - Scene Transition Effects (effects_scene_transitions)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_scene_transitions
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_scene_transitions.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class TransitionManager:
|
||||
"""Manages scene transitions with multiple effect types."""
|
||||
|
||||
def __init__(self, screen_width=1024, screen_height=768):
|
||||
self.width = screen_width
|
||||
self.height = screen_height
|
||||
self.is_transitioning = False
|
||||
|
||||
def go_to(self, scene_name, effect="fade", duration=0.5, **kwargs):
|
||||
"""
|
||||
Transition to a scene with the specified effect.
|
||||
|
||||
Args:
|
||||
scene_name: Target scene
|
||||
effect: "fade", "flash", "wipe", "instant"
|
||||
duration: Transition duration
|
||||
**kwargs: Effect-specific options (color, direction)
|
||||
"""
|
||||
if self.is_transitioning:
|
||||
return
|
||||
|
||||
self.is_transitioning = True
|
||||
|
||||
if effect == "instant":
|
||||
mcrfpy.setScene(scene_name)
|
||||
self.is_transitioning = False
|
||||
|
||||
elif effect == "fade":
|
||||
color = kwargs.get("color", (0, 0, 0))
|
||||
self._fade(scene_name, duration, color)
|
||||
|
||||
elif effect == "flash":
|
||||
color = kwargs.get("color", (255, 255, 255))
|
||||
self._flash(scene_name, duration, color)
|
||||
|
||||
elif effect == "wipe":
|
||||
direction = kwargs.get("direction", "right")
|
||||
color = kwargs.get("color", (0, 0, 0))
|
||||
self._wipe(scene_name, duration, direction, color)
|
||||
|
||||
def _fade(self, scene, duration, color):
|
||||
half = duration / 2
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 0)
|
||||
overlay.z_index = 9999
|
||||
ui.append(overlay)
|
||||
|
||||
anim = mcrfpy.Animation("opacity", 1.0, half, "easeIn")
|
||||
anim.start(overlay)
|
||||
|
||||
def phase2(timer_name):
|
||||
mcrfpy.setScene(scene)
|
||||
new_ui = mcrfpy.sceneUI(scene)
|
||||
|
||||
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
|
||||
new_overlay.z_index = 9999
|
||||
new_ui.append(new_overlay)
|
||||
|
||||
anim2 = mcrfpy.Animation("opacity", 0.0, half, "easeOut")
|
||||
anim2.start(new_overlay)
|
||||
|
||||
def cleanup(timer_name):
|
||||
for i, elem in enumerate(new_ui):
|
||||
if elem is new_overlay:
|
||||
new_ui.remove(i)
|
||||
break
|
||||
self.is_transitioning = False
|
||||
|
||||
mcrfpy.Timer("fade_done", cleanup, int(half * 1000) + 50, once=True)
|
||||
|
||||
mcrfpy.Timer("fade_switch", phase2, int(half * 1000), once=True)
|
||||
|
||||
def _flash(self, scene, duration, color):
|
||||
quarter = duration / 4
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 0)
|
||||
overlay.z_index = 9999
|
||||
ui.append(overlay)
|
||||
|
||||
anim = mcrfpy.Animation("opacity", 1.0, quarter, "easeOut")
|
||||
anim.start(overlay)
|
||||
|
||||
def phase2(timer_name):
|
||||
mcrfpy.setScene(scene)
|
||||
new_ui = mcrfpy.sceneUI(scene)
|
||||
|
||||
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
|
||||
new_overlay.z_index = 9999
|
||||
new_ui.append(new_overlay)
|
||||
|
||||
anim2 = mcrfpy.Animation("opacity", 0.0, duration / 2, "easeIn")
|
||||
anim2.start(new_overlay)
|
||||
|
||||
def cleanup(timer_name):
|
||||
for i, elem in enumerate(new_ui):
|
||||
if elem is new_overlay:
|
||||
new_ui.remove(i)
|
||||
break
|
||||
self.is_transitioning = False
|
||||
|
||||
mcrfpy.Timer("flash_done", cleanup, int(duration * 500) + 50, once=True)
|
||||
|
||||
mcrfpy.Timer("flash_switch", phase2, int(quarter * 2000), once=True)
|
||||
|
||||
def _wipe(self, scene, duration, direction, color):
|
||||
# Simplified wipe - right direction only for brevity
|
||||
half = duration / 2
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
overlay = mcrfpy.Frame(0, 0, 0, self.height)
|
||||
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
|
||||
overlay.z_index = 9999
|
||||
ui.append(overlay)
|
||||
|
||||
anim = mcrfpy.Animation("w", float(self.width), half, "easeInOut")
|
||||
anim.start(overlay)
|
||||
|
||||
def phase2(timer_name):
|
||||
mcrfpy.setScene(scene)
|
||||
new_ui = mcrfpy.sceneUI(scene)
|
||||
|
||||
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
|
||||
new_overlay.z_index = 9999
|
||||
new_ui.append(new_overlay)
|
||||
|
||||
anim2 = mcrfpy.Animation("x", float(self.width), half, "easeInOut")
|
||||
anim2.start(new_overlay)
|
||||
|
||||
def cleanup(timer_name):
|
||||
for i, elem in enumerate(new_ui):
|
||||
if elem is new_overlay:
|
||||
new_ui.remove(i)
|
||||
break
|
||||
self.is_transitioning = False
|
||||
|
||||
mcrfpy.Timer("wipe_done", cleanup, int(half * 1000) + 50, once=True)
|
||||
|
||||
mcrfpy.Timer("wipe_switch", phase2, int(half * 1000), once=True)
|
||||
|
||||
|
||||
# Usage
|
||||
transitions = TransitionManager()
|
||||
|
||||
# Various transition styles
|
||||
transitions.go_to("game", effect="fade", duration=0.5)
|
||||
transitions.go_to("menu", effect="flash", color=(255, 255, 255), duration=0.4)
|
||||
transitions.go_to("next_level", effect="wipe", direction="right", duration=0.6)
|
||||
transitions.go_to("options", effect="instant")
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
"""McRogueFace - Screen Shake Effect (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_screen_shake
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_screen_shake_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
def screen_shake(frame, intensity=5, duration=0.2):
|
||||
"""
|
||||
Shake a frame/container by animating its position.
|
||||
|
||||
Args:
|
||||
frame: The UI Frame to shake (often a container for all game elements)
|
||||
intensity: Maximum pixel offset
|
||||
duration: Total shake duration in seconds
|
||||
"""
|
||||
original_x = frame.x
|
||||
original_y = frame.y
|
||||
|
||||
# Quick shake to offset position
|
||||
shake_x = mcrfpy.Animation("x", float(original_x + intensity), duration / 4, "easeOut")
|
||||
shake_x.start(frame)
|
||||
|
||||
# Schedule return to center
|
||||
def return_to_center(timer_name):
|
||||
anim = mcrfpy.Animation("x", float(original_x), duration / 2, "easeInOut")
|
||||
anim.start(frame)
|
||||
|
||||
mcrfpy.Timer("shake_return", return_to_center, int(duration * 250), once=True)
|
||||
|
||||
# Usage - wrap your game content in a Frame
|
||||
game_container = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
# ... add game elements to game_container.children ...
|
||||
screen_shake(game_container, intensity=8, duration=0.3)
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
"""McRogueFace - Screen Shake Effect (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_screen_shake
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_screen_shake_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import math
|
||||
|
||||
def directional_shake(shaker, direction_x, direction_y, intensity=10, duration=0.2):
|
||||
"""
|
||||
Shake in a specific direction (e.g., direction of impact).
|
||||
|
||||
Args:
|
||||
shaker: ScreenShakeManager instance
|
||||
direction_x, direction_y: Direction vector (will be normalized)
|
||||
intensity: Shake strength
|
||||
duration: Shake duration
|
||||
"""
|
||||
# Normalize direction
|
||||
length = math.sqrt(direction_x * direction_x + direction_y * direction_y)
|
||||
if length == 0:
|
||||
return
|
||||
|
||||
dir_x = direction_x / length
|
||||
dir_y = direction_y / length
|
||||
|
||||
# Shake in the direction, then opposite, then back
|
||||
shaker._animate_position(
|
||||
shaker.original_x + dir_x * intensity,
|
||||
shaker.original_y + dir_y * intensity,
|
||||
duration / 3
|
||||
)
|
||||
|
||||
def reverse(timer_name):
|
||||
shaker._animate_position(
|
||||
shaker.original_x - dir_x * intensity * 0.5,
|
||||
shaker.original_y - dir_y * intensity * 0.5,
|
||||
duration / 3
|
||||
)
|
||||
|
||||
def reset(timer_name):
|
||||
shaker._animate_position(
|
||||
shaker.original_x,
|
||||
shaker.original_y,
|
||||
duration / 3
|
||||
)
|
||||
shaker.is_shaking = False
|
||||
|
||||
mcrfpy.Timer("dir_shake_rev", reverse, int(duration * 333), once=True)
|
||||
mcrfpy.Timer("dir_shake_reset", reset, int(duration * 666), once=True)
|
||||
|
||||
# Usage: shake away from impact direction
|
||||
hit_from_x, hit_from_y = -1, 0 # Hit from the left
|
||||
directional_shake(shaker, hit_from_x, hit_from_y, intensity=12)
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
"""McRogueFace - Cell Highlighting (Targeting) (animated)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_animated.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class TargetingSystem:
|
||||
"""Handle ability targeting with visual feedback."""
|
||||
|
||||
def __init__(self, grid, player):
|
||||
self.grid = grid
|
||||
self.player = player
|
||||
self.highlights = HighlightManager(grid)
|
||||
self.current_ability = None
|
||||
self.valid_targets = set()
|
||||
|
||||
def start_targeting(self, ability):
|
||||
"""Begin targeting for an ability."""
|
||||
self.current_ability = ability
|
||||
px, py = self.player.pos
|
||||
|
||||
# Get valid targets based on ability
|
||||
if ability.target_type == 'self':
|
||||
self.valid_targets = {(px, py)}
|
||||
elif ability.target_type == 'adjacent':
|
||||
self.valid_targets = get_adjacent(px, py)
|
||||
elif ability.target_type == 'ranged':
|
||||
self.valid_targets = get_radius_range(px, py, ability.range)
|
||||
elif ability.target_type == 'line':
|
||||
self.valid_targets = get_line_range(px, py, ability.range)
|
||||
|
||||
# Filter to visible tiles only
|
||||
self.valid_targets = {
|
||||
(x, y) for x, y in self.valid_targets
|
||||
if grid.is_in_fov(x, y)
|
||||
}
|
||||
|
||||
# Show valid targets
|
||||
self.highlights.add('attack', self.valid_targets)
|
||||
|
||||
def update_hover(self, x, y):
|
||||
"""Update when cursor moves."""
|
||||
if not self.current_ability:
|
||||
return
|
||||
|
||||
# Clear previous AoE preview
|
||||
self.highlights.remove('danger')
|
||||
|
||||
if (x, y) in self.valid_targets:
|
||||
# Valid target - highlight it
|
||||
self.highlights.add('select', [(x, y)])
|
||||
|
||||
# Show AoE if applicable
|
||||
if self.current_ability.aoe_radius > 0:
|
||||
aoe = get_radius_range(x, y, self.current_ability.aoe_radius, True)
|
||||
self.highlights.add('danger', aoe)
|
||||
else:
|
||||
self.highlights.remove('select')
|
||||
|
||||
def confirm_target(self, x, y):
|
||||
"""Confirm target selection."""
|
||||
if (x, y) in self.valid_targets:
|
||||
self.cancel_targeting()
|
||||
return (x, y)
|
||||
return None
|
||||
|
||||
def cancel_targeting(self):
|
||||
"""Cancel targeting mode."""
|
||||
self.current_ability = None
|
||||
self.valid_targets = set()
|
||||
self.highlights.clear()
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
"""McRogueFace - Cell Highlighting (Targeting) (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def get_line_range(start_x, start_y, max_range):
|
||||
"""Get cells in cardinal directions (ranged attack)."""
|
||||
cells = set()
|
||||
|
||||
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
|
||||
for dist in range(1, max_range + 1):
|
||||
x = start_x + dx * dist
|
||||
y = start_y + dy * dist
|
||||
|
||||
# Stop if wall blocks line of sight
|
||||
if not grid.at(x, y).transparent:
|
||||
break
|
||||
|
||||
cells.add((x, y))
|
||||
|
||||
return cells
|
||||
|
||||
def get_radius_range(center_x, center_y, radius, include_center=False):
|
||||
"""Get cells within a radius (spell area)."""
|
||||
cells = set()
|
||||
|
||||
for x in range(center_x - radius, center_x + radius + 1):
|
||||
for y in range(center_y - radius, center_y + radius + 1):
|
||||
# Euclidean distance
|
||||
dist = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5
|
||||
if dist <= radius:
|
||||
if include_center or (x, y) != (center_x, center_y):
|
||||
cells.add((x, y))
|
||||
|
||||
return cells
|
||||
|
||||
def get_cone_range(origin_x, origin_y, direction, length, spread):
|
||||
"""Get cells in a cone (breath attack)."""
|
||||
import math
|
||||
cells = set()
|
||||
|
||||
# Direction angles (in radians)
|
||||
angles = {
|
||||
'n': -math.pi / 2,
|
||||
's': math.pi / 2,
|
||||
'e': 0,
|
||||
'w': math.pi,
|
||||
'ne': -math.pi / 4,
|
||||
'nw': -3 * math.pi / 4,
|
||||
'se': math.pi / 4,
|
||||
'sw': 3 * math.pi / 4
|
||||
}
|
||||
|
||||
base_angle = angles.get(direction, 0)
|
||||
half_spread = math.radians(spread / 2)
|
||||
|
||||
for x in range(origin_x - length, origin_x + length + 1):
|
||||
for y in range(origin_y - length, origin_y + length + 1):
|
||||
dx = x - origin_x
|
||||
dy = y - origin_y
|
||||
dist = (dx * dx + dy * dy) ** 0.5
|
||||
|
||||
if dist > 0 and dist <= length:
|
||||
angle = math.atan2(dy, dx)
|
||||
angle_diff = abs((angle - base_angle + math.pi) % (2 * math.pi) - math.pi)
|
||||
|
||||
if angle_diff <= half_spread:
|
||||
cells.add((x, y))
|
||||
|
||||
return cells
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
"""McRogueFace - Cell Highlighting (Targeting) (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def show_path_preview(start, end):
|
||||
"""Highlight the path between two points."""
|
||||
path = find_path(start, end) # Your pathfinding function
|
||||
|
||||
if path:
|
||||
highlights.add('path', path)
|
||||
|
||||
# Highlight destination specially
|
||||
highlights.add('select', [end])
|
||||
|
||||
def hide_path_preview():
|
||||
"""Clear path display."""
|
||||
highlights.remove('path')
|
||||
highlights.remove('select')
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
"""McRogueFace - Dijkstra Distance Maps (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_dijkstra
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dijkstra_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def ai_flee(entity, threat_x, threat_y):
|
||||
"""Move entity away from threat using Dijkstra map."""
|
||||
grid.compute_dijkstra(threat_x, threat_y)
|
||||
|
||||
ex, ey = entity.pos
|
||||
current_dist = grid.get_dijkstra_distance(ex, ey)
|
||||
|
||||
# Find neighbor with highest distance
|
||||
best_move = None
|
||||
best_dist = current_dist
|
||||
|
||||
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
|
||||
nx, ny = ex + dx, ey + dy
|
||||
|
||||
if grid.at(nx, ny).walkable:
|
||||
dist = grid.get_dijkstra_distance(nx, ny)
|
||||
if dist > best_dist:
|
||||
best_dist = dist
|
||||
best_move = (nx, ny)
|
||||
|
||||
if best_move:
|
||||
entity.pos = best_move
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
"""McRogueFace - Dijkstra Distance Maps (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_dijkstra
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dijkstra_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
# Cache Dijkstra maps when possible
|
||||
class CachedDijkstra:
|
||||
"""Cache Dijkstra computations."""
|
||||
|
||||
def __init__(self, grid):
|
||||
self.grid = grid
|
||||
self.cache = {}
|
||||
self.cache_valid = False
|
||||
|
||||
def invalidate(self):
|
||||
"""Call when map changes."""
|
||||
self.cache = {}
|
||||
self.cache_valid = False
|
||||
|
||||
def get_distance(self, from_x, from_y, to_x, to_y):
|
||||
"""Get cached distance or compute."""
|
||||
key = (to_x, to_y) # Cache by destination
|
||||
|
||||
if key not in self.cache:
|
||||
self.grid.compute_dijkstra(to_x, to_y)
|
||||
# Store all distances from this computation
|
||||
self.cache[key] = self._snapshot_distances()
|
||||
|
||||
return self.cache[key].get((from_x, from_y), float('inf'))
|
||||
|
||||
def _snapshot_distances(self):
|
||||
"""Capture current distance values."""
|
||||
grid_w, grid_h = self.grid.grid_size
|
||||
distances = {}
|
||||
for x in range(grid_w):
|
||||
for y in range(grid_h):
|
||||
dist = self.grid.get_dijkstra_distance(x, y)
|
||||
if dist != float('inf'):
|
||||
distances[(x, y)] = dist
|
||||
return distances
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
"""McRogueFace - Room and Corridor Generator (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_dungeon_generator
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dungeon_generator_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class BSPNode:
|
||||
"""Node in a BSP tree for dungeon generation."""
|
||||
|
||||
MIN_SIZE = 6
|
||||
|
||||
def __init__(self, x, y, w, h):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.left = None
|
||||
self.right = None
|
||||
self.room = None
|
||||
|
||||
def split(self):
|
||||
"""Recursively split this node."""
|
||||
if self.left or self.right:
|
||||
return False
|
||||
|
||||
# Choose split direction
|
||||
if self.w > self.h and self.w / self.h >= 1.25:
|
||||
horizontal = False
|
||||
elif self.h > self.w and self.h / self.w >= 1.25:
|
||||
horizontal = True
|
||||
else:
|
||||
horizontal = random.random() < 0.5
|
||||
|
||||
max_size = (self.h if horizontal else self.w) - self.MIN_SIZE
|
||||
if max_size <= self.MIN_SIZE:
|
||||
return False
|
||||
|
||||
split = random.randint(self.MIN_SIZE, max_size)
|
||||
|
||||
if horizontal:
|
||||
self.left = BSPNode(self.x, self.y, self.w, split)
|
||||
self.right = BSPNode(self.x, self.y + split, self.w, self.h - split)
|
||||
else:
|
||||
self.left = BSPNode(self.x, self.y, split, self.h)
|
||||
self.right = BSPNode(self.x + split, self.y, self.w - split, self.h)
|
||||
|
||||
return True
|
||||
|
||||
def create_rooms(self, grid):
|
||||
"""Create rooms in leaf nodes and connect siblings."""
|
||||
if self.left or self.right:
|
||||
if self.left:
|
||||
self.left.create_rooms(grid)
|
||||
if self.right:
|
||||
self.right.create_rooms(grid)
|
||||
|
||||
# Connect children
|
||||
if self.left and self.right:
|
||||
left_room = self.left.get_room()
|
||||
right_room = self.right.get_room()
|
||||
if left_room and right_room:
|
||||
connect_points(grid, left_room.center, right_room.center)
|
||||
else:
|
||||
# Leaf node - create room
|
||||
w = random.randint(3, self.w - 2)
|
||||
h = random.randint(3, self.h - 2)
|
||||
x = self.x + random.randint(1, self.w - w - 1)
|
||||
y = self.y + random.randint(1, self.h - h - 1)
|
||||
self.room = Room(x, y, w, h)
|
||||
carve_room(grid, self.room)
|
||||
|
||||
def get_room(self):
|
||||
"""Get a room from this node or its children."""
|
||||
if self.room:
|
||||
return self.room
|
||||
|
||||
left_room = self.left.get_room() if self.left else None
|
||||
right_room = self.right.get_room() if self.right else None
|
||||
|
||||
if left_room and right_room:
|
||||
return random.choice([left_room, right_room])
|
||||
return left_room or right_room
|
||||
|
||||
|
||||
def generate_bsp_dungeon(grid, iterations=4):
|
||||
"""Generate a BSP-based dungeon."""
|
||||
grid_w, grid_h = grid.grid_size
|
||||
|
||||
# Fill with walls
|
||||
for x in range(grid_w):
|
||||
for y in range(grid_h):
|
||||
point = grid.at(x, y)
|
||||
point.tilesprite = TILE_WALL
|
||||
point.walkable = False
|
||||
point.transparent = False
|
||||
|
||||
# Build BSP tree
|
||||
root = BSPNode(0, 0, grid_w, grid_h)
|
||||
nodes = [root]
|
||||
|
||||
for _ in range(iterations):
|
||||
new_nodes = []
|
||||
for node in nodes:
|
||||
if node.split():
|
||||
new_nodes.extend([node.left, node.right])
|
||||
nodes = new_nodes or nodes
|
||||
|
||||
# Create rooms and corridors
|
||||
root.create_rooms(grid)
|
||||
|
||||
# Collect all rooms
|
||||
rooms = []
|
||||
def collect_rooms(node):
|
||||
if node.room:
|
||||
rooms.append(node.room)
|
||||
if node.left:
|
||||
collect_rooms(node.left)
|
||||
if node.right:
|
||||
collect_rooms(node.right)
|
||||
|
||||
collect_rooms(root)
|
||||
return rooms
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
"""McRogueFace - Room and Corridor Generator (complete)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_dungeon_generator
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dungeon_generator_complete.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Tile indices (adjust for your tileset)
|
||||
TILE_FLOOR = 0
|
||||
TILE_WALL = 1
|
||||
TILE_DOOR = 2
|
||||
TILE_STAIRS_DOWN = 3
|
||||
TILE_STAIRS_UP = 4
|
||||
|
||||
class DungeonGenerator:
|
||||
"""Procedural dungeon generator with rooms and corridors."""
|
||||
|
||||
def __init__(self, grid, seed=None):
|
||||
self.grid = grid
|
||||
self.grid_w, self.grid_h = grid.grid_size
|
||||
self.rooms = []
|
||||
|
||||
if seed is not None:
|
||||
random.seed(seed)
|
||||
|
||||
def generate(self, room_count=8, min_room=4, max_room=10):
|
||||
"""Generate a complete dungeon level."""
|
||||
self.rooms = []
|
||||
|
||||
# Fill with walls
|
||||
self._fill_walls()
|
||||
|
||||
# Place rooms
|
||||
attempts = 0
|
||||
max_attempts = room_count * 10
|
||||
|
||||
while len(self.rooms) < room_count and attempts < max_attempts:
|
||||
attempts += 1
|
||||
|
||||
# Random room size
|
||||
w = random.randint(min_room, max_room)
|
||||
h = random.randint(min_room, max_room)
|
||||
|
||||
# Random position (leaving border)
|
||||
x = random.randint(1, self.grid_w - w - 2)
|
||||
y = random.randint(1, self.grid_h - h - 2)
|
||||
|
||||
room = Room(x, y, w, h)
|
||||
|
||||
# Check overlap
|
||||
if not any(room.intersects(r) for r in self.rooms):
|
||||
self._carve_room(room)
|
||||
|
||||
# Connect to previous room
|
||||
if self.rooms:
|
||||
self._dig_corridor(self.rooms[-1].center, room.center)
|
||||
|
||||
self.rooms.append(room)
|
||||
|
||||
# Place stairs
|
||||
if len(self.rooms) >= 2:
|
||||
self._place_stairs()
|
||||
|
||||
return self.rooms
|
||||
|
||||
def _fill_walls(self):
|
||||
"""Fill the entire grid with wall tiles."""
|
||||
for x in range(self.grid_w):
|
||||
for y in range(self.grid_h):
|
||||
point = self.grid.at(x, y)
|
||||
point.tilesprite = TILE_WALL
|
||||
point.walkable = False
|
||||
point.transparent = False
|
||||
|
||||
def _carve_room(self, room):
|
||||
"""Carve out a room, making it walkable."""
|
||||
for x in range(room.x, room.x + room.width):
|
||||
for y in range(room.y, room.y + room.height):
|
||||
self._set_floor(x, y)
|
||||
|
||||
def _set_floor(self, x, y):
|
||||
"""Set a single tile as floor."""
|
||||
if 0 <= x < self.grid_w and 0 <= y < self.grid_h:
|
||||
point = self.grid.at(x, y)
|
||||
point.tilesprite = TILE_FLOOR
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
def _dig_corridor(self, start, end):
|
||||
"""Dig an L-shaped corridor between two points."""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
# Randomly choose horizontal-first or vertical-first
|
||||
if random.random() < 0.5:
|
||||
# Horizontal then vertical
|
||||
self._dig_horizontal(x1, x2, y1)
|
||||
self._dig_vertical(y1, y2, x2)
|
||||
else:
|
||||
# Vertical then horizontal
|
||||
self._dig_vertical(y1, y2, x1)
|
||||
self._dig_horizontal(x1, x2, y2)
|
||||
|
||||
def _dig_horizontal(self, x1, x2, y):
|
||||
"""Dig a horizontal tunnel."""
|
||||
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||
self._set_floor(x, y)
|
||||
|
||||
def _dig_vertical(self, y1, y2, x):
|
||||
"""Dig a vertical tunnel."""
|
||||
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||
self._set_floor(x, y)
|
||||
|
||||
def _place_stairs(self):
|
||||
"""Place stairs in first and last rooms."""
|
||||
# Stairs up in first room
|
||||
start_room = self.rooms[0]
|
||||
sx, sy = start_room.center
|
||||
point = self.grid.at(sx, sy)
|
||||
point.tilesprite = TILE_STAIRS_UP
|
||||
|
||||
# Stairs down in last room
|
||||
end_room = self.rooms[-1]
|
||||
ex, ey = end_room.center
|
||||
point = self.grid.at(ex, ey)
|
||||
point.tilesprite = TILE_STAIRS_DOWN
|
||||
|
||||
return (sx, sy), (ex, ey)
|
||||
|
||||
def get_spawn_point(self):
|
||||
"""Get a good spawn point for the player."""
|
||||
if self.rooms:
|
||||
return self.rooms[0].center
|
||||
return (self.grid_w // 2, self.grid_h // 2)
|
||||
|
||||
def get_random_floor(self):
|
||||
"""Get a random walkable floor tile."""
|
||||
floors = []
|
||||
for x in range(self.grid_w):
|
||||
for y in range(self.grid_h):
|
||||
if self.grid.at(x, y).walkable:
|
||||
floors.append((x, y))
|
||||
return random.choice(floors) if floors else None
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
"""McRogueFace - Basic Fog of War (grid_fog_of_war)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_fog_of_war
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_fog_of_war.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
# Shadowcasting (default) - fast and produces nice results
|
||||
grid.compute_fov(x, y, 10, mcrfpy.FOV.SHADOW)
|
||||
|
||||
# Recursive shadowcasting - slightly different corner behavior
|
||||
grid.compute_fov(x, y, 10, mcrfpy.FOV.RECURSIVE_SHADOW)
|
||||
|
||||
# Diamond - simple but produces diamond-shaped FOV
|
||||
grid.compute_fov(x, y, 10, mcrfpy.FOV.DIAMOND)
|
||||
|
||||
# Permissive - sees more tiles, good for tactical games
|
||||
grid.compute_fov(x, y, 10, mcrfpy.FOV.PERMISSIVE)
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
"""McRogueFace - Multi-Layer Tiles (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_multi_layer
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_multi_layer_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class EffectLayer:
|
||||
"""Manage visual effects with color overlays."""
|
||||
|
||||
def __init__(self, grid, z_index=2):
|
||||
self.grid = grid
|
||||
self.layer = grid.add_layer("color", z_index=z_index)
|
||||
self.effects = {} # (x, y) -> effect_data
|
||||
|
||||
def add_effect(self, x, y, effect_type, duration=None, **kwargs):
|
||||
"""Add a visual effect."""
|
||||
self.effects[(x, y)] = {
|
||||
'type': effect_type,
|
||||
'duration': duration,
|
||||
'time': 0,
|
||||
**kwargs
|
||||
}
|
||||
|
||||
def remove_effect(self, x, y):
|
||||
"""Remove an effect."""
|
||||
if (x, y) in self.effects:
|
||||
del self.effects[(x, y)]
|
||||
self.layer.set(x, y, mcrfpy.Color(0, 0, 0, 0))
|
||||
|
||||
def update(self, dt):
|
||||
"""Update all effects."""
|
||||
import math
|
||||
|
||||
to_remove = []
|
||||
|
||||
for (x, y), effect in self.effects.items():
|
||||
effect['time'] += dt
|
||||
|
||||
# Check expiration
|
||||
if effect['duration'] and effect['time'] >= effect['duration']:
|
||||
to_remove.append((x, y))
|
||||
continue
|
||||
|
||||
# Calculate color based on effect type
|
||||
color = self._calculate_color(effect)
|
||||
self.layer.set(x, y, color)
|
||||
|
||||
for pos in to_remove:
|
||||
self.remove_effect(*pos)
|
||||
|
||||
def _calculate_color(self, effect):
|
||||
"""Get color for an effect at current time."""
|
||||
import math
|
||||
|
||||
t = effect['time']
|
||||
effect_type = effect['type']
|
||||
|
||||
if effect_type == 'fire':
|
||||
# Flickering orange/red
|
||||
flicker = 0.7 + 0.3 * math.sin(t * 10)
|
||||
return mcrfpy.Color(
|
||||
255,
|
||||
int(100 + 50 * math.sin(t * 8)),
|
||||
0,
|
||||
int(180 * flicker)
|
||||
)
|
||||
|
||||
elif effect_type == 'poison':
|
||||
# Pulsing green
|
||||
pulse = 0.5 + 0.5 * math.sin(t * 3)
|
||||
return mcrfpy.Color(0, 200, 0, int(100 * pulse))
|
||||
|
||||
elif effect_type == 'ice':
|
||||
# Static blue with shimmer
|
||||
shimmer = 0.8 + 0.2 * math.sin(t * 5)
|
||||
return mcrfpy.Color(100, 150, 255, int(120 * shimmer))
|
||||
|
||||
elif effect_type == 'blood':
|
||||
# Fading red
|
||||
duration = effect.get('duration', 5)
|
||||
fade = 1 - (t / duration) if duration else 1
|
||||
return mcrfpy.Color(150, 0, 0, int(150 * fade))
|
||||
|
||||
elif effect_type == 'highlight':
|
||||
# Pulsing highlight
|
||||
pulse = 0.5 + 0.5 * math.sin(t * 4)
|
||||
base = effect.get('color', mcrfpy.Color(255, 255, 0, 100))
|
||||
return mcrfpy.Color(base.r, base.g, base.b, int(base.a * pulse))
|
||||
|
||||
return mcrfpy.Color(128, 128, 128, 50)
|
||||
|
||||
|
||||
# Usage
|
||||
effects = EffectLayer(grid)
|
||||
|
||||
# Add fire effect (permanent)
|
||||
effects.add_effect(5, 5, 'fire')
|
||||
|
||||
# Add blood stain (fades over 10 seconds)
|
||||
effects.add_effect(10, 10, 'blood', duration=10)
|
||||
|
||||
# Add poison cloud
|
||||
for x in range(8, 12):
|
||||
for y in range(8, 12):
|
||||
effects.add_effect(x, y, 'poison', duration=5)
|
||||
|
||||
# Update in game loop
|
||||
def game_update(runtime):
|
||||
effects.update(0.016) # 60 FPS
|
||||
|
||||
mcrfpy.setTimer("effects", game_update, 16)
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
"""McRogueFace - Multi-Layer Tiles (complete)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_multi_layer
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_multi_layer_complete.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class OptimizedLayers:
|
||||
"""Performance-optimized layer management."""
|
||||
|
||||
def __init__(self, grid):
|
||||
self.grid = grid
|
||||
self.dirty_effects = set() # Only update changed cells
|
||||
self.batch_updates = []
|
||||
|
||||
def mark_dirty(self, x, y):
|
||||
"""Mark a cell as needing update."""
|
||||
self.dirty_effects.add((x, y))
|
||||
|
||||
def batch_set(self, layer, cells_and_values):
|
||||
"""Queue batch updates."""
|
||||
self.batch_updates.append((layer, cells_and_values))
|
||||
|
||||
def flush(self):
|
||||
"""Apply all queued updates."""
|
||||
for layer, updates in self.batch_updates:
|
||||
for x, y, value in updates:
|
||||
layer.set(x, y, value)
|
||||
self.batch_updates = []
|
||||
|
||||
def update_dirty_only(self, effect_layer, effect_calculator):
|
||||
"""Only update cells marked dirty."""
|
||||
for x, y in self.dirty_effects:
|
||||
color = effect_calculator(x, y)
|
||||
effect_layer.set(x, y, color)
|
||||
self.dirty_effects.clear()
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
"""HeightMap Hills and Craters Demo
|
||||
|
||||
Demonstrates: add_hill, dig_hill
|
||||
Creates volcanic terrain with mountains and craters using ColorLayer visualization.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
# Full screen grid: 60x48 tiles at 16x16 = 960x768
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def height_to_color(h):
|
||||
"""Convert height value to terrain color."""
|
||||
if h < 0.1:
|
||||
return mcrfpy.Color(20, 40, int(80 + h * 400))
|
||||
elif h < 0.3:
|
||||
t = (h - 0.1) / 0.2
|
||||
return mcrfpy.Color(int(40 + t * 30), int(60 + t * 40), 30)
|
||||
elif h < 0.5:
|
||||
t = (h - 0.3) / 0.2
|
||||
return mcrfpy.Color(int(70 - t * 20), int(100 + t * 50), int(30 + t * 20))
|
||||
elif h < 0.7:
|
||||
t = (h - 0.5) / 0.2
|
||||
return mcrfpy.Color(int(120 + t * 40), int(100 + t * 30), int(60 + t * 20))
|
||||
elif h < 0.85:
|
||||
t = (h - 0.7) / 0.15
|
||||
return mcrfpy.Color(int(140 + t * 40), int(130 + t * 40), int(120 + t * 40))
|
||||
else:
|
||||
t = (h - 0.85) / 0.15
|
||||
return mcrfpy.Color(int(180 + t * 75), int(180 + t * 75), int(180 + t * 75))
|
||||
|
||||
# Setup scene
|
||||
scene = mcrfpy.Scene("hills_demo")
|
||||
|
||||
# Create grid with color layer
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
# Create heightmap
|
||||
hmap = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.3)
|
||||
|
||||
# Add volcanic mountains - large hills
|
||||
hmap.add_hill((15, 24), 18.0, 0.6) # Central volcano base
|
||||
hmap.add_hill((15, 24), 10.0, 0.3) # Volcano peak
|
||||
hmap.add_hill((45, 15), 12.0, 0.5) # Eastern mountain
|
||||
hmap.add_hill((35, 38), 14.0, 0.45) # Southern mountain
|
||||
hmap.add_hill((8, 10), 8.0, 0.35) # Small northern hill
|
||||
|
||||
# Create craters using dig_hill
|
||||
hmap.dig_hill((15, 24), 5.0, 0.1) # Volcanic crater
|
||||
hmap.dig_hill((45, 15), 4.0, 0.25) # Eastern crater
|
||||
hmap.dig_hill((25, 30), 6.0, 0.05) # Impact crater (deep)
|
||||
hmap.dig_hill((50, 40), 3.0, 0.2) # Small crater
|
||||
|
||||
# Add some smaller features for variety
|
||||
for i in range(8):
|
||||
x = 5 + (i * 7) % 55
|
||||
y = 5 + (i * 11) % 40
|
||||
hmap.add_hill((x, y), float(3 + (i % 4)), 0.15)
|
||||
|
||||
# Normalize to use full color range
|
||||
hmap.normalize(0.0, 1.0)
|
||||
|
||||
# Apply heightmap to color layer
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set((x, y), height_to_color(h))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="HeightMap: add_hill + dig_hill (volcanic terrain)", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Take screenshot directly (works in headless mode)
|
||||
automation.screenshot("procgen_01_heightmap_hills.png")
|
||||
print("Screenshot saved: procgen_01_heightmap_hills.png")
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
"""HeightMap Noise Integration Demo
|
||||
|
||||
Demonstrates: add_noise, multiply_noise with NoiseSource
|
||||
Shows terrain generation using different noise modes (flat, fbm, turbulence).
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def terrain_color(h):
|
||||
"""Height-based terrain coloring."""
|
||||
if h < 0.25:
|
||||
# Water - deep to shallow blue
|
||||
t = h / 0.25
|
||||
return mcrfpy.Color(int(30 + t * 30), int(60 + t * 60), int(120 + t * 80))
|
||||
elif h < 0.35:
|
||||
# Beach/sand
|
||||
t = (h - 0.25) / 0.1
|
||||
return mcrfpy.Color(int(180 + t * 40), int(160 + t * 30), int(100 + t * 20))
|
||||
elif h < 0.6:
|
||||
# Grass - varies with height
|
||||
t = (h - 0.35) / 0.25
|
||||
return mcrfpy.Color(int(50 + t * 30), int(120 + t * 40), int(40 + t * 20))
|
||||
elif h < 0.75:
|
||||
# Forest/hills
|
||||
t = (h - 0.6) / 0.15
|
||||
return mcrfpy.Color(int(40 - t * 10), int(80 + t * 20), int(30 + t * 10))
|
||||
elif h < 0.88:
|
||||
# Rock/mountain
|
||||
t = (h - 0.75) / 0.13
|
||||
return mcrfpy.Color(int(100 + t * 40), int(90 + t * 40), int(80 + t * 40))
|
||||
else:
|
||||
# Snow peaks
|
||||
t = (h - 0.88) / 0.12
|
||||
return mcrfpy.Color(int(200 + t * 55), int(200 + t * 55), int(210 + t * 45))
|
||||
|
||||
def apply_to_layer(hmap, layer):
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
h = hmap.get((x, y))
|
||||
layer.set(x, y, terrain_color(h))
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create three panels showing different noise modes
|
||||
panel_width = GRID_WIDTH // 3
|
||||
right_panel_width = GRID_WIDTH - 2 * panel_width # Handle non-divisible widths
|
||||
|
||||
# Create noise source with consistent seed
|
||||
noise = mcrfpy.NoiseSource(
|
||||
dimensions=2,
|
||||
algorithm='simplex',
|
||||
hurst=0.5,
|
||||
lacunarity=2.0,
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Left panel: Flat noise (single octave, raw)
|
||||
left_hmap = mcrfpy.HeightMap((panel_width, GRID_HEIGHT), fill=0.0)
|
||||
left_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='flat', octaves=1)
|
||||
left_hmap.normalize(0.0, 1.0)
|
||||
|
||||
# Middle panel: FBM noise (fractal brownian motion - natural terrain)
|
||||
mid_hmap = mcrfpy.HeightMap((panel_width, GRID_HEIGHT), fill=0.0)
|
||||
mid_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='fbm', octaves=6)
|
||||
mid_hmap.normalize(0.0, 1.0)
|
||||
|
||||
# Right panel: Turbulence (absolute value - clouds, marble)
|
||||
right_hmap = mcrfpy.HeightMap((right_panel_width, GRID_HEIGHT), fill=0.0)
|
||||
right_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='turbulence', octaves=6)
|
||||
right_hmap.normalize(0.0, 1.0)
|
||||
|
||||
# Apply to color layer with panel divisions
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if x < panel_width:
|
||||
h = left_hmap.get((x, y))
|
||||
elif x < panel_width * 2:
|
||||
h = mid_hmap.get((x - panel_width, y))
|
||||
else:
|
||||
h = right_hmap.get((x - panel_width * 2, y))
|
||||
color_layer.set(((x, y)), terrain_color(h))
|
||||
|
||||
# Add divider lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_width - 1, y)), mcrfpy.Color(255, 255, 255, 100))
|
||||
color_layer.set(((panel_width * 2 - 1, y)), mcrfpy.Color(255, 255, 255, 100))
|
||||
|
||||
|
||||
# Setup scene
|
||||
scene = mcrfpy.Scene("noise_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
# Labels for each panel
|
||||
labels = [
|
||||
("FLAT (raw)", 10),
|
||||
("FBM (terrain)", GRID_WIDTH * CELL_SIZE // 3 + 10),
|
||||
("TURBULENCE (clouds)", GRID_WIDTH * CELL_SIZE * 2 // 3 + 10)
|
||||
]
|
||||
for text, x in labels:
|
||||
label = mcrfpy.Caption(text=text, pos=(x, 10))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_02_heightmap_noise.png")
|
||||
print("Screenshot saved: procgen_02_heightmap_noise.png")
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
"""HeightMap Combination Operations Demo
|
||||
|
||||
Demonstrates: add, subtract, multiply, min, max, lerp, copy_from
|
||||
Shows how heightmaps can be combined for complex terrain effects.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def value_to_color(h):
|
||||
"""Simple grayscale with color tinting for visibility."""
|
||||
h = max(0.0, min(1.0, h))
|
||||
# Blue-white-red gradient for clear visualization
|
||||
if h < 0.5:
|
||||
t = h / 0.5
|
||||
return mcrfpy.Color(int(50 * t), int(100 * t), int(200 - 100 * t))
|
||||
else:
|
||||
t = (h - 0.5) / 0.5
|
||||
return mcrfpy.Color(int(50 + 200 * t), int(100 + 100 * t), int(100 - 50 * t))
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create 6 panels (2 rows x 3 columns)
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Create two base heightmaps for operations
|
||||
noise1 = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
noise2 = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=123)
|
||||
|
||||
base1 = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
base1.add_noise(noise1, world_size=(10, 10), mode='fbm', octaves=4)
|
||||
base1.normalize(0.0, 1.0)
|
||||
|
||||
base2 = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
base2.add_noise(noise2, world_size=(10, 10), mode='fbm', octaves=4)
|
||||
base2.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 1: ADD operation (combined terrain)
|
||||
add_result = base1.copy_from(base1) # Actually need to create new
|
||||
add_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
add_result.copy_from(base1).add(base2).normalize(0.0, 1.0)
|
||||
|
||||
# Panel 2: SUBTRACT operation (carving)
|
||||
sub_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
sub_result.copy_from(base1).subtract(base2).normalize(0.0, 1.0)
|
||||
|
||||
# Panel 3: MULTIPLY operation (masking)
|
||||
mul_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
mul_result.copy_from(base1).multiply(base2).normalize(0.0, 1.0)
|
||||
|
||||
# Panel 4: MIN operation (valleys)
|
||||
min_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
min_result.copy_from(base1).min(base2)
|
||||
|
||||
# Panel 5: MAX operation (ridges)
|
||||
max_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
max_result.copy_from(base1).max(base2)
|
||||
|
||||
# Panel 6: LERP operation (blending)
|
||||
lerp_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
lerp_result.copy_from(base1).lerp(base2, 0.5)
|
||||
|
||||
# Apply panels to grid
|
||||
panels = [
|
||||
(add_result, 0, 0, "ADD"),
|
||||
(sub_result, panel_w, 0, "SUBTRACT"),
|
||||
(mul_result, panel_w * 2, 0, "MULTIPLY"),
|
||||
(min_result, 0, panel_h, "MIN"),
|
||||
(max_result, panel_w, panel_h, "MAX"),
|
||||
(lerp_result, panel_w * 2, panel_h, "LERP(0.5)"),
|
||||
]
|
||||
|
||||
for hmap, ox, oy, name in panels:
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), value_to_color(h))
|
||||
|
||||
# Add label
|
||||
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Draw grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(255, 255, 255, 80))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(255, 255, 255, 80))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(255, 255, 255, 80))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("operations_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_03_heightmap_operations.png")
|
||||
print("Screenshot saved: procgen_03_heightmap_operations.png")
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
"""HeightMap Transform Operations Demo
|
||||
|
||||
Demonstrates: scale, clamp, normalize, smooth, kernel_transform
|
||||
Shows value manipulation and convolution effects.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def value_to_color(h):
|
||||
"""Grayscale with enhanced contrast."""
|
||||
h = max(0.0, min(1.0, h))
|
||||
v = int(h * 255)
|
||||
return mcrfpy.Color(v, v, v)
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create 6 panels showing different transforms
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Source noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
# Create base terrain with features
|
||||
base = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
base.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=4)
|
||||
base.add_hill((panel_w // 2, panel_h // 2), 8, 0.5)
|
||||
base.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 1: Original
|
||||
original = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
original.copy_from(base)
|
||||
|
||||
# Panel 2: SCALE (amplify contrast)
|
||||
scaled = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
scaled.copy_from(base).add_constant(-0.5).scale(2.0).clamp(0.0, 1.0)
|
||||
|
||||
# Panel 3: CLAMP (plateau effect)
|
||||
clamped = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
clamped.copy_from(base).clamp(0.3, 0.7).normalize(0.0, 1.0)
|
||||
|
||||
# Panel 4: SMOOTH (blur/average)
|
||||
smoothed = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
smoothed.copy_from(base).smooth(3)
|
||||
|
||||
# Panel 5: SHARPEN kernel
|
||||
sharpened = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
sharpened.copy_from(base)
|
||||
sharpen_kernel = {
|
||||
(0, -1): -1.0, (-1, 0): -1.0, (0, 0): 5.0, (1, 0): -1.0, (0, 1): -1.0
|
||||
}
|
||||
sharpened.kernel_transform(sharpen_kernel).clamp(0.0, 1.0)
|
||||
|
||||
# Panel 6: EDGE DETECTION kernel
|
||||
edges = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
edges.copy_from(base)
|
||||
edge_kernel = {
|
||||
(-1, -1): -1, (0, -1): -1, (1, -1): -1,
|
||||
(-1, 0): -1, (0, 0): 8, (1, 0): -1,
|
||||
(-1, 1): -1, (0, 1): -1, (1, 1): -1,
|
||||
}
|
||||
edges.kernel_transform(edge_kernel).normalize(0.0, 1.0)
|
||||
|
||||
# Apply to grid
|
||||
panels = [
|
||||
(original, 0, 0, "ORIGINAL"),
|
||||
(scaled, panel_w, 0, "SCALE (contrast)"),
|
||||
(clamped, panel_w * 2, 0, "CLAMP (plateau)"),
|
||||
(smoothed, 0, panel_h, "SMOOTH (blur)"),
|
||||
(sharpened, panel_w, panel_h, "SHARPEN kernel"),
|
||||
(edges, panel_w * 2, panel_h, "EDGE DETECT"),
|
||||
]
|
||||
|
||||
for hmap, ox, oy, name in panels:
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), value_to_color(h))
|
||||
|
||||
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(100, 100, 100))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(100, 100, 100))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("transforms_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_04_heightmap_transforms.png")
|
||||
print("Screenshot saved: procgen_04_heightmap_transforms.png")
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
"""HeightMap Erosion and Terrain Generation Demo
|
||||
|
||||
Demonstrates: rain_erosion, mid_point_displacement, smooth
|
||||
Shows natural terrain formation through erosion simulation.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def terrain_color(h):
|
||||
"""Natural terrain coloring."""
|
||||
if h < 0.2:
|
||||
# Deep water
|
||||
t = h / 0.2
|
||||
return mcrfpy.Color(int(20 + t * 30), int(40 + t * 40), int(100 + t * 55))
|
||||
elif h < 0.3:
|
||||
# Shallow water
|
||||
t = (h - 0.2) / 0.1
|
||||
return mcrfpy.Color(int(50 + t * 50), int(80 + t * 60), int(155 + t * 40))
|
||||
elif h < 0.35:
|
||||
# Beach
|
||||
t = (h - 0.3) / 0.05
|
||||
return mcrfpy.Color(int(194 - t * 30), int(178 - t * 30), int(128 - t * 20))
|
||||
elif h < 0.55:
|
||||
# Lowland grass
|
||||
t = (h - 0.35) / 0.2
|
||||
return mcrfpy.Color(int(80 + t * 20), int(140 - t * 30), int(60 + t * 10))
|
||||
elif h < 0.7:
|
||||
# Highland grass/forest
|
||||
t = (h - 0.55) / 0.15
|
||||
return mcrfpy.Color(int(50 + t * 30), int(100 + t * 10), int(40 + t * 20))
|
||||
elif h < 0.85:
|
||||
# Rock
|
||||
t = (h - 0.7) / 0.15
|
||||
return mcrfpy.Color(int(100 + t * 30), int(95 + t * 30), int(85 + t * 35))
|
||||
else:
|
||||
# Snow
|
||||
t = (h - 0.85) / 0.15
|
||||
return mcrfpy.Color(int(180 + t * 75), int(185 + t * 70), int(190 + t * 65))
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Panel 1: Mid-point displacement (raw)
|
||||
mpd_raw = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
mpd_raw.mid_point_displacement(roughness=0.6, seed=42)
|
||||
mpd_raw.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 2: Mid-point displacement + smoothing
|
||||
mpd_smooth = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
mpd_smooth.mid_point_displacement(roughness=0.6, seed=42)
|
||||
mpd_smooth.smooth(2)
|
||||
mpd_smooth.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 3: Mid-point + light erosion
|
||||
mpd_light_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
mpd_light_erode.mid_point_displacement(roughness=0.6, seed=42)
|
||||
mpd_light_erode.rain_erosion(drops=1000, erosion=0.05, sedimentation=0.03, seed=42)
|
||||
mpd_light_erode.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 4: Noise-based + moderate erosion
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=123)
|
||||
noise_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
noise_erode.add_noise(noise, world_size=(12, 12), mode='fbm', octaves=5)
|
||||
noise_erode.add_hill((panel_w // 2, panel_h // 2), 10, 0.4)
|
||||
noise_erode.rain_erosion(drops=3000, erosion=0.1, sedimentation=0.05, seed=42)
|
||||
noise_erode.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 5: Heavy erosion (river valleys)
|
||||
heavy_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
heavy_erode.mid_point_displacement(roughness=0.7, seed=99)
|
||||
heavy_erode.rain_erosion(drops=8000, erosion=0.15, sedimentation=0.02, seed=42)
|
||||
heavy_erode.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 6: Extreme erosion (canyon-like)
|
||||
extreme_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
extreme_erode.mid_point_displacement(roughness=0.5, seed=77)
|
||||
extreme_erode.rain_erosion(drops=15000, erosion=0.2, sedimentation=0.01, seed=42)
|
||||
extreme_erode.smooth(1)
|
||||
extreme_erode.normalize(0.0, 1.0)
|
||||
|
||||
# Apply to grid
|
||||
panels = [
|
||||
(mpd_raw, 0, 0, "MPD Raw"),
|
||||
(mpd_smooth, panel_w, 0, "MPD + Smooth"),
|
||||
(mpd_light_erode, panel_w * 2, 0, "Light Erosion"),
|
||||
(noise_erode, 0, panel_h, "Noise + Erosion"),
|
||||
(heavy_erode, panel_w, panel_h, "Heavy Erosion"),
|
||||
(extreme_erode, panel_w * 2, panel_h, "Extreme Erosion"),
|
||||
]
|
||||
|
||||
for hmap, ox, oy, name in panels:
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), terrain_color(h))
|
||||
|
||||
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("erosion_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_05_heightmap_erosion.png")
|
||||
print("Screenshot saved: procgen_05_heightmap_erosion.png")
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
"""HeightMap Voronoi Demo
|
||||
|
||||
Demonstrates: add_voronoi with different coefficients
|
||||
Shows cell-based patterns useful for biomes, regions, and organic structures.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def biome_color(h):
|
||||
"""Color cells as distinct biomes."""
|
||||
# Use value ranges to create distinct regions
|
||||
h = max(0.0, min(1.0, h))
|
||||
|
||||
if h < 0.15:
|
||||
return mcrfpy.Color(30, 60, 120) # Deep water
|
||||
elif h < 0.25:
|
||||
return mcrfpy.Color(50, 100, 180) # Shallow water
|
||||
elif h < 0.35:
|
||||
return mcrfpy.Color(194, 178, 128) # Beach/desert
|
||||
elif h < 0.5:
|
||||
return mcrfpy.Color(80, 160, 60) # Grassland
|
||||
elif h < 0.65:
|
||||
return mcrfpy.Color(40, 100, 40) # Forest
|
||||
elif h < 0.8:
|
||||
return mcrfpy.Color(100, 80, 60) # Hills
|
||||
elif h < 0.9:
|
||||
return mcrfpy.Color(130, 130, 130) # Mountains
|
||||
else:
|
||||
return mcrfpy.Color(240, 240, 250) # Snow
|
||||
|
||||
def cell_edges_color(h):
|
||||
"""Highlight cell boundaries."""
|
||||
h = max(0.0, min(1.0, h))
|
||||
if h < 0.3:
|
||||
return mcrfpy.Color(40, 40, 60)
|
||||
elif h < 0.6:
|
||||
return mcrfpy.Color(80, 80, 100)
|
||||
else:
|
||||
return mcrfpy.Color(200, 200, 220)
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Panel 1: Standard Voronoi (cell centers high)
|
||||
# coefficients (1, 0) = distance to nearest point
|
||||
v_standard = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_standard.add_voronoi(num_points=15, coefficients=(1.0, 0.0), seed=42)
|
||||
v_standard.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 2: Inverted (cell centers low, edges high)
|
||||
# coefficients (-1, 0) = inverted distance
|
||||
v_inverted = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_inverted.add_voronoi(num_points=15, coefficients=(-1.0, 0.0), seed=42)
|
||||
v_inverted.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 3: Cell difference (creates ridges)
|
||||
# coefficients (1, -1) = distance to nearest - distance to second nearest
|
||||
v_ridges = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_ridges.add_voronoi(num_points=15, coefficients=(1.0, -1.0), seed=42)
|
||||
v_ridges.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 4: Few large cells (biome-scale)
|
||||
v_biomes = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_biomes.add_voronoi(num_points=6, coefficients=(1.0, -0.3), seed=99)
|
||||
v_biomes.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 5: Many small cells (texture-scale)
|
||||
v_texture = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_texture.add_voronoi(num_points=50, coefficients=(1.0, -0.5), seed=77)
|
||||
v_texture.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 6: Voronoi + noise blend (natural regions)
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
v_natural = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_natural.add_voronoi(num_points=12, coefficients=(0.8, -0.4), seed=42)
|
||||
v_natural.add_noise(noise, world_size=(15, 15), mode='fbm', octaves=3, scale=0.3)
|
||||
v_natural.normalize(0.0, 1.0)
|
||||
|
||||
# Apply to grid
|
||||
panels = [
|
||||
(v_standard, 0, 0, "Standard (1,0)", biome_color),
|
||||
(v_inverted, panel_w, 0, "Inverted (-1,0)", biome_color),
|
||||
(v_ridges, panel_w * 2, 0, "Ridges (1,-1)", cell_edges_color),
|
||||
(v_biomes, 0, panel_h, "Biomes (6 pts)", biome_color),
|
||||
(v_texture, panel_w, panel_h, "Texture (50 pts)", cell_edges_color),
|
||||
(v_natural, panel_w * 2, panel_h, "Voronoi + Noise", biome_color),
|
||||
]
|
||||
|
||||
for hmap, ox, oy, name, color_func in panels:
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), color_func(h))
|
||||
|
||||
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(60, 60, 60))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(60, 60, 60))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(60, 60, 60))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("voronoi_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_06_heightmap_voronoi.png")
|
||||
print("Screenshot saved: procgen_06_heightmap_voronoi.png")
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
"""HeightMap Bezier Curves Demo
|
||||
|
||||
Demonstrates: dig_bezier for rivers, roads, and paths
|
||||
Shows path carving with variable width and depth.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import math
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def terrain_with_water(h):
|
||||
"""Terrain coloring with water in low areas."""
|
||||
if h < 0.15:
|
||||
# Water (carved paths)
|
||||
t = h / 0.15
|
||||
return mcrfpy.Color(int(30 + t * 30), int(60 + t * 50), int(140 + t * 40))
|
||||
elif h < 0.25:
|
||||
# Shore/wet ground
|
||||
t = (h - 0.15) / 0.1
|
||||
return mcrfpy.Color(int(80 + t * 40), int(100 + t * 30), int(80 - t * 20))
|
||||
elif h < 0.5:
|
||||
# Lowland
|
||||
t = (h - 0.25) / 0.25
|
||||
return mcrfpy.Color(int(70 + t * 20), int(130 + t * 20), int(50 + t * 10))
|
||||
elif h < 0.7:
|
||||
# Highland
|
||||
t = (h - 0.5) / 0.2
|
||||
return mcrfpy.Color(int(60 + t * 30), int(110 - t * 20), int(45 + t * 15))
|
||||
elif h < 0.85:
|
||||
# Hills
|
||||
t = (h - 0.7) / 0.15
|
||||
return mcrfpy.Color(int(100 + t * 30), int(95 + t * 25), int(70 + t * 30))
|
||||
else:
|
||||
# Peaks
|
||||
t = (h - 0.85) / 0.15
|
||||
return mcrfpy.Color(int(150 + t * 60), int(150 + t * 60), int(155 + t * 60))
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 2
|
||||
panel_h = GRID_HEIGHT
|
||||
|
||||
# Left panel: River system
|
||||
river_map = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
|
||||
# Add terrain
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
river_map.add_noise(noise, world_size=(10, 10), mode='fbm', octaves=4, scale=0.3)
|
||||
river_map.add_hill((panel_w // 2, 5), 12, 0.3) # Mountain source
|
||||
river_map.normalize(0.3, 0.9)
|
||||
|
||||
# Main river - wide, flowing from top to bottom
|
||||
river_map.dig_bezier(
|
||||
points=((panel_w // 2, 2), (panel_w // 4, 15), (panel_w * 3 // 4, 30), (panel_w // 2, panel_h - 3)),
|
||||
start_radius=2, end_radius=5,
|
||||
start_height=0.1, end_height=0.05
|
||||
)
|
||||
|
||||
# Tributary from left
|
||||
river_map.dig_bezier(
|
||||
points=((3, 20), (10, 18), (15, 22), (panel_w // 3, 20)),
|
||||
start_radius=1, end_radius=2,
|
||||
start_height=0.12, end_height=0.1
|
||||
)
|
||||
|
||||
# Tributary from right
|
||||
river_map.dig_bezier(
|
||||
points=((panel_w - 3, 15), (panel_w - 8, 20), (panel_w - 12, 18), (panel_w * 2 // 3, 25)),
|
||||
start_radius=1, end_radius=2,
|
||||
start_height=0.12, end_height=0.1
|
||||
)
|
||||
|
||||
# Right panel: Road network
|
||||
road_map = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
road_map.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=3, scale=0.2)
|
||||
road_map.normalize(0.35, 0.7)
|
||||
|
||||
# Main road - relatively straight
|
||||
road_map.dig_bezier(
|
||||
points=((5, panel_h // 2), (15, panel_h // 2 - 3), (panel_w - 15, panel_h // 2 + 3), (panel_w - 5, panel_h // 2)),
|
||||
start_radius=2, end_radius=2,
|
||||
start_height=0.25, end_height=0.25
|
||||
)
|
||||
|
||||
# North-south crossing road
|
||||
road_map.dig_bezier(
|
||||
points=((panel_w // 2, 5), (panel_w // 2 + 5, 15), (panel_w // 2 - 5, 35), (panel_w // 2, panel_h - 5)),
|
||||
start_radius=2, end_radius=2,
|
||||
start_height=0.25, end_height=0.25
|
||||
)
|
||||
|
||||
# Winding mountain path
|
||||
road_map.dig_bezier(
|
||||
points=((5, 8), (15, 5), (20, 15), (25, 10)),
|
||||
start_radius=1, end_radius=1,
|
||||
start_height=0.28, end_height=0.28
|
||||
)
|
||||
|
||||
# Curved path to settlement
|
||||
road_map.dig_bezier(
|
||||
points=((panel_w - 5, panel_h - 8), (panel_w - 15, panel_h - 5), (panel_w - 10, panel_h - 15), (panel_w // 2 + 5, panel_h - 10)),
|
||||
start_radius=1, end_radius=2,
|
||||
start_height=0.27, end_height=0.26
|
||||
)
|
||||
|
||||
# Apply to grid
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
# Left panel: rivers
|
||||
h = river_map.get((x, y))
|
||||
color_layer.set(((x, y)), terrain_with_water(h))
|
||||
|
||||
# Right panel: roads (use brown for roads)
|
||||
h2 = road_map.get((x, y))
|
||||
if h2 < 0.3:
|
||||
# Road surface
|
||||
t = h2 / 0.3
|
||||
color = mcrfpy.Color(int(140 - t * 40), int(120 - t * 30), int(80 - t * 20))
|
||||
else:
|
||||
color = terrain_with_water(h2)
|
||||
color_layer.set(((panel_w + x, y)), color)
|
||||
|
||||
# Divider
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
|
||||
|
||||
# Labels
|
||||
labels = [("Rivers (dig_bezier)", 10, 10), ("Roads & Paths", panel_w * CELL_SIZE + 10, 10)]
|
||||
for text, x, ypos in labels:
|
||||
label = mcrfpy.Caption(text=text, pos=(x, ypos))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bezier_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_07_heightmap_bezier.png")
|
||||
print("Screenshot saved: procgen_07_heightmap_bezier.png")
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
"""HeightMap Thresholds and ColorLayer Integration Demo
|
||||
|
||||
Demonstrates: threshold, threshold_binary, inverse, count_in_range
|
||||
Also: ColorLayer.apply_ranges for multi-threshold coloring
|
||||
Shows terrain classification and visualization techniques.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Create source terrain
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
source = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
source.add_noise(noise, world_size=(10, 10), mode='fbm', octaves=5)
|
||||
source.add_hill((panel_w // 2, panel_h // 2), 8, 0.3)
|
||||
source.normalize(0.0, 1.0)
|
||||
|
||||
# Create derived heightmaps
|
||||
water_mask = source.threshold((0.0, 0.3)) # Returns NEW heightmap with values only in range
|
||||
land_binary = source.threshold_binary((0.3, 1.0), value=1.0) # Binary mask
|
||||
inverted = source.inverse() # Inverted values
|
||||
|
||||
# Count cells in ranges for classification stats
|
||||
water_count = source.count_in_range((0.0, 0.3))
|
||||
land_count = source.count_in_range((0.3, 0.7))
|
||||
mountain_count = source.count_in_range((0.7, 1.0))
|
||||
|
||||
# IMPORTANT: Render apply_ranges FIRST since it affects the whole layer
|
||||
# Panel 6: Using ColorLayer.apply_ranges (bottom-right)
|
||||
# Create a full-size heightmap and copy source data to correct position
|
||||
panel6_hmap = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=-1.0) # -1 won't match any range
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = source.get((x, y))
|
||||
panel6_hmap.fill(h, pos=(panel_w * 2 + x, panel_h + y), size=(1, 1))
|
||||
|
||||
# apply_ranges colors cells based on height ranges
|
||||
# Cells with -1.0 won't match any range and stay unchanged
|
||||
color_layer.apply_ranges(panel6_hmap, [
|
||||
((0.0, 0.2), (30, 80, 160)), # Deep water
|
||||
((0.2, 0.3), ((60, 120, 180), (120, 160, 140))), # Gradient: shallow to shore
|
||||
((0.3, 0.5), (80, 150, 60)), # Lowland
|
||||
((0.5, 0.7), ((60, 120, 40), (100, 100, 80))), # Gradient: forest to hills
|
||||
((0.7, 0.85), (130, 120, 110)), # Rock
|
||||
((0.85, 1.0), ((180, 180, 190), (250, 250, 255))), # Gradient: rock to snow
|
||||
])
|
||||
|
||||
# Now render the other 5 panels (they will overwrite only their regions)
|
||||
|
||||
# Panel 1 (top-left): Original grayscale
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = source.get((x, y))
|
||||
v = int(h * 255)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(v, v, v))
|
||||
|
||||
# Panel 2 (top-middle): threshold() - shows only values in range 0.0-0.3
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = water_mask.get((x, y))
|
||||
if h > 0:
|
||||
# Values were preserved in 0.0-0.3 range
|
||||
t = h / 0.3
|
||||
color_layer.set(((panel_w + x, y)), mcrfpy.Color(
|
||||
int(30 + t * 40), int(60 + t * 60), int(150 + t * 50)))
|
||||
else:
|
||||
# Outside threshold range - dark
|
||||
color_layer.set(((panel_w + x, y)), mcrfpy.Color(20, 20, 30))
|
||||
|
||||
# Panel 3 (top-right): threshold_binary() - land mask
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = land_binary.get((x, y))
|
||||
if h > 0:
|
||||
color_layer.set(((panel_w * 2 + x, y)), mcrfpy.Color(80, 140, 60)) # Land
|
||||
else:
|
||||
color_layer.set(((panel_w * 2 + x, y)), mcrfpy.Color(40, 80, 150)) # Water
|
||||
|
||||
# Panel 4 (bottom-left): inverse()
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = inverted.get((x, y))
|
||||
v = int(h * 255)
|
||||
color_layer.set(((x, panel_h + y)), mcrfpy.Color(v, int(v * 0.8), int(v * 0.6)))
|
||||
|
||||
# Panel 5 (bottom-middle): Classification using count_in_range results
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = source.get((x, y))
|
||||
if h < 0.3:
|
||||
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(50, 100, 180)) # Water
|
||||
elif h < 0.7:
|
||||
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(70, 140, 50)) # Land
|
||||
else:
|
||||
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(140, 130, 120)) # Mountain
|
||||
|
||||
# Labels
|
||||
labels = [
|
||||
("Original (grayscale)", 5, 5),
|
||||
("threshold(0-0.3)", panel_w * CELL_SIZE + 5, 5),
|
||||
("threshold_binary(land)", panel_w * 2 * CELL_SIZE + 5, 5),
|
||||
("inverse()", 5, panel_h * CELL_SIZE + 5),
|
||||
(f"Classified (W:{water_count} L:{land_count} M:{mountain_count})", panel_w * CELL_SIZE + 5, panel_h * CELL_SIZE + 5),
|
||||
("apply_ranges (biome)", panel_w * 2 * CELL_SIZE + 5, panel_h * CELL_SIZE + 5),
|
||||
]
|
||||
|
||||
for text, x, y in labels:
|
||||
label = mcrfpy.Caption(text=text, pos=(x, y))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid divider lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("thresholds_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_08_heightmap_thresholds.png")
|
||||
print("Screenshot saved: procgen_08_heightmap_thresholds.png")
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
"""BSP Dungeon Generation Demo
|
||||
|
||||
Demonstrates: BSP, split_recursive, leaves iteration, to_heightmap
|
||||
Classic roguelike dungeon generation with rooms.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create BSP tree covering the map
|
||||
bsp = mcrfpy.BSP(pos=(1, 1), size=(GRID_WIDTH - 2, GRID_HEIGHT - 2))
|
||||
|
||||
# Split recursively to create rooms
|
||||
# depth=4 creates up to 16 rooms, min_size ensures rooms aren't too small
|
||||
bsp.split_recursive(depth=4, min_size=(8, 6), max_ratio=1.5, seed=42)
|
||||
|
||||
# Convert to heightmap for visualization
|
||||
# shrink=1 leaves 1-tile border for walls
|
||||
rooms_hmap = bsp.to_heightmap(
|
||||
size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
select='leaves',
|
||||
shrink=1,
|
||||
value=1.0
|
||||
)
|
||||
|
||||
# Fill background (walls)
|
||||
color_layer.fill(mcrfpy.Color(40, 35, 45))
|
||||
|
||||
# Draw rooms
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if rooms_hmap.get((x, y)) > 0:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(80, 75, 70))
|
||||
|
||||
# Add some visual variety to rooms
|
||||
room_colors = [
|
||||
mcrfpy.Color(85, 80, 75),
|
||||
mcrfpy.Color(75, 70, 65),
|
||||
mcrfpy.Color(90, 85, 80),
|
||||
mcrfpy.Color(70, 65, 60),
|
||||
]
|
||||
|
||||
for i, leaf in enumerate(bsp.leaves()):
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
color = room_colors[i % len(room_colors)]
|
||||
|
||||
# Fill room interior (with shrink)
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
color_layer.set(((x, y)), color)
|
||||
|
||||
# Mark room center
|
||||
cx, cy = leaf.center()
|
||||
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
|
||||
color_layer.set(((cx, cy)), mcrfpy.Color(200, 180, 100))
|
||||
|
||||
# Simple corridor generation: connect adjacent rooms
|
||||
# Using adjacency graph
|
||||
adjacency = bsp.adjacency
|
||||
connected = set()
|
||||
|
||||
for leaf_idx in range(len(bsp)):
|
||||
leaf = bsp.get_leaf(leaf_idx)
|
||||
cx1, cy1 = leaf.center()
|
||||
|
||||
for neighbor_idx in adjacency[leaf_idx]:
|
||||
if (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx)) in connected:
|
||||
continue
|
||||
connected.add((min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx)))
|
||||
|
||||
neighbor = bsp.get_leaf(neighbor_idx)
|
||||
cx2, cy2 = neighbor.center()
|
||||
|
||||
# Draw L-shaped corridor
|
||||
# Horizontal first, then vertical
|
||||
x1, x2 = min(cx1, cx2), max(cx1, cx2)
|
||||
for x in range(x1, x2 + 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= cy1 < GRID_HEIGHT:
|
||||
color_layer.set(((x, cy1)), mcrfpy.Color(100, 95, 90))
|
||||
|
||||
y1, y2 = min(cy1, cy2), max(cy1, cy2)
|
||||
for y in range(y1, y2 + 1):
|
||||
if 0 <= cx2 < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
color_layer.set(((cx2, y)), mcrfpy.Color(100, 95, 90))
|
||||
|
||||
# Draw outer border
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, 0)), mcrfpy.Color(60, 50, 70))
|
||||
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(60, 50, 70))
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((0, y)), mcrfpy.Color(60, 50, 70))
|
||||
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(60, 50, 70))
|
||||
|
||||
# Stats
|
||||
stats = mcrfpy.Caption(
|
||||
text=f"BSP Dungeon: {len(bsp)} rooms, depth=4, seed=42",
|
||||
pos=(10, 10)
|
||||
)
|
||||
stats.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
stats.outline = 1
|
||||
stats.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(stats)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_dungeon_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_10_bsp_dungeon.png")
|
||||
print("Screenshot saved: procgen_10_bsp_dungeon.png")
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
"""BSP Traversal Orders Demo
|
||||
|
||||
Demonstrates: traverse() with different Traversal orders
|
||||
Shows how traversal order affects leaf enumeration.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
traversal_orders = [
|
||||
(mcrfpy.Traversal.PRE_ORDER, "PRE_ORDER", "Root first, then children"),
|
||||
(mcrfpy.Traversal.IN_ORDER, "IN_ORDER", "Left, node, right"),
|
||||
(mcrfpy.Traversal.POST_ORDER, "POST_ORDER", "Children before parent"),
|
||||
(mcrfpy.Traversal.LEVEL_ORDER, "LEVEL_ORDER", "Breadth-first by level"),
|
||||
(mcrfpy.Traversal.INVERTED_LEVEL_ORDER, "INV_LEVEL", "Deepest levels first"),
|
||||
]
|
||||
|
||||
panels = [
|
||||
(0, 0), (panel_w, 0), (panel_w * 2, 0),
|
||||
(0, panel_h), (panel_w, panel_h), (panel_w * 2, panel_h)
|
||||
]
|
||||
|
||||
# Distinct color palette for 8+ leaves
|
||||
leaf_colors = [
|
||||
mcrfpy.Color(220, 60, 60), # Red
|
||||
mcrfpy.Color(60, 180, 60), # Green
|
||||
mcrfpy.Color(60, 100, 220), # Blue
|
||||
mcrfpy.Color(220, 180, 40), # Yellow
|
||||
mcrfpy.Color(180, 60, 180), # Magenta
|
||||
mcrfpy.Color(60, 200, 200), # Cyan
|
||||
mcrfpy.Color(220, 120, 60), # Orange
|
||||
mcrfpy.Color(160, 100, 200), # Purple
|
||||
mcrfpy.Color(100, 200, 120), # Mint
|
||||
mcrfpy.Color(200, 100, 140), # Pink
|
||||
]
|
||||
|
||||
for panel_idx, (order, name, desc) in enumerate(traversal_orders):
|
||||
if panel_idx >= 6:
|
||||
break
|
||||
|
||||
ox, oy = panels[panel_idx]
|
||||
|
||||
# Create BSP for this panel
|
||||
bsp = mcrfpy.BSP(pos=(ox + 2, oy + 4), size=(panel_w - 4, panel_h - 6))
|
||||
bsp.split_recursive(depth=3, min_size=(5, 4), seed=42)
|
||||
|
||||
# Fill panel background (dark gray = walls)
|
||||
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(40, 35, 45))
|
||||
|
||||
# Traverse and color ONLY LEAVES by their position in traversal
|
||||
leaf_idx = 0
|
||||
for node in bsp.traverse(order):
|
||||
if not node.is_leaf:
|
||||
continue # Skip branch nodes
|
||||
|
||||
color = leaf_colors[leaf_idx % len(leaf_colors)]
|
||||
pos = node.pos
|
||||
size = node.size
|
||||
|
||||
# Shrink by 1 to show walls between rooms
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
color_layer.set(((x, y)), color)
|
||||
|
||||
# Draw leaf index in center
|
||||
cx, cy = node.center()
|
||||
# Draw index as a darker spot
|
||||
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
|
||||
dark = mcrfpy.Color(color.r // 2, color.g // 2, color.b // 2)
|
||||
color_layer.set(((cx, cy)), dark)
|
||||
if cx + 1 < GRID_WIDTH:
|
||||
color_layer.set(((cx + 1, cy)), dark)
|
||||
|
||||
leaf_idx += 1
|
||||
|
||||
# Add labels
|
||||
label = mcrfpy.Caption(text=f"{name}", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
desc_label = mcrfpy.Caption(text=f"{desc} ({leaf_idx} leaves)", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
|
||||
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
desc_label.outline = 1
|
||||
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(desc_label)
|
||||
|
||||
# Panel 6: Show tree depth levels (branch AND leaf nodes)
|
||||
ox, oy = panels[5]
|
||||
bsp = mcrfpy.BSP(pos=(ox + 2, oy + 4), size=(panel_w - 4, panel_h - 6))
|
||||
bsp.split_recursive(depth=3, min_size=(5, 4), seed=42)
|
||||
|
||||
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(40, 35, 45))
|
||||
|
||||
# Draw by level - deepest first so leaves are on top
|
||||
level_colors = [
|
||||
mcrfpy.Color(60, 40, 40), # Level 0 (root) - dark
|
||||
mcrfpy.Color(80, 60, 50), # Level 1
|
||||
mcrfpy.Color(100, 80, 60), # Level 2
|
||||
mcrfpy.Color(140, 120, 80), # Level 3 (leaves usually)
|
||||
]
|
||||
|
||||
# Use INVERTED_LEVEL_ORDER so leaves are drawn last
|
||||
for node in bsp.traverse(mcrfpy.Traversal.INVERTED_LEVEL_ORDER):
|
||||
level = node.level
|
||||
color = level_colors[min(level, len(level_colors) - 1)]
|
||||
|
||||
# Make leaves brighter
|
||||
if node.is_leaf:
|
||||
color = mcrfpy.Color(
|
||||
min(255, color.r + 80),
|
||||
min(255, color.g + 80),
|
||||
min(255, color.b + 60)
|
||||
)
|
||||
|
||||
pos = node.pos
|
||||
size = node.size
|
||||
|
||||
for y in range(pos[1], pos[1] + size[1]):
|
||||
for x in range(pos[0], pos[0] + size[0]):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
# Draw border
|
||||
if x == pos[0] or x == pos[0] + size[0] - 1 or \
|
||||
y == pos[1] or y == pos[1] + size[1] - 1:
|
||||
border = mcrfpy.Color(20, 20, 30)
|
||||
color_layer.set(((x, y)), border)
|
||||
else:
|
||||
color_layer.set(((x, y)), color)
|
||||
|
||||
label = mcrfpy.Caption(text="BY LEVEL (depth)", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
desc_label = mcrfpy.Caption(text="Darker=root, Bright=leaves", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
|
||||
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
desc_label.outline = 1
|
||||
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(desc_label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(60, 60, 60))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(60, 60, 60))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(60, 60, 60))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_traversal_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_11_bsp_traversal.png")
|
||||
print("Screenshot saved: procgen_11_bsp_traversal.png")
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
"""BSP Adjacency Graph Demo
|
||||
|
||||
Demonstrates: adjacency property, get_leaf, adjacent_tiles
|
||||
Shows room connectivity for corridor generation.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create dungeon BSP
|
||||
bsp = mcrfpy.BSP(pos=(2, 2), size=(GRID_WIDTH - 4, GRID_HEIGHT - 4))
|
||||
bsp.split_recursive(depth=3, min_size=(10, 8), max_ratio=1.4, seed=42)
|
||||
|
||||
# Fill with wall color
|
||||
color_layer.fill(mcrfpy.Color(50, 45, 55))
|
||||
|
||||
# Generate distinct colors for each room
|
||||
num_rooms = len(bsp)
|
||||
room_colors = []
|
||||
for i in range(num_rooms):
|
||||
hue = (i * 137.5) % 360 # Golden angle for good distribution
|
||||
# HSV to RGB (simplified, saturation=0.6, value=0.7)
|
||||
h = hue / 60
|
||||
c = 0.42 # 0.6 * 0.7
|
||||
x = c * (1 - abs(h % 2 - 1))
|
||||
m = 0.28 # 0.7 - c
|
||||
|
||||
if h < 1: r, g, b = c, x, 0
|
||||
elif h < 2: r, g, b = x, c, 0
|
||||
elif h < 3: r, g, b = 0, c, x
|
||||
elif h < 4: r, g, b = 0, x, c
|
||||
elif h < 5: r, g, b = x, 0, c
|
||||
else: r, g, b = c, 0, x
|
||||
|
||||
room_colors.append(mcrfpy.Color(
|
||||
int((r + m) * 255),
|
||||
int((g + m) * 255),
|
||||
int((b + m) * 255)
|
||||
))
|
||||
|
||||
# Draw rooms with unique colors
|
||||
for i, leaf in enumerate(bsp.leaves()):
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
color = room_colors[i]
|
||||
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
color_layer.set(((x, y)), color)
|
||||
|
||||
# Room label
|
||||
cx, cy = leaf.center()
|
||||
label = mcrfpy.Caption(text=str(i), pos=(cx * CELL_SIZE - 4, cy * CELL_SIZE - 8))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Draw corridors using adjacency graph
|
||||
adjacency = bsp.adjacency
|
||||
connected = set()
|
||||
|
||||
corridor_color = mcrfpy.Color(100, 95, 90)
|
||||
door_color = mcrfpy.Color(180, 140, 80)
|
||||
|
||||
for leaf_idx in range(num_rooms):
|
||||
leaf = bsp.get_leaf(leaf_idx)
|
||||
|
||||
# Get adjacent_tiles for this leaf
|
||||
adj_tiles = leaf.adjacent_tiles
|
||||
|
||||
for neighbor_idx in adjacency[leaf_idx]:
|
||||
pair = (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx))
|
||||
if pair in connected:
|
||||
continue
|
||||
connected.add(pair)
|
||||
|
||||
neighbor = bsp.get_leaf(neighbor_idx)
|
||||
|
||||
# Find shared wall tiles
|
||||
if neighbor_idx in adj_tiles:
|
||||
wall_tiles = adj_tiles[neighbor_idx]
|
||||
if len(wall_tiles) > 0:
|
||||
# Pick middle tile for door
|
||||
mid_tile = wall_tiles[len(wall_tiles) // 2]
|
||||
dx, dy = int(mid_tile.x), int(mid_tile.y)
|
||||
|
||||
# Draw door
|
||||
color_layer.set(((dx, dy)), door_color)
|
||||
|
||||
# Simple corridor: connect room centers through door
|
||||
cx1, cy1 = leaf.center()
|
||||
cx2, cy2 = neighbor.center()
|
||||
|
||||
# Path from room 1 to door
|
||||
for x in range(min(cx1, dx), max(cx1, dx) + 1):
|
||||
color_layer.set(((x, cy1)), corridor_color)
|
||||
for y in range(min(cy1, dy), max(cy1, dy) + 1):
|
||||
color_layer.set(((dx, y)), corridor_color)
|
||||
|
||||
# Path from door to room 2
|
||||
for x in range(min(dx, cx2), max(dx, cx2) + 1):
|
||||
color_layer.set(((x, dy)), corridor_color)
|
||||
for y in range(min(dy, cy2), max(dy, cy2) + 1):
|
||||
color_layer.set(((cx2, y)), corridor_color)
|
||||
else:
|
||||
# Fallback: L-shaped corridor
|
||||
cx1, cy1 = leaf.center()
|
||||
cx2, cy2 = neighbor.center()
|
||||
|
||||
for x in range(min(cx1, cx2), max(cx1, cx2) + 1):
|
||||
color_layer.set(((x, cy1)), corridor_color)
|
||||
for y in range(min(cy1, cy2), max(cy1, cy2) + 1):
|
||||
color_layer.set(((cx2, y)), corridor_color)
|
||||
|
||||
# Title and stats
|
||||
title = mcrfpy.Caption(
|
||||
text=f"BSP Adjacency: {num_rooms} rooms, {len(connected)} connections",
|
||||
pos=(10, 10)
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
# Legend
|
||||
legend = mcrfpy.Caption(
|
||||
text="Numbers = room index, Gold = doors, Brown = corridors",
|
||||
pos=(10, GRID_HEIGHT * CELL_SIZE - 25)
|
||||
)
|
||||
legend.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
legend.outline = 1
|
||||
legend.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(legend)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_adjacency_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_12_bsp_adjacency.png")
|
||||
print("Screenshot saved: procgen_12_bsp_adjacency.png")
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
"""BSP Shrink Parameter Demo
|
||||
|
||||
Demonstrates: to_heightmap with different shrink values
|
||||
Shows room padding for walls and varied room sizes.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Use reasonable shrink values relative to room sizes
|
||||
shrink_values = [
|
||||
(0, "shrink=0", "Rooms fill BSP bounds"),
|
||||
(1, "shrink=1", "Standard 1-tile walls"),
|
||||
(2, "shrink=2", "Thick fortress walls"),
|
||||
(3, "shrink=3", "Wide hallway spacing"),
|
||||
(-1, "Random shrink", "Per-room variation"),
|
||||
(-2, "Gradient", "Shrink by leaf index"),
|
||||
]
|
||||
|
||||
panels = [
|
||||
(0, 0), (panel_w, 0), (panel_w * 2, 0),
|
||||
(0, panel_h), (panel_w, panel_h), (panel_w * 2, panel_h)
|
||||
]
|
||||
|
||||
for panel_idx, (shrink, title, desc) in enumerate(shrink_values):
|
||||
ox, oy = panels[panel_idx]
|
||||
|
||||
# Create BSP - use depth=2 for larger rooms, bigger min_size
|
||||
bsp = mcrfpy.BSP(pos=(ox + 1, oy + 3), size=(panel_w - 2, panel_h - 4))
|
||||
bsp.split_recursive(depth=2, min_size=(8, 6), seed=42)
|
||||
|
||||
# Fill panel background (stone wall)
|
||||
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(50, 45, 55))
|
||||
|
||||
if shrink >= 0:
|
||||
# Standard shrink value using to_heightmap
|
||||
rooms_hmap = bsp.to_heightmap(
|
||||
size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
select='leaves',
|
||||
shrink=shrink,
|
||||
value=1.0
|
||||
)
|
||||
|
||||
# Draw floors with color based on shrink level
|
||||
floor_colors = [
|
||||
mcrfpy.Color(140, 120, 100), # shrink=0: tan/full
|
||||
mcrfpy.Color(110, 100, 90), # shrink=1: gray-brown
|
||||
mcrfpy.Color(90, 95, 100), # shrink=2: blue-gray
|
||||
mcrfpy.Color(80, 90, 110), # shrink=3: slate
|
||||
]
|
||||
floor_color = floor_colors[min(shrink, len(floor_colors) - 1)]
|
||||
|
||||
for y in range(oy, oy + panel_h):
|
||||
for x in range(ox, ox + panel_w):
|
||||
if rooms_hmap.get((x, y)) > 0:
|
||||
# Add subtle tile pattern
|
||||
var = ((x + y) % 2) * 8
|
||||
c = mcrfpy.Color(
|
||||
floor_color.r + var,
|
||||
floor_color.g + var,
|
||||
floor_color.b + var
|
||||
)
|
||||
color_layer.set(((x, y)), c)
|
||||
elif shrink == -1:
|
||||
# Random shrink per room
|
||||
import random
|
||||
rand = random.Random(42)
|
||||
for leaf in bsp.leaves():
|
||||
room_shrink = rand.randint(0, 3)
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
|
||||
x1 = pos[0] + room_shrink
|
||||
y1 = pos[1] + room_shrink
|
||||
x2 = pos[0] + size[0] - room_shrink
|
||||
y2 = pos[1] + size[1] - room_shrink
|
||||
|
||||
if x2 > x1 and y2 > y1:
|
||||
colors = [
|
||||
mcrfpy.Color(160, 130, 100), # Full
|
||||
mcrfpy.Color(130, 120, 100),
|
||||
mcrfpy.Color(100, 110, 110),
|
||||
mcrfpy.Color(80, 90, 100), # Most shrunk
|
||||
]
|
||||
floor_color = colors[room_shrink]
|
||||
|
||||
for y in range(y1, y2):
|
||||
for x in range(x1, x2):
|
||||
if ox <= x < ox + panel_w and oy <= y < oy + panel_h:
|
||||
var = ((x + y) % 2) * 6
|
||||
c = mcrfpy.Color(
|
||||
floor_color.r + var,
|
||||
floor_color.g + var,
|
||||
floor_color.b + var
|
||||
)
|
||||
color_layer.set(((x, y)), c)
|
||||
else:
|
||||
# Gradient shrink by leaf index
|
||||
leaves = list(bsp.leaves())
|
||||
for i, leaf in enumerate(leaves):
|
||||
# Shrink increases with leaf index
|
||||
room_shrink = min(3, i)
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
|
||||
x1 = pos[0] + room_shrink
|
||||
y1 = pos[1] + room_shrink
|
||||
x2 = pos[0] + size[0] - room_shrink
|
||||
y2 = pos[1] + size[1] - room_shrink
|
||||
|
||||
if x2 > x1 and y2 > y1:
|
||||
# Color gradient: warm to cool as shrink increases
|
||||
t = i / max(1, len(leaves) - 1)
|
||||
floor_color = mcrfpy.Color(
|
||||
int(180 - t * 80),
|
||||
int(120 + t * 20),
|
||||
int(80 + t * 60)
|
||||
)
|
||||
|
||||
for y in range(y1, y2):
|
||||
for x in range(x1, x2):
|
||||
if ox <= x < ox + panel_w and oy <= y < oy + panel_h:
|
||||
var = ((x + y) % 2) * 6
|
||||
c = mcrfpy.Color(
|
||||
floor_color.r + var,
|
||||
floor_color.g + var - 2,
|
||||
floor_color.b + var
|
||||
)
|
||||
color_layer.set(((x, y)), c)
|
||||
|
||||
# Add labels
|
||||
label = mcrfpy.Caption(text=title, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
desc_label = mcrfpy.Caption(text=desc, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
|
||||
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
desc_label.outline = 1
|
||||
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(desc_label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(30, 30, 35))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(30, 30, 35))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(30, 30, 35))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_shrink_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_13_bsp_shrink.png")
|
||||
print("Screenshot saved: procgen_13_bsp_shrink.png")
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
"""BSP Manual Split Demo
|
||||
|
||||
Demonstrates: split_once for controlled layouts
|
||||
Shows handcrafted room placement with manual BSP control.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Fill background
|
||||
color_layer.fill(mcrfpy.Color(50, 45, 55))
|
||||
|
||||
# Create main BSP covering most of the map
|
||||
bsp = mcrfpy.BSP(pos=(2, 2), size=(GRID_WIDTH - 4, GRID_HEIGHT - 4))
|
||||
|
||||
# Manual split strategy for a temple-like layout:
|
||||
# 1. Split horizontally to create upper/lower sections
|
||||
# 2. Upper section: main hall (large) + side rooms
|
||||
# 3. Lower section: entrance + storage areas
|
||||
|
||||
# First split: horizontal, creating top (sanctuary) and bottom (entrance) areas
|
||||
# Split at about 60% height
|
||||
split_y = 2 + int((GRID_HEIGHT - 4) * 0.6)
|
||||
bsp.split_once(horizontal=True, position=split_y)
|
||||
|
||||
# Now manually color the structure
|
||||
root = bsp.root
|
||||
|
||||
# Get the two main regions
|
||||
upper = root.left # Sanctuary area
|
||||
lower = root.right # Entrance area
|
||||
|
||||
# Color the sanctuary (upper area) - golden temple floor
|
||||
if upper:
|
||||
pos, size = upper.pos, upper.size
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
# Create a pattern
|
||||
if (x + y) % 4 == 0:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(180, 150, 80))
|
||||
else:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(160, 130, 70))
|
||||
|
||||
# Add altar in center of sanctuary
|
||||
cx, cy = upper.center()
|
||||
for dy in range(-2, 3):
|
||||
for dx in range(-3, 4):
|
||||
nx, ny = cx + dx, cy + dy
|
||||
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
|
||||
if abs(dx) <= 1 and abs(dy) <= 1:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(200, 180, 100)) # Altar
|
||||
else:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(140, 100, 60)) # Altar base
|
||||
|
||||
# Color the entrance (lower area) - stone floor
|
||||
if lower:
|
||||
pos, size = lower.pos, lower.size
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
base = 80 + ((x * 3 + y * 7) % 20)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base, base - 5, base - 10))
|
||||
|
||||
# Add entrance path
|
||||
cx = pos[0] + size[0] // 2
|
||||
for y in range(pos[1] + size[1] - 1, pos[1], -1):
|
||||
for dx in range(-2, 3):
|
||||
nx = cx + dx
|
||||
if pos[0] < nx < pos[0] + size[0] - 1:
|
||||
color_layer.set(((nx, y)), mcrfpy.Color(100, 95, 85))
|
||||
|
||||
# Add pillars along the sides
|
||||
if upper:
|
||||
pos, size = upper.pos, upper.size
|
||||
for y in range(pos[1] + 3, pos[1] + size[1] - 3, 4):
|
||||
# Left pillars
|
||||
color_layer.set(((pos[0] + 3, y)), mcrfpy.Color(120, 110, 100))
|
||||
color_layer.set(((pos[0] + 3, y + 1)), mcrfpy.Color(120, 110, 100))
|
||||
# Right pillars
|
||||
color_layer.set(((pos[0] + size[0] - 4, y)), mcrfpy.Color(120, 110, 100))
|
||||
color_layer.set(((pos[0] + size[0] - 4, y + 1)), mcrfpy.Color(120, 110, 100))
|
||||
|
||||
# Add side chambers using manual rectangles
|
||||
# Left chamber
|
||||
chamber_w, chamber_h = 8, 6
|
||||
for y in range(10, 10 + chamber_h):
|
||||
for x in range(4, 4 + chamber_w):
|
||||
if x == 4 or x == 4 + chamber_w - 1 or y == 10 or y == 10 + chamber_h - 1:
|
||||
continue # Skip border (walls)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(100, 80, 90)) # Purple-ish storage
|
||||
|
||||
# Right chamber
|
||||
for y in range(10, 10 + chamber_h):
|
||||
for x in range(GRID_WIDTH - 4 - chamber_w, GRID_WIDTH - 4):
|
||||
if x == GRID_WIDTH - 4 - chamber_w or x == GRID_WIDTH - 5 or y == 10 or y == 10 + chamber_h - 1:
|
||||
continue
|
||||
color_layer.set(((x, y)), mcrfpy.Color(80, 100, 90)) # Green-ish treasury
|
||||
|
||||
# Connect chambers to main hall
|
||||
hall_y = 12
|
||||
for x in range(4 + chamber_w, 15):
|
||||
color_layer.set(((x, hall_y)), mcrfpy.Color(90, 85, 80))
|
||||
for x in range(GRID_WIDTH - 15, GRID_WIDTH - 4 - chamber_w):
|
||||
color_layer.set(((x, hall_y)), mcrfpy.Color(90, 85, 80))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="BSP split_once: Temple Layout", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
# Labels for areas
|
||||
labels = [
|
||||
("SANCTUARY", GRID_WIDTH // 2 * CELL_SIZE - 40, 80),
|
||||
("ENTRANCE", GRID_WIDTH // 2 * CELL_SIZE - 35, split_y * CELL_SIZE + 30),
|
||||
("Storage", 50, 180),
|
||||
("Treasury", (GRID_WIDTH - 10) * CELL_SIZE - 30, 180),
|
||||
]
|
||||
for text, x, y in labels:
|
||||
lbl = mcrfpy.Caption(text=text, pos=(x, y))
|
||||
lbl.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
lbl.outline = 1
|
||||
lbl.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(lbl)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_manual_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_14_bsp_manual_split.png")
|
||||
print("Screenshot saved: procgen_14_bsp_manual_split.png")
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
"""NoiseSource Algorithms Demo
|
||||
|
||||
Demonstrates: simplex, perlin, wavelet noise algorithms
|
||||
Shows visual differences between noise types.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def value_to_terrain(h):
|
||||
"""Convert noise value (-1 to 1) to terrain color."""
|
||||
# Normalize from -1..1 to 0..1
|
||||
h = (h + 1) / 2
|
||||
h = max(0.0, min(1.0, h))
|
||||
|
||||
if h < 0.3:
|
||||
t = h / 0.3
|
||||
return mcrfpy.Color(int(30 + t * 40), int(60 + t * 60), int(140 + t * 40))
|
||||
elif h < 0.45:
|
||||
t = (h - 0.3) / 0.15
|
||||
return mcrfpy.Color(int(70 + t * 120), int(120 + t * 60), int(100 - t * 60))
|
||||
elif h < 0.6:
|
||||
t = (h - 0.45) / 0.15
|
||||
return mcrfpy.Color(int(60 + t * 20), int(130 + t * 20), int(50 + t * 10))
|
||||
elif h < 0.75:
|
||||
t = (h - 0.6) / 0.15
|
||||
return mcrfpy.Color(int(50 + t * 50), int(110 - t * 20), int(40 + t * 20))
|
||||
elif h < 0.88:
|
||||
t = (h - 0.75) / 0.13
|
||||
return mcrfpy.Color(int(100 + t * 40), int(95 + t * 35), int(80 + t * 40))
|
||||
else:
|
||||
t = (h - 0.88) / 0.12
|
||||
return mcrfpy.Color(int(180 + t * 70), int(180 + t * 70), int(190 + t * 60))
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
algorithms = [
|
||||
('simplex', "SIMPLEX", "Fast, no visible artifacts"),
|
||||
('perlin', "PERLIN", "Classic, slight grid bias"),
|
||||
('wavelet', "WAVELET", "Smooth, no tiling"),
|
||||
]
|
||||
|
||||
# Top row: FBM (natural terrain)
|
||||
# Bottom row: Raw noise (single octave)
|
||||
for col, (algo, name, desc) in enumerate(algorithms):
|
||||
ox = col * panel_w
|
||||
|
||||
# Create noise source
|
||||
noise = mcrfpy.NoiseSource(
|
||||
dimensions=2,
|
||||
algorithm=algo,
|
||||
hurst=0.5,
|
||||
lacunarity=2.0,
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Top panel: FBM
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
# Sample at world coordinates
|
||||
wx = x * 0.15
|
||||
wy = y * 0.15
|
||||
val = noise.fbm((wx, wy), octaves=5)
|
||||
color_layer.set(((ox + x, y)), value_to_terrain(val))
|
||||
|
||||
# Bottom panel: Raw (flat)
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
wx = x * 0.15
|
||||
wy = y * 0.15
|
||||
val = noise.get((wx, wy))
|
||||
color_layer.set(((ox + x, panel_h + y)), value_to_terrain(val))
|
||||
|
||||
# Labels
|
||||
top_label = mcrfpy.Caption(text=f"{name} (FBM)", pos=(ox * CELL_SIZE + 5, 5))
|
||||
top_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
top_label.outline = 1
|
||||
top_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(top_label)
|
||||
|
||||
bottom_label = mcrfpy.Caption(text=f"{name} (raw)", pos=(ox * CELL_SIZE + 5, panel_h * CELL_SIZE + 5))
|
||||
bottom_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
bottom_label.outline = 1
|
||||
bottom_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(bottom_label)
|
||||
|
||||
desc_label = mcrfpy.Caption(text=desc, pos=(ox * CELL_SIZE + 5, 22))
|
||||
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
desc_label.outline = 1
|
||||
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(desc_label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("noise_algo_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_20_noise_algorithms.png")
|
||||
print("Screenshot saved: procgen_20_noise_algorithms.png")
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
"""NoiseSource Parameters Demo
|
||||
|
||||
Demonstrates: hurst (roughness), lacunarity (frequency scaling), octaves
|
||||
Shows how parameters affect terrain character.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def value_to_gray(h):
|
||||
"""Simple grayscale visualization."""
|
||||
h = (h + 1) / 2 # -1..1 to 0..1
|
||||
h = max(0.0, min(1.0, h))
|
||||
v = int(h * 255)
|
||||
return mcrfpy.Color(v, v, v)
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 3
|
||||
|
||||
# 3x3 grid showing parameter variations
|
||||
# Rows: different hurst values (roughness)
|
||||
# Cols: different lacunarity values
|
||||
|
||||
hurst_values = [0.2, 0.5, 0.8]
|
||||
lacunarity_values = [1.5, 2.0, 3.0]
|
||||
|
||||
for row, hurst in enumerate(hurst_values):
|
||||
for col, lacunarity in enumerate(lacunarity_values):
|
||||
ox = col * panel_w
|
||||
oy = row * panel_h
|
||||
|
||||
# Create noise with these parameters
|
||||
noise = mcrfpy.NoiseSource(
|
||||
dimensions=2,
|
||||
algorithm='simplex',
|
||||
hurst=hurst,
|
||||
lacunarity=lacunarity,
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Sample using heightmap for efficiency
|
||||
hmap = noise.sample(
|
||||
size=(panel_w, panel_h),
|
||||
world_origin=(0, 0),
|
||||
world_size=(10, 10),
|
||||
mode='fbm',
|
||||
octaves=6
|
||||
)
|
||||
|
||||
# Apply to color layer
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), value_to_gray(h))
|
||||
|
||||
# Parameter label
|
||||
label = mcrfpy.Caption(
|
||||
text=f"H={hurst} L={lacunarity}",
|
||||
pos=(ox * CELL_SIZE + 3, oy * CELL_SIZE + 3)
|
||||
)
|
||||
label.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Row/Column labels
|
||||
row_labels = ["Low Hurst (rough)", "Mid Hurst (natural)", "High Hurst (smooth)"]
|
||||
for row, text in enumerate(row_labels):
|
||||
label = mcrfpy.Caption(text=text, pos=(5, row * panel_h * CELL_SIZE + panel_h * CELL_SIZE - 20))
|
||||
label.fill_color = mcrfpy.Color(255, 200, 100)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
col_labels = ["Low Lacunarity", "Standard (2.0)", "High Lacunarity"]
|
||||
for col, text in enumerate(col_labels):
|
||||
label = mcrfpy.Caption(text=text, pos=(col * panel_w * CELL_SIZE + 5, GRID_HEIGHT * CELL_SIZE - 20))
|
||||
label.fill_color = mcrfpy.Color(100, 200, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(100, 100, 100))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(100, 100, 100))
|
||||
color_layer.set(((x, panel_h * 2 - 1)), mcrfpy.Color(100, 100, 100))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("noise_params_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_21_noise_parameters.png")
|
||||
print("Screenshot saved: procgen_21_noise_parameters.png")
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
"""Advanced: Cave-Carved Dungeon
|
||||
|
||||
Combines: BSP (room structure) + Noise (organic cave walls) + Erosion
|
||||
Creates a dungeon where rooms have been carved from natural cave formations.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Create base cave system using noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
cave_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
cave_map.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=4)
|
||||
cave_map.normalize(0.0, 1.0)
|
||||
|
||||
# Step 2: Create BSP rooms
|
||||
bsp = mcrfpy.BSP(pos=(3, 3), size=(GRID_WIDTH - 6, GRID_HEIGHT - 6))
|
||||
bsp.split_recursive(depth=3, min_size=(10, 8), max_ratio=1.5, seed=42)
|
||||
|
||||
rooms_hmap = bsp.to_heightmap(
|
||||
size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
select='leaves',
|
||||
shrink=2,
|
||||
value=1.0
|
||||
)
|
||||
|
||||
# Step 3: Combine - rooms carve into cave, cave affects walls
|
||||
combined = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
combined.copy_from(cave_map)
|
||||
|
||||
# Scale cave values to mid-range so rooms stand out
|
||||
combined.scale(0.5)
|
||||
combined.add_constant(0.2)
|
||||
|
||||
# Add room interiors (rooms become high values)
|
||||
combined.max(rooms_hmap)
|
||||
|
||||
# Step 4: Apply GENTLE erosion for organic edges
|
||||
# Use fewer drops and lower erosion rate
|
||||
combined.rain_erosion(drops=100, erosion=0.02, sedimentation=0.01, seed=42)
|
||||
|
||||
# Re-normalize to ensure we use the full value range
|
||||
combined.normalize(0.0, 1.0)
|
||||
|
||||
# Step 5: Create corridor connections
|
||||
adjacency = bsp.adjacency
|
||||
connected = set()
|
||||
|
||||
corridor_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
|
||||
for leaf_idx in range(len(bsp)):
|
||||
leaf = bsp.get_leaf(leaf_idx)
|
||||
cx1, cy1 = leaf.center()
|
||||
|
||||
for neighbor_idx in adjacency[leaf_idx]:
|
||||
pair = (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx))
|
||||
if pair in connected:
|
||||
continue
|
||||
connected.add(pair)
|
||||
|
||||
neighbor = bsp.get_leaf(neighbor_idx)
|
||||
cx2, cy2 = neighbor.center()
|
||||
|
||||
# Draw corridor using bezier for organic feel
|
||||
mid_x = (cx1 + cx2) // 2 + ((leaf_idx * 3) % 5 - 2)
|
||||
mid_y = (cy1 + cy2) // 2 + ((neighbor_idx * 7) % 5 - 2)
|
||||
|
||||
corridor_map.dig_bezier(
|
||||
points=((cx1, cy1), (mid_x, cy1), (mid_x, cy2), (cx2, cy2)),
|
||||
start_radius=1.5, end_radius=1.5,
|
||||
start_height=0.0, end_height=0.0
|
||||
)
|
||||
|
||||
# Add corridors - dig_bezier creates low values where corridors are
|
||||
# We want high values there, so invert the corridor map logic
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
corr_val = corridor_map.get((x, y))
|
||||
if corr_val < 0.5: # Corridor was dug here
|
||||
current = combined.get((x, y))
|
||||
combined.fill(max(current, 0.7), pos=(x, y), size=(1, 1))
|
||||
|
||||
# Step 6: Render with cave aesthetics
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
h = combined.get((x, y))
|
||||
|
||||
if h < 0.30:
|
||||
# Solid rock/wall - darker
|
||||
base = 30 + int(cave_map.get((x, y)) * 20)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
|
||||
elif h < 0.40:
|
||||
# Cave wall edge (rough transition)
|
||||
t = (h - 0.30) / 0.10
|
||||
base = int(40 + t * 15)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
|
||||
elif h < 0.55:
|
||||
# Cave floor (natural stone)
|
||||
t = (h - 0.40) / 0.15
|
||||
base = 65 + int(t * 20)
|
||||
var = ((x * 7 + y * 11) % 10)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + var, base - 5 + var, base - 10))
|
||||
elif h < 0.70:
|
||||
# Corridor/worked passage
|
||||
base = 85 + ((x + y) % 2) * 5
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base, base - 3, base - 6))
|
||||
else:
|
||||
# Room floor (finely worked stone)
|
||||
base = 105 + ((x + y) % 2) * 8
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base, base - 8, base - 12))
|
||||
|
||||
# Mark room centers with special tile
|
||||
for leaf in bsp.leaves():
|
||||
cx, cy = leaf.center()
|
||||
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
|
||||
color_layer.set(((cx, cy)), mcrfpy.Color(160, 140, 120))
|
||||
# Cross pattern
|
||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||
nx, ny = cx + dx, cy + dy
|
||||
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(140, 125, 105))
|
||||
|
||||
# Outer border
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, 0)), mcrfpy.Color(20, 15, 25))
|
||||
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(20, 15, 25))
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((0, y)), mcrfpy.Color(20, 15, 25))
|
||||
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(20, 15, 25))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Cave-Carved Dungeon: BSP + Noise + Erosion", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("cave_dungeon")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_30_advanced_cave_dungeon.png")
|
||||
print("Screenshot saved: procgen_30_advanced_cave_dungeon.png")
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
"""Advanced: Island Terrain Generation
|
||||
|
||||
Combines: Noise (base terrain) + Voronoi (biomes) + Hills + Erosion + Bezier (rivers)
|
||||
Creates a tropical island with varied biomes and water features.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def biome_color(elevation, moisture):
|
||||
"""Determine color based on elevation and moisture."""
|
||||
if elevation < 0.25:
|
||||
# Water
|
||||
t = elevation / 0.25
|
||||
return mcrfpy.Color(int(30 + t * 30), int(80 + t * 40), int(160 + t * 40))
|
||||
elif elevation < 0.32:
|
||||
# Beach
|
||||
return mcrfpy.Color(220, 200, 150)
|
||||
elif elevation < 0.5:
|
||||
# Lowland - varies by moisture
|
||||
if moisture < 0.3:
|
||||
return mcrfpy.Color(180, 170, 110) # Desert/savanna
|
||||
elif moisture < 0.6:
|
||||
return mcrfpy.Color(80, 140, 60) # Grassland
|
||||
else:
|
||||
return mcrfpy.Color(40, 100, 50) # Rainforest
|
||||
elif elevation < 0.7:
|
||||
# Highland
|
||||
if moisture < 0.4:
|
||||
return mcrfpy.Color(100, 90, 70) # Dry hills
|
||||
else:
|
||||
return mcrfpy.Color(50, 90, 45) # Forest
|
||||
elif elevation < 0.85:
|
||||
# Mountain
|
||||
return mcrfpy.Color(110, 105, 100)
|
||||
else:
|
||||
# Peak
|
||||
return mcrfpy.Color(220, 225, 230)
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Create base elevation using noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
elevation = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
elevation.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=5)
|
||||
elevation.normalize(0.0, 1.0)
|
||||
|
||||
# Step 2: Create island shape using radial falloff
|
||||
cx, cy = GRID_WIDTH / 2, GRID_HEIGHT / 2
|
||||
max_dist = min(cx, cy) * 0.85
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
dist = ((x - cx) ** 2 + (y - cy) ** 2) ** 0.5
|
||||
falloff = max(0, 1 - (dist / max_dist) ** 1.5)
|
||||
current = elevation.get((x, y))
|
||||
elevation.fill(current * falloff, pos=(x, y), size=(1, 1))
|
||||
|
||||
# Step 3: Add central mountain range
|
||||
elevation.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 15, 0.5)
|
||||
elevation.add_hill((GRID_WIDTH // 2 - 8, GRID_HEIGHT // 2 + 3), 8, 0.3)
|
||||
elevation.add_hill((GRID_WIDTH // 2 + 10, GRID_HEIGHT // 2 - 5), 6, 0.25)
|
||||
|
||||
# Step 4: Create moisture map using different noise
|
||||
moisture_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=123)
|
||||
moisture = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
moisture.add_noise(moisture_noise, world_size=(8, 8), mode='fbm', octaves=3)
|
||||
moisture.normalize(0.0, 1.0)
|
||||
|
||||
# Step 5: Add voronoi for biome boundaries
|
||||
biome_regions = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
biome_regions.add_voronoi(num_points=8, coefficients=(0.5, -0.3), seed=77)
|
||||
biome_regions.normalize(0.0, 1.0)
|
||||
|
||||
# Blend voronoi into moisture
|
||||
moisture.lerp(biome_regions, 0.4)
|
||||
|
||||
# Step 6: Apply erosion to elevation
|
||||
elevation.rain_erosion(drops=2000, erosion=0.08, sedimentation=0.04, seed=42)
|
||||
elevation.normalize(0.0, 1.0)
|
||||
|
||||
# Step 7: Carve rivers from mountains to sea
|
||||
# Main river
|
||||
elevation.dig_bezier(
|
||||
points=((GRID_WIDTH // 2, GRID_HEIGHT // 2 - 5),
|
||||
(GRID_WIDTH // 2 - 10, GRID_HEIGHT // 2),
|
||||
(GRID_WIDTH // 4, GRID_HEIGHT // 2 + 5),
|
||||
(5, GRID_HEIGHT // 2 + 8)),
|
||||
start_radius=0.5, end_radius=2,
|
||||
start_height=0.3, end_height=0.15
|
||||
)
|
||||
|
||||
# Secondary river
|
||||
elevation.dig_bezier(
|
||||
points=((GRID_WIDTH // 2 + 5, GRID_HEIGHT // 2),
|
||||
(GRID_WIDTH // 2 + 15, GRID_HEIGHT // 3),
|
||||
(GRID_WIDTH - 15, GRID_HEIGHT // 4),
|
||||
(GRID_WIDTH - 5, GRID_HEIGHT // 4 + 3)),
|
||||
start_radius=0.5, end_radius=1.5,
|
||||
start_height=0.32, end_height=0.18
|
||||
)
|
||||
|
||||
# Step 8: Render
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
elev = elevation.get((x, y))
|
||||
moist = moisture.get((x, y))
|
||||
color_layer.set(((x, y)), biome_color(elev, moist))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Island Terrain: Noise + Voronoi + Hills + Erosion + Rivers", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("island")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_31_advanced_island.png")
|
||||
print("Screenshot saved: procgen_31_advanced_island.png")
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
"""Advanced: Procedural City Map
|
||||
|
||||
Combines: BSP (city blocks/buildings) + Noise (terrain/parks) + Voronoi (districts)
|
||||
Creates a city map with districts, buildings, roads, and parks.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Create district map using voronoi
|
||||
districts = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
districts.add_voronoi(num_points=6, coefficients=(1.0, 0.0), seed=42)
|
||||
districts.normalize(0.0, 1.0)
|
||||
|
||||
# District types based on value
|
||||
# 0.0-0.2: Residential (green-ish)
|
||||
# 0.2-0.4: Commercial (blue-ish)
|
||||
# 0.4-0.6: Industrial (gray)
|
||||
# 0.6-0.8: Park/nature
|
||||
# 0.8-1.0: Downtown (tall buildings)
|
||||
|
||||
# Step 2: Create building blocks using BSP
|
||||
bsp = mcrfpy.BSP(pos=(1, 1), size=(GRID_WIDTH - 2, GRID_HEIGHT - 2))
|
||||
bsp.split_recursive(depth=4, min_size=(6, 5), max_ratio=2.0, seed=42)
|
||||
|
||||
# Step 3: Create park areas using noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=99)
|
||||
parks = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
parks.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=3)
|
||||
parks.normalize(0.0, 1.0)
|
||||
|
||||
# Step 4: Render base (roads)
|
||||
color_layer.fill(mcrfpy.Color(60, 60, 65)) # Asphalt
|
||||
|
||||
# Step 5: Draw buildings based on BSP and district type
|
||||
for leaf in bsp.leaves():
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
cx, cy = leaf.center()
|
||||
|
||||
# Get district type at center
|
||||
district_val = districts.get((cx, cy))
|
||||
|
||||
# Shrink for roads between buildings
|
||||
shrink = 1
|
||||
|
||||
# Determine building style based on district
|
||||
if district_val < 0.2:
|
||||
# Residential
|
||||
building_color = mcrfpy.Color(140, 160, 140)
|
||||
roof_color = mcrfpy.Color(160, 100, 80)
|
||||
shrink = 2 # More space between houses
|
||||
elif district_val < 0.4:
|
||||
# Commercial
|
||||
building_color = mcrfpy.Color(120, 140, 170)
|
||||
roof_color = mcrfpy.Color(80, 100, 130)
|
||||
elif district_val < 0.6:
|
||||
# Industrial
|
||||
building_color = mcrfpy.Color(100, 100, 105)
|
||||
roof_color = mcrfpy.Color(70, 70, 75)
|
||||
elif district_val < 0.8:
|
||||
# Park area - check noise for actual park placement
|
||||
park_val = parks.get((cx, cy))
|
||||
if park_val > 0.4:
|
||||
# This block is a park
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
t = parks.get((x, y))
|
||||
if t > 0.6:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(50, 120, 50)) # Trees
|
||||
else:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(80, 150, 80)) # Grass
|
||||
continue
|
||||
else:
|
||||
building_color = mcrfpy.Color(130, 150, 130)
|
||||
roof_color = mcrfpy.Color(100, 80, 70)
|
||||
else:
|
||||
# Downtown
|
||||
building_color = mcrfpy.Color(150, 155, 165)
|
||||
roof_color = mcrfpy.Color(90, 95, 110)
|
||||
shrink = 1 # Dense buildings
|
||||
|
||||
# Draw building
|
||||
for y in range(pos[1] + shrink, pos[1] + size[1] - shrink):
|
||||
for x in range(pos[0] + shrink, pos[0] + size[0] - shrink):
|
||||
# Building edge (roof)
|
||||
if y == pos[1] + shrink or y == pos[1] + size[1] - shrink - 1:
|
||||
color_layer.set(((x, y)), roof_color)
|
||||
elif x == pos[0] + shrink or x == pos[0] + size[0] - shrink - 1:
|
||||
color_layer.set(((x, y)), roof_color)
|
||||
else:
|
||||
color_layer.set(((x, y)), building_color)
|
||||
|
||||
# Step 6: Add main roads (cross the city)
|
||||
road_color = mcrfpy.Color(70, 70, 75)
|
||||
marking_color = mcrfpy.Color(200, 200, 100)
|
||||
|
||||
# Horizontal main road
|
||||
main_y = GRID_HEIGHT // 2
|
||||
for x in range(GRID_WIDTH):
|
||||
for dy in range(-1, 2):
|
||||
if 0 <= main_y + dy < GRID_HEIGHT:
|
||||
color_layer.set(((x, main_y + dy)), road_color)
|
||||
# Road markings
|
||||
if x % 4 == 0:
|
||||
color_layer.set(((x, main_y)), marking_color)
|
||||
|
||||
# Vertical main road
|
||||
main_x = GRID_WIDTH // 2
|
||||
for y in range(GRID_HEIGHT):
|
||||
for dx in range(-1, 2):
|
||||
if 0 <= main_x + dx < GRID_WIDTH:
|
||||
color_layer.set(((main_x + dx, y)), road_color)
|
||||
if y % 4 == 0:
|
||||
color_layer.set(((main_x, y)), marking_color)
|
||||
|
||||
# Intersection
|
||||
for dy in range(-1, 2):
|
||||
for dx in range(-1, 2):
|
||||
color_layer.set(((main_x + dx, main_y + dy)), road_color)
|
||||
|
||||
# Step 7: Add a central plaza
|
||||
plaza_x, plaza_y = main_x, main_y
|
||||
for dy in range(-3, 4):
|
||||
for dx in range(-4, 5):
|
||||
nx, ny = plaza_x + dx, plaza_y + dy
|
||||
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
|
||||
if abs(dx) <= 1 and abs(dy) <= 1:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(180, 160, 140)) # Fountain
|
||||
else:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(160, 150, 140)) # Plaza tiles
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Procedural City: BSP + Voronoi Districts + Noise Parks", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("city")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_32_advanced_city.png")
|
||||
print("Screenshot saved: procgen_32_advanced_city.png")
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
"""Advanced: Natural Cave System
|
||||
|
||||
Combines: Noise (cave formation) + Threshold (open areas) + Kernel (smoothing) + BSP (structured areas)
|
||||
Creates organic cave networks with some structured rooms.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Generate cave base using turbulent noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
cave_noise = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
cave_noise.add_noise(noise, world_size=(10, 8), mode='turbulence', octaves=4)
|
||||
cave_noise.normalize(0.0, 1.0)
|
||||
|
||||
# Step 2: Create cave mask via threshold
|
||||
# Values > 0.45 become open cave, rest is rock
|
||||
cave_mask = cave_noise.threshold_binary((0.4, 1.0), 1.0)
|
||||
|
||||
# Step 3: Apply smoothing kernel to remove isolated pixels
|
||||
smooth_kernel = {
|
||||
(-1, -1): 1, (0, -1): 2, (1, -1): 1,
|
||||
(-1, 0): 2, (0, 0): 4, (1, 0): 2,
|
||||
(-1, 1): 1, (0, 1): 2, (1, 1): 1,
|
||||
}
|
||||
cave_mask.kernel_transform(smooth_kernel)
|
||||
cave_mask.normalize(0.0, 1.0)
|
||||
|
||||
# Re-threshold after smoothing
|
||||
cave_mask = cave_mask.threshold_binary((0.5, 1.0), 1.0)
|
||||
|
||||
# Step 4: Add some structured rooms using BSP in one corner
|
||||
# This represents ancient ruins within the caves
|
||||
bsp = mcrfpy.BSP(pos=(GRID_WIDTH - 22, GRID_HEIGHT - 18), size=(18, 14))
|
||||
bsp.split_recursive(depth=2, min_size=(6, 5), seed=42)
|
||||
|
||||
ruins_hmap = bsp.to_heightmap(
|
||||
size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
select='leaves',
|
||||
shrink=1,
|
||||
value=1.0
|
||||
)
|
||||
|
||||
# Step 5: Combine caves and ruins
|
||||
combined = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
combined.copy_from(cave_mask)
|
||||
combined.max(ruins_hmap)
|
||||
|
||||
# Step 6: Add connecting tunnels from ruins to main cave
|
||||
# Find a cave entrance point
|
||||
tunnel_points = []
|
||||
for y in range(GRID_HEIGHT - 18, GRID_HEIGHT - 10):
|
||||
for x in range(GRID_WIDTH - 25, GRID_WIDTH - 20):
|
||||
if cave_mask.get((x, y)) > 0.5:
|
||||
tunnel_points.append((x, y))
|
||||
break
|
||||
if tunnel_points:
|
||||
break
|
||||
|
||||
if tunnel_points:
|
||||
tx, ty = tunnel_points[0]
|
||||
# Carve tunnel to ruins entrance
|
||||
combined.dig_bezier(
|
||||
points=((tx, ty), (tx + 3, ty), (GRID_WIDTH - 22, ty + 2), (GRID_WIDTH - 20, GRID_HEIGHT - 15)),
|
||||
start_radius=1.5, end_radius=1.5,
|
||||
start_height=1.0, end_height=1.0
|
||||
)
|
||||
|
||||
# Step 7: Add large cavern (central chamber)
|
||||
combined.add_hill((GRID_WIDTH // 3, GRID_HEIGHT // 2), 8, 0.6)
|
||||
|
||||
# Step 8: Create water pools in low noise areas
|
||||
water_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=99)
|
||||
water_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
water_map.add_noise(water_noise, world_size=(15, 12), mode='fbm', octaves=3)
|
||||
water_map.normalize(0.0, 1.0)
|
||||
|
||||
# Step 9: Render
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
cave_val = combined.get((x, y))
|
||||
water_val = water_map.get((x, y))
|
||||
original_noise = cave_noise.get((x, y))
|
||||
|
||||
# Check if in ruins area
|
||||
in_ruins = (x >= GRID_WIDTH - 22 and x < GRID_WIDTH - 4 and
|
||||
y >= GRID_HEIGHT - 18 and y < GRID_HEIGHT - 4)
|
||||
|
||||
if cave_val < 0.3:
|
||||
# Solid rock
|
||||
base = 30 + int(original_noise * 25)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
|
||||
elif cave_val < 0.5:
|
||||
# Cave wall edge
|
||||
color_layer.set(((x, y)), mcrfpy.Color(45, 40, 50))
|
||||
else:
|
||||
# Open cave floor
|
||||
if water_val > 0.7 and not in_ruins:
|
||||
# Water pool
|
||||
t = (water_val - 0.7) / 0.3
|
||||
color_layer.set(((x, y)), mcrfpy.Color(
|
||||
int(30 + t * 20), int(50 + t * 30), int(100 + t * 50)
|
||||
))
|
||||
elif in_ruins and ruins_hmap.get((x, y)) > 0.5:
|
||||
# Ruins floor (worked stone)
|
||||
base = 85 + ((x + y) % 3) * 5
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base))
|
||||
else:
|
||||
# Natural cave floor
|
||||
base = 55 + int(original_noise * 20)
|
||||
var = ((x * 3 + y * 5) % 8)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + var, base - 5 + var, base - 8))
|
||||
|
||||
# Glowing fungi spots
|
||||
fungi_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=777)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if combined.get((x, y)) > 0.5: # Only in open areas
|
||||
fungi_val = fungi_noise.get((x * 0.5, y * 0.5))
|
||||
if fungi_val > 0.8:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(80, 180, 120))
|
||||
|
||||
# Border
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, 0)), mcrfpy.Color(20, 18, 25))
|
||||
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(20, 18, 25))
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((0, y)), mcrfpy.Color(20, 18, 25))
|
||||
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(20, 18, 25))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Cave System: Noise + Threshold + Kernel + BSP Ruins", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("caves")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_33_advanced_caves.png")
|
||||
print("Screenshot saved: procgen_33_advanced_caves.png")
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
"""Advanced: Volcanic Crater Region
|
||||
|
||||
Combines: Hills (mountains) + dig_hill (craters) + Voronoi (lava flows) + Erosion + Noise
|
||||
Creates a volcanic landscape with active lava, ash fields, and rocky terrain.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def volcanic_color(elevation, lava_intensity, ash_level):
|
||||
"""Color based on elevation, lava presence, and ash coverage."""
|
||||
# Lava overrides everything
|
||||
if lava_intensity > 0.6:
|
||||
t = (lava_intensity - 0.6) / 0.4
|
||||
return mcrfpy.Color(
|
||||
int(200 + t * 55),
|
||||
int(80 + t * 80),
|
||||
int(20 + t * 30)
|
||||
)
|
||||
elif lava_intensity > 0.4:
|
||||
# Cooling lava
|
||||
t = (lava_intensity - 0.4) / 0.2
|
||||
return mcrfpy.Color(
|
||||
int(80 + t * 120),
|
||||
int(30 + t * 50),
|
||||
int(20)
|
||||
)
|
||||
|
||||
# Check for crater interior (very low elevation)
|
||||
if elevation < 0.15:
|
||||
t = elevation / 0.15
|
||||
return mcrfpy.Color(int(40 + t * 30), int(20 + t * 20), int(10 + t * 15))
|
||||
|
||||
# Ash coverage
|
||||
if ash_level > 0.6:
|
||||
t = (ash_level - 0.6) / 0.4
|
||||
base = int(60 + t * 40)
|
||||
return mcrfpy.Color(base, base - 5, base - 10)
|
||||
|
||||
# Normal terrain by elevation
|
||||
if elevation < 0.3:
|
||||
# Volcanic plain
|
||||
t = (elevation - 0.15) / 0.15
|
||||
return mcrfpy.Color(int(50 + t * 30), int(40 + t * 25), int(35 + t * 20))
|
||||
elif elevation < 0.5:
|
||||
# Rocky slopes
|
||||
t = (elevation - 0.3) / 0.2
|
||||
return mcrfpy.Color(int(70 + t * 20), int(60 + t * 15), int(50 + t * 15))
|
||||
elif elevation < 0.7:
|
||||
# Mountain sides
|
||||
t = (elevation - 0.5) / 0.2
|
||||
return mcrfpy.Color(int(85 + t * 25), int(75 + t * 20), int(65 + t * 20))
|
||||
elif elevation < 0.85:
|
||||
# High slopes
|
||||
t = (elevation - 0.7) / 0.15
|
||||
return mcrfpy.Color(int(100 + t * 30), int(90 + t * 25), int(80 + t * 25))
|
||||
else:
|
||||
# Peaks
|
||||
t = (elevation - 0.85) / 0.15
|
||||
return mcrfpy.Color(int(130 + t * 50), int(120 + t * 50), int(115 + t * 50))
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Create base terrain with noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
terrain = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.3)
|
||||
terrain.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=4, scale=0.2)
|
||||
|
||||
# Step 2: Add volcanic mountains
|
||||
# Main volcano
|
||||
terrain.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 20, 0.7)
|
||||
terrain.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 12, 0.3) # Steep peak
|
||||
|
||||
# Secondary volcanoes
|
||||
terrain.add_hill((15, 15), 10, 0.4)
|
||||
terrain.add_hill((GRID_WIDTH - 12, GRID_HEIGHT - 15), 8, 0.35)
|
||||
terrain.add_hill((10, GRID_HEIGHT - 10), 6, 0.25)
|
||||
|
||||
# Step 3: Create craters
|
||||
terrain.dig_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 6, 0.1) # Main crater
|
||||
terrain.dig_hill((15, 15), 4, 0.15) # Secondary crater
|
||||
terrain.dig_hill((GRID_WIDTH - 12, GRID_HEIGHT - 15), 3, 0.18) # Third crater
|
||||
|
||||
# Step 4: Create lava flow pattern using voronoi
|
||||
lava = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
lava.add_voronoi(num_points=12, coefficients=(1.0, -0.8), seed=77)
|
||||
lava.normalize(0.0, 1.0)
|
||||
|
||||
# Lava originates from craters - enhance around crater centers
|
||||
lava.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 8, 0.5)
|
||||
lava.add_hill((15, 15), 5, 0.3)
|
||||
|
||||
# Lava flows downhill - multiply by inverted terrain
|
||||
terrain_inv = terrain.inverse()
|
||||
terrain_inv.normalize(0.0, 1.0)
|
||||
lava.multiply(terrain_inv)
|
||||
lava.normalize(0.0, 1.0)
|
||||
|
||||
# Step 5: Create ash distribution using noise
|
||||
ash_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=123)
|
||||
ash = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
ash.add_noise(ash_noise, world_size=(8, 6), mode='turbulence', octaves=3)
|
||||
ash.normalize(0.0, 1.0)
|
||||
|
||||
# Ash settles on lower areas
|
||||
ash.multiply(terrain_inv)
|
||||
|
||||
# Step 6: Apply erosion for realistic channels
|
||||
terrain.rain_erosion(drops=1500, erosion=0.1, sedimentation=0.03, seed=42)
|
||||
terrain.normalize(0.0, 1.0)
|
||||
|
||||
# Step 7: Add lava rivers from craters
|
||||
lava.dig_bezier(
|
||||
points=((GRID_WIDTH // 2, GRID_HEIGHT // 2 + 5),
|
||||
(GRID_WIDTH // 2 - 5, GRID_HEIGHT // 2 + 15),
|
||||
(GRID_WIDTH // 3, GRID_HEIGHT - 10),
|
||||
(10, GRID_HEIGHT - 5)),
|
||||
start_radius=2, end_radius=3,
|
||||
start_height=0.9, end_height=0.7
|
||||
)
|
||||
|
||||
lava.dig_bezier(
|
||||
points=((GRID_WIDTH // 2 + 3, GRID_HEIGHT // 2 + 3),
|
||||
(GRID_WIDTH // 2 + 15, GRID_HEIGHT // 2 + 8),
|
||||
(GRID_WIDTH - 15, GRID_HEIGHT // 2 + 5),
|
||||
(GRID_WIDTH - 5, GRID_HEIGHT // 2 + 10)),
|
||||
start_radius=1.5, end_radius=2.5,
|
||||
start_height=0.85, end_height=0.65
|
||||
)
|
||||
|
||||
# Step 8: Render
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
elev = terrain.get((x, y))
|
||||
lava_val = lava.get((x, y))
|
||||
ash_val = ash.get((x, y))
|
||||
|
||||
color_layer.set(((x, y)), volcanic_color(elev, lava_val, ash_val))
|
||||
|
||||
# Add smoke/steam particles around crater rims
|
||||
crater_centers = [
|
||||
(GRID_WIDTH // 2, GRID_HEIGHT // 2, 6),
|
||||
(15, 15, 4),
|
||||
(GRID_WIDTH - 12, GRID_HEIGHT - 15, 3)
|
||||
]
|
||||
|
||||
import math
|
||||
for cx, cy, radius in crater_centers:
|
||||
for angle in range(0, 360, 30):
|
||||
rad = math.radians(angle)
|
||||
px = int(cx + math.cos(rad) * radius)
|
||||
py = int(cy + math.sin(rad) * radius)
|
||||
if 0 <= px < GRID_WIDTH and 0 <= py < GRID_HEIGHT:
|
||||
# Smoke color
|
||||
color_layer.set(((px, py)), mcrfpy.Color(150, 140, 130, 180))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Volcanic Region: Hills + Craters + Voronoi Lava + Erosion", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("volcanic")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_34_advanced_volcanic.png")
|
||||
print("Screenshot saved: procgen_34_advanced_volcanic.png")
|
||||
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 41 KiB |