Compare commits
4 commits
master
...
standardiz
| Author | SHA1 | Date | |
|---|---|---|---|
| 5edebdd643 | |||
| c13e185289 | |||
| 8871f6be6e | |||
| 1c12e8719c |
|
|
@ -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
|
||||
50
.gitignore
vendored
|
|
@ -7,52 +7,6 @@ PCbuild
|
|||
.vs
|
||||
obj
|
||||
build
|
||||
/lib
|
||||
__pycache__
|
||||
lib
|
||||
obj
|
||||
|
||||
# unimportant files that won't pass clean dir check
|
||||
build*
|
||||
docs
|
||||
.claude
|
||||
my_games
|
||||
|
||||
# images are produced by many tests
|
||||
*.png
|
||||
|
||||
# WASM stdlib for Emscripten build
|
||||
!wasm_stdlib/
|
||||
|
||||
.cache/
|
||||
7DRL2025 Release/
|
||||
CMakeFiles/
|
||||
Makefile
|
||||
*.zip
|
||||
__lib/
|
||||
__lib_debug/
|
||||
__lib_windows/
|
||||
build-windows/
|
||||
build_windows/
|
||||
_oldscripts/
|
||||
|
||||
# Audit tooling virtualenv (tools/audit_pymethoddef.py)
|
||||
.venv-audit/
|
||||
assets/
|
||||
cellular_automata_fire/
|
||||
deps/
|
||||
fetch_issues_txt.py
|
||||
forest_fire_CA.py
|
||||
mcrogueface.github.io
|
||||
scripts/
|
||||
tcod_reference
|
||||
.archive
|
||||
.mcp.json
|
||||
dist/
|
||||
|
||||
# Keep important documentation and tests
|
||||
!CLAUDE.md
|
||||
!README.md
|
||||
!tests/
|
||||
|
||||
# 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"]
|
||||
path = modules/SFML
|
||||
url = git@github.com:SFML/SFML.git
|
||||
[submodule "modules/libtcod-headless"]
|
||||
path = modules/libtcod-headless
|
||||
url = git@github.com:jmccardle/libtcod-headless.git
|
||||
branch = 2.2.1-headless
|
||||
[submodule "modules/RapidXML"]
|
||||
path = modules/RapidXML
|
||||
url = https://github.com/Fe-Bell/RapidXML
|
||||
[submodule "modules/json"]
|
||||
path = modules/json
|
||||
url = git@github.com:nlohmann/json.git
|
||||
[submodule "modules/libtcod"]
|
||||
path = modules/libtcod
|
||||
url = git@github.com:libtcod/libtcod.git
|
||||
|
|
|
|||
|
|
@ -1,306 +0,0 @@
|
|||
# Building McRogueFace from Source
|
||||
|
||||
This document describes how to build McRogueFace from a fresh clone.
|
||||
|
||||
## Build Options
|
||||
|
||||
There are two ways to build McRogueFace:
|
||||
|
||||
1. **Quick Build** (recommended): Use pre-built dependency libraries from a `build_deps` archive
|
||||
2. **Full Build**: Compile all dependencies from submodules
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### System Dependencies
|
||||
|
||||
Install these packages before building:
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install \
|
||||
build-essential \
|
||||
cmake \
|
||||
git \
|
||||
zlib1g-dev \
|
||||
libx11-dev \
|
||||
libxrandr-dev \
|
||||
libxcursor-dev \
|
||||
libfreetype-dev \
|
||||
libudev-dev \
|
||||
libvorbis-dev \
|
||||
libflac-dev \
|
||||
libgl-dev \
|
||||
libopenal-dev
|
||||
```
|
||||
|
||||
**Note:** SDL is NOT required - McRogueFace uses libtcod-headless which has no SDL dependency.
|
||||
|
||||
---
|
||||
|
||||
## Option 1: Quick Build (Using Pre-built Dependencies)
|
||||
|
||||
If you have a `build_deps.tar.gz` or `build_deps.zip` archive:
|
||||
|
||||
```bash
|
||||
# Clone McRogueFace (no submodules needed)
|
||||
git clone <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
|
||||
# Extract pre-built dependencies
|
||||
tar -xzf /path/to/build_deps.tar.gz
|
||||
# Or for zip: unzip /path/to/build_deps.zip
|
||||
|
||||
# Build McRogueFace
|
||||
mkdir -p build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# Run
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
The `build_deps` archive contains:
|
||||
- `__lib/` - Pre-built shared libraries (Python, SFML, libtcod-headless)
|
||||
- `deps/` - Header symlinks for compilation
|
||||
|
||||
**Total build time: ~30 seconds**
|
||||
|
||||
---
|
||||
|
||||
## Option 2: Full Build (Compiling All Dependencies)
|
||||
|
||||
### 1. Clone with Submodules
|
||||
|
||||
```bash
|
||||
git clone --recursive <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
```
|
||||
|
||||
If submodules weren't cloned:
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
**Note:** imgui/imgui-sfml submodules may fail - this is fine, they're not used.
|
||||
|
||||
### 2. Create Dependency Symlinks
|
||||
|
||||
```bash
|
||||
cd deps
|
||||
ln -sf ../modules/cpython cpython
|
||||
ln -sf ../modules/libtcod-headless/src/libtcod libtcod
|
||||
ln -sf ../modules/cpython/Include Python
|
||||
ln -sf ../modules/SFML/include/SFML SFML
|
||||
cd ..
|
||||
```
|
||||
|
||||
### 3. Build libtcod-headless
|
||||
|
||||
libtcod-headless is our SDL-free fork with vendored dependencies:
|
||||
|
||||
```bash
|
||||
cd modules/libtcod-headless
|
||||
mkdir build && cd build
|
||||
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=ON
|
||||
|
||||
make -j$(nproc)
|
||||
cd ../../..
|
||||
```
|
||||
|
||||
That's it! No special flags needed - libtcod-headless defaults to:
|
||||
- `LIBTCOD_SDL3=disable` (no SDL dependency)
|
||||
- Vendored lodepng, utf8proc, stb
|
||||
|
||||
### 4. Build Python 3.12
|
||||
|
||||
```bash
|
||||
cd modules/cpython
|
||||
./configure --enable-shared
|
||||
make -j$(nproc)
|
||||
cd ../..
|
||||
```
|
||||
|
||||
### 5. Build SFML 2.6
|
||||
|
||||
```bash
|
||||
cd modules/SFML
|
||||
mkdir build && cd build
|
||||
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=ON
|
||||
|
||||
make -j$(nproc)
|
||||
cd ../../..
|
||||
```
|
||||
|
||||
### 6. Copy Libraries
|
||||
|
||||
```bash
|
||||
mkdir -p __lib
|
||||
|
||||
# Python
|
||||
cp modules/cpython/libpython3.12.so* __lib/
|
||||
|
||||
# SFML
|
||||
cp modules/SFML/build/lib/libsfml-*.so* __lib/
|
||||
|
||||
# libtcod-headless
|
||||
cp modules/libtcod-headless/build/bin/libtcod.so* __lib/
|
||||
|
||||
# Python standard library
|
||||
cp -r modules/cpython/Lib __lib/Python
|
||||
```
|
||||
|
||||
### 7. Build McRogueFace
|
||||
|
||||
```bash
|
||||
mkdir -p build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
```
|
||||
|
||||
### 8. Run
|
||||
|
||||
```bash
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Submodule Versions
|
||||
|
||||
| Submodule | Version | Notes |
|
||||
|-----------|---------|-------|
|
||||
| SFML | 2.6.1 | Graphics, audio, windowing |
|
||||
| cpython | 3.12.2 | Embedded Python interpreter |
|
||||
| libtcod-headless | 2.2.1 | SDL-free fork for FOV, pathfinding |
|
||||
|
||||
---
|
||||
|
||||
## Creating a build_deps Archive
|
||||
|
||||
To create a `build_deps` archive for distribution:
|
||||
|
||||
```bash
|
||||
cd McRogueFace
|
||||
|
||||
# Create archive directory
|
||||
mkdir -p build_deps_staging
|
||||
|
||||
# Copy libraries
|
||||
cp -r __lib build_deps_staging/
|
||||
|
||||
# Copy/create deps symlinks as actual directories with only needed headers
|
||||
mkdir -p build_deps_staging/deps
|
||||
cp -rL deps/libtcod build_deps_staging/deps/ # Follow symlink
|
||||
cp -rL deps/Python build_deps_staging/deps/
|
||||
cp -rL deps/SFML build_deps_staging/deps/
|
||||
cp -r deps/platform build_deps_staging/deps/
|
||||
|
||||
# Create archives
|
||||
cd build_deps_staging
|
||||
tar -czf ../build_deps.tar.gz __lib deps
|
||||
zip -r ../build_deps.zip __lib deps
|
||||
cd ..
|
||||
|
||||
# Cleanup
|
||||
rm -rf build_deps_staging
|
||||
```
|
||||
|
||||
The resulting archive can be distributed alongside releases for users who want to build McRogueFace without compiling dependencies.
|
||||
|
||||
**Archive contents:**
|
||||
```
|
||||
build_deps.tar.gz
|
||||
├── __lib/
|
||||
│ ├── libpython3.12.so*
|
||||
│ ├── libsfml-*.so*
|
||||
│ ├── libtcod.so*
|
||||
│ └── Python/ # Python standard library
|
||||
└── deps/
|
||||
├── libtcod/ # libtcod headers
|
||||
├── Python/ # Python headers
|
||||
├── SFML/ # SFML headers
|
||||
└── platform/ # Platform-specific configs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verify the Build
|
||||
|
||||
```bash
|
||||
cd build
|
||||
|
||||
# Check version
|
||||
./mcrogueface --version
|
||||
|
||||
# Test headless mode
|
||||
./mcrogueface --headless -c "import mcrfpy; print('Success')"
|
||||
|
||||
# Verify no SDL dependencies
|
||||
ldd mcrogueface | grep -i sdl # Should output nothing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### OpenAL not found
|
||||
```bash
|
||||
sudo apt install libopenal-dev
|
||||
```
|
||||
|
||||
### FreeType not found
|
||||
```bash
|
||||
sudo apt install libfreetype-dev
|
||||
```
|
||||
|
||||
### X11/Xrandr not found
|
||||
```bash
|
||||
sudo apt install libx11-dev libxrandr-dev
|
||||
```
|
||||
|
||||
### Python standard library missing
|
||||
Ensure `__lib/Python` contains the standard library:
|
||||
```bash
|
||||
ls __lib/Python/os.py # Should exist
|
||||
```
|
||||
|
||||
### libtcod symbols not found
|
||||
Ensure libtcod.so is in `__lib/` with correct version:
|
||||
```bash
|
||||
ls -la __lib/libtcod.so*
|
||||
# Should show libtcod.so -> libtcod.so.2 -> libtcod.so.2.2.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Times (approximate)
|
||||
|
||||
On a typical 4-core system:
|
||||
|
||||
| Component | Time |
|
||||
|-----------|------|
|
||||
| libtcod-headless | ~30 seconds |
|
||||
| Python 3.12 | ~3-5 minutes |
|
||||
| SFML 2.6 | ~1 minute |
|
||||
| McRogueFace | ~30 seconds |
|
||||
| **Full build total** | **~5-7 minutes** |
|
||||
| **Quick build (pre-built deps)** | **~30 seconds** |
|
||||
|
||||
---
|
||||
|
||||
## Runtime Dependencies
|
||||
|
||||
The built executable requires these system libraries:
|
||||
- `libz.so.1` (zlib)
|
||||
- `libopenal.so.1` (OpenAL)
|
||||
- `libX11.so.6`, `libXrandr.so.2` (X11)
|
||||
- `libfreetype.so.6` (FreeType)
|
||||
- `libGL.so.1` (OpenGL)
|
||||
|
||||
All other dependencies (Python, SFML, libtcod) are bundled in `lib/`.
|
||||
585
CMakeLists.txt
|
|
@ -8,518 +8,49 @@ project(McRogueFace)
|
|||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
# Headless build option (no SFML, no graphics - for server/testing/Emscripten prep)
|
||||
option(MCRF_HEADLESS "Build without graphics dependencies (SFML, ImGui)" OFF)
|
||||
|
||||
# SDL2 backend option (SDL2 + OpenGL ES 2 - for Emscripten/WebGL, Android, cross-platform)
|
||||
option(MCRF_SDL2 "Build with SDL2+OpenGL ES 2 backend instead of SFML" OFF)
|
||||
|
||||
# Playground mode - minimal scripts for web playground (REPL-focused)
|
||||
option(MCRF_PLAYGROUND "Build with minimal playground scripts instead of full game" OFF)
|
||||
|
||||
# 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
|
||||
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps)
|
||||
include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/deps/libtcod)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/3d)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/platform)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/tiled)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/ldtk)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/audio)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/modules/RapidXML)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/modules/json/single_include)
|
||||
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux/Python-3.11.1)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
|
||||
|
||||
# Python includes: use different paths for Windows vs Linux vs Emscripten
|
||||
if(EMSCRIPTEN)
|
||||
# Emscripten build: use Python headers compiled for wasm32-emscripten
|
||||
# The pyconfig.h from cross-build has correct LONG_BIT and other settings
|
||||
set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python")
|
||||
# Force-include wasm pyconfig.h BEFORE anything else to set correct platform defines
|
||||
add_compile_options(-include ${PYTHON_WASM_BUILD}/pyconfig.h)
|
||||
# Override LONG_BIT - Emscripten's limits.h incorrectly defines it as 64 for wasm32
|
||||
add_compile_definitions(LONG_BIT=32)
|
||||
# Include wasm build directory FIRST so its pyconfig.h is found by #include "pyconfig.h"
|
||||
include_directories(BEFORE ${PYTHON_WASM_BUILD})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/Include)
|
||||
message(STATUS "Using Emscripten Python from: ${PYTHON_WASM_BUILD}")
|
||||
elseif(MCRF_CROSS_WINDOWS)
|
||||
# Windows cross-compilation: use cpython headers with PC/pyconfig.h
|
||||
# Problem: Python.h uses #include "pyconfig.h" which finds Include/pyconfig.h (Linux) first
|
||||
# Solution: Use -include to force Windows pyconfig.h to be included first
|
||||
# This defines MS_WINDOWS before Python.h is processed, ensuring correct struct layouts
|
||||
add_compile_options(-include ${CMAKE_SOURCE_DIR}/deps/cpython/PC/pyconfig.h)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/Include)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/PC) # For other Windows-specific headers
|
||||
# Also include SFML and libtcod Windows headers
|
||||
include_directories(${CMAKE_SOURCE_DIR}/__lib_windows/sfml/include)
|
||||
include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/include)
|
||||
else()
|
||||
# Native builds (Linux/Windows): use existing Python setup
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
|
||||
endif()
|
||||
|
||||
# ImGui and ImGui-SFML include directories (not needed in headless or SDL2 mode)
|
||||
# SDL2 builds will use ImGui with SDL2 backend later; for now, no ImGui
|
||||
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/modules/imgui)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/modules/imgui-sfml)
|
||||
|
||||
# ImGui source files
|
||||
set(IMGUI_SOURCES
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui/imgui.cpp
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_draw.cpp
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_tables.cpp
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_widgets.cpp
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui-sfml/imgui-SFML.cpp
|
||||
)
|
||||
endif()
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
|
||||
|
||||
# Collect all the source files
|
||||
file(GLOB_RECURSE SOURCES "src/*.cpp")
|
||||
|
||||
# Add ImGui sources to the build (only if using SFML)
|
||||
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
|
||||
list(APPEND SOURCES ${IMGUI_SOURCES})
|
||||
# Add GLAD for OpenGL function loading (needed for 3D rendering on SFML)
|
||||
list(APPEND SOURCES "${CMAKE_SOURCE_DIR}/src/3d/glad.c")
|
||||
endif()
|
||||
|
||||
# Find OpenGL (required by ImGui-SFML) - not needed in headless mode
|
||||
# SDL2 builds handle OpenGL ES 2 differently (via SDL2 or Emscripten)
|
||||
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
|
||||
if(MCRF_CROSS_WINDOWS)
|
||||
# For cross-compilation, OpenGL is provided by MinGW
|
||||
set(OPENGL_LIBRARIES opengl32)
|
||||
else()
|
||||
find_package(OpenGL REQUIRED)
|
||||
set(OPENGL_LIBRARIES OpenGL::GL)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Create a list of libraries to link against
|
||||
if(EMSCRIPTEN)
|
||||
# Emscripten build: link against WASM-compiled Python and libtcod
|
||||
set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python")
|
||||
set(PYTHON_WASM_PREFIX "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/prefix")
|
||||
set(LIBTCOD_WASM_BUILD "${CMAKE_SOURCE_DIR}/modules/libtcod-headless/build-emscripten")
|
||||
# Collect HACL crypto object files (not included in libpython3.14.a)
|
||||
file(GLOB PYTHON_HACL_OBJECTS "${PYTHON_WASM_BUILD}/Modules/_hacl/*.o")
|
||||
set(LINK_LIBS
|
||||
${PYTHON_WASM_BUILD}/libpython3.14.a
|
||||
${PYTHON_HACL_OBJECTS}
|
||||
${PYTHON_WASM_BUILD}/Modules/expat/libexpat.a
|
||||
${PYTHON_WASM_PREFIX}/lib/libmpdec.a
|
||||
${PYTHON_WASM_PREFIX}/lib/libffi.a
|
||||
${LIBTCOD_WASM_BUILD}/libtcod.a
|
||||
${LIBTCOD_WASM_BUILD}/_deps/lodepng-c-build/liblodepng-c.a
|
||||
${LIBTCOD_WASM_BUILD}/_deps/utf8proc-build/libutf8proc.a)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux) # Use Linux platform stubs for now
|
||||
# For SDL2 builds, add stb headers for image/font loading
|
||||
if(MCRF_SDL2)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/stb)
|
||||
endif()
|
||||
message(STATUS "Linking Emscripten Python: ${PYTHON_WASM_BUILD}/libpython3.14.a")
|
||||
message(STATUS "Linking Emscripten libtcod: ${LIBTCOD_WASM_BUILD}/libtcod.a")
|
||||
elseif(MCRF_SDL2)
|
||||
# SDL2 build (non-Emscripten): link against SDL2 and system libraries
|
||||
# Note: For desktop SDL2 builds in the future
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
set(LINK_LIBS
|
||||
SDL2::SDL2
|
||||
OpenGL::GL
|
||||
tcod
|
||||
python3.14
|
||||
m dl util pthread)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/stb) # stb_image.h, stb_truetype.h
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
message(STATUS "Building with SDL2 backend (desktop)")
|
||||
elseif(MCRF_HEADLESS)
|
||||
# Headless build: no SFML, no OpenGL
|
||||
if(WIN32 OR MCRF_CROSS_WINDOWS)
|
||||
set(LINK_LIBS
|
||||
libtcod
|
||||
python314)
|
||||
if(MCRF_CROSS_WINDOWS)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/lib)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows)
|
||||
else()
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
endif()
|
||||
else()
|
||||
# Unix/Linux headless build
|
||||
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
|
||||
)
|
||||
set(LINK_LIBS
|
||||
m
|
||||
dl
|
||||
util
|
||||
pthread
|
||||
python3.12
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod)
|
||||
|
||||
# On Windows, add any additional libs and include directories
|
||||
if(WIN32)
|
||||
# Add the necessary Windows-specific libraries and include directories
|
||||
# include_directories(path_to_additional_includes)
|
||||
# link_directories(path_to_additional_libs)
|
||||
# list(APPEND LINK_LIBS additional_windows_libs)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
|
||||
# Link directories for cross-compiled Windows libs
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/sfml/lib)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/lib)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows)
|
||||
elseif(WIN32)
|
||||
# Native Windows build (MSVC)
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod
|
||||
python314
|
||||
${OPENGL_LIBRARIES})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
else()
|
||||
# Unix/Linux build
|
||||
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
|
||||
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)
|
||||
if(MCRF_DEBUG_PYTHON OR MCRF_FREE_THREADED_PYTHON)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_debug)
|
||||
endif()
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
endif()
|
||||
|
||||
# Add the directory where the linker should look for the libraries
|
||||
#link_directories(${CMAKE_SOURCE_DIR}/deps_linux)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/lib)
|
||||
|
||||
# Define the executable target before linking libraries
|
||||
add_executable(mcrogueface ${SOURCES})
|
||||
|
||||
# Define NO_SDL for libtcod-headless headers (excludes SDL-dependent code)
|
||||
# We ALWAYS need this because libtcod headers expect SDL3, not SDL2
|
||||
# Our SDL2 backend is separate from libtcod's SDL3 renderer
|
||||
target_compile_definitions(mcrogueface PRIVATE NO_SDL)
|
||||
|
||||
# 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()
|
||||
|
||||
# Now the linker will find the libraries in the specified directory
|
||||
target_link_libraries(mcrogueface ${LINK_LIBS})
|
||||
|
||||
|
|
@ -534,63 +65,11 @@ add_custom_command(TARGET mcrogueface POST_BUILD
|
|||
${CMAKE_SOURCE_DIR}/src/scripts $<TARGET_FILE_DIR:mcrogueface>/scripts)
|
||||
|
||||
# Copy Python standard library to build directory
|
||||
if(MCRF_DEBUG_PYTHON)
|
||||
# Copy all libs first (SFML, libtcod, Python stdlib), then overwrite with debug Python
|
||||
# The debug lib has SONAME libpython3.14d.so.1.0, so we need both names
|
||||
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()
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/lib $<TARGET_FILE_DIR:mcrogueface>/lib)
|
||||
|
||||
# On Windows, copy DLLs to executable directory
|
||||
if(MCRF_CROSS_WINDOWS)
|
||||
# Cross-compilation: copy DLLs from __lib_windows
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/sfml/bin $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/bin $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/python314.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/python3.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/vcruntime140.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/vcruntime140_1.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
/usr/x86_64-w64-mingw32/lib/libwinpthread-1.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Copied Windows DLLs to executable directory")
|
||||
|
||||
# Copy Python standard library zip
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/python314.zip $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Copied Python stdlib")
|
||||
elseif(WIN32)
|
||||
# Native Windows build: copy DLLs from __lib
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Copied DLLs to executable directory")
|
||||
endif()
|
||||
|
||||
# rpath for including shared libraries (Linux/Unix only)
|
||||
if(NOT WIN32)
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
INSTALL_RPATH "$ORIGIN/./lib")
|
||||
endif()
|
||||
# rpath for including shared libraries
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
INSTALL_RPATH "./lib")
|
||||
|
||||
|
|
|
|||
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
|
||||
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
|
||||
242
README.md
|
|
@ -1,226 +1,30 @@
|
|||
# McRogueFace
|
||||
# McRogueFace - 2D Game Engine
|
||||
|
||||
An experimental prototype game engine built for my own use in 7DRL 2023.
|
||||
|
||||
*Blame my wife for the name*
|
||||
|
||||
A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML.
|
||||
## Tenets:
|
||||
|
||||
* Core roguelike logic from libtcod: field of view, pathfinding
|
||||
* Animate sprites with multiple frames. Smooth transitions for positions, sizes, zoom, and camera
|
||||
* Simple GUI element system allows keyboard and mouse input, composition
|
||||
* No compilation or installation necessary. The runtime is a full Python environment; "Zip And Ship"
|
||||
* C++ first, Python close behind.
|
||||
* Entity-Component system based on David Churchill's Memorial University COMP4300 course lectures available on Youtube.
|
||||
* Graphics, particles and shaders provided by SFML.
|
||||
* Pathfinding, noise generation, and other Roguelike goodness provided by TCOD.
|
||||
|
||||
📖 **[Full Documentation & Tutorials](https://mcrogueface.github.io/)** - Quickstart guide, API reference, and cookbook
|
||||
## Why?
|
||||
|
||||
## Quick Start
|
||||
I did the r/RoguelikeDev TCOD tutorial in Python. I loved it, but I did not want to be limited to ASCII. I want to be able to draw pixels on top of my tiles (like lines or circles) and eventually incorporate even more polish.
|
||||
|
||||
**Download** the [latest release](https://github.com/jmccardle/McRogueFace/releases/latest):
|
||||
- **Windows**: `McRogueFace-*-Win.zip`
|
||||
- **Linux**: `McRogueFace-*-Linux.tar.bz2`
|
||||
## To-do
|
||||
|
||||
Extract and run `mcrogueface` (or `mcrogueface.exe` on Windows) to see the demo game.
|
||||
|
||||
### Your First Game
|
||||
|
||||
Create `scripts/game.py` (or edit the existing one):
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Create and activate a scene
|
||||
scene = mcrfpy.Scene("game")
|
||||
scene.activate()
|
||||
|
||||
# Load a sprite sheet
|
||||
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
|
||||
# Create a tile grid
|
||||
grid = mcrfpy.Grid(grid_size=(20, 15), texture=texture, pos=(50, 50), size=(640, 480))
|
||||
grid.zoom = 2.0
|
||||
scene.children.append(grid)
|
||||
|
||||
# Add a player entity
|
||||
player = mcrfpy.Entity(pos=(10, 7), texture=texture, sprite_index=84)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Handle keyboard input
|
||||
def on_key(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
x, y = int(player.x), int(player.y)
|
||||
if key == "W": y -= 1
|
||||
elif key == "S": y += 1
|
||||
elif key == "A": x -= 1
|
||||
elif key == "D": x += 1
|
||||
player.x, player.y = x, y
|
||||
|
||||
scene.on_key = on_key
|
||||
```
|
||||
|
||||
Run `mcrogueface` and you have a movable character!
|
||||
|
||||
### Visual Framework
|
||||
|
||||
- **Sprite**: Single image or sprite from a shared sheet
|
||||
- **Caption**: Text rendering with fonts
|
||||
- **Frame**: Container rectangle for composing UIs
|
||||
- **Grid**: 2D tile array with zoom and camera control
|
||||
- **Entity**: Grid-based game object with sprite and pathfinding
|
||||
- **Animation**: Interpolate any property over time with easing
|
||||
|
||||
## Building from Source
|
||||
|
||||
For most users, pre-built releases are available. If you need to build from source:
|
||||
|
||||
### Quick Build (with pre-built dependencies)
|
||||
|
||||
Download `build_deps.tar.gz` from the releases page, then:
|
||||
|
||||
```bash
|
||||
git clone <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
tar -xzf /path/to/build_deps.tar.gz
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
```
|
||||
|
||||
### Full Build (compiling all dependencies)
|
||||
|
||||
```bash
|
||||
git clone --recursive <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
# See BUILD_FROM_SOURCE.md for complete instructions
|
||||
```
|
||||
|
||||
**[BUILD_FROM_SOURCE.md](BUILD_FROM_SOURCE.md)** - Complete build guide including:
|
||||
- System dependency installation
|
||||
- Compiling SFML, Python, and libtcod-headless from source
|
||||
- Creating `build_deps` archives for distribution
|
||||
- Troubleshooting common build issues
|
||||
|
||||
### System Requirements
|
||||
|
||||
- **Linux**: Debian/Ubuntu tested; other distros should work
|
||||
- **Windows**: Supported (see build guide for details)
|
||||
- **macOS**: Untested
|
||||
|
||||
## Example: Main Menu with Buttons
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Create a scene
|
||||
scene = mcrfpy.Scene("menu")
|
||||
|
||||
# Add a background frame
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
|
||||
fill_color=mcrfpy.Color(20, 20, 40))
|
||||
scene.children.append(bg)
|
||||
|
||||
# Add a title
|
||||
title = mcrfpy.Caption(pos=(312, 100), text="My Roguelike",
|
||||
fill_color=mcrfpy.Color(255, 255, 100))
|
||||
title.font_size = 48
|
||||
scene.children.append(title)
|
||||
|
||||
# Create a button
|
||||
button = mcrfpy.Frame(pos=(362, 300), size=(300, 80),
|
||||
fill_color=mcrfpy.Color(50, 150, 50))
|
||||
button_text = mcrfpy.Caption(pos=(90, 25), text="Start Game")
|
||||
button.children.append(button_text)
|
||||
|
||||
def on_click(x, y, btn):
|
||||
print("Game starting!")
|
||||
|
||||
button.on_click = on_click
|
||||
scene.children.append(button)
|
||||
|
||||
scene.activate()
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### 📚 Developer Documentation
|
||||
|
||||
For comprehensive documentation about systems, architecture, and development workflows:
|
||||
|
||||
**[Project Wiki](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki)**
|
||||
|
||||
Key wiki pages:
|
||||
|
||||
- **[Home](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Home)** - Documentation hub with multiple entry points
|
||||
- **[Grid System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Grid-System)** - Three-layer grid architecture
|
||||
- **[Python Binding System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Python-Binding-System)** - C++/Python integration
|
||||
- **[Performance and Profiling](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Performance-and-Profiling)** - Optimization tools
|
||||
- **[Adding Python Bindings](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Adding-Python-Bindings)** - Step-by-step binding guide
|
||||
- **[Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap)** - All open issues organized by system
|
||||
|
||||
### 📖 Development Guides
|
||||
|
||||
In the repository root:
|
||||
|
||||
- **[CLAUDE.md](CLAUDE.md)** - Build instructions, testing guidelines, common tasks
|
||||
- **[ROADMAP.md](ROADMAP.md)** - Strategic vision and development phases
|
||||
- **[roguelike_tutorial/](roguelike_tutorial/)** - Complete roguelike tutorial implementations
|
||||
|
||||
## Build Requirements
|
||||
|
||||
- C++17 compiler (GCC 7+ or Clang 5+)
|
||||
- CMake 3.14+
|
||||
- Python 3.14 (embedded)
|
||||
- SFML 2.6
|
||||
- Linux or Windows (macOS untested)
|
||||
|
||||
See [BUILD_FROM_SOURCE.md](BUILD_FROM_SOURCE.md) for detailed compilation instructions.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
McRogueFace/
|
||||
├── assets/ # Sprites, fonts, audio
|
||||
├── build/ # Build output: this is what you distribute
|
||||
│ ├── assets/ # (copied from assets/)
|
||||
│ ├── scripts/ # (copied from src/scripts/)
|
||||
│ └── lib/ # Python stdlib and extension modules
|
||||
├── docs/ # Generated HTML, markdown API docs
|
||||
├── src/ # C++ engine source
|
||||
│ └── scripts/ # Python game scripts
|
||||
├── stubs/ # .pyi type stubs for IDE integration
|
||||
├── tests/ # Automated test suite
|
||||
└── tools/ # Documentation generation scripts
|
||||
```
|
||||
|
||||
If you are building McRogueFace to implement game logic or scene configuration in C++, you'll have to compile the project.
|
||||
|
||||
If you are writing a game in Python using McRogueFace, you only need to rename and zip/distribute the `build` directory.
|
||||
|
||||
## Philosophy
|
||||
|
||||
- **C++ every frame, Python every tick**: All rendering data is handled in C++. Structure your UI and program animations in Python, and they are rendered without Python. All game logic can be written in Python.
|
||||
- **No Compiling Required; Zip And Ship**: Implement your game objects with Python, zip up McRogueFace with your "game.py" to ship
|
||||
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod
|
||||
- **Hands-Off Testing**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration
|
||||
- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter
|
||||
|
||||
## Contributing
|
||||
|
||||
PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request.
|
||||
|
||||
### Issue Tracking
|
||||
|
||||
The project uses [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for task tracking and bug reports. Issues are organized with labels:
|
||||
|
||||
- **System labels** (grid, animation, python-binding, etc.) - identify which codebase area
|
||||
- **Priority labels** (tier1-active, tier2-foundation, tier3-future) - development timeline
|
||||
- **Type labels** (Major Feature, Minor Feature, Bugfix, etc.) - effort and scope
|
||||
|
||||
See the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap) on the wiki for organized view of all open tasks.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see LICENSE file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Developed for 7-Day Roguelike 2023, 2024, 2025, 2026 - here's to many more
|
||||
- Built with [SFML](https://www.sfml-dev.org/), [libtcod](https://github.com/libtcod/libtcod), and Python
|
||||
- Inspired by David Churchill's COMP4300 game engine lectures
|
||||
* ✅ Initial Commit
|
||||
* ✅ Integrate scene, action, entity, component system from COMP4300 engine
|
||||
* ✅ Windows / Visual Studio project
|
||||
* ✅ Draw Sprites
|
||||
* ✅ Play Sounds
|
||||
* ✅ Draw UI, spawn entity from Python code
|
||||
* ❌ Python AI for entities (NPCs on set paths, enemies towards player)
|
||||
* ✅ Walking / Collision
|
||||
* ❌ "Boards" (stairs / doors / walk off edge of screen)
|
||||
* ❌ Cutscenes - interrupt normal controls, text scroll, character portraits
|
||||
* ❌ Mouse integration - tooltips, zoom, click to select targets, cursors
|
||||
|
|
|
|||
138
ROADMAP.md
|
|
@ -1,138 +0,0 @@
|
|||
# McRogueFace - Development Roadmap
|
||||
|
||||
**Version**: 0.2.7-prerelease | **Era**: McRogueFace (2D roguelikes) -- on the road to 1.0
|
||||
|
||||
For detailed architecture, philosophy, and decision framework, see the [Strategic Direction](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Strategic-Direction) wiki page. For per-issue tracking, see the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap).
|
||||
|
||||
---
|
||||
|
||||
## What Has Shipped
|
||||
|
||||
**Alpha 0.1** (2024) -- First complete release. Milestone: all datatypes behaving.
|
||||
|
||||
**0.2 series** (Jan-Mar 2026) -- Weekly updates to GitHub. Key additions:
|
||||
- 3D/Voxel pipeline (experimental): Viewport3D, Camera3D, Entity3D, VoxelGrid with greedy meshing and serialization
|
||||
- Procedural generation: HeightMap, BSP, NoiseSource, DiscreteMap
|
||||
- Tiled and LDtk import with Wang tile / AutoRule resolution
|
||||
- Emscripten/SDL2 backend for WebAssembly deployment
|
||||
- Animation callbacks, mouse event system, grid cell callbacks
|
||||
- Multi-layer grid system with chunk-based rendering and dirty-flag caching
|
||||
- Documentation macro system with auto-generated API docs, man pages, and type stubs
|
||||
- Windows cross-compilation, mobile-ish WASM support, SDL2_mixer audio
|
||||
- Behavior/Trigger turn manager: `grid.step()`, entity labels, `cell_pos`, Dijkstra-backed pathfinding (#295-#303)
|
||||
|
||||
**Proving grounds**: Crypt of Sokoban (7DRL 2025), then 7DRL 2026 -- both shipped on the same engine. The 2026 jam surfaced hotfix-worthy issues (SDL key scancodes, composite textures) that have since landed on master.
|
||||
|
||||
---
|
||||
|
||||
## Current Focus: API Freeze + Memory Safety Sweep
|
||||
|
||||
7DRL 2026 is behind us (Feb 28 -- Mar 8). The engine has two concurrent tracks to 1.0:
|
||||
|
||||
### Track 1: API Freeze
|
||||
The process is underway. Closed in this pass: camelCase module functions (#304), deprecated `sprite_number` (#305), legacy string enum comparisons (#306), `Color.__eq__`/`__ne__` (#307), `Grid.position` alias (#308).
|
||||
|
||||
Remaining freeze work:
|
||||
1. Catalog every public Python class, method, and property -- audit against `stubs/mcrfpy.pyi` and generated docs
|
||||
2. Identify any last naming/signature/default changes before committing
|
||||
3. Final breaking-change pass, bundled
|
||||
4. Document the stable API as the contract
|
||||
5. Experimental modules (3D/Voxel) stay out of the freeze with an `experimental` label
|
||||
|
||||
### Track 2: Fuzz-Driven Bug Sweep
|
||||
The libFuzzer+ASan harness (#283) has nine work tranches merged: build plumbing (W1), native harness (W2/W3), then six targeted fuzzers under `tests/fuzz/`:
|
||||
- `fuzz_grid_entity` -- EntityCollection lifetime (W4, fixed #258-#263, #273, #274)
|
||||
- `fuzz_property_types` -- refcount / type confusion (W5, fixed #267, #268, #272)
|
||||
- `fuzz_anim_timer_scene` -- animation/timer/scene lifecycles (W6)
|
||||
- `fuzz_fov` -- compute_fov parameters (W8, fixed #310)
|
||||
- `fuzz_maps_procgen` -- HeightMap/DiscreteMap interfaces (W7)
|
||||
- `fuzz_pathfinding_behavior` -- Dijkstra + turn manager (W9, fixed #311)
|
||||
|
||||
The active tier1 queue is empty. The last three findings (#309 Caption float→uint, #310 FOV enum, #311 DijkstraMap OOB) all landed on master in mid-April. Coverage extension to remaining public API surface is tracked under #312.
|
||||
|
||||
### Recently Shipped (April 2026)
|
||||
- **#294** -- `entity.perspective_map` replaces flat `vector<UIGridPointState>` with a 3-state DiscreteMap (UNKNOWN/DISCOVERED/VISIBLE). Per-entity FOV memory is now serializable, swappable, and structurally enforces visible-as-subset-of-discovered.
|
||||
- **#315** -- Pathfinding API extended with built-in heuristics (Euclidean/Manhattan/Chebyshev/Diagonal/Zero), multi-root Dijkstra, FLEE primitives (invert + descent), and an interactive demo. EntityBehavior SEEK/FLEE refactored to a `PathProvider` strategy.
|
||||
- **Phase 5.2** -- six performance benchmark scripts under `tests/benchmarks/` covering grid.step(), FOV writeback cost, spatial hash vs. O(n), pathfinding with collision labels, multi-GridView render, and Dijkstra variants. Baselines under `tests/benchmarks/baseline/phase5_2/`.
|
||||
- **Phase 5.3** -- documentation regenerated; `tools/generate_stubs_v2.py` rewritten as introspection-based so it can no longer drift from the C++ source.
|
||||
|
||||
### Active Follow-Ups
|
||||
- **#312** Extend fuzz coverage to remaining public API surface
|
||||
- **#313** Migrate `UIEntity::grid` from `shared_ptr<UIGrid>` to `shared_ptr<GridData>` (post-#252 refactor cleanup)
|
||||
- **#314** API audit follow-through: close gaps from `docs/api-audit-2026-04.md`
|
||||
- **#316** Sparse perspective writeback in `UIEntity::updateVisibility` (Phase 5.2 finding: full-grid demote+promote dominates over TCOD FOV cost)
|
||||
|
||||
### Other Post-7DRL Priorities
|
||||
- Progress on the r/roguelikedev tutorial series (#167)
|
||||
- Complete the API freeze catalog pass (#314)
|
||||
- Better pip/virtualenv integration for adding packages to McRogueFace's embedded interpreter
|
||||
|
||||
---
|
||||
|
||||
## Engine Eras
|
||||
|
||||
One engine, accumulating capabilities. Nothing is thrown away.
|
||||
|
||||
| Era | Focus | Status |
|
||||
|-----|-------|--------|
|
||||
| **McRogueFace** | 2D tiles, roguelike systems, procgen | Active -- approaching 1.0 |
|
||||
| **McVectorFace** | Sparse grids, vector graphics, physics | Planned |
|
||||
| **McVoxelFace** | Voxel terrain, 3D gameplay | Proof-of-concept complete |
|
||||
|
||||
---
|
||||
|
||||
## 3D/Voxel Pipeline (Experimental)
|
||||
|
||||
The 3D pipeline is proof-of-concept scouting for the McVoxelFace era. It works and is tested but is explicitly **not** part of the 1.0 API freeze.
|
||||
|
||||
**What exists**: Viewport3D, Camera3D, Entity3D, MeshLayer, Model3D (glTF), Billboard, Shader3D, VoxelGrid with greedy meshing, face culling, RLE serialization, and navigation projection.
|
||||
|
||||
**Known gaps**: Some Entity3D collection methods, animation stubs, shader pipeline incomplete.
|
||||
|
||||
**Maturity track**: These modules will mature on their own timeline, driven by games that need 3D. They won't block 2D stability.
|
||||
|
||||
---
|
||||
|
||||
## Future Directions
|
||||
|
||||
These are ideas on the horizon -- not yet concrete enough for issues, but worth capturing.
|
||||
|
||||
### McRogueFace Lite
|
||||
A spiritual port to MicroPython targeting the PicoCalc and other microcontrollers. Could provide a migration path to retro ROMs or compete in the Pico-8 space. The core idea: strip McRogueFace down to its essential tile/entity/scene model and run it on constrained hardware.
|
||||
|
||||
### McVectorFace Era
|
||||
The next major capability expansion. Sparse grid layers, a polygon/shape rendering class, and eventually physics integration. This would support games that aren't purely tile-based -- top-down action, strategy maps with irregular regions, or hybrid tile+vector visuals. See the [Strategic Direction](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Strategic-Direction) wiki for the full era model.
|
||||
|
||||
### McRogueFace Standard Library
|
||||
A built-in collection of reusable GUI widgets and game UI patterns: menus, dialogs, inventory screens, stat bars, text input fields, scrollable lists. These would ship with the engine as importable Python modules, saving every game from reimplementing the same UI primitives. Think of it as `mcrfpy.widgets` -- batteries included.
|
||||
|
||||
### Pip/Virtualenv Integration
|
||||
Rather than inverting the architecture to make McRogueFace a pip-installable package, the nearer-term goal is better integration in the other direction: making it easy to install and use third-party Python packages within McRogueFace's embedded interpreter. This could mean virtualenv awareness, a `mcrf install` command, or bundling pip itself.
|
||||
|
||||
---
|
||||
|
||||
## Open Issues by Area
|
||||
|
||||
25 open issues across the tracker. Key groupings:
|
||||
|
||||
- **Recent follow-ups** (#312, #313, #314, #316) -- Fuzz coverage extension, UIEntity grid refactor, API audit follow-through, sparse perspective writeback
|
||||
- **7DRL 2026 carry-over** (#248) -- Crypt of Sokoban remaster, superseded by the 7DRL 2026 entry but still relevant as a demo
|
||||
- **Tooling / infrastructure** (#282, #255) -- Modern Clang for TSan/fuzzing, performance profiling
|
||||
- **Demos / tutorials** (#167, #154, #156, #55) -- r/roguelikedev series, LLM agent simulations
|
||||
- **Grid enhancements** (#152, #67) -- Sparse layers, infinite worlds
|
||||
- **Performance** (#117, #124, #145) -- Memory pools, grid point animation, texture reuse
|
||||
- **Platform/distribution** (#70, #54, #62, #53) -- Packaging, Jupyter, multiple windows, input methods
|
||||
- **WASM tooling** (#239) -- Automated browser testing
|
||||
- **Rendering** (#107) -- Particle system
|
||||
- **Deferred** (#220, #46, #45) -- Subinterpreter support / tests, accessibility modes
|
||||
|
||||
See the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current status.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- **Issue Tracker**: [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues)
|
||||
- **Wiki**: [Strategic Direction](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Strategic-Direction), [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap), [Development Workflow](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Development-Workflow)
|
||||
- **Build Guide**: See `CLAUDE.md` for build instructions
|
||||
- **Tutorial**: `roguelike_tutorial/` for implementation examples
|
||||
BIN
assets/Sprite-0001.ase
Normal file
BIN
assets/Sprite-0001.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/alives_other.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
assets/boom.wav
Normal file
BIN
assets/custom_player.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/gamescale_buildings.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/gamescale_decor.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/temp_logo.png
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
assets/terrain.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
assets/terrain_alpha.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/test_portraits.ase
Normal file
BIN
assets/test_portraits.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
54
build.sh
|
|
@ -1,54 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Build script for McRogueFace - compiles everything into ./build directory
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}McRogueFace Build Script${NC}"
|
||||
echo "========================="
|
||||
|
||||
# Create build directory if it doesn't exist
|
||||
if [ ! -d "build" ]; then
|
||||
echo -e "${YELLOW}Creating build directory...${NC}"
|
||||
mkdir build
|
||||
fi
|
||||
|
||||
# Change to build directory
|
||||
cd build
|
||||
|
||||
# Run CMake to generate build files
|
||||
echo -e "${YELLOW}Running CMake...${NC}"
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
# Check if CMake succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}CMake configuration failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run make with parallel jobs
|
||||
echo -e "${YELLOW}Building with make...${NC}"
|
||||
make -j$(nproc)
|
||||
|
||||
# Check if make succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Build failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Build completed successfully!${NC}"
|
||||
echo ""
|
||||
echo "The build directory contains:"
|
||||
ls -la
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}To run McRogueFace:${NC}"
|
||||
echo " cd build"
|
||||
echo " ./mcrogueface"
|
||||
echo ""
|
||||
echo -e "${GREEN}To create a distribution archive:${NC}"
|
||||
echo " cd build"
|
||||
echo " zip -r ../McRogueFace-$(date +%Y%m%d).zip ."
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
@echo off
|
||||
REM Windows build script for McRogueFace
|
||||
REM Run this over SSH without Visual Studio GUI
|
||||
|
||||
echo Building McRogueFace for Windows...
|
||||
|
||||
REM Clean previous build
|
||||
if exist build_win rmdir /s /q build_win
|
||||
mkdir build_win
|
||||
cd build_win
|
||||
|
||||
REM Generate Visual Studio project files with CMake
|
||||
REM Use -G to specify generator, -A for architecture
|
||||
REM Visual Studio 2022 = "Visual Studio 17 2022"
|
||||
REM Visual Studio 2019 = "Visual Studio 16 2019"
|
||||
cmake -G "Visual Studio 17 2022" -A x64 ..
|
||||
if errorlevel 1 (
|
||||
echo CMake configuration failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build using MSBuild (comes with Visual Studio)
|
||||
REM You can also use cmake --build . --config Release
|
||||
msbuild McRogueFace.sln /p:Configuration=Release /p:Platform=x64 /m
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Build completed successfully!
|
||||
echo Executable location: build_win\Release\mcrogueface.exe
|
||||
|
||||
REM Alternative: Using cmake to build (works with any generator)
|
||||
REM cmake --build . --config Release --parallel
|
||||
|
||||
cd ..
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
@echo off
|
||||
REM Windows build script using cmake --build (generator-agnostic)
|
||||
REM This version works with any CMake generator
|
||||
|
||||
echo Building McRogueFace for Windows using CMake...
|
||||
|
||||
REM Set build directory
|
||||
set BUILD_DIR=build_win
|
||||
set CONFIG=Release
|
||||
|
||||
REM Clean previous build
|
||||
if exist %BUILD_DIR% rmdir /s /q %BUILD_DIR%
|
||||
mkdir %BUILD_DIR%
|
||||
cd %BUILD_DIR%
|
||||
|
||||
REM Configure with CMake
|
||||
REM You can change the generator here if needed:
|
||||
REM -G "Visual Studio 17 2022" (VS 2022)
|
||||
REM -G "Visual Studio 16 2019" (VS 2019)
|
||||
REM -G "MinGW Makefiles" (MinGW)
|
||||
REM -G "Ninja" (Ninja build system)
|
||||
cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=%CONFIG% ..
|
||||
if errorlevel 1 (
|
||||
echo CMake configuration failed!
|
||||
cd ..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build using cmake (works with any generator)
|
||||
cmake --build . --config %CONFIG% --parallel
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
cd ..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Build completed successfully!
|
||||
echo Executable: %BUILD_DIR%\%CONFIG%\mcrogueface.exe
|
||||
echo.
|
||||
|
||||
cd ..
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# CMake toolchain file for cross-compiling to Windows using MinGW-w64
|
||||
# Usage: cmake -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64-x86_64.cmake ..
|
||||
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
|
||||
# Specify the cross-compiler (use posix variant for std::mutex support)
|
||||
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc-posix)
|
||||
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++-posix)
|
||||
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||
|
||||
# Target environment location
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||
|
||||
# Add MinGW system include directories for Windows headers
|
||||
include_directories(SYSTEM /usr/x86_64-w64-mingw32/include)
|
||||
|
||||
# Adjust search behavior
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
|
||||
# Static linking of libgcc and libstdc++ to avoid runtime dependency issues
|
||||
# Enable auto-import for Python DLL data symbols
|
||||
set(CMAKE_EXE_LINKER_FLAGS_INIT "-static-libgcc -static-libstdc++ -Wl,--enable-auto-import")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-static-libgcc -static-libstdc++ -Wl,--enable-auto-import")
|
||||
|
||||
# Windows-specific defines
|
||||
add_definitions(-DWIN32 -D_WIN32 -D_WINDOWS)
|
||||
add_definitions(-DMINGW_HAS_SECURE_API)
|
||||
|
||||
# Disable console window for GUI applications (optional, can be overridden)
|
||||
# set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -mwindows")
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
[
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/GameEngine.cpp.o -c /home/john/Development/McRogueFace/src/GameEngine.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/GameEngine.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/IndexTexture.cpp.o -c /home/john/Development/McRogueFace/src/IndexTexture.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/IndexTexture.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/McRFPy_API.cpp.o -c /home/john/Development/McRogueFace/src/McRFPy_API.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/McRFPy_API.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyCallable.cpp.o -c /home/john/Development/McRogueFace/src/PyCallable.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyCallable.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyColor.cpp.o -c /home/john/Development/McRogueFace/src/PyColor.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyColor.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyFont.cpp.o -c /home/john/Development/McRogueFace/src/PyFont.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyFont.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyScene.cpp.o -c /home/john/Development/McRogueFace/src/PyScene.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyScene.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyTexture.cpp.o -c /home/john/Development/McRogueFace/src/PyTexture.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyTexture.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyVector.cpp.o -c /home/john/Development/McRogueFace/src/PyVector.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyVector.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Resources.cpp.o -c /home/john/Development/McRogueFace/src/Resources.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/Resources.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Scene.cpp.o -c /home/john/Development/McRogueFace/src/Scene.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/Scene.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Timer.cpp.o -c /home/john/Development/McRogueFace/src/Timer.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/Timer.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICaption.cpp.o -c /home/john/Development/McRogueFace/src/UICaption.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UICaption.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICollection.cpp.o -c /home/john/Development/McRogueFace/src/UICollection.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UICollection.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIDrawable.cpp.o -c /home/john/Development/McRogueFace/src/UIDrawable.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIDrawable.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIEntity.cpp.o -c /home/john/Development/McRogueFace/src/UIEntity.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIEntity.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIFrame.cpp.o -c /home/john/Development/McRogueFace/src/UIFrame.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIFrame.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGrid.cpp.o -c /home/john/Development/McRogueFace/src/UIGrid.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIGrid.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGridPoint.cpp.o -c /home/john/Development/McRogueFace/src/UIGridPoint.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIGridPoint.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UISprite.cpp.o -c /home/john/Development/McRogueFace/src/UISprite.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UISprite.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UITestScene.cpp.o -c /home/john/Development/McRogueFace/src/UITestScene.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UITestScene.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/main.cpp.o -c /home/john/Development/McRogueFace/src/main.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/main.cpp"
|
||||
}
|
||||
]
|
||||
157
css_colors.txt
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
aqua #00FFFF
|
||||
black #000000
|
||||
blue #0000FF
|
||||
fuchsia #FF00FF
|
||||
gray #808080
|
||||
green #008000
|
||||
lime #00FF00
|
||||
maroon #800000
|
||||
navy #000080
|
||||
olive #808000
|
||||
purple #800080
|
||||
red #FF0000
|
||||
silver #C0C0C0
|
||||
teal #008080
|
||||
white #FFFFFF
|
||||
yellow #FFFF00
|
||||
aliceblue #F0F8FF
|
||||
antiquewhite #FAEBD7
|
||||
aqua #00FFFF
|
||||
aquamarine #7FFFD4
|
||||
azure #F0FFFF
|
||||
beige #F5F5DC
|
||||
bisque #FFE4C4
|
||||
black #000000
|
||||
blanchedalmond #FFEBCD
|
||||
blue #0000FF
|
||||
blueviolet #8A2BE2
|
||||
brown #A52A2A
|
||||
burlywood #DEB887
|
||||
cadetblue #5F9EA0
|
||||
chartreuse #7FFF00
|
||||
chocolate #D2691E
|
||||
coral #FF7F50
|
||||
cornflowerblue #6495ED
|
||||
cornsilk #FFF8DC
|
||||
crimson #DC143C
|
||||
cyan #00FFFF
|
||||
darkblue #00008B
|
||||
darkcyan #008B8B
|
||||
darkgoldenrod #B8860B
|
||||
darkgray #A9A9A9
|
||||
darkgreen #006400
|
||||
darkkhaki #BDB76B
|
||||
darkmagenta #8B008B
|
||||
darkolivegreen #556B2F
|
||||
darkorange #FF8C00
|
||||
darkorchid #9932CC
|
||||
darkred #8B0000
|
||||
darksalmon #E9967A
|
||||
darkseagreen #8FBC8F
|
||||
darkslateblue #483D8B
|
||||
darkslategray #2F4F4F
|
||||
darkturquoise #00CED1
|
||||
darkviolet #9400D3
|
||||
deeppink #FF1493
|
||||
deepskyblue #00BFFF
|
||||
dimgray #696969
|
||||
dodgerblue #1E90FF
|
||||
firebrick #B22222
|
||||
floralwhite #FFFAF0
|
||||
forestgreen #228B22
|
||||
fuchsia #FF00FF
|
||||
gainsboro #DCDCDC
|
||||
ghostwhite #F8F8FF
|
||||
gold #FFD700
|
||||
goldenrod #DAA520
|
||||
gray #7F7F7F
|
||||
green #008000
|
||||
greenyellow #ADFF2F
|
||||
honeydew #F0FFF0
|
||||
hotpink #FF69B4
|
||||
indianred #CD5C5C
|
||||
indigo #4B0082
|
||||
ivory #FFFFF0
|
||||
khaki #F0E68C
|
||||
lavender #E6E6FA
|
||||
lavenderblush #FFF0F5
|
||||
lawngreen #7CFC00
|
||||
lemonchiffon #FFFACD
|
||||
lightblue #ADD8E6
|
||||
lightcoral #F08080
|
||||
lightcyan #E0FFFF
|
||||
lightgoldenrodyellow #FAFAD2
|
||||
lightgreen #90EE90
|
||||
lightgrey #D3D3D3
|
||||
lightpink #FFB6C1
|
||||
lightsalmon #FFA07A
|
||||
lightseagreen #20B2AA
|
||||
lightskyblue #87CEFA
|
||||
lightslategray #778899
|
||||
lightsteelblue #B0C4DE
|
||||
lightyellow #FFFFE0
|
||||
lime #00FF00
|
||||
limegreen #32CD32
|
||||
linen #FAF0E6
|
||||
magenta #FF00FF
|
||||
maroon #800000
|
||||
mediumaquamarine #66CDAA
|
||||
mediumblue #0000CD
|
||||
mediumorchid #BA55D3
|
||||
mediumpurple #9370DB
|
||||
mediumseagreen #3CB371
|
||||
mediumslateblue #7B68EE
|
||||
mediumspringgreen #00FA9A
|
||||
mediumturquoise #48D1CC
|
||||
mediumvioletred #C71585
|
||||
midnightblue #191970
|
||||
mintcream #F5FFFA
|
||||
mistyrose #FFE4E1
|
||||
moccasin #FFE4B5
|
||||
navajowhite #FFDEAD
|
||||
navy #000080
|
||||
navyblue #9FAFDF
|
||||
oldlace #FDF5E6
|
||||
olive #808000
|
||||
olivedrab #6B8E23
|
||||
orange #FFA500
|
||||
orangered #FF4500
|
||||
orchid #DA70D6
|
||||
palegoldenrod #EEE8AA
|
||||
palegreen #98FB98
|
||||
paleturquoise #AFEEEE
|
||||
palevioletred #DB7093
|
||||
papayawhip #FFEFD5
|
||||
peachpuff #FFDAB9
|
||||
peru #CD853F
|
||||
pink #FFC0CB
|
||||
plum #DDA0DD
|
||||
powderblue #B0E0E6
|
||||
purple #800080
|
||||
red #FF0000
|
||||
rosybrown #BC8F8F
|
||||
royalblue #4169E1
|
||||
saddlebrown #8B4513
|
||||
salmon #FA8072
|
||||
sandybrown #FA8072
|
||||
seagreen #2E8B57
|
||||
seashell #FFF5EE
|
||||
sienna #A0522D
|
||||
silver #C0C0C0
|
||||
skyblue #87CEEB
|
||||
slateblue #6A5ACD
|
||||
slategray #708090
|
||||
snow #FFFAFA
|
||||
springgreen #00FF7F
|
||||
steelblue #4682B4
|
||||
tan #D2B48C
|
||||
teal #008080
|
||||
thistle #D8BFD8
|
||||
tomato #FF6347
|
||||
turquoise #40E0D0
|
||||
violet #EE82EE
|
||||
wheat #F5DEB3
|
||||
white #FFFFFF
|
||||
whitesmoke #F5F5F5
|
||||
yellow #FFFF00
|
||||
yellowgreen #9ACD32
|
||||
54
deps/platform/linux/platform.h
vendored
|
|
@ -1,54 +1,6 @@
|
|||
#ifndef __PLATFORM
|
||||
#define __PLATFORM
|
||||
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// WASM/Emscripten platform - no /proc filesystem, limited std::filesystem support
|
||||
|
||||
std::wstring executable_path()
|
||||
{
|
||||
// In WASM, the executable is at the root of the virtual filesystem
|
||||
return L"/";
|
||||
}
|
||||
|
||||
std::wstring executable_filename()
|
||||
{
|
||||
// In WASM, we use a fixed executable name
|
||||
return L"/mcrogueface";
|
||||
}
|
||||
|
||||
std::wstring working_path()
|
||||
{
|
||||
// In WASM, working directory is root of virtual filesystem
|
||||
return L"/";
|
||||
}
|
||||
|
||||
std::string narrow_string(std::wstring convertme)
|
||||
{
|
||||
// Simple conversion for ASCII/UTF-8 compatible strings
|
||||
std::string result;
|
||||
result.reserve(convertme.size());
|
||||
for (wchar_t wc : convertme) {
|
||||
if (wc < 128) {
|
||||
result.push_back(static_cast<char>(wc));
|
||||
} else {
|
||||
// For non-ASCII, use a simple UTF-8 encoding
|
||||
if (wc < 0x800) {
|
||||
result.push_back(static_cast<char>(0xC0 | (wc >> 6)));
|
||||
result.push_back(static_cast<char>(0x80 | (wc & 0x3F)));
|
||||
} else {
|
||||
result.push_back(static_cast<char>(0xE0 | (wc >> 12)));
|
||||
result.push_back(static_cast<char>(0x80 | ((wc >> 6) & 0x3F)));
|
||||
result.push_back(static_cast<char>(0x80 | (wc & 0x3F)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#else
|
||||
// Native Linux platform
|
||||
|
||||
std::wstring executable_path()
|
||||
{
|
||||
/*
|
||||
|
|
@ -60,7 +12,7 @@ std::wstring executable_path()
|
|||
return exec_path.wstring();
|
||||
//size_t path_index = exec_path.find_last_of('/');
|
||||
//return exec_path.substr(0, path_index);
|
||||
|
||||
|
||||
}
|
||||
|
||||
std::wstring executable_filename()
|
||||
|
|
@ -85,6 +37,4 @@ std::string narrow_string(std::wstring convertme)
|
|||
return converter.to_bytes(convertme);
|
||||
}
|
||||
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
||||
#endif // __PLATFORM
|
||||
#endif
|
||||
|
|
|
|||
8
deps/platform/windows/platform.h
vendored
|
|
@ -1,12 +1,12 @@
|
|||
#ifndef __PLATFORM
|
||||
#define __PLATFORM
|
||||
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
|
||||
#include <windows.h>
|
||||
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 0
|
||||
#include <Windows.h>
|
||||
|
||||
std::wstring executable_path()
|
||||
{
|
||||
wchar_t buffer[MAX_PATH];
|
||||
GetModuleFileNameW(NULL, buffer, MAX_PATH); // Use explicit Unicode version
|
||||
GetModuleFileName(NULL, buffer, MAX_PATH);
|
||||
std::wstring exec_path = buffer;
|
||||
size_t path_index = exec_path.find_last_of(L"\\/");
|
||||
return exec_path.substr(0, path_index);
|
||||
|
|
@ -15,7 +15,7 @@ std::wstring executable_path()
|
|||
std::wstring executable_filename()
|
||||
{
|
||||
wchar_t buffer[MAX_PATH];
|
||||
GetModuleFileNameW(NULL, buffer, MAX_PATH); // Use explicit Unicode version
|
||||
GetModuleFileName(NULL, buffer, MAX_PATH);
|
||||
std::wstring exec_path = buffer;
|
||||
return exec_path;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,860 +0,0 @@
|
|||
# McRogueFace Emscripten & Renderer Abstraction Research
|
||||
|
||||
**Date**: 2026-01-30
|
||||
**Branch**: `emscripten-mcrogueface`
|
||||
**Related Issues**: #157 (True Headless), #158 (Emscripten/WASM)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document analyzes the technical requirements for:
|
||||
1. **SFML 2.6 → 3.0 migration** (modernization)
|
||||
2. **Emscripten/WebAssembly compilation** (browser deployment)
|
||||
|
||||
Both goals share a common prerequisite: **renderer abstraction**. The codebase already has a partial abstraction via `sf::RenderTarget*` pointer, but SFML types are pervasive (1276 occurrences across 78 files).
|
||||
|
||||
**Key Insight**: This is a **build-time configuration**, not runtime switching. The standard McRogueFace binary remains a dynamic environment; Emscripten builds bundle assets and scripts at compile time.
|
||||
|
||||
---
|
||||
|
||||
## Current Architecture Analysis
|
||||
|
||||
### Existing Abstraction Strengths
|
||||
|
||||
1. **RenderTarget Pointer Pattern** (`GameEngine.h:156`)
|
||||
```cpp
|
||||
sf::RenderTarget* render_target;
|
||||
// Points to either window.get() or headless_renderer->getRenderTarget()
|
||||
```
|
||||
This already decouples rendering logic from the specific backend.
|
||||
|
||||
2. **HeadlessRenderer** (`src/HeadlessRenderer.h`)
|
||||
- Uses `sf::RenderTexture` internally
|
||||
- Provides unified interface: `getRenderTarget()`, `display()`, `saveScreenshot()`
|
||||
- Demonstrates the pattern for additional backends
|
||||
|
||||
3. **UIDrawable Hierarchy**
|
||||
- Virtual `render(sf::Vector2f, sf::RenderTarget&)` method
|
||||
- 7 drawable types: Frame, Caption, Sprite, Entity, Grid, Line, Circle, Arc
|
||||
- Each manages its own SFML primitives internally
|
||||
|
||||
4. **Asset Wrappers**
|
||||
- `PyTexture`, `PyFont`, `PyShader` wrap SFML types
|
||||
- Python reference counting integrated
|
||||
- Single point of change for asset loading APIs
|
||||
|
||||
### Current SFML Coupling Points
|
||||
|
||||
| Area | Count | Difficulty | Notes |
|
||||
|------|-------|------------|-------|
|
||||
| `sf::Vector2f` | ~200+ | Medium | Used everywhere for positions, sizes |
|
||||
| `sf::Color` | ~100+ | Easy | Simple 4-byte struct replacement |
|
||||
| `sf::FloatRect` | ~50+ | Medium | Bounds, intersection testing |
|
||||
| `sf::RenderTexture` | ~20 | Hard | Shader effects, caching |
|
||||
| `sf::Sprite/Text` | ~30 | Hard | Core rendering primitives |
|
||||
| `sf::Event` | ~15 | Medium | Input system coupling |
|
||||
| `sf::Keyboard/Mouse` | ~50+ | Easy | Enum mappings |
|
||||
|
||||
Total: **1276 occurrences across 78 files**
|
||||
|
||||
---
|
||||
|
||||
## SFML 3.0 Migration Analysis
|
||||
|
||||
### Breaking Changes Requiring Code Updates
|
||||
|
||||
#### 1. Vector Parameters (High Impact)
|
||||
```cpp
|
||||
// SFML 2.6
|
||||
setPosition(10, 20);
|
||||
sf::VideoMode(1024, 768, 32);
|
||||
sf::FloatRect(x, y, w, h);
|
||||
|
||||
// SFML 3.0
|
||||
setPosition({10, 20});
|
||||
sf::VideoMode({1024, 768}, 32);
|
||||
sf::FloatRect({x, y}, {w, h});
|
||||
```
|
||||
|
||||
**Strategy**: Regex-based search/replace with manual verification.
|
||||
|
||||
#### 2. Rect Member Changes (Medium Impact)
|
||||
```cpp
|
||||
// SFML 2.6
|
||||
rect.left, rect.top, rect.width, rect.height
|
||||
rect.getPosition(), rect.getSize()
|
||||
|
||||
// SFML 3.0
|
||||
rect.position.x, rect.position.y, rect.size.x, rect.size.y
|
||||
rect.position, rect.size // direct access
|
||||
rect.findIntersection() -> std::optional<Rect<T>>
|
||||
```
|
||||
|
||||
#### 3. Resource Constructors (Low Impact)
|
||||
```cpp
|
||||
// SFML 2.6
|
||||
sf::Sound sound; // default constructible
|
||||
sound.setBuffer(buffer);
|
||||
|
||||
// SFML 3.0
|
||||
sf::Sound sound(buffer); // requires buffer at construction
|
||||
```
|
||||
|
||||
#### 4. Keyboard/Mouse Enum Scoping (Medium Impact)
|
||||
```cpp
|
||||
// SFML 2.6
|
||||
sf::Keyboard::A
|
||||
sf::Mouse::Left
|
||||
|
||||
// SFML 3.0
|
||||
sf::Keyboard::Key::A
|
||||
sf::Mouse::Button::Left
|
||||
```
|
||||
|
||||
#### 5. Event Handling (Medium Impact)
|
||||
```cpp
|
||||
// SFML 2.6
|
||||
sf::Event event;
|
||||
while (window.pollEvent(event)) {
|
||||
if (event.type == sf::Event::Closed) ...
|
||||
}
|
||||
|
||||
// SFML 3.0
|
||||
while (auto event = window.pollEvent()) {
|
||||
if (event->is<sf::Event::Closed>()) ...
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. CMake Target Changes
|
||||
```cmake
|
||||
# SFML 2.6
|
||||
find_package(SFML 2 REQUIRED COMPONENTS graphics audio)
|
||||
target_link_libraries(app sfml-graphics sfml-audio)
|
||||
|
||||
# SFML 3.0
|
||||
find_package(SFML 3 REQUIRED COMPONENTS Graphics Audio)
|
||||
target_link_libraries(app SFML::Graphics SFML::Audio)
|
||||
```
|
||||
|
||||
### Migration Effort Estimate
|
||||
|
||||
| Phase | Files | Changes | Effort |
|
||||
|-------|-------|---------|--------|
|
||||
| CMakeLists.txt | 1 | Target names | 1 hour |
|
||||
| Vector parameters | 30+ | ~200 calls | 4-8 hours |
|
||||
| Rect refactoring | 20+ | ~50 usages | 2-4 hours |
|
||||
| Event handling | 5 | ~15 sites | 2 hours |
|
||||
| Keyboard/Mouse | 10 | ~50 enums | 2 hours |
|
||||
| Resource constructors | 10 | ~30 sites | 2 hours |
|
||||
| **Total** | - | - | **~15-25 hours** |
|
||||
|
||||
---
|
||||
|
||||
## Emscripten/VRSFML Analysis
|
||||
|
||||
### Why VRSFML Over Waiting for SFML 4.x?
|
||||
|
||||
1. **Available Now**: VRSFML is working today with browser demos
|
||||
2. **Modern OpenGL**: Removes legacy calls, targets OpenGL ES 3.0+ (WebGL 2)
|
||||
3. **SFML_GAME_LOOP Macro**: Handles blocking vs callback loop abstraction
|
||||
4. **Performance**: 500k sprites @ 60FPS vs 3 FPS upstream (batching)
|
||||
5. **SFML 4.x Timeline**: Unknown, potentially years away
|
||||
|
||||
### VRSFML API Differences from SFML
|
||||
|
||||
| Feature | SFML 2.6/3.0 | VRSFML |
|
||||
|---------|--------------|--------|
|
||||
| Default constructors | Allowed | Not allowed for resources |
|
||||
| Texture ownership | Pointer in Sprite | Passed at draw time |
|
||||
| Context management | Hidden global | Explicit `GraphicsContext` |
|
||||
| Drawable base class | Polymorphic | Removed |
|
||||
| Loading methods | `loadFromFile()` returns bool | Returns `std::optional` |
|
||||
| Main loop | `while(running)` | `SFML_GAME_LOOP { }` |
|
||||
|
||||
### Main Loop Refactoring
|
||||
|
||||
Current blocking loop:
|
||||
```cpp
|
||||
void GameEngine::run() {
|
||||
while (running) {
|
||||
processEvents();
|
||||
update();
|
||||
render();
|
||||
display();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Emscripten-compatible pattern:
|
||||
```cpp
|
||||
// Option A: VRSFML macro
|
||||
SFML_GAME_LOOP {
|
||||
processEvents();
|
||||
update();
|
||||
render();
|
||||
display();
|
||||
}
|
||||
|
||||
// Option B: Manual Emscripten integration
|
||||
#ifdef __EMSCRIPTEN__
|
||||
void mainLoopCallback() {
|
||||
if (!game.running) {
|
||||
emscripten_cancel_main_loop();
|
||||
return;
|
||||
}
|
||||
game.doFrame();
|
||||
}
|
||||
emscripten_set_main_loop(mainLoopCallback, 0, 1);
|
||||
#else
|
||||
while (running) { doFrame(); }
|
||||
#endif
|
||||
```
|
||||
|
||||
**Recommendation**: Use preprocessor-based approach with `doFrame()` extraction for cleaner separation.
|
||||
|
||||
---
|
||||
|
||||
## Build-Time Configuration Strategy
|
||||
|
||||
### Normal Build (Desktop)
|
||||
- Dynamic loading of assets from `assets/` directory
|
||||
- Python scripts loaded from `scripts/` directory at runtime
|
||||
- Full McRogueFace environment with dynamic game loading
|
||||
|
||||
### Emscripten Build (Web)
|
||||
- Assets bundled via `--preload-file assets`
|
||||
- Scripts bundled via `--preload-file scripts`
|
||||
- Virtual filesystem (MEMFS/IDBFS)
|
||||
- Optional: Script linting with Pyodide before bundling
|
||||
- Single-purpose deployment (one game per build)
|
||||
|
||||
### CMake Configuration
|
||||
```cmake
|
||||
option(MCRF_BUILD_EMSCRIPTEN "Build for Emscripten/WebAssembly" OFF)
|
||||
|
||||
if(MCRF_BUILD_EMSCRIPTEN)
|
||||
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/cmake/toolchains/emscripten.cmake)
|
||||
add_definitions(-DMCRF_EMSCRIPTEN)
|
||||
|
||||
# Bundle assets
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} \
|
||||
--preload-file ${CMAKE_SOURCE_DIR}/assets@/assets \
|
||||
--preload-file ${CMAKE_SOURCE_DIR}/scripts@/scripts")
|
||||
endif()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phased Implementation Plan
|
||||
|
||||
### Phase 0: Preparation (This PR)
|
||||
- [ ] Create `docs/EMSCRIPTEN_RESEARCH.md` (this document)
|
||||
- [ ] Update Gitea issues #157, #158 with findings
|
||||
- [ ] Identify specific files requiring changes
|
||||
- [ ] Create test matrix for rendering features
|
||||
|
||||
### Phase 1: Type Abstraction Layer
|
||||
**Goal**: Isolate SFML types behind McRogueFace wrappers
|
||||
|
||||
```cpp
|
||||
// src/types/McrfTypes.h
|
||||
namespace mcrf {
|
||||
using Vector2f = sf::Vector2f; // Alias initially, replace later
|
||||
using Color = sf::Color;
|
||||
using FloatRect = sf::FloatRect;
|
||||
}
|
||||
```
|
||||
|
||||
Changes:
|
||||
- [ ] Create `src/types/` directory with wrapper types
|
||||
- [ ] Gradually replace `sf::` with `mcrf::` namespace
|
||||
- [ ] Update Common.h to provide both namespaces during transition
|
||||
|
||||
### Phase 2: Main Loop Extraction
|
||||
**Goal**: Make game loop callback-compatible
|
||||
|
||||
- [ ] Extract `GameEngine::doFrame()` from `run()`
|
||||
- [ ] Add `#ifdef __EMSCRIPTEN__` conditional in `run()`
|
||||
- [ ] Test that desktop behavior is unchanged
|
||||
|
||||
### Phase 3: Render Backend Interface
|
||||
**Goal**: Abstract RenderTarget operations
|
||||
|
||||
```cpp
|
||||
class RenderBackend {
|
||||
public:
|
||||
virtual ~RenderBackend() = default;
|
||||
virtual void clear(const Color& color) = 0;
|
||||
virtual void draw(const Sprite& sprite) = 0;
|
||||
virtual void draw(const Text& text) = 0;
|
||||
virtual void display() = 0;
|
||||
virtual bool isOpen() const = 0;
|
||||
virtual Vector2u getSize() const = 0;
|
||||
};
|
||||
|
||||
class SFMLBackend : public RenderBackend { ... };
|
||||
class VRSFMLBackend : public RenderBackend { ... }; // Future
|
||||
```
|
||||
|
||||
### Phase 4: SFML 3.0 Migration
|
||||
**Goal**: Update to SFML 3.0 API
|
||||
|
||||
- [ ] Update CMakeLists.txt targets
|
||||
- [ ] Fix vector parameter calls
|
||||
- [ ] Fix rect member access
|
||||
- [ ] Fix event handling
|
||||
- [ ] Fix keyboard/mouse enums
|
||||
- [ ] Test thoroughly
|
||||
|
||||
### Phase 5: VRSFML Integration (Experimental)
|
||||
**Goal**: Add VRSFML as alternative backend
|
||||
|
||||
- [ ] Add VRSFML as submodule/dependency
|
||||
- [ ] Implement VRSFMLBackend
|
||||
- [ ] Add Emscripten CMake configuration
|
||||
- [ ] Test in browser
|
||||
|
||||
### Phase 6: Python-in-WASM
|
||||
**Goal**: Get Python scripting working in browser
|
||||
|
||||
**High Risk** - This is the major unknown:
|
||||
- [ ] Build CPython for Emscripten
|
||||
- [ ] Test `McRFPy_API` binding compatibility
|
||||
- [ ] Evaluate Pyodide vs raw CPython
|
||||
- [ ] Handle filesystem virtualization
|
||||
- [ ] Test threading limitations
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Probability | Impact | Mitigation |
|
||||
|------|-------------|--------|------------|
|
||||
| SFML 3.0 breaks unexpected code | Medium | Medium | Comprehensive test suite |
|
||||
| VRSFML API too different | Low | High | Can fork/patch VRSFML |
|
||||
| Python-in-WASM fails | Medium | Critical | Evaluate Pyodide early |
|
||||
| Performance regression | Low | Medium | Benchmark before/after |
|
||||
| Binary size too large | Medium | Medium | Lazy loading, stdlib trimming |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
### SFML 3.0
|
||||
- [Migration Guide](https://www.sfml-dev.org/tutorials/3.0/getting-started/migrate/)
|
||||
- [Changelog](https://www.sfml-dev.org/development/changelog/)
|
||||
- [Release Notes](https://github.com/SFML/SFML/releases/tag/3.0.0)
|
||||
|
||||
### VRSFML/Emscripten
|
||||
- [VRSFML Blog Post](https://vittorioromeo.com/index/blog/vrsfml.html)
|
||||
- [VRSFML GitHub](https://github.com/vittorioromeo/VRSFML)
|
||||
- [Browser Demos](https://vittorioromeo.github.io/VRSFML_HTML5_Examples/)
|
||||
|
||||
### Python WASM
|
||||
- [PEP 776 - Python Emscripten Support](https://peps.python.org/pep-0776/)
|
||||
- [CPython WASM Build Guide](https://github.com/python/cpython/blob/main/Tools/wasm/README.md)
|
||||
- [Pyodide](https://github.com/pyodide/pyodide)
|
||||
|
||||
### Related Issues
|
||||
- [SFML Emscripten Discussion #1494](https://github.com/SFML/SFML/issues/1494)
|
||||
- [libtcod Emscripten #41](https://github.com/libtcod/libtcod/issues/41)
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: File-by-File SFML Usage Inventory
|
||||
|
||||
### Critical Files (Must Abstract for Emscripten)
|
||||
|
||||
| File | SFML Types Used | Role | Abstraction Difficulty |
|
||||
|------|-----------------|------|------------------------|
|
||||
| `GameEngine.h/cpp` | RenderWindow, Clock, Font, Event | Main loop, window | **CRITICAL** |
|
||||
| `HeadlessRenderer.h/cpp` | RenderTexture | Headless backend | **CRITICAL** |
|
||||
| `UIDrawable.h/cpp` | Vector2f, RenderTarget, FloatRect | Base render interface | **HARD** |
|
||||
| `UIFrame.h/cpp` | RectangleShape, Vector2f, Color | Container rendering | **HARD** |
|
||||
| `UISprite.h/cpp` | Sprite, Texture, Vector2f | Texture display | **HARD** |
|
||||
| `UICaption.h/cpp` | Text, Font, Vector2f, Color | Text rendering | **HARD** |
|
||||
| `UIGrid.h/cpp` | RenderTexture, Sprite, Vector2f | Tile grid system | **HARD** |
|
||||
| `UIEntity.h/cpp` | Sprite, Vector2f | Game entities | **HARD** |
|
||||
| `UICircle.h/cpp` | CircleShape, Vector2f, Color | Circle shape | **MEDIUM** |
|
||||
| `UILine.h/cpp` | VertexArray, Vector2f, Color | Line rendering | **MEDIUM** |
|
||||
| `UIArc.h/cpp` | CircleShape segments, Vector2f | Arc shape | **MEDIUM** |
|
||||
| `Scene.h/cpp` | Vector2f, RenderTarget | Scene management | **MEDIUM** |
|
||||
| `SceneTransition.h/cpp` | RenderTexture, Sprite | Transitions | **MEDIUM** |
|
||||
|
||||
### Wrapper Files (Already Partially Abstracted)
|
||||
|
||||
| File | SFML Types Wrapped | Python API | Notes |
|
||||
|------|-------------------|------------|-------|
|
||||
| `PyVector.h/cpp` | sf::Vector2f | Vector | Ready for backend swap |
|
||||
| `PyColor.h/cpp` | sf::Color | Color | Ready for backend swap |
|
||||
| `PyTexture.h/cpp` | sf::Texture | Texture | Asset loading needs work |
|
||||
| `PyFont.h/cpp` | sf::Font | Font | Asset loading needs work |
|
||||
| `PyShader.h/cpp` | sf::Shader | Shader | Optional feature |
|
||||
|
||||
### Input System Files
|
||||
|
||||
| File | SFML Types Used | Notes |
|
||||
|------|-----------------|-------|
|
||||
| `ActionCode.h` | Keyboard::Key, Mouse::Button | Enum encoding only |
|
||||
| `PyKey.h/cpp` | Keyboard::Key enum | 140+ key mappings |
|
||||
| `PyMouseButton.h/cpp` | Mouse::Button enum | Simple enum |
|
||||
| `PyKeyboard.h/cpp` | Keyboard::isKeyPressed | State queries |
|
||||
| `PyMouse.h/cpp` | Mouse::getPosition | Position queries |
|
||||
| `PyInputState.h/cpp` | None (pure enum) | No SFML dependency |
|
||||
|
||||
### Support Files (Low Priority)
|
||||
|
||||
| File | SFML Types Used | Notes |
|
||||
|------|-----------------|-------|
|
||||
| `Animation.h/cpp` | Vector2f, Color (as values) | Pure data animation |
|
||||
| `GridLayers.h/cpp` | RenderTexture, Color | Layer caching |
|
||||
| `IndexTexture.h/cpp` | Texture, IntRect | Legacy texture format |
|
||||
| `Resources.h/cpp` | Font | Global font storage |
|
||||
| `ProfilerOverlay.cpp` | Text, RectangleShape | Debug overlay |
|
||||
| `McRFPy_Automation.h/cpp` | Various | Testing only |
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Recommended First Steps
|
||||
|
||||
### Immediate (Non-Breaking Changes)
|
||||
|
||||
1. **Extract `GameEngine::doFrame()`**
|
||||
- Move loop body to separate method
|
||||
- No API changes, just internal refactoring
|
||||
- Enables future Emscripten callback integration
|
||||
|
||||
2. **Create type aliases in Common.h**
|
||||
```cpp
|
||||
namespace mcrf {
|
||||
using Vector2f = sf::Vector2f;
|
||||
using Vector2i = sf::Vector2i;
|
||||
using Color = sf::Color;
|
||||
using FloatRect = sf::FloatRect;
|
||||
}
|
||||
```
|
||||
- Allows gradual migration from `sf::` to `mcrf::`
|
||||
- No functional changes
|
||||
|
||||
3. **Document current render path**
|
||||
- Add comments to key rendering functions
|
||||
- Identify all `target.draw()` call sites
|
||||
- Create rendering flow diagram
|
||||
|
||||
### Short-Term (Preparation for SFML 3.0)
|
||||
|
||||
1. **Audit vector parameter calls**
|
||||
- Find all `setPosition(x, y)` style calls
|
||||
- Prepare regex patterns for migration
|
||||
|
||||
2. **Audit rect member access**
|
||||
- Find all `.left`, `.top`, `.width`, `.height` uses
|
||||
- Prepare for `.position.x`, `.size.x` style
|
||||
|
||||
3. **Test suite expansion**
|
||||
- Add rendering validation tests
|
||||
- Screenshot comparison tests
|
||||
- Animation correctness tests
|
||||
|
||||
---
|
||||
|
||||
## Appendix C: libtcod Architecture Analysis
|
||||
|
||||
**Key Finding**: libtcod uses a much simpler abstraction pattern than initially proposed.
|
||||
|
||||
### libtcod's Context Vtable Pattern
|
||||
|
||||
libtcod doesn't wrap every SDL type. Instead, it abstracts at the **context level** using a C-style vtable:
|
||||
|
||||
```c
|
||||
struct TCOD_Context {
|
||||
int type;
|
||||
void* contextdata_; // Backend-specific data (opaque pointer)
|
||||
|
||||
// Function pointers - the "vtable"
|
||||
void (*c_destructor_)(struct TCOD_Context* self);
|
||||
TCOD_Error (*c_present_)(struct TCOD_Context* self,
|
||||
const TCOD_Console* console,
|
||||
const TCOD_ViewportOptions* viewport);
|
||||
void (*c_pixel_to_tile_)(struct TCOD_Context* self, double* x, double* y);
|
||||
TCOD_Error (*c_save_screenshot_)(struct TCOD_Context* self, const char* filename);
|
||||
struct SDL_Window* (*c_get_sdl_window_)(struct TCOD_Context* self);
|
||||
TCOD_Error (*c_set_tileset_)(struct TCOD_Context* self, TCOD_Tileset* tileset);
|
||||
TCOD_Error (*c_screen_capture_)(struct TCOD_Context* self, ...);
|
||||
// ... more operations
|
||||
};
|
||||
```
|
||||
|
||||
### How Backends Implement It
|
||||
|
||||
Each renderer fills in the function pointers:
|
||||
|
||||
```c
|
||||
// In renderer_sdl2.c
|
||||
context->c_destructor_ = sdl2_destructor;
|
||||
context->c_present_ = sdl2_present;
|
||||
context->c_get_sdl_window_ = sdl2_get_window;
|
||||
// ...
|
||||
|
||||
// In renderer_xterm.c
|
||||
context->c_destructor_ = xterm_destructor;
|
||||
context->c_present_ = xterm_present;
|
||||
// ...
|
||||
```
|
||||
|
||||
### Conditional Compilation with NO_SDL
|
||||
|
||||
libtcod uses simple preprocessor guards:
|
||||
|
||||
```c
|
||||
// In CMakeLists.txt
|
||||
if(LIBTCOD_SDL3)
|
||||
target_link_libraries(${PROJECT_NAME} PUBLIC SDL3::SDL3)
|
||||
else()
|
||||
target_compile_definitions(${PROJECT_NAME} PUBLIC NO_SDL)
|
||||
endif()
|
||||
|
||||
// In source files
|
||||
#ifndef NO_SDL
|
||||
#include <SDL3/SDL.h>
|
||||
// ... SDL-dependent code ...
|
||||
#endif
|
||||
```
|
||||
|
||||
**47 files** use this pattern. When building headless, SDL code is simply excluded.
|
||||
|
||||
### Why This Pattern Works
|
||||
|
||||
1. **Core functionality is SDL-independent**: Console manipulation, pathfinding, FOV, noise, BSP, etc. don't need SDL
|
||||
2. **Only rendering needs abstraction**: The `TCOD_Context` is the single point of abstraction
|
||||
3. **Minimal API surface**: Just ~10 function pointers instead of wrapping every primitive
|
||||
4. **Backend-specific data is opaque**: `contextdata_` holds renderer-specific state
|
||||
|
||||
### Implications for McRogueFace
|
||||
|
||||
**libtcod's approach suggests we should NOT try to abstract every `sf::` type.**
|
||||
|
||||
Instead, consider:
|
||||
|
||||
1. **Keep SFML types internally** - `sf::Vector2f`, `sf::Color`, `sf::FloatRect` are fine
|
||||
2. **Abstract at the RenderContext level** - One vtable for window/rendering operations
|
||||
3. **Use `#ifndef NO_SFML` guards** - Compile-time backend selection
|
||||
4. **Create alternative backend for Emscripten** - WebGL + canvas implementation
|
||||
|
||||
### Proposed McRogueFace Context Pattern
|
||||
|
||||
```cpp
|
||||
struct McRF_RenderContext {
|
||||
void* backend_data; // SFML or WebGL specific data
|
||||
|
||||
// Function pointers
|
||||
void (*destroy)(McRF_RenderContext* self);
|
||||
void (*clear)(McRF_RenderContext* self, uint32_t color);
|
||||
void (*present)(McRF_RenderContext* self);
|
||||
void (*draw_sprite)(McRF_RenderContext* self, const Sprite* sprite);
|
||||
void (*draw_text)(McRF_RenderContext* self, const Text* text);
|
||||
void (*draw_rect)(McRF_RenderContext* self, const Rect* rect);
|
||||
bool (*poll_event)(McRF_RenderContext* self, Event* event);
|
||||
void (*screenshot)(McRF_RenderContext* self, const char* path);
|
||||
// ...
|
||||
};
|
||||
|
||||
// SFML backend
|
||||
McRF_RenderContext* mcrf_sfml_context_new(int width, int height, const char* title);
|
||||
|
||||
// Emscripten backend (future)
|
||||
McRF_RenderContext* mcrf_webgl_context_new(const char* canvas_id);
|
||||
```
|
||||
|
||||
### Comparison: Original Plan vs libtcod-Inspired Plan
|
||||
|
||||
| Aspect | Original Plan | libtcod-Inspired Plan |
|
||||
|--------|---------------|----------------------|
|
||||
| Type abstraction | Replace all `sf::*` with `mcrf::*` | Keep `sf::*` internally |
|
||||
| Abstraction point | Every primitive type | Single Context object |
|
||||
| Files affected | 78+ files | ~10 core files |
|
||||
| Compile-time switching | Complex namespace aliasing | Simple `#ifndef NO_SFML` |
|
||||
| Backend complexity | Full reimplementation | Focused vtable |
|
||||
|
||||
**Recommendation**: Adopt libtcod's simpler pattern. Focus abstraction on the rendering context, not on data types.
|
||||
|
||||
---
|
||||
|
||||
## Appendix D: Headless Build Experiment Results
|
||||
|
||||
**Experiment Date**: 2026-01-30
|
||||
**Branch**: `emscripten-mcrogueface`
|
||||
|
||||
### Objective
|
||||
|
||||
Attempt to compile McRogueFace without SFML dependencies to identify true coupling points.
|
||||
|
||||
### What We Created
|
||||
|
||||
1. **`src/platform/HeadlessTypes.h`** - Complete SFML type stubs (~600 lines):
|
||||
- Vector2f, Vector2i, Vector2u
|
||||
- Color with standard color constants
|
||||
- FloatRect, IntRect
|
||||
- Time, Clock (with chrono-based implementation)
|
||||
- Transform, Vertex, View
|
||||
- Shape hierarchy (RectangleShape, CircleShape, etc.)
|
||||
- Texture, Sprite, Font, Text stubs
|
||||
- RenderTarget, RenderTexture, RenderWindow stubs
|
||||
- Audio stubs (Sound, Music, SoundBuffer)
|
||||
- Input stubs (Keyboard, Mouse, Event)
|
||||
- Shader stub
|
||||
|
||||
2. **Modified `src/Common.h`** - Conditional include:
|
||||
```cpp
|
||||
#ifdef MCRF_HEADLESS
|
||||
#include "platform/HeadlessTypes.h"
|
||||
#else
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/Audio.hpp>
|
||||
#endif
|
||||
```
|
||||
|
||||
### Build Attempt Result
|
||||
|
||||
**SUCCESS** - Headless build compiles after consolidating includes and adding stubs.
|
||||
|
||||
### Work Completed
|
||||
|
||||
#### 1. Consolidated SFML Includes
|
||||
|
||||
**15 files** had direct SFML includes that bypassed Common.h. All were modified to use `#include "Common.h"` instead:
|
||||
|
||||
| File | Original Include | Fixed |
|
||||
|------|------------------|-------|
|
||||
| `main.cpp` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `Animation.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `GridChunk.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `GridLayers.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `HeadlessRenderer.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `SceneTransition.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `McRFPy_Automation.h` | `<SFML/Graphics.hpp>`, `<SFML/Window.hpp>` | ✓ |
|
||||
| `PyWindow.cpp` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `ActionCode.h` | `<SFML/Window/Keyboard.hpp>` | ✓ |
|
||||
| `PyKey.h` | `<SFML/Window/Keyboard.hpp>` | ✓ |
|
||||
| `PyMouseButton.h` | `<SFML/Window/Mouse.hpp>` | ✓ |
|
||||
| `PyBSP.h` | `<SFML/System/Vector2.hpp>` | ✓ |
|
||||
| `UIGridPathfinding.h` | `<SFML/System/Vector2.hpp>` | ✓ |
|
||||
|
||||
#### 2. Wrapped ImGui-SFML with Guards
|
||||
|
||||
ImGui-SFML is disabled entirely in headless builds since debug tools can't be accessed through the API:
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `GameEngine.h` | Guarded includes and member variables |
|
||||
| `GameEngine.cpp` | Guarded all ImGui::SFML calls |
|
||||
| `ImGuiConsole.h/cpp` | Entire file wrapped with `#ifndef MCRF_HEADLESS` |
|
||||
| `ImGuiSceneExplorer.h/cpp` | Entire file wrapped with `#ifndef MCRF_HEADLESS` |
|
||||
| `McRFPy_API.cpp` | Guarded ImGuiConsole include and setEnabled call |
|
||||
|
||||
#### 3. Extended HeadlessTypes.h
|
||||
|
||||
The stub file grew from ~700 lines to ~900 lines with additional types and methods:
|
||||
|
||||
**Types Added:**
|
||||
- `sf::Image` - For screenshot functionality
|
||||
- `sf::Glsl::Vec3`, `sf::Glsl::Vec4` - For shader uniforms
|
||||
- `sf::BlendMode` - For rendering states
|
||||
- `sf::CurrentTextureType` - For shader texture binding
|
||||
|
||||
**Methods Added:**
|
||||
- `Font::Info` struct and `Font::getInfo()`
|
||||
- `Texture::update()` overloads
|
||||
- `Texture::copyToImage()`
|
||||
- `Transform::getInverse()`
|
||||
- `RenderStates` constructors from Transform, BlendMode, Shader*
|
||||
- `Music::getDuration()`, `getPlayingOffset()`, `setPlayingOffset()`
|
||||
- `SoundBuffer::getDuration()`
|
||||
- `RenderWindow::setMouseCursorGrabbed()`
|
||||
- `sf::err()` stream function
|
||||
- Keyboard aliases: `BackSpace`, `BackSlash`, `SemiColon`, `Dash`
|
||||
|
||||
### Build Commands
|
||||
|
||||
```bash
|
||||
# Normal SFML build (default)
|
||||
make
|
||||
|
||||
# Headless build (no SFML/ImGui dependencies)
|
||||
mkdir build-headless && cd build-headless
|
||||
cmake .. -DMCRF_HEADLESS=ON -DCMAKE_BUILD_TYPE=Release
|
||||
make
|
||||
```
|
||||
|
||||
### Key Insight
|
||||
|
||||
The libtcod approach of `#ifndef NO_SDL` guards works when **all platform includes go through a single point**. The consolidation of 15+ bypass points into Common.h was the prerequisite that made this work.
|
||||
|
||||
### Actual Effort
|
||||
|
||||
| Task | Files | Time |
|
||||
|------|-------|------|
|
||||
| Replace direct SFML includes with Common.h | 15 | ~30 min |
|
||||
| Wrap ImGui-SFML in guards | 5 | ~20 min |
|
||||
| Extend HeadlessTypes.h with missing stubs | 1 | ~1 hour |
|
||||
| Fix compilation errors iteratively | - | ~1 hour |
|
||||
|
||||
**Total**: ~3 hours for clean headless compilation
|
||||
|
||||
### Completed Milestones
|
||||
|
||||
1. ✅ **Test Python bindings** - mcrfpy module loads and works in headless mode
|
||||
- Vector, Color, Scene, Frame, Grid all functional
|
||||
- libtcod integrations (BSP, pathfinding) available
|
||||
2. ✅ **Add CMake option** - `option(MCRF_HEADLESS "Build without graphics" OFF)`
|
||||
- Proper conditional compilation and linking
|
||||
- No SFML symbols in headless binary
|
||||
3. ✅ **Link-time validation** - `ldd` confirms zero SFML/OpenGL dependencies
|
||||
4. ✅ **Binary size reduction** - Headless is 1.6 MB vs 2.5 MB normal build (36% smaller)
|
||||
|
||||
### Python Test Results (Headless Mode)
|
||||
|
||||
```python
|
||||
# All these work in headless build:
|
||||
import mcrfpy
|
||||
v = mcrfpy.Vector(10, 20) # ✅
|
||||
c = mcrfpy.Color(255, 128, 64) # ✅
|
||||
scene = mcrfpy.Scene('test') # ✅
|
||||
frame = mcrfpy.Frame(pos=(0,0)) # ✅
|
||||
grid = mcrfpy.Grid(grid_size=(10,10)) # ✅
|
||||
```
|
||||
|
||||
### Remaining Steps for Emscripten
|
||||
|
||||
1. ✅ **Main loop extraction** - `GameEngine::doFrame()` extracted with Emscripten callback support
|
||||
- `run()` now uses `#ifdef __EMSCRIPTEN__` to choose between callback and blocking loop
|
||||
- `emscripten_set_main_loop_arg()` integration ready
|
||||
2. ✅ **Emscripten toolchain** - `emcmake cmake` works with headless mode
|
||||
3. ✅ **Python-in-WASM** - Built CPython 3.14.2 for wasm32-emscripten target
|
||||
- Uses official `Tools/wasm/emscripten build` script from CPython repo
|
||||
- Produced libpython3.14.a (47MB static library)
|
||||
- Also builds: libmpdec, libffi, libexpat for WASM
|
||||
4. ✅ **libtcod-in-WASM** - Built libtcod-headless for Emscripten
|
||||
- Uses `LIBTCOD_SDL3=OFF` to avoid SDL dependency
|
||||
- Includes lodepng and utf8proc dependencies
|
||||
5. ✅ **First successful WASM build** - mcrogueface.wasm (8.9MB) + mcrogueface.js (126KB)
|
||||
- All 68 C++ source files compile with emcc
|
||||
- Links: Python, libtcod, HACL crypto, expat, mpdec, ffi, zlib, bzip2, sqlite3
|
||||
6. 🔲 **Python stdlib bundling** - Need to package Python stdlib for WASM filesystem
|
||||
7. 🔲 **VRSFML integration** - Replace stubs with actual WebGL rendering
|
||||
|
||||
### First Emscripten Build Attempt (2026-01-31)
|
||||
|
||||
**Command:**
|
||||
```bash
|
||||
source ~/emsdk/emsdk_env.sh
|
||||
emcmake cmake .. -DMCRF_HEADLESS=ON -DCMAKE_BUILD_TYPE=Release
|
||||
emmake make -j8
|
||||
```
|
||||
|
||||
**Result:** Build failed on Python headers.
|
||||
|
||||
**Key Errors:**
|
||||
```
|
||||
deps/Python/pyport.h:429:2: error: "LONG_BIT definition appears wrong for platform"
|
||||
```
|
||||
```
|
||||
warning: shift count >= width of type [-Wshift-count-overflow]
|
||||
_Py_STATIC_FLAG_BITS << 48 // 48-bit shift on 32-bit WASM!
|
||||
```
|
||||
|
||||
**Root Cause:**
|
||||
1. Desktop Python 3.14 headers assume 64-bit Linux with glibc
|
||||
2. Emscripten targets 32-bit WASM with musl-based libc
|
||||
3. Python's immortal reference counting uses `<< 48` shifts that overflow on 32-bit
|
||||
4. `LONG_BIT` check fails because WASM's `long` is 32 bits
|
||||
|
||||
**Analysis:**
|
||||
The HeadlessTypes.h stubs and game engine code compile fine. The blocker is exclusively the Python C API integration.
|
||||
|
||||
### Python-in-WASM Options
|
||||
|
||||
| Option | Complexity | Description |
|
||||
|--------|------------|-------------|
|
||||
| **Pyodide** | Medium | Pre-built Python WASM with package ecosystem |
|
||||
| **CPython WASM** | High | Build CPython ourselves with Emscripten |
|
||||
| **No-Python mode** | Low | New CMake option to exclude Python entirely |
|
||||
|
||||
**Pyodide Approach (Recommended):**
|
||||
- Pyodide provides Python 3.12 compiled for WASM
|
||||
- Would need to replace `deps/Python` with Pyodide headers
|
||||
- `McRFPy_API` binding layer needs adaptation
|
||||
- Pyodide handles asyncio, file system virtualization
|
||||
- Active project with good documentation
|
||||
|
||||
### CPython WASM Build (Successful!)
|
||||
|
||||
**Date**: 2026-01-31
|
||||
|
||||
Used the official CPython WASM build process:
|
||||
|
||||
```bash
|
||||
# From deps/cpython directory
|
||||
./Tools/wasm/emscripten build
|
||||
|
||||
# This produces:
|
||||
# - cross-build/wasm32-emscripten/build/python/libpython3.14.a
|
||||
# - cross-build/wasm32-emscripten/prefix/lib/libmpdec.a
|
||||
# - cross-build/wasm32-emscripten/prefix/lib/libffi.a
|
||||
# - cross-build/wasm32-emscripten/build/python/Modules/expat/libexpat.a
|
||||
```
|
||||
|
||||
**CMake Integration:**
|
||||
```cmake
|
||||
if(EMSCRIPTEN)
|
||||
set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python")
|
||||
set(PYTHON_WASM_PREFIX "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/prefix")
|
||||
|
||||
# Force WASM-compatible pyconfig.h
|
||||
add_compile_options(-include ${PYTHON_WASM_BUILD}/pyconfig.h)
|
||||
|
||||
# Link all Python dependencies
|
||||
set(LINK_LIBS
|
||||
${PYTHON_WASM_BUILD}/libpython3.14.a
|
||||
${PYTHON_WASM_BUILD}/Modules/_hacl/*.o # HACL crypto not in libpython
|
||||
${PYTHON_WASM_BUILD}/Modules/expat/libexpat.a
|
||||
${PYTHON_WASM_PREFIX}/lib/libmpdec.a
|
||||
${PYTHON_WASM_PREFIX}/lib/libffi.a
|
||||
)
|
||||
|
||||
# Emscripten ports for common libraries
|
||||
target_link_options(mcrogueface PRIVATE
|
||||
-sUSE_ZLIB=1
|
||||
-sUSE_BZIP2=1
|
||||
-sUSE_SQLITE3=1
|
||||
)
|
||||
endif()
|
||||
```
|
||||
|
||||
**No-Python Mode (For Testing):**
|
||||
- Add `MCRF_NO_PYTHON` CMake option
|
||||
- Allows testing WASM build without Python complexity
|
||||
- Game engine would be pure C++ (no scripting)
|
||||
- Useful for validating rendering, input, timing first
|
||||
|
||||
### Main Loop Architecture
|
||||
|
||||
The game loop now supports both desktop (blocking) and browser (callback) modes:
|
||||
|
||||
```cpp
|
||||
// GameEngine::run() - build-time conditional
|
||||
#ifdef __EMSCRIPTEN__
|
||||
emscripten_set_main_loop_arg(emscriptenMainLoopCallback, this, 0, 1);
|
||||
#else
|
||||
while (running) { doFrame(); }
|
||||
#endif
|
||||
|
||||
// GameEngine::doFrame() - same code runs in both modes
|
||||
void GameEngine::doFrame() {
|
||||
metrics.resetPerFrame();
|
||||
currentScene()->update();
|
||||
testTimers();
|
||||
// ... animations, input, rendering ...
|
||||
currentFrame++;
|
||||
frameTime = clock.restart().asSeconds();
|
||||
}
|
||||
```
|
||||
|
|
@ -1,238 +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
|
||||
|
|
@ -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,944 +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` | `() -> tuple` |
|
||||
| `peek` | `() -> tuple` |
|
||||
|
||||
Protocols: `len`, `bool`, iteration
|
||||
|
||||
### `DijkstraMap`
|
||||
|
||||
| Properties | Type | R/W |
|
||||
|-----------|------|-----|
|
||||
| `root` | tuple | R |
|
||||
|
||||
| Methods | Signature |
|
||||
|---------|-----------|
|
||||
| `distance` | `(x, y) -> float` |
|
||||
| `path_from` | `(x, y) -> list` |
|
||||
| `step_from` | `(x, y) -> tuple` |
|
||||
| `to_heightmap` | `() -> 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** |
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
"""McRogueFace - Animated Movement (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_animated_movement
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_animated_movement_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
if new_x != current_x:
|
||||
anim = mcrfpy.Animation("x", float(new_x), duration, "easeInOut", callback=done)
|
||||
else:
|
||||
anim = mcrfpy.Animation("y", float(new_y), duration, "easeInOut", callback=done)
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
"""McRogueFace - Animated Movement (basic_2)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_animated_movement
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_animated_movement_basic_2.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
current_anim = mcrfpy.Animation("x", 100.0, 0.5, "linear")
|
||||
current_anim.start(entity)
|
||||
# Later: current_anim = None # Let it complete or create new one
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
"""McRogueFace - Basic Enemy AI (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
def wander(enemy, grid):
|
||||
"""Move randomly to an adjacent walkable tile."""
|
||||
ex, ey = int(enemy.x), int(enemy.y)
|
||||
|
||||
# Get valid adjacent tiles
|
||||
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
|
||||
random.shuffle(directions)
|
||||
|
||||
for dx, dy in directions:
|
||||
new_x, new_y = ex + dx, ey + dy
|
||||
|
||||
if is_walkable(grid, new_x, new_y) and not is_occupied(new_x, new_y):
|
||||
enemy.x = new_x
|
||||
enemy.y = new_y
|
||||
return
|
||||
|
||||
# No valid moves - stay in place
|
||||
|
||||
def is_walkable(grid, x, y):
|
||||
"""Check if a tile can be walked on."""
|
||||
grid_w, grid_h = grid.grid_size
|
||||
if x < 0 or x >= grid_w or y < 0 or y >= grid_h:
|
||||
return False
|
||||
return grid.at(x, y).walkable
|
||||
|
||||
def is_occupied(x, y, entities=None):
|
||||
"""Check if a tile is occupied by another entity."""
|
||||
if entities is None:
|
||||
return False
|
||||
|
||||
for entity in entities:
|
||||
if int(entity.x) == x and int(entity.y) == y:
|
||||
return True
|
||||
return False
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
"""McRogueFace - Basic Enemy AI (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
# Filter to cardinal directions only
|
||||
path = [p for p in path if abs(p[0] - ex) + abs(p[1] - ey) == 1]
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
"""McRogueFace - Basic Enemy AI (multi_2)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_multi_2.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def alert_nearby(x, y, radius, enemies):
|
||||
for enemy in enemies:
|
||||
dist = abs(enemy.entity.x - x) + abs(enemy.entity.y - y)
|
||||
if dist <= radius and hasattr(enemy.ai, 'alert'):
|
||||
enemy.ai.alert = True
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
"""McRogueFace - Melee Combat System (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class CombatLog:
|
||||
"""Scrolling combat message log."""
|
||||
|
||||
def __init__(self, x, y, width, height, max_messages=10):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.max_messages = max_messages
|
||||
self.messages = []
|
||||
self.captions = []
|
||||
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
# Background
|
||||
self.frame = mcrfpy.Frame(x, y, width, height)
|
||||
self.frame.fill_color = mcrfpy.Color(0, 0, 0, 180)
|
||||
ui.append(self.frame)
|
||||
|
||||
def add_message(self, text, color=None):
|
||||
"""Add a message to the log."""
|
||||
if color is None:
|
||||
color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
self.messages.append((text, color))
|
||||
|
||||
# Keep only recent messages
|
||||
if len(self.messages) > self.max_messages:
|
||||
self.messages.pop(0)
|
||||
|
||||
self._refresh_display()
|
||||
|
||||
def _refresh_display(self):
|
||||
"""Redraw all messages."""
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
# Remove old captions
|
||||
for caption in self.captions:
|
||||
try:
|
||||
ui.remove(caption)
|
||||
except:
|
||||
pass
|
||||
self.captions.clear()
|
||||
|
||||
# Create new captions
|
||||
line_height = 18
|
||||
for i, (text, color) in enumerate(self.messages):
|
||||
caption = mcrfpy.Caption(text, self.x + 5, self.y + 5 + i * line_height)
|
||||
caption.fill_color = color
|
||||
ui.append(caption)
|
||||
self.captions.append(caption)
|
||||
|
||||
def log_attack(self, attacker_name, defender_name, damage, killed=False, critical=False):
|
||||
"""Log an attack event."""
|
||||
if critical:
|
||||
text = f"{attacker_name} CRITS {defender_name} for {damage}!"
|
||||
color = mcrfpy.Color(255, 255, 0)
|
||||
else:
|
||||
text = f"{attacker_name} hits {defender_name} for {damage}."
|
||||
color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
self.add_message(text, color)
|
||||
|
||||
if killed:
|
||||
self.add_message(f"{defender_name} is defeated!", mcrfpy.Color(255, 100, 100))
|
||||
|
||||
|
||||
# Global combat log
|
||||
combat_log = None
|
||||
|
||||
def init_combat_log():
|
||||
global combat_log
|
||||
combat_log = CombatLog(10, 500, 400, 200)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
"""McRogueFace - Melee Combat System (complete)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_complete.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def die_with_animation(entity):
|
||||
# Play death animation
|
||||
anim = mcrfpy.Animation("opacity", 0.0, 0.5, "linear")
|
||||
anim.start(entity)
|
||||
# Remove after animation
|
||||
mcrfpy.setTimer("remove", lambda dt: remove_entity(entity), 500)
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
"""McRogueFace - Melee Combat System (complete_2)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_complete_2.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
@dataclass
|
||||
class AdvancedFighter(Fighter):
|
||||
fire_resist: float = 0.0
|
||||
ice_resist: float = 0.0
|
||||
physical_resist: float = 0.0
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
"""McRogueFace - Status Effects (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class StackableEffect(StatusEffect):
|
||||
"""Effect that stacks intensity."""
|
||||
|
||||
def __init__(self, name, duration, intensity=1, max_stacks=5, **kwargs):
|
||||
super().__init__(name, duration, **kwargs)
|
||||
self.intensity = intensity
|
||||
self.max_stacks = max_stacks
|
||||
self.stacks = 1
|
||||
|
||||
def add_stack(self):
|
||||
"""Add another stack."""
|
||||
if self.stacks < self.max_stacks:
|
||||
self.stacks += 1
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class StackingEffectManager(EffectManager):
|
||||
"""Effect manager with stacking support."""
|
||||
|
||||
def add_effect(self, effect):
|
||||
if isinstance(effect, StackableEffect):
|
||||
# Check for existing stacks
|
||||
for existing in self.effects:
|
||||
if existing.name == effect.name:
|
||||
if existing.add_stack():
|
||||
# Refresh duration
|
||||
existing.duration = max(existing.duration, effect.duration)
|
||||
return
|
||||
else:
|
||||
return # Max stacks
|
||||
|
||||
# Default behavior
|
||||
super().add_effect(effect)
|
||||
|
||||
|
||||
# Stacking poison example
|
||||
def create_stacking_poison(base_damage=1, duration=5):
|
||||
def on_tick(target):
|
||||
# Find the poison effect to get stack count
|
||||
effect = target.effects.get_effect("poison")
|
||||
if effect:
|
||||
damage = base_damage * effect.stacks
|
||||
target.hp -= damage
|
||||
print(f"{target.name} takes {damage} poison damage! ({effect.stacks} stacks)")
|
||||
|
||||
return StackableEffect("poison", duration, on_tick=on_tick, max_stacks=5)
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
"""McRogueFace - Status Effects (basic_2)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic_2.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def apply_effect(self, effect):
|
||||
if effect.name in self.immunities:
|
||||
print(f"{self.name} is immune to {effect.name}!")
|
||||
return
|
||||
if effect.name in self.resistances:
|
||||
effect.duration //= 2 # Half duration
|
||||
self.effects.add_effect(effect)
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
"""McRogueFace - Status Effects (basic_3)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic_3.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def serialize_effects(effect_manager):
|
||||
return [{"name": e.name, "duration": e.duration}
|
||||
for e in effect_manager.effects]
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
"""McRogueFace - Turn-Based Game Loop (combat_turn_system)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_turn_system
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_turn_system.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def create_turn_order_ui(turn_manager, x=800, y=50):
|
||||
"""Create a visual turn order display."""
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
# Background frame
|
||||
frame = mcrfpy.Frame(x, y, 200, 300)
|
||||
frame.fill_color = mcrfpy.Color(30, 30, 30, 200)
|
||||
frame.outline = 2
|
||||
frame.outline_color = mcrfpy.Color(100, 100, 100)
|
||||
ui.append(frame)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Turn Order", x + 10, y + 10)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
return frame
|
||||
|
||||
def update_turn_order_display(frame, turn_manager, x=800, y=50):
|
||||
"""Update the turn order display."""
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
# Clear old entries (keep frame and title)
|
||||
# In practice, store references to caption objects and update them
|
||||
|
||||
for i, actor_data in enumerate(turn_manager.actors):
|
||||
actor = actor_data["actor"]
|
||||
is_current = (i == turn_manager.current)
|
||||
|
||||
# Actor name/type
|
||||
name = getattr(actor, 'name', f"Actor {i}")
|
||||
color = mcrfpy.Color(255, 255, 0) if is_current else mcrfpy.Color(200, 200, 200)
|
||||
|
||||
caption = mcrfpy.Caption(name, x + 10, y + 40 + i * 25)
|
||||
caption.fill_color = color
|
||||
ui.append(caption)
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
"""McRogueFace - Color Pulse Effect (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_color_pulse
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_color_pulse_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class PulsingCell:
|
||||
"""A cell that continuously pulses until stopped."""
|
||||
|
||||
def __init__(self, grid, x, y, color, period=1.0, max_alpha=180):
|
||||
"""
|
||||
Args:
|
||||
grid: Grid with color layer
|
||||
x, y: Cell position
|
||||
color: RGB tuple
|
||||
period: Time for one complete pulse cycle
|
||||
max_alpha: Maximum alpha value (0-255)
|
||||
"""
|
||||
self.grid = grid
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.color = color
|
||||
self.period = period
|
||||
self.max_alpha = max_alpha
|
||||
self.is_pulsing = False
|
||||
self.pulse_id = 0
|
||||
self.cell = None
|
||||
|
||||
self._setup_layer()
|
||||
|
||||
def _setup_layer(self):
|
||||
"""Ensure color layer exists and get cell reference."""
|
||||
color_layer = None
|
||||
for layer in self.grid.layers:
|
||||
if isinstance(layer, mcrfpy.ColorLayer):
|
||||
color_layer = layer
|
||||
break
|
||||
|
||||
if not color_layer:
|
||||
self.grid.add_layer("color")
|
||||
color_layer = self.grid.layers[-1]
|
||||
|
||||
self.cell = color_layer.at(self.x, self.y)
|
||||
if self.cell:
|
||||
self.cell.color = mcrfpy.Color(self.color[0], self.color[1],
|
||||
self.color[2], 0)
|
||||
|
||||
def start(self):
|
||||
"""Start continuous pulsing."""
|
||||
if self.is_pulsing or not self.cell:
|
||||
return
|
||||
|
||||
self.is_pulsing = True
|
||||
self.pulse_id += 1
|
||||
self._pulse_up()
|
||||
|
||||
def _pulse_up(self):
|
||||
"""Animate alpha increasing."""
|
||||
if not self.is_pulsing:
|
||||
return
|
||||
|
||||
current_id = self.pulse_id
|
||||
half_period = self.period / 2
|
||||
|
||||
anim = mcrfpy.Animation("a", float(self.max_alpha), half_period, "easeInOut")
|
||||
anim.start(self.cell.color)
|
||||
|
||||
def next_phase(timer_name):
|
||||
if self.is_pulsing and self.pulse_id == current_id:
|
||||
self._pulse_down()
|
||||
|
||||
mcrfpy.Timer(f"pulse_up_{id(self)}_{current_id}",
|
||||
next_phase, int(half_period * 1000), once=True)
|
||||
|
||||
def _pulse_down(self):
|
||||
"""Animate alpha decreasing."""
|
||||
if not self.is_pulsing:
|
||||
return
|
||||
|
||||
current_id = self.pulse_id
|
||||
half_period = self.period / 2
|
||||
|
||||
anim = mcrfpy.Animation("a", 0.0, half_period, "easeInOut")
|
||||
anim.start(self.cell.color)
|
||||
|
||||
def next_phase(timer_name):
|
||||
if self.is_pulsing and self.pulse_id == current_id:
|
||||
self._pulse_up()
|
||||
|
||||
mcrfpy.Timer(f"pulse_down_{id(self)}_{current_id}",
|
||||
next_phase, int(half_period * 1000), once=True)
|
||||
|
||||
def stop(self):
|
||||
"""Stop pulsing and fade out."""
|
||||
self.is_pulsing = False
|
||||
if self.cell:
|
||||
anim = mcrfpy.Animation("a", 0.0, 0.2, "easeOut")
|
||||
anim.start(self.cell.color)
|
||||
|
||||
def set_color(self, color):
|
||||
"""Change pulse color."""
|
||||
self.color = color
|
||||
if self.cell:
|
||||
current_alpha = self.cell.color.a
|
||||
self.cell.color = mcrfpy.Color(color[0], color[1], color[2], current_alpha)
|
||||
|
||||
|
||||
# Usage
|
||||
objective_pulse = PulsingCell(grid, 10, 10, (0, 255, 100), period=1.5)
|
||||
objective_pulse.start()
|
||||
|
||||
# Later, when objective is reached:
|
||||
objective_pulse.stop()
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
"""McRogueFace - Color Pulse Effect (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_color_pulse
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_color_pulse_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
def ripple_effect(grid, center_x, center_y, color, max_radius=5, duration=1.0):
|
||||
"""
|
||||
Create an expanding ripple effect.
|
||||
|
||||
Args:
|
||||
grid: Grid with color layer
|
||||
center_x, center_y: Ripple origin
|
||||
color: RGB tuple
|
||||
max_radius: Maximum ripple size
|
||||
duration: Total animation time
|
||||
"""
|
||||
# Get color layer
|
||||
color_layer = None
|
||||
for layer in grid.layers:
|
||||
if isinstance(layer, mcrfpy.ColorLayer):
|
||||
color_layer = layer
|
||||
break
|
||||
|
||||
if not color_layer:
|
||||
grid.add_layer("color")
|
||||
color_layer = grid.layers[-1]
|
||||
|
||||
step_duration = duration / max_radius
|
||||
|
||||
for radius in range(max_radius + 1):
|
||||
# Get cells at this radius (ring, not filled)
|
||||
ring_cells = []
|
||||
for dy in range(-radius, radius + 1):
|
||||
for dx in range(-radius, radius + 1):
|
||||
dist_sq = dx * dx + dy * dy
|
||||
# Include cells approximately on the ring edge
|
||||
if radius * radius - radius <= dist_sq <= radius * radius + radius:
|
||||
cell = color_layer.at(center_x + dx, center_y + dy)
|
||||
if cell:
|
||||
ring_cells.append(cell)
|
||||
|
||||
# Schedule this ring to animate
|
||||
def animate_ring(timer_name, cells=ring_cells, c=color):
|
||||
for cell in cells:
|
||||
cell.color = mcrfpy.Color(c[0], c[1], c[2], 200)
|
||||
# Fade out
|
||||
anim = mcrfpy.Animation("a", 0.0, step_duration * 2, "easeOut")
|
||||
anim.start(cell.color)
|
||||
|
||||
delay = int(radius * step_duration * 1000)
|
||||
mcrfpy.Timer(f"ripple_{radius}", animate_ring, delay, once=True)
|
||||
|
||||
|
||||
# Usage
|
||||
ripple_effect(grid, 10, 10, (100, 200, 255), max_radius=6, duration=0.8)
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
"""McRogueFace - Damage Flash Effect (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Add a color layer to your grid (do this once during setup)
|
||||
grid.add_layer("color")
|
||||
color_layer = grid.layers[-1] # Get the color layer
|
||||
|
||||
def flash_cell(grid, x, y, color, duration=0.3):
|
||||
"""Flash a grid cell with a color overlay."""
|
||||
# Get the color layer (assumes it's the last layer added)
|
||||
color_layer = None
|
||||
for layer in grid.layers:
|
||||
if isinstance(layer, mcrfpy.ColorLayer):
|
||||
color_layer = layer
|
||||
break
|
||||
|
||||
if not color_layer:
|
||||
return
|
||||
|
||||
# Set cell to flash color
|
||||
cell = color_layer.at(x, y)
|
||||
cell.color = mcrfpy.Color(color[0], color[1], color[2], 200)
|
||||
|
||||
# Animate alpha back to 0
|
||||
anim = mcrfpy.Animation("a", 0.0, duration, "easeOut")
|
||||
anim.start(cell.color)
|
||||
|
||||
def damage_at_position(grid, x, y, duration=0.3):
|
||||
"""Flash red at a grid position when damage occurs."""
|
||||
flash_cell(grid, x, y, (255, 0, 0), duration)
|
||||
|
||||
# Usage when entity takes damage
|
||||
damage_at_position(grid, int(enemy.x), int(enemy.y))
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
"""McRogueFace - Damage Flash Effect (complete)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_complete.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class DamageEffects:
|
||||
"""Manages visual damage feedback effects."""
|
||||
|
||||
# Color presets
|
||||
DAMAGE_RED = (255, 50, 50)
|
||||
HEAL_GREEN = (50, 255, 50)
|
||||
POISON_PURPLE = (150, 50, 200)
|
||||
FIRE_ORANGE = (255, 150, 50)
|
||||
ICE_BLUE = (100, 200, 255)
|
||||
|
||||
def __init__(self, grid):
|
||||
self.grid = grid
|
||||
self.color_layer = None
|
||||
self._setup_color_layer()
|
||||
|
||||
def _setup_color_layer(self):
|
||||
"""Ensure grid has a color layer for effects."""
|
||||
self.grid.add_layer("color")
|
||||
self.color_layer = self.grid.layers[-1]
|
||||
|
||||
def flash_entity(self, entity, color, duration=0.3):
|
||||
"""Flash an entity with a color tint."""
|
||||
# Flash at entity's grid position
|
||||
x, y = int(entity.x), int(entity.y)
|
||||
self.flash_cell(x, y, color, duration)
|
||||
|
||||
def flash_cell(self, x, y, color, duration=0.3):
|
||||
"""Flash a specific grid cell."""
|
||||
if not self.color_layer:
|
||||
return
|
||||
|
||||
cell = self.color_layer.at(x, y)
|
||||
if cell:
|
||||
cell.color = mcrfpy.Color(color[0], color[1], color[2], 180)
|
||||
|
||||
# Fade out
|
||||
anim = mcrfpy.Animation("a", 0.0, duration, "easeOut")
|
||||
anim.start(cell.color)
|
||||
|
||||
def damage(self, entity, amount, duration=0.3):
|
||||
"""Standard damage flash."""
|
||||
self.flash_entity(entity, self.DAMAGE_RED, duration)
|
||||
|
||||
def heal(self, entity, amount, duration=0.4):
|
||||
"""Healing effect - green flash."""
|
||||
self.flash_entity(entity, self.HEAL_GREEN, duration)
|
||||
|
||||
def poison(self, entity, duration=0.5):
|
||||
"""Poison damage - purple flash."""
|
||||
self.flash_entity(entity, self.POISON_PURPLE, duration)
|
||||
|
||||
def fire(self, entity, duration=0.3):
|
||||
"""Fire damage - orange flash."""
|
||||
self.flash_entity(entity, self.FIRE_ORANGE, duration)
|
||||
|
||||
def ice(self, entity, duration=0.4):
|
||||
"""Ice damage - blue flash."""
|
||||
self.flash_entity(entity, self.ICE_BLUE, duration)
|
||||
|
||||
def area_damage(self, center_x, center_y, radius, color, duration=0.4):
|
||||
"""Flash all cells in a radius."""
|
||||
for dy in range(-radius, radius + 1):
|
||||
for dx in range(-radius, radius + 1):
|
||||
if dx * dx + dy * dy <= radius * radius:
|
||||
self.flash_cell(center_x + dx, center_y + dy, color, duration)
|
||||
|
||||
# Setup
|
||||
effects = DamageEffects(grid)
|
||||
|
||||
# Usage examples
|
||||
effects.damage(player, 10) # Red flash
|
||||
effects.heal(player, 5) # Green flash
|
||||
effects.poison(enemy) # Purple flash
|
||||
effects.area_damage(5, 5, 3, effects.FIRE_ORANGE) # Area effect
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
"""McRogueFace - Damage Flash Effect (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
def multi_flash(grid, x, y, color, flashes=3, flash_duration=0.1):
|
||||
"""Flash a cell multiple times for emphasis."""
|
||||
delay = 0
|
||||
|
||||
for i in range(flashes):
|
||||
# Schedule each flash with increasing delay
|
||||
def do_flash(timer_name, fx=x, fy=y, fc=color, fd=flash_duration):
|
||||
flash_cell(grid, fx, fy, fc, fd)
|
||||
|
||||
mcrfpy.Timer(f"flash_{x}_{y}_{i}", do_flash, int(delay * 1000), once=True)
|
||||
delay += flash_duration * 1.5 # Gap between flashes
|
||||
|
||||
# Usage for critical hit
|
||||
multi_flash(grid, int(enemy.x), int(enemy.y), (255, 255, 0), flashes=3)
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
"""McRogueFace - Floating Damage Numbers (effects_floating_text)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_floating_text
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_floating_text.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class StackedFloatingText:
|
||||
"""Prevents overlapping text by stacking vertically."""
|
||||
|
||||
def __init__(self, scene_name, grid=None):
|
||||
self.manager = FloatingTextManager(scene_name, grid)
|
||||
self.position_stack = {} # Track recent spawns per position
|
||||
|
||||
def spawn_stacked(self, x, y, text, color, **kwargs):
|
||||
"""Spawn with automatic vertical stacking."""
|
||||
key = (int(x), int(y))
|
||||
|
||||
# Calculate offset based on recent spawns at this position
|
||||
offset = self.position_stack.get(key, 0)
|
||||
actual_y = y - (offset * 20) # 20 pixels between stacked texts
|
||||
|
||||
self.manager.spawn(x, actual_y, text, color, **kwargs)
|
||||
|
||||
# Increment stack counter
|
||||
self.position_stack[key] = offset + 1
|
||||
|
||||
# Reset stack after delay
|
||||
def reset_stack(timer_name, k=key):
|
||||
if k in self.position_stack:
|
||||
self.position_stack[k] = max(0, self.position_stack[k] - 1)
|
||||
|
||||
mcrfpy.Timer(f"stack_reset_{x}_{y}_{offset}", reset_stack, 300, once=True)
|
||||
|
||||
# Usage
|
||||
stacked = StackedFloatingText("game", grid)
|
||||
# Rapid hits will stack vertically instead of overlapping
|
||||
stacked.spawn_stacked(5, 5, "-10", (255, 0, 0), is_grid_pos=True)
|
||||
stacked.spawn_stacked(5, 5, "-8", (255, 0, 0), is_grid_pos=True)
|
||||
stacked.spawn_stacked(5, 5, "-12", (255, 0, 0), is_grid_pos=True)
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
"""McRogueFace - Path Animation (Multi-Step Movement) (effects_path_animation)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_path_animation
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_path_animation.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class CameraFollowingPath:
|
||||
"""Path animator that also moves the camera."""
|
||||
|
||||
def __init__(self, entity, grid, path, step_duration=0.2):
|
||||
self.entity = entity
|
||||
self.grid = grid
|
||||
self.path = path
|
||||
self.step_duration = step_duration
|
||||
self.index = 0
|
||||
self.on_complete = None
|
||||
|
||||
def start(self):
|
||||
self.index = 0
|
||||
self._next()
|
||||
|
||||
def _next(self):
|
||||
if self.index >= len(self.path):
|
||||
if self.on_complete:
|
||||
self.on_complete(self)
|
||||
return
|
||||
|
||||
x, y = self.path[self.index]
|
||||
|
||||
def done(anim, target):
|
||||
self.index += 1
|
||||
self._next()
|
||||
|
||||
# Animate entity
|
||||
if self.entity.x != x:
|
||||
anim = mcrfpy.Animation("x", float(x), self.step_duration,
|
||||
"easeInOut", callback=done)
|
||||
anim.start(self.entity)
|
||||
elif self.entity.y != y:
|
||||
anim = mcrfpy.Animation("y", float(y), self.step_duration,
|
||||
"easeInOut", callback=done)
|
||||
anim.start(self.entity)
|
||||
else:
|
||||
done(None, None)
|
||||
return
|
||||
|
||||
# Animate camera to follow
|
||||
cam_x = mcrfpy.Animation("center_x", (x + 0.5) * 16,
|
||||
self.step_duration, "easeInOut")
|
||||
cam_y = mcrfpy.Animation("center_y", (y + 0.5) * 16,
|
||||
self.step_duration, "easeInOut")
|
||||
cam_x.start(self.grid)
|
||||
cam_y.start(self.grid)
|
||||
|
||||
|
||||
# Usage
|
||||
path = [(5, 5), (5, 10), (10, 10)]
|
||||
mover = CameraFollowingPath(player, grid, path)
|
||||
mover.on_complete = lambda m: print("Journey complete!")
|
||||
mover.start()
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
"""McRogueFace - Scene Transition Effects (effects_scene_transitions)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_scene_transitions
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_scene_transitions.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class TransitionManager:
|
||||
"""Manages scene transitions with multiple effect types."""
|
||||
|
||||
def __init__(self, screen_width=1024, screen_height=768):
|
||||
self.width = screen_width
|
||||
self.height = screen_height
|
||||
self.is_transitioning = False
|
||||
|
||||
def go_to(self, scene_name, effect="fade", duration=0.5, **kwargs):
|
||||
"""
|
||||
Transition to a scene with the specified effect.
|
||||
|
||||
Args:
|
||||
scene_name: Target scene
|
||||
effect: "fade", "flash", "wipe", "instant"
|
||||
duration: Transition duration
|
||||
**kwargs: Effect-specific options (color, direction)
|
||||
"""
|
||||
if self.is_transitioning:
|
||||
return
|
||||
|
||||
self.is_transitioning = True
|
||||
|
||||
if effect == "instant":
|
||||
mcrfpy.setScene(scene_name)
|
||||
self.is_transitioning = False
|
||||
|
||||
elif effect == "fade":
|
||||
color = kwargs.get("color", (0, 0, 0))
|
||||
self._fade(scene_name, duration, color)
|
||||
|
||||
elif effect == "flash":
|
||||
color = kwargs.get("color", (255, 255, 255))
|
||||
self._flash(scene_name, duration, color)
|
||||
|
||||
elif effect == "wipe":
|
||||
direction = kwargs.get("direction", "right")
|
||||
color = kwargs.get("color", (0, 0, 0))
|
||||
self._wipe(scene_name, duration, direction, color)
|
||||
|
||||
def _fade(self, scene, duration, color):
|
||||
half = duration / 2
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 0)
|
||||
overlay.z_index = 9999
|
||||
ui.append(overlay)
|
||||
|
||||
anim = mcrfpy.Animation("opacity", 1.0, half, "easeIn")
|
||||
anim.start(overlay)
|
||||
|
||||
def phase2(timer_name):
|
||||
mcrfpy.setScene(scene)
|
||||
new_ui = mcrfpy.sceneUI(scene)
|
||||
|
||||
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
|
||||
new_overlay.z_index = 9999
|
||||
new_ui.append(new_overlay)
|
||||
|
||||
anim2 = mcrfpy.Animation("opacity", 0.0, half, "easeOut")
|
||||
anim2.start(new_overlay)
|
||||
|
||||
def cleanup(timer_name):
|
||||
for i, elem in enumerate(new_ui):
|
||||
if elem is new_overlay:
|
||||
new_ui.remove(i)
|
||||
break
|
||||
self.is_transitioning = False
|
||||
|
||||
mcrfpy.Timer("fade_done", cleanup, int(half * 1000) + 50, once=True)
|
||||
|
||||
mcrfpy.Timer("fade_switch", phase2, int(half * 1000), once=True)
|
||||
|
||||
def _flash(self, scene, duration, color):
|
||||
quarter = duration / 4
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 0)
|
||||
overlay.z_index = 9999
|
||||
ui.append(overlay)
|
||||
|
||||
anim = mcrfpy.Animation("opacity", 1.0, quarter, "easeOut")
|
||||
anim.start(overlay)
|
||||
|
||||
def phase2(timer_name):
|
||||
mcrfpy.setScene(scene)
|
||||
new_ui = mcrfpy.sceneUI(scene)
|
||||
|
||||
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
|
||||
new_overlay.z_index = 9999
|
||||
new_ui.append(new_overlay)
|
||||
|
||||
anim2 = mcrfpy.Animation("opacity", 0.0, duration / 2, "easeIn")
|
||||
anim2.start(new_overlay)
|
||||
|
||||
def cleanup(timer_name):
|
||||
for i, elem in enumerate(new_ui):
|
||||
if elem is new_overlay:
|
||||
new_ui.remove(i)
|
||||
break
|
||||
self.is_transitioning = False
|
||||
|
||||
mcrfpy.Timer("flash_done", cleanup, int(duration * 500) + 50, once=True)
|
||||
|
||||
mcrfpy.Timer("flash_switch", phase2, int(quarter * 2000), once=True)
|
||||
|
||||
def _wipe(self, scene, duration, direction, color):
|
||||
# Simplified wipe - right direction only for brevity
|
||||
half = duration / 2
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
overlay = mcrfpy.Frame(0, 0, 0, self.height)
|
||||
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
|
||||
overlay.z_index = 9999
|
||||
ui.append(overlay)
|
||||
|
||||
anim = mcrfpy.Animation("w", float(self.width), half, "easeInOut")
|
||||
anim.start(overlay)
|
||||
|
||||
def phase2(timer_name):
|
||||
mcrfpy.setScene(scene)
|
||||
new_ui = mcrfpy.sceneUI(scene)
|
||||
|
||||
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
|
||||
new_overlay.z_index = 9999
|
||||
new_ui.append(new_overlay)
|
||||
|
||||
anim2 = mcrfpy.Animation("x", float(self.width), half, "easeInOut")
|
||||
anim2.start(new_overlay)
|
||||
|
||||
def cleanup(timer_name):
|
||||
for i, elem in enumerate(new_ui):
|
||||
if elem is new_overlay:
|
||||
new_ui.remove(i)
|
||||
break
|
||||
self.is_transitioning = False
|
||||
|
||||
mcrfpy.Timer("wipe_done", cleanup, int(half * 1000) + 50, once=True)
|
||||
|
||||
mcrfpy.Timer("wipe_switch", phase2, int(half * 1000), once=True)
|
||||
|
||||
|
||||
# Usage
|
||||
transitions = TransitionManager()
|
||||
|
||||
# Various transition styles
|
||||
transitions.go_to("game", effect="fade", duration=0.5)
|
||||
transitions.go_to("menu", effect="flash", color=(255, 255, 255), duration=0.4)
|
||||
transitions.go_to("next_level", effect="wipe", direction="right", duration=0.6)
|
||||
transitions.go_to("options", effect="instant")
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
"""McRogueFace - Screen Shake Effect (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_screen_shake
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_screen_shake_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
def screen_shake(frame, intensity=5, duration=0.2):
|
||||
"""
|
||||
Shake a frame/container by animating its position.
|
||||
|
||||
Args:
|
||||
frame: The UI Frame to shake (often a container for all game elements)
|
||||
intensity: Maximum pixel offset
|
||||
duration: Total shake duration in seconds
|
||||
"""
|
||||
original_x = frame.x
|
||||
original_y = frame.y
|
||||
|
||||
# Quick shake to offset position
|
||||
shake_x = mcrfpy.Animation("x", float(original_x + intensity), duration / 4, "easeOut")
|
||||
shake_x.start(frame)
|
||||
|
||||
# Schedule return to center
|
||||
def return_to_center(timer_name):
|
||||
anim = mcrfpy.Animation("x", float(original_x), duration / 2, "easeInOut")
|
||||
anim.start(frame)
|
||||
|
||||
mcrfpy.Timer("shake_return", return_to_center, int(duration * 250), once=True)
|
||||
|
||||
# Usage - wrap your game content in a Frame
|
||||
game_container = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
# ... add game elements to game_container.children ...
|
||||
screen_shake(game_container, intensity=8, duration=0.3)
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
"""McRogueFace - Screen Shake Effect (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_screen_shake
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_screen_shake_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import math
|
||||
|
||||
def directional_shake(shaker, direction_x, direction_y, intensity=10, duration=0.2):
|
||||
"""
|
||||
Shake in a specific direction (e.g., direction of impact).
|
||||
|
||||
Args:
|
||||
shaker: ScreenShakeManager instance
|
||||
direction_x, direction_y: Direction vector (will be normalized)
|
||||
intensity: Shake strength
|
||||
duration: Shake duration
|
||||
"""
|
||||
# Normalize direction
|
||||
length = math.sqrt(direction_x * direction_x + direction_y * direction_y)
|
||||
if length == 0:
|
||||
return
|
||||
|
||||
dir_x = direction_x / length
|
||||
dir_y = direction_y / length
|
||||
|
||||
# Shake in the direction, then opposite, then back
|
||||
shaker._animate_position(
|
||||
shaker.original_x + dir_x * intensity,
|
||||
shaker.original_y + dir_y * intensity,
|
||||
duration / 3
|
||||
)
|
||||
|
||||
def reverse(timer_name):
|
||||
shaker._animate_position(
|
||||
shaker.original_x - dir_x * intensity * 0.5,
|
||||
shaker.original_y - dir_y * intensity * 0.5,
|
||||
duration / 3
|
||||
)
|
||||
|
||||
def reset(timer_name):
|
||||
shaker._animate_position(
|
||||
shaker.original_x,
|
||||
shaker.original_y,
|
||||
duration / 3
|
||||
)
|
||||
shaker.is_shaking = False
|
||||
|
||||
mcrfpy.Timer("dir_shake_rev", reverse, int(duration * 333), once=True)
|
||||
mcrfpy.Timer("dir_shake_reset", reset, int(duration * 666), once=True)
|
||||
|
||||
# Usage: shake away from impact direction
|
||||
hit_from_x, hit_from_y = -1, 0 # Hit from the left
|
||||
directional_shake(shaker, hit_from_x, hit_from_y, intensity=12)
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
"""McRogueFace - Cell Highlighting (Targeting) (animated)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_animated.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class TargetingSystem:
|
||||
"""Handle ability targeting with visual feedback."""
|
||||
|
||||
def __init__(self, grid, player):
|
||||
self.grid = grid
|
||||
self.player = player
|
||||
self.highlights = HighlightManager(grid)
|
||||
self.current_ability = None
|
||||
self.valid_targets = set()
|
||||
|
||||
def start_targeting(self, ability):
|
||||
"""Begin targeting for an ability."""
|
||||
self.current_ability = ability
|
||||
px, py = self.player.pos
|
||||
|
||||
# Get valid targets based on ability
|
||||
if ability.target_type == 'self':
|
||||
self.valid_targets = {(px, py)}
|
||||
elif ability.target_type == 'adjacent':
|
||||
self.valid_targets = get_adjacent(px, py)
|
||||
elif ability.target_type == 'ranged':
|
||||
self.valid_targets = get_radius_range(px, py, ability.range)
|
||||
elif ability.target_type == 'line':
|
||||
self.valid_targets = get_line_range(px, py, ability.range)
|
||||
|
||||
# Filter to visible tiles only
|
||||
self.valid_targets = {
|
||||
(x, y) for x, y in self.valid_targets
|
||||
if grid.is_in_fov(x, y)
|
||||
}
|
||||
|
||||
# Show valid targets
|
||||
self.highlights.add('attack', self.valid_targets)
|
||||
|
||||
def update_hover(self, x, y):
|
||||
"""Update when cursor moves."""
|
||||
if not self.current_ability:
|
||||
return
|
||||
|
||||
# Clear previous AoE preview
|
||||
self.highlights.remove('danger')
|
||||
|
||||
if (x, y) in self.valid_targets:
|
||||
# Valid target - highlight it
|
||||
self.highlights.add('select', [(x, y)])
|
||||
|
||||
# Show AoE if applicable
|
||||
if self.current_ability.aoe_radius > 0:
|
||||
aoe = get_radius_range(x, y, self.current_ability.aoe_radius, True)
|
||||
self.highlights.add('danger', aoe)
|
||||
else:
|
||||
self.highlights.remove('select')
|
||||
|
||||
def confirm_target(self, x, y):
|
||||
"""Confirm target selection."""
|
||||
if (x, y) in self.valid_targets:
|
||||
self.cancel_targeting()
|
||||
return (x, y)
|
||||
return None
|
||||
|
||||
def cancel_targeting(self):
|
||||
"""Cancel targeting mode."""
|
||||
self.current_ability = None
|
||||
self.valid_targets = set()
|
||||
self.highlights.clear()
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
"""McRogueFace - Cell Highlighting (Targeting) (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def get_line_range(start_x, start_y, max_range):
|
||||
"""Get cells in cardinal directions (ranged attack)."""
|
||||
cells = set()
|
||||
|
||||
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
|
||||
for dist in range(1, max_range + 1):
|
||||
x = start_x + dx * dist
|
||||
y = start_y + dy * dist
|
||||
|
||||
# Stop if wall blocks line of sight
|
||||
if not grid.at(x, y).transparent:
|
||||
break
|
||||
|
||||
cells.add((x, y))
|
||||
|
||||
return cells
|
||||
|
||||
def get_radius_range(center_x, center_y, radius, include_center=False):
|
||||
"""Get cells within a radius (spell area)."""
|
||||
cells = set()
|
||||
|
||||
for x in range(center_x - radius, center_x + radius + 1):
|
||||
for y in range(center_y - radius, center_y + radius + 1):
|
||||
# Euclidean distance
|
||||
dist = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5
|
||||
if dist <= radius:
|
||||
if include_center or (x, y) != (center_x, center_y):
|
||||
cells.add((x, y))
|
||||
|
||||
return cells
|
||||
|
||||
def get_cone_range(origin_x, origin_y, direction, length, spread):
|
||||
"""Get cells in a cone (breath attack)."""
|
||||
import math
|
||||
cells = set()
|
||||
|
||||
# Direction angles (in radians)
|
||||
angles = {
|
||||
'n': -math.pi / 2,
|
||||
's': math.pi / 2,
|
||||
'e': 0,
|
||||
'w': math.pi,
|
||||
'ne': -math.pi / 4,
|
||||
'nw': -3 * math.pi / 4,
|
||||
'se': math.pi / 4,
|
||||
'sw': 3 * math.pi / 4
|
||||
}
|
||||
|
||||
base_angle = angles.get(direction, 0)
|
||||
half_spread = math.radians(spread / 2)
|
||||
|
||||
for x in range(origin_x - length, origin_x + length + 1):
|
||||
for y in range(origin_y - length, origin_y + length + 1):
|
||||
dx = x - origin_x
|
||||
dy = y - origin_y
|
||||
dist = (dx * dx + dy * dy) ** 0.5
|
||||
|
||||
if dist > 0 and dist <= length:
|
||||
angle = math.atan2(dy, dx)
|
||||
angle_diff = abs((angle - base_angle + math.pi) % (2 * math.pi) - math.pi)
|
||||
|
||||
if angle_diff <= half_spread:
|
||||
cells.add((x, y))
|
||||
|
||||
return cells
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
"""McRogueFace - Cell Highlighting (Targeting) (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def show_path_preview(start, end):
|
||||
"""Highlight the path between two points."""
|
||||
path = find_path(start, end) # Your pathfinding function
|
||||
|
||||
if path:
|
||||
highlights.add('path', path)
|
||||
|
||||
# Highlight destination specially
|
||||
highlights.add('select', [end])
|
||||
|
||||
def hide_path_preview():
|
||||
"""Clear path display."""
|
||||
highlights.remove('path')
|
||||
highlights.remove('select')
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
"""McRogueFace - Dijkstra Distance Maps (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_dijkstra
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dijkstra_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def ai_flee(entity, threat_x, threat_y):
|
||||
"""Move entity away from threat using Dijkstra map."""
|
||||
grid.compute_dijkstra(threat_x, threat_y)
|
||||
|
||||
ex, ey = entity.pos
|
||||
current_dist = grid.get_dijkstra_distance(ex, ey)
|
||||
|
||||
# Find neighbor with highest distance
|
||||
best_move = None
|
||||
best_dist = current_dist
|
||||
|
||||
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
|
||||
nx, ny = ex + dx, ey + dy
|
||||
|
||||
if grid.at(nx, ny).walkable:
|
||||
dist = grid.get_dijkstra_distance(nx, ny)
|
||||
if dist > best_dist:
|
||||
best_dist = dist
|
||||
best_move = (nx, ny)
|
||||
|
||||
if best_move:
|
||||
entity.pos = best_move
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
"""McRogueFace - Dijkstra Distance Maps (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_dijkstra
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dijkstra_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
# Cache Dijkstra maps when possible
|
||||
class CachedDijkstra:
|
||||
"""Cache Dijkstra computations."""
|
||||
|
||||
def __init__(self, grid):
|
||||
self.grid = grid
|
||||
self.cache = {}
|
||||
self.cache_valid = False
|
||||
|
||||
def invalidate(self):
|
||||
"""Call when map changes."""
|
||||
self.cache = {}
|
||||
self.cache_valid = False
|
||||
|
||||
def get_distance(self, from_x, from_y, to_x, to_y):
|
||||
"""Get cached distance or compute."""
|
||||
key = (to_x, to_y) # Cache by destination
|
||||
|
||||
if key not in self.cache:
|
||||
self.grid.compute_dijkstra(to_x, to_y)
|
||||
# Store all distances from this computation
|
||||
self.cache[key] = self._snapshot_distances()
|
||||
|
||||
return self.cache[key].get((from_x, from_y), float('inf'))
|
||||
|
||||
def _snapshot_distances(self):
|
||||
"""Capture current distance values."""
|
||||
grid_w, grid_h = self.grid.grid_size
|
||||
distances = {}
|
||||
for x in range(grid_w):
|
||||
for y in range(grid_h):
|
||||
dist = self.grid.get_dijkstra_distance(x, y)
|
||||
if dist != float('inf'):
|
||||
distances[(x, y)] = dist
|
||||
return distances
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
"""McRogueFace - Room and Corridor Generator (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_dungeon_generator
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dungeon_generator_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class BSPNode:
|
||||
"""Node in a BSP tree for dungeon generation."""
|
||||
|
||||
MIN_SIZE = 6
|
||||
|
||||
def __init__(self, x, y, w, h):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.left = None
|
||||
self.right = None
|
||||
self.room = None
|
||||
|
||||
def split(self):
|
||||
"""Recursively split this node."""
|
||||
if self.left or self.right:
|
||||
return False
|
||||
|
||||
# Choose split direction
|
||||
if self.w > self.h and self.w / self.h >= 1.25:
|
||||
horizontal = False
|
||||
elif self.h > self.w and self.h / self.w >= 1.25:
|
||||
horizontal = True
|
||||
else:
|
||||
horizontal = random.random() < 0.5
|
||||
|
||||
max_size = (self.h if horizontal else self.w) - self.MIN_SIZE
|
||||
if max_size <= self.MIN_SIZE:
|
||||
return False
|
||||
|
||||
split = random.randint(self.MIN_SIZE, max_size)
|
||||
|
||||
if horizontal:
|
||||
self.left = BSPNode(self.x, self.y, self.w, split)
|
||||
self.right = BSPNode(self.x, self.y + split, self.w, self.h - split)
|
||||
else:
|
||||
self.left = BSPNode(self.x, self.y, split, self.h)
|
||||
self.right = BSPNode(self.x + split, self.y, self.w - split, self.h)
|
||||
|
||||
return True
|
||||
|
||||
def create_rooms(self, grid):
|
||||
"""Create rooms in leaf nodes and connect siblings."""
|
||||
if self.left or self.right:
|
||||
if self.left:
|
||||
self.left.create_rooms(grid)
|
||||
if self.right:
|
||||
self.right.create_rooms(grid)
|
||||
|
||||
# Connect children
|
||||
if self.left and self.right:
|
||||
left_room = self.left.get_room()
|
||||
right_room = self.right.get_room()
|
||||
if left_room and right_room:
|
||||
connect_points(grid, left_room.center, right_room.center)
|
||||
else:
|
||||
# Leaf node - create room
|
||||
w = random.randint(3, self.w - 2)
|
||||
h = random.randint(3, self.h - 2)
|
||||
x = self.x + random.randint(1, self.w - w - 1)
|
||||
y = self.y + random.randint(1, self.h - h - 1)
|
||||
self.room = Room(x, y, w, h)
|
||||
carve_room(grid, self.room)
|
||||
|
||||
def get_room(self):
|
||||
"""Get a room from this node or its children."""
|
||||
if self.room:
|
||||
return self.room
|
||||
|
||||
left_room = self.left.get_room() if self.left else None
|
||||
right_room = self.right.get_room() if self.right else None
|
||||
|
||||
if left_room and right_room:
|
||||
return random.choice([left_room, right_room])
|
||||
return left_room or right_room
|
||||
|
||||
|
||||
def generate_bsp_dungeon(grid, iterations=4):
|
||||
"""Generate a BSP-based dungeon."""
|
||||
grid_w, grid_h = grid.grid_size
|
||||
|
||||
# Fill with walls
|
||||
for x in range(grid_w):
|
||||
for y in range(grid_h):
|
||||
point = grid.at(x, y)
|
||||
point.tilesprite = TILE_WALL
|
||||
point.walkable = False
|
||||
point.transparent = False
|
||||
|
||||
# Build BSP tree
|
||||
root = BSPNode(0, 0, grid_w, grid_h)
|
||||
nodes = [root]
|
||||
|
||||
for _ in range(iterations):
|
||||
new_nodes = []
|
||||
for node in nodes:
|
||||
if node.split():
|
||||
new_nodes.extend([node.left, node.right])
|
||||
nodes = new_nodes or nodes
|
||||
|
||||
# Create rooms and corridors
|
||||
root.create_rooms(grid)
|
||||
|
||||
# Collect all rooms
|
||||
rooms = []
|
||||
def collect_rooms(node):
|
||||
if node.room:
|
||||
rooms.append(node.room)
|
||||
if node.left:
|
||||
collect_rooms(node.left)
|
||||
if node.right:
|
||||
collect_rooms(node.right)
|
||||
|
||||
collect_rooms(root)
|
||||
return rooms
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
"""McRogueFace - Room and Corridor Generator (complete)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_dungeon_generator
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dungeon_generator_complete.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Tile indices (adjust for your tileset)
|
||||
TILE_FLOOR = 0
|
||||
TILE_WALL = 1
|
||||
TILE_DOOR = 2
|
||||
TILE_STAIRS_DOWN = 3
|
||||
TILE_STAIRS_UP = 4
|
||||
|
||||
class DungeonGenerator:
|
||||
"""Procedural dungeon generator with rooms and corridors."""
|
||||
|
||||
def __init__(self, grid, seed=None):
|
||||
self.grid = grid
|
||||
self.grid_w, self.grid_h = grid.grid_size
|
||||
self.rooms = []
|
||||
|
||||
if seed is not None:
|
||||
random.seed(seed)
|
||||
|
||||
def generate(self, room_count=8, min_room=4, max_room=10):
|
||||
"""Generate a complete dungeon level."""
|
||||
self.rooms = []
|
||||
|
||||
# Fill with walls
|
||||
self._fill_walls()
|
||||
|
||||
# Place rooms
|
||||
attempts = 0
|
||||
max_attempts = room_count * 10
|
||||
|
||||
while len(self.rooms) < room_count and attempts < max_attempts:
|
||||
attempts += 1
|
||||
|
||||
# Random room size
|
||||
w = random.randint(min_room, max_room)
|
||||
h = random.randint(min_room, max_room)
|
||||
|
||||
# Random position (leaving border)
|
||||
x = random.randint(1, self.grid_w - w - 2)
|
||||
y = random.randint(1, self.grid_h - h - 2)
|
||||
|
||||
room = Room(x, y, w, h)
|
||||
|
||||
# Check overlap
|
||||
if not any(room.intersects(r) for r in self.rooms):
|
||||
self._carve_room(room)
|
||||
|
||||
# Connect to previous room
|
||||
if self.rooms:
|
||||
self._dig_corridor(self.rooms[-1].center, room.center)
|
||||
|
||||
self.rooms.append(room)
|
||||
|
||||
# Place stairs
|
||||
if len(self.rooms) >= 2:
|
||||
self._place_stairs()
|
||||
|
||||
return self.rooms
|
||||
|
||||
def _fill_walls(self):
|
||||
"""Fill the entire grid with wall tiles."""
|
||||
for x in range(self.grid_w):
|
||||
for y in range(self.grid_h):
|
||||
point = self.grid.at(x, y)
|
||||
point.tilesprite = TILE_WALL
|
||||
point.walkable = False
|
||||
point.transparent = False
|
||||
|
||||
def _carve_room(self, room):
|
||||
"""Carve out a room, making it walkable."""
|
||||
for x in range(room.x, room.x + room.width):
|
||||
for y in range(room.y, room.y + room.height):
|
||||
self._set_floor(x, y)
|
||||
|
||||
def _set_floor(self, x, y):
|
||||
"""Set a single tile as floor."""
|
||||
if 0 <= x < self.grid_w and 0 <= y < self.grid_h:
|
||||
point = self.grid.at(x, y)
|
||||
point.tilesprite = TILE_FLOOR
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
def _dig_corridor(self, start, end):
|
||||
"""Dig an L-shaped corridor between two points."""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
# Randomly choose horizontal-first or vertical-first
|
||||
if random.random() < 0.5:
|
||||
# Horizontal then vertical
|
||||
self._dig_horizontal(x1, x2, y1)
|
||||
self._dig_vertical(y1, y2, x2)
|
||||
else:
|
||||
# Vertical then horizontal
|
||||
self._dig_vertical(y1, y2, x1)
|
||||
self._dig_horizontal(x1, x2, y2)
|
||||
|
||||
def _dig_horizontal(self, x1, x2, y):
|
||||
"""Dig a horizontal tunnel."""
|
||||
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||
self._set_floor(x, y)
|
||||
|
||||
def _dig_vertical(self, y1, y2, x):
|
||||
"""Dig a vertical tunnel."""
|
||||
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||
self._set_floor(x, y)
|
||||
|
||||
def _place_stairs(self):
|
||||
"""Place stairs in first and last rooms."""
|
||||
# Stairs up in first room
|
||||
start_room = self.rooms[0]
|
||||
sx, sy = start_room.center
|
||||
point = self.grid.at(sx, sy)
|
||||
point.tilesprite = TILE_STAIRS_UP
|
||||
|
||||
# Stairs down in last room
|
||||
end_room = self.rooms[-1]
|
||||
ex, ey = end_room.center
|
||||
point = self.grid.at(ex, ey)
|
||||
point.tilesprite = TILE_STAIRS_DOWN
|
||||
|
||||
return (sx, sy), (ex, ey)
|
||||
|
||||
def get_spawn_point(self):
|
||||
"""Get a good spawn point for the player."""
|
||||
if self.rooms:
|
||||
return self.rooms[0].center
|
||||
return (self.grid_w // 2, self.grid_h // 2)
|
||||
|
||||
def get_random_floor(self):
|
||||
"""Get a random walkable floor tile."""
|
||||
floors = []
|
||||
for x in range(self.grid_w):
|
||||
for y in range(self.grid_h):
|
||||
if self.grid.at(x, y).walkable:
|
||||
floors.append((x, y))
|
||||
return random.choice(floors) if floors else None
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
"""McRogueFace - Basic Fog of War (grid_fog_of_war)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_fog_of_war
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_fog_of_war.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
# Shadowcasting (default) - fast and produces nice results
|
||||
grid.compute_fov(x, y, 10, mcrfpy.FOV.SHADOW)
|
||||
|
||||
# Recursive shadowcasting - slightly different corner behavior
|
||||
grid.compute_fov(x, y, 10, mcrfpy.FOV.RECURSIVE_SHADOW)
|
||||
|
||||
# Diamond - simple but produces diamond-shaped FOV
|
||||
grid.compute_fov(x, y, 10, mcrfpy.FOV.DIAMOND)
|
||||
|
||||
# Permissive - sees more tiles, good for tactical games
|
||||
grid.compute_fov(x, y, 10, mcrfpy.FOV.PERMISSIVE)
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
"""McRogueFace - Multi-Layer Tiles (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_multi_layer
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_multi_layer_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class EffectLayer:
|
||||
"""Manage visual effects with color overlays."""
|
||||
|
||||
def __init__(self, grid, z_index=2):
|
||||
self.grid = grid
|
||||
self.layer = grid.add_layer("color", z_index=z_index)
|
||||
self.effects = {} # (x, y) -> effect_data
|
||||
|
||||
def add_effect(self, x, y, effect_type, duration=None, **kwargs):
|
||||
"""Add a visual effect."""
|
||||
self.effects[(x, y)] = {
|
||||
'type': effect_type,
|
||||
'duration': duration,
|
||||
'time': 0,
|
||||
**kwargs
|
||||
}
|
||||
|
||||
def remove_effect(self, x, y):
|
||||
"""Remove an effect."""
|
||||
if (x, y) in self.effects:
|
||||
del self.effects[(x, y)]
|
||||
self.layer.set(x, y, mcrfpy.Color(0, 0, 0, 0))
|
||||
|
||||
def update(self, dt):
|
||||
"""Update all effects."""
|
||||
import math
|
||||
|
||||
to_remove = []
|
||||
|
||||
for (x, y), effect in self.effects.items():
|
||||
effect['time'] += dt
|
||||
|
||||
# Check expiration
|
||||
if effect['duration'] and effect['time'] >= effect['duration']:
|
||||
to_remove.append((x, y))
|
||||
continue
|
||||
|
||||
# Calculate color based on effect type
|
||||
color = self._calculate_color(effect)
|
||||
self.layer.set(x, y, color)
|
||||
|
||||
for pos in to_remove:
|
||||
self.remove_effect(*pos)
|
||||
|
||||
def _calculate_color(self, effect):
|
||||
"""Get color for an effect at current time."""
|
||||
import math
|
||||
|
||||
t = effect['time']
|
||||
effect_type = effect['type']
|
||||
|
||||
if effect_type == 'fire':
|
||||
# Flickering orange/red
|
||||
flicker = 0.7 + 0.3 * math.sin(t * 10)
|
||||
return mcrfpy.Color(
|
||||
255,
|
||||
int(100 + 50 * math.sin(t * 8)),
|
||||
0,
|
||||
int(180 * flicker)
|
||||
)
|
||||
|
||||
elif effect_type == 'poison':
|
||||
# Pulsing green
|
||||
pulse = 0.5 + 0.5 * math.sin(t * 3)
|
||||
return mcrfpy.Color(0, 200, 0, int(100 * pulse))
|
||||
|
||||
elif effect_type == 'ice':
|
||||
# Static blue with shimmer
|
||||
shimmer = 0.8 + 0.2 * math.sin(t * 5)
|
||||
return mcrfpy.Color(100, 150, 255, int(120 * shimmer))
|
||||
|
||||
elif effect_type == 'blood':
|
||||
# Fading red
|
||||
duration = effect.get('duration', 5)
|
||||
fade = 1 - (t / duration) if duration else 1
|
||||
return mcrfpy.Color(150, 0, 0, int(150 * fade))
|
||||
|
||||
elif effect_type == 'highlight':
|
||||
# Pulsing highlight
|
||||
pulse = 0.5 + 0.5 * math.sin(t * 4)
|
||||
base = effect.get('color', mcrfpy.Color(255, 255, 0, 100))
|
||||
return mcrfpy.Color(base.r, base.g, base.b, int(base.a * pulse))
|
||||
|
||||
return mcrfpy.Color(128, 128, 128, 50)
|
||||
|
||||
|
||||
# Usage
|
||||
effects = EffectLayer(grid)
|
||||
|
||||
# Add fire effect (permanent)
|
||||
effects.add_effect(5, 5, 'fire')
|
||||
|
||||
# Add blood stain (fades over 10 seconds)
|
||||
effects.add_effect(10, 10, 'blood', duration=10)
|
||||
|
||||
# Add poison cloud
|
||||
for x in range(8, 12):
|
||||
for y in range(8, 12):
|
||||
effects.add_effect(x, y, 'poison', duration=5)
|
||||
|
||||
# Update in game loop
|
||||
def game_update(runtime):
|
||||
effects.update(0.016) # 60 FPS
|
||||
|
||||
mcrfpy.setTimer("effects", game_update, 16)
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
"""McRogueFace - Multi-Layer Tiles (complete)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_multi_layer
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_multi_layer_complete.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class OptimizedLayers:
|
||||
"""Performance-optimized layer management."""
|
||||
|
||||
def __init__(self, grid):
|
||||
self.grid = grid
|
||||
self.dirty_effects = set() # Only update changed cells
|
||||
self.batch_updates = []
|
||||
|
||||
def mark_dirty(self, x, y):
|
||||
"""Mark a cell as needing update."""
|
||||
self.dirty_effects.add((x, y))
|
||||
|
||||
def batch_set(self, layer, cells_and_values):
|
||||
"""Queue batch updates."""
|
||||
self.batch_updates.append((layer, cells_and_values))
|
||||
|
||||
def flush(self):
|
||||
"""Apply all queued updates."""
|
||||
for layer, updates in self.batch_updates:
|
||||
for x, y, value in updates:
|
||||
layer.set(x, y, value)
|
||||
self.batch_updates = []
|
||||
|
||||
def update_dirty_only(self, effect_layer, effect_calculator):
|
||||
"""Only update cells marked dirty."""
|
||||
for x, y in self.dirty_effects:
|
||||
color = effect_calculator(x, y)
|
||||
effect_layer.set(x, y, color)
|
||||
self.dirty_effects.clear()
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
"""HeightMap Hills and Craters Demo
|
||||
|
||||
Demonstrates: add_hill, dig_hill
|
||||
Creates volcanic terrain with mountains and craters using ColorLayer visualization.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
# Full screen grid: 60x48 tiles at 16x16 = 960x768
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def height_to_color(h):
|
||||
"""Convert height value to terrain color."""
|
||||
if h < 0.1:
|
||||
return mcrfpy.Color(20, 40, int(80 + h * 400))
|
||||
elif h < 0.3:
|
||||
t = (h - 0.1) / 0.2
|
||||
return mcrfpy.Color(int(40 + t * 30), int(60 + t * 40), 30)
|
||||
elif h < 0.5:
|
||||
t = (h - 0.3) / 0.2
|
||||
return mcrfpy.Color(int(70 - t * 20), int(100 + t * 50), int(30 + t * 20))
|
||||
elif h < 0.7:
|
||||
t = (h - 0.5) / 0.2
|
||||
return mcrfpy.Color(int(120 + t * 40), int(100 + t * 30), int(60 + t * 20))
|
||||
elif h < 0.85:
|
||||
t = (h - 0.7) / 0.15
|
||||
return mcrfpy.Color(int(140 + t * 40), int(130 + t * 40), int(120 + t * 40))
|
||||
else:
|
||||
t = (h - 0.85) / 0.15
|
||||
return mcrfpy.Color(int(180 + t * 75), int(180 + t * 75), int(180 + t * 75))
|
||||
|
||||
# Setup scene
|
||||
scene = mcrfpy.Scene("hills_demo")
|
||||
|
||||
# Create grid with color layer
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
# Create heightmap
|
||||
hmap = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.3)
|
||||
|
||||
# Add volcanic mountains - large hills
|
||||
hmap.add_hill((15, 24), 18.0, 0.6) # Central volcano base
|
||||
hmap.add_hill((15, 24), 10.0, 0.3) # Volcano peak
|
||||
hmap.add_hill((45, 15), 12.0, 0.5) # Eastern mountain
|
||||
hmap.add_hill((35, 38), 14.0, 0.45) # Southern mountain
|
||||
hmap.add_hill((8, 10), 8.0, 0.35) # Small northern hill
|
||||
|
||||
# Create craters using dig_hill
|
||||
hmap.dig_hill((15, 24), 5.0, 0.1) # Volcanic crater
|
||||
hmap.dig_hill((45, 15), 4.0, 0.25) # Eastern crater
|
||||
hmap.dig_hill((25, 30), 6.0, 0.05) # Impact crater (deep)
|
||||
hmap.dig_hill((50, 40), 3.0, 0.2) # Small crater
|
||||
|
||||
# Add some smaller features for variety
|
||||
for i in range(8):
|
||||
x = 5 + (i * 7) % 55
|
||||
y = 5 + (i * 11) % 40
|
||||
hmap.add_hill((x, y), float(3 + (i % 4)), 0.15)
|
||||
|
||||
# Normalize to use full color range
|
||||
hmap.normalize(0.0, 1.0)
|
||||
|
||||
# Apply heightmap to color layer
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set((x, y), height_to_color(h))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="HeightMap: add_hill + dig_hill (volcanic terrain)", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Take screenshot directly (works in headless mode)
|
||||
automation.screenshot("procgen_01_heightmap_hills.png")
|
||||
print("Screenshot saved: procgen_01_heightmap_hills.png")
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
"""HeightMap Noise Integration Demo
|
||||
|
||||
Demonstrates: add_noise, multiply_noise with NoiseSource
|
||||
Shows terrain generation using different noise modes (flat, fbm, turbulence).
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def terrain_color(h):
|
||||
"""Height-based terrain coloring."""
|
||||
if h < 0.25:
|
||||
# Water - deep to shallow blue
|
||||
t = h / 0.25
|
||||
return mcrfpy.Color(int(30 + t * 30), int(60 + t * 60), int(120 + t * 80))
|
||||
elif h < 0.35:
|
||||
# Beach/sand
|
||||
t = (h - 0.25) / 0.1
|
||||
return mcrfpy.Color(int(180 + t * 40), int(160 + t * 30), int(100 + t * 20))
|
||||
elif h < 0.6:
|
||||
# Grass - varies with height
|
||||
t = (h - 0.35) / 0.25
|
||||
return mcrfpy.Color(int(50 + t * 30), int(120 + t * 40), int(40 + t * 20))
|
||||
elif h < 0.75:
|
||||
# Forest/hills
|
||||
t = (h - 0.6) / 0.15
|
||||
return mcrfpy.Color(int(40 - t * 10), int(80 + t * 20), int(30 + t * 10))
|
||||
elif h < 0.88:
|
||||
# Rock/mountain
|
||||
t = (h - 0.75) / 0.13
|
||||
return mcrfpy.Color(int(100 + t * 40), int(90 + t * 40), int(80 + t * 40))
|
||||
else:
|
||||
# Snow peaks
|
||||
t = (h - 0.88) / 0.12
|
||||
return mcrfpy.Color(int(200 + t * 55), int(200 + t * 55), int(210 + t * 45))
|
||||
|
||||
def apply_to_layer(hmap, layer):
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
h = hmap.get((x, y))
|
||||
layer.set(x, y, terrain_color(h))
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create three panels showing different noise modes
|
||||
panel_width = GRID_WIDTH // 3
|
||||
right_panel_width = GRID_WIDTH - 2 * panel_width # Handle non-divisible widths
|
||||
|
||||
# Create noise source with consistent seed
|
||||
noise = mcrfpy.NoiseSource(
|
||||
dimensions=2,
|
||||
algorithm='simplex',
|
||||
hurst=0.5,
|
||||
lacunarity=2.0,
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Left panel: Flat noise (single octave, raw)
|
||||
left_hmap = mcrfpy.HeightMap((panel_width, GRID_HEIGHT), fill=0.0)
|
||||
left_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='flat', octaves=1)
|
||||
left_hmap.normalize(0.0, 1.0)
|
||||
|
||||
# Middle panel: FBM noise (fractal brownian motion - natural terrain)
|
||||
mid_hmap = mcrfpy.HeightMap((panel_width, GRID_HEIGHT), fill=0.0)
|
||||
mid_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='fbm', octaves=6)
|
||||
mid_hmap.normalize(0.0, 1.0)
|
||||
|
||||
# Right panel: Turbulence (absolute value - clouds, marble)
|
||||
right_hmap = mcrfpy.HeightMap((right_panel_width, GRID_HEIGHT), fill=0.0)
|
||||
right_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='turbulence', octaves=6)
|
||||
right_hmap.normalize(0.0, 1.0)
|
||||
|
||||
# Apply to color layer with panel divisions
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if x < panel_width:
|
||||
h = left_hmap.get((x, y))
|
||||
elif x < panel_width * 2:
|
||||
h = mid_hmap.get((x - panel_width, y))
|
||||
else:
|
||||
h = right_hmap.get((x - panel_width * 2, y))
|
||||
color_layer.set(((x, y)), terrain_color(h))
|
||||
|
||||
# Add divider lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_width - 1, y)), mcrfpy.Color(255, 255, 255, 100))
|
||||
color_layer.set(((panel_width * 2 - 1, y)), mcrfpy.Color(255, 255, 255, 100))
|
||||
|
||||
|
||||
# Setup scene
|
||||
scene = mcrfpy.Scene("noise_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
# Labels for each panel
|
||||
labels = [
|
||||
("FLAT (raw)", 10),
|
||||
("FBM (terrain)", GRID_WIDTH * CELL_SIZE // 3 + 10),
|
||||
("TURBULENCE (clouds)", GRID_WIDTH * CELL_SIZE * 2 // 3 + 10)
|
||||
]
|
||||
for text, x in labels:
|
||||
label = mcrfpy.Caption(text=text, pos=(x, 10))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_02_heightmap_noise.png")
|
||||
print("Screenshot saved: procgen_02_heightmap_noise.png")
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
"""HeightMap Combination Operations Demo
|
||||
|
||||
Demonstrates: add, subtract, multiply, min, max, lerp, copy_from
|
||||
Shows how heightmaps can be combined for complex terrain effects.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def value_to_color(h):
|
||||
"""Simple grayscale with color tinting for visibility."""
|
||||
h = max(0.0, min(1.0, h))
|
||||
# Blue-white-red gradient for clear visualization
|
||||
if h < 0.5:
|
||||
t = h / 0.5
|
||||
return mcrfpy.Color(int(50 * t), int(100 * t), int(200 - 100 * t))
|
||||
else:
|
||||
t = (h - 0.5) / 0.5
|
||||
return mcrfpy.Color(int(50 + 200 * t), int(100 + 100 * t), int(100 - 50 * t))
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create 6 panels (2 rows x 3 columns)
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Create two base heightmaps for operations
|
||||
noise1 = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
noise2 = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=123)
|
||||
|
||||
base1 = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
base1.add_noise(noise1, world_size=(10, 10), mode='fbm', octaves=4)
|
||||
base1.normalize(0.0, 1.0)
|
||||
|
||||
base2 = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
base2.add_noise(noise2, world_size=(10, 10), mode='fbm', octaves=4)
|
||||
base2.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 1: ADD operation (combined terrain)
|
||||
add_result = base1.copy_from(base1) # Actually need to create new
|
||||
add_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
add_result.copy_from(base1).add(base2).normalize(0.0, 1.0)
|
||||
|
||||
# Panel 2: SUBTRACT operation (carving)
|
||||
sub_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
sub_result.copy_from(base1).subtract(base2).normalize(0.0, 1.0)
|
||||
|
||||
# Panel 3: MULTIPLY operation (masking)
|
||||
mul_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
mul_result.copy_from(base1).multiply(base2).normalize(0.0, 1.0)
|
||||
|
||||
# Panel 4: MIN operation (valleys)
|
||||
min_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
min_result.copy_from(base1).min(base2)
|
||||
|
||||
# Panel 5: MAX operation (ridges)
|
||||
max_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
max_result.copy_from(base1).max(base2)
|
||||
|
||||
# Panel 6: LERP operation (blending)
|
||||
lerp_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
lerp_result.copy_from(base1).lerp(base2, 0.5)
|
||||
|
||||
# Apply panels to grid
|
||||
panels = [
|
||||
(add_result, 0, 0, "ADD"),
|
||||
(sub_result, panel_w, 0, "SUBTRACT"),
|
||||
(mul_result, panel_w * 2, 0, "MULTIPLY"),
|
||||
(min_result, 0, panel_h, "MIN"),
|
||||
(max_result, panel_w, panel_h, "MAX"),
|
||||
(lerp_result, panel_w * 2, panel_h, "LERP(0.5)"),
|
||||
]
|
||||
|
||||
for hmap, ox, oy, name in panels:
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), value_to_color(h))
|
||||
|
||||
# Add label
|
||||
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Draw grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(255, 255, 255, 80))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(255, 255, 255, 80))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(255, 255, 255, 80))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("operations_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_03_heightmap_operations.png")
|
||||
print("Screenshot saved: procgen_03_heightmap_operations.png")
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
"""HeightMap Transform Operations Demo
|
||||
|
||||
Demonstrates: scale, clamp, normalize, smooth, kernel_transform
|
||||
Shows value manipulation and convolution effects.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def value_to_color(h):
|
||||
"""Grayscale with enhanced contrast."""
|
||||
h = max(0.0, min(1.0, h))
|
||||
v = int(h * 255)
|
||||
return mcrfpy.Color(v, v, v)
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create 6 panels showing different transforms
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Source noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
# Create base terrain with features
|
||||
base = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
base.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=4)
|
||||
base.add_hill((panel_w // 2, panel_h // 2), 8, 0.5)
|
||||
base.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 1: Original
|
||||
original = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
original.copy_from(base)
|
||||
|
||||
# Panel 2: SCALE (amplify contrast)
|
||||
scaled = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
scaled.copy_from(base).add_constant(-0.5).scale(2.0).clamp(0.0, 1.0)
|
||||
|
||||
# Panel 3: CLAMP (plateau effect)
|
||||
clamped = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
clamped.copy_from(base).clamp(0.3, 0.7).normalize(0.0, 1.0)
|
||||
|
||||
# Panel 4: SMOOTH (blur/average)
|
||||
smoothed = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
smoothed.copy_from(base).smooth(3)
|
||||
|
||||
# Panel 5: SHARPEN kernel
|
||||
sharpened = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
sharpened.copy_from(base)
|
||||
sharpen_kernel = {
|
||||
(0, -1): -1.0, (-1, 0): -1.0, (0, 0): 5.0, (1, 0): -1.0, (0, 1): -1.0
|
||||
}
|
||||
sharpened.kernel_transform(sharpen_kernel).clamp(0.0, 1.0)
|
||||
|
||||
# Panel 6: EDGE DETECTION kernel
|
||||
edges = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
edges.copy_from(base)
|
||||
edge_kernel = {
|
||||
(-1, -1): -1, (0, -1): -1, (1, -1): -1,
|
||||
(-1, 0): -1, (0, 0): 8, (1, 0): -1,
|
||||
(-1, 1): -1, (0, 1): -1, (1, 1): -1,
|
||||
}
|
||||
edges.kernel_transform(edge_kernel).normalize(0.0, 1.0)
|
||||
|
||||
# Apply to grid
|
||||
panels = [
|
||||
(original, 0, 0, "ORIGINAL"),
|
||||
(scaled, panel_w, 0, "SCALE (contrast)"),
|
||||
(clamped, panel_w * 2, 0, "CLAMP (plateau)"),
|
||||
(smoothed, 0, panel_h, "SMOOTH (blur)"),
|
||||
(sharpened, panel_w, panel_h, "SHARPEN kernel"),
|
||||
(edges, panel_w * 2, panel_h, "EDGE DETECT"),
|
||||
]
|
||||
|
||||
for hmap, ox, oy, name in panels:
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), value_to_color(h))
|
||||
|
||||
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(100, 100, 100))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(100, 100, 100))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("transforms_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_04_heightmap_transforms.png")
|
||||
print("Screenshot saved: procgen_04_heightmap_transforms.png")
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
"""HeightMap Erosion and Terrain Generation Demo
|
||||
|
||||
Demonstrates: rain_erosion, mid_point_displacement, smooth
|
||||
Shows natural terrain formation through erosion simulation.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def terrain_color(h):
|
||||
"""Natural terrain coloring."""
|
||||
if h < 0.2:
|
||||
# Deep water
|
||||
t = h / 0.2
|
||||
return mcrfpy.Color(int(20 + t * 30), int(40 + t * 40), int(100 + t * 55))
|
||||
elif h < 0.3:
|
||||
# Shallow water
|
||||
t = (h - 0.2) / 0.1
|
||||
return mcrfpy.Color(int(50 + t * 50), int(80 + t * 60), int(155 + t * 40))
|
||||
elif h < 0.35:
|
||||
# Beach
|
||||
t = (h - 0.3) / 0.05
|
||||
return mcrfpy.Color(int(194 - t * 30), int(178 - t * 30), int(128 - t * 20))
|
||||
elif h < 0.55:
|
||||
# Lowland grass
|
||||
t = (h - 0.35) / 0.2
|
||||
return mcrfpy.Color(int(80 + t * 20), int(140 - t * 30), int(60 + t * 10))
|
||||
elif h < 0.7:
|
||||
# Highland grass/forest
|
||||
t = (h - 0.55) / 0.15
|
||||
return mcrfpy.Color(int(50 + t * 30), int(100 + t * 10), int(40 + t * 20))
|
||||
elif h < 0.85:
|
||||
# Rock
|
||||
t = (h - 0.7) / 0.15
|
||||
return mcrfpy.Color(int(100 + t * 30), int(95 + t * 30), int(85 + t * 35))
|
||||
else:
|
||||
# Snow
|
||||
t = (h - 0.85) / 0.15
|
||||
return mcrfpy.Color(int(180 + t * 75), int(185 + t * 70), int(190 + t * 65))
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Panel 1: Mid-point displacement (raw)
|
||||
mpd_raw = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
mpd_raw.mid_point_displacement(roughness=0.6, seed=42)
|
||||
mpd_raw.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 2: Mid-point displacement + smoothing
|
||||
mpd_smooth = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
mpd_smooth.mid_point_displacement(roughness=0.6, seed=42)
|
||||
mpd_smooth.smooth(2)
|
||||
mpd_smooth.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 3: Mid-point + light erosion
|
||||
mpd_light_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
mpd_light_erode.mid_point_displacement(roughness=0.6, seed=42)
|
||||
mpd_light_erode.rain_erosion(drops=1000, erosion=0.05, sedimentation=0.03, seed=42)
|
||||
mpd_light_erode.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 4: Noise-based + moderate erosion
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=123)
|
||||
noise_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
noise_erode.add_noise(noise, world_size=(12, 12), mode='fbm', octaves=5)
|
||||
noise_erode.add_hill((panel_w // 2, panel_h // 2), 10, 0.4)
|
||||
noise_erode.rain_erosion(drops=3000, erosion=0.1, sedimentation=0.05, seed=42)
|
||||
noise_erode.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 5: Heavy erosion (river valleys)
|
||||
heavy_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
heavy_erode.mid_point_displacement(roughness=0.7, seed=99)
|
||||
heavy_erode.rain_erosion(drops=8000, erosion=0.15, sedimentation=0.02, seed=42)
|
||||
heavy_erode.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 6: Extreme erosion (canyon-like)
|
||||
extreme_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
extreme_erode.mid_point_displacement(roughness=0.5, seed=77)
|
||||
extreme_erode.rain_erosion(drops=15000, erosion=0.2, sedimentation=0.01, seed=42)
|
||||
extreme_erode.smooth(1)
|
||||
extreme_erode.normalize(0.0, 1.0)
|
||||
|
||||
# Apply to grid
|
||||
panels = [
|
||||
(mpd_raw, 0, 0, "MPD Raw"),
|
||||
(mpd_smooth, panel_w, 0, "MPD + Smooth"),
|
||||
(mpd_light_erode, panel_w * 2, 0, "Light Erosion"),
|
||||
(noise_erode, 0, panel_h, "Noise + Erosion"),
|
||||
(heavy_erode, panel_w, panel_h, "Heavy Erosion"),
|
||||
(extreme_erode, panel_w * 2, panel_h, "Extreme Erosion"),
|
||||
]
|
||||
|
||||
for hmap, ox, oy, name in panels:
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), terrain_color(h))
|
||||
|
||||
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("erosion_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_05_heightmap_erosion.png")
|
||||
print("Screenshot saved: procgen_05_heightmap_erosion.png")
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
"""HeightMap Voronoi Demo
|
||||
|
||||
Demonstrates: add_voronoi with different coefficients
|
||||
Shows cell-based patterns useful for biomes, regions, and organic structures.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def biome_color(h):
|
||||
"""Color cells as distinct biomes."""
|
||||
# Use value ranges to create distinct regions
|
||||
h = max(0.0, min(1.0, h))
|
||||
|
||||
if h < 0.15:
|
||||
return mcrfpy.Color(30, 60, 120) # Deep water
|
||||
elif h < 0.25:
|
||||
return mcrfpy.Color(50, 100, 180) # Shallow water
|
||||
elif h < 0.35:
|
||||
return mcrfpy.Color(194, 178, 128) # Beach/desert
|
||||
elif h < 0.5:
|
||||
return mcrfpy.Color(80, 160, 60) # Grassland
|
||||
elif h < 0.65:
|
||||
return mcrfpy.Color(40, 100, 40) # Forest
|
||||
elif h < 0.8:
|
||||
return mcrfpy.Color(100, 80, 60) # Hills
|
||||
elif h < 0.9:
|
||||
return mcrfpy.Color(130, 130, 130) # Mountains
|
||||
else:
|
||||
return mcrfpy.Color(240, 240, 250) # Snow
|
||||
|
||||
def cell_edges_color(h):
|
||||
"""Highlight cell boundaries."""
|
||||
h = max(0.0, min(1.0, h))
|
||||
if h < 0.3:
|
||||
return mcrfpy.Color(40, 40, 60)
|
||||
elif h < 0.6:
|
||||
return mcrfpy.Color(80, 80, 100)
|
||||
else:
|
||||
return mcrfpy.Color(200, 200, 220)
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Panel 1: Standard Voronoi (cell centers high)
|
||||
# coefficients (1, 0) = distance to nearest point
|
||||
v_standard = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_standard.add_voronoi(num_points=15, coefficients=(1.0, 0.0), seed=42)
|
||||
v_standard.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 2: Inverted (cell centers low, edges high)
|
||||
# coefficients (-1, 0) = inverted distance
|
||||
v_inverted = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_inverted.add_voronoi(num_points=15, coefficients=(-1.0, 0.0), seed=42)
|
||||
v_inverted.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 3: Cell difference (creates ridges)
|
||||
# coefficients (1, -1) = distance to nearest - distance to second nearest
|
||||
v_ridges = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_ridges.add_voronoi(num_points=15, coefficients=(1.0, -1.0), seed=42)
|
||||
v_ridges.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 4: Few large cells (biome-scale)
|
||||
v_biomes = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_biomes.add_voronoi(num_points=6, coefficients=(1.0, -0.3), seed=99)
|
||||
v_biomes.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 5: Many small cells (texture-scale)
|
||||
v_texture = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_texture.add_voronoi(num_points=50, coefficients=(1.0, -0.5), seed=77)
|
||||
v_texture.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 6: Voronoi + noise blend (natural regions)
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
v_natural = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_natural.add_voronoi(num_points=12, coefficients=(0.8, -0.4), seed=42)
|
||||
v_natural.add_noise(noise, world_size=(15, 15), mode='fbm', octaves=3, scale=0.3)
|
||||
v_natural.normalize(0.0, 1.0)
|
||||
|
||||
# Apply to grid
|
||||
panels = [
|
||||
(v_standard, 0, 0, "Standard (1,0)", biome_color),
|
||||
(v_inverted, panel_w, 0, "Inverted (-1,0)", biome_color),
|
||||
(v_ridges, panel_w * 2, 0, "Ridges (1,-1)", cell_edges_color),
|
||||
(v_biomes, 0, panel_h, "Biomes (6 pts)", biome_color),
|
||||
(v_texture, panel_w, panel_h, "Texture (50 pts)", cell_edges_color),
|
||||
(v_natural, panel_w * 2, panel_h, "Voronoi + Noise", biome_color),
|
||||
]
|
||||
|
||||
for hmap, ox, oy, name, color_func in panels:
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), color_func(h))
|
||||
|
||||
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(60, 60, 60))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(60, 60, 60))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(60, 60, 60))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("voronoi_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_06_heightmap_voronoi.png")
|
||||
print("Screenshot saved: procgen_06_heightmap_voronoi.png")
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
"""HeightMap Bezier Curves Demo
|
||||
|
||||
Demonstrates: dig_bezier for rivers, roads, and paths
|
||||
Shows path carving with variable width and depth.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import math
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def terrain_with_water(h):
|
||||
"""Terrain coloring with water in low areas."""
|
||||
if h < 0.15:
|
||||
# Water (carved paths)
|
||||
t = h / 0.15
|
||||
return mcrfpy.Color(int(30 + t * 30), int(60 + t * 50), int(140 + t * 40))
|
||||
elif h < 0.25:
|
||||
# Shore/wet ground
|
||||
t = (h - 0.15) / 0.1
|
||||
return mcrfpy.Color(int(80 + t * 40), int(100 + t * 30), int(80 - t * 20))
|
||||
elif h < 0.5:
|
||||
# Lowland
|
||||
t = (h - 0.25) / 0.25
|
||||
return mcrfpy.Color(int(70 + t * 20), int(130 + t * 20), int(50 + t * 10))
|
||||
elif h < 0.7:
|
||||
# Highland
|
||||
t = (h - 0.5) / 0.2
|
||||
return mcrfpy.Color(int(60 + t * 30), int(110 - t * 20), int(45 + t * 15))
|
||||
elif h < 0.85:
|
||||
# Hills
|
||||
t = (h - 0.7) / 0.15
|
||||
return mcrfpy.Color(int(100 + t * 30), int(95 + t * 25), int(70 + t * 30))
|
||||
else:
|
||||
# Peaks
|
||||
t = (h - 0.85) / 0.15
|
||||
return mcrfpy.Color(int(150 + t * 60), int(150 + t * 60), int(155 + t * 60))
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 2
|
||||
panel_h = GRID_HEIGHT
|
||||
|
||||
# Left panel: River system
|
||||
river_map = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
|
||||
# Add terrain
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
river_map.add_noise(noise, world_size=(10, 10), mode='fbm', octaves=4, scale=0.3)
|
||||
river_map.add_hill((panel_w // 2, 5), 12, 0.3) # Mountain source
|
||||
river_map.normalize(0.3, 0.9)
|
||||
|
||||
# Main river - wide, flowing from top to bottom
|
||||
river_map.dig_bezier(
|
||||
points=((panel_w // 2, 2), (panel_w // 4, 15), (panel_w * 3 // 4, 30), (panel_w // 2, panel_h - 3)),
|
||||
start_radius=2, end_radius=5,
|
||||
start_height=0.1, end_height=0.05
|
||||
)
|
||||
|
||||
# Tributary from left
|
||||
river_map.dig_bezier(
|
||||
points=((3, 20), (10, 18), (15, 22), (panel_w // 3, 20)),
|
||||
start_radius=1, end_radius=2,
|
||||
start_height=0.12, end_height=0.1
|
||||
)
|
||||
|
||||
# Tributary from right
|
||||
river_map.dig_bezier(
|
||||
points=((panel_w - 3, 15), (panel_w - 8, 20), (panel_w - 12, 18), (panel_w * 2 // 3, 25)),
|
||||
start_radius=1, end_radius=2,
|
||||
start_height=0.12, end_height=0.1
|
||||
)
|
||||
|
||||
# Right panel: Road network
|
||||
road_map = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
road_map.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=3, scale=0.2)
|
||||
road_map.normalize(0.35, 0.7)
|
||||
|
||||
# Main road - relatively straight
|
||||
road_map.dig_bezier(
|
||||
points=((5, panel_h // 2), (15, panel_h // 2 - 3), (panel_w - 15, panel_h // 2 + 3), (panel_w - 5, panel_h // 2)),
|
||||
start_radius=2, end_radius=2,
|
||||
start_height=0.25, end_height=0.25
|
||||
)
|
||||
|
||||
# North-south crossing road
|
||||
road_map.dig_bezier(
|
||||
points=((panel_w // 2, 5), (panel_w // 2 + 5, 15), (panel_w // 2 - 5, 35), (panel_w // 2, panel_h - 5)),
|
||||
start_radius=2, end_radius=2,
|
||||
start_height=0.25, end_height=0.25
|
||||
)
|
||||
|
||||
# Winding mountain path
|
||||
road_map.dig_bezier(
|
||||
points=((5, 8), (15, 5), (20, 15), (25, 10)),
|
||||
start_radius=1, end_radius=1,
|
||||
start_height=0.28, end_height=0.28
|
||||
)
|
||||
|
||||
# Curved path to settlement
|
||||
road_map.dig_bezier(
|
||||
points=((panel_w - 5, panel_h - 8), (panel_w - 15, panel_h - 5), (panel_w - 10, panel_h - 15), (panel_w // 2 + 5, panel_h - 10)),
|
||||
start_radius=1, end_radius=2,
|
||||
start_height=0.27, end_height=0.26
|
||||
)
|
||||
|
||||
# Apply to grid
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
# Left panel: rivers
|
||||
h = river_map.get((x, y))
|
||||
color_layer.set(((x, y)), terrain_with_water(h))
|
||||
|
||||
# Right panel: roads (use brown for roads)
|
||||
h2 = road_map.get((x, y))
|
||||
if h2 < 0.3:
|
||||
# Road surface
|
||||
t = h2 / 0.3
|
||||
color = mcrfpy.Color(int(140 - t * 40), int(120 - t * 30), int(80 - t * 20))
|
||||
else:
|
||||
color = terrain_with_water(h2)
|
||||
color_layer.set(((panel_w + x, y)), color)
|
||||
|
||||
# Divider
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
|
||||
|
||||
# Labels
|
||||
labels = [("Rivers (dig_bezier)", 10, 10), ("Roads & Paths", panel_w * CELL_SIZE + 10, 10)]
|
||||
for text, x, ypos in labels:
|
||||
label = mcrfpy.Caption(text=text, pos=(x, ypos))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bezier_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_07_heightmap_bezier.png")
|
||||
print("Screenshot saved: procgen_07_heightmap_bezier.png")
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
"""HeightMap Thresholds and ColorLayer Integration Demo
|
||||
|
||||
Demonstrates: threshold, threshold_binary, inverse, count_in_range
|
||||
Also: ColorLayer.apply_ranges for multi-threshold coloring
|
||||
Shows terrain classification and visualization techniques.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Create source terrain
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
source = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
source.add_noise(noise, world_size=(10, 10), mode='fbm', octaves=5)
|
||||
source.add_hill((panel_w // 2, panel_h // 2), 8, 0.3)
|
||||
source.normalize(0.0, 1.0)
|
||||
|
||||
# Create derived heightmaps
|
||||
water_mask = source.threshold((0.0, 0.3)) # Returns NEW heightmap with values only in range
|
||||
land_binary = source.threshold_binary((0.3, 1.0), value=1.0) # Binary mask
|
||||
inverted = source.inverse() # Inverted values
|
||||
|
||||
# Count cells in ranges for classification stats
|
||||
water_count = source.count_in_range((0.0, 0.3))
|
||||
land_count = source.count_in_range((0.3, 0.7))
|
||||
mountain_count = source.count_in_range((0.7, 1.0))
|
||||
|
||||
# IMPORTANT: Render apply_ranges FIRST since it affects the whole layer
|
||||
# Panel 6: Using ColorLayer.apply_ranges (bottom-right)
|
||||
# Create a full-size heightmap and copy source data to correct position
|
||||
panel6_hmap = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=-1.0) # -1 won't match any range
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = source.get((x, y))
|
||||
panel6_hmap.fill(h, pos=(panel_w * 2 + x, panel_h + y), size=(1, 1))
|
||||
|
||||
# apply_ranges colors cells based on height ranges
|
||||
# Cells with -1.0 won't match any range and stay unchanged
|
||||
color_layer.apply_ranges(panel6_hmap, [
|
||||
((0.0, 0.2), (30, 80, 160)), # Deep water
|
||||
((0.2, 0.3), ((60, 120, 180), (120, 160, 140))), # Gradient: shallow to shore
|
||||
((0.3, 0.5), (80, 150, 60)), # Lowland
|
||||
((0.5, 0.7), ((60, 120, 40), (100, 100, 80))), # Gradient: forest to hills
|
||||
((0.7, 0.85), (130, 120, 110)), # Rock
|
||||
((0.85, 1.0), ((180, 180, 190), (250, 250, 255))), # Gradient: rock to snow
|
||||
])
|
||||
|
||||
# Now render the other 5 panels (they will overwrite only their regions)
|
||||
|
||||
# Panel 1 (top-left): Original grayscale
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = source.get((x, y))
|
||||
v = int(h * 255)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(v, v, v))
|
||||
|
||||
# Panel 2 (top-middle): threshold() - shows only values in range 0.0-0.3
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = water_mask.get((x, y))
|
||||
if h > 0:
|
||||
# Values were preserved in 0.0-0.3 range
|
||||
t = h / 0.3
|
||||
color_layer.set(((panel_w + x, y)), mcrfpy.Color(
|
||||
int(30 + t * 40), int(60 + t * 60), int(150 + t * 50)))
|
||||
else:
|
||||
# Outside threshold range - dark
|
||||
color_layer.set(((panel_w + x, y)), mcrfpy.Color(20, 20, 30))
|
||||
|
||||
# Panel 3 (top-right): threshold_binary() - land mask
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = land_binary.get((x, y))
|
||||
if h > 0:
|
||||
color_layer.set(((panel_w * 2 + x, y)), mcrfpy.Color(80, 140, 60)) # Land
|
||||
else:
|
||||
color_layer.set(((panel_w * 2 + x, y)), mcrfpy.Color(40, 80, 150)) # Water
|
||||
|
||||
# Panel 4 (bottom-left): inverse()
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = inverted.get((x, y))
|
||||
v = int(h * 255)
|
||||
color_layer.set(((x, panel_h + y)), mcrfpy.Color(v, int(v * 0.8), int(v * 0.6)))
|
||||
|
||||
# Panel 5 (bottom-middle): Classification using count_in_range results
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = source.get((x, y))
|
||||
if h < 0.3:
|
||||
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(50, 100, 180)) # Water
|
||||
elif h < 0.7:
|
||||
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(70, 140, 50)) # Land
|
||||
else:
|
||||
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(140, 130, 120)) # Mountain
|
||||
|
||||
# Labels
|
||||
labels = [
|
||||
("Original (grayscale)", 5, 5),
|
||||
("threshold(0-0.3)", panel_w * CELL_SIZE + 5, 5),
|
||||
("threshold_binary(land)", panel_w * 2 * CELL_SIZE + 5, 5),
|
||||
("inverse()", 5, panel_h * CELL_SIZE + 5),
|
||||
(f"Classified (W:{water_count} L:{land_count} M:{mountain_count})", panel_w * CELL_SIZE + 5, panel_h * CELL_SIZE + 5),
|
||||
("apply_ranges (biome)", panel_w * 2 * CELL_SIZE + 5, panel_h * CELL_SIZE + 5),
|
||||
]
|
||||
|
||||
for text, x, y in labels:
|
||||
label = mcrfpy.Caption(text=text, pos=(x, y))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid divider lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("thresholds_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_08_heightmap_thresholds.png")
|
||||
print("Screenshot saved: procgen_08_heightmap_thresholds.png")
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
"""BSP Dungeon Generation Demo
|
||||
|
||||
Demonstrates: BSP, split_recursive, leaves iteration, to_heightmap
|
||||
Classic roguelike dungeon generation with rooms.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create BSP tree covering the map
|
||||
bsp = mcrfpy.BSP(pos=(1, 1), size=(GRID_WIDTH - 2, GRID_HEIGHT - 2))
|
||||
|
||||
# Split recursively to create rooms
|
||||
# depth=4 creates up to 16 rooms, min_size ensures rooms aren't too small
|
||||
bsp.split_recursive(depth=4, min_size=(8, 6), max_ratio=1.5, seed=42)
|
||||
|
||||
# Convert to heightmap for visualization
|
||||
# shrink=1 leaves 1-tile border for walls
|
||||
rooms_hmap = bsp.to_heightmap(
|
||||
size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
select='leaves',
|
||||
shrink=1,
|
||||
value=1.0
|
||||
)
|
||||
|
||||
# Fill background (walls)
|
||||
color_layer.fill(mcrfpy.Color(40, 35, 45))
|
||||
|
||||
# Draw rooms
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if rooms_hmap.get((x, y)) > 0:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(80, 75, 70))
|
||||
|
||||
# Add some visual variety to rooms
|
||||
room_colors = [
|
||||
mcrfpy.Color(85, 80, 75),
|
||||
mcrfpy.Color(75, 70, 65),
|
||||
mcrfpy.Color(90, 85, 80),
|
||||
mcrfpy.Color(70, 65, 60),
|
||||
]
|
||||
|
||||
for i, leaf in enumerate(bsp.leaves()):
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
color = room_colors[i % len(room_colors)]
|
||||
|
||||
# Fill room interior (with shrink)
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
color_layer.set(((x, y)), color)
|
||||
|
||||
# Mark room center
|
||||
cx, cy = leaf.center()
|
||||
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
|
||||
color_layer.set(((cx, cy)), mcrfpy.Color(200, 180, 100))
|
||||
|
||||
# Simple corridor generation: connect adjacent rooms
|
||||
# Using adjacency graph
|
||||
adjacency = bsp.adjacency
|
||||
connected = set()
|
||||
|
||||
for leaf_idx in range(len(bsp)):
|
||||
leaf = bsp.get_leaf(leaf_idx)
|
||||
cx1, cy1 = leaf.center()
|
||||
|
||||
for neighbor_idx in adjacency[leaf_idx]:
|
||||
if (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx)) in connected:
|
||||
continue
|
||||
connected.add((min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx)))
|
||||
|
||||
neighbor = bsp.get_leaf(neighbor_idx)
|
||||
cx2, cy2 = neighbor.center()
|
||||
|
||||
# Draw L-shaped corridor
|
||||
# Horizontal first, then vertical
|
||||
x1, x2 = min(cx1, cx2), max(cx1, cx2)
|
||||
for x in range(x1, x2 + 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= cy1 < GRID_HEIGHT:
|
||||
color_layer.set(((x, cy1)), mcrfpy.Color(100, 95, 90))
|
||||
|
||||
y1, y2 = min(cy1, cy2), max(cy1, cy2)
|
||||
for y in range(y1, y2 + 1):
|
||||
if 0 <= cx2 < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
color_layer.set(((cx2, y)), mcrfpy.Color(100, 95, 90))
|
||||
|
||||
# Draw outer border
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, 0)), mcrfpy.Color(60, 50, 70))
|
||||
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(60, 50, 70))
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((0, y)), mcrfpy.Color(60, 50, 70))
|
||||
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(60, 50, 70))
|
||||
|
||||
# Stats
|
||||
stats = mcrfpy.Caption(
|
||||
text=f"BSP Dungeon: {len(bsp)} rooms, depth=4, seed=42",
|
||||
pos=(10, 10)
|
||||
)
|
||||
stats.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
stats.outline = 1
|
||||
stats.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(stats)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_dungeon_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_10_bsp_dungeon.png")
|
||||
print("Screenshot saved: procgen_10_bsp_dungeon.png")
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
"""BSP Traversal Orders Demo
|
||||
|
||||
Demonstrates: traverse() with different Traversal orders
|
||||
Shows how traversal order affects leaf enumeration.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
traversal_orders = [
|
||||
(mcrfpy.Traversal.PRE_ORDER, "PRE_ORDER", "Root first, then children"),
|
||||
(mcrfpy.Traversal.IN_ORDER, "IN_ORDER", "Left, node, right"),
|
||||
(mcrfpy.Traversal.POST_ORDER, "POST_ORDER", "Children before parent"),
|
||||
(mcrfpy.Traversal.LEVEL_ORDER, "LEVEL_ORDER", "Breadth-first by level"),
|
||||
(mcrfpy.Traversal.INVERTED_LEVEL_ORDER, "INV_LEVEL", "Deepest levels first"),
|
||||
]
|
||||
|
||||
panels = [
|
||||
(0, 0), (panel_w, 0), (panel_w * 2, 0),
|
||||
(0, panel_h), (panel_w, panel_h), (panel_w * 2, panel_h)
|
||||
]
|
||||
|
||||
# Distinct color palette for 8+ leaves
|
||||
leaf_colors = [
|
||||
mcrfpy.Color(220, 60, 60), # Red
|
||||
mcrfpy.Color(60, 180, 60), # Green
|
||||
mcrfpy.Color(60, 100, 220), # Blue
|
||||
mcrfpy.Color(220, 180, 40), # Yellow
|
||||
mcrfpy.Color(180, 60, 180), # Magenta
|
||||
mcrfpy.Color(60, 200, 200), # Cyan
|
||||
mcrfpy.Color(220, 120, 60), # Orange
|
||||
mcrfpy.Color(160, 100, 200), # Purple
|
||||
mcrfpy.Color(100, 200, 120), # Mint
|
||||
mcrfpy.Color(200, 100, 140), # Pink
|
||||
]
|
||||
|
||||
for panel_idx, (order, name, desc) in enumerate(traversal_orders):
|
||||
if panel_idx >= 6:
|
||||
break
|
||||
|
||||
ox, oy = panels[panel_idx]
|
||||
|
||||
# Create BSP for this panel
|
||||
bsp = mcrfpy.BSP(pos=(ox + 2, oy + 4), size=(panel_w - 4, panel_h - 6))
|
||||
bsp.split_recursive(depth=3, min_size=(5, 4), seed=42)
|
||||
|
||||
# Fill panel background (dark gray = walls)
|
||||
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(40, 35, 45))
|
||||
|
||||
# Traverse and color ONLY LEAVES by their position in traversal
|
||||
leaf_idx = 0
|
||||
for node in bsp.traverse(order):
|
||||
if not node.is_leaf:
|
||||
continue # Skip branch nodes
|
||||
|
||||
color = leaf_colors[leaf_idx % len(leaf_colors)]
|
||||
pos = node.pos
|
||||
size = node.size
|
||||
|
||||
# Shrink by 1 to show walls between rooms
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
color_layer.set(((x, y)), color)
|
||||
|
||||
# Draw leaf index in center
|
||||
cx, cy = node.center()
|
||||
# Draw index as a darker spot
|
||||
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
|
||||
dark = mcrfpy.Color(color.r // 2, color.g // 2, color.b // 2)
|
||||
color_layer.set(((cx, cy)), dark)
|
||||
if cx + 1 < GRID_WIDTH:
|
||||
color_layer.set(((cx + 1, cy)), dark)
|
||||
|
||||
leaf_idx += 1
|
||||
|
||||
# Add labels
|
||||
label = mcrfpy.Caption(text=f"{name}", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
desc_label = mcrfpy.Caption(text=f"{desc} ({leaf_idx} leaves)", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
|
||||
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
desc_label.outline = 1
|
||||
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(desc_label)
|
||||
|
||||
# Panel 6: Show tree depth levels (branch AND leaf nodes)
|
||||
ox, oy = panels[5]
|
||||
bsp = mcrfpy.BSP(pos=(ox + 2, oy + 4), size=(panel_w - 4, panel_h - 6))
|
||||
bsp.split_recursive(depth=3, min_size=(5, 4), seed=42)
|
||||
|
||||
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(40, 35, 45))
|
||||
|
||||
# Draw by level - deepest first so leaves are on top
|
||||
level_colors = [
|
||||
mcrfpy.Color(60, 40, 40), # Level 0 (root) - dark
|
||||
mcrfpy.Color(80, 60, 50), # Level 1
|
||||
mcrfpy.Color(100, 80, 60), # Level 2
|
||||
mcrfpy.Color(140, 120, 80), # Level 3 (leaves usually)
|
||||
]
|
||||
|
||||
# Use INVERTED_LEVEL_ORDER so leaves are drawn last
|
||||
for node in bsp.traverse(mcrfpy.Traversal.INVERTED_LEVEL_ORDER):
|
||||
level = node.level
|
||||
color = level_colors[min(level, len(level_colors) - 1)]
|
||||
|
||||
# Make leaves brighter
|
||||
if node.is_leaf:
|
||||
color = mcrfpy.Color(
|
||||
min(255, color.r + 80),
|
||||
min(255, color.g + 80),
|
||||
min(255, color.b + 60)
|
||||
)
|
||||
|
||||
pos = node.pos
|
||||
size = node.size
|
||||
|
||||
for y in range(pos[1], pos[1] + size[1]):
|
||||
for x in range(pos[0], pos[0] + size[0]):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
# Draw border
|
||||
if x == pos[0] or x == pos[0] + size[0] - 1 or \
|
||||
y == pos[1] or y == pos[1] + size[1] - 1:
|
||||
border = mcrfpy.Color(20, 20, 30)
|
||||
color_layer.set(((x, y)), border)
|
||||
else:
|
||||
color_layer.set(((x, y)), color)
|
||||
|
||||
label = mcrfpy.Caption(text="BY LEVEL (depth)", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
desc_label = mcrfpy.Caption(text="Darker=root, Bright=leaves", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
|
||||
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
desc_label.outline = 1
|
||||
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(desc_label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(60, 60, 60))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(60, 60, 60))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(60, 60, 60))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_traversal_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_11_bsp_traversal.png")
|
||||
print("Screenshot saved: procgen_11_bsp_traversal.png")
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
"""BSP Adjacency Graph Demo
|
||||
|
||||
Demonstrates: adjacency property, get_leaf, adjacent_tiles
|
||||
Shows room connectivity for corridor generation.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create dungeon BSP
|
||||
bsp = mcrfpy.BSP(pos=(2, 2), size=(GRID_WIDTH - 4, GRID_HEIGHT - 4))
|
||||
bsp.split_recursive(depth=3, min_size=(10, 8), max_ratio=1.4, seed=42)
|
||||
|
||||
# Fill with wall color
|
||||
color_layer.fill(mcrfpy.Color(50, 45, 55))
|
||||
|
||||
# Generate distinct colors for each room
|
||||
num_rooms = len(bsp)
|
||||
room_colors = []
|
||||
for i in range(num_rooms):
|
||||
hue = (i * 137.5) % 360 # Golden angle for good distribution
|
||||
# HSV to RGB (simplified, saturation=0.6, value=0.7)
|
||||
h = hue / 60
|
||||
c = 0.42 # 0.6 * 0.7
|
||||
x = c * (1 - abs(h % 2 - 1))
|
||||
m = 0.28 # 0.7 - c
|
||||
|
||||
if h < 1: r, g, b = c, x, 0
|
||||
elif h < 2: r, g, b = x, c, 0
|
||||
elif h < 3: r, g, b = 0, c, x
|
||||
elif h < 4: r, g, b = 0, x, c
|
||||
elif h < 5: r, g, b = x, 0, c
|
||||
else: r, g, b = c, 0, x
|
||||
|
||||
room_colors.append(mcrfpy.Color(
|
||||
int((r + m) * 255),
|
||||
int((g + m) * 255),
|
||||
int((b + m) * 255)
|
||||
))
|
||||
|
||||
# Draw rooms with unique colors
|
||||
for i, leaf in enumerate(bsp.leaves()):
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
color = room_colors[i]
|
||||
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
color_layer.set(((x, y)), color)
|
||||
|
||||
# Room label
|
||||
cx, cy = leaf.center()
|
||||
label = mcrfpy.Caption(text=str(i), pos=(cx * CELL_SIZE - 4, cy * CELL_SIZE - 8))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Draw corridors using adjacency graph
|
||||
adjacency = bsp.adjacency
|
||||
connected = set()
|
||||
|
||||
corridor_color = mcrfpy.Color(100, 95, 90)
|
||||
door_color = mcrfpy.Color(180, 140, 80)
|
||||
|
||||
for leaf_idx in range(num_rooms):
|
||||
leaf = bsp.get_leaf(leaf_idx)
|
||||
|
||||
# Get adjacent_tiles for this leaf
|
||||
adj_tiles = leaf.adjacent_tiles
|
||||
|
||||
for neighbor_idx in adjacency[leaf_idx]:
|
||||
pair = (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx))
|
||||
if pair in connected:
|
||||
continue
|
||||
connected.add(pair)
|
||||
|
||||
neighbor = bsp.get_leaf(neighbor_idx)
|
||||
|
||||
# Find shared wall tiles
|
||||
if neighbor_idx in adj_tiles:
|
||||
wall_tiles = adj_tiles[neighbor_idx]
|
||||
if len(wall_tiles) > 0:
|
||||
# Pick middle tile for door
|
||||
mid_tile = wall_tiles[len(wall_tiles) // 2]
|
||||
dx, dy = int(mid_tile.x), int(mid_tile.y)
|
||||
|
||||
# Draw door
|
||||
color_layer.set(((dx, dy)), door_color)
|
||||
|
||||
# Simple corridor: connect room centers through door
|
||||
cx1, cy1 = leaf.center()
|
||||
cx2, cy2 = neighbor.center()
|
||||
|
||||
# Path from room 1 to door
|
||||
for x in range(min(cx1, dx), max(cx1, dx) + 1):
|
||||
color_layer.set(((x, cy1)), corridor_color)
|
||||
for y in range(min(cy1, dy), max(cy1, dy) + 1):
|
||||
color_layer.set(((dx, y)), corridor_color)
|
||||
|
||||
# Path from door to room 2
|
||||
for x in range(min(dx, cx2), max(dx, cx2) + 1):
|
||||
color_layer.set(((x, dy)), corridor_color)
|
||||
for y in range(min(dy, cy2), max(dy, cy2) + 1):
|
||||
color_layer.set(((cx2, y)), corridor_color)
|
||||
else:
|
||||
# Fallback: L-shaped corridor
|
||||
cx1, cy1 = leaf.center()
|
||||
cx2, cy2 = neighbor.center()
|
||||
|
||||
for x in range(min(cx1, cx2), max(cx1, cx2) + 1):
|
||||
color_layer.set(((x, cy1)), corridor_color)
|
||||
for y in range(min(cy1, cy2), max(cy1, cy2) + 1):
|
||||
color_layer.set(((cx2, y)), corridor_color)
|
||||
|
||||
# Title and stats
|
||||
title = mcrfpy.Caption(
|
||||
text=f"BSP Adjacency: {num_rooms} rooms, {len(connected)} connections",
|
||||
pos=(10, 10)
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
# Legend
|
||||
legend = mcrfpy.Caption(
|
||||
text="Numbers = room index, Gold = doors, Brown = corridors",
|
||||
pos=(10, GRID_HEIGHT * CELL_SIZE - 25)
|
||||
)
|
||||
legend.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
legend.outline = 1
|
||||
legend.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(legend)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_adjacency_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_12_bsp_adjacency.png")
|
||||
print("Screenshot saved: procgen_12_bsp_adjacency.png")
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
"""BSP Shrink Parameter Demo
|
||||
|
||||
Demonstrates: to_heightmap with different shrink values
|
||||
Shows room padding for walls and varied room sizes.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Use reasonable shrink values relative to room sizes
|
||||
shrink_values = [
|
||||
(0, "shrink=0", "Rooms fill BSP bounds"),
|
||||
(1, "shrink=1", "Standard 1-tile walls"),
|
||||
(2, "shrink=2", "Thick fortress walls"),
|
||||
(3, "shrink=3", "Wide hallway spacing"),
|
||||
(-1, "Random shrink", "Per-room variation"),
|
||||
(-2, "Gradient", "Shrink by leaf index"),
|
||||
]
|
||||
|
||||
panels = [
|
||||
(0, 0), (panel_w, 0), (panel_w * 2, 0),
|
||||
(0, panel_h), (panel_w, panel_h), (panel_w * 2, panel_h)
|
||||
]
|
||||
|
||||
for panel_idx, (shrink, title, desc) in enumerate(shrink_values):
|
||||
ox, oy = panels[panel_idx]
|
||||
|
||||
# Create BSP - use depth=2 for larger rooms, bigger min_size
|
||||
bsp = mcrfpy.BSP(pos=(ox + 1, oy + 3), size=(panel_w - 2, panel_h - 4))
|
||||
bsp.split_recursive(depth=2, min_size=(8, 6), seed=42)
|
||||
|
||||
# Fill panel background (stone wall)
|
||||
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(50, 45, 55))
|
||||
|
||||
if shrink >= 0:
|
||||
# Standard shrink value using to_heightmap
|
||||
rooms_hmap = bsp.to_heightmap(
|
||||
size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
select='leaves',
|
||||
shrink=shrink,
|
||||
value=1.0
|
||||
)
|
||||
|
||||
# Draw floors with color based on shrink level
|
||||
floor_colors = [
|
||||
mcrfpy.Color(140, 120, 100), # shrink=0: tan/full
|
||||
mcrfpy.Color(110, 100, 90), # shrink=1: gray-brown
|
||||
mcrfpy.Color(90, 95, 100), # shrink=2: blue-gray
|
||||
mcrfpy.Color(80, 90, 110), # shrink=3: slate
|
||||
]
|
||||
floor_color = floor_colors[min(shrink, len(floor_colors) - 1)]
|
||||
|
||||
for y in range(oy, oy + panel_h):
|
||||
for x in range(ox, ox + panel_w):
|
||||
if rooms_hmap.get((x, y)) > 0:
|
||||
# Add subtle tile pattern
|
||||
var = ((x + y) % 2) * 8
|
||||
c = mcrfpy.Color(
|
||||
floor_color.r + var,
|
||||
floor_color.g + var,
|
||||
floor_color.b + var
|
||||
)
|
||||
color_layer.set(((x, y)), c)
|
||||
elif shrink == -1:
|
||||
# Random shrink per room
|
||||
import random
|
||||
rand = random.Random(42)
|
||||
for leaf in bsp.leaves():
|
||||
room_shrink = rand.randint(0, 3)
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
|
||||
x1 = pos[0] + room_shrink
|
||||
y1 = pos[1] + room_shrink
|
||||
x2 = pos[0] + size[0] - room_shrink
|
||||
y2 = pos[1] + size[1] - room_shrink
|
||||
|
||||
if x2 > x1 and y2 > y1:
|
||||
colors = [
|
||||
mcrfpy.Color(160, 130, 100), # Full
|
||||
mcrfpy.Color(130, 120, 100),
|
||||
mcrfpy.Color(100, 110, 110),
|
||||
mcrfpy.Color(80, 90, 100), # Most shrunk
|
||||
]
|
||||
floor_color = colors[room_shrink]
|
||||
|
||||
for y in range(y1, y2):
|
||||
for x in range(x1, x2):
|
||||
if ox <= x < ox + panel_w and oy <= y < oy + panel_h:
|
||||
var = ((x + y) % 2) * 6
|
||||
c = mcrfpy.Color(
|
||||
floor_color.r + var,
|
||||
floor_color.g + var,
|
||||
floor_color.b + var
|
||||
)
|
||||
color_layer.set(((x, y)), c)
|
||||
else:
|
||||
# Gradient shrink by leaf index
|
||||
leaves = list(bsp.leaves())
|
||||
for i, leaf in enumerate(leaves):
|
||||
# Shrink increases with leaf index
|
||||
room_shrink = min(3, i)
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
|
||||
x1 = pos[0] + room_shrink
|
||||
y1 = pos[1] + room_shrink
|
||||
x2 = pos[0] + size[0] - room_shrink
|
||||
y2 = pos[1] + size[1] - room_shrink
|
||||
|
||||
if x2 > x1 and y2 > y1:
|
||||
# Color gradient: warm to cool as shrink increases
|
||||
t = i / max(1, len(leaves) - 1)
|
||||
floor_color = mcrfpy.Color(
|
||||
int(180 - t * 80),
|
||||
int(120 + t * 20),
|
||||
int(80 + t * 60)
|
||||
)
|
||||
|
||||
for y in range(y1, y2):
|
||||
for x in range(x1, x2):
|
||||
if ox <= x < ox + panel_w and oy <= y < oy + panel_h:
|
||||
var = ((x + y) % 2) * 6
|
||||
c = mcrfpy.Color(
|
||||
floor_color.r + var,
|
||||
floor_color.g + var - 2,
|
||||
floor_color.b + var
|
||||
)
|
||||
color_layer.set(((x, y)), c)
|
||||
|
||||
# Add labels
|
||||
label = mcrfpy.Caption(text=title, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
desc_label = mcrfpy.Caption(text=desc, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
|
||||
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
desc_label.outline = 1
|
||||
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(desc_label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(30, 30, 35))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(30, 30, 35))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(30, 30, 35))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_shrink_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_13_bsp_shrink.png")
|
||||
print("Screenshot saved: procgen_13_bsp_shrink.png")
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
"""BSP Manual Split Demo
|
||||
|
||||
Demonstrates: split_once for controlled layouts
|
||||
Shows handcrafted room placement with manual BSP control.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Fill background
|
||||
color_layer.fill(mcrfpy.Color(50, 45, 55))
|
||||
|
||||
# Create main BSP covering most of the map
|
||||
bsp = mcrfpy.BSP(pos=(2, 2), size=(GRID_WIDTH - 4, GRID_HEIGHT - 4))
|
||||
|
||||
# Manual split strategy for a temple-like layout:
|
||||
# 1. Split horizontally to create upper/lower sections
|
||||
# 2. Upper section: main hall (large) + side rooms
|
||||
# 3. Lower section: entrance + storage areas
|
||||
|
||||
# First split: horizontal, creating top (sanctuary) and bottom (entrance) areas
|
||||
# Split at about 60% height
|
||||
split_y = 2 + int((GRID_HEIGHT - 4) * 0.6)
|
||||
bsp.split_once(horizontal=True, position=split_y)
|
||||
|
||||
# Now manually color the structure
|
||||
root = bsp.root
|
||||
|
||||
# Get the two main regions
|
||||
upper = root.left # Sanctuary area
|
||||
lower = root.right # Entrance area
|
||||
|
||||
# Color the sanctuary (upper area) - golden temple floor
|
||||
if upper:
|
||||
pos, size = upper.pos, upper.size
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
# Create a pattern
|
||||
if (x + y) % 4 == 0:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(180, 150, 80))
|
||||
else:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(160, 130, 70))
|
||||
|
||||
# Add altar in center of sanctuary
|
||||
cx, cy = upper.center()
|
||||
for dy in range(-2, 3):
|
||||
for dx in range(-3, 4):
|
||||
nx, ny = cx + dx, cy + dy
|
||||
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
|
||||
if abs(dx) <= 1 and abs(dy) <= 1:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(200, 180, 100)) # Altar
|
||||
else:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(140, 100, 60)) # Altar base
|
||||
|
||||
# Color the entrance (lower area) - stone floor
|
||||
if lower:
|
||||
pos, size = lower.pos, lower.size
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
base = 80 + ((x * 3 + y * 7) % 20)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base, base - 5, base - 10))
|
||||
|
||||
# Add entrance path
|
||||
cx = pos[0] + size[0] // 2
|
||||
for y in range(pos[1] + size[1] - 1, pos[1], -1):
|
||||
for dx in range(-2, 3):
|
||||
nx = cx + dx
|
||||
if pos[0] < nx < pos[0] + size[0] - 1:
|
||||
color_layer.set(((nx, y)), mcrfpy.Color(100, 95, 85))
|
||||
|
||||
# Add pillars along the sides
|
||||
if upper:
|
||||
pos, size = upper.pos, upper.size
|
||||
for y in range(pos[1] + 3, pos[1] + size[1] - 3, 4):
|
||||
# Left pillars
|
||||
color_layer.set(((pos[0] + 3, y)), mcrfpy.Color(120, 110, 100))
|
||||
color_layer.set(((pos[0] + 3, y + 1)), mcrfpy.Color(120, 110, 100))
|
||||
# Right pillars
|
||||
color_layer.set(((pos[0] + size[0] - 4, y)), mcrfpy.Color(120, 110, 100))
|
||||
color_layer.set(((pos[0] + size[0] - 4, y + 1)), mcrfpy.Color(120, 110, 100))
|
||||
|
||||
# Add side chambers using manual rectangles
|
||||
# Left chamber
|
||||
chamber_w, chamber_h = 8, 6
|
||||
for y in range(10, 10 + chamber_h):
|
||||
for x in range(4, 4 + chamber_w):
|
||||
if x == 4 or x == 4 + chamber_w - 1 or y == 10 or y == 10 + chamber_h - 1:
|
||||
continue # Skip border (walls)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(100, 80, 90)) # Purple-ish storage
|
||||
|
||||
# Right chamber
|
||||
for y in range(10, 10 + chamber_h):
|
||||
for x in range(GRID_WIDTH - 4 - chamber_w, GRID_WIDTH - 4):
|
||||
if x == GRID_WIDTH - 4 - chamber_w or x == GRID_WIDTH - 5 or y == 10 or y == 10 + chamber_h - 1:
|
||||
continue
|
||||
color_layer.set(((x, y)), mcrfpy.Color(80, 100, 90)) # Green-ish treasury
|
||||
|
||||
# Connect chambers to main hall
|
||||
hall_y = 12
|
||||
for x in range(4 + chamber_w, 15):
|
||||
color_layer.set(((x, hall_y)), mcrfpy.Color(90, 85, 80))
|
||||
for x in range(GRID_WIDTH - 15, GRID_WIDTH - 4 - chamber_w):
|
||||
color_layer.set(((x, hall_y)), mcrfpy.Color(90, 85, 80))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="BSP split_once: Temple Layout", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
# Labels for areas
|
||||
labels = [
|
||||
("SANCTUARY", GRID_WIDTH // 2 * CELL_SIZE - 40, 80),
|
||||
("ENTRANCE", GRID_WIDTH // 2 * CELL_SIZE - 35, split_y * CELL_SIZE + 30),
|
||||
("Storage", 50, 180),
|
||||
("Treasury", (GRID_WIDTH - 10) * CELL_SIZE - 30, 180),
|
||||
]
|
||||
for text, x, y in labels:
|
||||
lbl = mcrfpy.Caption(text=text, pos=(x, y))
|
||||
lbl.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
lbl.outline = 1
|
||||
lbl.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(lbl)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_manual_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_14_bsp_manual_split.png")
|
||||
print("Screenshot saved: procgen_14_bsp_manual_split.png")
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
"""NoiseSource Algorithms Demo
|
||||
|
||||
Demonstrates: simplex, perlin, wavelet noise algorithms
|
||||
Shows visual differences between noise types.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def value_to_terrain(h):
|
||||
"""Convert noise value (-1 to 1) to terrain color."""
|
||||
# Normalize from -1..1 to 0..1
|
||||
h = (h + 1) / 2
|
||||
h = max(0.0, min(1.0, h))
|
||||
|
||||
if h < 0.3:
|
||||
t = h / 0.3
|
||||
return mcrfpy.Color(int(30 + t * 40), int(60 + t * 60), int(140 + t * 40))
|
||||
elif h < 0.45:
|
||||
t = (h - 0.3) / 0.15
|
||||
return mcrfpy.Color(int(70 + t * 120), int(120 + t * 60), int(100 - t * 60))
|
||||
elif h < 0.6:
|
||||
t = (h - 0.45) / 0.15
|
||||
return mcrfpy.Color(int(60 + t * 20), int(130 + t * 20), int(50 + t * 10))
|
||||
elif h < 0.75:
|
||||
t = (h - 0.6) / 0.15
|
||||
return mcrfpy.Color(int(50 + t * 50), int(110 - t * 20), int(40 + t * 20))
|
||||
elif h < 0.88:
|
||||
t = (h - 0.75) / 0.13
|
||||
return mcrfpy.Color(int(100 + t * 40), int(95 + t * 35), int(80 + t * 40))
|
||||
else:
|
||||
t = (h - 0.88) / 0.12
|
||||
return mcrfpy.Color(int(180 + t * 70), int(180 + t * 70), int(190 + t * 60))
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
algorithms = [
|
||||
('simplex', "SIMPLEX", "Fast, no visible artifacts"),
|
||||
('perlin', "PERLIN", "Classic, slight grid bias"),
|
||||
('wavelet', "WAVELET", "Smooth, no tiling"),
|
||||
]
|
||||
|
||||
# Top row: FBM (natural terrain)
|
||||
# Bottom row: Raw noise (single octave)
|
||||
for col, (algo, name, desc) in enumerate(algorithms):
|
||||
ox = col * panel_w
|
||||
|
||||
# Create noise source
|
||||
noise = mcrfpy.NoiseSource(
|
||||
dimensions=2,
|
||||
algorithm=algo,
|
||||
hurst=0.5,
|
||||
lacunarity=2.0,
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Top panel: FBM
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
# Sample at world coordinates
|
||||
wx = x * 0.15
|
||||
wy = y * 0.15
|
||||
val = noise.fbm((wx, wy), octaves=5)
|
||||
color_layer.set(((ox + x, y)), value_to_terrain(val))
|
||||
|
||||
# Bottom panel: Raw (flat)
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
wx = x * 0.15
|
||||
wy = y * 0.15
|
||||
val = noise.get((wx, wy))
|
||||
color_layer.set(((ox + x, panel_h + y)), value_to_terrain(val))
|
||||
|
||||
# Labels
|
||||
top_label = mcrfpy.Caption(text=f"{name} (FBM)", pos=(ox * CELL_SIZE + 5, 5))
|
||||
top_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
top_label.outline = 1
|
||||
top_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(top_label)
|
||||
|
||||
bottom_label = mcrfpy.Caption(text=f"{name} (raw)", pos=(ox * CELL_SIZE + 5, panel_h * CELL_SIZE + 5))
|
||||
bottom_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
bottom_label.outline = 1
|
||||
bottom_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(bottom_label)
|
||||
|
||||
desc_label = mcrfpy.Caption(text=desc, pos=(ox * CELL_SIZE + 5, 22))
|
||||
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
desc_label.outline = 1
|
||||
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(desc_label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("noise_algo_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_20_noise_algorithms.png")
|
||||
print("Screenshot saved: procgen_20_noise_algorithms.png")
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
"""NoiseSource Parameters Demo
|
||||
|
||||
Demonstrates: hurst (roughness), lacunarity (frequency scaling), octaves
|
||||
Shows how parameters affect terrain character.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def value_to_gray(h):
|
||||
"""Simple grayscale visualization."""
|
||||
h = (h + 1) / 2 # -1..1 to 0..1
|
||||
h = max(0.0, min(1.0, h))
|
||||
v = int(h * 255)
|
||||
return mcrfpy.Color(v, v, v)
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 3
|
||||
|
||||
# 3x3 grid showing parameter variations
|
||||
# Rows: different hurst values (roughness)
|
||||
# Cols: different lacunarity values
|
||||
|
||||
hurst_values = [0.2, 0.5, 0.8]
|
||||
lacunarity_values = [1.5, 2.0, 3.0]
|
||||
|
||||
for row, hurst in enumerate(hurst_values):
|
||||
for col, lacunarity in enumerate(lacunarity_values):
|
||||
ox = col * panel_w
|
||||
oy = row * panel_h
|
||||
|
||||
# Create noise with these parameters
|
||||
noise = mcrfpy.NoiseSource(
|
||||
dimensions=2,
|
||||
algorithm='simplex',
|
||||
hurst=hurst,
|
||||
lacunarity=lacunarity,
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Sample using heightmap for efficiency
|
||||
hmap = noise.sample(
|
||||
size=(panel_w, panel_h),
|
||||
world_origin=(0, 0),
|
||||
world_size=(10, 10),
|
||||
mode='fbm',
|
||||
octaves=6
|
||||
)
|
||||
|
||||
# Apply to color layer
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), value_to_gray(h))
|
||||
|
||||
# Parameter label
|
||||
label = mcrfpy.Caption(
|
||||
text=f"H={hurst} L={lacunarity}",
|
||||
pos=(ox * CELL_SIZE + 3, oy * CELL_SIZE + 3)
|
||||
)
|
||||
label.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Row/Column labels
|
||||
row_labels = ["Low Hurst (rough)", "Mid Hurst (natural)", "High Hurst (smooth)"]
|
||||
for row, text in enumerate(row_labels):
|
||||
label = mcrfpy.Caption(text=text, pos=(5, row * panel_h * CELL_SIZE + panel_h * CELL_SIZE - 20))
|
||||
label.fill_color = mcrfpy.Color(255, 200, 100)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
col_labels = ["Low Lacunarity", "Standard (2.0)", "High Lacunarity"]
|
||||
for col, text in enumerate(col_labels):
|
||||
label = mcrfpy.Caption(text=text, pos=(col * panel_w * CELL_SIZE + 5, GRID_HEIGHT * CELL_SIZE - 20))
|
||||
label.fill_color = mcrfpy.Color(100, 200, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(100, 100, 100))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(100, 100, 100))
|
||||
color_layer.set(((x, panel_h * 2 - 1)), mcrfpy.Color(100, 100, 100))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("noise_params_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_21_noise_parameters.png")
|
||||
print("Screenshot saved: procgen_21_noise_parameters.png")
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
"""Advanced: Cave-Carved Dungeon
|
||||
|
||||
Combines: BSP (room structure) + Noise (organic cave walls) + Erosion
|
||||
Creates a dungeon where rooms have been carved from natural cave formations.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Create base cave system using noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
cave_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
cave_map.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=4)
|
||||
cave_map.normalize(0.0, 1.0)
|
||||
|
||||
# Step 2: Create BSP rooms
|
||||
bsp = mcrfpy.BSP(pos=(3, 3), size=(GRID_WIDTH - 6, GRID_HEIGHT - 6))
|
||||
bsp.split_recursive(depth=3, min_size=(10, 8), max_ratio=1.5, seed=42)
|
||||
|
||||
rooms_hmap = bsp.to_heightmap(
|
||||
size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
select='leaves',
|
||||
shrink=2,
|
||||
value=1.0
|
||||
)
|
||||
|
||||
# Step 3: Combine - rooms carve into cave, cave affects walls
|
||||
combined = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
combined.copy_from(cave_map)
|
||||
|
||||
# Scale cave values to mid-range so rooms stand out
|
||||
combined.scale(0.5)
|
||||
combined.add_constant(0.2)
|
||||
|
||||
# Add room interiors (rooms become high values)
|
||||
combined.max(rooms_hmap)
|
||||
|
||||
# Step 4: Apply GENTLE erosion for organic edges
|
||||
# Use fewer drops and lower erosion rate
|
||||
combined.rain_erosion(drops=100, erosion=0.02, sedimentation=0.01, seed=42)
|
||||
|
||||
# Re-normalize to ensure we use the full value range
|
||||
combined.normalize(0.0, 1.0)
|
||||
|
||||
# Step 5: Create corridor connections
|
||||
adjacency = bsp.adjacency
|
||||
connected = set()
|
||||
|
||||
corridor_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
|
||||
for leaf_idx in range(len(bsp)):
|
||||
leaf = bsp.get_leaf(leaf_idx)
|
||||
cx1, cy1 = leaf.center()
|
||||
|
||||
for neighbor_idx in adjacency[leaf_idx]:
|
||||
pair = (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx))
|
||||
if pair in connected:
|
||||
continue
|
||||
connected.add(pair)
|
||||
|
||||
neighbor = bsp.get_leaf(neighbor_idx)
|
||||
cx2, cy2 = neighbor.center()
|
||||
|
||||
# Draw corridor using bezier for organic feel
|
||||
mid_x = (cx1 + cx2) // 2 + ((leaf_idx * 3) % 5 - 2)
|
||||
mid_y = (cy1 + cy2) // 2 + ((neighbor_idx * 7) % 5 - 2)
|
||||
|
||||
corridor_map.dig_bezier(
|
||||
points=((cx1, cy1), (mid_x, cy1), (mid_x, cy2), (cx2, cy2)),
|
||||
start_radius=1.5, end_radius=1.5,
|
||||
start_height=0.0, end_height=0.0
|
||||
)
|
||||
|
||||
# Add corridors - dig_bezier creates low values where corridors are
|
||||
# We want high values there, so invert the corridor map logic
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
corr_val = corridor_map.get((x, y))
|
||||
if corr_val < 0.5: # Corridor was dug here
|
||||
current = combined.get((x, y))
|
||||
combined.fill(max(current, 0.7), pos=(x, y), size=(1, 1))
|
||||
|
||||
# Step 6: Render with cave aesthetics
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
h = combined.get((x, y))
|
||||
|
||||
if h < 0.30:
|
||||
# Solid rock/wall - darker
|
||||
base = 30 + int(cave_map.get((x, y)) * 20)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
|
||||
elif h < 0.40:
|
||||
# Cave wall edge (rough transition)
|
||||
t = (h - 0.30) / 0.10
|
||||
base = int(40 + t * 15)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
|
||||
elif h < 0.55:
|
||||
# Cave floor (natural stone)
|
||||
t = (h - 0.40) / 0.15
|
||||
base = 65 + int(t * 20)
|
||||
var = ((x * 7 + y * 11) % 10)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + var, base - 5 + var, base - 10))
|
||||
elif h < 0.70:
|
||||
# Corridor/worked passage
|
||||
base = 85 + ((x + y) % 2) * 5
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base, base - 3, base - 6))
|
||||
else:
|
||||
# Room floor (finely worked stone)
|
||||
base = 105 + ((x + y) % 2) * 8
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base, base - 8, base - 12))
|
||||
|
||||
# Mark room centers with special tile
|
||||
for leaf in bsp.leaves():
|
||||
cx, cy = leaf.center()
|
||||
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
|
||||
color_layer.set(((cx, cy)), mcrfpy.Color(160, 140, 120))
|
||||
# Cross pattern
|
||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||
nx, ny = cx + dx, cy + dy
|
||||
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(140, 125, 105))
|
||||
|
||||
# Outer border
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, 0)), mcrfpy.Color(20, 15, 25))
|
||||
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(20, 15, 25))
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((0, y)), mcrfpy.Color(20, 15, 25))
|
||||
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(20, 15, 25))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Cave-Carved Dungeon: BSP + Noise + Erosion", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("cave_dungeon")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_30_advanced_cave_dungeon.png")
|
||||
print("Screenshot saved: procgen_30_advanced_cave_dungeon.png")
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
"""Advanced: Island Terrain Generation
|
||||
|
||||
Combines: Noise (base terrain) + Voronoi (biomes) + Hills + Erosion + Bezier (rivers)
|
||||
Creates a tropical island with varied biomes and water features.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def biome_color(elevation, moisture):
|
||||
"""Determine color based on elevation and moisture."""
|
||||
if elevation < 0.25:
|
||||
# Water
|
||||
t = elevation / 0.25
|
||||
return mcrfpy.Color(int(30 + t * 30), int(80 + t * 40), int(160 + t * 40))
|
||||
elif elevation < 0.32:
|
||||
# Beach
|
||||
return mcrfpy.Color(220, 200, 150)
|
||||
elif elevation < 0.5:
|
||||
# Lowland - varies by moisture
|
||||
if moisture < 0.3:
|
||||
return mcrfpy.Color(180, 170, 110) # Desert/savanna
|
||||
elif moisture < 0.6:
|
||||
return mcrfpy.Color(80, 140, 60) # Grassland
|
||||
else:
|
||||
return mcrfpy.Color(40, 100, 50) # Rainforest
|
||||
elif elevation < 0.7:
|
||||
# Highland
|
||||
if moisture < 0.4:
|
||||
return mcrfpy.Color(100, 90, 70) # Dry hills
|
||||
else:
|
||||
return mcrfpy.Color(50, 90, 45) # Forest
|
||||
elif elevation < 0.85:
|
||||
# Mountain
|
||||
return mcrfpy.Color(110, 105, 100)
|
||||
else:
|
||||
# Peak
|
||||
return mcrfpy.Color(220, 225, 230)
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Create base elevation using noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
elevation = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
elevation.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=5)
|
||||
elevation.normalize(0.0, 1.0)
|
||||
|
||||
# Step 2: Create island shape using radial falloff
|
||||
cx, cy = GRID_WIDTH / 2, GRID_HEIGHT / 2
|
||||
max_dist = min(cx, cy) * 0.85
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
dist = ((x - cx) ** 2 + (y - cy) ** 2) ** 0.5
|
||||
falloff = max(0, 1 - (dist / max_dist) ** 1.5)
|
||||
current = elevation.get((x, y))
|
||||
elevation.fill(current * falloff, pos=(x, y), size=(1, 1))
|
||||
|
||||
# Step 3: Add central mountain range
|
||||
elevation.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 15, 0.5)
|
||||
elevation.add_hill((GRID_WIDTH // 2 - 8, GRID_HEIGHT // 2 + 3), 8, 0.3)
|
||||
elevation.add_hill((GRID_WIDTH // 2 + 10, GRID_HEIGHT // 2 - 5), 6, 0.25)
|
||||
|
||||
# Step 4: Create moisture map using different noise
|
||||
moisture_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=123)
|
||||
moisture = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
moisture.add_noise(moisture_noise, world_size=(8, 8), mode='fbm', octaves=3)
|
||||
moisture.normalize(0.0, 1.0)
|
||||
|
||||
# Step 5: Add voronoi for biome boundaries
|
||||
biome_regions = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
biome_regions.add_voronoi(num_points=8, coefficients=(0.5, -0.3), seed=77)
|
||||
biome_regions.normalize(0.0, 1.0)
|
||||
|
||||
# Blend voronoi into moisture
|
||||
moisture.lerp(biome_regions, 0.4)
|
||||
|
||||
# Step 6: Apply erosion to elevation
|
||||
elevation.rain_erosion(drops=2000, erosion=0.08, sedimentation=0.04, seed=42)
|
||||
elevation.normalize(0.0, 1.0)
|
||||
|
||||
# Step 7: Carve rivers from mountains to sea
|
||||
# Main river
|
||||
elevation.dig_bezier(
|
||||
points=((GRID_WIDTH // 2, GRID_HEIGHT // 2 - 5),
|
||||
(GRID_WIDTH // 2 - 10, GRID_HEIGHT // 2),
|
||||
(GRID_WIDTH // 4, GRID_HEIGHT // 2 + 5),
|
||||
(5, GRID_HEIGHT // 2 + 8)),
|
||||
start_radius=0.5, end_radius=2,
|
||||
start_height=0.3, end_height=0.15
|
||||
)
|
||||
|
||||
# Secondary river
|
||||
elevation.dig_bezier(
|
||||
points=((GRID_WIDTH // 2 + 5, GRID_HEIGHT // 2),
|
||||
(GRID_WIDTH // 2 + 15, GRID_HEIGHT // 3),
|
||||
(GRID_WIDTH - 15, GRID_HEIGHT // 4),
|
||||
(GRID_WIDTH - 5, GRID_HEIGHT // 4 + 3)),
|
||||
start_radius=0.5, end_radius=1.5,
|
||||
start_height=0.32, end_height=0.18
|
||||
)
|
||||
|
||||
# Step 8: Render
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
elev = elevation.get((x, y))
|
||||
moist = moisture.get((x, y))
|
||||
color_layer.set(((x, y)), biome_color(elev, moist))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Island Terrain: Noise + Voronoi + Hills + Erosion + Rivers", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("island")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_31_advanced_island.png")
|
||||
print("Screenshot saved: procgen_31_advanced_island.png")
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
"""Advanced: Procedural City Map
|
||||
|
||||
Combines: BSP (city blocks/buildings) + Noise (terrain/parks) + Voronoi (districts)
|
||||
Creates a city map with districts, buildings, roads, and parks.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Create district map using voronoi
|
||||
districts = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
districts.add_voronoi(num_points=6, coefficients=(1.0, 0.0), seed=42)
|
||||
districts.normalize(0.0, 1.0)
|
||||
|
||||
# District types based on value
|
||||
# 0.0-0.2: Residential (green-ish)
|
||||
# 0.2-0.4: Commercial (blue-ish)
|
||||
# 0.4-0.6: Industrial (gray)
|
||||
# 0.6-0.8: Park/nature
|
||||
# 0.8-1.0: Downtown (tall buildings)
|
||||
|
||||
# Step 2: Create building blocks using BSP
|
||||
bsp = mcrfpy.BSP(pos=(1, 1), size=(GRID_WIDTH - 2, GRID_HEIGHT - 2))
|
||||
bsp.split_recursive(depth=4, min_size=(6, 5), max_ratio=2.0, seed=42)
|
||||
|
||||
# Step 3: Create park areas using noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=99)
|
||||
parks = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
parks.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=3)
|
||||
parks.normalize(0.0, 1.0)
|
||||
|
||||
# Step 4: Render base (roads)
|
||||
color_layer.fill(mcrfpy.Color(60, 60, 65)) # Asphalt
|
||||
|
||||
# Step 5: Draw buildings based on BSP and district type
|
||||
for leaf in bsp.leaves():
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
cx, cy = leaf.center()
|
||||
|
||||
# Get district type at center
|
||||
district_val = districts.get((cx, cy))
|
||||
|
||||
# Shrink for roads between buildings
|
||||
shrink = 1
|
||||
|
||||
# Determine building style based on district
|
||||
if district_val < 0.2:
|
||||
# Residential
|
||||
building_color = mcrfpy.Color(140, 160, 140)
|
||||
roof_color = mcrfpy.Color(160, 100, 80)
|
||||
shrink = 2 # More space between houses
|
||||
elif district_val < 0.4:
|
||||
# Commercial
|
||||
building_color = mcrfpy.Color(120, 140, 170)
|
||||
roof_color = mcrfpy.Color(80, 100, 130)
|
||||
elif district_val < 0.6:
|
||||
# Industrial
|
||||
building_color = mcrfpy.Color(100, 100, 105)
|
||||
roof_color = mcrfpy.Color(70, 70, 75)
|
||||
elif district_val < 0.8:
|
||||
# Park area - check noise for actual park placement
|
||||
park_val = parks.get((cx, cy))
|
||||
if park_val > 0.4:
|
||||
# This block is a park
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
t = parks.get((x, y))
|
||||
if t > 0.6:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(50, 120, 50)) # Trees
|
||||
else:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(80, 150, 80)) # Grass
|
||||
continue
|
||||
else:
|
||||
building_color = mcrfpy.Color(130, 150, 130)
|
||||
roof_color = mcrfpy.Color(100, 80, 70)
|
||||
else:
|
||||
# Downtown
|
||||
building_color = mcrfpy.Color(150, 155, 165)
|
||||
roof_color = mcrfpy.Color(90, 95, 110)
|
||||
shrink = 1 # Dense buildings
|
||||
|
||||
# Draw building
|
||||
for y in range(pos[1] + shrink, pos[1] + size[1] - shrink):
|
||||
for x in range(pos[0] + shrink, pos[0] + size[0] - shrink):
|
||||
# Building edge (roof)
|
||||
if y == pos[1] + shrink or y == pos[1] + size[1] - shrink - 1:
|
||||
color_layer.set(((x, y)), roof_color)
|
||||
elif x == pos[0] + shrink or x == pos[0] + size[0] - shrink - 1:
|
||||
color_layer.set(((x, y)), roof_color)
|
||||
else:
|
||||
color_layer.set(((x, y)), building_color)
|
||||
|
||||
# Step 6: Add main roads (cross the city)
|
||||
road_color = mcrfpy.Color(70, 70, 75)
|
||||
marking_color = mcrfpy.Color(200, 200, 100)
|
||||
|
||||
# Horizontal main road
|
||||
main_y = GRID_HEIGHT // 2
|
||||
for x in range(GRID_WIDTH):
|
||||
for dy in range(-1, 2):
|
||||
if 0 <= main_y + dy < GRID_HEIGHT:
|
||||
color_layer.set(((x, main_y + dy)), road_color)
|
||||
# Road markings
|
||||
if x % 4 == 0:
|
||||
color_layer.set(((x, main_y)), marking_color)
|
||||
|
||||
# Vertical main road
|
||||
main_x = GRID_WIDTH // 2
|
||||
for y in range(GRID_HEIGHT):
|
||||
for dx in range(-1, 2):
|
||||
if 0 <= main_x + dx < GRID_WIDTH:
|
||||
color_layer.set(((main_x + dx, y)), road_color)
|
||||
if y % 4 == 0:
|
||||
color_layer.set(((main_x, y)), marking_color)
|
||||
|
||||
# Intersection
|
||||
for dy in range(-1, 2):
|
||||
for dx in range(-1, 2):
|
||||
color_layer.set(((main_x + dx, main_y + dy)), road_color)
|
||||
|
||||
# Step 7: Add a central plaza
|
||||
plaza_x, plaza_y = main_x, main_y
|
||||
for dy in range(-3, 4):
|
||||
for dx in range(-4, 5):
|
||||
nx, ny = plaza_x + dx, plaza_y + dy
|
||||
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
|
||||
if abs(dx) <= 1 and abs(dy) <= 1:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(180, 160, 140)) # Fountain
|
||||
else:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(160, 150, 140)) # Plaza tiles
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Procedural City: BSP + Voronoi Districts + Noise Parks", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("city")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_32_advanced_city.png")
|
||||
print("Screenshot saved: procgen_32_advanced_city.png")
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
"""Advanced: Natural Cave System
|
||||
|
||||
Combines: Noise (cave formation) + Threshold (open areas) + Kernel (smoothing) + BSP (structured areas)
|
||||
Creates organic cave networks with some structured rooms.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Generate cave base using turbulent noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
cave_noise = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
cave_noise.add_noise(noise, world_size=(10, 8), mode='turbulence', octaves=4)
|
||||
cave_noise.normalize(0.0, 1.0)
|
||||
|
||||
# Step 2: Create cave mask via threshold
|
||||
# Values > 0.45 become open cave, rest is rock
|
||||
cave_mask = cave_noise.threshold_binary((0.4, 1.0), 1.0)
|
||||
|
||||
# Step 3: Apply smoothing kernel to remove isolated pixels
|
||||
smooth_kernel = {
|
||||
(-1, -1): 1, (0, -1): 2, (1, -1): 1,
|
||||
(-1, 0): 2, (0, 0): 4, (1, 0): 2,
|
||||
(-1, 1): 1, (0, 1): 2, (1, 1): 1,
|
||||
}
|
||||
cave_mask.kernel_transform(smooth_kernel)
|
||||
cave_mask.normalize(0.0, 1.0)
|
||||
|
||||
# Re-threshold after smoothing
|
||||
cave_mask = cave_mask.threshold_binary((0.5, 1.0), 1.0)
|
||||
|
||||
# Step 4: Add some structured rooms using BSP in one corner
|
||||
# This represents ancient ruins within the caves
|
||||
bsp = mcrfpy.BSP(pos=(GRID_WIDTH - 22, GRID_HEIGHT - 18), size=(18, 14))
|
||||
bsp.split_recursive(depth=2, min_size=(6, 5), seed=42)
|
||||
|
||||
ruins_hmap = bsp.to_heightmap(
|
||||
size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
select='leaves',
|
||||
shrink=1,
|
||||
value=1.0
|
||||
)
|
||||
|
||||
# Step 5: Combine caves and ruins
|
||||
combined = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
combined.copy_from(cave_mask)
|
||||
combined.max(ruins_hmap)
|
||||
|
||||
# Step 6: Add connecting tunnels from ruins to main cave
|
||||
# Find a cave entrance point
|
||||
tunnel_points = []
|
||||
for y in range(GRID_HEIGHT - 18, GRID_HEIGHT - 10):
|
||||
for x in range(GRID_WIDTH - 25, GRID_WIDTH - 20):
|
||||
if cave_mask.get((x, y)) > 0.5:
|
||||
tunnel_points.append((x, y))
|
||||
break
|
||||
if tunnel_points:
|
||||
break
|
||||
|
||||
if tunnel_points:
|
||||
tx, ty = tunnel_points[0]
|
||||
# Carve tunnel to ruins entrance
|
||||
combined.dig_bezier(
|
||||
points=((tx, ty), (tx + 3, ty), (GRID_WIDTH - 22, ty + 2), (GRID_WIDTH - 20, GRID_HEIGHT - 15)),
|
||||
start_radius=1.5, end_radius=1.5,
|
||||
start_height=1.0, end_height=1.0
|
||||
)
|
||||
|
||||
# Step 7: Add large cavern (central chamber)
|
||||
combined.add_hill((GRID_WIDTH // 3, GRID_HEIGHT // 2), 8, 0.6)
|
||||
|
||||
# Step 8: Create water pools in low noise areas
|
||||
water_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=99)
|
||||
water_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
water_map.add_noise(water_noise, world_size=(15, 12), mode='fbm', octaves=3)
|
||||
water_map.normalize(0.0, 1.0)
|
||||
|
||||
# Step 9: Render
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
cave_val = combined.get((x, y))
|
||||
water_val = water_map.get((x, y))
|
||||
original_noise = cave_noise.get((x, y))
|
||||
|
||||
# Check if in ruins area
|
||||
in_ruins = (x >= GRID_WIDTH - 22 and x < GRID_WIDTH - 4 and
|
||||
y >= GRID_HEIGHT - 18 and y < GRID_HEIGHT - 4)
|
||||
|
||||
if cave_val < 0.3:
|
||||
# Solid rock
|
||||
base = 30 + int(original_noise * 25)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
|
||||
elif cave_val < 0.5:
|
||||
# Cave wall edge
|
||||
color_layer.set(((x, y)), mcrfpy.Color(45, 40, 50))
|
||||
else:
|
||||
# Open cave floor
|
||||
if water_val > 0.7 and not in_ruins:
|
||||
# Water pool
|
||||
t = (water_val - 0.7) / 0.3
|
||||
color_layer.set(((x, y)), mcrfpy.Color(
|
||||
int(30 + t * 20), int(50 + t * 30), int(100 + t * 50)
|
||||
))
|
||||
elif in_ruins and ruins_hmap.get((x, y)) > 0.5:
|
||||
# Ruins floor (worked stone)
|
||||
base = 85 + ((x + y) % 3) * 5
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base))
|
||||
else:
|
||||
# Natural cave floor
|
||||
base = 55 + int(original_noise * 20)
|
||||
var = ((x * 3 + y * 5) % 8)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + var, base - 5 + var, base - 8))
|
||||
|
||||
# Glowing fungi spots
|
||||
fungi_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=777)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if combined.get((x, y)) > 0.5: # Only in open areas
|
||||
fungi_val = fungi_noise.get((x * 0.5, y * 0.5))
|
||||
if fungi_val > 0.8:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(80, 180, 120))
|
||||
|
||||
# Border
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, 0)), mcrfpy.Color(20, 18, 25))
|
||||
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(20, 18, 25))
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((0, y)), mcrfpy.Color(20, 18, 25))
|
||||
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(20, 18, 25))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Cave System: Noise + Threshold + Kernel + BSP Ruins", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("caves")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_33_advanced_caves.png")
|
||||
print("Screenshot saved: procgen_33_advanced_caves.png")
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
"""Advanced: Volcanic Crater Region
|
||||
|
||||
Combines: Hills (mountains) + dig_hill (craters) + Voronoi (lava flows) + Erosion + Noise
|
||||
Creates a volcanic landscape with active lava, ash fields, and rocky terrain.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def volcanic_color(elevation, lava_intensity, ash_level):
|
||||
"""Color based on elevation, lava presence, and ash coverage."""
|
||||
# Lava overrides everything
|
||||
if lava_intensity > 0.6:
|
||||
t = (lava_intensity - 0.6) / 0.4
|
||||
return mcrfpy.Color(
|
||||
int(200 + t * 55),
|
||||
int(80 + t * 80),
|
||||
int(20 + t * 30)
|
||||
)
|
||||
elif lava_intensity > 0.4:
|
||||
# Cooling lava
|
||||
t = (lava_intensity - 0.4) / 0.2
|
||||
return mcrfpy.Color(
|
||||
int(80 + t * 120),
|
||||
int(30 + t * 50),
|
||||
int(20)
|
||||
)
|
||||
|
||||
# Check for crater interior (very low elevation)
|
||||
if elevation < 0.15:
|
||||
t = elevation / 0.15
|
||||
return mcrfpy.Color(int(40 + t * 30), int(20 + t * 20), int(10 + t * 15))
|
||||
|
||||
# Ash coverage
|
||||
if ash_level > 0.6:
|
||||
t = (ash_level - 0.6) / 0.4
|
||||
base = int(60 + t * 40)
|
||||
return mcrfpy.Color(base, base - 5, base - 10)
|
||||
|
||||
# Normal terrain by elevation
|
||||
if elevation < 0.3:
|
||||
# Volcanic plain
|
||||
t = (elevation - 0.15) / 0.15
|
||||
return mcrfpy.Color(int(50 + t * 30), int(40 + t * 25), int(35 + t * 20))
|
||||
elif elevation < 0.5:
|
||||
# Rocky slopes
|
||||
t = (elevation - 0.3) / 0.2
|
||||
return mcrfpy.Color(int(70 + t * 20), int(60 + t * 15), int(50 + t * 15))
|
||||
elif elevation < 0.7:
|
||||
# Mountain sides
|
||||
t = (elevation - 0.5) / 0.2
|
||||
return mcrfpy.Color(int(85 + t * 25), int(75 + t * 20), int(65 + t * 20))
|
||||
elif elevation < 0.85:
|
||||
# High slopes
|
||||
t = (elevation - 0.7) / 0.15
|
||||
return mcrfpy.Color(int(100 + t * 30), int(90 + t * 25), int(80 + t * 25))
|
||||
else:
|
||||
# Peaks
|
||||
t = (elevation - 0.85) / 0.15
|
||||
return mcrfpy.Color(int(130 + t * 50), int(120 + t * 50), int(115 + t * 50))
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Create base terrain with noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
terrain = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.3)
|
||||
terrain.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=4, scale=0.2)
|
||||
|
||||
# Step 2: Add volcanic mountains
|
||||
# Main volcano
|
||||
terrain.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 20, 0.7)
|
||||
terrain.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 12, 0.3) # Steep peak
|
||||
|
||||
# Secondary volcanoes
|
||||
terrain.add_hill((15, 15), 10, 0.4)
|
||||
terrain.add_hill((GRID_WIDTH - 12, GRID_HEIGHT - 15), 8, 0.35)
|
||||
terrain.add_hill((10, GRID_HEIGHT - 10), 6, 0.25)
|
||||
|
||||
# Step 3: Create craters
|
||||
terrain.dig_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 6, 0.1) # Main crater
|
||||
terrain.dig_hill((15, 15), 4, 0.15) # Secondary crater
|
||||
terrain.dig_hill((GRID_WIDTH - 12, GRID_HEIGHT - 15), 3, 0.18) # Third crater
|
||||
|
||||
# Step 4: Create lava flow pattern using voronoi
|
||||
lava = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
lava.add_voronoi(num_points=12, coefficients=(1.0, -0.8), seed=77)
|
||||
lava.normalize(0.0, 1.0)
|
||||
|
||||
# Lava originates from craters - enhance around crater centers
|
||||
lava.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 8, 0.5)
|
||||
lava.add_hill((15, 15), 5, 0.3)
|
||||
|
||||
# Lava flows downhill - multiply by inverted terrain
|
||||
terrain_inv = terrain.inverse()
|
||||
terrain_inv.normalize(0.0, 1.0)
|
||||
lava.multiply(terrain_inv)
|
||||
lava.normalize(0.0, 1.0)
|
||||
|
||||
# Step 5: Create ash distribution using noise
|
||||
ash_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=123)
|
||||
ash = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
ash.add_noise(ash_noise, world_size=(8, 6), mode='turbulence', octaves=3)
|
||||
ash.normalize(0.0, 1.0)
|
||||
|
||||
# Ash settles on lower areas
|
||||
ash.multiply(terrain_inv)
|
||||
|
||||
# Step 6: Apply erosion for realistic channels
|
||||
terrain.rain_erosion(drops=1500, erosion=0.1, sedimentation=0.03, seed=42)
|
||||
terrain.normalize(0.0, 1.0)
|
||||
|
||||
# Step 7: Add lava rivers from craters
|
||||
lava.dig_bezier(
|
||||
points=((GRID_WIDTH // 2, GRID_HEIGHT // 2 + 5),
|
||||
(GRID_WIDTH // 2 - 5, GRID_HEIGHT // 2 + 15),
|
||||
(GRID_WIDTH // 3, GRID_HEIGHT - 10),
|
||||
(10, GRID_HEIGHT - 5)),
|
||||
start_radius=2, end_radius=3,
|
||||
start_height=0.9, end_height=0.7
|
||||
)
|
||||
|
||||
lava.dig_bezier(
|
||||
points=((GRID_WIDTH // 2 + 3, GRID_HEIGHT // 2 + 3),
|
||||
(GRID_WIDTH // 2 + 15, GRID_HEIGHT // 2 + 8),
|
||||
(GRID_WIDTH - 15, GRID_HEIGHT // 2 + 5),
|
||||
(GRID_WIDTH - 5, GRID_HEIGHT // 2 + 10)),
|
||||
start_radius=1.5, end_radius=2.5,
|
||||
start_height=0.85, end_height=0.65
|
||||
)
|
||||
|
||||
# Step 8: Render
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
elev = terrain.get((x, y))
|
||||
lava_val = lava.get((x, y))
|
||||
ash_val = ash.get((x, y))
|
||||
|
||||
color_layer.set(((x, y)), volcanic_color(elev, lava_val, ash_val))
|
||||
|
||||
# Add smoke/steam particles around crater rims
|
||||
crater_centers = [
|
||||
(GRID_WIDTH // 2, GRID_HEIGHT // 2, 6),
|
||||
(15, 15, 4),
|
||||
(GRID_WIDTH - 12, GRID_HEIGHT - 15, 3)
|
||||
]
|
||||
|
||||
import math
|
||||
for cx, cy, radius in crater_centers:
|
||||
for angle in range(0, 360, 30):
|
||||
rad = math.radians(angle)
|
||||
px = int(cx + math.cos(rad) * radius)
|
||||
py = int(cy + math.sin(rad) * radius)
|
||||
if 0 <= px < GRID_WIDTH and 0 <= py < GRID_HEIGHT:
|
||||
# Smoke color
|
||||
color_layer.set(((px, py)), mcrfpy.Color(150, 140, 130, 180))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Volcanic Region: Hills + Craters + Voronoi Lava + Erosion", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("volcanic")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_34_advanced_volcanic.png")
|
||||
print("Screenshot saved: procgen_34_advanced_volcanic.png")
|
||||
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 55 KiB |