Compare commits
12 commits
master
...
alpha_pres
| Author | SHA1 | Date | |
|---|---|---|---|
| c5e7e8e298 | |||
| 6d29652ae7 | |||
| a010e5fa96 | |||
| 9c8d6c4591 | |||
| dcd1b0ca33 | |||
| 6813fb5129 | |||
| 6f67fbb51e | |||
| eb88c7b3aa | |||
| 9fb428dd01 | |||
| bde82028b5 | |||
| 062e4dadc4 | |||
| 98fc49a978 |
|
|
@ -1,123 +0,0 @@
|
||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
build-essential cmake git \
|
|
||||||
zlib1g-dev libx11-dev libxrandr-dev libxcursor-dev \
|
|
||||||
libfreetype-dev libudev-dev libvorbis-dev libflac-dev \
|
|
||||||
libgl-dev libopenal-dev
|
|
||||||
|
|
||||||
- name: Check for pre-built libraries
|
|
||||||
run: |
|
|
||||||
if [ ! -d "__lib" ]; then
|
|
||||||
echo "::error::__lib/ directory not found. Pre-built libraries must be available on the runner."
|
|
||||||
echo "See BUILD_FROM_SOURCE.md for instructions on building dependencies."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build (Release)
|
|
||||||
run: make linux
|
|
||||||
|
|
||||||
- name: Run tests (Release)
|
|
||||||
run: cd tests && python3 run_tests.py -v
|
|
||||||
|
|
||||||
debug-test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
build-essential cmake git \
|
|
||||||
zlib1g-dev libx11-dev libxrandr-dev libxcursor-dev \
|
|
||||||
libfreetype-dev libudev-dev libvorbis-dev libflac-dev \
|
|
||||||
libgl-dev libopenal-dev
|
|
||||||
|
|
||||||
- name: Check for debug libraries
|
|
||||||
run: |
|
|
||||||
if [ ! -d "__lib_debug" ]; then
|
|
||||||
echo "::error::__lib_debug/ directory not found. Build debug Python first: tools/build_debug_python.sh"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build and test (debug Python)
|
|
||||||
run: make debug-test
|
|
||||||
|
|
||||||
asan-test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
build-essential cmake git \
|
|
||||||
zlib1g-dev libx11-dev libxrandr-dev libxcursor-dev \
|
|
||||||
libfreetype-dev libudev-dev libvorbis-dev libflac-dev \
|
|
||||||
libgl-dev libopenal-dev
|
|
||||||
|
|
||||||
- name: Check for debug libraries
|
|
||||||
run: |
|
|
||||||
if [ ! -d "__lib_debug" ]; then
|
|
||||||
echo "::error::__lib_debug/ directory not found. Build debug Python first: tools/build_debug_python.sh"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build and test (ASan + UBSan)
|
|
||||||
run: make asan-test
|
|
||||||
|
|
||||||
valgrind-test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
build-essential cmake git valgrind \
|
|
||||||
zlib1g-dev libx11-dev libxrandr-dev libxcursor-dev \
|
|
||||||
libfreetype-dev libudev-dev libvorbis-dev libflac-dev \
|
|
||||||
libgl-dev libopenal-dev
|
|
||||||
|
|
||||||
- name: Check for debug libraries
|
|
||||||
run: |
|
|
||||||
if [ ! -d "__lib_debug" ]; then
|
|
||||||
echo "::error::__lib_debug/ directory not found. Build debug Python first: tools/build_debug_python.sh"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build and test (Valgrind memcheck)
|
|
||||||
run: make valgrind-test
|
|
||||||
timeout-minutes: 30
|
|
||||||
37
.gitignore
vendored
|
|
@ -7,52 +7,27 @@ PCbuild
|
||||||
.vs
|
.vs
|
||||||
obj
|
obj
|
||||||
build
|
build
|
||||||
/lib
|
lib
|
||||||
|
obj
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
||||||
# 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/
|
.cache/
|
||||||
7DRL2025 Release/
|
7DRL2025 Release/
|
||||||
CMakeFiles/
|
CMakeFiles/
|
||||||
Makefile
|
Makefile
|
||||||
|
*.md
|
||||||
*.zip
|
*.zip
|
||||||
__lib/
|
__lib/
|
||||||
__lib_debug/
|
|
||||||
__lib_windows/
|
|
||||||
build-windows/
|
|
||||||
build_windows/
|
|
||||||
_oldscripts/
|
_oldscripts/
|
||||||
|
|
||||||
# Audit tooling virtualenv (tools/audit_pymethoddef.py)
|
|
||||||
.venv-audit/
|
|
||||||
assets/
|
assets/
|
||||||
cellular_automata_fire/
|
cellular_automata_fire/
|
||||||
|
*.txt
|
||||||
deps/
|
deps/
|
||||||
fetch_issues_txt.py
|
fetch_issues_txt.py
|
||||||
forest_fire_CA.py
|
forest_fire_CA.py
|
||||||
mcrogueface.github.io
|
mcrogueface.github.io
|
||||||
scripts/
|
scripts/
|
||||||
|
test_*
|
||||||
|
|
||||||
tcod_reference
|
tcod_reference
|
||||||
.archive
|
.archive
|
||||||
.mcp.json
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# Keep important documentation and tests
|
|
||||||
!CLAUDE.md
|
|
||||||
!README.md
|
|
||||||
!tests/
|
|
||||||
|
|
||||||
# Fuzzing build artifacts and runtime data (build-fuzz matched by build* above)
|
|
||||||
tests/fuzz/corpora/
|
|
||||||
tests/fuzz/crashes/
|
|
||||||
|
|
|
||||||
13
.gitmodules
vendored
|
|
@ -10,13 +10,6 @@
|
||||||
[submodule "modules/SFML"]
|
[submodule "modules/SFML"]
|
||||||
path = modules/SFML
|
path = modules/SFML
|
||||||
url = git@github.com:SFML/SFML.git
|
url = git@github.com:SFML/SFML.git
|
||||||
[submodule "modules/libtcod-headless"]
|
[submodule "modules/libtcod"]
|
||||||
path = modules/libtcod-headless
|
path = modules/libtcod
|
||||||
url = git@github.com:jmccardle/libtcod-headless.git
|
url = git@github.com:libtcod/libtcod.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
|
|
||||||
|
|
|
||||||
|
|
@ -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/`.
|
|
||||||
582
CMakeLists.txt
|
|
@ -8,516 +8,53 @@ project(McRogueFace)
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
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)
|
|
||||||
|
|
||||||
# Demo mode - self-contained demo game for web showcase
|
|
||||||
option(MCRF_DEMO "Build with demo scripts (web showcase)" 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)
|
|
||||||
|
|
||||||
# Debug/sanitizer build options
|
|
||||||
option(MCRF_SANITIZE_ADDRESS "Build with AddressSanitizer" OFF)
|
|
||||||
option(MCRF_SANITIZE_UNDEFINED "Build with UBSan" OFF)
|
|
||||||
option(MCRF_SANITIZE_THREAD "Build with ThreadSanitizer" OFF)
|
|
||||||
option(MCRF_DEBUG_PYTHON "Link against debug CPython from __lib_debug/" OFF)
|
|
||||||
option(MCRF_FREE_THREADED_PYTHON "Link against free-threaded CPython (python3.14t)" OFF)
|
|
||||||
option(MCRF_WASM_DEBUG "Build WASM with DWARF debug info and source maps" OFF)
|
|
||||||
option(MCRF_FUZZER "Build with libFuzzer coverage instrumentation for atheris" OFF)
|
|
||||||
|
|
||||||
# Validate mutually exclusive sanitizers
|
|
||||||
if(MCRF_SANITIZE_ADDRESS AND MCRF_SANITIZE_THREAD)
|
|
||||||
message(FATAL_ERROR "ASan and TSan are mutually exclusive. Use one or the other.")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Validate debug Python library exists when requested
|
|
||||||
if(MCRF_DEBUG_PYTHON)
|
|
||||||
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/__lib_debug/libpython3.14.so.1.0")
|
|
||||||
message(FATAL_ERROR
|
|
||||||
"__lib_debug/libpython3.14.so.1.0 not found.\n"
|
|
||||||
"Build it first: tools/build_debug_python.sh")
|
|
||||||
endif()
|
|
||||||
message(STATUS "Using debug CPython from __lib_debug/")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# 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
|
# Add include directories
|
||||||
|
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux)
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/deps)
|
include_directories(${CMAKE_SOURCE_DIR}/deps)
|
||||||
include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/deps/libtcod)
|
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux/Python-3.11.1)
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/src)
|
include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
|
||||||
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)
|
|
||||||
|
|
||||||
# Python includes: use different paths for Windows vs Linux vs Emscripten
|
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
|
||||||
if(EMSCRIPTEN)
|
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
|
||||||
# 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()
|
|
||||||
|
|
||||||
# Collect all the source files
|
# Collect all the source files
|
||||||
file(GLOB_RECURSE SOURCES "src/*.cpp")
|
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
|
# Create a list of libraries to link against
|
||||||
if(EMSCRIPTEN)
|
set(LINK_LIBS
|
||||||
# Emscripten build: link against WASM-compiled Python and libtcod
|
sfml-graphics
|
||||||
set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python")
|
sfml-window
|
||||||
set(PYTHON_WASM_PREFIX "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/prefix")
|
sfml-system
|
||||||
set(LIBTCOD_WASM_BUILD "${CMAKE_SOURCE_DIR}/modules/libtcod-headless/build-emscripten")
|
sfml-audio
|
||||||
# Collect HACL crypto object files (not included in libpython3.14.a)
|
tcod)
|
||||||
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
|
|
||||||
if(MCRF_FREE_THREADED_PYTHON)
|
|
||||||
set(PYTHON_LIB python3.14t)
|
|
||||||
elseif(MCRF_DEBUG_PYTHON)
|
|
||||||
set(PYTHON_LIB python3.14d)
|
|
||||||
else()
|
|
||||||
set(PYTHON_LIB python3.14)
|
|
||||||
endif()
|
|
||||||
set(LINK_LIBS
|
|
||||||
tcod
|
|
||||||
${PYTHON_LIB}
|
|
||||||
m dl util pthread)
|
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
|
||||||
if(MCRF_DEBUG_PYTHON OR MCRF_FREE_THREADED_PYTHON)
|
|
||||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_debug)
|
|
||||||
endif()
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# On Windows, add any additional libs and include directories
|
||||||
|
if(WIN32)
|
||||||
|
# Windows-specific Python library name (no dots)
|
||||||
|
list(APPEND LINK_LIBS python312)
|
||||||
|
# Add the necessary Windows-specific libraries and include directories
|
||||||
|
# include_directories(path_to_additional_includes)
|
||||||
|
# link_directories(path_to_additional_libs)
|
||||||
|
# list(APPEND LINK_LIBS additional_windows_libs)
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
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()
|
else()
|
||||||
# Unix/Linux build
|
# Unix/Linux specific libraries
|
||||||
if(MCRF_FREE_THREADED_PYTHON)
|
list(APPEND LINK_LIBS python3.12 m dl util pthread)
|
||||||
set(PYTHON_LIB python3.14t)
|
|
||||||
elseif(MCRF_DEBUG_PYTHON)
|
|
||||||
set(PYTHON_LIB python3.14d)
|
|
||||||
else()
|
|
||||||
set(PYTHON_LIB python3.14)
|
|
||||||
endif()
|
|
||||||
set(LINK_LIBS
|
|
||||||
sfml-graphics
|
|
||||||
sfml-window
|
|
||||||
sfml-system
|
|
||||||
sfml-audio
|
|
||||||
tcod
|
|
||||||
${PYTHON_LIB}
|
|
||||||
m dl util pthread
|
|
||||||
${OPENGL_LIBRARIES})
|
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||||
if(MCRF_DEBUG_PYTHON OR MCRF_FREE_THREADED_PYTHON)
|
|
||||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_debug)
|
|
||||||
endif()
|
|
||||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
|
||||||
endif()
|
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
|
# Define the executable target before linking libraries
|
||||||
add_executable(mcrogueface ${SOURCES})
|
add_executable(mcrogueface ${SOURCES})
|
||||||
|
|
||||||
# Define NO_SDL for libtcod-headless headers (excludes SDL-dependent code)
|
# On Windows, set subsystem to WINDOWS to hide console
|
||||||
# We ALWAYS need this because libtcod headers expect SDL3, not SDL2
|
if(WIN32)
|
||||||
# Our SDL2 backend is separate from libtcod's SDL3 renderer
|
set_target_properties(mcrogueface PROPERTIES
|
||||||
target_compile_definitions(mcrogueface PRIVATE NO_SDL)
|
WIN32_EXECUTABLE TRUE
|
||||||
|
LINK_FLAGS "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup")
|
||||||
# Sanitizer instrumentation — applied to mcrogueface target only (not imported libs)
|
|
||||||
if(MCRF_SANITIZE_ADDRESS)
|
|
||||||
message(STATUS "AddressSanitizer enabled")
|
|
||||||
target_compile_options(mcrogueface PRIVATE
|
|
||||||
-fsanitize=address -fno-omit-frame-pointer -g -O1)
|
|
||||||
target_link_options(mcrogueface PRIVATE
|
|
||||||
-fsanitize=address)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(MCRF_SANITIZE_UNDEFINED)
|
|
||||||
message(STATUS "UndefinedBehaviorSanitizer enabled")
|
|
||||||
# -fno-sanitize=function is Clang-only; -fno-sanitize=vptr avoids CPython false positives
|
|
||||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
|
||||||
set(UBSAN_EXCLUSIONS -fno-sanitize=function,vptr)
|
|
||||||
else()
|
|
||||||
set(UBSAN_EXCLUSIONS -fno-sanitize=vptr)
|
|
||||||
endif()
|
|
||||||
target_compile_options(mcrogueface PRIVATE
|
|
||||||
-fsanitize=undefined ${UBSAN_EXCLUSIONS} -g -O1)
|
|
||||||
target_link_options(mcrogueface PRIVATE
|
|
||||||
-fsanitize=undefined ${UBSAN_EXCLUSIONS})
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(MCRF_SANITIZE_THREAD)
|
|
||||||
message(STATUS "ThreadSanitizer enabled")
|
|
||||||
target_compile_options(mcrogueface PRIVATE
|
|
||||||
-fsanitize=thread -g -O1)
|
|
||||||
target_link_options(mcrogueface PRIVATE
|
|
||||||
-fsanitize=thread)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(MCRF_FUZZER)
|
|
||||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
|
||||||
message(FATAL_ERROR "MCRF_FUZZER=ON requires Clang. Invoke with CC=clang-18 CXX=clang++-18.")
|
|
||||||
endif()
|
|
||||||
message(STATUS "Building mcrfpy_fuzz harness (libFuzzer + ASan + UBSan)")
|
|
||||||
|
|
||||||
set(MCRF_FUZZ_SOURCES ${SOURCES})
|
|
||||||
list(REMOVE_ITEM MCRF_FUZZ_SOURCES ${CMAKE_SOURCE_DIR}/src/main.cpp)
|
|
||||||
list(APPEND MCRF_FUZZ_SOURCES ${CMAKE_SOURCE_DIR}/tests/fuzz/fuzz_common.cpp)
|
|
||||||
|
|
||||||
add_executable(mcrfpy_fuzz ${MCRF_FUZZ_SOURCES})
|
|
||||||
target_compile_definitions(mcrfpy_fuzz PRIVATE NO_SDL MCRF_FUZZ_HARNESS)
|
|
||||||
if(MCRF_DEBUG_PYTHON OR MCRF_FREE_THREADED_PYTHON)
|
|
||||||
target_compile_definitions(mcrfpy_fuzz PRIVATE Py_DEBUG)
|
|
||||||
endif()
|
|
||||||
if(MCRF_FREE_THREADED_PYTHON)
|
|
||||||
target_compile_definitions(mcrfpy_fuzz PRIVATE Py_GIL_DISABLED)
|
|
||||||
endif()
|
|
||||||
if(MCRF_HEADLESS)
|
|
||||||
target_compile_definitions(mcrfpy_fuzz PRIVATE MCRF_HEADLESS)
|
|
||||||
endif()
|
|
||||||
if(MCRF_SDL2)
|
|
||||||
target_compile_definitions(mcrfpy_fuzz PRIVATE MCRF_SDL2)
|
|
||||||
endif()
|
|
||||||
target_include_directories(mcrfpy_fuzz PRIVATE
|
|
||||||
${CMAKE_SOURCE_DIR}/src
|
|
||||||
${CMAKE_SOURCE_DIR}/tests/fuzz)
|
|
||||||
target_compile_options(mcrfpy_fuzz PRIVATE
|
|
||||||
-fsanitize=fuzzer-no-link,address,undefined
|
|
||||||
-fno-sanitize=function,vptr
|
|
||||||
-fno-omit-frame-pointer -g -O1)
|
|
||||||
target_link_options(mcrfpy_fuzz PRIVATE
|
|
||||||
-fsanitize=fuzzer,address,undefined
|
|
||||||
-fno-sanitize=function,vptr)
|
|
||||||
target_link_libraries(mcrfpy_fuzz ${LINK_LIBS})
|
|
||||||
|
|
||||||
# Copy Python runtime + assets next to mcrfpy_fuzz so the embedded
|
|
||||||
# interpreter finds the stdlib and default_font/default_texture load.
|
|
||||||
add_custom_command(TARGET mcrfpy_fuzz POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
|
||||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrfpy_fuzz>/lib
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
|
||||||
${CMAKE_SOURCE_DIR}/assets $<TARGET_FILE_DIR:mcrfpy_fuzz>/assets)
|
|
||||||
if(MCRF_DEBUG_PYTHON)
|
|
||||||
add_custom_command(TARGET mcrfpy_fuzz POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy
|
|
||||||
${CMAKE_SOURCE_DIR}/__lib_debug/libpython3.14.so.1.0
|
|
||||||
$<TARGET_FILE_DIR:mcrfpy_fuzz>/lib/libpython3.14.so.1.0
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy
|
|
||||||
${CMAKE_SOURCE_DIR}/__lib_debug/libpython3.14.so.1.0
|
|
||||||
$<TARGET_FILE_DIR:mcrfpy_fuzz>/lib/libpython3.14d.so.1.0
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E create_symlink
|
|
||||||
libpython3.14d.so.1.0
|
|
||||||
$<TARGET_FILE_DIR:mcrfpy_fuzz>/lib/libpython3.14d.so)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Enable Py_DEBUG when linking against debug CPython (matches pydebug ABI)
|
|
||||||
if(MCRF_DEBUG_PYTHON OR MCRF_FREE_THREADED_PYTHON)
|
|
||||||
target_compile_definitions(mcrogueface PRIVATE Py_DEBUG)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Enable Py_GIL_DISABLED for free-threaded CPython (no-GIL build)
|
|
||||||
if(MCRF_FREE_THREADED_PYTHON)
|
|
||||||
target_compile_definitions(mcrogueface PRIVATE Py_GIL_DISABLED)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# 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")
|
|
||||||
set(MCRF_SCRIPTS_DEMO_DIR "${CMAKE_SOURCE_DIR}/src/scripts_demo" CACHE PATH "Demo scripts for WASM showcase")
|
|
||||||
|
|
||||||
# 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 (playground, demo, or full game)
|
|
||||||
--preload-file=$<IF:$<BOOL:${MCRF_PLAYGROUND}>,${MCRF_SCRIPTS_PLAYGROUND_DIR},$<IF:$<BOOL:${MCRF_DEMO}>,${MCRF_SCRIPTS_DEMO_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()
|
|
||||||
|
|
||||||
# WASM debug builds: DWARF symbols, source maps, symbol map for stack traces
|
|
||||||
if(MCRF_WASM_DEBUG)
|
|
||||||
list(APPEND EMSCRIPTEN_LINK_OPTIONS
|
|
||||||
-g4
|
|
||||||
-gsource-map
|
|
||||||
--emit-symbol-map
|
|
||||||
)
|
|
||||||
target_compile_options(mcrogueface PRIVATE -g4)
|
|
||||||
message(STATUS "Emscripten debug enabled: DWARF (-g4), source maps, symbol map")
|
|
||||||
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()
|
endif()
|
||||||
|
|
||||||
# Now the linker will find the libraries in the specified directory
|
# Now the linker will find the libraries in the specified directory
|
||||||
|
|
@ -534,58 +71,25 @@ add_custom_command(TARGET mcrogueface POST_BUILD
|
||||||
${CMAKE_SOURCE_DIR}/src/scripts $<TARGET_FILE_DIR:mcrogueface>/scripts)
|
${CMAKE_SOURCE_DIR}/src/scripts $<TARGET_FILE_DIR:mcrogueface>/scripts)
|
||||||
|
|
||||||
# Copy Python standard library to build directory
|
# Copy Python standard library to build directory
|
||||||
if(MCRF_DEBUG_PYTHON)
|
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||||
# Copy all libs first (SFML, libtcod, Python stdlib), then overwrite with debug Python
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
# The debug lib has SONAME libpython3.14d.so.1.0, so we need both names
|
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib)
|
||||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
|
||||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy
|
|
||||||
${CMAKE_SOURCE_DIR}/__lib_debug/libpython3.14.so.1.0
|
|
||||||
$<TARGET_FILE_DIR:mcrogueface>/lib/libpython3.14.so.1.0
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy
|
|
||||||
${CMAKE_SOURCE_DIR}/__lib_debug/libpython3.14.so.1.0
|
|
||||||
$<TARGET_FILE_DIR:mcrogueface>/lib/libpython3.14d.so.1.0
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E create_symlink
|
|
||||||
libpython3.14d.so.1.0
|
|
||||||
$<TARGET_FILE_DIR:mcrogueface>/lib/libpython3.14d.so)
|
|
||||||
else()
|
|
||||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
|
||||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# On Windows, copy DLLs to executable directory
|
# On Windows, copy DLLs to executable directory
|
||||||
if(MCRF_CROSS_WINDOWS)
|
if(WIN32)
|
||||||
# Cross-compilation: copy DLLs from __lib_windows
|
# Copy all DLL files from lib to the executable directory
|
||||||
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
|
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>
|
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>
|
||||||
COMMAND ${CMAKE_COMMAND} -E echo "Copied DLLs to executable directory")
|
COMMAND ${CMAKE_COMMAND} -E echo "Copied DLLs to executable directory")
|
||||||
|
|
||||||
|
# Alternative: Copy specific DLLs if you want more control
|
||||||
|
# file(GLOB DLLS "${CMAKE_SOURCE_DIR}/__lib/*.dll")
|
||||||
|
# foreach(DLL ${DLLS})
|
||||||
|
# add_custom_command(TARGET mcrogueface POST_BUILD
|
||||||
|
# COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
# ${DLL} $<TARGET_FILE_DIR:mcrogueface>)
|
||||||
|
# endforeach()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# rpath for including shared libraries (Linux/Unix only)
|
# rpath for including shared libraries (Linux/Unix only)
|
||||||
|
|
|
||||||
54
GNUmakefile
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Convenience Makefile wrapper for McRogueFace
|
||||||
|
# This delegates to CMake build in the build directory
|
||||||
|
|
||||||
|
.PHONY: all build clean run test dist help
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
all: build
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
build:
|
||||||
|
@./build.sh
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
clean:
|
||||||
|
@./clean.sh
|
||||||
|
|
||||||
|
# Run the game
|
||||||
|
run: build
|
||||||
|
@cd build && ./mcrogueface
|
||||||
|
|
||||||
|
# Run in Python mode
|
||||||
|
python: build
|
||||||
|
@cd build && ./mcrogueface -i
|
||||||
|
|
||||||
|
# Test basic functionality
|
||||||
|
test: build
|
||||||
|
@echo "Testing McRogueFace..."
|
||||||
|
@cd build && ./mcrogueface -V
|
||||||
|
@cd build && ./mcrogueface -c "print('Test passed')"
|
||||||
|
@cd build && ./mcrogueface --headless -c "import mcrfpy; print('mcrfpy imported successfully')"
|
||||||
|
|
||||||
|
# Create distribution archive
|
||||||
|
dist: build
|
||||||
|
@echo "Creating distribution archive..."
|
||||||
|
@cd build && zip -r ../McRogueFace-$$(date +%Y%m%d).zip . -x "*.o" "CMakeFiles/*" "Makefile" "*.cmake"
|
||||||
|
@echo "Distribution archive created: McRogueFace-$$(date +%Y%m%d).zip"
|
||||||
|
|
||||||
|
# Show help
|
||||||
|
help:
|
||||||
|
@echo "McRogueFace Build System"
|
||||||
|
@echo "======================="
|
||||||
|
@echo ""
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " make - Build the project (default)"
|
||||||
|
@echo " make build - Build the project"
|
||||||
|
@echo " make clean - Remove all build artifacts"
|
||||||
|
@echo " make run - Build and run the game"
|
||||||
|
@echo " make python - Build and run in Python interactive mode"
|
||||||
|
@echo " make test - Run basic tests"
|
||||||
|
@echo " make dist - Create distribution archive"
|
||||||
|
@echo " make help - Show this help message"
|
||||||
|
@echo ""
|
||||||
|
@echo "Build output goes to: ./build/"
|
||||||
|
@echo "Distribution archives are created in project root"
|
||||||
461
Makefile
|
|
@ -1,461 +0,0 @@
|
||||||
# McRogueFace Build Makefile
|
|
||||||
# Usage:
|
|
||||||
# make - Build for Linux (default)
|
|
||||||
# make windows - Cross-compile for Windows using MinGW (release)
|
|
||||||
# make windows-debug - Cross-compile for Windows with console & debug symbols
|
|
||||||
# make clean - Clean Linux build
|
|
||||||
# make clean-windows - Clean Windows build
|
|
||||||
# make run - Run the Linux build
|
|
||||||
#
|
|
||||||
# WebAssembly / Emscripten:
|
|
||||||
# make wasm - Build full game for web (requires emsdk activated)
|
|
||||||
# make wasm-game - Build game for web with fullscreen canvas (no REPL)
|
|
||||||
# make playground - Build minimal playground for web REPL
|
|
||||||
# make serve - Serve wasm build locally on port 8080
|
|
||||||
# make serve-game - Serve wasm-game build locally on port 8080
|
|
||||||
# make clean-wasm - Clean Emscripten builds
|
|
||||||
#
|
|
||||||
# Packaging:
|
|
||||||
# make package-windows-light - Windows with minimal stdlib (~5 MB)
|
|
||||||
# make package-windows-full - Windows with full stdlib (~15 MB)
|
|
||||||
# make package-linux-light - Linux with minimal stdlib
|
|
||||||
# make package-linux-full - Linux with full stdlib
|
|
||||||
# make package-all - All platform/preset combinations
|
|
||||||
#
|
|
||||||
# Release:
|
|
||||||
# make version-bump NEXT_VERSION=x.y.z-suffix
|
|
||||||
# Tags HEAD with current version, builds all packages, bumps to NEXT_VERSION
|
|
||||||
|
|
||||||
.PHONY: all linux windows windows-debug clean clean-windows clean-dist run
|
|
||||||
.PHONY: wasm wasm-game wasm-debug playground playground-debug serve serve-game serve-playground clean-wasm
|
|
||||||
.PHONY: package-windows-light package-windows-full package-linux-light package-linux-full package-all
|
|
||||||
.PHONY: version-bump
|
|
||||||
.PHONY: debug debug-test asan asan-test tsan tsan-test valgrind-test massif-test analyze clean-debug
|
|
||||||
|
|
||||||
# Number of parallel jobs for compilation
|
|
||||||
JOBS := $(shell nproc 2>/dev/null || echo 4)
|
|
||||||
|
|
||||||
all: linux
|
|
||||||
|
|
||||||
linux:
|
|
||||||
@echo "Building McRogueFace for Linux..."
|
|
||||||
@mkdir -p build
|
|
||||||
@cd build && cmake .. -DCMAKE_BUILD_TYPE=Release && make -j$(JOBS)
|
|
||||||
@echo "Build complete! Run with: ./build/mcrogueface"
|
|
||||||
|
|
||||||
windows:
|
|
||||||
@echo "Cross-compiling McRogueFace for Windows..."
|
|
||||||
@mkdir -p build-windows
|
|
||||||
@cd build-windows && cmake .. \
|
|
||||||
-DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
|
|
||||||
-DCMAKE_BUILD_TYPE=Release && make -j$(JOBS)
|
|
||||||
@echo "Windows build complete! Output: build-windows/mcrogueface.exe"
|
|
||||||
|
|
||||||
windows-debug:
|
|
||||||
@echo "Cross-compiling McRogueFace for Windows (debug with console)..."
|
|
||||||
@mkdir -p build-windows-debug
|
|
||||||
@cd build-windows-debug && cmake .. \
|
|
||||||
-DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
|
|
||||||
-DCMAKE_BUILD_TYPE=Debug \
|
|
||||||
-DMCRF_WINDOWS_CONSOLE=ON && make -j$(JOBS)
|
|
||||||
@echo "Windows debug build complete! Output: build-windows-debug/mcrogueface.exe"
|
|
||||||
@echo "Run from cmd.exe to see console output"
|
|
||||||
|
|
||||||
clean:
|
|
||||||
@echo "Cleaning Linux build..."
|
|
||||||
@rm -rf build
|
|
||||||
|
|
||||||
clean-windows:
|
|
||||||
@echo "Cleaning Windows builds..."
|
|
||||||
@rm -rf build-windows build-windows-debug
|
|
||||||
|
|
||||||
clean-dist:
|
|
||||||
@echo "Cleaning distribution packages..."
|
|
||||||
@rm -rf dist
|
|
||||||
|
|
||||||
clean-all: clean clean-windows clean-wasm clean-debug clean-dist
|
|
||||||
@echo "All builds and packages cleaned."
|
|
||||||
|
|
||||||
run: linux
|
|
||||||
@cd build && ./mcrogueface
|
|
||||||
|
|
||||||
# Debug and sanitizer targets
|
|
||||||
debug:
|
|
||||||
@echo "Building McRogueFace with debug Python (pydebug assertions)..."
|
|
||||||
@mkdir -p build-debug
|
|
||||||
@cd build-debug && cmake .. \
|
|
||||||
-DCMAKE_BUILD_TYPE=Debug \
|
|
||||||
-DMCRF_DEBUG_PYTHON=ON && make -j$(JOBS)
|
|
||||||
@echo "Debug build complete! Output: build-debug/mcrogueface"
|
|
||||||
|
|
||||||
debug-test: debug
|
|
||||||
@echo "Running test suite with debug Python..."
|
|
||||||
cd tests && MCRF_BUILD_DIR=../build-debug \
|
|
||||||
MCRF_LIB_DIR=../__lib_debug \
|
|
||||||
python3 run_tests.py -v
|
|
||||||
|
|
||||||
asan:
|
|
||||||
@echo "Building McRogueFace with ASan + UBSan..."
|
|
||||||
@mkdir -p build-asan
|
|
||||||
@cd build-asan && cmake .. \
|
|
||||||
-DCMAKE_BUILD_TYPE=Debug \
|
|
||||||
-DMCRF_DEBUG_PYTHON=ON \
|
|
||||||
-DMCRF_SANITIZE_ADDRESS=ON \
|
|
||||||
-DMCRF_SANITIZE_UNDEFINED=ON && make -j$(JOBS)
|
|
||||||
@echo "ASan build complete! Output: build-asan/mcrogueface"
|
|
||||||
|
|
||||||
asan-test: asan
|
|
||||||
@echo "Running test suite under ASan + UBSan..."
|
|
||||||
cd tests && MCRF_BUILD_DIR=../build-asan \
|
|
||||||
MCRF_LIB_DIR=../__lib_debug \
|
|
||||||
PYTHONMALLOC=malloc \
|
|
||||||
ASAN_OPTIONS="detect_leaks=1:halt_on_error=1:print_summary=1" \
|
|
||||||
LSAN_OPTIONS="suppressions=$(CURDIR)/sanitizers/asan.supp" \
|
|
||||||
UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1" \
|
|
||||||
python3 run_tests.py -v --sanitizer
|
|
||||||
|
|
||||||
# Fuzzing targets (clang-18 + libFuzzer + ASan + UBSan).
|
|
||||||
# Design: ONE instrumented executable `mcrfpy_fuzz` that embeds CPython,
|
|
||||||
# registers the mcrfpy module, and dispatches each libFuzzer iteration to
|
|
||||||
# a Python `fuzz_one_input(data)` function loaded from the script named by
|
|
||||||
# the MCRF_FUZZ_TARGET env var. libFuzzer instruments the C++ engine code
|
|
||||||
# where all the #258-#278 bugs live. No atheris dependency.
|
|
||||||
FUZZ_TARGETS := grid_entity property_types anim_timer_scene maps_procgen fov pathfinding_behavior audio_dsp import_parsers texture_factory shader_bindings
|
|
||||||
FUZZ_SECONDS ?= 30
|
|
||||||
|
|
||||||
# Shared env for running the fuzz binary. PYTHONHOME points at the build-fuzz
|
|
||||||
# copy of the bundled stdlib (post-build copied into build-fuzz/lib/).
|
|
||||||
# ASAN_OPTIONS: leak detection disabled because libFuzzer intentionally holds
|
|
||||||
# inputs for its corpus; abort_on_error ensures crashes are loud and repro-able.
|
|
||||||
define FUZZ_ENV
|
|
||||||
MCRF_LIB_DIR=../__lib_debug \
|
|
||||||
PYTHONMALLOC=malloc \
|
|
||||||
PYTHONHOME=../__lib/Python \
|
|
||||||
ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1:print_stacktrace=1" \
|
|
||||||
UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1"
|
|
||||||
endef
|
|
||||||
|
|
||||||
fuzz-build:
|
|
||||||
@echo "Building mcrfpy_fuzz with libFuzzer + ASan (clang-18)..."
|
|
||||||
@mkdir -p build-fuzz
|
|
||||||
@cd build-fuzz && CC=clang-18 CXX=clang++-18 cmake .. \
|
|
||||||
-DCMAKE_BUILD_TYPE=Debug \
|
|
||||||
-DMCRF_DEBUG_PYTHON=ON \
|
|
||||||
-DMCRF_SANITIZE_ADDRESS=ON \
|
|
||||||
-DMCRF_SANITIZE_UNDEFINED=ON \
|
|
||||||
-DMCRF_FUZZER=ON \
|
|
||||||
-DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld && make -j$(JOBS) mcrfpy_fuzz
|
|
||||||
@echo "Fuzz build complete! Output: build-fuzz/mcrfpy_fuzz"
|
|
||||||
|
|
||||||
fuzz: fuzz-build
|
|
||||||
@for t in $(FUZZ_TARGETS); do \
|
|
||||||
if [ ! -f tests/fuzz/fuzz_$$t.py ]; then \
|
|
||||||
echo "SKIP: tests/fuzz/fuzz_$$t.py does not exist yet"; \
|
|
||||||
continue; \
|
|
||||||
fi; \
|
|
||||||
echo "=== fuzzing $$t for $(FUZZ_SECONDS)s ==="; \
|
|
||||||
mkdir -p tests/fuzz/corpora/$$t tests/fuzz/crashes; \
|
|
||||||
( cd build-fuzz && $(FUZZ_ENV) MCRF_FUZZ_TARGET=$$t \
|
|
||||||
./mcrfpy_fuzz \
|
|
||||||
-max_total_time=$(FUZZ_SECONDS) \
|
|
||||||
-artifact_prefix=../tests/fuzz/crashes/$$t- \
|
|
||||||
../tests/fuzz/corpora/$$t ../tests/fuzz/seeds/$$t ) || exit 1; \
|
|
||||||
done
|
|
||||||
|
|
||||||
fuzz-long: fuzz-build
|
|
||||||
@test -n "$(TARGET)" || (echo "Usage: make fuzz-long TARGET=<name> SECONDS=<n>"; exit 1)
|
|
||||||
@test -f tests/fuzz/fuzz_$(TARGET).py || (echo "No target: tests/fuzz/fuzz_$(TARGET).py"; exit 1)
|
|
||||||
@mkdir -p tests/fuzz/corpora/$(TARGET) tests/fuzz/crashes
|
|
||||||
@( cd build-fuzz && $(FUZZ_ENV) MCRF_FUZZ_TARGET=$(TARGET) \
|
|
||||||
./mcrfpy_fuzz \
|
|
||||||
-max_total_time=$(or $(SECONDS),3600) \
|
|
||||||
-artifact_prefix=../tests/fuzz/crashes/$(TARGET)- \
|
|
||||||
../tests/fuzz/corpora/$(TARGET) ../tests/fuzz/seeds/$(TARGET) )
|
|
||||||
|
|
||||||
fuzz-repro:
|
|
||||||
@test -n "$(TARGET)" || (echo "Usage: make fuzz-repro TARGET=<name> CRASH=<path>"; exit 1)
|
|
||||||
@test -n "$(CRASH)" || (echo "Usage: make fuzz-repro TARGET=<name> CRASH=<path>"; exit 1)
|
|
||||||
@( cd build-fuzz && $(FUZZ_ENV) MCRF_FUZZ_TARGET=$(TARGET) \
|
|
||||||
./mcrfpy_fuzz ../$(CRASH) )
|
|
||||||
|
|
||||||
clean-fuzz:
|
|
||||||
@echo "Cleaning fuzz build and corpora..."
|
|
||||||
@rm -rf build-fuzz tests/fuzz/corpora tests/fuzz/crashes
|
|
||||||
|
|
||||||
tsan:
|
|
||||||
@echo "Building McRogueFace with TSan + free-threaded Python..."
|
|
||||||
@echo "NOTE: Requires free-threaded debug Python built with:"
|
|
||||||
@echo " tools/build_debug_python.sh --tsan"
|
|
||||||
@mkdir -p build-tsan
|
|
||||||
@cd build-tsan && cmake .. \
|
|
||||||
-DCMAKE_BUILD_TYPE=Debug \
|
|
||||||
-DMCRF_FREE_THREADED_PYTHON=ON \
|
|
||||||
-DMCRF_SANITIZE_THREAD=ON && make -j$(JOBS)
|
|
||||||
@echo "TSan build complete! Output: build-tsan/mcrogueface"
|
|
||||||
|
|
||||||
tsan-test: tsan
|
|
||||||
@echo "Running test suite under TSan..."
|
|
||||||
cd tests && MCRF_BUILD_DIR=../build-tsan \
|
|
||||||
MCRF_LIB_DIR=../__lib_debug \
|
|
||||||
TSAN_OPTIONS="halt_on_error=1:second_deadlock_stack=1" \
|
|
||||||
python3 run_tests.py -v --sanitizer
|
|
||||||
|
|
||||||
valgrind-test: debug
|
|
||||||
@echo "Running test suite under Valgrind memcheck..."
|
|
||||||
cd tests && MCRF_BUILD_DIR=../build-debug \
|
|
||||||
MCRF_LIB_DIR=../__lib_debug \
|
|
||||||
MCRF_TIMEOUT_MULTIPLIER=50 \
|
|
||||||
PYTHONMALLOC=malloc \
|
|
||||||
python3 run_tests.py -v --valgrind
|
|
||||||
|
|
||||||
massif-test: debug
|
|
||||||
@echo "Running heap profiling under Valgrind Massif..."
|
|
||||||
@mkdir -p build-debug
|
|
||||||
cd build-debug && valgrind --tool=massif \
|
|
||||||
--massif-out-file=massif.out \
|
|
||||||
--pages-as-heap=no \
|
|
||||||
--detailed-freq=10 \
|
|
||||||
--max-snapshots=100 \
|
|
||||||
./mcrogueface --headless --exec ../tests/benchmarks/stress_test_suite.py
|
|
||||||
@echo "Massif output: build-debug/massif.out"
|
|
||||||
@echo "View with: ms_print build-debug/massif.out"
|
|
||||||
|
|
||||||
analyze:
|
|
||||||
@echo "Running cppcheck static analysis..."
|
|
||||||
cppcheck --enable=warning,performance,portability \
|
|
||||||
--suppress=missingIncludeSystem \
|
|
||||||
--suppress=unusedFunction \
|
|
||||||
--suppress=noExplicitConstructor \
|
|
||||||
--suppress=missingOverride \
|
|
||||||
--inline-suppr \
|
|
||||||
-I src/ -I deps/ -I deps/cpython -I deps/Python \
|
|
||||||
-I src/platform -I src/3d -I src/tiled -I src/ldtk -I src/audio \
|
|
||||||
--std=c++20 \
|
|
||||||
--quiet \
|
|
||||||
src/ 2>&1
|
|
||||||
@echo "Static analysis complete."
|
|
||||||
|
|
||||||
clean-debug:
|
|
||||||
@echo "Cleaning debug/sanitizer builds..."
|
|
||||||
@rm -rf build-debug build-asan build-tsan build-fuzz
|
|
||||||
|
|
||||||
# Packaging targets using tools/package.sh
|
|
||||||
package-windows-light: windows
|
|
||||||
@./tools/package.sh windows light
|
|
||||||
|
|
||||||
package-windows-full: windows
|
|
||||||
@./tools/package.sh windows full
|
|
||||||
|
|
||||||
package-linux-light: linux
|
|
||||||
@./tools/package.sh linux light
|
|
||||||
|
|
||||||
package-linux-full: linux
|
|
||||||
@./tools/package.sh linux full
|
|
||||||
|
|
||||||
package-all: windows linux
|
|
||||||
@./tools/package.sh all
|
|
||||||
|
|
||||||
# Legacy target for backwards compatibility
|
|
||||||
package-windows: package-windows-full
|
|
||||||
|
|
||||||
# Emscripten / WebAssembly targets
|
|
||||||
# Requires: source ~/emsdk/emsdk_env.sh (or wherever your emsdk is installed)
|
|
||||||
#
|
|
||||||
# For iterative development, configure once then rebuild:
|
|
||||||
# source ~/emsdk/emsdk_env.sh && emmake make -C build-emscripten
|
|
||||||
#
|
|
||||||
wasm:
|
|
||||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
|
||||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@if [ ! -f build-emscripten/Makefile ]; then \
|
|
||||||
echo "Configuring WebAssembly build (full game)..."; \
|
|
||||||
mkdir -p build-emscripten; \
|
|
||||||
cd build-emscripten && emcmake cmake .. \
|
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
|
||||||
-DMCRF_SDL2=ON; \
|
|
||||||
fi
|
|
||||||
@echo "Building McRogueFace for WebAssembly..."
|
|
||||||
@emmake make -C build-emscripten -j$(JOBS)
|
|
||||||
@echo "WebAssembly build complete! Files in build-emscripten/"
|
|
||||||
@echo "Run 'make serve' to test locally"
|
|
||||||
|
|
||||||
playground:
|
|
||||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
|
||||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@if [ ! -f build-playground/Makefile ]; then \
|
|
||||||
echo "Configuring Playground build..."; \
|
|
||||||
mkdir -p build-playground; \
|
|
||||||
cd build-playground && emcmake cmake .. \
|
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
|
||||||
-DMCRF_SDL2=ON \
|
|
||||||
-DMCRF_PLAYGROUND=ON; \
|
|
||||||
fi
|
|
||||||
@echo "Building McRogueFace Playground for WebAssembly..."
|
|
||||||
@emmake make -C build-playground -j$(JOBS)
|
|
||||||
@echo "Playground build complete! Files in build-playground/"
|
|
||||||
@echo "Run 'make serve-playground' to test locally"
|
|
||||||
|
|
||||||
serve:
|
|
||||||
@echo "Serving WebAssembly build at http://localhost:8080"
|
|
||||||
@echo "Press Ctrl+C to stop"
|
|
||||||
@cd build-emscripten && python3 -m http.server 8080
|
|
||||||
|
|
||||||
serve-playground:
|
|
||||||
@echo "Serving Playground build at http://localhost:8080"
|
|
||||||
@echo "Press Ctrl+C to stop"
|
|
||||||
@cd build-playground && python3 -m http.server 8080
|
|
||||||
|
|
||||||
wasm-game:
|
|
||||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
|
||||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@if [ ! -f build-wasm-game/Makefile ]; then \
|
|
||||||
echo "Configuring WebAssembly game build (fullscreen, no REPL)..."; \
|
|
||||||
mkdir -p build-wasm-game; \
|
|
||||||
cd build-wasm-game && emcmake cmake .. \
|
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
|
||||||
-DMCRF_SDL2=ON \
|
|
||||||
-DMCRF_GAME_SHELL=ON; \
|
|
||||||
fi
|
|
||||||
@echo "Building McRogueFace game for WebAssembly..."
|
|
||||||
@emmake make -C build-wasm-game -j$(JOBS)
|
|
||||||
@echo "Game build complete! Files in build-wasm-game/"
|
|
||||||
@echo "Run 'make serve-game' to test locally"
|
|
||||||
|
|
||||||
serve-game:
|
|
||||||
@echo "Serving game build at http://localhost:8080"
|
|
||||||
@echo "Press Ctrl+C to stop"
|
|
||||||
@cd build-wasm-game && python3 -m http.server 8080
|
|
||||||
|
|
||||||
wasm-demo:
|
|
||||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
|
||||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@if [ ! -f build-wasm-demo/Makefile ]; then \
|
|
||||||
echo "Configuring WebAssembly demo build..."; \
|
|
||||||
mkdir -p build-wasm-demo; \
|
|
||||||
cd build-wasm-demo && emcmake cmake .. \
|
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
|
||||||
-DMCRF_SDL2=ON \
|
|
||||||
-DMCRF_DEMO=ON \
|
|
||||||
-DMCRF_GAME_SHELL=ON; \
|
|
||||||
fi
|
|
||||||
@echo "Building McRogueFace demo for WebAssembly..."
|
|
||||||
@emmake make -C build-wasm-demo -j$(JOBS)
|
|
||||||
@cp web/index.html build-wasm-demo/index.html
|
|
||||||
@echo "Demo build complete! Files in build-wasm-demo/"
|
|
||||||
@echo "Run 'make serve-demo' to test locally"
|
|
||||||
|
|
||||||
serve-demo:
|
|
||||||
@echo "Serving demo build at http://localhost:8080"
|
|
||||||
@echo "Press Ctrl+C to stop"
|
|
||||||
@cd build-wasm-demo && python3 -m http.server 8080
|
|
||||||
|
|
||||||
clean-wasm:
|
|
||||||
@echo "Cleaning Emscripten builds..."
|
|
||||||
@rm -rf build-emscripten build-playground build-wasm-game build-wasm-demo build-wasm-debug build-playground-debug
|
|
||||||
|
|
||||||
wasm-debug:
|
|
||||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
|
||||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@if [ ! -f build-wasm-debug/Makefile ]; then \
|
|
||||||
echo "Configuring WebAssembly debug build (DWARF + source maps)..."; \
|
|
||||||
mkdir -p build-wasm-debug; \
|
|
||||||
cd build-wasm-debug && emcmake cmake .. \
|
|
||||||
-DCMAKE_BUILD_TYPE=Debug \
|
|
||||||
-DMCRF_SDL2=ON \
|
|
||||||
-DMCRF_WASM_DEBUG=ON; \
|
|
||||||
fi
|
|
||||||
@echo "Building McRogueFace for WebAssembly (debug)..."
|
|
||||||
@emmake make -C build-wasm-debug -j$(JOBS)
|
|
||||||
@echo "Debug WASM build complete! Files in build-wasm-debug/"
|
|
||||||
@echo "Debug artifacts: .wasm.map (source map), .symbols (symbol map)"
|
|
||||||
@echo "Run 'make serve-wasm-debug' to test locally"
|
|
||||||
|
|
||||||
serve-wasm-debug:
|
|
||||||
@echo "Serving debug WASM build at http://localhost:8080"
|
|
||||||
@echo "Press Ctrl+C to stop"
|
|
||||||
@cd build-wasm-debug && python3 -m http.server 8080
|
|
||||||
|
|
||||||
playground-debug:
|
|
||||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
|
||||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@if [ ! -f build-playground-debug/Makefile ]; then \
|
|
||||||
echo "Configuring Playground debug build (DWARF + source maps)..."; \
|
|
||||||
mkdir -p build-playground-debug; \
|
|
||||||
cd build-playground-debug && emcmake cmake .. \
|
|
||||||
-DCMAKE_BUILD_TYPE=Debug \
|
|
||||||
-DMCRF_SDL2=ON \
|
|
||||||
-DMCRF_PLAYGROUND=ON \
|
|
||||||
-DMCRF_WASM_DEBUG=ON; \
|
|
||||||
fi
|
|
||||||
@echo "Building McRogueFace Playground for WebAssembly (debug)..."
|
|
||||||
@emmake make -C build-playground-debug -j$(JOBS)
|
|
||||||
@echo "Playground debug build complete! Files in build-playground-debug/"
|
|
||||||
@echo "Run 'make serve-playground-debug' to test locally"
|
|
||||||
|
|
||||||
serve-playground-debug:
|
|
||||||
@echo "Serving debug Playground build at http://localhost:8080"
|
|
||||||
@echo "Press Ctrl+C to stop"
|
|
||||||
@cd build-playground-debug && python3 -m http.server 8080
|
|
||||||
|
|
||||||
# Current version extracted from source
|
|
||||||
CURRENT_VERSION := $(shell grep 'MCRFPY_VERSION' src/McRogueFaceVersion.h | sed 's/.*"\(.*\)"/\1/')
|
|
||||||
|
|
||||||
# Release workflow: tag current version, build all packages, bump to next version
|
|
||||||
# Usage: make version-bump NEXT_VERSION=0.2.6-prerelease-7drl2026
|
|
||||||
version-bump:
|
|
||||||
ifndef NEXT_VERSION
|
|
||||||
$(error Usage: make version-bump NEXT_VERSION=x.y.z-suffix)
|
|
||||||
endif
|
|
||||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
|
||||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
# git status (clean working dir check), but ignore modules/, because building submodules dirties their subdirs
|
|
||||||
@if [ -n "$$(git status --porcelain | grep -v modules)" ]; then \
|
|
||||||
echo "Error: Working tree is not clean. Commit or stash changes first."; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@echo "=== Releasing $(CURRENT_VERSION) ==="
|
|
||||||
@# Idempotent tag: ok if it already points at HEAD (resuming partial run)
|
|
||||||
@if git rev-parse "$(CURRENT_VERSION)" >/dev/null 2>&1; then \
|
|
||||||
TAG_COMMIT=$$(git rev-parse "$(CURRENT_VERSION)^{}"); \
|
|
||||||
HEAD_COMMIT=$$(git rev-parse HEAD); \
|
|
||||||
if [ "$$TAG_COMMIT" != "$$HEAD_COMMIT" ]; then \
|
|
||||||
echo "Error: Tag $(CURRENT_VERSION) already exists but points to a different commit."; \
|
|
||||||
exit 1; \
|
|
||||||
fi; \
|
|
||||||
echo "Tag $(CURRENT_VERSION) already exists at HEAD (resuming)."; \
|
|
||||||
else \
|
|
||||||
git tag "$(CURRENT_VERSION)"; \
|
|
||||||
fi
|
|
||||||
$(MAKE) package-linux-full
|
|
||||||
$(MAKE) package-windows-full
|
|
||||||
$(MAKE) wasm
|
|
||||||
@echo "Packaging WASM build..."
|
|
||||||
@mkdir -p dist
|
|
||||||
cd build-emscripten && zip -r ../dist/McRogueFace-$(CURRENT_VERSION)-WASM.zip \
|
|
||||||
mcrogueface.html mcrogueface.js mcrogueface.wasm mcrogueface.data
|
|
||||||
@echo ""
|
|
||||||
@echo "Bumping version: $(CURRENT_VERSION) -> $(NEXT_VERSION)"
|
|
||||||
@sed -i 's|MCRFPY_VERSION "$(CURRENT_VERSION)"|MCRFPY_VERSION "$(NEXT_VERSION)"|' src/McRogueFaceVersion.h
|
|
||||||
@TAGGED_HASH=$$(git rev-parse --short HEAD); \
|
|
||||||
git add src/McRogueFaceVersion.h && \
|
|
||||||
git commit -m "Version bump: $(CURRENT_VERSION) ($$TAGGED_HASH) -> $(NEXT_VERSION)"
|
|
||||||
@echo ""
|
|
||||||
@echo "=== Release $(CURRENT_VERSION) complete ==="
|
|
||||||
@echo "Tag: $(CURRENT_VERSION)"
|
|
||||||
@echo "Next: $(NEXT_VERSION)"
|
|
||||||
@echo "Packages:"
|
|
||||||
@ls -lh dist/*$(CURRENT_VERSION)* 2>/dev/null
|
|
||||||
211
README.md
|
|
@ -8,185 +8,94 @@ A Python-powered 2D game engine for creating roguelike games, built with C++ and
|
||||||
* Simple GUI element system allows keyboard and mouse input, composition
|
* 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"
|
* No compilation or installation necessary. The runtime is a full Python environment; "Zip And Ship"
|
||||||
|
|
||||||
📖 **[Full Documentation & Tutorials](https://mcrogueface.github.io/)** - Quickstart guide, API reference, and cookbook
|
![ Image ]()
|
||||||
|
|
||||||
|
**Pre-Alpha Release Demo**: my 7DRL 2025 entry *"Crypt of Sokoban"* - a prototype with buttons, boulders, enemies, and items.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
**Download** the [latest release](https://github.com/jmccardle/McRogueFace/releases/latest):
|
**Download**:
|
||||||
- **Windows**: `McRogueFace-*-Win.zip`
|
|
||||||
- **Linux**: `McRogueFace-*-Linux.tar.bz2`
|
|
||||||
|
|
||||||
Extract and run `mcrogueface` (or `mcrogueface.exe` on Windows) to see the demo game.
|
- The entire McRogueFace visual framework:
|
||||||
|
- **Sprite**: an image file or one sprite from a shared sprite sheet
|
||||||
|
- **Caption**: load a font, display text
|
||||||
|
- **Frame**: A rectangle; put other things on it to move or manage GUIs as modules
|
||||||
|
- **Grid**: A 2D array of tiles with zoom + position control
|
||||||
|
- **Entity**: Lives on a Grid, displays a sprite, and can have a perspective or move along a path
|
||||||
|
- **Animation**: Change any property on any of the above over time
|
||||||
|
|
||||||
### Your First Game
|
```bash
|
||||||
|
# Clone and build
|
||||||
|
git clone <wherever you found this repo>
|
||||||
|
cd McRogueFace
|
||||||
|
make
|
||||||
|
|
||||||
Create `scripts/game.py` (or edit the existing one):
|
# Run the example game
|
||||||
|
cd build
|
||||||
|
./mcrogueface
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Creating a Simple Scene
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import mcrfpy
|
import mcrfpy
|
||||||
|
|
||||||
# Create and activate a scene
|
# Create a new scene
|
||||||
scene = mcrfpy.Scene("game")
|
mcrfpy.createScene("intro")
|
||||||
scene.activate()
|
|
||||||
|
|
||||||
# Load a sprite sheet
|
# Add a text caption
|
||||||
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!")
|
||||||
|
caption.size = 48
|
||||||
|
caption.fill_color = (255, 255, 255)
|
||||||
|
|
||||||
# Create a tile grid
|
# Add to scene
|
||||||
grid = mcrfpy.Grid(grid_size=(20, 15), texture=texture, pos=(50, 50), size=(640, 480))
|
mcrfpy.sceneUI("intro").append(caption)
|
||||||
grid.zoom = 2.0
|
|
||||||
scene.children.append(grid)
|
|
||||||
|
|
||||||
# Add a player entity
|
# Switch to the scene
|
||||||
player = mcrfpy.Entity(pos=(10, 7), texture=texture, sprite_index=84)
|
mcrfpy.setScene("intro")
|
||||||
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
|
## Documentation
|
||||||
|
|
||||||
### 📚 Developer Documentation
|
### 📚 Full Documentation Site
|
||||||
|
|
||||||
For comprehensive documentation about systems, architecture, and development workflows:
|
For comprehensive documentation, tutorials, and API reference, visit:
|
||||||
|
**[https://mcrogueface.github.io](https://mcrogueface.github.io)**
|
||||||
|
|
||||||
**[Project Wiki](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki)**
|
The documentation site includes:
|
||||||
|
|
||||||
Key wiki pages:
|
- **[Quickstart Guide](https://mcrogueface.github.io/quickstart/)** - Get running in 5 minutes
|
||||||
|
- **[McRogueFace Does The Entire Roguelike Tutorial](https://mcrogueface.github.io/tutorials/)** - Step-by-step game building
|
||||||
- **[Home](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Home)** - Documentation hub with multiple entry points
|
- **[Complete API Reference](https://mcrogueface.github.io/api/)** - Every function documented
|
||||||
- **[Grid System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Grid-System)** - Three-layer grid architecture
|
- **[Cookbook](https://mcrogueface.github.io/cookbook/)** - Ready-to-use code recipes
|
||||||
- **[Python Binding System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Python-Binding-System)** - C++/Python integration
|
- **[C++ Extension Guide](https://mcrogueface.github.io/extending-cpp/)** - For C++ developers: Add engine features
|
||||||
- **[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
|
## Build Requirements
|
||||||
|
|
||||||
- C++17 compiler (GCC 7+ or Clang 5+)
|
- C++17 compiler (GCC 7+ or Clang 5+)
|
||||||
- CMake 3.14+
|
- CMake 3.14+
|
||||||
- Python 3.14 (embedded)
|
- Python 3.12+
|
||||||
- SFML 2.6
|
- SFML 2.6
|
||||||
- Linux or Windows (macOS untested)
|
- Linux or Windows (macOS untested)
|
||||||
|
|
||||||
See [BUILD_FROM_SOURCE.md](BUILD_FROM_SOURCE.md) for detailed compilation instructions.
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
McRogueFace/
|
McRogueFace/
|
||||||
├── assets/ # Sprites, fonts, audio
|
├── assets/ # Sprites, fonts, audio
|
||||||
├── build/ # Build output: this is what you distribute
|
├── build/ # Build output directory: zip + ship
|
||||||
│ ├── assets/ # (copied from assets/)
|
│ ├─ (*)assets/ # (copied location of assets)
|
||||||
│ ├── scripts/ # (copied from src/scripts/)
|
│ ├─ (*)scripts/ # (copied location of src/scripts)
|
||||||
│ └── lib/ # Python stdlib and extension modules
|
│ └─ lib/ # SFML, TCOD libraries, Python + standard library / modules
|
||||||
├── docs/ # Generated HTML, markdown API docs
|
├── deps/ # Python, SFML, and libtcod imports can be tossed in here to build
|
||||||
|
│ └─ platform/ # windows, linux subdirectories for OS-specific cpython config
|
||||||
|
├── docs/ # generated HTML, markdown docs
|
||||||
|
│ └─ stubs/ # .pyi files for editor integration
|
||||||
|
├── modules/ # git submodules, to build all of McRogueFace's dependencies from source
|
||||||
├── src/ # C++ engine source
|
├── src/ # C++ engine source
|
||||||
│ └── scripts/ # Python game scripts
|
│ └─ scripts/ # Python game scripts (copied during build)
|
||||||
├── stubs/ # .pyi type stubs for IDE integration
|
└── tests/ # Automated test suite
|
||||||
├── tests/ # Automated test suite
|
└── tools/ # For the McRogueFace ecosystem: docs generation
|
||||||
└── 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 building McRogueFace to implement game logic or scene configuration in C++, you'll have to compile the project.
|
||||||
|
|
@ -205,15 +114,7 @@ If you are writing a game in Python using McRogueFace, you only need to rename a
|
||||||
|
|
||||||
PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request.
|
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 has a private roadmap and issue list. Reach out via email or social media if you have bugs or feature requests.
|
||||||
|
|
||||||
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
|
## License
|
||||||
|
|
||||||
|
|
@ -221,6 +122,6 @@ This project is licensed under the MIT License - see LICENSE file for details.
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
- Developed for 7-Day Roguelike 2023, 2024, 2025, 2026 - here's to many more
|
- Developed for 7-Day Roguelike 2023, 2024, 2025 - here's to many more
|
||||||
- Built with [SFML](https://www.sfml-dev.org/), [libtcod](https://github.com/libtcod/libtcod), and Python
|
- 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
|
- Inspired by David Churchill's COMP4300 game engine lectures
|
||||||
|
|
|
||||||
997
ROADMAP.md
BIN
assets/48px_ui_icons-KenneyNL.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
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/kenney_TD_MR_IP.png
Normal file
|
After Width: | Height: | Size: 674 KiB |
BIN
assets/sfx/splat1.ogg
Normal file
BIN
assets/sfx/splat2.ogg
Normal file
BIN
assets/sfx/splat3.ogg
Normal file
BIN
assets/sfx/splat4.ogg
Normal file
BIN
assets/sfx/splat5.ogg
Normal file
BIN
assets/sfx/splat6.ogg
Normal file
BIN
assets/sfx/splat7.ogg
Normal file
BIN
assets/sfx/splat8.ogg
Normal file
BIN
assets/sfx/splat9.ogg
Normal file
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 |
|
|
@ -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")
|
|
||||||
54
deps/platform/linux/platform.h
vendored
|
|
@ -1,54 +1,6 @@
|
||||||
#ifndef __PLATFORM
|
#ifndef __PLATFORM
|
||||||
#define __PLATFORM
|
#define __PLATFORM
|
||||||
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
|
#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()
|
std::wstring executable_path()
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
|
|
@ -60,7 +12,7 @@ std::wstring executable_path()
|
||||||
return exec_path.wstring();
|
return exec_path.wstring();
|
||||||
//size_t path_index = exec_path.find_last_of('/');
|
//size_t path_index = exec_path.find_last_of('/');
|
||||||
//return exec_path.substr(0, path_index);
|
//return exec_path.substr(0, path_index);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring executable_filename()
|
std::wstring executable_filename()
|
||||||
|
|
@ -85,6 +37,4 @@ std::string narrow_string(std::wstring convertme)
|
||||||
return converter.to_bytes(convertme);
|
return converter.to_bytes(convertme);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // __EMSCRIPTEN__
|
#endif
|
||||||
|
|
||||||
#endif // __PLATFORM
|
|
||||||
|
|
|
||||||
8
deps/platform/windows/platform.h
vendored
|
|
@ -1,12 +1,12 @@
|
||||||
#ifndef __PLATFORM
|
#ifndef __PLATFORM
|
||||||
#define __PLATFORM
|
#define __PLATFORM
|
||||||
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
|
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 0
|
||||||
#include <windows.h>
|
#include <Windows.h>
|
||||||
|
|
||||||
std::wstring executable_path()
|
std::wstring executable_path()
|
||||||
{
|
{
|
||||||
wchar_t buffer[MAX_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;
|
std::wstring exec_path = buffer;
|
||||||
size_t path_index = exec_path.find_last_of(L"\\/");
|
size_t path_index = exec_path.find_last_of(L"\\/");
|
||||||
return exec_path.substr(0, path_index);
|
return exec_path.substr(0, path_index);
|
||||||
|
|
@ -15,7 +15,7 @@ std::wstring executable_path()
|
||||||
std::wstring executable_filename()
|
std::wstring executable_filename()
|
||||||
{
|
{
|
||||||
wchar_t buffer[MAX_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;
|
std::wstring exec_path = buffer;
|
||||||
return exec_path;
|
return exec_path;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
# McRogueFace API Reference
|
# McRogueFace API Reference
|
||||||
|
|
||||||
*Generated on 2025-07-15 21:28:42*
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
McRogueFace Python API
|
McRogueFace Python API
|
||||||
|
|
@ -375,6 +373,14 @@ A rectangular frame UI element that can contain other drawable elements.
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
|
#### `get_bounds()`
|
||||||
|
|
||||||
|
Get the bounding rectangle of this drawable element.
|
||||||
|
|
||||||
|
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||||
|
|
||||||
|
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||||
|
|
||||||
#### `resize(width, height)`
|
#### `resize(width, height)`
|
||||||
|
|
||||||
Resize the element to new dimensions.
|
Resize the element to new dimensions.
|
||||||
|
|
@ -395,14 +401,6 @@ Move the element by a relative offset.
|
||||||
|
|
||||||
**Note:** This modifies the x and y position properties by the given amounts.
|
**Note:** This modifies the x and y position properties by the given amounts.
|
||||||
|
|
||||||
#### `get_bounds()`
|
|
||||||
|
|
||||||
Get the bounding rectangle of this drawable element.
|
|
||||||
|
|
||||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
|
||||||
|
|
||||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### class `Caption`
|
### class `Caption`
|
||||||
|
|
@ -411,6 +409,14 @@ A text display UI element with customizable font and styling.
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
|
#### `get_bounds()`
|
||||||
|
|
||||||
|
Get the bounding rectangle of this drawable element.
|
||||||
|
|
||||||
|
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||||
|
|
||||||
|
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||||
|
|
||||||
#### `resize(width, height)`
|
#### `resize(width, height)`
|
||||||
|
|
||||||
Resize the element to new dimensions.
|
Resize the element to new dimensions.
|
||||||
|
|
@ -431,14 +437,6 @@ Move the element by a relative offset.
|
||||||
|
|
||||||
**Note:** This modifies the x and y position properties by the given amounts.
|
**Note:** This modifies the x and y position properties by the given amounts.
|
||||||
|
|
||||||
#### `get_bounds()`
|
|
||||||
|
|
||||||
Get the bounding rectangle of this drawable element.
|
|
||||||
|
|
||||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
|
||||||
|
|
||||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### class `Sprite`
|
### class `Sprite`
|
||||||
|
|
@ -447,6 +445,14 @@ A sprite UI element that displays a texture or portion of a texture atlas.
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
|
#### `get_bounds()`
|
||||||
|
|
||||||
|
Get the bounding rectangle of this drawable element.
|
||||||
|
|
||||||
|
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||||
|
|
||||||
|
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||||
|
|
||||||
#### `resize(width, height)`
|
#### `resize(width, height)`
|
||||||
|
|
||||||
Resize the element to new dimensions.
|
Resize the element to new dimensions.
|
||||||
|
|
@ -467,14 +473,6 @@ Move the element by a relative offset.
|
||||||
|
|
||||||
**Note:** This modifies the x and y position properties by the given amounts.
|
**Note:** This modifies the x and y position properties by the given amounts.
|
||||||
|
|
||||||
#### `get_bounds()`
|
|
||||||
|
|
||||||
Get the bounding rectangle of this drawable element.
|
|
||||||
|
|
||||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
|
||||||
|
|
||||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### class `Grid`
|
### class `Grid`
|
||||||
|
|
@ -483,16 +481,6 @@ A grid-based tilemap UI element for rendering tile-based levels and game worlds.
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
#### `resize(width, height)`
|
|
||||||
|
|
||||||
Resize the element to new dimensions.
|
|
||||||
|
|
||||||
**Arguments:**
|
|
||||||
- `width` (*float*): New width in pixels
|
|
||||||
- `height` (*float*): New height in pixels
|
|
||||||
|
|
||||||
**Note:** For Caption and Sprite, this may not change actual size if determined by content.
|
|
||||||
|
|
||||||
#### `at(x, y)`
|
#### `at(x, y)`
|
||||||
|
|
||||||
Get the GridPoint at the specified grid coordinates.
|
Get the GridPoint at the specified grid coordinates.
|
||||||
|
|
@ -503,6 +491,24 @@ Get the GridPoint at the specified grid coordinates.
|
||||||
|
|
||||||
**Returns:** GridPoint or None: The grid point at (x, y), or None if out of bounds
|
**Returns:** GridPoint or None: The grid point at (x, y), or None if out of bounds
|
||||||
|
|
||||||
|
#### `get_bounds()`
|
||||||
|
|
||||||
|
Get the bounding rectangle of this drawable element.
|
||||||
|
|
||||||
|
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||||
|
|
||||||
|
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||||
|
|
||||||
|
#### `resize(width, height)`
|
||||||
|
|
||||||
|
Resize the element to new dimensions.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `width` (*float*): New width in pixels
|
||||||
|
- `height` (*float*): New height in pixels
|
||||||
|
|
||||||
|
**Note:** For Caption and Sprite, this may not change actual size if determined by content.
|
||||||
|
|
||||||
#### `move(dx, dy)`
|
#### `move(dx, dy)`
|
||||||
|
|
||||||
Move the element by a relative offset.
|
Move the element by a relative offset.
|
||||||
|
|
@ -513,14 +519,6 @@ Move the element by a relative offset.
|
||||||
|
|
||||||
**Note:** This modifies the x and y position properties by the given amounts.
|
**Note:** This modifies the x and y position properties by the given amounts.
|
||||||
|
|
||||||
#### `get_bounds()`
|
|
||||||
|
|
||||||
Get the bounding rectangle of this drawable element.
|
|
||||||
|
|
||||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
|
||||||
|
|
||||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### class `Entity`
|
### class `Entity`
|
||||||
|
|
@ -529,6 +527,12 @@ Game entity that can be placed in a Grid.
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
|
#### `die()`
|
||||||
|
|
||||||
|
Remove this entity from its parent grid.
|
||||||
|
|
||||||
|
**Note:** The entity object remains valid but is no longer rendered or updated.
|
||||||
|
|
||||||
#### `move(dx, dy)`
|
#### `move(dx, dy)`
|
||||||
|
|
||||||
Move the element by a relative offset.
|
Move the element by a relative offset.
|
||||||
|
|
@ -557,11 +561,11 @@ Get the bounding rectangle of this drawable element.
|
||||||
|
|
||||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||||
|
|
||||||
#### `die()`
|
#### `index()`
|
||||||
|
|
||||||
Remove this entity from its parent grid.
|
Get the index of this entity in its parent grid's entity list.
|
||||||
|
|
||||||
**Note:** The entity object remains valid but is no longer rendered or updated.
|
**Returns:** int: Index position, or -1 if not in a grid
|
||||||
|
|
||||||
#### `resize(width, height)`
|
#### `resize(width, height)`
|
||||||
|
|
||||||
|
|
@ -573,12 +577,6 @@ Resize the element to new dimensions.
|
||||||
|
|
||||||
**Note:** For Caption and Sprite, this may not change actual size if determined by content.
|
**Note:** For Caption and Sprite, this may not change actual size if determined by content.
|
||||||
|
|
||||||
#### `index()`
|
|
||||||
|
|
||||||
Get the index of this entity in its parent grid's entity list.
|
|
||||||
|
|
||||||
**Returns:** int: Index position, or -1 if not in a grid
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Collections
|
### Collections
|
||||||
|
|
@ -589,6 +587,13 @@ Container for Entity objects in a Grid. Supports iteration and indexing.
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
|
#### `append(entity)`
|
||||||
|
|
||||||
|
Add an entity to the end of the collection.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `entity` (*Entity*): The entity to add
|
||||||
|
|
||||||
#### `remove(entity)`
|
#### `remove(entity)`
|
||||||
|
|
||||||
Remove the first occurrence of an entity from the collection.
|
Remove the first occurrence of an entity from the collection.
|
||||||
|
|
@ -598,13 +603,6 @@ Remove the first occurrence of an entity from the collection.
|
||||||
|
|
||||||
**Raises:** ValueError: If entity is not in collection
|
**Raises:** ValueError: If entity is not in collection
|
||||||
|
|
||||||
#### `extend(iterable)`
|
|
||||||
|
|
||||||
Add all entities from an iterable to the collection.
|
|
||||||
|
|
||||||
**Arguments:**
|
|
||||||
- `iterable` (*Iterable[Entity]*): Entities to add
|
|
||||||
|
|
||||||
#### `count(entity)`
|
#### `count(entity)`
|
||||||
|
|
||||||
Count the number of occurrences of an entity in the collection.
|
Count the number of occurrences of an entity in the collection.
|
||||||
|
|
@ -625,12 +623,12 @@ Find the index of the first occurrence of an entity.
|
||||||
|
|
||||||
**Raises:** ValueError: If entity is not in collection
|
**Raises:** ValueError: If entity is not in collection
|
||||||
|
|
||||||
#### `append(entity)`
|
#### `extend(iterable)`
|
||||||
|
|
||||||
Add an entity to the end of the collection.
|
Add all entities from an iterable to the collection.
|
||||||
|
|
||||||
**Arguments:**
|
**Arguments:**
|
||||||
- `entity` (*Entity*): The entity to add
|
- `iterable` (*Iterable[Entity]*): Entities to add
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -640,6 +638,13 @@ Container for UI drawable elements. Supports iteration and indexing.
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
|
#### `append(drawable)`
|
||||||
|
|
||||||
|
Add a drawable element to the end of the collection.
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `drawable` (*UIDrawable*): The drawable element to add
|
||||||
|
|
||||||
#### `remove(drawable)`
|
#### `remove(drawable)`
|
||||||
|
|
||||||
Remove the first occurrence of a drawable from the collection.
|
Remove the first occurrence of a drawable from the collection.
|
||||||
|
|
@ -649,13 +654,6 @@ Remove the first occurrence of a drawable from the collection.
|
||||||
|
|
||||||
**Raises:** ValueError: If drawable is not in collection
|
**Raises:** ValueError: If drawable is not in collection
|
||||||
|
|
||||||
#### `extend(iterable)`
|
|
||||||
|
|
||||||
Add all drawables from an iterable to the collection.
|
|
||||||
|
|
||||||
**Arguments:**
|
|
||||||
- `iterable` (*Iterable[UIDrawable]*): Drawables to add
|
|
||||||
|
|
||||||
#### `count(drawable)`
|
#### `count(drawable)`
|
||||||
|
|
||||||
Count the number of occurrences of a drawable in the collection.
|
Count the number of occurrences of a drawable in the collection.
|
||||||
|
|
@ -676,12 +674,12 @@ Find the index of the first occurrence of a drawable.
|
||||||
|
|
||||||
**Raises:** ValueError: If drawable is not in collection
|
**Raises:** ValueError: If drawable is not in collection
|
||||||
|
|
||||||
#### `append(drawable)`
|
#### `extend(iterable)`
|
||||||
|
|
||||||
Add a drawable element to the end of the collection.
|
Add all drawables from an iterable to the collection.
|
||||||
|
|
||||||
**Arguments:**
|
**Arguments:**
|
||||||
- `drawable` (*UIDrawable*): The drawable element to add
|
- `iterable` (*Iterable[UIDrawable]*): Drawables to add
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -705,17 +703,6 @@ RGBA color representation.
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
#### `to_hex()`
|
|
||||||
|
|
||||||
Convert this Color to a hexadecimal string.
|
|
||||||
|
|
||||||
**Returns:** str: Hex color string in format "#RRGGBB"
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```python
|
|
||||||
hex_str = color.to_hex() # Returns "#FF0000"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `from_hex(hex_string)`
|
#### `from_hex(hex_string)`
|
||||||
|
|
||||||
Create a Color from a hexadecimal color string.
|
Create a Color from a hexadecimal color string.
|
||||||
|
|
@ -730,6 +717,17 @@ Create a Color from a hexadecimal color string.
|
||||||
red = Color.from_hex("#FF0000")
|
red = Color.from_hex("#FF0000")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `to_hex()`
|
||||||
|
|
||||||
|
Convert this Color to a hexadecimal string.
|
||||||
|
|
||||||
|
**Returns:** str: Hex color string in format "#RRGGBB"
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
hex_str = color.to_hex() # Returns "#FF0000"
|
||||||
|
```
|
||||||
|
|
||||||
#### `lerp(other, t)`
|
#### `lerp(other, t)`
|
||||||
|
|
||||||
Linearly interpolate between this color and another.
|
Linearly interpolate between this color and another.
|
||||||
|
|
@ -759,23 +757,6 @@ Calculate the length/magnitude of this vector.
|
||||||
|
|
||||||
**Returns:** float: The magnitude of the vector
|
**Returns:** float: The magnitude of the vector
|
||||||
|
|
||||||
#### `normalize()`
|
|
||||||
|
|
||||||
Return a unit vector in the same direction.
|
|
||||||
|
|
||||||
**Returns:** Vector: New normalized vector with magnitude 1.0
|
|
||||||
|
|
||||||
**Raises:** ValueError: If vector has zero magnitude
|
|
||||||
|
|
||||||
#### `dot(other)`
|
|
||||||
|
|
||||||
Calculate the dot product with another vector.
|
|
||||||
|
|
||||||
**Arguments:**
|
|
||||||
- `other` (*Vector*): The other vector
|
|
||||||
|
|
||||||
**Returns:** float: Dot product of the two vectors
|
|
||||||
|
|
||||||
#### `distance_to(other)`
|
#### `distance_to(other)`
|
||||||
|
|
||||||
Calculate the distance to another vector.
|
Calculate the distance to another vector.
|
||||||
|
|
@ -785,11 +766,14 @@ Calculate the distance to another vector.
|
||||||
|
|
||||||
**Returns:** float: Distance between the two vectors
|
**Returns:** float: Distance between the two vectors
|
||||||
|
|
||||||
#### `copy()`
|
#### `dot(other)`
|
||||||
|
|
||||||
Create a copy of this vector.
|
Calculate the dot product with another vector.
|
||||||
|
|
||||||
**Returns:** Vector: New Vector object with same x and y values
|
**Arguments:**
|
||||||
|
- `other` (*Vector*): The other vector
|
||||||
|
|
||||||
|
**Returns:** float: Dot product of the two vectors
|
||||||
|
|
||||||
#### `angle()`
|
#### `angle()`
|
||||||
|
|
||||||
|
|
@ -805,6 +789,20 @@ Calculate the squared magnitude of this vector.
|
||||||
|
|
||||||
**Note:** Use this for comparisons to avoid expensive square root calculation.
|
**Note:** Use this for comparisons to avoid expensive square root calculation.
|
||||||
|
|
||||||
|
#### `copy()`
|
||||||
|
|
||||||
|
Create a copy of this vector.
|
||||||
|
|
||||||
|
**Returns:** Vector: New Vector object with same x and y values
|
||||||
|
|
||||||
|
#### `normalize()`
|
||||||
|
|
||||||
|
Return a unit vector in the same direction.
|
||||||
|
|
||||||
|
**Returns:** Vector: New normalized vector with magnitude 1.0
|
||||||
|
|
||||||
|
**Raises:** ValueError: If vector has zero magnitude
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### class `Texture`
|
### class `Texture`
|
||||||
|
|
@ -836,12 +834,6 @@ Animate UI element properties over time.
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
#### `get_current_value()`
|
|
||||||
|
|
||||||
Get the current interpolated value of the animation.
|
|
||||||
|
|
||||||
**Returns:** float: Current animation value between start and end
|
|
||||||
|
|
||||||
#### `update(delta_time)`
|
#### `update(delta_time)`
|
||||||
|
|
||||||
Update the animation by the given time delta.
|
Update the animation by the given time delta.
|
||||||
|
|
@ -860,6 +852,12 @@ Start the animation on a target UI element.
|
||||||
|
|
||||||
**Note:** The target must have the property specified in the animation constructor.
|
**Note:** The target must have the property specified in the animation constructor.
|
||||||
|
|
||||||
|
#### `get_current_value()`
|
||||||
|
|
||||||
|
Get the current interpolated value of the animation.
|
||||||
|
|
||||||
|
**Returns:** float: Current animation value between start and end
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### class `Drawable`
|
### class `Drawable`
|
||||||
|
|
@ -868,6 +866,14 @@ Base class for all drawable UI elements.
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
|
#### `get_bounds()`
|
||||||
|
|
||||||
|
Get the bounding rectangle of this drawable element.
|
||||||
|
|
||||||
|
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
||||||
|
|
||||||
|
**Note:** The bounds are in screen coordinates and account for current position and size.
|
||||||
|
|
||||||
#### `resize(width, height)`
|
#### `resize(width, height)`
|
||||||
|
|
||||||
Resize the element to new dimensions.
|
Resize the element to new dimensions.
|
||||||
|
|
@ -888,14 +894,6 @@ Move the element by a relative offset.
|
||||||
|
|
||||||
**Note:** This modifies the x and y position properties by the given amounts.
|
**Note:** This modifies the x and y position properties by the given amounts.
|
||||||
|
|
||||||
#### `get_bounds()`
|
|
||||||
|
|
||||||
Get the bounding rectangle of this drawable element.
|
|
||||||
|
|
||||||
**Returns:** tuple: (x, y, width, height) representing the element's bounds
|
|
||||||
|
|
||||||
**Note:** The bounds are in screen coordinates and account for current position and size.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### class `GridPoint`
|
### class `GridPoint`
|
||||||
|
|
@ -947,18 +945,18 @@ def handle_keyboard(key, action):
|
||||||
scene.register_keyboard(handle_keyboard)
|
scene.register_keyboard(handle_keyboard)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `get_ui()`
|
|
||||||
|
|
||||||
Get the UI element collection for this scene.
|
|
||||||
|
|
||||||
**Returns:** UICollection: Collection of all UI elements in this scene
|
|
||||||
|
|
||||||
#### `activate()`
|
#### `activate()`
|
||||||
|
|
||||||
Make this scene the active scene.
|
Make this scene the active scene.
|
||||||
|
|
||||||
**Note:** Equivalent to calling setScene() with this scene's name.
|
**Note:** Equivalent to calling setScene() with this scene's name.
|
||||||
|
|
||||||
|
#### `get_ui()`
|
||||||
|
|
||||||
|
Get the UI element collection for this scene.
|
||||||
|
|
||||||
|
**Returns:** UICollection: Collection of all UI elements in this scene
|
||||||
|
|
||||||
#### `keypress(handler)`
|
#### `keypress(handler)`
|
||||||
|
|
||||||
Register a keyboard handler function for this scene.
|
Register a keyboard handler function for this scene.
|
||||||
|
|
@ -976,18 +974,6 @@ Timer object for scheduled callbacks.
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
#### `pause()`
|
|
||||||
|
|
||||||
Pause the timer, stopping its callback execution.
|
|
||||||
|
|
||||||
**Note:** Use resume() to continue the timer from where it was paused.
|
|
||||||
|
|
||||||
#### `resume()`
|
|
||||||
|
|
||||||
Resume a paused timer.
|
|
||||||
|
|
||||||
**Note:** Has no effect if timer is not paused.
|
|
||||||
|
|
||||||
#### `restart()`
|
#### `restart()`
|
||||||
|
|
||||||
Restart the timer from the beginning.
|
Restart the timer from the beginning.
|
||||||
|
|
@ -1000,6 +986,18 @@ Cancel the timer and remove it from the system.
|
||||||
|
|
||||||
**Note:** After cancelling, the timer object cannot be reused.
|
**Note:** After cancelling, the timer object cannot be reused.
|
||||||
|
|
||||||
|
#### `pause()`
|
||||||
|
|
||||||
|
Pause the timer, stopping its callback execution.
|
||||||
|
|
||||||
|
**Note:** Use resume() to continue the timer from where it was paused.
|
||||||
|
|
||||||
|
#### `resume()`
|
||||||
|
|
||||||
|
Resume a paused timer.
|
||||||
|
|
||||||
|
**Note:** Has no effect if timer is not paused.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### class `Window`
|
### class `Window`
|
||||||
|
|
@ -1008,6 +1006,14 @@ Window singleton for accessing and modifying the game window properties.
|
||||||
|
|
||||||
#### Methods
|
#### Methods
|
||||||
|
|
||||||
|
#### `get()`
|
||||||
|
|
||||||
|
Get the Window singleton instance.
|
||||||
|
|
||||||
|
**Returns:** Window: The singleton window object
|
||||||
|
|
||||||
|
**Note:** This is a static method that returns the same instance every time.
|
||||||
|
|
||||||
#### `screenshot(filename)`
|
#### `screenshot(filename)`
|
||||||
|
|
||||||
Take a screenshot and save it to a file.
|
Take a screenshot and save it to a file.
|
||||||
|
|
@ -1017,14 +1023,6 @@ Take a screenshot and save it to a file.
|
||||||
|
|
||||||
**Note:** Supports PNG, JPG, and BMP formats based on file extension.
|
**Note:** Supports PNG, JPG, and BMP formats based on file extension.
|
||||||
|
|
||||||
#### `get()`
|
|
||||||
|
|
||||||
Get the Window singleton instance.
|
|
||||||
|
|
||||||
**Returns:** Window: The singleton window object
|
|
||||||
|
|
||||||
**Note:** This is a static method that returns the same instance every time.
|
|
||||||
|
|
||||||
#### `center()`
|
#### `center()`
|
||||||
|
|
||||||
Center the window on the screen.
|
Center the window on the screen.
|
||||||
|
|
|
||||||
|
|
@ -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,273 +0,0 @@
|
||||||
# McRogueFace Issue Triage — April 2026
|
|
||||||
|
|
||||||
**46 open issues** across #53–#304. Grouped by system, ordered by impact.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 1: Render Cache Dirty Flags (Bugfix Cluster)
|
|
||||||
|
|
||||||
**4 issues — all quick-to-moderate fixes, high user-visible impact**
|
|
||||||
|
|
||||||
These are systemic bugs where Python property setters bypass the render cache invalidation system (#144). They cause stale frames when using `clip_children` or `cache_subtree`. Issue #291 is the umbrella audit; the other three are specific bugs it identified.
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #291 | Audit all Python property setters for missing markDirty() calls | Medium — systematic sweep of all tp_getset setters |
|
|
||||||
| #290 | UIDrawable base x/y/pos setters don't propagate dirty flags to parent | Quick — add markCompositeDirty() call in set_float_member() |
|
|
||||||
| #289 | Caption Python property setters don't call markDirty() | Quick — add markDirty() to text/font_size/fill_color setters |
|
|
||||||
| #288 | UICollection mutations don't invalidate parent Frame's render cache | Quick — add markCompositeDirty() in append/remove/etc |
|
|
||||||
|
|
||||||
**Dependencies:** None external. #291 depends on #288–#290 being fixed first (or done together).
|
|
||||||
|
|
||||||
**Recommendation: Tackle first.** These are correctness bugs affecting every user of the caching system. The fixes are mechanical (add missing dirty-flag calls), low risk, and testable. One focused session can close all four.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 2: Grid Dangling Pointer Bugs
|
|
||||||
|
|
||||||
**3 issues — moderate fixes, memory safety impact**
|
|
||||||
|
|
||||||
All three are the same class of bug: raw `UIGrid*` pointers in child objects that dangle when the parent grid is destroyed. Part of the broader memory safety audit (#279).
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #270 | GridLayer::parent_grid dangling raw pointer | Moderate — convert to weak_ptr or add invalidation |
|
|
||||||
| #271 | UIGridPoint::parent_grid dangling raw pointer | Moderate — same pattern as #270 |
|
|
||||||
| #277 | GridChunk::parent_grid dangling raw pointer | Moderate — same pattern as #270 |
|
|
||||||
|
|
||||||
**Dependencies:** These are the last 3 unfixed bugs from the memory safety audit (#279). Fixing all three would effectively close #279.
|
|
||||||
|
|
||||||
**Recommendation: Tackle second.** Same fix pattern applied three times. Closes the memory safety audit chapter.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 3: Animation System Fixes
|
|
||||||
|
|
||||||
**2 issues — one bugfix, one feature**
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #256 | Animation system bypasses spatial hash updates for entity position | Moderate — hook animation property changes into spatial hash |
|
|
||||||
| #218 | Color and Vector animation targets | Minor feature — compound property animation support |
|
|
||||||
|
|
||||||
**Dependencies:** #256 is independent. #218 is a nice-to-have that improves DX.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 4: Grid Layer & Rendering Fixes
|
|
||||||
|
|
||||||
**2 issues — quick fixes**
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #257 | Grid layers with z_index of zero are on top of entities | Quick — change `>=0` to `>0` or similar in draw order |
|
|
||||||
| #152 | Sparse Grid Layers | Major feature — default values + sub-grid chunk optimization |
|
|
||||||
|
|
||||||
**Dependencies:** #257 is standalone. #152 builds on the existing layer system.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 5: API Cleanup & Consistency
|
|
||||||
|
|
||||||
**1 issue — quick, blocks v1.0**
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #304 | Remove camelCase module functions before 1.0 | Quick — delete 4 method entries from mcrfpyMethods[], update tests |
|
|
||||||
|
|
||||||
**Dependencies:** Snake_case aliases already added. This is a breaking change gated on the 1.0 release.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 6: Multi-Tile Entity Rendering
|
|
||||||
|
|
||||||
**5 issues — parent + 4 children, all tier3-future**
|
|
||||||
|
|
||||||
Umbrella issue #233 with four sub-issues for different approaches to entities larger than one grid cell.
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #233 | Enhance Entity rendering and positioning capabilities (parent) | Meta/tracking |
|
|
||||||
| #234 | Entity origin offset for oversized sprites | Minor — add pixel offset to entity draw position |
|
|
||||||
| #235 | Texture display bounds for non-uniform sprite content | Minor — support non-cell-aligned sprite regions |
|
|
||||||
| #236 | Multi-tile entities using oversized sprites | Minor — render single large sprite across cells |
|
|
||||||
| #237 | Multi-tile entities using composite sprites | Major — multiple sprite indices per entity |
|
|
||||||
|
|
||||||
**Dependencies:** #234 is the simplest starting point. #236 and #237 build on #234/#235.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 7: Memory Safety Audit Tail
|
|
||||||
|
|
||||||
**9 issues — testing/tooling infrastructure for the #279 audit**
|
|
||||||
|
|
||||||
These are the remaining items from the 7DRL 2026 post-mortem. The actual bugs are mostly fixed; these are about preventing regressions and improving the safety toolchain.
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #279 | Engine memory safety audit — meta/tracking | Meta — close when #270/#271/#277 done |
|
|
||||||
| #287 | Regression tests for each bug from #258–#278 | Medium — write targeted test scripts |
|
|
||||||
| #285 | CI pipeline for debug-test and asan-test | Medium — CI/CD configuration |
|
|
||||||
| #286 | Re-enable ASan leak detection | Tiny — remove detect_leaks=0 suppression |
|
|
||||||
| #284 | Valgrind Massif heap profiling target | Tiny — add Makefile target |
|
|
||||||
| #283 | Atheris fuzzing harness for Python API | Major — significant new infrastructure |
|
|
||||||
| #282 | Install modern Clang for TSan/fuzzing | Minor — toolchain upgrade |
|
|
||||||
| #281 | Free-threaded CPython + TSan Makefile targets | Minor — Makefile additions |
|
|
||||||
| #280 | Instrumented libtcod debug build | Minor — rebuild libtcod with sanitizers |
|
|
||||||
|
|
||||||
**Dependencies:** #286 depends on #266/#275 (both closed). #287 depends on the actual bugs being fixed. #283 depends on #282.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 8: Grid Data Model Enhancements
|
|
||||||
|
|
||||||
**3 issues — foundation work for game data**
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #293 | DiscreteMap serialization via bytes | Minor — add bytes() and from_bytes() to DiscreteMap |
|
|
||||||
| #294 | Entity.gridstate as DiscreteMap reference | Minor — refactor internal representation |
|
|
||||||
| #149 | Reduce the size of UIGrid.cpp | Refactoring — break 1400+ line file into logical units |
|
|
||||||
|
|
||||||
**Dependencies:** #294 depends on #293. #149 is independent refactoring.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 9: Performance Optimization
|
|
||||||
|
|
||||||
**4 issues — significant effort, needs benchmarks first**
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #255 | Tracking down performance improvement opportunities | Investigation — profiling session |
|
|
||||||
| #117 | Memory Pool for Entities | Major — custom allocator |
|
|
||||||
| #145 | TexturePool with power-of-2 RenderTexture reuse | Major — deferred from #144 |
|
|
||||||
| #124 | Grid Point Animation | Major — per-tile animation system, needs design |
|
|
||||||
|
|
||||||
**Dependencies:** #255 should be done first to identify where optimization matters. #145 builds on the dirty-flag system (#144, closed). #124 is a large standalone feature.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 10: WASM / Playground Tooling
|
|
||||||
|
|
||||||
**3 issues — all tier3-future**
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #238 | Emscripten debugging infrastructure (DWARF, source maps) | Minor — build config additions |
|
|
||||||
| #239 | Automated WASM testing with headless browser | Major — new test infrastructure |
|
|
||||||
| #240 | Developer troubleshooting docs for WASM deployments | Documentation — write guide |
|
|
||||||
|
|
||||||
**Dependencies:** #238 supports #239. #240 is standalone documentation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 11: LLM Agent Testbed
|
|
||||||
|
|
||||||
**3 issues — research/demo infrastructure**
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #55 | McRogueFace as Agent Simulation Environment | Major — umbrella/vision issue |
|
|
||||||
| #154 | Grounded Multi-Agent Testbed | Major — research infrastructure |
|
|
||||||
| #156 | Turn-based LLM Agent Orchestration | Major — orchestration layer |
|
|
||||||
|
|
||||||
**Dependencies:** #156 depends on #154. Both depend on #55 conceptually. These also depend on #53 (alternative input methods) and mature API stability.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 12: Demo Games & Tutorials
|
|
||||||
|
|
||||||
**2 issues — showcase/marketing**
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #248 | Crypt of Sokoban Remaster (7DRL prep) | Major — full game remaster |
|
|
||||||
| #167 | r/roguelikedev Tutorial Series Demo Game | Major — tutorial content + demo game |
|
|
||||||
|
|
||||||
**Dependencies:** Both benefit from a stable, well-documented API. #167 specifically needs the API to be settled.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 13: Platform & Architecture (Far Future)
|
|
||||||
|
|
||||||
**5 issues — large features, mostly deferred**
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #70 | Package mcrfpy without embedded interpreter (wheels) | Major — significant build system rework |
|
|
||||||
| #62 | Multiple Windows | Major — architectural change |
|
|
||||||
| #67 | Grid Stitching / infinite world prototype | Major — new rendering/data infrastructure |
|
|
||||||
| #54 | Jupyter Notebook Interface | Major — alternative rendering target |
|
|
||||||
| #53 | Alternative Input Methods | Major — depends on #220 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Group 14: Concurrency
|
|
||||||
|
|
||||||
**1 issue — deferred**
|
|
||||||
|
|
||||||
| Issue | Title | Difficulty |
|
|
||||||
|-------|-------|------------|
|
|
||||||
| #220 | Secondary Concurrency Model: Subinterpreter Support | Major — Python 3.12+ subinterpreters |
|
|
||||||
|
|
||||||
**Dependencies:** Depends on free-threaded CPython work (#281).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary by Priority
|
|
||||||
|
|
||||||
| Priority | Groups | Issue Count | Session Estimate |
|
|
||||||
|----------|--------|-------------|-----------------|
|
|
||||||
| **Do now** | G1 (dirty flags), G2 (dangling ptrs) | 7 | 1 session |
|
|
||||||
| **Do soon** | G3 (animation), G4 (grid fixes), G5 (API cleanup) | 5 | 1 session |
|
|
||||||
| **Foundation** | G7 (safety tests), G8 (grid data) | 12 | 2-3 sessions |
|
|
||||||
| **When ready** | G6 (multi-tile), G9 (perf), G10 (WASM) | 12 | 3-4 sessions |
|
|
||||||
| **Future** | G11 (LLM), G12 (demos), G13 (platform), G14 (concurrency) | 10 | unbounded |
|
|
||||||
|
|
||||||
## Recommended First Session
|
|
||||||
|
|
||||||
**Groups 1 + 2: Dirty flags + dangling pointers (7 issues)**
|
|
||||||
|
|
||||||
Rationale:
|
|
||||||
- All are correctness/safety bugs, not features — fixes don't need design decisions
|
|
||||||
- Dirty flag fixes (#288-#291) share the same mechanical pattern: add missing `markDirty()` or `markCompositeDirty()` calls
|
|
||||||
- Dangling pointer fixes (#270, #271, #277) share the same pattern: convert `UIGrid*` to `weak_ptr<UIGrid>` or add invalidation on grid destruction
|
|
||||||
- Closing these also effectively closes the meta issue #279
|
|
||||||
- High confidence of completing all 7 in one session
|
|
||||||
- Clears the way for performance work (Group 9) which depends on correct caching
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Triage Completion Status (2026-04-20)
|
|
||||||
|
|
||||||
### Groups 1–5: COMPLETE (overnight sessions, prior to this entry)
|
|
||||||
|
|
||||||
All issues fixed or labeled. See commit history for details.
|
|
||||||
|
|
||||||
### Groups 6–14: COMPLETE (2026-04-20)
|
|
||||||
|
|
||||||
| Group | Issues | Status |
|
|
||||||
|-------|--------|--------|
|
|
||||||
| G6 Multi-tile entities | #233–#237 | All **closed** |
|
|
||||||
| G7 Memory safety tooling | #279–#287 | All **closed** except #282 (open, labeled) |
|
|
||||||
| G8 Grid data model | #149, #293, #294 | All **closed** |
|
|
||||||
| G9 Performance | #117, #124, #145, #255 | Open, all labeled |
|
|
||||||
| G10 WASM/Playground | #238, #239, #240 | #238/#240 closed; #239 open, labeled |
|
|
||||||
| G11 LLM agent testbed | #55, #154, #156 | Open, all labeled |
|
|
||||||
| G12 Demo games | #167, #248 | Open, all labeled |
|
|
||||||
| G13 Platform/architecture | #53, #54, #62, #67, #70 | Open, all labeled |
|
|
||||||
| G14 Concurrency | #220 | Open, labeled |
|
|
||||||
|
|
||||||
**Label pass completed:** All open issues in groups 6–14 now have `system:*`, `priority:tier*`, and type labels applied.
|
|
||||||
|
|
||||||
### Post-triage new issues (#312–#316, created 2026-04-19)
|
|
||||||
|
|
||||||
These appeared after the triage document was written and have been labeled in the same session:
|
|
||||||
|
|
||||||
| Issue | Title | Labels Applied |
|
|
||||||
|-------|-------|----------------|
|
|
||||||
| #312 | Extend fuzz coverage to remaining API surface | Minor Feature, system:performance, priority:tier2-foundation |
|
|
||||||
| #313 | Migrate UIEntity::grid to shared_ptr\<GridData\> | Refactoring & Cleanup, system:grid, system:python-binding, priority:tier1-active |
|
|
||||||
| #314 | API audit documentation follow-through | Documentation, system:documentation, priority:tier1-active |
|
|
||||||
| #316 | Sparse perspective writeback in updateVisibility | Minor Feature, system:performance, system:grid, priority:tier2-foundation, workflow:needs-benchmark |
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
# WASM / Emscripten Troubleshooting Guide
|
|
||||||
|
|
||||||
Practical solutions for common issues when building, testing, and deploying McRogueFace as a WebAssembly application.
|
|
||||||
|
|
||||||
## Build Issues
|
|
||||||
|
|
||||||
### "emcmake not found"
|
|
||||||
|
|
||||||
The Emscripten SDK must be activated in your current shell before building:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
source ~/emsdk/emsdk_env.sh
|
|
||||||
make wasm
|
|
||||||
```
|
|
||||||
|
|
||||||
This sets `PATH`, `EMSDK`, and other environment variables. You need to re-run it for each new terminal session.
|
|
||||||
|
|
||||||
### Build fails during CMake configure
|
|
||||||
|
|
||||||
If CMake fails during the Emscripten configure step, delete the build directory and re-configure:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm -rf build-emscripten
|
|
||||||
make wasm
|
|
||||||
```
|
|
||||||
|
|
||||||
The Makefile targets skip CMake if a `Makefile` already exists in the build directory. Stale CMake caches from a prior SDK version or changed options cause configure errors.
|
|
||||||
|
|
||||||
### "memory access out of bounds" at startup
|
|
||||||
|
|
||||||
Usually caused by insufficient stack or memory. The build defaults to a 2 MB stack (`-sSTACK_SIZE=2097152`) and growable heap (`-sALLOW_MEMORY_GROWTH=1`). If you hit stack limits with deep recursion (e.g. during Python import), increase the stack size in `CMakeLists.txt`:
|
|
||||||
|
|
||||||
```cmake
|
|
||||||
-sSTACK_SIZE=4194304 # 4 MB
|
|
||||||
```
|
|
||||||
|
|
||||||
### Link errors about undefined symbols
|
|
||||||
|
|
||||||
The Emscripten build uses `-sERROR_ON_UNDEFINED_SYMBOLS=0` because some libc/POSIX symbols are stubbed. If you add new C++ code that calls missing POSIX APIs, you will get a runtime error rather than a link error. Check the browser console for `Aborted(Assertion failed: missing function: ...)`.
|
|
||||||
|
|
||||||
## Runtime Issues
|
|
||||||
|
|
||||||
### Python import errors
|
|
||||||
|
|
||||||
The WASM build bundles a filtered Python stdlib at build time via `--preload-file`. If a Python module is missing at runtime:
|
|
||||||
|
|
||||||
1. Check `wasm_stdlib/lib/` — this is the preloaded stdlib tree
|
|
||||||
2. If the module should be included, add it to `tools/stdlib_modules.yaml` under the appropriate category
|
|
||||||
3. Rebuild: `rm -rf build-emscripten && make wasm`
|
|
||||||
|
|
||||||
Some modules (like `socket`, `ssl`, `multiprocessing`) are intentionally excluded because they require OS features unavailable in the browser.
|
|
||||||
|
|
||||||
### "Synchronous XMLHttpRequest on the main thread is deprecated"
|
|
||||||
|
|
||||||
This warning appears when Python code triggers synchronous file I/O during module import. It's harmless but can cause slight UI freezes. The engine preloads all files into Emscripten's virtual filesystem before Python starts, so actual network requests don't happen.
|
|
||||||
|
|
||||||
### IndexedDB / persistent storage errors
|
|
||||||
|
|
||||||
The build uses `-lidbfs.js` for persistent storage (save games, user preferences). Common issues:
|
|
||||||
|
|
||||||
- **"mkdir failed" on first load**: The engine calls `FS.mkdir('/idbfs')` during initialization. If the path already exists from a prior version, this fails silently. The `emscripten_pre.js` file patches this.
|
|
||||||
- **Data not persisting**: Call `FS.syncfs(false, callback)` from JavaScript to flush changes to IndexedDB. The C++ side exposes `sync_storage()` via `Module.ccall`.
|
|
||||||
- **Private browsing**: IndexedDB is unavailable in some private/incognito modes. The engine falls back gracefully but data won't persist.
|
|
||||||
|
|
||||||
### Black screen / no rendering
|
|
||||||
|
|
||||||
Check the browser's developer console (F12) for errors. Common causes:
|
|
||||||
|
|
||||||
- **WebGL 2 not supported**: The build requires WebGL 2 (`-sMIN_WEBGL_VERSION=2`). Very old browsers or software renderers may not support it.
|
|
||||||
- **Canvas size is zero**: If the HTML container has no explicit size, the canvas may render at 0x0. The custom `shell.html` handles this, but custom embedding needs to set canvas dimensions.
|
|
||||||
- **Exception during init**: A Python error during `game.py` execution will abort rendering. Check console for Python tracebacks.
|
|
||||||
|
|
||||||
### Audio not working
|
|
||||||
|
|
||||||
Audio is stubbed in the WASM build. `SoundBuffer`, `Sound`, and `Music` objects exist but do nothing. This is documented in the Web Build Constraints table in CLAUDE.md.
|
|
||||||
|
|
||||||
## Debugging
|
|
||||||
|
|
||||||
### Enable debug builds
|
|
||||||
|
|
||||||
Use the debug WASM targets for full DWARF symbols and source maps:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make wasm-debug # Full game with debug info
|
|
||||||
make playground-debug # REPL with debug info
|
|
||||||
```
|
|
||||||
|
|
||||||
These produce larger binaries but enable:
|
|
||||||
- **Source-level debugging** in Chrome DevTools (via DWARF and source maps)
|
|
||||||
- **Readable stack traces** (via `--emit-symbol-map`)
|
|
||||||
|
|
||||||
### Reading WASM stack traces
|
|
||||||
|
|
||||||
Production WASM stack traces show mangled names like `$_ZN7UIFrame6renderEv`. To demangle:
|
|
||||||
|
|
||||||
1. Use the debug build which emits a `.symbols` file
|
|
||||||
2. Or pipe through `c++filt`: `echo '_ZN7UIFrame6renderEv' | c++filt`
|
|
||||||
3. Or use Chrome's DWARF extension for inline source mapping
|
|
||||||
|
|
||||||
### Browser developer tools
|
|
||||||
|
|
||||||
- **Chrome**: DevTools > Sources > shows C++ source files with DWARF debug builds
|
|
||||||
- **Firefox**: Debugger > limited DWARF support, better with source maps
|
|
||||||
- **Console**: All `printf`/`std::cout` output goes to the browser console
|
|
||||||
- **Network**: Check that `.data` (preloaded files) and `.wasm` loaded successfully
|
|
||||||
- **Memory**: Use Chrome's Memory tab to profile WASM heap usage
|
|
||||||
|
|
||||||
### Assertions
|
|
||||||
|
|
||||||
The build enables `-sASSERTIONS=2` and `-sSTACK_OVERFLOW_CHECK=2` by default (both debug and release). These catch:
|
|
||||||
- Null pointer dereferences in WASM memory
|
|
||||||
- Stack overflow before it corrupts the heap
|
|
||||||
- Invalid Emscripten API usage
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### File sizes
|
|
||||||
|
|
||||||
Typical build sizes:
|
|
||||||
|
|
||||||
| Build | .wasm | .data | .js | Total |
|
|
||||||
|-------|-------|-------|-----|-------|
|
|
||||||
| Release | ~15 MB | ~25 MB | ~200 KB | ~40 MB |
|
|
||||||
| Debug | ~40 MB | ~25 MB | ~300 KB | ~65 MB |
|
|
||||||
|
|
||||||
The `.data` file contains the Python stdlib and game assets. Use the "light" stdlib preset to reduce it.
|
|
||||||
|
|
||||||
### Serving requirements
|
|
||||||
|
|
||||||
WASM files require specific HTTP headers:
|
|
||||||
- `Content-Type: application/wasm` for `.wasm` files
|
|
||||||
- CORS headers if serving from a CDN
|
|
||||||
|
|
||||||
The `make serve` targets use Python's `http.server` which handles MIME types correctly for local development.
|
|
||||||
|
|
||||||
### Embedding in custom pages
|
|
||||||
|
|
||||||
The build produces an HTML file from `shell.html` (or `shell_game.html`). To embed in your own page, you need:
|
|
||||||
|
|
||||||
1. The `.js`, `.wasm`, and `.data` files from the build directory
|
|
||||||
2. A canvas element with `id="canvas"`
|
|
||||||
3. Load the `.js` file, which bootstraps everything:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<canvas id="canvas" width="1024" height="768"></canvas>
|
|
||||||
<script src="mcrogueface.js"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Game shell vs playground shell
|
|
||||||
|
|
||||||
- `make wasm` / `make wasm-game`: Uses `shell.html` or `shell_game.html` — includes REPL widget or fullscreen canvas
|
|
||||||
- `make playground`: Uses `shell.html` with REPL chrome — intended for interactive testing
|
|
||||||
- Set `MCRF_GAME_SHELL=ON` in CMake for fullscreen-only (no REPL)
|
|
||||||
|
|
||||||
## Known Limitations
|
|
||||||
|
|
||||||
1. **No dynamic module loading**: All Python modules must be preloaded at build time
|
|
||||||
2. **No threading**: JavaScript is single-threaded; Python's `threading` module is non-functional
|
|
||||||
3. **No filesystem writes to disk**: Writes go to an in-memory filesystem (optionally synced to IndexedDB)
|
|
||||||
4. **No audio**: Sound API is fully stubbed
|
|
||||||
5. **No ImGui console**: The debug overlay is desktop-only
|
|
||||||
6. **Input differences**: Some keyboard shortcuts are intercepted by the browser (Ctrl+W, F5, etc.)
|
|
||||||
|
|
@ -1,994 +0,0 @@
|
||||||
# McRogueFace Python API Consistency Audit
|
|
||||||
|
|
||||||
**Date**: 2026-04-09
|
|
||||||
**Version**: 0.2.6-prerelease
|
|
||||||
**Purpose**: Catalog the full public API surface, identify inconsistencies and issues before 1.0 API freeze.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
1. [Executive Summary](#executive-summary)
|
|
||||||
2. [Module-Level API](#module-level-api)
|
|
||||||
3. [Core Value Types](#core-value-types)
|
|
||||||
4. [UI Drawable Types](#ui-drawable-types)
|
|
||||||
5. [Grid System](#grid-system)
|
|
||||||
6. [Entity System](#entity-system)
|
|
||||||
7. [Collections](#collections)
|
|
||||||
8. [Audio Types](#audio-types)
|
|
||||||
9. [Procedural Generation](#procedural-generation)
|
|
||||||
10. [Pathfinding](#pathfinding)
|
|
||||||
11. [Shader System](#shader-system)
|
|
||||||
12. [Tiled/LDtk Import](#tiledldtk-import)
|
|
||||||
13. [3D/Experimental Types](#3dexperimental-types)
|
|
||||||
14. [Enums](#enums)
|
|
||||||
15. [Findings: Naming Inconsistencies](#findings-naming-inconsistencies)
|
|
||||||
16. [Findings: Missing Functionality](#findings-missing-functionality)
|
|
||||||
17. [Findings: Deprecations to Resolve](#findings-deprecations-to-resolve)
|
|
||||||
18. [Findings: Documentation Gaps](#findings-documentation-gaps)
|
|
||||||
19. [Recommendations](#recommendations)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
The McRogueFace Python API exposes **46 exported types**, **14 internal types**, **10 enums**, **13 module-level functions**, **7 module-level properties**, and **5 singleton instances** through the `mcrfpy` module.
|
|
||||||
|
|
||||||
Overall, the API is remarkably consistent. Properties and methods use snake_case throughout the type system. The major inconsistencies are concentrated in a few areas:
|
|
||||||
|
|
||||||
1. **4 module-level functions use camelCase** (`setScale`, `findAll`, `getMetrics`, `setDevConsole`)
|
|
||||||
2. **Terse/placeholder docstrings** on 5 core types (Vector, Font, Texture, GridPoint, GridPointState)
|
|
||||||
3. **Deprecated property aliases** still exposed (`sprite_number`)
|
|
||||||
4. **Color property naming split**: some types use `fill_color`/`outline_color`, others use `color`
|
|
||||||
5. **Redundant position aliases** on Entity (`grid_pos` vs `cell_pos` for the same data)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Module-Level API
|
|
||||||
|
|
||||||
### Functions (`mcrfpy.*`)
|
|
||||||
|
|
||||||
| Function | Signature | Notes |
|
|
||||||
|----------|-----------|-------|
|
|
||||||
| `step` | `(dt: float = None) -> float` | Advance simulation (headless mode) |
|
|
||||||
| `exit` | `() -> None` | Shutdown engine |
|
|
||||||
| `find` | `(name: str, scene: str = None) -> Drawable \| None` | Find UI element by name |
|
|
||||||
| `lock` | `() -> _LockContext` | Thread-safe UI update context manager |
|
|
||||||
| `bresenham` | `(start, end, *, include_start=True, include_end=True) -> list[tuple]` | Line algorithm |
|
|
||||||
| `start_benchmark` | `() -> None` | Begin benchmark capture |
|
|
||||||
| `end_benchmark` | `() -> str` | End benchmark, return filename |
|
|
||||||
| `log_benchmark` | `(message: str) -> None` | Add benchmark annotation |
|
|
||||||
| `_sync_storage` | `() -> None` | WASM persistent storage flush |
|
|
||||||
| **`setScale`** | `(multiplier: float) -> None` | **CAMELCASE - deprecated** |
|
|
||||||
| **`findAll`** | `(pattern: str, scene: str = None) -> list` | **CAMELCASE** |
|
|
||||||
| **`getMetrics`** | `() -> dict` | **CAMELCASE** |
|
|
||||||
| **`setDevConsole`** | `(enabled: bool) -> None` | **CAMELCASE** |
|
|
||||||
|
|
||||||
### Properties (`mcrfpy.*`)
|
|
||||||
|
|
||||||
| Property | Type | Writable | Notes |
|
|
||||||
|----------|------|----------|-------|
|
|
||||||
| `current_scene` | `Scene \| None` | Yes | Active scene |
|
|
||||||
| `scenes` | `dict[str, Scene]` | No | All registered scenes |
|
|
||||||
| `timers` | `list[Timer]` | No | Active timers |
|
|
||||||
| `animations` | `list[Animation]` | No | Active animations |
|
|
||||||
| `default_transition` | `Transition` | Yes | Scene transition effect |
|
|
||||||
| `default_transition_duration` | `float` | Yes | Transition duration |
|
|
||||||
| `save_dir` | `str` | No | Platform-specific save path |
|
|
||||||
|
|
||||||
### Singletons
|
|
||||||
|
|
||||||
| Name | Type | Notes |
|
|
||||||
|------|------|-------|
|
|
||||||
| `keyboard` | `Keyboard` | Modifier key state |
|
|
||||||
| `mouse` | `Mouse` | Position and button state |
|
|
||||||
| `window` | `Window` | Window properties |
|
|
||||||
| `default_font` | `Font` | JetBrains Mono |
|
|
||||||
| `default_texture` | `Texture` | Kenney Tiny Dungeon (16x16) |
|
|
||||||
|
|
||||||
### Constants
|
|
||||||
|
|
||||||
| Name | Type | Value |
|
|
||||||
|------|------|-------|
|
|
||||||
| `__version__` | `str` | Build version string |
|
|
||||||
| `default_fov` | `FOV` | `FOV.BASIC` |
|
|
||||||
|
|
||||||
### Submodules
|
|
||||||
|
|
||||||
| Name | Contents |
|
|
||||||
|------|----------|
|
|
||||||
| `automation` | Screenshot, click simulation, testing utilities |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Core Value Types
|
|
||||||
|
|
||||||
### `Color`
|
|
||||||
|
|
||||||
```
|
|
||||||
Color(r: int = 0, g: int = 0, b: int = 0, a: int = 255)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `r`, `g`, `b`, `a` | int (0-255) | R/W |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `from_hex` | `(cls, hex_string: str) -> Color` (classmethod) |
|
|
||||||
| `to_hex` | `() -> str` |
|
|
||||||
| `lerp` | `(other: Color, t: float) -> Color` |
|
|
||||||
|
|
||||||
Protocols: `__repr__`, `__hash__`
|
|
||||||
|
|
||||||
### `Vector`
|
|
||||||
|
|
||||||
```
|
|
||||||
Vector(x: float = 0, y: float = 0)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `x`, `y` | float | R/W |
|
|
||||||
| `int` | tuple[int, int] | R |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `magnitude` | `() -> float` |
|
|
||||||
| `magnitude_squared` | `() -> float` |
|
|
||||||
| `normalize` | `() -> Vector` |
|
|
||||||
| `dot` | `(other: Vector) -> float` |
|
|
||||||
| `distance_to` | `(other: Vector) -> float` |
|
|
||||||
| `angle` | `() -> float` |
|
|
||||||
| `copy` | `() -> Vector` |
|
|
||||||
| `floor` | `() -> Vector` |
|
|
||||||
|
|
||||||
Protocols: `__repr__`, `__hash__`, `__eq__`/`__ne__`, arithmetic (`+`, `-`, `*`, `/`, `-x`, `abs`), sequence (`len`, `[0]`/`[1]`)
|
|
||||||
|
|
||||||
### `Font`
|
|
||||||
|
|
||||||
```
|
|
||||||
Font(filename: str)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `family` | str | R |
|
|
||||||
| `source` | str | R |
|
|
||||||
|
|
||||||
Methods: None
|
|
||||||
Protocols: `__repr__`
|
|
||||||
|
|
||||||
### `Texture`
|
|
||||||
|
|
||||||
```
|
|
||||||
Texture(filename: str, sprite_width: int, sprite_height: int)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `sprite_width`, `sprite_height` | int | R |
|
|
||||||
| `sheet_width`, `sheet_height` | int | R |
|
|
||||||
| `sprite_count` | int | R |
|
|
||||||
| `source` | str | R |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `from_bytes` | `(cls, data, w, h, sprite_w, sprite_h, name=...) -> Texture` (classmethod) |
|
|
||||||
| `composite` | `(cls, layers, sprite_w, sprite_h, name=...) -> Texture` (classmethod) |
|
|
||||||
| `hsl_shift` | `(hue_shift, sat_shift=0, lit_shift=0) -> Texture` |
|
|
||||||
|
|
||||||
Protocols: `__repr__`, `__hash__`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## UI Drawable Types
|
|
||||||
|
|
||||||
### Base: `Drawable` (abstract)
|
|
||||||
|
|
||||||
Cannot be instantiated directly.
|
|
||||||
|
|
||||||
| Properties | Type | R/W | Notes |
|
|
||||||
|-----------|------|-----|-------|
|
|
||||||
| `on_click` | callable | R/W | `(pos, button, action)` |
|
|
||||||
| `z_index` | int | R/W | Render order |
|
|
||||||
| `visible` | bool | R/W | |
|
|
||||||
| `opacity` | float | R/W | 0.0-1.0 |
|
|
||||||
| `name` | str | R/W | |
|
|
||||||
| `pos` | Vector | R/W | |
|
|
||||||
| `parent` | Drawable | R | |
|
|
||||||
| `align` | Alignment | R/W | |
|
|
||||||
| `margin`, `horiz_margin`, `vert_margin` | float | R/W | |
|
|
||||||
| `shader` | Shader | R/W | |
|
|
||||||
| `uniforms` | UniformCollection | R | |
|
|
||||||
| `rotation` | float | R/W | |
|
|
||||||
| `origin` | Vector | R/W | |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `move` | `(dx, dy)` or `(delta)` |
|
|
||||||
| `resize` | `(w, h)` or `(size)` |
|
|
||||||
| `animate` | `(property, target, duration, easing, ...)` |
|
|
||||||
|
|
||||||
### `Frame`
|
|
||||||
|
|
||||||
```
|
|
||||||
Frame(pos=None, size=None, **kwargs)
|
|
||||||
```
|
|
||||||
|
|
||||||
Additional properties beyond Drawable:
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `x`, `y`, `w`, `h` | float | R/W |
|
|
||||||
| `fill_color` | Color | R/W |
|
|
||||||
| `outline_color` | Color | R/W |
|
|
||||||
| `outline` | float | R/W |
|
|
||||||
| `children` | UICollection | R |
|
|
||||||
| `clip_children` | bool | R/W |
|
|
||||||
| `cache_subtree` | bool | R/W |
|
|
||||||
| `grid_pos`, `grid_size` | Vector | R/W |
|
|
||||||
|
|
||||||
### `Caption`
|
|
||||||
|
|
||||||
```
|
|
||||||
Caption(pos=None, font=None, text='', **kwargs)
|
|
||||||
```
|
|
||||||
|
|
||||||
Additional properties beyond Drawable:
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `x`, `y` | float | R/W |
|
|
||||||
| `w`, `h` | float | R (computed) |
|
|
||||||
| `size` | Vector | R (computed) |
|
|
||||||
| `text` | str | R/W |
|
|
||||||
| `font_size` | float | R/W |
|
|
||||||
| `fill_color` | Color | R/W |
|
|
||||||
| `outline_color` | Color | R/W |
|
|
||||||
| `outline` | float | R/W |
|
|
||||||
|
|
||||||
### `Sprite`
|
|
||||||
|
|
||||||
```
|
|
||||||
Sprite(pos=None, texture=None, sprite_index=0, **kwargs)
|
|
||||||
```
|
|
||||||
|
|
||||||
Additional properties beyond Drawable:
|
|
||||||
|
|
||||||
| Properties | Type | R/W | Notes |
|
|
||||||
|-----------|------|-----|-------|
|
|
||||||
| `x`, `y` | float | R/W | |
|
|
||||||
| `w`, `h` | float | R (computed) | |
|
|
||||||
| `scale` | float | R/W | Uniform scale |
|
|
||||||
| `scale_x`, `scale_y` | float | R/W | Per-axis scale |
|
|
||||||
| `sprite_index` | int | R/W | |
|
|
||||||
| `sprite_number` | int | R/W | **DEPRECATED alias** |
|
|
||||||
| `texture` | Texture | R/W | |
|
|
||||||
|
|
||||||
### `Line`
|
|
||||||
|
|
||||||
```
|
|
||||||
Line(start=None, end=None, thickness=1.0, color=None, **kwargs)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W | Notes |
|
|
||||||
|-----------|------|-----|-------|
|
|
||||||
| `start` | Vector | R/W | |
|
|
||||||
| `end` | Vector | R/W | |
|
|
||||||
| `color` | Color | R/W | **Not `fill_color`** |
|
|
||||||
| `thickness` | float | R/W | |
|
|
||||||
|
|
||||||
### `Circle`
|
|
||||||
|
|
||||||
```
|
|
||||||
Circle(radius=0, center=None, fill_color=None, outline_color=None, outline=0, **kwargs)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `radius` | float | R/W |
|
|
||||||
| `center` | Vector | R/W |
|
|
||||||
| `fill_color` | Color | R/W |
|
|
||||||
| `outline_color` | Color | R/W |
|
|
||||||
| `outline` | float | R/W |
|
|
||||||
|
|
||||||
### `Arc`
|
|
||||||
|
|
||||||
```
|
|
||||||
Arc(center=None, radius=0, start_angle=0, end_angle=90, color=None, thickness=1, **kwargs)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W | Notes |
|
|
||||||
|-----------|------|-----|-------|
|
|
||||||
| `center` | Vector | R/W | |
|
|
||||||
| `radius` | float | R/W | |
|
|
||||||
| `start_angle`, `end_angle` | float | R/W | Degrees |
|
|
||||||
| `color` | Color | R/W | **Not `fill_color`** |
|
|
||||||
| `thickness` | float | R/W | |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Grid System
|
|
||||||
|
|
||||||
### `Grid` (also available as `GridView`)
|
|
||||||
|
|
||||||
```
|
|
||||||
Grid(grid_size=None, pos=None, size=None, texture=None, **kwargs)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W | Notes |
|
|
||||||
|-----------|------|-----|-------|
|
|
||||||
| `grid_size`, `grid_w`, `grid_h` | tuple/int | R | |
|
|
||||||
| `x`, `y`, `w`, `h` | float | R/W | |
|
|
||||||
| `pos`, `position` | Vector | R/W | `position` is redundant alias |
|
|
||||||
| `center` | Vector | R/W | Camera center (pixels) |
|
|
||||||
| `center_x`, `center_y` | float | R/W | |
|
|
||||||
| `zoom` | float | R/W | |
|
|
||||||
| `camera_rotation` | float | R/W | |
|
|
||||||
| `fill_color` | Color | R/W | |
|
|
||||||
| `texture` | Texture | R | |
|
|
||||||
| `entities` | EntityCollection | R | |
|
|
||||||
| `children` | UICollection | R | |
|
|
||||||
| `layers` | tuple | R | |
|
|
||||||
| `perspective`, `perspective_enabled` | various | R/W | |
|
|
||||||
| `fov`, `fov_radius` | various | R/W | |
|
|
||||||
| `on_cell_enter`, `on_cell_exit`, `on_cell_click` | callable | R/W | |
|
|
||||||
| `hovered_cell` | tuple | R | |
|
|
||||||
| `grid_data` | _GridData | R/W | Internal grid reference |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `at` | `(x, y)` or `(pos)` -> GridPoint |
|
|
||||||
| `compute_fov` | `(pos, radius, light_walls, algorithm)` |
|
|
||||||
| `is_in_fov` | `(pos) -> bool` |
|
|
||||||
| `find_path` | `(start, end, diagonal_cost, collide) -> AStarPath` |
|
|
||||||
| `get_dijkstra_map` | `(root, diagonal_cost, collide) -> DijkstraMap` |
|
|
||||||
| `clear_dijkstra_maps` | `()` |
|
|
||||||
| `add_layer` | `(layer)` |
|
|
||||||
| `remove_layer` | `(name_or_layer)` |
|
|
||||||
| `layer` | `(name) -> ColorLayer \| TileLayer` |
|
|
||||||
| `entities_in_radius` | `(pos, radius) -> list` |
|
|
||||||
| `center_camera` | `(pos)` -- tile coordinates |
|
|
||||||
| `apply_threshold` | `(source, range, walkable, transparent)` |
|
|
||||||
| `apply_ranges` | `(source, ranges)` |
|
|
||||||
| `step` | `(n, turn_order)` -- turn management |
|
|
||||||
|
|
||||||
### `GridPoint` (internal, returned by `Grid.at()`)
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `walkable` | bool | R/W |
|
|
||||||
| `transparent` | bool | R/W |
|
|
||||||
| `entities` | list | R |
|
|
||||||
| `grid_pos` | tuple | R |
|
|
||||||
|
|
||||||
Dynamic attributes: named layer data via `__getattr__`/`__setattr__`
|
|
||||||
|
|
||||||
### `GridPointState` (internal, returned by entity gridstate)
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `visible` | bool | R/W |
|
|
||||||
| `discovered` | bool | R/W |
|
|
||||||
| `point` | GridPoint | R |
|
|
||||||
|
|
||||||
### `ColorLayer`
|
|
||||||
|
|
||||||
```
|
|
||||||
ColorLayer(z_index=-1, name=None, grid_size=None)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `z_index` | int | R/W |
|
|
||||||
| `visible` | bool | R/W |
|
|
||||||
| `grid_size` | tuple | R |
|
|
||||||
| `name` | str | R |
|
|
||||||
| `grid` | Grid | R/W |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `at` | `(x, y)` or `(pos) -> Color` |
|
|
||||||
| `set` | `(pos, color)` |
|
|
||||||
| `fill` | `(color)` |
|
|
||||||
| `fill_rect` | `(pos, size, color)` |
|
|
||||||
| `draw_fov` | `(source, radius, fov, visible, discovered, unknown)` |
|
|
||||||
| `apply_perspective` | `(entity, visible, discovered, unknown)` |
|
|
||||||
| `update_perspective` | `()` |
|
|
||||||
| `clear_perspective` | `()` |
|
|
||||||
| `apply_threshold` | `(source, range, color)` |
|
|
||||||
| `apply_gradient` | `(source, range, color_low, color_high)` |
|
|
||||||
| `apply_ranges` | `(source, ranges)` |
|
|
||||||
|
|
||||||
### `TileLayer`
|
|
||||||
|
|
||||||
```
|
|
||||||
TileLayer(z_index=-1, name=None, texture=None, grid_size=None)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `z_index` | int | R/W |
|
|
||||||
| `visible` | bool | R/W |
|
|
||||||
| `texture` | Texture | R/W |
|
|
||||||
| `grid_size` | tuple | R |
|
|
||||||
| `name` | str | R |
|
|
||||||
| `grid` | Grid | R/W |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `at` | `(x, y)` or `(pos) -> int` |
|
|
||||||
| `set` | `(pos, index)` |
|
|
||||||
| `fill` | `(index)` |
|
|
||||||
| `fill_rect` | `(pos, size, index)` |
|
|
||||||
| `apply_threshold` | `(source, range, tile)` |
|
|
||||||
| `apply_ranges` | `(source, ranges)` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Entity System
|
|
||||||
|
|
||||||
### `Entity`
|
|
||||||
|
|
||||||
```
|
|
||||||
Entity(grid_pos=None, texture=None, sprite_index=0, **kwargs)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W | Notes |
|
|
||||||
|-----------|------|-----|-------|
|
|
||||||
| `pos`, `x`, `y` | Vector/float | R/W | Pixel position |
|
|
||||||
| `cell_pos`, `cell_x`, `cell_y` | Vector/int | R/W | Integer cell coords |
|
|
||||||
| `grid_pos`, `grid_x`, `grid_y` | Vector/int | R/W | **Same as cell_pos** |
|
|
||||||
| `draw_pos` | Vector | R/W | Fractional tile position |
|
|
||||||
| `sprite_index` | int | R/W | |
|
|
||||||
| `sprite_number` | int | R/W | **DEPRECATED alias** |
|
|
||||||
| `sprite_offset`, `sprite_offset_x`, `sprite_offset_y` | Vector/float | R/W | |
|
|
||||||
| `grid` | Grid | R/W | |
|
|
||||||
| `gridstate` | GridPointState | R | |
|
|
||||||
| `labels` | frozenset | R/W | |
|
|
||||||
| `step` | callable | R/W | Turn callback |
|
|
||||||
| `default_behavior` | Behavior | R/W | |
|
|
||||||
| `behavior_type` | Behavior | R | |
|
|
||||||
| `turn_order` | int | R/W | |
|
|
||||||
| `move_speed` | float | R/W | |
|
|
||||||
| `target_label` | str | R/W | |
|
|
||||||
| `sight_radius` | int | R/W | |
|
|
||||||
| `visible`, `opacity`, `name` | various | R/W | |
|
|
||||||
| `shader`, `uniforms` | various | R/W | |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `at` | `(x, y)` or `(pos) -> GridPoint` |
|
|
||||||
| `index` | `() -> int` |
|
|
||||||
| `die` | `()` |
|
|
||||||
| `path_to` | `(x, y)` or `(target) -> AStarPath` |
|
|
||||||
| `find_path` | `(target, diagonal_cost, collide) -> AStarPath` |
|
|
||||||
| `update_visibility` | `()` |
|
|
||||||
| `visible_entities` | `(fov, radius) -> list` |
|
|
||||||
| `animate` | `(property, target, duration, easing, ...)` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Collections
|
|
||||||
|
|
||||||
### `UICollection` (internal, returned by `Frame.children` / `Scene.children`)
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `append` | `(element)` |
|
|
||||||
| `extend` | `(iterable)` |
|
|
||||||
| `insert` | `(index, element)` |
|
|
||||||
| `remove` | `(element)` |
|
|
||||||
| `pop` | `([index]) -> Drawable` |
|
|
||||||
| `index` | `(element) -> int` |
|
|
||||||
| `count` | `(element) -> int` |
|
|
||||||
| `find` | `(name, recursive=False) -> Drawable \| None` |
|
|
||||||
|
|
||||||
Protocols: `len`, `[]`, slicing, iteration
|
|
||||||
|
|
||||||
### `EntityCollection` (internal, returned by `Grid.entities`)
|
|
||||||
|
|
||||||
Same methods as UICollection. Protocols: `len`, `[]`, slicing, iteration.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Audio Types
|
|
||||||
|
|
||||||
### `Sound`
|
|
||||||
|
|
||||||
```
|
|
||||||
Sound(source: str | SoundBuffer)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `volume` | float (0-100) | R/W |
|
|
||||||
| `loop` | bool | R/W |
|
|
||||||
| `playing` | bool | R |
|
|
||||||
| `duration` | float | R |
|
|
||||||
| `source` | str | R |
|
|
||||||
| `pitch` | float | R/W |
|
|
||||||
| `buffer` | SoundBuffer | R |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `play` | `()` |
|
|
||||||
| `pause` | `()` |
|
|
||||||
| `stop` | `()` |
|
|
||||||
| `play_varied` | `(pitch_range=0.1, volume_range=3.0)` |
|
|
||||||
|
|
||||||
### `SoundBuffer`
|
|
||||||
|
|
||||||
```
|
|
||||||
SoundBuffer(filename: str)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `duration` | float | R |
|
|
||||||
| `sample_count` | int | R |
|
|
||||||
| `sample_rate` | int | R |
|
|
||||||
| `channels` | int | R |
|
|
||||||
| `sfxr_params` | dict | R |
|
|
||||||
|
|
||||||
| Methods | Signature | Notes |
|
|
||||||
|---------|-----------|-------|
|
|
||||||
| `from_samples` | `(cls, data, channels, sample_rate) -> SoundBuffer` | classmethod |
|
|
||||||
| `tone` | `(cls, frequency, duration, waveform='sine', ...) -> SoundBuffer` | classmethod |
|
|
||||||
| `sfxr` | `(cls, preset, seed=None) -> SoundBuffer` | classmethod |
|
|
||||||
| `concat` | `(cls, buffers) -> SoundBuffer` | classmethod |
|
|
||||||
| `mix` | `(cls, buffers) -> SoundBuffer` | classmethod |
|
|
||||||
| `pitch_shift` | `(semitones) -> SoundBuffer` | returns new |
|
|
||||||
| `low_pass` | `(cutoff) -> SoundBuffer` | returns new |
|
|
||||||
| `high_pass` | `(cutoff) -> SoundBuffer` | returns new |
|
|
||||||
| `echo` | `(delay, decay) -> SoundBuffer` | returns new |
|
|
||||||
| `reverb` | `(room_size) -> SoundBuffer` | returns new |
|
|
||||||
| `distortion` | `(gain) -> SoundBuffer` | returns new |
|
|
||||||
| `bit_crush` | `(bits) -> SoundBuffer` | returns new |
|
|
||||||
| `gain` | `(amount) -> SoundBuffer` | returns new |
|
|
||||||
| `normalize` | `() -> SoundBuffer` | returns new |
|
|
||||||
| `reverse` | `() -> SoundBuffer` | returns new |
|
|
||||||
| `slice` | `(start, end) -> SoundBuffer` | returns new |
|
|
||||||
| `sfxr_mutate` | `(amount) -> SoundBuffer` | returns new |
|
|
||||||
|
|
||||||
### `Music`
|
|
||||||
|
|
||||||
```
|
|
||||||
Music(filename: str)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `volume` | float (0-100) | R/W |
|
|
||||||
| `loop` | bool | R/W |
|
|
||||||
| `playing` | bool | R |
|
|
||||||
| `duration` | float | R |
|
|
||||||
| `position` | float | R/W |
|
|
||||||
| `source` | str | R |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `play` | `()` |
|
|
||||||
| `pause` | `()` |
|
|
||||||
| `stop` | `()` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Procedural Generation
|
|
||||||
|
|
||||||
### `HeightMap`
|
|
||||||
|
|
||||||
```
|
|
||||||
HeightMap(size: tuple[int, int], fill: float = 0.0)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `size` | tuple | R |
|
|
||||||
|
|
||||||
46 methods covering: fill/clear, get/set (via `[]`), math operations, noise, erosion, BSP integration, kernel operations, binary operations.
|
|
||||||
|
|
||||||
Protocols: `[x, y]` subscript (get/set)
|
|
||||||
|
|
||||||
### `DiscreteMap`
|
|
||||||
|
|
||||||
```
|
|
||||||
DiscreteMap(size: tuple[int, int], fill: int = 0, enum: type[IntEnum] = None)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `size` | tuple | R |
|
|
||||||
| `enum_type` | type | R/W |
|
|
||||||
|
|
||||||
22 methods covering: fill/clear, get/set (via `[]`), math, bitwise, statistics, conversion.
|
|
||||||
|
|
||||||
Protocols: `[x, y]` subscript (get/set)
|
|
||||||
|
|
||||||
### `BSP`
|
|
||||||
|
|
||||||
```
|
|
||||||
BSP(pos: tuple[int, int], size: tuple[int, int])
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `bounds`, `pos`, `size` | tuple | R |
|
|
||||||
| `root` | BSPNode | R |
|
|
||||||
| `adjacency` | BSPAdjacency | R |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `split_once` | `(...)` |
|
|
||||||
| `split_recursive` | `(...)` |
|
|
||||||
| `clear` | `()` |
|
|
||||||
| `leaves` | `() -> list[BSPNode]` |
|
|
||||||
| `traverse` | `(order) -> BSPIter` |
|
|
||||||
| `find` | `(pos) -> BSPNode` |
|
|
||||||
| `get_leaf` | `(index) -> BSPNode` |
|
|
||||||
| `to_heightmap` | `() -> HeightMap` |
|
|
||||||
|
|
||||||
### `NoiseSource`
|
|
||||||
|
|
||||||
```
|
|
||||||
NoiseSource(dimensions=2, algorithm='simplex', hurst=0.5, lacunarity=2.0, seed=None)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `dimensions`, `algorithm`, `hurst`, `lacunarity`, `seed` | various | R |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `get` | `(pos) -> float` |
|
|
||||||
| `fbm` | `(pos, octaves=4) -> float` |
|
|
||||||
| `turbulence` | `(pos, octaves=4) -> float` |
|
|
||||||
| `sample` | `(size, world_origin, world_size, mode, octaves) -> HeightMap` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pathfinding
|
|
||||||
|
|
||||||
### `AStarPath`
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `origin` | tuple | R |
|
|
||||||
| `destination` | tuple | R |
|
|
||||||
| `remaining` | int | R |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `walk` | `() -> Vector` |
|
|
||||||
| `peek` | `() -> Vector` |
|
|
||||||
|
|
||||||
Protocols: `len`, `bool`, iteration
|
|
||||||
|
|
||||||
### `DijkstraMap`
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `root` | tuple | R |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `distance` | `(pos) -> float \| None` |
|
|
||||||
| `path_from` | `(pos) -> AStarPath` |
|
|
||||||
| `step_from` | `(pos) -> Vector \| None` |
|
|
||||||
| `to_heightmap` | `(size=None, unreachable=-1.0) -> HeightMap` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Shader System
|
|
||||||
|
|
||||||
### `Shader`
|
|
||||||
|
|
||||||
```
|
|
||||||
Shader(fragment_source: str, dynamic: bool = False)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `dynamic` | bool | R/W |
|
|
||||||
| `source` | str | R |
|
|
||||||
| `is_valid` | bool | R |
|
|
||||||
|
|
||||||
| Methods | Signature |
|
|
||||||
|---------|-----------|
|
|
||||||
| `set_uniform` | `(name: str, value: float \| tuple)` |
|
|
||||||
|
|
||||||
### `PropertyBinding`
|
|
||||||
|
|
||||||
```
|
|
||||||
PropertyBinding(target: Drawable, property: str)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `target` | Drawable | R |
|
|
||||||
| `property` | str | R |
|
|
||||||
| `value` | float | R |
|
|
||||||
| `is_valid` | bool | R |
|
|
||||||
|
|
||||||
### `CallableBinding`
|
|
||||||
|
|
||||||
```
|
|
||||||
CallableBinding(callable: Callable[[], float])
|
|
||||||
```
|
|
||||||
|
|
||||||
| Properties | Type | R/W |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| `callable` | callable | R |
|
|
||||||
| `value` | float | R |
|
|
||||||
| `is_valid` | bool | R |
|
|
||||||
|
|
||||||
### `UniformCollection` (internal, returned by `drawable.uniforms`)
|
|
||||||
|
|
||||||
Dict-like container. Supports `[]`, `del`, `in`, `keys()`, `values()`, `items()`, `clear()`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tiled/LDtk Import
|
|
||||||
|
|
||||||
### `TileSetFile`
|
|
||||||
|
|
||||||
```
|
|
||||||
TileSetFile(path: str)
|
|
||||||
```
|
|
||||||
|
|
||||||
Properties (all R): `name`, `tile_width`, `tile_height`, `tile_count`, `columns`, `margin`, `spacing`, `image_source`, `properties`, `wang_sets`
|
|
||||||
|
|
||||||
Methods: `to_texture()`, `tile_info(id)`, `wang_set(name)`
|
|
||||||
|
|
||||||
### `TileMapFile`
|
|
||||||
|
|
||||||
```
|
|
||||||
TileMapFile(path: str)
|
|
||||||
```
|
|
||||||
|
|
||||||
Properties (all R): `width`, `height`, `tile_width`, `tile_height`, `orientation`, `properties`, `tileset_count`, `tile_layer_names`, `object_layer_names`
|
|
||||||
|
|
||||||
Methods: `tileset(index)`, `tile_layer_data(name)`, `resolve_gid(gid)`, `object_layer(name)`, `apply_to_tile_layer(layer, name)`
|
|
||||||
|
|
||||||
### `WangSet` (factory-created from TileSetFile)
|
|
||||||
|
|
||||||
Properties (all R): `name`, `type`, `color_count`, `colors`
|
|
||||||
|
|
||||||
Methods: `terrain_enum()`, `resolve(discrete_map)`, `apply(discrete_map, tile_layer)`
|
|
||||||
|
|
||||||
### `LdtkProject`
|
|
||||||
|
|
||||||
```
|
|
||||||
LdtkProject(path: str)
|
|
||||||
```
|
|
||||||
|
|
||||||
Properties (all R): `version`, `tileset_names`, `ruleset_names`, `level_names`, `enums`
|
|
||||||
|
|
||||||
Methods: `tileset(name)`, `ruleset(name)`, `level(name)`
|
|
||||||
|
|
||||||
### `AutoRuleSet` (factory-created from LdtkProject)
|
|
||||||
|
|
||||||
Properties (all R): `name`, `grid_size`, `value_count`, `values`, `rule_count`, `group_count`
|
|
||||||
|
|
||||||
Methods: `terrain_enum()`, `resolve(discrete_map)`, `apply(discrete_map, tile_layer)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3D/Experimental Types
|
|
||||||
|
|
||||||
> These are exempt from the 1.0 API freeze per ROADMAP.md.
|
|
||||||
|
|
||||||
Viewport3D, Entity3D, EntityCollection3D, Model3D, Billboard, VoxelGrid, VoxelRegion, VoxelPoint, Camera3D (via Viewport3D properties).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Enums
|
|
||||||
|
|
||||||
| Enum | Values | Notes |
|
|
||||||
|------|--------|-------|
|
|
||||||
| `Key` | 42+ keyboard keys | Legacy string comparison (`Key.ESCAPE == "Escape"`) |
|
|
||||||
| `InputState` | `PRESSED`, `RELEASED` | Legacy: `"start"`, `"end"` |
|
|
||||||
| `MouseButton` | `LEFT`, `RIGHT`, `MIDDLE`, `X1`, `X2` | Legacy: `"left"`, `"right"`, `"middle"` |
|
|
||||||
| `Easing` | 32 easing functions | Linear, Quad, Cubic, etc. |
|
|
||||||
| `Transition` | Scene transition effects | |
|
|
||||||
| `Traversal` | BSP traversal orders | |
|
|
||||||
| `Alignment` | 9 positions + NONE | TOP_LEFT through BOTTOM_RIGHT |
|
|
||||||
| `Behavior` | 11 entity behaviors | For `grid.step()` turn system |
|
|
||||||
| `Trigger` | 3 trigger types | Entity step callbacks |
|
|
||||||
| `FOV` | FOV algorithms | Maps to libtcod |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Findings: Naming Inconsistencies
|
|
||||||
|
|
||||||
### F1: Module-level camelCase functions (CRITICAL)
|
|
||||||
|
|
||||||
Four module-level functions use camelCase while everything else uses snake_case:
|
|
||||||
|
|
||||||
| Current | Should Be | Status |
|
|
||||||
|---------|-----------|--------|
|
|
||||||
| `setScale` | `set_scale` | Deprecated anyway (use `Window.resolution`) |
|
|
||||||
| `findAll` | `find_all` | Active, needs alias |
|
|
||||||
| `getMetrics` | `get_metrics` | Active, needs alias |
|
|
||||||
| `setDevConsole` | `set_dev_console` | Active, needs alias |
|
|
||||||
|
|
||||||
**Resolution**: Add snake_case aliases. Keep camelCase temporarily for backward compatibility. Remove camelCase in 1.0.
|
|
||||||
|
|
||||||
### F2: Color property naming split
|
|
||||||
|
|
||||||
Filled shapes (Frame, Caption, Circle) use `fill_color`/`outline_color`. Stroke-only shapes (Line, Arc) use `color`. This is actually semantically correct -- Line and Arc don't have a "fill" concept. **No change needed**, but worth documenting.
|
|
||||||
|
|
||||||
### F3: Redundant Entity position aliases
|
|
||||||
|
|
||||||
Entity exposes the same cell position data under two names:
|
|
||||||
- `grid_pos`, `grid_x`, `grid_y`
|
|
||||||
- `cell_pos`, `cell_x`, `cell_y`
|
|
||||||
|
|
||||||
Both exist because `grid_pos` is the constructor parameter name and `cell_pos` is more descriptive. **Recommendation**: Keep both but document `grid_pos` as the canonical name (matches constructor).
|
|
||||||
|
|
||||||
### F4: Grid `position` alias
|
|
||||||
|
|
||||||
`Grid.position` is a redundant alias for `Grid.pos`. All other types use only `pos`. **Recommendation**: Deprecate `position`, keep `pos`.
|
|
||||||
|
|
||||||
### F5: Iterator type naming
|
|
||||||
|
|
||||||
- `UICollectionIter` -- has "UI" prefix
|
|
||||||
- `UIEntityCollectionIter` -- has "UI" prefix
|
|
||||||
- `EntityCollection3DIter` -- no "UI" prefix
|
|
||||||
|
|
||||||
The "UI" prefix is an internal detail leaking into type names. Since these are internal types (not exported), this is cosmetic but worth noting.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Findings: Missing Functionality
|
|
||||||
|
|
||||||
### F6: No `__eq__` on Color
|
|
||||||
|
|
||||||
`Color` has `__hash__` but no `__eq__`/`__ne__`. Two colors with the same RGBA values may not compare equal. This is a bug.
|
|
||||||
|
|
||||||
### F7: No `Music.pitch`
|
|
||||||
|
|
||||||
`Sound` has a `pitch` property but `Music` does not, despite SFML supporting it. Minor omission.
|
|
||||||
|
|
||||||
### F8: No `Font` methods
|
|
||||||
|
|
||||||
`Font` has no methods at all -- not even a way to query available sizes or get text metrics. This limits text layout capabilities.
|
|
||||||
|
|
||||||
### F9: GridPoint has no `__init__`
|
|
||||||
|
|
||||||
`GridPoint` cannot be constructed from Python (`tp_new = NULL`). This is intentional (it's a view into grid data) but should be clearly documented.
|
|
||||||
|
|
||||||
### F10: Animation direct construction deprecated but not marked
|
|
||||||
|
|
||||||
The `Animation` class can still be instantiated directly even though `.animate()` on drawables is preferred. No deprecation warning is emitted.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Findings: Deprecations to Resolve
|
|
||||||
|
|
||||||
### F11: `sprite_number` on Sprite and Entity
|
|
||||||
|
|
||||||
Both types expose `sprite_number` as a deprecated alias for `sprite_index`. This should be removed before 1.0.
|
|
||||||
|
|
||||||
### F12: `setScale` module function
|
|
||||||
|
|
||||||
Deprecated in favor of `Window.resolution`. Should be removed before 1.0.
|
|
||||||
|
|
||||||
### F13: Legacy string enum comparisons
|
|
||||||
|
|
||||||
`Key`, `InputState`, `MouseButton` support comparing to legacy string values (e.g., `Key.ESCAPE == "Escape"`, `InputState.PRESSED == "start"`). This backward compatibility layer should be removed before 1.0.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Findings: Documentation Gaps
|
|
||||||
|
|
||||||
### F14: Terse docstrings on core types
|
|
||||||
|
|
||||||
Several types have placeholder-quality `tp_doc` strings:
|
|
||||||
|
|
||||||
| Type | Current tp_doc | Should be |
|
|
||||||
|------|---------------|-----------|
|
|
||||||
| `Vector` | `"SFML Vector Object"` | Full constructor docs with args |
|
|
||||||
| `Font` | `"SFML Font Object"` | Full constructor docs |
|
|
||||||
| `Texture` | `"SFML Texture Object"` | Full constructor docs |
|
|
||||||
| `GridPoint` | `"UIGridPoint object"` | Description of purpose and access pattern |
|
|
||||||
| `GridPointState` | `"UIGridPointState object"` | Description of purpose |
|
|
||||||
|
|
||||||
### F15: Missing MCRF_* macro usage
|
|
||||||
|
|
||||||
Some types use raw string docstrings for methods instead of MCRF_METHOD macros. This means the documentation pipeline may miss them.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recommendations
|
|
||||||
|
|
||||||
### Before 1.0 (Breaking Changes)
|
|
||||||
|
|
||||||
1. **Remove camelCase functions**: `setScale`, `findAll`, `getMetrics`, `setDevConsole`
|
|
||||||
2. **Remove `sprite_number`** deprecated alias from Sprite and Entity
|
|
||||||
3. **Remove legacy string enum comparisons** from Key, InputState, MouseButton
|
|
||||||
4. **Remove `Grid.position`** redundant alias (keep `pos`)
|
|
||||||
5. **Add `__eq__`/`__ne__` to Color** type
|
|
||||||
|
|
||||||
### Immediate (Non-Breaking)
|
|
||||||
|
|
||||||
1. **Add snake_case aliases** for the 4 camelCase module functions
|
|
||||||
2. **Improve docstrings** on Vector, Font, Texture, GridPoint, GridPointState
|
|
||||||
3. **Document `grid_pos` vs `cell_pos`** -- state that `grid_pos` is canonical
|
|
||||||
|
|
||||||
### Future Considerations
|
|
||||||
|
|
||||||
1. Add `pitch` to `Music`
|
|
||||||
2. Add basic text metrics to `Font`
|
|
||||||
3. Consider deprecation warnings for `Animation()` direct construction
|
|
||||||
4. Unify iterator type naming (remove "UI" prefix from internal types)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Statistics
|
|
||||||
|
|
||||||
| Category | Count |
|
|
||||||
|----------|-------|
|
|
||||||
| Exported types | 46 |
|
|
||||||
| Internal types | 14 |
|
|
||||||
| Enums | 10 |
|
|
||||||
| Module functions | 13 |
|
|
||||||
| Module properties | 7 |
|
|
||||||
| Singletons | 5 |
|
|
||||||
| **Total public API surface** | **~93 named items** |
|
|
||||||
| Naming inconsistencies found | 5 |
|
|
||||||
| Missing functionality items | 5 |
|
|
||||||
| Deprecations to resolve | 3 |
|
|
||||||
| Documentation gaps | 2 |
|
|
||||||
| **Total findings** | **15** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## #314 Freeze Decisions (2026-06, recorded before generating the API-surface snapshot)
|
|
||||||
|
|
||||||
The April audit body above is partly stale. Live introspection of the built module gives the
|
|
||||||
authoritative counts, and the snapshot test (`tests/unit/api_surface_snapshot_test.py`) is built
|
|
||||||
from live introspection, not from this document.
|
|
||||||
|
|
||||||
**Corrected live counts (2026-06):** 12 enums (audit said 10 — adds `Perspective` #294 and
|
|
||||||
`Heuristic` #315), 46 exported classes, 12 module functions, 7 singletons/constants (incl. the
|
|
||||||
`automation` submodule), 1 submodule. `GridPointState` was removed in #294 (its F14 row is moot).
|
|
||||||
|
|
||||||
**Per-finding final status:** F1, F4, F6, F10, F11, F13, F14 = RESOLVED (verified in source via
|
|
||||||
#304–#308). F2 = correct-by-design (docs-only). F5, F9 = cosmetic, unchanged. F7 (`Music.pitch`),
|
|
||||||
F8 (`Font` methods) = Future, explicitly NOT 1.0 blockers.
|
|
||||||
|
|
||||||
**Decisions locked for the freeze (the snapshot golden enshrines these):**
|
|
||||||
|
|
||||||
- **F3 (cell-position canonical name):** `grid_pos` is canonical (matches the `grid_pos=`
|
|
||||||
constructor argument); `cell_pos`/`cell_x`/`cell_y` are documented aliases. Both share the same
|
|
||||||
getter/setter and remain interchangeable. Docstrings aligned at `src/UIEntity.cpp` getsetters.
|
|
||||||
- **F12 (`set_scale`):** KEPT in the 1.0 surface as a documented-deprecated function. Removing it
|
|
||||||
now would itself be a new breaking change; the snapshot locks it in.
|
|
||||||
- **`mcrfpy.automation`:** PyAutoGUI-compatibility camelCase (`moveRel`/`dragTo`/etc.) is EXEMPT
|
|
||||||
from the snake_case rule. The snapshot records it in a clearly-labeled section.
|
|
||||||
- **`entity.texture` (new in #313):** additive read/write property; getter returns the entity's
|
|
||||||
real texture, `None` only in the degenerate (default_texture-null) case — never re-derefs a null
|
|
||||||
default_texture. Added to the frozen contract + stubs + docs when #313 lands (golden gains exactly
|
|
||||||
one line). Known edges, frozen as-is (2026-06-11 adversarial review): the getter mints a NEW
|
|
||||||
Texture wrapper per access and Texture has no `__eq__`, so `e.texture == e.texture` is False —
|
|
||||||
compare `.source`/sprite dims instead (same behavior as the pre-existing `Sprite.texture`);
|
|
||||||
setting does not re-validate `sprite_index` against the new atlas; setter rejects non-Texture
|
|
||||||
(TypeError), null-data Texture wrappers (ValueError, mirrors `Sprite.texture`), and deletion.
|
|
||||||
|
|
||||||
**1.0 freeze scope — class classification** (the snapshot segregates FROZEN vs EXPERIMENTAL):
|
|
||||||
|
|
||||||
- **FROZEN (stable 1.0):** core value types (Color, Vector, Font, Texture), UI drawables
|
|
||||||
(Drawable [root], Frame, Caption, Sprite, Line, Circle, Arc), Grid/GridView/Entity, Scene,
|
|
||||||
Window, Timer, Keyboard, Mouse, audio (Sound, SoundBuffer, Music), procgen (BSP, HeightMap,
|
|
||||||
NoiseSource, DijkstraMap, AStarPath), all 12 enums, the snake_case module functions, and the
|
|
||||||
singletons. (Provisionally frozen, flagged for confirmation at golden review: `ColorLayer`,
|
|
||||||
`TileLayer` [core grid layers, distinct from Tiled import], `DiscreteMap` [#293/#294 grid data].)
|
|
||||||
- **EXPERIMENTAL (exempt, may change post-1.0):** 3D/Voxel (Billboard, Entity3D,
|
|
||||||
EntityCollection3D[Iter], Model3D, Viewport3D, VoxelGrid, VoxelRegion), Tiled import (TileSetFile,
|
|
||||||
TileMapFile, WangSet), LDtk import (LdtkProject, AutoRuleSet), Shader, and binding helpers
|
|
||||||
(CallableBinding, PropertyBinding).
|
|
||||||
|
|
||||||
The snapshot test FAILS on any exported class not classified (forces a deliberate decision for
|
|
||||||
future additions).
|
|
||||||
|
|
@ -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 |