Compare commits
5 commits
master
...
grid_entit
| Author | SHA1 | Date | |
|---|---|---|---|
| d93311fea8 | |||
| b920a51736 | |||
| c975599251 | |||
| 1d852f875b | |||
| 3b86089128 |
28
.gitignore
vendored
|
|
@ -8,31 +8,5 @@ PCbuild
|
|||
obj
|
||||
build
|
||||
lib
|
||||
__pycache__
|
||||
obj
|
||||
|
||||
.cache/
|
||||
7DRL2025 Release/
|
||||
CMakeFiles/
|
||||
Makefile
|
||||
*.zip
|
||||
__lib/
|
||||
__lib_windows/
|
||||
build-windows/
|
||||
build_windows/
|
||||
_oldscripts/
|
||||
assets/
|
||||
cellular_automata_fire/
|
||||
deps/
|
||||
fetch_issues_txt.py
|
||||
forest_fire_CA.py
|
||||
mcrogueface.github.io
|
||||
scripts/
|
||||
tcod_reference
|
||||
.archive
|
||||
.mcp.json
|
||||
dist/
|
||||
|
||||
# Keep important documentation and tests
|
||||
!CLAUDE.md
|
||||
!README.md
|
||||
!tests/
|
||||
|
|
|
|||
7
.gitmodules
vendored
|
|
@ -10,7 +10,6 @@
|
|||
[submodule "modules/SFML"]
|
||||
path = modules/SFML
|
||||
url = git@github.com:SFML/SFML.git
|
||||
[submodule "modules/libtcod-headless"]
|
||||
path = modules/libtcod-headless
|
||||
url = git@github.com:jmccardle/libtcod-headless.git
|
||||
branch = 2.2.1-headless
|
||||
[submodule "modules/libtcod"]
|
||||
path = modules/libtcod
|
||||
url = git@github.com:libtcod/libtcod.git
|
||||
|
|
|
|||
|
|
@ -1,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/`.
|
||||
847
CLAUDE.md
|
|
@ -1,847 +0,0 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Gitea-First Workflow
|
||||
|
||||
**IMPORTANT**: This project uses Gitea for issue tracking, documentation, and project management. Always consult and update Gitea resources before and during development work.
|
||||
|
||||
**Gitea Instance**: https://gamedev.ffwf.net/gitea/john/McRogueFace
|
||||
|
||||
### Core Principles
|
||||
|
||||
1. **Gitea is the Single Source of Truth**
|
||||
- Issue tracker contains current tasks, bugs, and feature requests
|
||||
- Wiki contains living documentation and architecture decisions
|
||||
- Use Gitea MCP tools to query and update issues programmatically
|
||||
|
||||
2. **Always Check Gitea First**
|
||||
- Before starting work: Check open issues for related tasks or blockers
|
||||
- Before implementing: Read relevant wiki pages per the [Development Workflow](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Development-Workflow) consultation table
|
||||
- When using `/roadmap` command: Query Gitea for up-to-date issue status
|
||||
- When researching a feature: Search Gitea wiki and issues before grepping codebase
|
||||
- When encountering a bug: Check if an issue already exists
|
||||
|
||||
3. **Create Granular Issues**
|
||||
- Break large features into separate, focused issues
|
||||
- Each issue should address one specific problem or enhancement
|
||||
- Tag issues appropriately: `[Bugfix]`, `[Major Feature]`, `[Minor Feature]`, etc.
|
||||
- Link related issues using dependencies or blocking relationships
|
||||
|
||||
4. **Document as You Go**
|
||||
- When work on one issue interacts with another system: Add notes to related issues
|
||||
- When discovering undocumented behavior: Note it for wiki update
|
||||
- When documentation misleads you: Note it for wiki correction
|
||||
- After committing code changes: Update relevant wiki pages (with user permission)
|
||||
- Follow the [Development Workflow](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Development-Workflow) for wiki update procedures
|
||||
|
||||
5. **Cross-Reference Everything**
|
||||
- Commit messages should reference issue numbers (e.g., "Fixes #104", "Addresses #125")
|
||||
- Issue comments should link to commits when work is done
|
||||
- Wiki pages should reference relevant issues for implementation details
|
||||
- Issues should link to each other when dependencies exist
|
||||
|
||||
### Workflow Pattern
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 1. Check Gitea Issues & Wiki │
|
||||
│ - Is there an existing issue for this? │
|
||||
│ - What's the current status? │
|
||||
│ - Are there related issues or blockers? │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 2. Create Issues (if needed) │
|
||||
│ - Break work into granular tasks │
|
||||
│ - Tag appropriately │
|
||||
│ - Link dependencies │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 3. Do the Work │
|
||||
│ - Implement/fix/document │
|
||||
│ - Write tests first (TDD) │
|
||||
│ - Add inline documentation │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 4. Update Gitea │
|
||||
│ - Add notes to affected issues │
|
||||
│ - Create follow-up issues for discovered work │
|
||||
│ - Update wiki if architecture/APIs changed │
|
||||
│ - Add documentation correction tasks │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 5. Commit & Reference │
|
||||
│ - Commit messages reference issue numbers │
|
||||
│ - Close issues or update status │
|
||||
│ - Add commit links to issue comments │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Benefits of Gitea-First Approach
|
||||
|
||||
- **Reduced Context Switching**: Check brief issue descriptions instead of re-reading entire codebase
|
||||
- **Better Planning**: Issues provide roadmap; avoid duplicate or contradictory work
|
||||
- **Living Documentation**: Wiki and issues stay current as work progresses
|
||||
- **Historical Context**: Issue comments capture why decisions were made
|
||||
- **Efficiency**: MCP tools allow programmatic access to project state
|
||||
|
||||
### MCP Tools Available
|
||||
|
||||
Claude Code has access to Gitea MCP tools for:
|
||||
- `list_repo_issues` - Query current issues with filtering
|
||||
- `get_issue` - Get detailed issue information
|
||||
- `create_issue` - Create new issues programmatically
|
||||
- `create_issue_comment` - Add comments to issues
|
||||
- `edit_issue` - Update issue status, title, body
|
||||
- `add_issue_labels` - Tag issues appropriately
|
||||
- `add_issue_dependency` / `add_issue_blocking` - Link related issues
|
||||
- Plus wiki, milestone, and label management tools
|
||||
|
||||
Use these tools liberally to keep the project organized!
|
||||
|
||||
### Gitea Label System
|
||||
|
||||
**IMPORTANT**: Always apply appropriate labels when creating new issues!
|
||||
|
||||
The project uses a structured label system to organize issues:
|
||||
|
||||
**Label Categories:**
|
||||
|
||||
1. **System Labels** (identify affected codebase area):
|
||||
- `system:rendering` - Rendering pipeline and visuals
|
||||
- `system:ui-hierarchy` - UI component hierarchy and composition
|
||||
- `system:grid` - Grid system and spatial containers
|
||||
- `system:animation` - Animation and property interpolation
|
||||
- `system:python-binding` - Python/C++ binding layer
|
||||
- `system:input` - Input handling and events
|
||||
- `system:performance` - Performance optimization and profiling
|
||||
- `system:documentation` - Documentation infrastructure
|
||||
|
||||
2. **Priority Labels** (development timeline):
|
||||
- `priority:tier1-active` - Current development focus - critical path to v1.0
|
||||
- `priority:tier2-foundation` - Important foundation work - not blocking v1.0
|
||||
- `priority:tier3-future` - Future features - deferred until after v1.0
|
||||
|
||||
3. **Type/Scope Labels** (effort and complexity):
|
||||
- `Major Feature` - Significant time and effort required
|
||||
- `Minor Feature` - Some effort required to create or overhaul functionality
|
||||
- `Tiny Feature` - Quick and easy - a few lines or little interconnection
|
||||
- `Bugfix` - Fixes incorrect behavior
|
||||
- `Refactoring & Cleanup` - No new functionality, just improving codebase
|
||||
- `Documentation` - Documentation work
|
||||
- `Demo Target` - Functionality to demonstrate
|
||||
|
||||
4. **Workflow Labels** (current blockers/needs):
|
||||
- `workflow:blocked` - Blocked by other work - waiting on dependencies
|
||||
- `workflow:needs-documentation` - Needs documentation before or after implementation
|
||||
- `workflow:needs-benchmark` - Needs performance testing and benchmarks
|
||||
- `Alpha Release Requirement` - Blocker to 0.1 Alpha release
|
||||
|
||||
**When creating issues:**
|
||||
- Apply at least one `system:*` label (what part of codebase)
|
||||
- Apply one `priority:tier*` label (when to address it)
|
||||
- Apply one type label (`Major Feature`, `Minor Feature`, `Tiny Feature`, or `Bugfix`)
|
||||
- Apply `workflow:*` labels if applicable (blocked, needs docs, needs benchmarks)
|
||||
|
||||
**Example label combinations:**
|
||||
- New rendering feature: `system:rendering`, `priority:tier2-foundation`, `Major Feature`
|
||||
- Python API improvement: `system:python-binding`, `priority:tier1-active`, `Minor Feature`
|
||||
- Performance work: `system:performance`, `priority:tier1-active`, `Major Feature`, `workflow:needs-benchmark`
|
||||
|
||||
**⚠️ CRITICAL BUG**: The Gitea MCP tool (v0.07) has a label application bug documented in `GITEA_MCP_LABEL_BUG_REPORT.md`:
|
||||
- `add_issue_labels` and `replace_issue_labels` behave inconsistently
|
||||
- Single ID arrays produce different results than multi-ID arrays for the SAME IDs
|
||||
- Label IDs do not map reliably to actual labels
|
||||
|
||||
**Workaround Options:**
|
||||
1. **Best**: Apply labels manually via web interface: `https://gamedev.ffwf.net/gitea/john/McRogueFace/issues/<number>`
|
||||
2. **Automated**: Apply labels ONE AT A TIME using single-element arrays (slower but more reliable)
|
||||
3. **Use single-ID mapping** (documented below)
|
||||
|
||||
**Label ID Reference** (for documentation purposes - see issue #131 for details):
|
||||
```
|
||||
1=Major Feature, 2=Alpha Release, 3=Bugfix, 4=Demo Target, 5=Documentation,
|
||||
6=Minor Feature, 7=tier1-active, 8=tier2-foundation, 9=tier3-future,
|
||||
10=Refactoring, 11=animation, 12=docs, 13=grid, 14=input, 15=performance,
|
||||
16=python-binding, 17=rendering, 18=ui-hierarchy, 19=Tiny Feature,
|
||||
20=blocked, 21=needs-benchmark, 22=needs-documentation
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
McRogueFace uses a unified Makefile for both Linux native builds and Windows cross-compilation.
|
||||
|
||||
**IMPORTANT**: All `make` commands must be run from the **project root directory** (`/home/john/Development/McRogueFace/`), not from `build/` or any subdirectory.
|
||||
|
||||
### Quick Reference
|
||||
|
||||
```bash
|
||||
# Linux builds
|
||||
make # Build for Linux (default target)
|
||||
make linux # Same as above
|
||||
make run # Build and run
|
||||
make clean # Remove Linux build artifacts
|
||||
|
||||
# Windows cross-compilation (requires MinGW-w64)
|
||||
make windows # Release build for Windows
|
||||
make windows-debug # Debug build with console output
|
||||
make clean-windows # Remove Windows build artifacts
|
||||
|
||||
# Distribution packages
|
||||
make package-linux-light # Linux with minimal stdlib (~25 MB)
|
||||
make package-linux-full # Linux with full stdlib (~26 MB)
|
||||
make package-windows-light # Windows with minimal stdlib
|
||||
make package-windows-full # Windows with full stdlib
|
||||
make package-all # All platform/preset combinations
|
||||
|
||||
# Cleanup
|
||||
make clean-all # Remove all builds and packages
|
||||
make clean-dist # Remove only distribution packages
|
||||
```
|
||||
|
||||
### Build Outputs
|
||||
|
||||
| Command | Output Directory | Executable |
|
||||
|---------|------------------|------------|
|
||||
| `make` / `make linux` | `build/` | `build/mcrogueface` |
|
||||
| `make windows` | `build-windows/` | `build-windows/mcrogueface.exe` |
|
||||
| `make windows-debug` | `build-windows-debug/` | `build-windows-debug/mcrogueface.exe` |
|
||||
| `make package-*` | `dist/` | `.tar.gz` or `.zip` archives |
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**Linux build:**
|
||||
- CMake 3.14+
|
||||
- GCC/G++ with C++17 support
|
||||
- SFML 2.6 development libraries
|
||||
- Libraries in `__lib/` directory (libpython3.14, libtcod, etc.)
|
||||
|
||||
**Windows cross-compilation:**
|
||||
- MinGW-w64 (`x86_64-w64-mingw32-g++-posix`)
|
||||
- Libraries in `__lib_windows/` directory
|
||||
- Toolchain file: `cmake/toolchains/mingw-w64-x86_64.cmake`
|
||||
|
||||
### Library Dependencies
|
||||
|
||||
The build expects pre-built libraries in:
|
||||
- `__lib/` - Linux shared libraries (libpython3.14.so, libsfml-*.so, libtcod.so)
|
||||
- `__lib/Python/Lib/` - Python standard library source
|
||||
- `__lib/Python/lib.linux-x86_64-3.14/` - Python extension modules (.so)
|
||||
- `__lib_windows/` - Windows DLLs and libraries
|
||||
|
||||
### Manual CMake Build
|
||||
|
||||
If you need more control over the build:
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# Windows cross-compile
|
||||
mkdir build-windows && cd build-windows
|
||||
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# Windows debug with console
|
||||
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_WINDOWS_CONSOLE=ON
|
||||
```
|
||||
|
||||
### Distribution Packaging
|
||||
|
||||
The packaging system creates self-contained archives with:
|
||||
- Executable
|
||||
- Required shared libraries
|
||||
- Assets (sprites, fonts, audio)
|
||||
- Python scripts
|
||||
- Filtered Python stdlib (light or full variant)
|
||||
|
||||
**Light variant** (~25 MB): Core + gamedev + utility modules only
|
||||
**Full variant** (~26 MB): Includes networking, async, debugging modules
|
||||
|
||||
Packaging tools:
|
||||
- `tools/package.sh` - Main packaging orchestrator
|
||||
- `tools/package_stdlib.py` - Creates filtered stdlib archives
|
||||
- `tools/stdlib_modules.yaml` - Module categorization config
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**"No rule to make target 'linux'"**: You're in the wrong directory. Run `make` from project root.
|
||||
|
||||
**Library linking errors**: Ensure `__lib/` contains all required .so files. Check `CMakeLists.txt` for `link_directories(${CMAKE_SOURCE_DIR}/__lib)`.
|
||||
|
||||
**Windows build fails**: Verify MinGW-w64 is installed with posix thread model: `x86_64-w64-mingw32-g++-posix --version`
|
||||
|
||||
### Legacy Build Scripts
|
||||
|
||||
The following are deprecated but kept for reference:
|
||||
- `build.sh` - Original Linux build script (use `make` instead)
|
||||
- `GNUmakefile.legacy` - Old wrapper makefile (renamed to avoid conflicts)
|
||||
|
||||
## Project Architecture
|
||||
|
||||
McRogueFace is a C++ game engine with Python scripting support, designed for creating roguelike games. The architecture consists of:
|
||||
|
||||
### Core Engine (C++)
|
||||
- **Entry Point**: `src/main.cpp` initializes the game engine
|
||||
- **Scene System**: `Scene.h/cpp` manages game states
|
||||
- **Entity System**: `UIEntity.h/cpp` provides game objects
|
||||
- **Python Integration**: `McRFPy_API.h/cpp` exposes engine functionality to Python
|
||||
- **UI Components**: `UIFrame`, `UICaption`, `UISprite`, `UIGrid` for rendering
|
||||
|
||||
### Game Logic (Python)
|
||||
- **Main Script**: `src/scripts/game.py` contains game initialization and scene setup
|
||||
- **Entity System**: `src/scripts/cos_entities.py` implements game entities (Player, Enemy, Boulder, etc.)
|
||||
- **Level Generation**: `src/scripts/cos_level.py` uses BSP for procedural dungeon generation
|
||||
- **Tile System**: `src/scripts/cos_tiles.py` implements Wave Function Collapse for tile placement
|
||||
|
||||
### Key Python API (`mcrfpy` module)
|
||||
The C++ engine exposes these primary functions to Python:
|
||||
- Scene Management: `Scene("name")` object
|
||||
- Entity Creation: `Entity()` with position and sprite properties
|
||||
- Grid Management: `Grid()` for tilemap rendering
|
||||
- Input Handling: `keypressScene()` for keyboard events
|
||||
- Audio: `createSoundBuffer()`, `playSound()`, `setVolume()`
|
||||
- Timers: `Timer("name")` object for event scheduling
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Running the Game
|
||||
After building, the executable expects:
|
||||
- `assets/` directory with sprites, fonts, and audio
|
||||
- `scripts/` directory with Python game files
|
||||
- Python 3.12 shared libraries in `./lib/`
|
||||
|
||||
### Modifying Game Logic
|
||||
- Game scripts are in `src/scripts/`
|
||||
- Main game entry is `game.py`
|
||||
- Entity behavior in `cos_entities.py`
|
||||
- Level generation in `cos_level.py`
|
||||
|
||||
### Adding New Features
|
||||
1. C++ API additions go in `src/McRFPy_API.cpp`
|
||||
2. Expose to Python using the existing binding pattern
|
||||
3. Update Python scripts to use new functionality
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Suite Structure
|
||||
|
||||
The `tests/` directory contains the comprehensive test suite:
|
||||
|
||||
```
|
||||
tests/
|
||||
├── run_tests.py # Test runner - executes all tests with timeout
|
||||
├── unit/ # Unit tests for individual components (105+ tests)
|
||||
├── integration/ # Integration tests for system interactions
|
||||
├── regression/ # Bug regression tests (issue_XX_*.py)
|
||||
├── benchmarks/ # Performance benchmarks
|
||||
├── demo/ # Feature demonstration system
|
||||
│ ├── demo_main.py # Interactive demo runner
|
||||
│ ├── screens/ # Per-feature demo screens
|
||||
│ └── screenshots/ # Generated demo screenshots
|
||||
└── notes/ # Analysis files and documentation
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run the full test suite (from tests/ directory)
|
||||
cd tests && python3 run_tests.py
|
||||
|
||||
# Run a specific test
|
||||
cd build && ./mcrogueface --headless --exec ../tests/unit/some_test.py
|
||||
|
||||
# Run the demo system interactively
|
||||
cd build && ./mcrogueface ../tests/demo/demo_main.py
|
||||
|
||||
# Generate demo screenshots (headless)
|
||||
cd build && ./mcrogueface --headless --exec ../tests/demo/demo_main.py
|
||||
```
|
||||
|
||||
### Reading Tests as Examples
|
||||
|
||||
**IMPORTANT**: Before implementing a feature or fixing a bug, check existing tests for API usage examples:
|
||||
|
||||
- `tests/unit/` - Shows correct usage of individual mcrfpy classes and functions
|
||||
- `tests/demo/screens/` - Complete working examples of UI components
|
||||
- `tests/regression/` - Documents edge cases and bug scenarios
|
||||
|
||||
Example: To understand Animation API:
|
||||
```bash
|
||||
grep -r "Animation" tests/unit/
|
||||
cat tests/demo/screens/animation_demo.py
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
**Always write tests when adding features or fixing bugs:**
|
||||
|
||||
1. **For new features**: Create `tests/unit/feature_name_test.py`
|
||||
2. **For bug fixes**: Create `tests/regression/issue_XX_description_test.py`
|
||||
3. **For demos**: Add to `tests/demo/screens/` if it showcases a feature
|
||||
|
||||
### Quick Testing Commands
|
||||
```bash
|
||||
# Test headless mode with inline Python
|
||||
cd build
|
||||
./mcrogueface --headless -c "import mcrfpy; print('Headless test')"
|
||||
|
||||
# Run specific test with output
|
||||
./mcrogueface --headless --exec ../tests/unit/my_test.py 2>&1
|
||||
```
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Compiling McRogueFace
|
||||
|
||||
See the [Build System](#build-system) section above for comprehensive build instructions.
|
||||
|
||||
```bash
|
||||
# Quick reference (run from project root!)
|
||||
make # Linux build
|
||||
make windows # Windows cross-compile
|
||||
make clean && make # Full rebuild
|
||||
```
|
||||
|
||||
### Running and Capturing Output
|
||||
```bash
|
||||
# Run with timeout and capture output
|
||||
cd build
|
||||
timeout 5 ./mcrogueface 2>&1 | tee output.log
|
||||
|
||||
# Run in background and kill after delay
|
||||
./mcrogueface > output.txt 2>&1 & PID=$!; sleep 3; kill $PID 2>/dev/null
|
||||
|
||||
# Just capture first N lines (useful for crashes)
|
||||
./mcrogueface 2>&1 | head -50
|
||||
```
|
||||
|
||||
### Debugging with GDB
|
||||
```bash
|
||||
# Interactive debugging
|
||||
gdb ./mcrogueface
|
||||
(gdb) run
|
||||
(gdb) bt # backtrace after crash
|
||||
|
||||
# Batch mode debugging (non-interactive)
|
||||
gdb -batch -ex run -ex where -ex quit ./mcrogueface 2>&1
|
||||
|
||||
# Get just the backtrace after a crash
|
||||
gdb -batch -ex "run" -ex "bt" ./mcrogueface 2>&1 | head -50
|
||||
|
||||
# Debug with specific commands
|
||||
echo -e "run\nbt 5\nquit\ny" | gdb ./mcrogueface 2>&1
|
||||
```
|
||||
|
||||
### Testing Different Python Scripts
|
||||
```bash
|
||||
# The game automatically runs build/scripts/game.py on startup
|
||||
# To test different behavior:
|
||||
|
||||
# Option 1: Replace game.py temporarily
|
||||
cd build
|
||||
cp scripts/my_test_script.py scripts/game.py
|
||||
./mcrogueface
|
||||
|
||||
# Option 2: Backup original and test
|
||||
mv scripts/game.py scripts/game.py.bak
|
||||
cp my_test.py scripts/game.py
|
||||
./mcrogueface
|
||||
mv scripts/game.py.bak scripts/game.py
|
||||
|
||||
# Option 3: For quick tests, create minimal game.py
|
||||
echo 'import mcrfpy; print("Test"); scene = mcrfpy.Scene("test"); scene.activate()' > scripts/game.py
|
||||
```
|
||||
|
||||
### Understanding Key Macros and Patterns
|
||||
|
||||
#### RET_PY_INSTANCE Macro (UIDrawable.h)
|
||||
This macro handles converting C++ UI objects to their Python equivalents:
|
||||
```cpp
|
||||
RET_PY_INSTANCE(target);
|
||||
// Expands to a switch on target->derived_type() that:
|
||||
// 1. Allocates the correct Python object type (Frame, Caption, Sprite, Grid)
|
||||
// 2. Sets the shared_ptr data member
|
||||
// 3. Returns the PyObject*
|
||||
```
|
||||
|
||||
#### Collection Patterns
|
||||
- `UICollection` wraps `std::vector<std::shared_ptr<UIDrawable>>`
|
||||
- `UIEntityCollection` wraps `std::list<std::shared_ptr<UIEntity>>`
|
||||
- Different containers require different iteration code (vector vs list)
|
||||
|
||||
#### Python Object Creation Patterns
|
||||
```cpp
|
||||
// Pattern 1: Using tp_alloc (most common)
|
||||
auto o = (PyUIFrameObject*)type->tp_alloc(type, 0);
|
||||
o->data = std::make_shared<UIFrame>();
|
||||
|
||||
// Pattern 2: Getting type from module
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
||||
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
||||
|
||||
// Pattern 3: Direct shared_ptr assignment
|
||||
iterObj->data = self->data; // Shares the C++ object
|
||||
```
|
||||
|
||||
### Working Directory Structure
|
||||
```
|
||||
build/
|
||||
├── mcrogueface # The executable
|
||||
├── scripts/
|
||||
│ └── game.py # Auto-loaded Python script
|
||||
├── assets/ # Copied from source during build
|
||||
└── lib/ # Python libraries (copied from __lib/)
|
||||
```
|
||||
|
||||
### Quick Iteration Tips
|
||||
- Keep a test script ready for quick experiments
|
||||
- Use `timeout` to auto-kill hanging processes
|
||||
- The game expects a window manager; use Xvfb for headless testing
|
||||
- Python errors go to stderr, game output to stdout
|
||||
- Segfaults usually mean Python type initialization issues
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The project uses SFML for graphics/audio and libtcod for roguelike utilities
|
||||
- Python scripts are loaded at runtime from the `scripts/` directory
|
||||
- Asset loading expects specific paths relative to the executable
|
||||
- The game was created for 7DRL 2025 as "Crypt of Sokoban"
|
||||
- Iterator implementations require careful handling of C++/Python boundaries
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Test-Driven Development
|
||||
- **Always write tests first**: Create tests in `./tests/` for all bugs and new features
|
||||
- **Practice TDD**: Write tests that fail to demonstrate the issue, then pass after the fix
|
||||
- **Read existing tests**: Check `tests/unit/` and `tests/demo/screens/` for API examples before writing code
|
||||
- **Close the loop**: Reproduce issue → change code → recompile → run test → verify
|
||||
|
||||
### Two Types of Tests
|
||||
|
||||
#### 1. Direct Execution Tests (No Game Loop)
|
||||
For tests that only need class initialization or direct code execution:
|
||||
```python
|
||||
# tests/unit/my_feature_test.py
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Test code - runs immediately
|
||||
frame = mcrfpy.Frame(pos=(0,0), size=(100,100))
|
||||
assert frame.x == 0
|
||||
assert frame.w == 100
|
||||
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
```
|
||||
|
||||
#### 2. Game Loop Tests (Timer-Based)
|
||||
For tests requiring rendering, screenshots, or elapsed time:
|
||||
```python
|
||||
# tests/unit/my_visual_test.py
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
def run_test(runtime):
|
||||
"""Timer callback - runs after game loop starts"""
|
||||
automation.screenshot("test_result.png")
|
||||
# Validate results...
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
|
||||
test_scene = mcrfpy.Scene("test")
|
||||
ui = test_scene.children
|
||||
ui.append(mcrfpy.Frame(pos=(50,50), size=(100,100)))
|
||||
mcrfpy.current_scene = test_scene
|
||||
timer = mcrfpy.Timer("test", run_test, 100)
|
||||
```
|
||||
|
||||
### Key Testing Principles
|
||||
- **Timer callbacks are essential**: Screenshots only work after the render loop starts
|
||||
- **Use automation API**: `automation.screenshot()`, `automation.click()` for visual testing
|
||||
- **Exit properly**: Always call `sys.exit(0)` for PASS or `sys.exit(1)` for FAIL
|
||||
- **Headless mode**: Use `--headless --exec` for CI/automated testing
|
||||
- **Check examples first**: Read `tests/demo/screens/*.py` for correct API usage
|
||||
|
||||
### API Quick Reference (from tests)
|
||||
```python
|
||||
# Scene: create and activate a scene, or create another scene
|
||||
mcrfpy.current_scene = mcrfpy.Scene("test")
|
||||
demo_scene = mcrfpy.Scene("demo")
|
||||
|
||||
# Animation: (property, target_value, duration, easing)
|
||||
# direct use of Animation object: deprecated
|
||||
#anim = mcrfpy.Animation("x", 500.0, 2.0, "easeInOut")
|
||||
#anim.start(frame)
|
||||
|
||||
# preferred: create animations directly against the targeted object; use Enum of easing functions
|
||||
frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT)
|
||||
|
||||
# Caption: use keyword arguments to avoid positional conflicts
|
||||
cap = mcrfpy.Caption(text="Hello", pos=(100, 100))
|
||||
|
||||
# Grid center: uses pixel coordinates, not cell coordinates
|
||||
grid = mcrfpy.Grid(grid_size=(15, 10), pos=(50, 50), size=(400, 300))
|
||||
grid.center = (120, 80) # pixels: (cells * cell_size / 2)
|
||||
# grid center defaults to the position that puts (0, 0) in the top left corner of the grid's visible area.
|
||||
# set grid.center to focus on that position. To position the camera in tile coordinates, use grid.center_camera():
|
||||
grid.center_camera((14.5, 8.5)) # offset of 0.5 tiles to point at the middle of the tile
|
||||
|
||||
# Keyboard handler: key names are "Num1", "Num2", "Escape", "Q", etc.
|
||||
def on_key(key, state):
|
||||
if key == "Num1" and state == "start":
|
||||
demo_scene.activate()
|
||||
```
|
||||
|
||||
## Development Best Practices
|
||||
|
||||
### Testing and Deployment
|
||||
- **Keep tests in ./tests, not ./build/tests** - ./build gets shipped, tests shouldn't be included
|
||||
- **Run full suite before commits**: `cd tests && python3 run_tests.py`
|
||||
|
||||
## Documentation Guidelines
|
||||
|
||||
### Documentation Macro System
|
||||
|
||||
**As of 2025-10-30, McRogueFace uses a macro-based documentation system** (`src/McRFPy_Doc.h`) that ensures consistent, complete docstrings across all Python bindings.
|
||||
|
||||
#### Include the Header
|
||||
|
||||
```cpp
|
||||
#include "McRFPy_Doc.h"
|
||||
```
|
||||
|
||||
#### Documenting Methods
|
||||
|
||||
For methods in PyMethodDef arrays, use `MCRF_METHOD`:
|
||||
|
||||
```cpp
|
||||
{"method_name", (PyCFunction)Class::method, METH_VARARGS,
|
||||
MCRF_METHOD(ClassName, method_name,
|
||||
MCRF_SIG("(arg1: type, arg2: type)", "return_type"),
|
||||
MCRF_DESC("Brief description of what the method does."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("arg1", "Description of first argument")
|
||||
MCRF_ARG("arg2", "Description of second argument")
|
||||
MCRF_RETURNS("Description of return value")
|
||||
MCRF_RAISES("ValueError", "Condition that raises this exception")
|
||||
MCRF_NOTE("Important notes or caveats")
|
||||
MCRF_LINK("docs/guide.md", "Related Documentation")
|
||||
)},
|
||||
```
|
||||
|
||||
#### Documenting Properties
|
||||
|
||||
For properties in PyGetSetDef arrays, use `MCRF_PROPERTY`:
|
||||
|
||||
```cpp
|
||||
{"property_name", (getter)getter_func, (setter)setter_func,
|
||||
MCRF_PROPERTY(property_name,
|
||||
"Brief description of the property. "
|
||||
"Additional details about valid values, side effects, etc."
|
||||
), NULL},
|
||||
```
|
||||
|
||||
#### Available Macros
|
||||
|
||||
- `MCRF_SIG(params, ret)` - Method signature
|
||||
- `MCRF_DESC(text)` - Description paragraph
|
||||
- `MCRF_ARGS_START` - Begin arguments section
|
||||
- `MCRF_ARG(name, desc)` - Individual argument
|
||||
- `MCRF_RETURNS(text)` - Return value description
|
||||
- `MCRF_RAISES(exception, condition)` - Exception documentation
|
||||
- `MCRF_NOTE(text)` - Important notes
|
||||
- `MCRF_LINK(path, text)` - Reference to external documentation
|
||||
|
||||
#### Documentation Prose Guidelines
|
||||
|
||||
**Keep C++ docstrings concise** (1-2 sentences per section). For complex topics, use `MCRF_LINK` to reference external guides:
|
||||
|
||||
```cpp
|
||||
MCRF_LINK("docs/animation-guide.md", "Animation System Tutorial")
|
||||
```
|
||||
|
||||
**External documentation** (in `docs/`) can be verbose with examples, tutorials, and design rationale.
|
||||
|
||||
### Regenerating Documentation
|
||||
|
||||
After modifying C++ inline documentation with MCRF_* macros:
|
||||
|
||||
1. **Rebuild the project**: `make -j$(nproc)`
|
||||
|
||||
2. **Generate all documentation** (recommended - single command):
|
||||
```bash
|
||||
./tools/generate_all_docs.sh
|
||||
```
|
||||
This creates:
|
||||
- `docs/api_reference_dynamic.html` - HTML API reference
|
||||
- `docs/API_REFERENCE_DYNAMIC.md` - Markdown API reference
|
||||
- `docs/mcrfpy.3` - Unix man page (section 3)
|
||||
- `stubs/mcrfpy.pyi` - Type stubs for IDE support
|
||||
|
||||
3. **Or generate individually**:
|
||||
```bash
|
||||
# API docs (HTML + Markdown)
|
||||
./build/mcrogueface --headless --exec tools/generate_dynamic_docs.py
|
||||
|
||||
# Type stubs (manually-maintained with @overload support)
|
||||
./build/mcrogueface --headless --exec tools/generate_stubs_v2.py
|
||||
|
||||
# Man page (requires pandoc)
|
||||
./tools/generate_man_page.sh
|
||||
```
|
||||
|
||||
**System Requirements:**
|
||||
- `pandoc` must be installed for man page generation: `sudo apt-get install pandoc`
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Single source of truth**: Documentation lives in C++ source files via MCRF_* macros
|
||||
- **McRogueFace as Python interpreter**: Documentation scripts MUST be run using McRogueFace itself, not system Python
|
||||
- **Use --headless --exec**: For non-interactive documentation generation
|
||||
- **Link transformation**: `MCRF_LINK` references are transformed to appropriate format (HTML, Markdown, etc.)
|
||||
- **No manual dictionaries**: The old hardcoded documentation system has been removed
|
||||
|
||||
### Documentation Pipeline Architecture
|
||||
|
||||
1. **C++ Source** → MCRF_* macros in PyMethodDef/PyGetSetDef arrays
|
||||
2. **Compilation** → Macros expand to complete docstrings embedded in module
|
||||
3. **Introspection** → Scripts use `dir()`, `getattr()`, `__doc__` to extract
|
||||
4. **Generation** → HTML/Markdown/Stub files created with transformed links
|
||||
5. **No drift**: Impossible for docs and code to disagree - they're the same file!
|
||||
|
||||
The macro system ensures complete, consistent documentation across all Python bindings.
|
||||
|
||||
### Adding Documentation for New Python Types
|
||||
|
||||
When adding a new Python class/type to the engine, follow these steps to ensure it's properly documented:
|
||||
|
||||
#### 1. Class Docstring (tp_doc)
|
||||
|
||||
In the `PyTypeObject` definition (usually in the header file), set `tp_doc` with a comprehensive docstring:
|
||||
|
||||
```cpp
|
||||
// In PyMyClass.h
|
||||
.tp_doc = PyDoc_STR(
|
||||
"MyClass(arg1: type, arg2: type)\n\n"
|
||||
"Brief description of what this class does.\n\n"
|
||||
"Args:\n"
|
||||
" arg1: Description of first argument.\n"
|
||||
" arg2: Description of second argument.\n\n"
|
||||
"Properties:\n"
|
||||
" prop1 (type, read-only): Description of property.\n"
|
||||
" prop2 (type): Description of writable property.\n\n"
|
||||
"Example:\n"
|
||||
" obj = mcrfpy.MyClass('example', 42)\n"
|
||||
" print(obj.prop1)\n"
|
||||
),
|
||||
```
|
||||
|
||||
#### 2. Method Documentation (PyMethodDef)
|
||||
|
||||
For each method in the `methods[]` array, use the MCRF_* macros:
|
||||
|
||||
```cpp
|
||||
// In PyMyClass.cpp
|
||||
PyMethodDef PyMyClass::methods[] = {
|
||||
{"do_something", (PyCFunction)do_something, METH_VARARGS,
|
||||
MCRF_METHOD(MyClass, do_something,
|
||||
MCRF_SIG("(value: int)", "bool"),
|
||||
MCRF_DESC("Does something with the value."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("value", "The value to process")
|
||||
MCRF_RETURNS("True if successful, False otherwise")
|
||||
)},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. Property Documentation (PyGetSetDef)
|
||||
|
||||
For each property in the `getsetters[]` array, include a docstring:
|
||||
|
||||
```cpp
|
||||
// In PyMyClass.cpp
|
||||
PyGetSetDef PyMyClass::getsetters[] = {
|
||||
{"property_name", (getter)get_property, (setter)set_property,
|
||||
"Property description. Include (type, read-only) if not writable.",
|
||||
NULL},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
```
|
||||
|
||||
**Important for read-only properties:** Include "read-only" in the docstring so the doc generator detects it:
|
||||
```cpp
|
||||
{"name", (getter)get_name, NULL, // NULL setter = read-only
|
||||
"Object name (str, read-only). Unique identifier.",
|
||||
NULL},
|
||||
```
|
||||
|
||||
#### 4. Register Type in Module
|
||||
|
||||
Ensure the type is properly registered in `McRFPy_API.cpp` and its methods/getsetters are assigned:
|
||||
|
||||
```cpp
|
||||
// Set methods and getsetters before PyType_Ready
|
||||
mcrfpydef::PyMyClassType.tp_methods = PyMyClass::methods;
|
||||
mcrfpydef::PyMyClassType.tp_getset = PyMyClass::getsetters;
|
||||
|
||||
// Then call PyType_Ready and add to module
|
||||
```
|
||||
|
||||
#### 5. Regenerate Documentation
|
||||
|
||||
After adding the new type, regenerate all docs:
|
||||
|
||||
```bash
|
||||
make -j4 # Rebuild with new documentation
|
||||
cd build
|
||||
./mcrogueface --headless --exec ../tools/generate_dynamic_docs.py
|
||||
cp docs/API_REFERENCE_DYNAMIC.md ../docs/
|
||||
cp docs/api_reference_dynamic.html ../docs/
|
||||
```
|
||||
|
||||
#### 6. Update Type Stubs (Optional)
|
||||
|
||||
For IDE support, update `stubs/mcrfpy.pyi` with the new class:
|
||||
|
||||
```python
|
||||
class MyClass:
|
||||
"""Brief description."""
|
||||
def __init__(self, arg1: str, arg2: int) -> None: ...
|
||||
@property
|
||||
def prop1(self) -> str: ...
|
||||
def do_something(self, value: int) -> bool: ...
|
||||
```
|
||||
|
||||
### Documentation Extraction Details
|
||||
|
||||
The doc generator (`tools/generate_dynamic_docs.py`) uses Python introspection:
|
||||
|
||||
- **Classes**: Detected via `inspect.isclass()`, docstring from `cls.__doc__`
|
||||
- **Methods**: Detected via `callable()` check on class attributes
|
||||
- **Properties**: Detected via `types.GetSetDescriptorType` (C++ extension) or `property` (Python)
|
||||
- **Read-only detection**: Checks if "read-only" appears in property docstring
|
||||
|
||||
If documentation isn't appearing, verify:
|
||||
1. The type is exported to the `mcrfpy` module
|
||||
2. Methods/getsetters arrays are properly assigned before `PyType_Ready()`
|
||||
3. Docstrings don't contain null bytes or invalid UTF-8
|
||||
|
||||
---
|
||||
|
||||
- Close issues automatically in gitea by adding to the commit message "closes #X", where X is the issue number. This associates the issue closure with the specific commit, so granular commits are preferred. You should only use the MCP tool to close issues directly when discovering that the issue is already complete; when committing changes, always such "closes" (or the opposite, "reopens") references to related issues. If on a feature branch, the issue will be referenced by the commit, and when merged to master, the issue will be actually closed (or reopened).
|
||||
203
CMakeLists.txt
|
|
@ -8,157 +8,49 @@ project(McRogueFace)
|
|||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
# 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}/deps_linux/Python-3.11.1)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
|
||||
|
||||
# Python includes: use different paths for Windows vs Linux
|
||||
if(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
|
||||
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
|
||||
)
|
||||
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
|
||||
list(APPEND SOURCES ${IMGUI_SOURCES})
|
||||
|
||||
# Find OpenGL (required by ImGui-SFML)
|
||||
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()
|
||||
|
||||
# Create a list of libraries to link against
|
||||
if(MCRF_CROSS_WINDOWS)
|
||||
# MinGW cross-compilation: use full library names
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
libtcod
|
||||
python314
|
||||
${OPENGL_LIBRARIES})
|
||||
|
||||
# Add Windows system libraries needed by SFML and MinGW
|
||||
list(APPEND LINK_LIBS
|
||||
winmm # Windows multimedia (for audio)
|
||||
gdi32 # Graphics Device Interface
|
||||
ws2_32 # Winsock (networking, used by some deps)
|
||||
ole32 # OLE support
|
||||
oleaut32 # OLE automation
|
||||
uuid # UUID library
|
||||
comdlg32 # Common dialogs
|
||||
imm32 # Input Method Manager
|
||||
version # Version info
|
||||
)
|
||||
set(LINK_LIBS
|
||||
m
|
||||
dl
|
||||
util
|
||||
pthread
|
||||
python3.12
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod)
|
||||
|
||||
# On Windows, add any additional libs and include directories
|
||||
if(WIN32)
|
||||
# Add the necessary Windows-specific libraries and include directories
|
||||
# include_directories(path_to_additional_includes)
|
||||
# link_directories(path_to_additional_libs)
|
||||
# list(APPEND LINK_LIBS additional_windows_libs)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
|
||||
# Link directories for cross-compiled Windows libs
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/sfml/lib)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/lib)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows)
|
||||
elseif(WIN32)
|
||||
# Native Windows build (MSVC)
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod
|
||||
python314
|
||||
${OPENGL_LIBRARIES})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
else()
|
||||
# Unix/Linux build
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod
|
||||
python3.14
|
||||
m dl util pthread
|
||||
${OPENGL_LIBRARIES})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
endif()
|
||||
|
||||
# Add the directory where the linker should look for the libraries
|
||||
#link_directories(${CMAKE_SOURCE_DIR}/deps_linux)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/lib)
|
||||
|
||||
# Define the executable target before linking libraries
|
||||
add_executable(mcrogueface ${SOURCES})
|
||||
|
||||
# Define NO_SDL for libtcod-headless headers (excludes SDL-dependent code)
|
||||
target_compile_definitions(mcrogueface PRIVATE NO_SDL)
|
||||
|
||||
# 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})
|
||||
|
||||
|
|
@ -175,44 +67,9 @@ add_custom_command(TARGET mcrogueface POST_BUILD
|
|||
# Copy Python standard library to build directory
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib)
|
||||
${CMAKE_SOURCE_DIR}/lib $<TARGET_FILE_DIR:mcrogueface>/lib)
|
||||
|
||||
# On Windows, copy DLLs to executable directory
|
||||
if(MCRF_CROSS_WINDOWS)
|
||||
# Cross-compilation: copy DLLs from __lib_windows
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/sfml/bin $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/bin $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/python314.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/python3.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/vcruntime140.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/vcruntime140_1.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
/usr/x86_64-w64-mingw32/lib/libwinpthread-1.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Copied Windows DLLs to executable directory")
|
||||
|
||||
# Copy Python standard library zip
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/python314.zip $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Copied Python stdlib")
|
||||
elseif(WIN32)
|
||||
# Native Windows build: copy DLLs from __lib
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Copied DLLs to executable directory")
|
||||
endif()
|
||||
|
||||
# rpath for including shared libraries (Linux/Unix only)
|
||||
if(NOT WIN32)
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
INSTALL_RPATH "$ORIGIN/./lib")
|
||||
endif()
|
||||
# rpath for including shared libraries
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
INSTALL_RPATH "./lib")
|
||||
|
||||
|
|
|
|||
198
README.md
|
|
@ -1,182 +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.
|
||||
|
||||
![ Image ]()
|
||||
## Why?
|
||||
|
||||
**Pre-Alpha Release Demo**: my 7DRL 2025 entry *"Crypt of Sokoban"* - a prototype with buttons, boulders, enemies, and items.
|
||||
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.
|
||||
|
||||
## Quick Start
|
||||
## To-do
|
||||
|
||||
**Download**:
|
||||
|
||||
- The entire McRogueFace visual framework:
|
||||
- **Sprite**: an image file or one sprite from a shared sprite sheet
|
||||
- **Caption**: load a font, display text
|
||||
- **Frame**: A rectangle; put other things on it to move or manage GUIs as modules
|
||||
- **Grid**: A 2D array of tiles with zoom + position control
|
||||
- **Entity**: Lives on a Grid, displays a sprite, and can have a perspective or move along a path
|
||||
- **Animation**: Change any property on any of the above over time
|
||||
|
||||
```bash
|
||||
# Clone and build
|
||||
git clone <wherever you found this repo>
|
||||
cd McRogueFace
|
||||
make
|
||||
|
||||
# Run the example game
|
||||
cd build
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
|
||||
For most users, pre-built releases are available. If you need to build from source:
|
||||
|
||||
### Quick Build (with pre-built dependencies)
|
||||
|
||||
Download `build_deps.tar.gz` from the releases page, then:
|
||||
|
||||
```bash
|
||||
git clone <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
tar -xzf /path/to/build_deps.tar.gz
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
```
|
||||
|
||||
### Full Build (compiling all dependencies)
|
||||
|
||||
```bash
|
||||
git clone --recursive <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
# See BUILD_FROM_SOURCE.md for complete instructions
|
||||
```
|
||||
|
||||
**[BUILD_FROM_SOURCE.md](BUILD_FROM_SOURCE.md)** - Complete build guide including:
|
||||
- System dependency installation
|
||||
- Compiling SFML, Python, and libtcod-headless from source
|
||||
- Creating `build_deps` archives for distribution
|
||||
- Troubleshooting common build issues
|
||||
|
||||
### System Requirements
|
||||
|
||||
- **Linux**: Debian/Ubuntu tested; other distros should work
|
||||
- **Windows**: Supported (see build guide for details)
|
||||
- **macOS**: Untested
|
||||
|
||||
## Example: Creating a Simple Scene
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Create a new scene
|
||||
intro = mcrfpy.Scene("intro")
|
||||
|
||||
# Add a text caption
|
||||
caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!")
|
||||
caption.size = 48
|
||||
caption.fill_color = (255, 255, 255)
|
||||
|
||||
# Add to scene
|
||||
intro.children.append(caption)
|
||||
|
||||
# Switch to the scene
|
||||
intro.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.12+
|
||||
- SFML 2.6
|
||||
- Linux or Windows (macOS untested)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
McRogueFace/
|
||||
├── assets/ # Sprites, fonts, audio
|
||||
├── build/ # Build output directory: zip + ship
|
||||
│ ├─ (*)assets/ # (copied location of assets)
|
||||
│ ├─ (*)scripts/ # (copied location of src/scripts)
|
||||
│ └─ lib/ # SFML, TCOD libraries, Python + standard library / modules
|
||||
├── deps/ # Python, SFML, and libtcod imports can be tossed in here to build
|
||||
│ └─ platform/ # windows, linux subdirectories for OS-specific cpython config
|
||||
├── docs/ # generated HTML, markdown docs
|
||||
│ └─ stubs/ # .pyi files for editor integration
|
||||
├── modules/ # git submodules, to build all of McRogueFace's dependencies from source
|
||||
├── src/ # C++ engine source
|
||||
│ └─ scripts/ # Python game scripts (copied during build)
|
||||
└── tests/ # Automated test suite
|
||||
└── tools/ # For the McRogueFace ecosystem: docs generation
|
||||
```
|
||||
|
||||
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 - 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
|
||||
|
|
|
|||
223
ROADMAP.md
|
|
@ -1,223 +0,0 @@
|
|||
# McRogueFace - Development Roadmap
|
||||
|
||||
## Project Status
|
||||
|
||||
**Current State**: Active development - C++ game engine with Python scripting
|
||||
**Latest Release**: Alpha 0.1
|
||||
**Issue Tracking**: See [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current tasks and bugs
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Strategic Vision
|
||||
|
||||
### Engine Philosophy
|
||||
|
||||
- **C++ First**: Performance-critical code stays in C++
|
||||
- **Python Close Behind**: Rich scripting without frame-rate impact
|
||||
- **Game-Ready**: Each improvement should benefit actual game development
|
||||
|
||||
### Architecture Goals
|
||||
|
||||
1. **Clean Inheritance**: Drawable → UI components, proper type preservation
|
||||
2. **Collection Consistency**: Uniform iteration, indexing, and search patterns
|
||||
3. **Resource Management**: RAII everywhere, proper lifecycle handling
|
||||
4. **Multi-Platform**: Windows/Linux feature parity maintained
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture Decisions
|
||||
|
||||
### Three-Layer Grid Architecture
|
||||
Following successful roguelike patterns (Caves of Qud, Cogmind, DCSS):
|
||||
|
||||
1. **Visual Layer** (UIGridPoint) - Sprites, colors, animations
|
||||
2. **World State Layer** (TCODMap) - Walkability, transparency, physics
|
||||
3. **Entity Perspective Layer** (UIGridPointState) - Per-entity FOV, knowledge
|
||||
|
||||
### Performance Architecture
|
||||
Critical for large maps (1000x1000):
|
||||
|
||||
- **Spatial Hashing** for entity queries (not quadtrees!)
|
||||
- **Batch Operations** with context managers (10-100x speedup)
|
||||
- **Memory Pooling** for entities and components
|
||||
- **Dirty Flag System** to avoid unnecessary updates
|
||||
- **Zero-Copy NumPy Integration** via buffer protocol
|
||||
|
||||
### Key Insight from Research
|
||||
"Minimizing Python/C++ boundary crossings matters more than individual function complexity"
|
||||
- Batch everything possible
|
||||
- Use context managers for logical operations
|
||||
- Expose arrays, not individual cells
|
||||
- Profile and optimize hot paths only
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Development Phases
|
||||
|
||||
For detailed task tracking and current priorities, see the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues).
|
||||
|
||||
### Phase 1: Foundation Stabilization ✅
|
||||
**Status**: Complete
|
||||
**Key Issues**: #7 (Safe Constructors), #71 (Base Class), #87 (Visibility), #88 (Opacity)
|
||||
|
||||
### Phase 2: Constructor & API Polish ✅
|
||||
**Status**: Complete
|
||||
**Key Features**: Pythonic API, tuple support, standardized defaults
|
||||
|
||||
### Phase 3: Entity Lifecycle Management ✅
|
||||
**Status**: Complete
|
||||
**Key Issues**: #30 (Entity.die()), #93 (Vector methods), #94 (Color helpers), #103 (Timer objects)
|
||||
|
||||
### Phase 4: Visibility & Performance ✅
|
||||
**Status**: Complete
|
||||
**Key Features**: AABB culling, name system, profiling tools
|
||||
|
||||
### Phase 5: Window/Scene Architecture ✅
|
||||
**Status**: Complete
|
||||
**Key Issues**: #34 (Window object), #61 (Scene object), #1 (Resize events), #105 (Scene transitions)
|
||||
|
||||
### Phase 6: Rendering Revolution ✅
|
||||
**Status**: Complete
|
||||
**Key Issues**: #50 (Grid backgrounds), #6 (RenderTexture), #8 (Viewport rendering)
|
||||
|
||||
### Phase 7: Documentation & Distribution ✅
|
||||
**Status**: Complete (2025-10-30)
|
||||
**Key Issues**: #85 (Docstrings), #86 (Parameter docs), #108 (Type stubs), #97 (API docs)
|
||||
**Completed**: All classes and functions converted to MCRF_* macro system with automated HTML/Markdown/man page generation
|
||||
|
||||
See [current open issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues?state=open) for active work.
|
||||
|
||||
---
|
||||
|
||||
## 🔮 Future Vision: Pure Python Extension Architecture
|
||||
|
||||
### Concept: McRogueFace as a Traditional Python Package
|
||||
**Status**: Long-term vision
|
||||
**Complexity**: Major architectural overhaul
|
||||
|
||||
Instead of being a C++ application that embeds Python, McRogueFace could be redesigned as a pure Python extension module that can be installed via `pip install mcrogueface`.
|
||||
|
||||
### Technical Approach
|
||||
|
||||
1. **Separate Core Engine from Python Embedding**
|
||||
- Extract SFML rendering, audio, and input into C++ extension modules
|
||||
- Remove embedded CPython interpreter
|
||||
- Use Python's C API to expose functionality
|
||||
|
||||
2. **Module Structure**
|
||||
```
|
||||
mcrfpy/
|
||||
├── __init__.py # Pure Python coordinator
|
||||
├── _core.so # C++ rendering/game loop extension
|
||||
├── _sfml.so # SFML bindings
|
||||
├── _audio.so # Audio system bindings
|
||||
└── engine.py # Python game engine logic
|
||||
```
|
||||
|
||||
3. **Inverted Control Flow**
|
||||
- Python drives the main loop instead of C++
|
||||
- C++ extensions handle performance-critical operations
|
||||
- Python manages game logic, scenes, and entity systems
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Standard Python Packaging**: `pip install mcrogueface`
|
||||
- **Virtual Environment Support**: Works with venv, conda, poetry
|
||||
- **Better IDE Integration**: Standard Python development workflow
|
||||
- **Easier Testing**: Use pytest, standard Python testing tools
|
||||
- **Cross-Python Compatibility**: Support multiple Python versions
|
||||
- **Modular Architecture**: Users can import only what they need
|
||||
|
||||
### Challenges
|
||||
|
||||
- **Major Refactoring**: Complete restructure of codebase
|
||||
- **Performance Considerations**: Python-driven main loop overhead
|
||||
- **Build Complexity**: Multiple extension modules to compile
|
||||
- **Platform Support**: Need wheels for many platform/Python combinations
|
||||
- **API Stability**: Would need careful design to maintain compatibility
|
||||
|
||||
### Example Usage (Future Vision)
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
from mcrfpy import Scene, Frame, Sprite, Grid
|
||||
|
||||
# Create game directly in Python
|
||||
game = mcrfpy.Game(width=1024, height=768)
|
||||
|
||||
# Define scenes using Python classes
|
||||
class MainMenu(Scene):
|
||||
def on_enter(self):
|
||||
self.ui.append(Frame(100, 100, 200, 50))
|
||||
self.ui.append(Sprite("logo.png", x=400, y=100))
|
||||
|
||||
def on_keypress(self, key, pressed):
|
||||
if key == "ENTER" and pressed:
|
||||
self.game.set_scene("game")
|
||||
|
||||
# Run the game
|
||||
game.add_scene("menu", MainMenu())
|
||||
game.run()
|
||||
```
|
||||
|
||||
This architecture would make McRogueFace a first-class Python citizen, following standard Python packaging conventions while maintaining high performance through C++ extensions.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Major Feature Areas
|
||||
|
||||
For current status and detailed tasks, see the corresponding Gitea issue labels:
|
||||
|
||||
### Core Systems
|
||||
- **UI/Rendering System**: Issues tagged `[Major Feature]` related to rendering
|
||||
- **Grid/Entity System**: Pathfinding, FOV, entity management
|
||||
- **Animation System**: Property animation, easing functions, callbacks
|
||||
- **Scene/Window Management**: Scene lifecycle, transitions, viewport
|
||||
|
||||
### Performance Optimization
|
||||
- **#115**: SpatialHash for 10,000+ entities
|
||||
- **#116**: Dirty flag system
|
||||
- **#113**: Batch operations for NumPy-style access
|
||||
- **#117**: Memory pool for entities
|
||||
|
||||
### Advanced Features
|
||||
- **#118**: Scene as Drawable (scenes can be drawn/animated)
|
||||
- **#122**: Parent-Child UI System
|
||||
- **#123**: Grid Subgrid System (256x256 chunks)
|
||||
- **#124**: Grid Point Animation
|
||||
- **#106**: Shader support
|
||||
- **#107**: Particle system
|
||||
|
||||
### Documentation
|
||||
- **#92**: Inline C++ documentation system
|
||||
- **#91**: Python type stub files (.pyi)
|
||||
- **#97**: Automated API documentation extraction
|
||||
- **#126**: Generate perfectly consistent Python interface
|
||||
|
||||
---
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
- **Issue Tracker**: [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues)
|
||||
- **Source Code**: [Gitea Repository](https://gamedev.ffwf.net/gitea/john/McRogueFace)
|
||||
- **Documentation**: See `CLAUDE.md` for build instructions and development guide
|
||||
- **Tutorial**: See `roguelike_tutorial/` for implementation examples
|
||||
- **Workflow**: See "Gitea-First Workflow" section in `CLAUDE.md` for issue management best practices
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Development Workflow
|
||||
|
||||
**Gitea is the Single Source of Truth** for this project. Before starting any work:
|
||||
|
||||
1. **Check Gitea Issues** for existing tasks, bugs, or related work
|
||||
2. **Create granular issues** for new features or problems
|
||||
3. **Update issues** when work affects other systems
|
||||
4. **Document discoveries** - if something is undocumented or misleading, create a task to fix it
|
||||
5. **Cross-reference commits** with issue numbers (e.g., "Fixes #104")
|
||||
|
||||
See the "Gitea-First Workflow" section in `CLAUDE.md` for detailed guidelines on efficient development practices using the Gitea MCP tools.
|
||||
|
||||
---
|
||||
|
||||
*For current priorities, task tracking, and bug reports, please use the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues).*
|
||||
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
|
||||
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,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,120 +0,0 @@
|
|||
"""McRogueFace - Health Bar Widget (animated)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_animated.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class AnimatedHealthBar:
|
||||
"""Health bar with smooth fill animation."""
|
||||
|
||||
def __init__(self, x, y, w, h, current, maximum):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.current = current
|
||||
self.display_current = current # What's visually shown
|
||||
self.maximum = maximum
|
||||
self.timer_name = f"hp_anim_{id(self)}"
|
||||
|
||||
# Background
|
||||
self.background = mcrfpy.Frame(x, y, w, h)
|
||||
self.background.fill_color = mcrfpy.Color(40, 40, 40)
|
||||
self.background.outline = 2
|
||||
self.background.outline_color = mcrfpy.Color(60, 60, 60)
|
||||
|
||||
# Damage preview (shows recent damage in different color)
|
||||
self.damage_fill = mcrfpy.Frame(x + 2, y + 2, w - 4, h - 4)
|
||||
self.damage_fill.fill_color = mcrfpy.Color(180, 50, 50)
|
||||
self.damage_fill.outline = 0
|
||||
|
||||
# Main fill
|
||||
self.fill = mcrfpy.Frame(x + 2, y + 2, w - 4, h - 4)
|
||||
self.fill.fill_color = mcrfpy.Color(50, 200, 50)
|
||||
self.fill.outline = 0
|
||||
|
||||
self._update_display()
|
||||
|
||||
def _update_display(self):
|
||||
"""Update the visual fill based on display_current."""
|
||||
ratio = max(0, min(1, self.display_current / self.maximum))
|
||||
self.fill.w = (self.w - 4) * ratio
|
||||
|
||||
# Color based on ratio
|
||||
if ratio > 0.6:
|
||||
self.fill.fill_color = mcrfpy.Color(50, 200, 50)
|
||||
elif ratio > 0.3:
|
||||
self.fill.fill_color = mcrfpy.Color(230, 180, 30)
|
||||
else:
|
||||
self.fill.fill_color = mcrfpy.Color(200, 50, 50)
|
||||
|
||||
def set_health(self, new_current, animate=True):
|
||||
"""
|
||||
Set health with optional animation.
|
||||
|
||||
Args:
|
||||
new_current: New health value
|
||||
animate: Whether to animate the transition
|
||||
"""
|
||||
old_current = self.current
|
||||
self.current = max(0, min(self.maximum, new_current))
|
||||
|
||||
if not animate:
|
||||
self.display_current = self.current
|
||||
self._update_display()
|
||||
return
|
||||
|
||||
# Show damage preview immediately
|
||||
if self.current < old_current:
|
||||
damage_ratio = self.current / self.maximum
|
||||
self.damage_fill.w = (self.w - 4) * (old_current / self.maximum)
|
||||
|
||||
# Animate the fill
|
||||
self._start_animation()
|
||||
|
||||
def _start_animation(self):
|
||||
"""Start animating toward target health."""
|
||||
mcrfpy.delTimer(self.timer_name)
|
||||
|
||||
def animate_step(dt):
|
||||
# Lerp toward target
|
||||
diff = self.current - self.display_current
|
||||
if abs(diff) < 0.5:
|
||||
self.display_current = self.current
|
||||
mcrfpy.delTimer(self.timer_name)
|
||||
# Also update damage preview
|
||||
self.damage_fill.w = self.fill.w
|
||||
else:
|
||||
# Move 10% of the way each frame
|
||||
self.display_current += diff * 0.1
|
||||
|
||||
self._update_display()
|
||||
|
||||
mcrfpy.setTimer(self.timer_name, animate_step, 16)
|
||||
|
||||
def damage(self, amount):
|
||||
"""Apply damage with animation."""
|
||||
self.set_health(self.current - amount, animate=True)
|
||||
|
||||
def heal(self, amount):
|
||||
"""Apply healing with animation."""
|
||||
self.set_health(self.current + amount, animate=True)
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
"""Add all frames to scene."""
|
||||
ui.append(self.background)
|
||||
ui.append(self.damage_fill)
|
||||
ui.append(self.fill)
|
||||
|
||||
|
||||
# Usage
|
||||
hp_bar = AnimatedHealthBar(50, 50, 300, 30, current=100, maximum=100)
|
||||
hp_bar.add_to_scene(ui)
|
||||
|
||||
# Damage will animate smoothly
|
||||
hp_bar.damage(40)
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
"""McRogueFace - Health Bar Widget (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
|
||||
# Player health bar at top
|
||||
player_hp = EnhancedHealthBar(10, 10, 300, 30, 100, 100)
|
||||
player_hp.add_to_scene(ui)
|
||||
|
||||
# Enemy health bar
|
||||
enemy_hp = EnhancedHealthBar(400, 10, 200, 20, 50, 50)
|
||||
enemy_hp.add_to_scene(ui)
|
||||
|
||||
# Simulate combat
|
||||
def combat_tick(dt):
|
||||
import random
|
||||
if random.random() < 0.3:
|
||||
player_hp.damage(random.randint(5, 15))
|
||||
if random.random() < 0.4:
|
||||
enemy_hp.damage(random.randint(3, 8))
|
||||
|
||||
mcrfpy.setTimer("combat", combat_tick, 1000)
|
||||
|
||||
# Keyboard controls for testing
|
||||
def on_key(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
if key == "H":
|
||||
player_hp.heal(20)
|
||||
elif key == "D":
|
||||
player_hp.damage(10)
|
||||
|
||||
mcrfpy.keypressScene(on_key)
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
"""McRogueFace - Health Bar Widget (enhanced)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_enhanced.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class EnhancedHealthBar:
|
||||
"""Health bar with text display, color transitions, and animations."""
|
||||
|
||||
def __init__(self, x, y, w, h, current, maximum, show_text=True):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.current = current
|
||||
self.maximum = maximum
|
||||
self.show_text = show_text
|
||||
|
||||
# Color thresholds (ratio -> color)
|
||||
self.colors = {
|
||||
0.6: mcrfpy.Color(50, 205, 50), # Green when > 60%
|
||||
0.3: mcrfpy.Color(255, 165, 0), # Orange when > 30%
|
||||
0.0: mcrfpy.Color(220, 20, 20), # Red when <= 30%
|
||||
}
|
||||
|
||||
# Background frame with dark fill
|
||||
self.background = mcrfpy.Frame(x, y, w, h)
|
||||
self.background.fill_color = mcrfpy.Color(30, 30, 30)
|
||||
self.background.outline = 2
|
||||
self.background.outline_color = mcrfpy.Color(100, 100, 100)
|
||||
|
||||
# Fill frame (nested inside background conceptually)
|
||||
padding = 2
|
||||
self.fill = mcrfpy.Frame(
|
||||
x + padding,
|
||||
y + padding,
|
||||
w - padding * 2,
|
||||
h - padding * 2
|
||||
)
|
||||
self.fill.outline = 0
|
||||
|
||||
# Text label
|
||||
self.label = None
|
||||
if show_text:
|
||||
self.label = mcrfpy.Caption(
|
||||
"",
|
||||
mcrfpy.default_font,
|
||||
x + w / 2 - 20,
|
||||
y + h / 2 - 8
|
||||
)
|
||||
self.label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
self.label.outline = 1
|
||||
self.label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
self._update()
|
||||
|
||||
def _get_color_for_ratio(self, ratio):
|
||||
"""Get the appropriate color based on health ratio."""
|
||||
for threshold, color in sorted(self.colors.items(), reverse=True):
|
||||
if ratio > threshold:
|
||||
return color
|
||||
# Return the lowest threshold color if ratio is 0 or below
|
||||
return self.colors[0.0]
|
||||
|
||||
def _update(self):
|
||||
"""Update fill width, color, and text."""
|
||||
ratio = max(0, min(1, self.current / self.maximum))
|
||||
|
||||
# Update fill width (accounting for padding)
|
||||
padding = 2
|
||||
self.fill.w = (self.w - padding * 2) * ratio
|
||||
|
||||
# Update color based on ratio
|
||||
self.fill.fill_color = self._get_color_for_ratio(ratio)
|
||||
|
||||
# Update text
|
||||
if self.label:
|
||||
self.label.text = f"{int(self.current)}/{int(self.maximum)}"
|
||||
# Center the text
|
||||
text_width = len(self.label.text) * 8 # Approximate
|
||||
self.label.x = self.x + (self.w - text_width) / 2
|
||||
|
||||
def set_health(self, current, maximum=None):
|
||||
"""Update health values."""
|
||||
self.current = max(0, current)
|
||||
if maximum is not None:
|
||||
self.maximum = maximum
|
||||
self._update()
|
||||
|
||||
def damage(self, amount):
|
||||
"""Apply damage (convenience method)."""
|
||||
self.set_health(self.current - amount)
|
||||
|
||||
def heal(self, amount):
|
||||
"""Apply healing (convenience method)."""
|
||||
self.set_health(min(self.maximum, self.current + amount))
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
"""Add all components to scene UI."""
|
||||
ui.append(self.background)
|
||||
ui.append(self.fill)
|
||||
if self.label:
|
||||
ui.append(self.label)
|
||||
|
||||
|
||||
# Usage
|
||||
mcrfpy.createScene("demo")
|
||||
mcrfpy.setScene("demo")
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
|
||||
# Create enhanced health bar
|
||||
hp = EnhancedHealthBar(50, 50, 250, 25, current=100, maximum=100)
|
||||
hp.add_to_scene(ui)
|
||||
|
||||
# Simulate damage
|
||||
hp.damage(30) # Now 70/100, shows green
|
||||
hp.damage(25) # Now 45/100, shows orange
|
||||
hp.damage(20) # Now 25/100, shows red
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
"""McRogueFace - Health Bar Widget (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class ResourceBar:
|
||||
"""Generic resource bar that can represent any stat."""
|
||||
|
||||
def __init__(self, x, y, w, h, current, maximum,
|
||||
fill_color, bg_color=None, label=""):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.current = current
|
||||
self.maximum = maximum
|
||||
self.label_text = label
|
||||
|
||||
if bg_color is None:
|
||||
bg_color = mcrfpy.Color(30, 30, 30)
|
||||
|
||||
# Background
|
||||
self.background = mcrfpy.Frame(x, y, w, h)
|
||||
self.background.fill_color = bg_color
|
||||
self.background.outline = 1
|
||||
self.background.outline_color = mcrfpy.Color(60, 60, 60)
|
||||
|
||||
# Fill
|
||||
self.fill = mcrfpy.Frame(x + 1, y + 1, w - 2, h - 2)
|
||||
self.fill.fill_color = fill_color
|
||||
self.fill.outline = 0
|
||||
|
||||
# Label (left side)
|
||||
self.label = mcrfpy.Caption(label, mcrfpy.default_font, x - 30, y + 2)
|
||||
self.label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
ratio = max(0, min(1, self.current / self.maximum))
|
||||
self.fill.w = (self.w - 2) * ratio
|
||||
|
||||
def set_value(self, current, maximum=None):
|
||||
self.current = max(0, current)
|
||||
if maximum:
|
||||
self.maximum = maximum
|
||||
self._update()
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
if self.label_text:
|
||||
ui.append(self.label)
|
||||
ui.append(self.background)
|
||||
ui.append(self.fill)
|
||||
|
||||
|
||||
class PlayerStats:
|
||||
"""Collection of resource bars for a player."""
|
||||
|
||||
def __init__(self, x, y):
|
||||
bar_width = 200
|
||||
bar_height = 18
|
||||
spacing = 25
|
||||
|
||||
self.hp = ResourceBar(
|
||||
x, y, bar_width, bar_height,
|
||||
current=100, maximum=100,
|
||||
fill_color=mcrfpy.Color(220, 50, 50),
|
||||
label="HP"
|
||||
)
|
||||
|
||||
self.mp = ResourceBar(
|
||||
x, y + spacing, bar_width, bar_height,
|
||||
current=50, maximum=50,
|
||||
fill_color=mcrfpy.Color(50, 100, 220),
|
||||
label="MP"
|
||||
)
|
||||
|
||||
self.stamina = ResourceBar(
|
||||
x, y + spacing * 2, bar_width, bar_height,
|
||||
current=80, maximum=80,
|
||||
fill_color=mcrfpy.Color(50, 180, 50),
|
||||
label="SP"
|
||||
)
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
self.hp.add_to_scene(ui)
|
||||
self.mp.add_to_scene(ui)
|
||||
self.stamina.add_to_scene(ui)
|
||||
|
||||
|
||||
# Usage
|
||||
mcrfpy.createScene("stats_demo")
|
||||
mcrfpy.setScene("stats_demo")
|
||||
ui = mcrfpy.sceneUI("stats_demo")
|
||||
|
||||
stats = PlayerStats(80, 20)
|
||||
stats.add_to_scene(ui)
|
||||
|
||||
# Update individual stats
|
||||
stats.hp.set_value(75)
|
||||
stats.mp.set_value(30)
|
||||
stats.stamina.set_value(60)
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
"""McRogueFace - Selection Menu Widget (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_menu
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_menu_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Setup
|
||||
mcrfpy.createScene("main_menu")
|
||||
mcrfpy.setScene("main_menu")
|
||||
ui = mcrfpy.sceneUI("main_menu")
|
||||
|
||||
# Background
|
||||
bg = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
bg.fill_color = mcrfpy.Color(20, 20, 35)
|
||||
ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("DUNGEON QUEST", mcrfpy.default_font, 350, 100)
|
||||
title.fill_color = mcrfpy.Color(255, 200, 50)
|
||||
ui.append(title)
|
||||
|
||||
# Menu
|
||||
def start_game():
|
||||
print("Starting game...")
|
||||
|
||||
def show_options():
|
||||
print("Options...")
|
||||
|
||||
menu = Menu(
|
||||
362, 250,
|
||||
["New Game", "Continue", "Options", "Quit"],
|
||||
lambda i, opt: {
|
||||
0: start_game,
|
||||
1: lambda: print("Continue..."),
|
||||
2: show_options,
|
||||
3: mcrfpy.exit
|
||||
}.get(i, lambda: None)(),
|
||||
title="Main Menu"
|
||||
)
|
||||
menu.add_to_scene(ui)
|
||||
|
||||
# Input
|
||||
def on_key(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
menu.handle_key(key)
|
||||
|
||||
mcrfpy.keypressScene(on_key)
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
"""McRogueFace - Selection Menu Widget (enhanced)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_menu
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_menu_enhanced.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class MenuBar:
|
||||
"""Horizontal menu bar with dropdown submenus."""
|
||||
|
||||
def __init__(self, y=0, items=None):
|
||||
"""
|
||||
Create a menu bar.
|
||||
|
||||
Args:
|
||||
y: Y position (usually 0 for top)
|
||||
items: List of dicts with 'label' and 'options' keys
|
||||
"""
|
||||
self.y = y
|
||||
self.items = items or []
|
||||
self.selected_item = 0
|
||||
self.dropdown_open = False
|
||||
self.dropdown_selected = 0
|
||||
|
||||
self.item_width = 100
|
||||
self.height = 30
|
||||
|
||||
# Main bar frame
|
||||
self.bar = mcrfpy.Frame(0, y, 1024, self.height)
|
||||
self.bar.fill_color = mcrfpy.Color(50, 50, 70)
|
||||
self.bar.outline = 0
|
||||
|
||||
# Item captions
|
||||
self.item_captions = []
|
||||
for i, item in enumerate(items):
|
||||
cap = mcrfpy.Caption(
|
||||
item['label'],
|
||||
mcrfpy.default_font,
|
||||
10 + i * self.item_width,
|
||||
y + 7
|
||||
)
|
||||
cap.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.item_captions.append(cap)
|
||||
|
||||
# Dropdown panel (hidden initially)
|
||||
self.dropdown = None
|
||||
self.dropdown_captions = []
|
||||
|
||||
def _update_highlight(self):
|
||||
"""Update visual selection on bar."""
|
||||
for i, cap in enumerate(self.item_captions):
|
||||
if i == self.selected_item and self.dropdown_open:
|
||||
cap.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
else:
|
||||
cap.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
def _show_dropdown(self, ui):
|
||||
"""Show dropdown for selected item."""
|
||||
# Remove existing dropdown
|
||||
self._hide_dropdown(ui)
|
||||
|
||||
item = self.items[self.selected_item]
|
||||
options = item.get('options', [])
|
||||
|
||||
if not options:
|
||||
return
|
||||
|
||||
x = 5 + self.selected_item * self.item_width
|
||||
y = self.y + self.height
|
||||
width = 150
|
||||
height = len(options) * 25 + 10
|
||||
|
||||
self.dropdown = mcrfpy.Frame(x, y, width, height)
|
||||
self.dropdown.fill_color = mcrfpy.Color(40, 40, 60, 250)
|
||||
self.dropdown.outline = 1
|
||||
self.dropdown.outline_color = mcrfpy.Color(80, 80, 100)
|
||||
ui.append(self.dropdown)
|
||||
|
||||
self.dropdown_captions = []
|
||||
for i, opt in enumerate(options):
|
||||
cap = mcrfpy.Caption(
|
||||
opt['label'],
|
||||
mcrfpy.default_font,
|
||||
x + 10,
|
||||
y + 5 + i * 25
|
||||
)
|
||||
cap.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.dropdown_captions.append(cap)
|
||||
ui.append(cap)
|
||||
|
||||
self.dropdown_selected = 0
|
||||
self._update_dropdown_highlight()
|
||||
|
||||
def _hide_dropdown(self, ui):
|
||||
"""Hide dropdown menu."""
|
||||
if self.dropdown:
|
||||
try:
|
||||
ui.remove(self.dropdown)
|
||||
except:
|
||||
pass
|
||||
self.dropdown = None
|
||||
|
||||
for cap in self.dropdown_captions:
|
||||
try:
|
||||
ui.remove(cap)
|
||||
except:
|
||||
pass
|
||||
self.dropdown_captions = []
|
||||
|
||||
def _update_dropdown_highlight(self):
|
||||
"""Update dropdown selection highlight."""
|
||||
for i, cap in enumerate(self.dropdown_captions):
|
||||
if i == self.dropdown_selected:
|
||||
cap.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
else:
|
||||
cap.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
ui.append(self.bar)
|
||||
for cap in self.item_captions:
|
||||
ui.append(cap)
|
||||
|
||||
def handle_key(self, key, ui):
|
||||
"""Handle keyboard navigation."""
|
||||
if not self.dropdown_open:
|
||||
if key == "Left":
|
||||
self.selected_item = (self.selected_item - 1) % len(self.items)
|
||||
self._update_highlight()
|
||||
elif key == "Right":
|
||||
self.selected_item = (self.selected_item + 1) % len(self.items)
|
||||
self._update_highlight()
|
||||
elif key == "Return" or key == "Down":
|
||||
self.dropdown_open = True
|
||||
self._show_dropdown(ui)
|
||||
self._update_highlight()
|
||||
else:
|
||||
if key == "Up":
|
||||
options = self.items[self.selected_item].get('options', [])
|
||||
self.dropdown_selected = (self.dropdown_selected - 1) % len(options)
|
||||
self._update_dropdown_highlight()
|
||||
elif key == "Down":
|
||||
options = self.items[self.selected_item].get('options', [])
|
||||
self.dropdown_selected = (self.dropdown_selected + 1) % len(options)
|
||||
self._update_dropdown_highlight()
|
||||
elif key == "Return":
|
||||
opt = self.items[self.selected_item]['options'][self.dropdown_selected]
|
||||
if opt.get('action'):
|
||||
opt['action']()
|
||||
self.dropdown_open = False
|
||||
self._hide_dropdown(ui)
|
||||
self._update_highlight()
|
||||
elif key == "Escape":
|
||||
self.dropdown_open = False
|
||||
self._hide_dropdown(ui)
|
||||
self._update_highlight()
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
"""McRogueFace - Message Log Widget (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_message_log
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_message_log_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Initialize
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
|
||||
# Create log at bottom of screen
|
||||
log = EnhancedMessageLog(10, 500, 700, 250, line_height=20)
|
||||
ui.append(log.frame)
|
||||
|
||||
# Simulate game events
|
||||
def simulate_combat(dt):
|
||||
import random
|
||||
events = [
|
||||
("You swing your sword!", "combat"),
|
||||
("The orc dodges!", "combat"),
|
||||
("Critical hit!", "combat"),
|
||||
("You found a potion!", "loot"),
|
||||
]
|
||||
event = random.choice(events)
|
||||
log.add(event[0], event[1])
|
||||
|
||||
# Add messages every 2 seconds for demo
|
||||
mcrfpy.setTimer("combat_sim", simulate_combat, 2000)
|
||||
|
||||
# Keyboard controls
|
||||
def on_key(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
if key == "PageUp":
|
||||
log.scroll_up(3)
|
||||
elif key == "PageDown":
|
||||
log.scroll_down(3)
|
||||
elif key == "C":
|
||||
log.set_filter('combat')
|
||||
elif key == "L":
|
||||
log.set_filter('loot')
|
||||
elif key == "A":
|
||||
log.set_filter(None) # All
|
||||
|
||||
mcrfpy.keypressScene(on_key)
|
||||
|
||||
log.system("Press PageUp/PageDown to scroll")
|
||||
log.system("Press C for combat, L for loot, A for all")
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
"""McRogueFace - Message Log Widget (enhanced)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_message_log
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_message_log_enhanced.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def handle_keys(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if key == "PageUp":
|
||||
log.scroll_up(5)
|
||||
elif key == "PageDown":
|
||||
log.scroll_down(5)
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Or with mouse scroll on the frame
|
||||
def on_log_scroll(x, y, button, action):
|
||||
# Note: You may need to implement scroll detection
|
||||
# based on your input system
|
||||
pass
|
||||
|
||||
log.frame.click = on_log_scroll
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
"""McRogueFace - Modal Dialog Widget (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_modal_dialog
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_modal_dialog_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Scene setup
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
|
||||
# Game background
|
||||
bg = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
bg.fill_color = mcrfpy.Color(25, 35, 45)
|
||||
ui.append(bg)
|
||||
|
||||
title = mcrfpy.Caption("My Game", mcrfpy.default_font, 450, 50)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Quit button
|
||||
quit_btn = mcrfpy.Frame(430, 400, 160, 50)
|
||||
quit_btn.fill_color = mcrfpy.Color(150, 50, 50)
|
||||
quit_btn.outline = 2
|
||||
quit_btn.outline_color = mcrfpy.Color(200, 100, 100)
|
||||
ui.append(quit_btn)
|
||||
|
||||
quit_label = mcrfpy.Caption("Quit Game", mcrfpy.default_font, 460, 415)
|
||||
quit_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(quit_label)
|
||||
|
||||
# Confirmation dialog
|
||||
confirm_dialog = None
|
||||
|
||||
def show_quit_confirm():
|
||||
global confirm_dialog
|
||||
|
||||
def on_response(index, label):
|
||||
if label == "Yes":
|
||||
mcrfpy.exit()
|
||||
|
||||
confirm_dialog = EnhancedDialog(
|
||||
"Quit Game?",
|
||||
"Are you sure you want to quit?\nUnsaved progress will be lost.",
|
||||
["Yes", "No"],
|
||||
DialogStyle.WARNING,
|
||||
on_response
|
||||
)
|
||||
confirm_dialog.add_to_scene(ui)
|
||||
confirm_dialog.show()
|
||||
|
||||
quit_btn.click = lambda x, y, b, a: show_quit_confirm() if a == "start" else None
|
||||
|
||||
def on_key(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if confirm_dialog and confirm_dialog.handle_key(key):
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
show_quit_confirm()
|
||||
|
||||
mcrfpy.keypressScene(on_key)
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
"""McRogueFace - Modal Dialog Widget (enhanced)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_modal_dialog
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_modal_dialog_enhanced.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class DialogManager:
|
||||
"""Manages a queue of dialogs."""
|
||||
|
||||
def __init__(self, ui):
|
||||
self.ui = ui
|
||||
self.queue = []
|
||||
self.current = None
|
||||
|
||||
def show(self, title, message, buttons=None, style=None, callback=None):
|
||||
"""
|
||||
Queue a dialog to show.
|
||||
|
||||
If no dialog is active, shows immediately.
|
||||
Otherwise, queues for later.
|
||||
"""
|
||||
dialog_data = {
|
||||
'title': title,
|
||||
'message': message,
|
||||
'buttons': buttons or ["OK"],
|
||||
'style': style or DialogStyle.INFO,
|
||||
'callback': callback
|
||||
}
|
||||
|
||||
if self.current is None:
|
||||
self._show_dialog(dialog_data)
|
||||
else:
|
||||
self.queue.append(dialog_data)
|
||||
|
||||
def _show_dialog(self, data):
|
||||
"""Actually display a dialog."""
|
||||
def on_close(index, label):
|
||||
if data['callback']:
|
||||
data['callback'](index, label)
|
||||
self._on_dialog_closed()
|
||||
|
||||
self.current = EnhancedDialog(
|
||||
data['title'],
|
||||
data['message'],
|
||||
data['buttons'],
|
||||
data['style'],
|
||||
on_close
|
||||
)
|
||||
self.current.add_to_scene(self.ui)
|
||||
self.current.show()
|
||||
|
||||
def _on_dialog_closed(self):
|
||||
"""Handle dialog close, show next if queued."""
|
||||
self.current = None
|
||||
|
||||
if self.queue:
|
||||
next_dialog = self.queue.pop(0)
|
||||
self._show_dialog(next_dialog)
|
||||
|
||||
def handle_key(self, key):
|
||||
"""Forward key events to current dialog."""
|
||||
if self.current:
|
||||
return self.current.handle_key(key)
|
||||
return False
|
||||
|
||||
|
||||
# Usage
|
||||
manager = DialogManager(ui)
|
||||
|
||||
# Queue multiple dialogs
|
||||
manager.show("First", "This is the first message")
|
||||
manager.show("Second", "This appears after closing the first")
|
||||
manager.show("Third", "And this is last", ["Done"])
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
"""McRogueFace - Tooltip on Hover (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_tooltip
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_tooltip_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
|
||||
# Background
|
||||
bg = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
bg.fill_color = mcrfpy.Color(25, 25, 35)
|
||||
ui.append(bg)
|
||||
|
||||
# Create inventory slots with tooltips
|
||||
class InventorySlot:
|
||||
def __init__(self, x, y, item_name, item_desc, tooltip_mgr):
|
||||
self.frame = mcrfpy.Frame(x, y, 50, 50)
|
||||
self.frame.fill_color = mcrfpy.Color(50, 50, 60)
|
||||
self.frame.outline = 1
|
||||
self.frame.outline_color = mcrfpy.Color(80, 80, 90)
|
||||
|
||||
self.label = mcrfpy.Caption(item_name[:3], mcrfpy.default_font, x + 10, y + 15)
|
||||
self.label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
tooltip_mgr.register(self.frame, item_desc, title=item_name)
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
ui.append(self.frame)
|
||||
ui.append(self.label)
|
||||
|
||||
# Setup tooltip manager
|
||||
tips = TooltipManager()
|
||||
tips.hover_delay = 300
|
||||
|
||||
# Create inventory
|
||||
items = [
|
||||
("Health Potion", "Restores 50 HP\nConsumable"),
|
||||
("Mana Crystal", "Restores 30 MP\nConsumable"),
|
||||
("Iron Key", "Opens iron doors\nQuest Item"),
|
||||
("Gold Ring", "Worth 100 gold\nSell to merchant"),
|
||||
]
|
||||
|
||||
slots = []
|
||||
for i, (name, desc) in enumerate(items):
|
||||
slot = InventorySlot(100 + i * 60, 100, name, desc, tips)
|
||||
slot.add_to_scene(ui)
|
||||
slots.append(slot)
|
||||
|
||||
# Add tooltip last
|
||||
tips.add_to_scene(ui)
|
||||
|
||||
# Update loop
|
||||
def update(dt):
|
||||
from mcrfpy import automation
|
||||
x, y = automation.position()
|
||||
tips.update(x, y)
|
||||
|
||||
mcrfpy.setTimer("update", update, 50)
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
"""McRogueFace - Tooltip on Hover (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_tooltip
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_tooltip_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 create_info_icon(x, y, tooltip_text, ui):
|
||||
"""
|
||||
Create an info icon that shows tooltip on hover.
|
||||
|
||||
Args:
|
||||
x, y: Position of the icon
|
||||
tooltip_text: Text to show
|
||||
ui: Scene UI to add elements to
|
||||
"""
|
||||
# Info icon (small circle with "i")
|
||||
icon = mcrfpy.Frame(x, y, 20, 20)
|
||||
icon.fill_color = mcrfpy.Color(70, 130, 180)
|
||||
icon.outline = 1
|
||||
icon.outline_color = mcrfpy.Color(100, 160, 210)
|
||||
|
||||
icon_label = mcrfpy.Caption("i", mcrfpy.default_font, x + 6, y + 2)
|
||||
icon_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
|
||||
# Tooltip (positioned to the right of icon)
|
||||
tip_frame = mcrfpy.Frame(x + 25, y - 5, 180, 50)
|
||||
tip_frame.fill_color = mcrfpy.Color(40, 40, 55, 240)
|
||||
tip_frame.outline = 1
|
||||
tip_frame.outline_color = mcrfpy.Color(80, 80, 100)
|
||||
tip_frame.visible = False
|
||||
|
||||
tip_text = mcrfpy.Caption(tooltip_text, mcrfpy.default_font, x + 33, y + 3)
|
||||
tip_text.fill_color = mcrfpy.Color(220, 220, 220)
|
||||
tip_text.visible = False
|
||||
|
||||
# Hover behavior
|
||||
def on_icon_hover(mx, my, button, action):
|
||||
tip_frame.visible = True
|
||||
tip_text.visible = True
|
||||
|
||||
icon.click = on_icon_hover
|
||||
|
||||
# Track when to hide
|
||||
def check_hover(dt):
|
||||
from mcrfpy import automation
|
||||
mx, my = automation.position()
|
||||
if not (icon.x <= mx <= icon.x + icon.w and
|
||||
icon.y <= my <= icon.y + icon.h):
|
||||
if tip_frame.visible:
|
||||
tip_frame.visible = False
|
||||
tip_text.visible = False
|
||||
|
||||
timer_name = f"info_hover_{id(icon)}"
|
||||
mcrfpy.setTimer(timer_name, check_hover, 100)
|
||||
|
||||
# Add to scene
|
||||
ui.append(icon)
|
||||
ui.append(icon_label)
|
||||
ui.append(tip_frame)
|
||||
ui.append(tip_text)
|
||||
|
||||
return icon
|
||||
|
||||
|
||||
# Usage
|
||||
mcrfpy.createScene("info_demo")
|
||||
mcrfpy.setScene("info_demo")
|
||||
ui = mcrfpy.sceneUI("info_demo")
|
||||
|
||||
# Setting with info icon
|
||||
setting_label = mcrfpy.Caption("Difficulty:", mcrfpy.default_font, 100, 100)
|
||||
setting_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
ui.append(setting_label)
|
||||
|
||||
create_info_icon(200, 98, "Affects enemy\nHP and damage", ui)
|
||||
1528
docs/mcrfpy.3
|
|
@ -1,532 +0,0 @@
|
|||
"""Type stubs for McRogueFace Python API.
|
||||
|
||||
Core game engine interface for creating roguelike games with Python.
|
||||
"""
|
||||
|
||||
from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
|
||||
|
||||
# Type aliases
|
||||
UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid']
|
||||
Transition = Union[str, None]
|
||||
|
||||
# Classes
|
||||
|
||||
class Color:
|
||||
"""SFML Color Object for RGBA colors."""
|
||||
|
||||
r: int
|
||||
g: int
|
||||
b: int
|
||||
a: int
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ...
|
||||
|
||||
def from_hex(self, hex_string: str) -> 'Color':
|
||||
"""Create color from hex string (e.g., '#FF0000' or 'FF0000')."""
|
||||
...
|
||||
|
||||
def to_hex(self) -> str:
|
||||
"""Convert color to hex string format."""
|
||||
...
|
||||
|
||||
def lerp(self, other: 'Color', t: float) -> 'Color':
|
||||
"""Linear interpolation between two colors."""
|
||||
...
|
||||
|
||||
class Vector:
|
||||
"""SFML Vector Object for 2D coordinates."""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float, y: float) -> None: ...
|
||||
|
||||
def add(self, other: 'Vector') -> 'Vector': ...
|
||||
def subtract(self, other: 'Vector') -> 'Vector': ...
|
||||
def multiply(self, scalar: float) -> 'Vector': ...
|
||||
def divide(self, scalar: float) -> 'Vector': ...
|
||||
def distance(self, other: 'Vector') -> float: ...
|
||||
def normalize(self) -> 'Vector': ...
|
||||
def dot(self, other: 'Vector') -> float: ...
|
||||
|
||||
class Texture:
|
||||
"""SFML Texture Object for images."""
|
||||
|
||||
def __init__(self, filename: str) -> None: ...
|
||||
|
||||
filename: str
|
||||
width: int
|
||||
height: int
|
||||
sprite_count: int
|
||||
|
||||
class Font:
|
||||
"""SFML Font Object for text rendering."""
|
||||
|
||||
def __init__(self, filename: str) -> None: ...
|
||||
|
||||
filename: str
|
||||
family: str
|
||||
|
||||
class Drawable:
|
||||
"""Base class for all drawable UI elements."""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
visible: bool
|
||||
z_index: int
|
||||
name: str
|
||||
pos: Vector
|
||||
|
||||
def get_bounds(self) -> Tuple[float, float, float, float]:
|
||||
"""Get bounding box as (x, y, width, height)."""
|
||||
...
|
||||
|
||||
def move(self, dx: float, dy: float) -> None:
|
||||
"""Move by relative offset (dx, dy)."""
|
||||
...
|
||||
|
||||
def resize(self, width: float, height: float) -> None:
|
||||
"""Resize to new dimensions (width, height)."""
|
||||
...
|
||||
|
||||
class Frame(Drawable):
|
||||
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)
|
||||
|
||||
A rectangular frame UI element that can contain other drawable elements.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0,
|
||||
fill_color: Optional[Color] = None, outline_color: Optional[Color] = None,
|
||||
outline: float = 0, click: Optional[Callable] = None,
|
||||
children: Optional[List[UIElement]] = None) -> None: ...
|
||||
|
||||
w: float
|
||||
h: float
|
||||
fill_color: Color
|
||||
outline_color: Color
|
||||
outline: float
|
||||
click: Optional[Callable[[float, float, int], None]]
|
||||
children: 'UICollection'
|
||||
clip_children: bool
|
||||
|
||||
class Caption(Drawable):
|
||||
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)
|
||||
|
||||
A text display UI element with customizable font and styling.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, text: str = '', x: float = 0, y: float = 0,
|
||||
font: Optional[Font] = None, fill_color: Optional[Color] = None,
|
||||
outline_color: Optional[Color] = None, outline: float = 0,
|
||||
click: Optional[Callable] = None) -> None: ...
|
||||
|
||||
text: str
|
||||
font: Font
|
||||
fill_color: Color
|
||||
outline_color: Color
|
||||
outline: float
|
||||
click: Optional[Callable[[float, float, int], None]]
|
||||
w: float # Read-only, computed from text
|
||||
h: float # Read-only, computed from text
|
||||
|
||||
class Sprite(Drawable):
|
||||
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)
|
||||
|
||||
A sprite UI element that displays a texture or portion of a texture atlas.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None,
|
||||
sprite_index: int = 0, scale: float = 1.0,
|
||||
click: Optional[Callable] = None) -> None: ...
|
||||
|
||||
texture: Texture
|
||||
sprite_index: int
|
||||
scale: float
|
||||
click: Optional[Callable[[float, float, int], None]]
|
||||
w: float # Read-only, computed from texture
|
||||
h: float # Read-only, computed from texture
|
||||
|
||||
class Grid(Drawable):
|
||||
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)
|
||||
|
||||
A grid-based tilemap UI element for rendering tile-based levels and game worlds.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20),
|
||||
texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16,
|
||||
scale: float = 1.0, click: Optional[Callable] = None) -> None: ...
|
||||
|
||||
grid_size: Tuple[int, int]
|
||||
tile_width: int
|
||||
tile_height: int
|
||||
texture: Texture
|
||||
scale: float
|
||||
points: List[List['GridPoint']]
|
||||
entities: 'EntityCollection'
|
||||
background_color: Color
|
||||
click: Optional[Callable[[int, int, int], None]]
|
||||
|
||||
def at(self, x: int, y: int) -> 'GridPoint':
|
||||
"""Get grid point at tile coordinates."""
|
||||
...
|
||||
|
||||
class GridPoint:
|
||||
"""Grid point representing a single tile."""
|
||||
|
||||
texture_index: int
|
||||
solid: bool
|
||||
color: Color
|
||||
|
||||
class GridPointState:
|
||||
"""State information for a grid point."""
|
||||
|
||||
texture_index: int
|
||||
color: Color
|
||||
|
||||
class Entity(Drawable):
|
||||
"""Entity(grid_x=0, grid_y=0, texture=None, sprite_index=0, name='')
|
||||
|
||||
Game entity that lives within a Grid.
|
||||
"""
|
||||
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
@overload
|
||||
def __init__(self, grid_x: float = 0, grid_y: float = 0, texture: Optional[Texture] = None,
|
||||
sprite_index: int = 0, name: str = '') -> None: ...
|
||||
|
||||
grid_x: float
|
||||
grid_y: float
|
||||
texture: Texture
|
||||
sprite_index: int
|
||||
grid: Optional[Grid]
|
||||
|
||||
def at(self, grid_x: float, grid_y: float) -> None:
|
||||
"""Move entity to grid position."""
|
||||
...
|
||||
|
||||
def die(self) -> None:
|
||||
"""Remove entity from its grid."""
|
||||
...
|
||||
|
||||
def index(self) -> int:
|
||||
"""Get index in parent grid's entity collection."""
|
||||
...
|
||||
|
||||
class UICollection:
|
||||
"""Collection of UI drawable elements (Frame, Caption, Sprite, Grid)."""
|
||||
|
||||
def __len__(self) -> int: ...
|
||||
def __getitem__(self, index: int) -> UIElement: ...
|
||||
def __setitem__(self, index: int, value: UIElement) -> None: ...
|
||||
def __delitem__(self, index: int) -> None: ...
|
||||
def __contains__(self, item: UIElement) -> bool: ...
|
||||
def __iter__(self) -> Any: ...
|
||||
def __add__(self, other: 'UICollection') -> 'UICollection': ...
|
||||
def __iadd__(self, other: 'UICollection') -> 'UICollection': ...
|
||||
|
||||
def append(self, item: UIElement) -> None: ...
|
||||
def extend(self, items: List[UIElement]) -> None: ...
|
||||
def remove(self, item: UIElement) -> None: ...
|
||||
def index(self, item: UIElement) -> int: ...
|
||||
def count(self, item: UIElement) -> int: ...
|
||||
|
||||
class EntityCollection:
|
||||
"""Collection of Entity objects."""
|
||||
|
||||
def __len__(self) -> int: ...
|
||||
def __getitem__(self, index: int) -> Entity: ...
|
||||
def __setitem__(self, index: int, value: Entity) -> None: ...
|
||||
def __delitem__(self, index: int) -> None: ...
|
||||
def __contains__(self, item: Entity) -> bool: ...
|
||||
def __iter__(self) -> Any: ...
|
||||
def __add__(self, other: 'EntityCollection') -> 'EntityCollection': ...
|
||||
def __iadd__(self, other: 'EntityCollection') -> 'EntityCollection': ...
|
||||
|
||||
def append(self, item: Entity) -> None: ...
|
||||
def extend(self, items: List[Entity]) -> None: ...
|
||||
def remove(self, item: Entity) -> None: ...
|
||||
def index(self, item: Entity) -> int: ...
|
||||
def count(self, item: Entity) -> int: ...
|
||||
|
||||
class Scene:
|
||||
"""Base class for object-oriented scenes."""
|
||||
|
||||
name: str
|
||||
|
||||
def __init__(self, name: str) -> None: ...
|
||||
|
||||
def activate(self) -> None:
|
||||
"""Called when scene becomes active."""
|
||||
...
|
||||
|
||||
def deactivate(self) -> None:
|
||||
"""Called when scene becomes inactive."""
|
||||
...
|
||||
|
||||
def get_ui(self) -> UICollection:
|
||||
"""Get UI elements collection."""
|
||||
...
|
||||
|
||||
def on_keypress(self, key: str, pressed: bool) -> None:
|
||||
"""Handle keyboard events."""
|
||||
...
|
||||
|
||||
def on_click(self, x: float, y: float, button: int) -> None:
|
||||
"""Handle mouse clicks."""
|
||||
...
|
||||
|
||||
def on_enter(self) -> None:
|
||||
"""Called when entering the scene."""
|
||||
...
|
||||
|
||||
def on_exit(self) -> None:
|
||||
"""Called when leaving the scene."""
|
||||
...
|
||||
|
||||
def on_resize(self, width: int, height: int) -> None:
|
||||
"""Handle window resize events."""
|
||||
...
|
||||
|
||||
def update(self, dt: float) -> None:
|
||||
"""Update scene logic."""
|
||||
...
|
||||
|
||||
class Timer:
|
||||
"""Timer object for scheduled callbacks."""
|
||||
|
||||
name: str
|
||||
interval: int
|
||||
active: bool
|
||||
|
||||
def __init__(self, name: str, callback: Callable[[float], None], interval: int) -> None: ...
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pause the timer."""
|
||||
...
|
||||
|
||||
def resume(self) -> None:
|
||||
"""Resume the timer."""
|
||||
...
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""Cancel and remove the timer."""
|
||||
...
|
||||
|
||||
class Window:
|
||||
"""Window singleton for managing the game window."""
|
||||
|
||||
resolution: Tuple[int, int]
|
||||
fullscreen: bool
|
||||
vsync: bool
|
||||
title: str
|
||||
fps_limit: int
|
||||
game_resolution: Tuple[int, int]
|
||||
scaling_mode: str
|
||||
|
||||
@staticmethod
|
||||
def get() -> 'Window':
|
||||
"""Get the window singleton instance."""
|
||||
...
|
||||
|
||||
class Animation:
|
||||
"""Animation object for animating UI properties."""
|
||||
|
||||
target: Any
|
||||
property: str
|
||||
duration: float
|
||||
easing: str
|
||||
loop: bool
|
||||
on_complete: Optional[Callable]
|
||||
|
||||
def __init__(self, target: Any, property: str, start_value: Any, end_value: Any,
|
||||
duration: float, easing: str = 'linear', loop: bool = False,
|
||||
on_complete: Optional[Callable] = None) -> None: ...
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the animation."""
|
||||
...
|
||||
|
||||
def update(self, dt: float) -> bool:
|
||||
"""Update animation, returns True if still running."""
|
||||
...
|
||||
|
||||
def get_current_value(self) -> Any:
|
||||
"""Get the current interpolated value."""
|
||||
...
|
||||
|
||||
# Module functions
|
||||
|
||||
def createSoundBuffer(filename: str) -> int:
|
||||
"""Load a sound effect from a file and return its buffer ID."""
|
||||
...
|
||||
|
||||
def loadMusic(filename: str) -> None:
|
||||
"""Load and immediately play background music from a file."""
|
||||
...
|
||||
|
||||
def setMusicVolume(volume: int) -> None:
|
||||
"""Set the global music volume (0-100)."""
|
||||
...
|
||||
|
||||
def setSoundVolume(volume: int) -> None:
|
||||
"""Set the global sound effects volume (0-100)."""
|
||||
...
|
||||
|
||||
def playSound(buffer_id: int) -> None:
|
||||
"""Play a sound effect using a previously loaded buffer."""
|
||||
...
|
||||
|
||||
def getMusicVolume() -> int:
|
||||
"""Get the current music volume level (0-100)."""
|
||||
...
|
||||
|
||||
def getSoundVolume() -> int:
|
||||
"""Get the current sound effects volume level (0-100)."""
|
||||
...
|
||||
|
||||
def sceneUI(scene: Optional[str] = None) -> UICollection:
|
||||
"""Get all UI elements for a scene."""
|
||||
...
|
||||
|
||||
def currentScene() -> str:
|
||||
"""Get the name of the currently active scene."""
|
||||
...
|
||||
|
||||
def setScene(scene: str, transition: Optional[str] = None, duration: float = 0.0) -> None:
|
||||
"""Switch to a different scene with optional transition effect."""
|
||||
...
|
||||
|
||||
def createScene(name: str) -> None:
|
||||
"""Create a new empty scene."""
|
||||
...
|
||||
|
||||
def keypressScene(handler: Callable[[str, bool], None]) -> None:
|
||||
"""Set the keyboard event handler for the current scene."""
|
||||
...
|
||||
|
||||
def setTimer(name: str, handler: Callable[[float], None], interval: int) -> None:
|
||||
"""Create or update a recurring timer."""
|
||||
...
|
||||
|
||||
def delTimer(name: str) -> None:
|
||||
"""Stop and remove a timer."""
|
||||
...
|
||||
|
||||
def exit() -> None:
|
||||
"""Cleanly shut down the game engine and exit the application."""
|
||||
...
|
||||
|
||||
def setScale(multiplier: float) -> None:
|
||||
"""Scale the game window size (deprecated - use Window.resolution)."""
|
||||
...
|
||||
|
||||
def find(name: str, scene: Optional[str] = None) -> Optional[UIElement]:
|
||||
"""Find the first UI element with the specified name."""
|
||||
...
|
||||
|
||||
def findAll(pattern: str, scene: Optional[str] = None) -> List[UIElement]:
|
||||
"""Find all UI elements matching a name pattern (supports * wildcards)."""
|
||||
...
|
||||
|
||||
def getMetrics() -> Dict[str, Union[int, float]]:
|
||||
"""Get current performance metrics."""
|
||||
...
|
||||
|
||||
# Submodule
|
||||
class automation:
|
||||
"""Automation API for testing and scripting."""
|
||||
|
||||
@staticmethod
|
||||
def screenshot(filename: str) -> bool:
|
||||
"""Save a screenshot to the specified file."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def position() -> Tuple[int, int]:
|
||||
"""Get current mouse position as (x, y) tuple."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def size() -> Tuple[int, int]:
|
||||
"""Get screen size as (width, height) tuple."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def onScreen(x: int, y: int) -> bool:
|
||||
"""Check if coordinates are within screen bounds."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def moveTo(x: int, y: int, duration: float = 0.0) -> None:
|
||||
"""Move mouse to absolute position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def moveRel(xOffset: int, yOffset: int, duration: float = 0.0) -> None:
|
||||
"""Move mouse relative to current position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def dragTo(x: int, y: int, duration: float = 0.0, button: str = 'left') -> None:
|
||||
"""Drag mouse to position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def dragRel(xOffset: int, yOffset: int, duration: float = 0.0, button: str = 'left') -> None:
|
||||
"""Drag mouse relative to current position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def click(x: Optional[int] = None, y: Optional[int] = None, clicks: int = 1,
|
||||
interval: float = 0.0, button: str = 'left') -> None:
|
||||
"""Click mouse at position."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def mouseDown(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
|
||||
"""Press mouse button down."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def mouseUp(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
|
||||
"""Release mouse button."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def keyDown(key: str) -> None:
|
||||
"""Press key down."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def keyUp(key: str) -> None:
|
||||
"""Release key."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def press(key: str) -> None:
|
||||
"""Press and release a key."""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def typewrite(text: str, interval: float = 0.0) -> None:
|
||||
"""Type text with optional interval between characters."""
|
||||
...
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
"""Type stubs for McRogueFace Python API.
|
||||
|
||||
Auto-generated - do not edit directly.
|
||||
"""
|
||||
|
||||
from typing import Any, List, Dict, Tuple, Optional, Callable, Union
|
||||
|
||||
# Module documentation
|
||||
# McRogueFace Python API\n\nCore game engine interface for creating roguelike games with Python.\n\nThis module provides:\n- Scene management (createScene, setScene, currentScene)\n- UI components (Frame, Caption, Sprite, Grid)\n- Entity system for game objects\n- Audio playback (sound effects and music)\n- Timer system for scheduled events\n- Input handling\n- Performance metrics\n\nExample:\n import mcrfpy\n \n # Create a new scene\n mcrfpy.createScene('game')\n mcrfpy.setScene('game')\n \n # Add UI elements\n frame = mcrfpy.Frame(10, 10, 200, 100)\n caption = mcrfpy.Caption('Hello World', 50, 50)\n mcrfpy.sceneUI().extend([frame, caption])\n
|
||||
|
||||
# Classes
|
||||
|
||||
class Animation:
|
||||
"""Animation object for animating UI properties"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_current_value(self, *args, **kwargs) -> Any: ...
|
||||
def start(self, *args, **kwargs) -> Any: ...
|
||||
def update(selfreturns True if still running) -> Any: ...
|
||||
|
||||
class Caption:
|
||||
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Color:
|
||||
"""SFML Color Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def from_hex(selfe.g., '#FF0000' or 'FF0000') -> Any: ...
|
||||
def lerp(self, *args, **kwargs) -> Any: ...
|
||||
def to_hex(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class Drawable:
|
||||
"""Base class for all drawable UI elements"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Entity:
|
||||
"""UIEntity objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def at(self, *args, **kwargs) -> Any: ...
|
||||
def die(self, *args, **kwargs) -> Any: ...
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def index(self, *args, **kwargs) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def path_to(selfx: int, y: int) -> bool: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
def update_visibility(self) -> None: ...
|
||||
|
||||
class EntityCollection:
|
||||
"""Iterable, indexable collection of Entities"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def append(self, *args, **kwargs) -> Any: ...
|
||||
def count(self, *args, **kwargs) -> Any: ...
|
||||
def extend(self, *args, **kwargs) -> Any: ...
|
||||
def index(self, *args, **kwargs) -> Any: ...
|
||||
def remove(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class Font:
|
||||
"""SFML Font Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Frame:
|
||||
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Grid:
|
||||
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def at(self, *args, **kwargs) -> Any: ...
|
||||
def compute_astar_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
|
||||
def compute_dijkstra(selfroot_x: int, root_y: int, diagonal_cost: float = 1.41) -> None: ...
|
||||
def compute_fov(selfx: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None: ...
|
||||
def find_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def get_dijkstra_distance(selfx: int, y: int) -> Optional[float]: ...
|
||||
def get_dijkstra_path(selfx: int, y: int) -> List[Tuple[int, int]]: ...
|
||||
def is_in_fov(selfx: int, y: int) -> bool: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class GridPoint:
|
||||
"""UIGridPoint object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class GridPointState:
|
||||
"""UIGridPointState object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Scene:
|
||||
"""Base class for object-oriented scenes"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def activate(self, *args, **kwargs) -> Any: ...
|
||||
def get_ui(self, *args, **kwargs) -> Any: ...
|
||||
def register_keyboard(selfalternative to overriding on_keypress) -> Any: ...
|
||||
|
||||
class Sprite:
|
||||
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def get_bounds(selfx, y, width, height) -> Any: ...
|
||||
def move(selfdx, dy) -> Any: ...
|
||||
def resize(selfwidth, height) -> Any: ...
|
||||
|
||||
class Texture:
|
||||
"""SFML Texture Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Timer:
|
||||
"""Timer object for scheduled callbacks"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def cancel(self, *args, **kwargs) -> Any: ...
|
||||
def pause(self, *args, **kwargs) -> Any: ...
|
||||
def restart(self, *args, **kwargs) -> Any: ...
|
||||
def resume(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class UICollection:
|
||||
"""Iterable, indexable collection of UI objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def append(self, *args, **kwargs) -> Any: ...
|
||||
def count(self, *args, **kwargs) -> Any: ...
|
||||
def extend(self, *args, **kwargs) -> Any: ...
|
||||
def index(self, *args, **kwargs) -> Any: ...
|
||||
def remove(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class UICollectionIter:
|
||||
"""Iterator for a collection of UI objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class UIEntityCollectionIter:
|
||||
"""Iterator for a collection of UI objects"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
class Vector:
|
||||
"""SFML Vector Object"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def angle(self, *args, **kwargs) -> Any: ...
|
||||
def copy(self, *args, **kwargs) -> Any: ...
|
||||
def distance_to(self, *args, **kwargs) -> Any: ...
|
||||
def dot(self, *args, **kwargs) -> Any: ...
|
||||
def magnitude(self, *args, **kwargs) -> Any: ...
|
||||
def magnitude_squared(self, *args, **kwargs) -> Any: ...
|
||||
def normalize(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
class Window:
|
||||
"""Window singleton for accessing and modifying the game window properties"""
|
||||
def __init__(selftype(self)) -> None: ...
|
||||
|
||||
def center(self, *args, **kwargs) -> Any: ...
|
||||
def get(self, *args, **kwargs) -> Any: ...
|
||||
def screenshot(self, *args, **kwargs) -> Any: ...
|
||||
|
||||
# Functions
|
||||
|
||||
def createScene(name: str) -> None: ...
|
||||
def createSoundBuffer(filename: str) -> int: ...
|
||||
def currentScene() -> str: ...
|
||||
def delTimer(name: str) -> None: ...
|
||||
def exit() -> None: ...
|
||||
def find(name: str, scene: str = None) -> UIDrawable | None: ...
|
||||
def findAll(pattern: str, scene: str = None) -> list: ...
|
||||
def getMetrics() -> dict: ...
|
||||
def getMusicVolume() -> int: ...
|
||||
def getSoundVolume() -> int: ...
|
||||
def keypressScene(handler: callable) -> None: ...
|
||||
def loadMusic(filename: str) -> None: ...
|
||||
def playSound(buffer_id: int) -> None: ...
|
||||
def sceneUI(scene: str = None) -> list: ...
|
||||
def setMusicVolume(volume: int) -> None: ...
|
||||
def setScale(multiplier: float) -> None: ...
|
||||
def setScene(scene: str, transition: str = None, duration: float = 0.0) -> None: ...
|
||||
def setSoundVolume(volume: int) -> None: ...
|
||||
def setTimer(name: str, handler: callable, interval: int) -> None: ...
|
||||
|
||||
# Constants
|
||||
|
||||
FOV_BASIC: int
|
||||
FOV_DIAMOND: int
|
||||
FOV_PERMISSIVE_0: int
|
||||
FOV_PERMISSIVE_1: int
|
||||
FOV_PERMISSIVE_2: int
|
||||
FOV_PERMISSIVE_3: int
|
||||
FOV_PERMISSIVE_4: int
|
||||
FOV_PERMISSIVE_5: int
|
||||
FOV_PERMISSIVE_6: int
|
||||
FOV_PERMISSIVE_7: int
|
||||
FOV_PERMISSIVE_8: int
|
||||
FOV_RESTRICTIVE: int
|
||||
FOV_SHADOW: int
|
||||
default_font: Any
|
||||
default_texture: Any
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
"""Type stubs for McRogueFace automation API."""
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
def click(x=None, y=None, clicks=1, interval=0.0, button='left') -> Any: ...
|
||||
def doubleClick(x=None, y=None) -> Any: ...
|
||||
def dragRel(xOffset, yOffset, duration=0.0, button='left') -> Any: ...
|
||||
def dragTo(x, y, duration=0.0, button='left') -> Any: ...
|
||||
def hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c')) -> Any: ...
|
||||
def keyDown(key) -> Any: ...
|
||||
def keyUp(key) -> Any: ...
|
||||
def middleClick(x=None, y=None) -> Any: ...
|
||||
def mouseDown(x=None, y=None, button='left') -> Any: ...
|
||||
def mouseUp(x=None, y=None, button='left') -> Any: ...
|
||||
def moveRel(xOffset, yOffset, duration=0.0) -> Any: ...
|
||||
def moveTo(x, y, duration=0.0) -> Any: ...
|
||||
def onScreen(x, y) -> Any: ...
|
||||
def position() - Get current mouse position as (x, y) -> Any: ...
|
||||
def rightClick(x=None, y=None) -> Any: ...
|
||||
def screenshot(filename) -> Any: ...
|
||||
def scroll(clicks, x=None, y=None) -> Any: ...
|
||||
def size() - Get screen size as (width, height) -> Any: ...
|
||||
def tripleClick(x=None, y=None) -> Any: ...
|
||||
def typewrite(message, interval=0.0) -> Any: ...
|
||||
289
docs/templates/complete/ai.py
vendored
|
|
@ -1,289 +0,0 @@
|
|||
"""
|
||||
ai.py - Enemy AI System for McRogueFace Roguelike
|
||||
|
||||
Simple AI behaviors for enemies: chase player when visible, wander otherwise.
|
||||
Uses A* pathfinding via entity.path_to() for movement.
|
||||
"""
|
||||
|
||||
from typing import List, Tuple, Optional, TYPE_CHECKING
|
||||
import random
|
||||
|
||||
from entities import Enemy, Player, Actor
|
||||
from combat import melee_attack, CombatResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dungeon import Dungeon
|
||||
|
||||
|
||||
class AIBehavior:
|
||||
"""Base class for AI behaviors."""
|
||||
|
||||
def take_turn(self, enemy: Enemy, player: Player, dungeon: 'Dungeon',
|
||||
enemies: List[Enemy]) -> Optional[CombatResult]:
|
||||
"""
|
||||
Execute one turn of AI behavior.
|
||||
|
||||
Args:
|
||||
enemy: The enemy taking a turn
|
||||
player: The player to potentially chase/attack
|
||||
dungeon: The dungeon map
|
||||
enemies: List of all enemies (for collision avoidance)
|
||||
|
||||
Returns:
|
||||
CombatResult if combat occurred, None otherwise
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BasicChaseAI(AIBehavior):
|
||||
"""
|
||||
Simple chase AI: If player is visible, move toward them.
|
||||
If adjacent, attack. Otherwise, stand still or wander.
|
||||
"""
|
||||
|
||||
def __init__(self, sight_range: int = 8):
|
||||
"""
|
||||
Args:
|
||||
sight_range: How far the enemy can see
|
||||
"""
|
||||
self.sight_range = sight_range
|
||||
|
||||
def can_see_player(self, enemy: Enemy, player: Player,
|
||||
dungeon: 'Dungeon') -> bool:
|
||||
"""Check if enemy can see the player."""
|
||||
# Simple distance check combined with line of sight
|
||||
distance = enemy.distance_to(player)
|
||||
|
||||
if distance > self.sight_range:
|
||||
return False
|
||||
|
||||
# Check line of sight using Bresenham's line
|
||||
return self._has_line_of_sight(enemy.x, enemy.y, player.x, player.y, dungeon)
|
||||
|
||||
def _has_line_of_sight(self, x1: int, y1: int, x2: int, y2: int,
|
||||
dungeon: 'Dungeon') -> bool:
|
||||
"""
|
||||
Check if there's a clear line of sight between two points.
|
||||
Uses Bresenham's line algorithm.
|
||||
"""
|
||||
dx = abs(x2 - x1)
|
||||
dy = abs(y2 - y1)
|
||||
x, y = x1, y1
|
||||
sx = 1 if x1 < x2 else -1
|
||||
sy = 1 if y1 < y2 else -1
|
||||
|
||||
if dx > dy:
|
||||
err = dx / 2
|
||||
while x != x2:
|
||||
if not dungeon.is_transparent(x, y):
|
||||
return False
|
||||
err -= dy
|
||||
if err < 0:
|
||||
y += sy
|
||||
err += dx
|
||||
x += sx
|
||||
else:
|
||||
err = dy / 2
|
||||
while y != y2:
|
||||
if not dungeon.is_transparent(x, y):
|
||||
return False
|
||||
err -= dx
|
||||
if err < 0:
|
||||
x += sx
|
||||
err += dy
|
||||
y += sy
|
||||
|
||||
return True
|
||||
|
||||
def get_path_to_player(self, enemy: Enemy, player: Player) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
Get a path from enemy to player using A* pathfinding.
|
||||
|
||||
Uses the entity's built-in path_to method.
|
||||
"""
|
||||
try:
|
||||
path = enemy.entity.path_to(player.x, player.y)
|
||||
# Convert path to list of tuples
|
||||
return [(int(p[0]), int(p[1])) for p in path] if path else []
|
||||
except (AttributeError, TypeError):
|
||||
# Fallback: simple direction-based movement
|
||||
return []
|
||||
|
||||
def is_position_blocked(self, x: int, y: int, dungeon: 'Dungeon',
|
||||
enemies: List[Enemy], player: Player) -> bool:
|
||||
"""Check if a position is blocked by terrain or another actor."""
|
||||
# Check terrain
|
||||
if not dungeon.is_walkable(x, y):
|
||||
return True
|
||||
|
||||
# Check player position
|
||||
if player.x == x and player.y == y:
|
||||
return True
|
||||
|
||||
# Check other enemies
|
||||
for other in enemies:
|
||||
if other.is_alive and other.x == x and other.y == y:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def move_toward(self, enemy: Enemy, target_x: int, target_y: int,
|
||||
dungeon: 'Dungeon', enemies: List[Enemy],
|
||||
player: Player) -> bool:
|
||||
"""
|
||||
Move one step toward the target position.
|
||||
|
||||
Returns True if movement occurred, False otherwise.
|
||||
"""
|
||||
# Try pathfinding first
|
||||
path = self.get_path_to_player(enemy, player)
|
||||
|
||||
if path and len(path) > 1:
|
||||
# First element is current position, second is next step
|
||||
next_x, next_y = path[1]
|
||||
else:
|
||||
# Fallback: move in the general direction
|
||||
dx = 0
|
||||
dy = 0
|
||||
|
||||
if target_x < enemy.x:
|
||||
dx = -1
|
||||
elif target_x > enemy.x:
|
||||
dx = 1
|
||||
|
||||
if target_y < enemy.y:
|
||||
dy = -1
|
||||
elif target_y > enemy.y:
|
||||
dy = 1
|
||||
|
||||
next_x = enemy.x + dx
|
||||
next_y = enemy.y + dy
|
||||
|
||||
# Check if the position is blocked
|
||||
if not self.is_position_blocked(next_x, next_y, dungeon, enemies, player):
|
||||
enemy.move_to(next_x, next_y)
|
||||
return True
|
||||
|
||||
# Try moving in just one axis
|
||||
if next_x != enemy.x:
|
||||
if not self.is_position_blocked(next_x, enemy.y, dungeon, enemies, player):
|
||||
enemy.move_to(next_x, enemy.y)
|
||||
return True
|
||||
|
||||
if next_y != enemy.y:
|
||||
if not self.is_position_blocked(enemy.x, next_y, dungeon, enemies, player):
|
||||
enemy.move_to(enemy.x, next_y)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def take_turn(self, enemy: Enemy, player: Player, dungeon: 'Dungeon',
|
||||
enemies: List[Enemy]) -> Optional[CombatResult]:
|
||||
"""Execute the enemy's turn."""
|
||||
if not enemy.is_alive:
|
||||
return None
|
||||
|
||||
# Check if adjacent to player (can attack)
|
||||
if enemy.distance_to(player) == 1:
|
||||
return melee_attack(enemy, player)
|
||||
|
||||
# Check if can see player
|
||||
if self.can_see_player(enemy, player, dungeon):
|
||||
# Move toward player
|
||||
self.move_toward(enemy, player.x, player.y, dungeon, enemies, player)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class WanderingAI(BasicChaseAI):
|
||||
"""
|
||||
AI that wanders randomly when it can't see the player.
|
||||
More active than BasicChaseAI.
|
||||
"""
|
||||
|
||||
def __init__(self, sight_range: int = 8, wander_chance: float = 0.3):
|
||||
"""
|
||||
Args:
|
||||
sight_range: How far the enemy can see
|
||||
wander_chance: Probability of wandering each turn (0.0 to 1.0)
|
||||
"""
|
||||
super().__init__(sight_range)
|
||||
self.wander_chance = wander_chance
|
||||
|
||||
def wander(self, enemy: Enemy, dungeon: 'Dungeon',
|
||||
enemies: List[Enemy], player: Player) -> bool:
|
||||
"""
|
||||
Move in a random direction.
|
||||
|
||||
Returns True if movement occurred.
|
||||
"""
|
||||
# Random direction
|
||||
directions = [
|
||||
(-1, 0), (1, 0), (0, -1), (0, 1), # Cardinal
|
||||
(-1, -1), (1, -1), (-1, 1), (1, 1) # Diagonal
|
||||
]
|
||||
random.shuffle(directions)
|
||||
|
||||
for dx, dy in directions:
|
||||
new_x = enemy.x + dx
|
||||
new_y = enemy.y + dy
|
||||
|
||||
if not self.is_position_blocked(new_x, new_y, dungeon, enemies, player):
|
||||
enemy.move_to(new_x, new_y)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def take_turn(self, enemy: Enemy, player: Player, dungeon: 'Dungeon',
|
||||
enemies: List[Enemy]) -> Optional[CombatResult]:
|
||||
"""Execute the enemy's turn with wandering behavior."""
|
||||
if not enemy.is_alive:
|
||||
return None
|
||||
|
||||
# Check if adjacent to player (can attack)
|
||||
if enemy.distance_to(player) == 1:
|
||||
return melee_attack(enemy, player)
|
||||
|
||||
# Check if can see player
|
||||
if self.can_see_player(enemy, player, dungeon):
|
||||
# Chase player
|
||||
self.move_toward(enemy, player.x, player.y, dungeon, enemies, player)
|
||||
else:
|
||||
# Wander randomly
|
||||
if random.random() < self.wander_chance:
|
||||
self.wander(enemy, dungeon, enemies, player)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Default AI instance
|
||||
default_ai = WanderingAI(sight_range=8, wander_chance=0.3)
|
||||
|
||||
|
||||
def process_enemy_turns(enemies: List[Enemy], player: Player,
|
||||
dungeon: 'Dungeon',
|
||||
ai: AIBehavior = None) -> List[CombatResult]:
|
||||
"""
|
||||
Process turns for all enemies.
|
||||
|
||||
Args:
|
||||
enemies: List of all enemies
|
||||
player: The player
|
||||
dungeon: The dungeon map
|
||||
ai: AI behavior to use (defaults to WanderingAI)
|
||||
|
||||
Returns:
|
||||
List of combat results from this round of enemy actions
|
||||
"""
|
||||
if ai is None:
|
||||
ai = default_ai
|
||||
|
||||
results = []
|
||||
|
||||
for enemy in enemies:
|
||||
if enemy.is_alive:
|
||||
result = ai.take_turn(enemy, player, dungeon, enemies)
|
||||
if result:
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
187
docs/templates/complete/combat.py
vendored
|
|
@ -1,187 +0,0 @@
|
|||
"""
|
||||
combat.py - Combat System for McRogueFace Roguelike
|
||||
|
||||
Handles attack resolution, damage calculation, and combat outcomes.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple, Optional
|
||||
import random
|
||||
|
||||
from entities import Actor, Player, Enemy
|
||||
from constants import (
|
||||
MSG_PLAYER_ATTACK, MSG_PLAYER_KILL, MSG_PLAYER_MISS,
|
||||
MSG_ENEMY_ATTACK, MSG_ENEMY_MISS
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CombatResult:
|
||||
"""
|
||||
Result of a combat action.
|
||||
|
||||
Attributes:
|
||||
attacker: The attacking actor
|
||||
defender: The defending actor
|
||||
damage: Damage dealt (after defense)
|
||||
killed: Whether the defender was killed
|
||||
message: Human-readable result message
|
||||
message_color: Color tuple for the message
|
||||
"""
|
||||
attacker: Actor
|
||||
defender: Actor
|
||||
damage: int
|
||||
killed: bool
|
||||
message: str
|
||||
message_color: Tuple[int, int, int, int]
|
||||
|
||||
|
||||
def calculate_damage(attack: int, defense: int, variance: float = 0.2) -> int:
|
||||
"""
|
||||
Calculate damage with some randomness.
|
||||
|
||||
Args:
|
||||
attack: Attacker's attack power
|
||||
defense: Defender's defense value
|
||||
variance: Random variance as percentage (0.2 = +/-20%)
|
||||
|
||||
Returns:
|
||||
Final damage amount (minimum 0)
|
||||
"""
|
||||
# Base damage is attack vs defense
|
||||
base_damage = attack - defense
|
||||
|
||||
# Add some variance
|
||||
if base_damage > 0:
|
||||
variance_amount = int(base_damage * variance)
|
||||
damage = base_damage + random.randint(-variance_amount, variance_amount)
|
||||
else:
|
||||
# Small chance to do 1 damage even with high defense
|
||||
damage = 1 if random.random() < 0.1 else 0
|
||||
|
||||
return max(0, damage)
|
||||
|
||||
|
||||
def attack(attacker: Actor, defender: Actor) -> CombatResult:
|
||||
"""
|
||||
Perform an attack from one actor to another.
|
||||
|
||||
Args:
|
||||
attacker: The actor making the attack
|
||||
defender: The actor being attacked
|
||||
|
||||
Returns:
|
||||
CombatResult with outcome details
|
||||
"""
|
||||
# Calculate damage
|
||||
damage = calculate_damage(
|
||||
attacker.fighter.attack,
|
||||
defender.fighter.defense
|
||||
)
|
||||
|
||||
# Apply damage
|
||||
actual_damage = defender.fighter.take_damage(damage + defender.fighter.defense)
|
||||
# Note: take_damage applies defense internally, so we add it back
|
||||
# Actually, we calculated damage already reduced by defense, so just apply it:
|
||||
defender.fighter.hp = max(0, defender.fighter.hp - damage + actual_damage)
|
||||
# Simplified: just use take_damage properly
|
||||
# Reset and do it right:
|
||||
|
||||
# Apply raw damage (defense already calculated)
|
||||
defender.fighter.hp = max(0, defender.fighter.hp - damage)
|
||||
killed = not defender.is_alive
|
||||
|
||||
# Generate message based on attacker/defender types
|
||||
if isinstance(attacker, Player):
|
||||
if killed:
|
||||
message = MSG_PLAYER_KILL % defender.name
|
||||
color = (255, 255, 100, 255) # Yellow for kills
|
||||
elif damage > 0:
|
||||
message = MSG_PLAYER_ATTACK % (defender.name, damage)
|
||||
color = (255, 255, 255, 255) # White for hits
|
||||
else:
|
||||
message = MSG_PLAYER_MISS % defender.name
|
||||
color = (150, 150, 150, 255) # Gray for misses
|
||||
else:
|
||||
if damage > 0:
|
||||
message = MSG_ENEMY_ATTACK % (attacker.name, damage)
|
||||
color = (255, 100, 100, 255) # Red for enemy hits
|
||||
else:
|
||||
message = MSG_ENEMY_MISS % attacker.name
|
||||
color = (150, 150, 150, 255) # Gray for misses
|
||||
|
||||
return CombatResult(
|
||||
attacker=attacker,
|
||||
defender=defender,
|
||||
damage=damage,
|
||||
killed=killed,
|
||||
message=message,
|
||||
message_color=color
|
||||
)
|
||||
|
||||
|
||||
def melee_attack(attacker: Actor, defender: Actor) -> CombatResult:
|
||||
"""
|
||||
Perform a melee attack (bump attack).
|
||||
This is the standard roguelike bump-to-attack.
|
||||
|
||||
Args:
|
||||
attacker: The actor making the attack
|
||||
defender: The actor being attacked
|
||||
|
||||
Returns:
|
||||
CombatResult with outcome details
|
||||
"""
|
||||
return attack(attacker, defender)
|
||||
|
||||
|
||||
def try_attack(attacker: Actor, target_x: int, target_y: int,
|
||||
enemies: list, player: Optional[Player] = None) -> Optional[CombatResult]:
|
||||
"""
|
||||
Attempt to attack whatever is at the target position.
|
||||
|
||||
Args:
|
||||
attacker: The actor making the attack
|
||||
target_x: X coordinate to attack
|
||||
target_y: Y coordinate to attack
|
||||
enemies: List of Enemy actors
|
||||
player: The player (if attacker is an enemy)
|
||||
|
||||
Returns:
|
||||
CombatResult if something was attacked, None otherwise
|
||||
"""
|
||||
# Check if player is attacking
|
||||
if isinstance(attacker, Player):
|
||||
# Look for enemy at position
|
||||
for enemy in enemies:
|
||||
if enemy.is_alive and enemy.x == target_x and enemy.y == target_y:
|
||||
return melee_attack(attacker, enemy)
|
||||
else:
|
||||
# Enemy attacking - check if player is at position
|
||||
if player and player.x == target_x and player.y == target_y:
|
||||
return melee_attack(attacker, player)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def process_kill(attacker: Actor, defender: Actor) -> int:
|
||||
"""
|
||||
Process the aftermath of killing an enemy.
|
||||
|
||||
Args:
|
||||
attacker: The actor that made the kill
|
||||
defender: The actor that was killed
|
||||
|
||||
Returns:
|
||||
XP gained (if attacker is player and defender is enemy)
|
||||
"""
|
||||
xp_gained = 0
|
||||
|
||||
if isinstance(attacker, Player) and isinstance(defender, Enemy):
|
||||
xp_gained = defender.xp_reward
|
||||
attacker.gain_xp(xp_gained)
|
||||
|
||||
# Remove the dead actor from the grid
|
||||
defender.remove()
|
||||
|
||||
return xp_gained
|
||||
210
docs/templates/complete/constants.py
vendored
|
|
@ -1,210 +0,0 @@
|
|||
"""
|
||||
constants.py - Game Constants for McRogueFace Complete Roguelike Template
|
||||
|
||||
All configuration values in one place for easy tweaking.
|
||||
"""
|
||||
|
||||
# =============================================================================
|
||||
# WINDOW AND DISPLAY
|
||||
# =============================================================================
|
||||
SCREEN_WIDTH = 1024
|
||||
SCREEN_HEIGHT = 768
|
||||
|
||||
# Grid display area (where the dungeon is rendered)
|
||||
GRID_X = 0
|
||||
GRID_Y = 0
|
||||
GRID_WIDTH = 800
|
||||
GRID_HEIGHT = 600
|
||||
|
||||
# Tile dimensions (must match your texture)
|
||||
TILE_WIDTH = 16
|
||||
TILE_HEIGHT = 16
|
||||
|
||||
# =============================================================================
|
||||
# DUNGEON GENERATION
|
||||
# =============================================================================
|
||||
# Size of the dungeon in tiles
|
||||
DUNGEON_WIDTH = 80
|
||||
DUNGEON_HEIGHT = 45
|
||||
|
||||
# Room size constraints
|
||||
ROOM_MIN_SIZE = 6
|
||||
ROOM_MAX_SIZE = 12
|
||||
MAX_ROOMS = 15
|
||||
|
||||
# Enemy spawning per room
|
||||
MAX_ENEMIES_PER_ROOM = 3
|
||||
MIN_ENEMIES_PER_ROOM = 0
|
||||
|
||||
# =============================================================================
|
||||
# SPRITE INDICES (for kenney_tinydungeon.png - 16x16 tiles)
|
||||
# Adjust these if using a different tileset
|
||||
# =============================================================================
|
||||
# Terrain
|
||||
SPRITE_FLOOR = 48 # Dungeon floor
|
||||
SPRITE_WALL = 33 # Wall tile
|
||||
SPRITE_STAIRS_DOWN = 50 # Stairs going down
|
||||
SPRITE_DOOR = 49 # Door tile
|
||||
|
||||
# Player sprites
|
||||
SPRITE_PLAYER = 84 # Player character (knight)
|
||||
|
||||
# Enemy sprites
|
||||
SPRITE_GOBLIN = 111 # Goblin enemy
|
||||
SPRITE_ORC = 112 # Orc enemy
|
||||
SPRITE_TROLL = 116 # Troll enemy
|
||||
|
||||
# Items (for future expansion)
|
||||
SPRITE_POTION = 89 # Health potion
|
||||
SPRITE_CHEST = 91 # Treasure chest
|
||||
|
||||
# =============================================================================
|
||||
# COLORS (R, G, B, A)
|
||||
# =============================================================================
|
||||
# Map colors
|
||||
COLOR_DARK_WALL = (50, 50, 100, 255)
|
||||
COLOR_DARK_FLOOR = (30, 30, 50, 255)
|
||||
COLOR_LIGHT_WALL = (100, 100, 150, 255)
|
||||
COLOR_LIGHT_FLOOR = (80, 80, 100, 255)
|
||||
|
||||
# FOV overlay colors
|
||||
COLOR_FOG = (0, 0, 0, 200) # Unexplored areas
|
||||
COLOR_REMEMBERED = (0, 0, 0, 128) # Seen but not visible
|
||||
COLOR_VISIBLE = (0, 0, 0, 0) # Currently visible (transparent)
|
||||
|
||||
# UI Colors
|
||||
COLOR_UI_BG = (20, 20, 30, 230)
|
||||
COLOR_UI_BORDER = (80, 80, 120, 255)
|
||||
COLOR_TEXT = (255, 255, 255, 255)
|
||||
COLOR_TEXT_HIGHLIGHT = (255, 255, 100, 255)
|
||||
|
||||
# Health bar colors
|
||||
COLOR_HP_BAR_BG = (80, 0, 0, 255)
|
||||
COLOR_HP_BAR_FILL = (0, 180, 0, 255)
|
||||
COLOR_HP_BAR_WARNING = (180, 180, 0, 255)
|
||||
COLOR_HP_BAR_CRITICAL = (180, 0, 0, 255)
|
||||
|
||||
# Message log colors
|
||||
COLOR_MSG_DEFAULT = (255, 255, 255, 255)
|
||||
COLOR_MSG_DAMAGE = (255, 100, 100, 255)
|
||||
COLOR_MSG_HEAL = (100, 255, 100, 255)
|
||||
COLOR_MSG_INFO = (100, 100, 255, 255)
|
||||
COLOR_MSG_IMPORTANT = (255, 255, 100, 255)
|
||||
|
||||
# =============================================================================
|
||||
# PLAYER STATS
|
||||
# =============================================================================
|
||||
PLAYER_START_HP = 30
|
||||
PLAYER_START_ATTACK = 5
|
||||
PLAYER_START_DEFENSE = 2
|
||||
|
||||
# =============================================================================
|
||||
# ENEMY STATS
|
||||
# Each enemy type: (hp, attack, defense, xp_reward, name)
|
||||
# =============================================================================
|
||||
ENEMY_STATS = {
|
||||
'goblin': {
|
||||
'hp': 10,
|
||||
'attack': 3,
|
||||
'defense': 0,
|
||||
'xp': 35,
|
||||
'sprite': SPRITE_GOBLIN,
|
||||
'name': 'Goblin'
|
||||
},
|
||||
'orc': {
|
||||
'hp': 16,
|
||||
'attack': 4,
|
||||
'defense': 1,
|
||||
'xp': 50,
|
||||
'sprite': SPRITE_ORC,
|
||||
'name': 'Orc'
|
||||
},
|
||||
'troll': {
|
||||
'hp': 24,
|
||||
'attack': 6,
|
||||
'defense': 2,
|
||||
'xp': 100,
|
||||
'sprite': SPRITE_TROLL,
|
||||
'name': 'Troll'
|
||||
}
|
||||
}
|
||||
|
||||
# Enemy spawn weights per dungeon level
|
||||
# Format: {level: [(enemy_type, weight), ...]}
|
||||
# Higher weight = more likely to spawn
|
||||
ENEMY_SPAWN_WEIGHTS = {
|
||||
1: [('goblin', 100)],
|
||||
2: [('goblin', 80), ('orc', 20)],
|
||||
3: [('goblin', 60), ('orc', 40)],
|
||||
4: [('goblin', 40), ('orc', 50), ('troll', 10)],
|
||||
5: [('goblin', 20), ('orc', 50), ('troll', 30)],
|
||||
}
|
||||
|
||||
# Default weights for levels beyond those defined
|
||||
DEFAULT_SPAWN_WEIGHTS = [('goblin', 10), ('orc', 50), ('troll', 40)]
|
||||
|
||||
# =============================================================================
|
||||
# FOV (Field of View) SETTINGS
|
||||
# =============================================================================
|
||||
FOV_RADIUS = 8 # How far the player can see
|
||||
FOV_LIGHT_WALLS = True # Whether walls at FOV edge are visible
|
||||
|
||||
# =============================================================================
|
||||
# INPUT KEYS
|
||||
# Key names as returned by McRogueFace keypressScene
|
||||
# =============================================================================
|
||||
KEY_UP = ['Up', 'W', 'Numpad8']
|
||||
KEY_DOWN = ['Down', 'S', 'Numpad2']
|
||||
KEY_LEFT = ['Left', 'A', 'Numpad4']
|
||||
KEY_RIGHT = ['Right', 'D', 'Numpad6']
|
||||
|
||||
# Diagonal movement (numpad)
|
||||
KEY_UP_LEFT = ['Numpad7']
|
||||
KEY_UP_RIGHT = ['Numpad9']
|
||||
KEY_DOWN_LEFT = ['Numpad1']
|
||||
KEY_DOWN_RIGHT = ['Numpad3']
|
||||
|
||||
# Actions
|
||||
KEY_WAIT = ['Period', 'Numpad5'] # Skip turn
|
||||
KEY_DESCEND = ['Greater', 'Space'] # Go down stairs (> key or space)
|
||||
|
||||
# =============================================================================
|
||||
# GAME MESSAGES
|
||||
# =============================================================================
|
||||
MSG_WELCOME = "Welcome to the dungeon! Find the stairs to descend deeper."
|
||||
MSG_DESCEND = "You descend the stairs to level %d..."
|
||||
MSG_PLAYER_ATTACK = "You attack the %s for %d damage!"
|
||||
MSG_PLAYER_KILL = "You have slain the %s!"
|
||||
MSG_PLAYER_MISS = "You attack the %s but do no damage."
|
||||
MSG_ENEMY_ATTACK = "The %s attacks you for %d damage!"
|
||||
MSG_ENEMY_MISS = "The %s attacks you but does no damage."
|
||||
MSG_BLOCKED = "You can't move there!"
|
||||
MSG_STAIRS = "You see stairs leading down here. Press > or Space to descend."
|
||||
MSG_DEATH = "You have died! Press R to restart."
|
||||
MSG_NO_STAIRS = "There are no stairs here."
|
||||
|
||||
# =============================================================================
|
||||
# UI LAYOUT
|
||||
# =============================================================================
|
||||
# Health bar
|
||||
HP_BAR_X = 10
|
||||
HP_BAR_Y = 620
|
||||
HP_BAR_WIDTH = 200
|
||||
HP_BAR_HEIGHT = 24
|
||||
|
||||
# Message log
|
||||
MSG_LOG_X = 10
|
||||
MSG_LOG_Y = 660
|
||||
MSG_LOG_WIDTH = 780
|
||||
MSG_LOG_HEIGHT = 100
|
||||
MSG_LOG_MAX_LINES = 5
|
||||
|
||||
# Dungeon level display
|
||||
LEVEL_DISPLAY_X = 700
|
||||
LEVEL_DISPLAY_Y = 620
|
||||
|
||||
# =============================================================================
|
||||
# ASSET PATHS
|
||||
# =============================================================================
|
||||
TEXTURE_PATH = "assets/kenney_tinydungeon.png"
|
||||
FONT_PATH = "assets/JetbrainsMono.ttf"
|
||||
298
docs/templates/complete/dungeon.py
vendored
|
|
@ -1,298 +0,0 @@
|
|||
"""
|
||||
dungeon.py - Procedural Dungeon Generation for McRogueFace
|
||||
|
||||
Generates a roguelike dungeon with rooms connected by corridors.
|
||||
Includes stairs placement for multi-level progression.
|
||||
"""
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
from constants import (
|
||||
DUNGEON_WIDTH, DUNGEON_HEIGHT,
|
||||
ROOM_MIN_SIZE, ROOM_MAX_SIZE, MAX_ROOMS,
|
||||
SPRITE_FLOOR, SPRITE_WALL, SPRITE_STAIRS_DOWN,
|
||||
MAX_ENEMIES_PER_ROOM, MIN_ENEMIES_PER_ROOM,
|
||||
ENEMY_SPAWN_WEIGHTS, DEFAULT_SPAWN_WEIGHTS
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Rect:
|
||||
"""A rectangle representing a room in the dungeon."""
|
||||
x: int
|
||||
y: int
|
||||
width: int
|
||||
height: int
|
||||
|
||||
@property
|
||||
def x2(self) -> int:
|
||||
return self.x + self.width
|
||||
|
||||
@property
|
||||
def y2(self) -> int:
|
||||
return self.y + self.height
|
||||
|
||||
@property
|
||||
def center(self) -> Tuple[int, int]:
|
||||
"""Return the center coordinates of this room."""
|
||||
center_x = (self.x + self.x2) // 2
|
||||
center_y = (self.y + self.y2) // 2
|
||||
return center_x, center_y
|
||||
|
||||
def intersects(self, other: 'Rect') -> bool:
|
||||
"""Check if this room overlaps with another (with 1 tile buffer)."""
|
||||
return (self.x <= other.x2 + 1 and self.x2 + 1 >= other.x and
|
||||
self.y <= other.y2 + 1 and self.y2 + 1 >= other.y)
|
||||
|
||||
def inner(self) -> Tuple[int, int, int, int]:
|
||||
"""Return the inner area of the room (excluding walls)."""
|
||||
return self.x + 1, self.y + 1, self.width - 2, self.height - 2
|
||||
|
||||
|
||||
class Tile:
|
||||
"""Represents a single tile in the dungeon."""
|
||||
|
||||
def __init__(self, walkable: bool = False, transparent: bool = False,
|
||||
sprite: int = SPRITE_WALL):
|
||||
self.walkable = walkable
|
||||
self.transparent = transparent
|
||||
self.sprite = sprite
|
||||
self.explored = False
|
||||
self.visible = False
|
||||
|
||||
|
||||
class Dungeon:
|
||||
"""
|
||||
The dungeon map with rooms, corridors, and tile data.
|
||||
|
||||
Attributes:
|
||||
width: Width of the dungeon in tiles
|
||||
height: Height of the dungeon in tiles
|
||||
level: Current dungeon depth
|
||||
tiles: 2D array of Tile objects
|
||||
rooms: List of rooms (Rect objects)
|
||||
player_start: Starting position for the player
|
||||
stairs_pos: Position of the stairs down
|
||||
"""
|
||||
|
||||
def __init__(self, width: int = DUNGEON_WIDTH, height: int = DUNGEON_HEIGHT,
|
||||
level: int = 1):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.level = level
|
||||
self.tiles: List[List[Tile]] = []
|
||||
self.rooms: List[Rect] = []
|
||||
self.player_start: Tuple[int, int] = (0, 0)
|
||||
self.stairs_pos: Tuple[int, int] = (0, 0)
|
||||
|
||||
# Initialize all tiles as walls
|
||||
self._init_tiles()
|
||||
|
||||
def _init_tiles(self) -> None:
|
||||
"""Fill the dungeon with wall tiles."""
|
||||
self.tiles = [
|
||||
[Tile(walkable=False, transparent=False, sprite=SPRITE_WALL)
|
||||
for _ in range(self.height)]
|
||||
for _ in range(self.width)
|
||||
]
|
||||
|
||||
def in_bounds(self, x: int, y: int) -> bool:
|
||||
"""Check if coordinates are within dungeon bounds."""
|
||||
return 0 <= x < self.width and 0 <= y < self.height
|
||||
|
||||
def is_walkable(self, x: int, y: int) -> bool:
|
||||
"""Check if a tile can be walked on."""
|
||||
if not self.in_bounds(x, y):
|
||||
return False
|
||||
return self.tiles[x][y].walkable
|
||||
|
||||
def is_transparent(self, x: int, y: int) -> bool:
|
||||
"""Check if a tile allows light to pass through."""
|
||||
if not self.in_bounds(x, y):
|
||||
return False
|
||||
return self.tiles[x][y].transparent
|
||||
|
||||
def get_tile(self, x: int, y: int) -> Optional[Tile]:
|
||||
"""Get the tile at the given position."""
|
||||
if not self.in_bounds(x, y):
|
||||
return None
|
||||
return self.tiles[x][y]
|
||||
|
||||
def set_tile(self, x: int, y: int, walkable: bool, transparent: bool,
|
||||
sprite: int) -> None:
|
||||
"""Set properties of a tile."""
|
||||
if self.in_bounds(x, y):
|
||||
tile = self.tiles[x][y]
|
||||
tile.walkable = walkable
|
||||
tile.transparent = transparent
|
||||
tile.sprite = sprite
|
||||
|
||||
def carve_room(self, room: Rect) -> None:
|
||||
"""Carve out a room in the dungeon (make tiles walkable)."""
|
||||
inner_x, inner_y, inner_w, inner_h = room.inner()
|
||||
|
||||
for x in range(inner_x, inner_x + inner_w):
|
||||
for y in range(inner_y, inner_y + inner_h):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite=SPRITE_FLOOR)
|
||||
|
||||
def carve_tunnel_h(self, x1: int, x2: int, y: int) -> None:
|
||||
"""Carve a horizontal tunnel."""
|
||||
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite=SPRITE_FLOOR)
|
||||
|
||||
def carve_tunnel_v(self, y1: int, y2: int, x: int) -> None:
|
||||
"""Carve a vertical tunnel."""
|
||||
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite=SPRITE_FLOOR)
|
||||
|
||||
def connect_rooms(self, room1: Rect, room2: Rect) -> None:
|
||||
"""Connect two rooms with an L-shaped corridor."""
|
||||
x1, y1 = room1.center
|
||||
x2, y2 = room2.center
|
||||
|
||||
# Randomly choose to go horizontal then vertical, or vice versa
|
||||
if random.random() < 0.5:
|
||||
self.carve_tunnel_h(x1, x2, y1)
|
||||
self.carve_tunnel_v(y1, y2, x2)
|
||||
else:
|
||||
self.carve_tunnel_v(y1, y2, x1)
|
||||
self.carve_tunnel_h(x1, x2, y2)
|
||||
|
||||
def place_stairs(self) -> None:
|
||||
"""Place stairs in the last room."""
|
||||
if self.rooms:
|
||||
# Stairs go in the center of the last room
|
||||
self.stairs_pos = self.rooms[-1].center
|
||||
x, y = self.stairs_pos
|
||||
self.set_tile(x, y, walkable=True, transparent=True,
|
||||
sprite=SPRITE_STAIRS_DOWN)
|
||||
|
||||
def generate(self) -> None:
|
||||
"""Generate the dungeon using BSP-style room placement."""
|
||||
self._init_tiles()
|
||||
self.rooms.clear()
|
||||
|
||||
for _ in range(MAX_ROOMS):
|
||||
# Random room dimensions
|
||||
w = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
h = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
|
||||
# Random position (ensure room fits in dungeon)
|
||||
x = random.randint(1, self.width - w - 1)
|
||||
y = random.randint(1, self.height - h - 1)
|
||||
|
||||
new_room = Rect(x, y, w, h)
|
||||
|
||||
# Check for intersections with existing rooms
|
||||
if any(new_room.intersects(other) for other in self.rooms):
|
||||
continue
|
||||
|
||||
# Room is valid - carve it out
|
||||
self.carve_room(new_room)
|
||||
|
||||
if self.rooms:
|
||||
# Connect to previous room
|
||||
self.connect_rooms(self.rooms[-1], new_room)
|
||||
else:
|
||||
# First room - player starts here
|
||||
self.player_start = new_room.center
|
||||
|
||||
self.rooms.append(new_room)
|
||||
|
||||
# Place stairs in the last room
|
||||
self.place_stairs()
|
||||
|
||||
def get_spawn_positions(self) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
Get valid spawn positions for enemies.
|
||||
Returns positions from all rooms except the first (player start).
|
||||
"""
|
||||
positions = []
|
||||
|
||||
for room in self.rooms[1:]: # Skip first room (player start)
|
||||
inner_x, inner_y, inner_w, inner_h = room.inner()
|
||||
|
||||
for x in range(inner_x, inner_x + inner_w):
|
||||
for y in range(inner_y, inner_y + inner_h):
|
||||
# Don't spawn on stairs
|
||||
if (x, y) != self.stairs_pos:
|
||||
positions.append((x, y))
|
||||
|
||||
return positions
|
||||
|
||||
def get_enemy_spawns(self) -> List[Tuple[str, int, int]]:
|
||||
"""
|
||||
Determine which enemies to spawn and where.
|
||||
Returns list of (enemy_type, x, y) tuples.
|
||||
"""
|
||||
spawns = []
|
||||
|
||||
# Get spawn weights for this level
|
||||
weights = ENEMY_SPAWN_WEIGHTS.get(self.level, DEFAULT_SPAWN_WEIGHTS)
|
||||
|
||||
# Create weighted list for random selection
|
||||
enemy_types = []
|
||||
for enemy_type, weight in weights:
|
||||
enemy_types.extend([enemy_type] * weight)
|
||||
|
||||
# Spawn enemies in each room (except the first)
|
||||
for room in self.rooms[1:]:
|
||||
num_enemies = random.randint(MIN_ENEMIES_PER_ROOM, MAX_ENEMIES_PER_ROOM)
|
||||
|
||||
# Scale up enemies slightly with dungeon level
|
||||
num_enemies = min(num_enemies + (self.level - 1) // 2, MAX_ENEMIES_PER_ROOM + 2)
|
||||
|
||||
inner_x, inner_y, inner_w, inner_h = room.inner()
|
||||
used_positions = set()
|
||||
|
||||
for _ in range(num_enemies):
|
||||
# Find an unused position
|
||||
attempts = 0
|
||||
while attempts < 20:
|
||||
x = random.randint(inner_x, inner_x + inner_w - 1)
|
||||
y = random.randint(inner_y, inner_y + inner_h - 1)
|
||||
|
||||
if (x, y) not in used_positions and (x, y) != self.stairs_pos:
|
||||
enemy_type = random.choice(enemy_types)
|
||||
spawns.append((enemy_type, x, y))
|
||||
used_positions.add((x, y))
|
||||
break
|
||||
|
||||
attempts += 1
|
||||
|
||||
return spawns
|
||||
|
||||
def apply_to_grid(self, grid) -> None:
|
||||
"""
|
||||
Apply the dungeon data to a McRogueFace Grid object.
|
||||
|
||||
Args:
|
||||
grid: A mcrfpy.Grid object to update
|
||||
"""
|
||||
for x in range(self.width):
|
||||
for y in range(self.height):
|
||||
tile = self.tiles[x][y]
|
||||
point = grid.at(x, y)
|
||||
point.tilesprite = tile.sprite
|
||||
point.walkable = tile.walkable
|
||||
point.transparent = tile.transparent
|
||||
|
||||
|
||||
def generate_dungeon(level: int = 1) -> Dungeon:
|
||||
"""
|
||||
Convenience function to generate a new dungeon.
|
||||
|
||||
Args:
|
||||
level: The dungeon depth (affects enemy spawns)
|
||||
|
||||
Returns:
|
||||
A fully generated Dungeon object
|
||||
"""
|
||||
dungeon = Dungeon(level=level)
|
||||
dungeon.generate()
|
||||
return dungeon
|
||||
319
docs/templates/complete/entities.py
vendored
|
|
@ -1,319 +0,0 @@
|
|||
"""
|
||||
entities.py - Player and Enemy Entity Definitions
|
||||
|
||||
Defines the game actors with stats, rendering, and basic behaviors.
|
||||
Uses composition with McRogueFace Entity objects for rendering.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, List, Tuple, TYPE_CHECKING
|
||||
import mcrfpy
|
||||
|
||||
from constants import (
|
||||
PLAYER_START_HP, PLAYER_START_ATTACK, PLAYER_START_DEFENSE,
|
||||
SPRITE_PLAYER, ENEMY_STATS, FOV_RADIUS
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dungeon import Dungeon
|
||||
|
||||
|
||||
@dataclass
|
||||
class Fighter:
|
||||
"""
|
||||
Combat statistics component for entities that can fight.
|
||||
|
||||
Attributes:
|
||||
hp: Current hit points
|
||||
max_hp: Maximum hit points
|
||||
attack: Attack power
|
||||
defense: Damage reduction
|
||||
"""
|
||||
hp: int
|
||||
max_hp: int
|
||||
attack: int
|
||||
defense: int
|
||||
|
||||
@property
|
||||
def is_alive(self) -> bool:
|
||||
"""Check if this fighter is still alive."""
|
||||
return self.hp > 0
|
||||
|
||||
@property
|
||||
def hp_percent(self) -> float:
|
||||
"""Return HP as a percentage (0.0 to 1.0)."""
|
||||
if self.max_hp <= 0:
|
||||
return 0.0
|
||||
return self.hp / self.max_hp
|
||||
|
||||
def heal(self, amount: int) -> int:
|
||||
"""
|
||||
Heal by the given amount, up to max_hp.
|
||||
|
||||
Returns:
|
||||
The actual amount healed.
|
||||
"""
|
||||
old_hp = self.hp
|
||||
self.hp = min(self.hp + amount, self.max_hp)
|
||||
return self.hp - old_hp
|
||||
|
||||
def take_damage(self, amount: int) -> int:
|
||||
"""
|
||||
Take damage, reduced by defense.
|
||||
|
||||
Args:
|
||||
amount: Raw damage before defense calculation
|
||||
|
||||
Returns:
|
||||
The actual damage taken after defense.
|
||||
"""
|
||||
# Defense reduces damage, minimum 0
|
||||
actual_damage = max(0, amount - self.defense)
|
||||
self.hp = max(0, self.hp - actual_damage)
|
||||
return actual_damage
|
||||
|
||||
|
||||
class Actor:
|
||||
"""
|
||||
Base class for all game actors (player and enemies).
|
||||
|
||||
Wraps a McRogueFace Entity and adds game logic.
|
||||
"""
|
||||
|
||||
def __init__(self, x: int, y: int, sprite: int, name: str,
|
||||
texture: mcrfpy.Texture, grid: mcrfpy.Grid,
|
||||
fighter: Fighter):
|
||||
"""
|
||||
Create a new actor.
|
||||
|
||||
Args:
|
||||
x: Starting X position
|
||||
y: Starting Y position
|
||||
sprite: Sprite index for rendering
|
||||
name: Display name of this actor
|
||||
texture: Texture for the entity sprite
|
||||
grid: Grid to add the entity to
|
||||
fighter: Combat statistics
|
||||
"""
|
||||
self.name = name
|
||||
self.fighter = fighter
|
||||
self.grid = grid
|
||||
self._x = x
|
||||
self._y = y
|
||||
|
||||
# Create the McRogueFace entity
|
||||
self.entity = mcrfpy.Entity((x, y), texture, sprite)
|
||||
grid.entities.append(self.entity)
|
||||
|
||||
@property
|
||||
def x(self) -> int:
|
||||
return self._x
|
||||
|
||||
@x.setter
|
||||
def x(self, value: int) -> None:
|
||||
self._x = value
|
||||
self.entity.pos = (value, self._y)
|
||||
|
||||
@property
|
||||
def y(self) -> int:
|
||||
return self._y
|
||||
|
||||
@y.setter
|
||||
def y(self, value: int) -> None:
|
||||
self._y = value
|
||||
self.entity.pos = (self._x, value)
|
||||
|
||||
@property
|
||||
def pos(self) -> Tuple[int, int]:
|
||||
return (self._x, self._y)
|
||||
|
||||
@pos.setter
|
||||
def pos(self, value: Tuple[int, int]) -> None:
|
||||
self._x, self._y = value
|
||||
self.entity.pos = value
|
||||
|
||||
@property
|
||||
def is_alive(self) -> bool:
|
||||
return self.fighter.is_alive
|
||||
|
||||
def move(self, dx: int, dy: int) -> None:
|
||||
"""Move by the given delta."""
|
||||
self.x += dx
|
||||
self.y += dy
|
||||
|
||||
def move_to(self, x: int, y: int) -> None:
|
||||
"""Move to an absolute position."""
|
||||
self.pos = (x, y)
|
||||
|
||||
def distance_to(self, other: 'Actor') -> int:
|
||||
"""Calculate Manhattan distance to another actor."""
|
||||
return abs(self.x - other.x) + abs(self.y - other.y)
|
||||
|
||||
def remove(self) -> None:
|
||||
"""Remove this actor's entity from the grid."""
|
||||
try:
|
||||
idx = self.entity.index()
|
||||
self.grid.entities.remove(idx)
|
||||
except (ValueError, RuntimeError):
|
||||
pass # Already removed
|
||||
|
||||
|
||||
class Player(Actor):
|
||||
"""
|
||||
The player character with additional player-specific functionality.
|
||||
"""
|
||||
|
||||
def __init__(self, x: int, y: int, texture: mcrfpy.Texture,
|
||||
grid: mcrfpy.Grid):
|
||||
fighter = Fighter(
|
||||
hp=PLAYER_START_HP,
|
||||
max_hp=PLAYER_START_HP,
|
||||
attack=PLAYER_START_ATTACK,
|
||||
defense=PLAYER_START_DEFENSE
|
||||
)
|
||||
super().__init__(
|
||||
x=x, y=y,
|
||||
sprite=SPRITE_PLAYER,
|
||||
name="Player",
|
||||
texture=texture,
|
||||
grid=grid,
|
||||
fighter=fighter
|
||||
)
|
||||
self.xp = 0
|
||||
self.level = 1
|
||||
self.dungeon_level = 1
|
||||
|
||||
def gain_xp(self, amount: int) -> bool:
|
||||
"""
|
||||
Gain experience points.
|
||||
|
||||
Args:
|
||||
amount: XP to gain
|
||||
|
||||
Returns:
|
||||
True if the player leveled up
|
||||
"""
|
||||
self.xp += amount
|
||||
xp_to_level = self.xp_for_next_level
|
||||
|
||||
if self.xp >= xp_to_level:
|
||||
self.level_up()
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def xp_for_next_level(self) -> int:
|
||||
"""XP required for the next level."""
|
||||
return self.level * 100
|
||||
|
||||
def level_up(self) -> None:
|
||||
"""Level up the player, improving stats."""
|
||||
self.level += 1
|
||||
|
||||
# Improve stats
|
||||
hp_increase = 5
|
||||
attack_increase = 1
|
||||
defense_increase = 1 if self.level % 3 == 0 else 0
|
||||
|
||||
self.fighter.max_hp += hp_increase
|
||||
self.fighter.hp += hp_increase # Heal the increase amount
|
||||
self.fighter.attack += attack_increase
|
||||
self.fighter.defense += defense_increase
|
||||
|
||||
def update_fov(self, dungeon: 'Dungeon') -> None:
|
||||
"""
|
||||
Update field of view based on player position.
|
||||
|
||||
Uses entity.update_visibility() for TCOD FOV calculation.
|
||||
"""
|
||||
# Update the entity's visibility data
|
||||
self.entity.update_visibility()
|
||||
|
||||
# Apply FOV to dungeon tiles
|
||||
for x in range(dungeon.width):
|
||||
for y in range(dungeon.height):
|
||||
state = self.entity.at(x, y)
|
||||
tile = dungeon.get_tile(x, y)
|
||||
|
||||
if tile:
|
||||
tile.visible = state.visible
|
||||
if state.visible:
|
||||
tile.explored = True
|
||||
|
||||
|
||||
class Enemy(Actor):
|
||||
"""
|
||||
An enemy actor with AI behavior.
|
||||
"""
|
||||
|
||||
def __init__(self, x: int, y: int, enemy_type: str,
|
||||
texture: mcrfpy.Texture, grid: mcrfpy.Grid):
|
||||
"""
|
||||
Create a new enemy.
|
||||
|
||||
Args:
|
||||
x: Starting X position
|
||||
y: Starting Y position
|
||||
enemy_type: Key into ENEMY_STATS dictionary
|
||||
texture: Texture for the entity sprite
|
||||
grid: Grid to add the entity to
|
||||
"""
|
||||
stats = ENEMY_STATS.get(enemy_type, ENEMY_STATS['goblin'])
|
||||
|
||||
fighter = Fighter(
|
||||
hp=stats['hp'],
|
||||
max_hp=stats['hp'],
|
||||
attack=stats['attack'],
|
||||
defense=stats['defense']
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
x=x, y=y,
|
||||
sprite=stats['sprite'],
|
||||
name=stats['name'],
|
||||
texture=texture,
|
||||
grid=grid,
|
||||
fighter=fighter
|
||||
)
|
||||
|
||||
self.enemy_type = enemy_type
|
||||
self.xp_reward = stats['xp']
|
||||
|
||||
# AI state
|
||||
self.target: Optional[Actor] = None
|
||||
self.path: List[Tuple[int, int]] = []
|
||||
|
||||
|
||||
def create_player(x: int, y: int, texture: mcrfpy.Texture,
|
||||
grid: mcrfpy.Grid) -> Player:
|
||||
"""
|
||||
Factory function to create the player.
|
||||
|
||||
Args:
|
||||
x: Starting X position
|
||||
y: Starting Y position
|
||||
texture: Texture for player sprite
|
||||
grid: Grid to add player to
|
||||
|
||||
Returns:
|
||||
A new Player instance
|
||||
"""
|
||||
return Player(x, y, texture, grid)
|
||||
|
||||
|
||||
def create_enemy(x: int, y: int, enemy_type: str,
|
||||
texture: mcrfpy.Texture, grid: mcrfpy.Grid) -> Enemy:
|
||||
"""
|
||||
Factory function to create an enemy.
|
||||
|
||||
Args:
|
||||
x: Starting X position
|
||||
y: Starting Y position
|
||||
enemy_type: Type of enemy ('goblin', 'orc', 'troll')
|
||||
texture: Texture for enemy sprite
|
||||
grid: Grid to add enemy to
|
||||
|
||||
Returns:
|
||||
A new Enemy instance
|
||||
"""
|
||||
return Enemy(x, y, enemy_type, texture, grid)
|
||||
313
docs/templates/complete/game.py
vendored
|
|
@ -1,313 +0,0 @@
|
|||
"""
|
||||
game.py - Main Entry Point for McRogueFace Complete Roguelike Template
|
||||
|
||||
This is the main game file that ties everything together:
|
||||
- Scene setup
|
||||
- Input handling
|
||||
- Game loop
|
||||
- Level transitions
|
||||
|
||||
To run: Copy this template to your McRogueFace scripts/ directory
|
||||
and rename to game.py (or import from game.py).
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
from typing import List, Optional
|
||||
|
||||
# Import game modules
|
||||
from constants import (
|
||||
SCREEN_WIDTH, SCREEN_HEIGHT,
|
||||
GRID_X, GRID_Y, GRID_WIDTH, GRID_HEIGHT,
|
||||
DUNGEON_WIDTH, DUNGEON_HEIGHT,
|
||||
TEXTURE_PATH, FONT_PATH,
|
||||
KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT,
|
||||
KEY_UP_LEFT, KEY_UP_RIGHT, KEY_DOWN_LEFT, KEY_DOWN_RIGHT,
|
||||
KEY_WAIT, KEY_DESCEND,
|
||||
MSG_WELCOME, MSG_DESCEND, MSG_BLOCKED, MSG_STAIRS, MSG_DEATH, MSG_NO_STAIRS,
|
||||
FOV_RADIUS, COLOR_FOG, COLOR_REMEMBERED, COLOR_VISIBLE
|
||||
)
|
||||
from dungeon import Dungeon, generate_dungeon
|
||||
from entities import Player, Enemy, create_player, create_enemy
|
||||
from turns import TurnManager, GameState
|
||||
from ui import GameUI, DeathScreen
|
||||
|
||||
|
||||
class Game:
|
||||
"""
|
||||
Main game class that manages the complete roguelike experience.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the game."""
|
||||
# Load resources
|
||||
self.texture = mcrfpy.Texture(TEXTURE_PATH, 16, 16)
|
||||
self.font = mcrfpy.Font(FONT_PATH)
|
||||
|
||||
# Create scene
|
||||
mcrfpy.createScene("game")
|
||||
self.ui_collection = mcrfpy.sceneUI("game")
|
||||
|
||||
# Create grid
|
||||
self.grid = mcrfpy.Grid(
|
||||
DUNGEON_WIDTH, DUNGEON_HEIGHT,
|
||||
self.texture,
|
||||
GRID_X, GRID_Y,
|
||||
GRID_WIDTH, GRID_HEIGHT
|
||||
)
|
||||
self.ui_collection.append(self.grid)
|
||||
|
||||
# Game state
|
||||
self.dungeon: Optional[Dungeon] = None
|
||||
self.player: Optional[Player] = None
|
||||
self.enemies: List[Enemy] = []
|
||||
self.turn_manager: Optional[TurnManager] = None
|
||||
self.current_level = 1
|
||||
|
||||
# UI
|
||||
self.game_ui = GameUI(self.font)
|
||||
self.game_ui.add_to_scene(self.ui_collection)
|
||||
|
||||
self.death_screen: Optional[DeathScreen] = None
|
||||
self.game_over = False
|
||||
|
||||
# Set up input handling
|
||||
mcrfpy.keypressScene(self.handle_keypress)
|
||||
|
||||
# Start the game
|
||||
self.new_game()
|
||||
|
||||
# Switch to game scene
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
def new_game(self) -> None:
|
||||
"""Start a new game from level 1."""
|
||||
self.current_level = 1
|
||||
self.game_over = False
|
||||
|
||||
# Clear any death screen
|
||||
if self.death_screen:
|
||||
self.death_screen.remove_from_scene(self.ui_collection)
|
||||
self.death_screen = None
|
||||
|
||||
# Generate first level
|
||||
self.generate_level()
|
||||
|
||||
# Welcome message
|
||||
self.game_ui.clear_messages()
|
||||
self.game_ui.add_message(MSG_WELCOME, (255, 255, 100, 255))
|
||||
|
||||
def generate_level(self) -> None:
|
||||
"""Generate a new dungeon level."""
|
||||
# Clear existing entities from grid
|
||||
while len(self.grid.entities) > 0:
|
||||
self.grid.entities.remove(0)
|
||||
|
||||
self.enemies.clear()
|
||||
|
||||
# Generate dungeon
|
||||
self.dungeon = generate_dungeon(self.current_level)
|
||||
self.dungeon.apply_to_grid(self.grid)
|
||||
|
||||
# Create player at start position
|
||||
start_x, start_y = self.dungeon.player_start
|
||||
self.player = create_player(start_x, start_y, self.texture, self.grid)
|
||||
self.player.dungeon_level = self.current_level
|
||||
|
||||
# Spawn enemies
|
||||
enemy_spawns = self.dungeon.get_enemy_spawns()
|
||||
for enemy_type, x, y in enemy_spawns:
|
||||
enemy = create_enemy(x, y, enemy_type, self.texture, self.grid)
|
||||
self.enemies.append(enemy)
|
||||
|
||||
# Set up turn manager
|
||||
self.turn_manager = TurnManager(self.player, self.enemies, self.dungeon)
|
||||
self.turn_manager.on_message = self.game_ui.add_message
|
||||
self.turn_manager.on_player_death = self.on_player_death
|
||||
|
||||
# Update FOV
|
||||
self.update_fov()
|
||||
|
||||
# Center camera on player
|
||||
self.center_camera()
|
||||
|
||||
# Update UI
|
||||
self.game_ui.update_level(self.current_level)
|
||||
self.update_ui()
|
||||
|
||||
def descend(self) -> None:
|
||||
"""Go down to the next dungeon level."""
|
||||
# Check if player is on stairs
|
||||
if self.player.pos != self.dungeon.stairs_pos:
|
||||
self.game_ui.add_message(MSG_NO_STAIRS, (150, 150, 150, 255))
|
||||
return
|
||||
|
||||
self.current_level += 1
|
||||
self.game_ui.add_message(MSG_DESCEND % self.current_level, (100, 100, 255, 255))
|
||||
|
||||
# Keep player stats
|
||||
old_hp = self.player.fighter.hp
|
||||
old_max_hp = self.player.fighter.max_hp
|
||||
old_attack = self.player.fighter.attack
|
||||
old_defense = self.player.fighter.defense
|
||||
old_xp = self.player.xp
|
||||
old_level = self.player.level
|
||||
|
||||
# Generate new level
|
||||
self.generate_level()
|
||||
|
||||
# Restore player stats
|
||||
self.player.fighter.hp = old_hp
|
||||
self.player.fighter.max_hp = old_max_hp
|
||||
self.player.fighter.attack = old_attack
|
||||
self.player.fighter.defense = old_defense
|
||||
self.player.xp = old_xp
|
||||
self.player.level = old_level
|
||||
|
||||
self.update_ui()
|
||||
|
||||
def update_fov(self) -> None:
|
||||
"""Update field of view and apply to grid tiles."""
|
||||
if not self.player or not self.dungeon:
|
||||
return
|
||||
|
||||
# Use entity's built-in FOV calculation
|
||||
self.player.entity.update_visibility()
|
||||
|
||||
# Apply visibility to tiles
|
||||
for x in range(self.dungeon.width):
|
||||
for y in range(self.dungeon.height):
|
||||
point = self.grid.at(x, y)
|
||||
tile = self.dungeon.get_tile(x, y)
|
||||
|
||||
if tile:
|
||||
state = self.player.entity.at(x, y)
|
||||
|
||||
if state.visible:
|
||||
# Currently visible
|
||||
tile.explored = True
|
||||
tile.visible = True
|
||||
point.color_overlay = mcrfpy.Color(*COLOR_VISIBLE)
|
||||
elif tile.explored:
|
||||
# Explored but not visible
|
||||
tile.visible = False
|
||||
point.color_overlay = mcrfpy.Color(*COLOR_REMEMBERED)
|
||||
else:
|
||||
# Never seen
|
||||
point.color_overlay = mcrfpy.Color(*COLOR_FOG)
|
||||
|
||||
def center_camera(self) -> None:
|
||||
"""Center the camera on the player."""
|
||||
if self.player:
|
||||
self.grid.center = (self.player.x, self.player.y)
|
||||
|
||||
def update_ui(self) -> None:
|
||||
"""Update all UI elements."""
|
||||
if self.player:
|
||||
self.game_ui.update_hp(
|
||||
self.player.fighter.hp,
|
||||
self.player.fighter.max_hp
|
||||
)
|
||||
|
||||
def on_player_death(self) -> None:
|
||||
"""Handle player death."""
|
||||
self.game_over = True
|
||||
self.game_ui.add_message(MSG_DEATH, (255, 0, 0, 255))
|
||||
|
||||
# Show death screen
|
||||
self.death_screen = DeathScreen(self.font)
|
||||
self.death_screen.add_to_scene(self.ui_collection)
|
||||
|
||||
def handle_keypress(self, key: str, state: str) -> None:
|
||||
"""
|
||||
Handle keyboard input.
|
||||
|
||||
Args:
|
||||
key: Key name
|
||||
state: "start" for key down, "end" for key up
|
||||
"""
|
||||
# Only handle key down events
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
# Handle restart when dead
|
||||
if self.game_over:
|
||||
if key == "R":
|
||||
self.new_game()
|
||||
return
|
||||
|
||||
# Handle movement
|
||||
dx, dy = 0, 0
|
||||
|
||||
if key in KEY_UP:
|
||||
dy = -1
|
||||
elif key in KEY_DOWN:
|
||||
dy = 1
|
||||
elif key in KEY_LEFT:
|
||||
dx = -1
|
||||
elif key in KEY_RIGHT:
|
||||
dx = 1
|
||||
elif key in KEY_UP_LEFT:
|
||||
dx, dy = -1, -1
|
||||
elif key in KEY_UP_RIGHT:
|
||||
dx, dy = 1, -1
|
||||
elif key in KEY_DOWN_LEFT:
|
||||
dx, dy = -1, 1
|
||||
elif key in KEY_DOWN_RIGHT:
|
||||
dx, dy = 1, 1
|
||||
elif key in KEY_WAIT:
|
||||
# Skip turn
|
||||
self.turn_manager.handle_wait()
|
||||
self.after_turn()
|
||||
return
|
||||
elif key in KEY_DESCEND:
|
||||
# Try to descend
|
||||
self.descend()
|
||||
return
|
||||
elif key == "Escape":
|
||||
# Quit game
|
||||
mcrfpy.exit()
|
||||
return
|
||||
|
||||
# Process movement/attack
|
||||
if dx != 0 or dy != 0:
|
||||
if self.turn_manager.handle_player_action(dx, dy):
|
||||
self.after_turn()
|
||||
else:
|
||||
# Movement was blocked
|
||||
self.game_ui.add_message(MSG_BLOCKED, (150, 150, 150, 255))
|
||||
|
||||
def after_turn(self) -> None:
|
||||
"""Called after each player turn."""
|
||||
# Update FOV
|
||||
self.update_fov()
|
||||
|
||||
# Center camera
|
||||
self.center_camera()
|
||||
|
||||
# Update UI
|
||||
self.update_ui()
|
||||
|
||||
# Check if standing on stairs
|
||||
if self.player.pos == self.dungeon.stairs_pos:
|
||||
self.game_ui.add_message(MSG_STAIRS, (100, 255, 100, 255))
|
||||
|
||||
# Clean up dead enemies
|
||||
self.enemies = [e for e in self.enemies if e.is_alive]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ENTRY POINT
|
||||
# =============================================================================
|
||||
|
||||
# Global game instance
|
||||
game: Optional[Game] = None
|
||||
|
||||
|
||||
def start_game():
|
||||
"""Start the game."""
|
||||
global game
|
||||
game = Game()
|
||||
|
||||
|
||||
# Auto-start when this script is loaded
|
||||
start_game()
|
||||
232
docs/templates/complete/turns.py
vendored
|
|
@ -1,232 +0,0 @@
|
|||
"""
|
||||
turns.py - Turn Management System for McRogueFace Roguelike
|
||||
|
||||
Handles the turn-based game flow: player turn, then enemy turns.
|
||||
"""
|
||||
|
||||
from enum import Enum, auto
|
||||
from typing import List, Optional, Callable, TYPE_CHECKING
|
||||
|
||||
from entities import Player, Enemy
|
||||
from combat import try_attack, process_kill, CombatResult
|
||||
from ai import process_enemy_turns
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dungeon import Dungeon
|
||||
|
||||
|
||||
class GameState(Enum):
|
||||
"""Current state of the game."""
|
||||
PLAYER_TURN = auto() # Waiting for player input
|
||||
ENEMY_TURN = auto() # Processing enemy actions
|
||||
PLAYER_DEAD = auto() # Player has died
|
||||
VICTORY = auto() # Player has won (optional)
|
||||
LEVEL_TRANSITION = auto() # Moving to next level
|
||||
|
||||
|
||||
class TurnManager:
|
||||
"""
|
||||
Manages the turn-based game loop.
|
||||
|
||||
The game follows this flow:
|
||||
1. Player takes action (move or attack)
|
||||
2. If action was valid, enemies take turns
|
||||
3. Check for game over conditions
|
||||
4. Return to step 1
|
||||
"""
|
||||
|
||||
def __init__(self, player: Player, enemies: List[Enemy], dungeon: 'Dungeon'):
|
||||
"""
|
||||
Initialize the turn manager.
|
||||
|
||||
Args:
|
||||
player: The player entity
|
||||
enemies: List of all enemies
|
||||
dungeon: The dungeon map
|
||||
"""
|
||||
self.player = player
|
||||
self.enemies = enemies
|
||||
self.dungeon = dungeon
|
||||
self.state = GameState.PLAYER_TURN
|
||||
self.turn_count = 0
|
||||
|
||||
# Callbacks for game events
|
||||
self.on_message: Optional[Callable[[str, tuple], None]] = None
|
||||
self.on_player_death: Optional[Callable[[], None]] = None
|
||||
self.on_enemy_death: Optional[Callable[[Enemy], None]] = None
|
||||
self.on_turn_end: Optional[Callable[[int], None]] = None
|
||||
|
||||
def reset(self, player: Player, enemies: List[Enemy], dungeon: 'Dungeon') -> None:
|
||||
"""Reset the turn manager with new game state."""
|
||||
self.player = player
|
||||
self.enemies = enemies
|
||||
self.dungeon = dungeon
|
||||
self.state = GameState.PLAYER_TURN
|
||||
self.turn_count = 0
|
||||
|
||||
def add_message(self, message: str, color: tuple = (255, 255, 255, 255)) -> None:
|
||||
"""Add a message to the log via callback."""
|
||||
if self.on_message:
|
||||
self.on_message(message, color)
|
||||
|
||||
def handle_player_action(self, dx: int, dy: int) -> bool:
|
||||
"""
|
||||
Handle a player movement or attack action.
|
||||
|
||||
Args:
|
||||
dx: X direction (-1, 0, or 1)
|
||||
dy: Y direction (-1, 0, or 1)
|
||||
|
||||
Returns:
|
||||
True if the action consumed a turn, False otherwise
|
||||
"""
|
||||
if self.state != GameState.PLAYER_TURN:
|
||||
return False
|
||||
|
||||
target_x = self.player.x + dx
|
||||
target_y = self.player.y + dy
|
||||
|
||||
# Check for attack
|
||||
result = try_attack(self.player, target_x, target_y, self.enemies)
|
||||
|
||||
if result:
|
||||
# Player attacked something
|
||||
self.add_message(result.message, result.message_color)
|
||||
|
||||
if result.killed:
|
||||
# Process kill
|
||||
xp = process_kill(self.player, result.defender)
|
||||
self.enemies.remove(result.defender)
|
||||
|
||||
if xp > 0:
|
||||
self.add_message(f"You gain {xp} XP!", (255, 255, 100, 255))
|
||||
|
||||
if self.on_enemy_death:
|
||||
self.on_enemy_death(result.defender)
|
||||
|
||||
# Action consumed a turn
|
||||
self._end_player_turn()
|
||||
return True
|
||||
|
||||
# No attack - try to move
|
||||
if self.dungeon.is_walkable(target_x, target_y):
|
||||
# Check for enemy blocking
|
||||
blocked = False
|
||||
for enemy in self.enemies:
|
||||
if enemy.is_alive and enemy.x == target_x and enemy.y == target_y:
|
||||
blocked = True
|
||||
break
|
||||
|
||||
if not blocked:
|
||||
self.player.move_to(target_x, target_y)
|
||||
self._end_player_turn()
|
||||
return True
|
||||
|
||||
# Movement blocked
|
||||
return False
|
||||
|
||||
def handle_wait(self) -> bool:
|
||||
"""
|
||||
Handle the player choosing to wait (skip turn).
|
||||
|
||||
Returns:
|
||||
True (always consumes a turn)
|
||||
"""
|
||||
if self.state != GameState.PLAYER_TURN:
|
||||
return False
|
||||
|
||||
self.add_message("You wait...", (150, 150, 150, 255))
|
||||
self._end_player_turn()
|
||||
return True
|
||||
|
||||
def _end_player_turn(self) -> None:
|
||||
"""End the player's turn and process enemy turns."""
|
||||
self.state = GameState.ENEMY_TURN
|
||||
self._process_enemy_turns()
|
||||
|
||||
def _process_enemy_turns(self) -> None:
|
||||
"""Process all enemy turns."""
|
||||
# Get combat results from enemy actions
|
||||
results = process_enemy_turns(
|
||||
self.enemies,
|
||||
self.player,
|
||||
self.dungeon
|
||||
)
|
||||
|
||||
# Report results
|
||||
for result in results:
|
||||
self.add_message(result.message, result.message_color)
|
||||
|
||||
# Check if player died
|
||||
if not self.player.is_alive:
|
||||
self.state = GameState.PLAYER_DEAD
|
||||
if self.on_player_death:
|
||||
self.on_player_death()
|
||||
else:
|
||||
# End turn
|
||||
self.turn_count += 1
|
||||
self.state = GameState.PLAYER_TURN
|
||||
|
||||
if self.on_turn_end:
|
||||
self.on_turn_end(self.turn_count)
|
||||
|
||||
def is_player_turn(self) -> bool:
|
||||
"""Check if it's the player's turn."""
|
||||
return self.state == GameState.PLAYER_TURN
|
||||
|
||||
def is_game_over(self) -> bool:
|
||||
"""Check if the game is over (player dead)."""
|
||||
return self.state == GameState.PLAYER_DEAD
|
||||
|
||||
def get_enemy_count(self) -> int:
|
||||
"""Get the number of living enemies."""
|
||||
return sum(1 for e in self.enemies if e.is_alive)
|
||||
|
||||
|
||||
class ActionResult:
|
||||
"""Result of a player action."""
|
||||
|
||||
def __init__(self, success: bool, message: str = "",
|
||||
color: tuple = (255, 255, 255, 255)):
|
||||
self.success = success
|
||||
self.message = message
|
||||
self.color = color
|
||||
|
||||
|
||||
def try_move_or_attack(player: Player, dx: int, dy: int,
|
||||
dungeon: 'Dungeon', enemies: List[Enemy]) -> ActionResult:
|
||||
"""
|
||||
Attempt to move or attack in a direction.
|
||||
|
||||
This is a simpler, standalone function for games that don't want
|
||||
the full TurnManager.
|
||||
|
||||
Args:
|
||||
player: The player
|
||||
dx: X direction
|
||||
dy: Y direction
|
||||
dungeon: The dungeon map
|
||||
enemies: List of enemies
|
||||
|
||||
Returns:
|
||||
ActionResult indicating success and any message
|
||||
"""
|
||||
target_x = player.x + dx
|
||||
target_y = player.y + dy
|
||||
|
||||
# Check for attack
|
||||
for enemy in enemies:
|
||||
if enemy.is_alive and enemy.x == target_x and enemy.y == target_y:
|
||||
result = try_attack(player, target_x, target_y, enemies)
|
||||
if result:
|
||||
if result.killed:
|
||||
process_kill(player, enemy)
|
||||
enemies.remove(enemy)
|
||||
return ActionResult(True, result.message, result.message_color)
|
||||
|
||||
# Check for movement
|
||||
if dungeon.is_walkable(target_x, target_y):
|
||||
player.move_to(target_x, target_y)
|
||||
return ActionResult(True)
|
||||
|
||||
return ActionResult(False, "You can't move there!", (150, 150, 150, 255))
|
||||
330
docs/templates/complete/ui.py
vendored
|
|
@ -1,330 +0,0 @@
|
|||
"""
|
||||
ui.py - User Interface Components for McRogueFace Roguelike
|
||||
|
||||
Contains the health bar and message log UI elements.
|
||||
"""
|
||||
|
||||
from typing import List, Tuple, Optional
|
||||
from dataclasses import dataclass
|
||||
import mcrfpy
|
||||
|
||||
from constants import (
|
||||
HP_BAR_X, HP_BAR_Y, HP_BAR_WIDTH, HP_BAR_HEIGHT,
|
||||
MSG_LOG_X, MSG_LOG_Y, MSG_LOG_WIDTH, MSG_LOG_HEIGHT, MSG_LOG_MAX_LINES,
|
||||
LEVEL_DISPLAY_X, LEVEL_DISPLAY_Y,
|
||||
COLOR_UI_BG, COLOR_UI_BORDER, COLOR_TEXT,
|
||||
COLOR_HP_BAR_BG, COLOR_HP_BAR_FILL, COLOR_HP_BAR_WARNING, COLOR_HP_BAR_CRITICAL,
|
||||
COLOR_MSG_DEFAULT
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
"""A message in the message log."""
|
||||
text: str
|
||||
color: Tuple[int, int, int, int]
|
||||
|
||||
|
||||
class HealthBar:
|
||||
"""
|
||||
Visual health bar displaying player HP.
|
||||
|
||||
Uses nested Frames: an outer background frame and an inner fill frame
|
||||
that resizes based on HP percentage.
|
||||
"""
|
||||
|
||||
def __init__(self, x: int = HP_BAR_X, y: int = HP_BAR_Y,
|
||||
width: int = HP_BAR_WIDTH, height: int = HP_BAR_HEIGHT,
|
||||
font: mcrfpy.Font = None):
|
||||
"""
|
||||
Create a health bar.
|
||||
|
||||
Args:
|
||||
x: X position
|
||||
y: Y position
|
||||
width: Total width of the bar
|
||||
height: Height of the bar
|
||||
font: Font for the HP text
|
||||
"""
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.font = font or mcrfpy.default_font
|
||||
|
||||
# Background frame
|
||||
self.bg_frame = mcrfpy.Frame(x, y, width, height)
|
||||
self.bg_frame.fill_color = mcrfpy.Color(*COLOR_HP_BAR_BG)
|
||||
self.bg_frame.outline = 2
|
||||
self.bg_frame.outline_color = mcrfpy.Color(*COLOR_UI_BORDER)
|
||||
|
||||
# Fill frame (inside background)
|
||||
self.fill_frame = mcrfpy.Frame(x + 2, y + 2, width - 4, height - 4)
|
||||
self.fill_frame.fill_color = mcrfpy.Color(*COLOR_HP_BAR_FILL)
|
||||
self.fill_frame.outline = 0
|
||||
|
||||
# HP text
|
||||
self.hp_text = mcrfpy.Caption("HP: 0 / 0", self.font, x + 8, y + 4)
|
||||
self.hp_text.fill_color = mcrfpy.Color(*COLOR_TEXT)
|
||||
|
||||
self._max_fill_width = width - 4
|
||||
|
||||
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
|
||||
"""Add all health bar components to a scene."""
|
||||
ui.append(self.bg_frame)
|
||||
ui.append(self.fill_frame)
|
||||
ui.append(self.hp_text)
|
||||
|
||||
def update(self, current_hp: int, max_hp: int) -> None:
|
||||
"""
|
||||
Update the health bar display.
|
||||
|
||||
Args:
|
||||
current_hp: Current hit points
|
||||
max_hp: Maximum hit points
|
||||
"""
|
||||
# Calculate fill percentage
|
||||
if max_hp <= 0:
|
||||
percent = 0.0
|
||||
else:
|
||||
percent = max(0.0, min(1.0, current_hp / max_hp))
|
||||
|
||||
# Update fill bar width
|
||||
self.fill_frame.w = int(self._max_fill_width * percent)
|
||||
|
||||
# Update color based on HP percentage
|
||||
if percent > 0.6:
|
||||
color = COLOR_HP_BAR_FILL
|
||||
elif percent > 0.3:
|
||||
color = COLOR_HP_BAR_WARNING
|
||||
else:
|
||||
color = COLOR_HP_BAR_CRITICAL
|
||||
|
||||
self.fill_frame.fill_color = mcrfpy.Color(*color)
|
||||
|
||||
# Update text
|
||||
self.hp_text.text = f"HP: {current_hp} / {max_hp}"
|
||||
|
||||
|
||||
class MessageLog:
|
||||
"""
|
||||
Scrolling message log displaying game events.
|
||||
|
||||
Uses a Frame container with Caption children for each line.
|
||||
"""
|
||||
|
||||
def __init__(self, x: int = MSG_LOG_X, y: int = MSG_LOG_Y,
|
||||
width: int = MSG_LOG_WIDTH, height: int = MSG_LOG_HEIGHT,
|
||||
max_lines: int = MSG_LOG_MAX_LINES,
|
||||
font: mcrfpy.Font = None):
|
||||
"""
|
||||
Create a message log.
|
||||
|
||||
Args:
|
||||
x: X position
|
||||
y: Y position
|
||||
width: Width of the log
|
||||
height: Height of the log
|
||||
max_lines: Maximum number of visible lines
|
||||
font: Font for the messages
|
||||
"""
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.max_lines = max_lines
|
||||
self.font = font or mcrfpy.default_font
|
||||
|
||||
# Container frame
|
||||
self.frame = mcrfpy.Frame(x, y, width, height)
|
||||
self.frame.fill_color = mcrfpy.Color(*COLOR_UI_BG)
|
||||
self.frame.outline = 1
|
||||
self.frame.outline_color = mcrfpy.Color(*COLOR_UI_BORDER)
|
||||
|
||||
# Message storage
|
||||
self.messages: List[Message] = []
|
||||
self.captions: List[mcrfpy.Caption] = []
|
||||
|
||||
# Line height (approximate based on font)
|
||||
self.line_height = 18
|
||||
|
||||
# Create caption objects for each line
|
||||
self._init_captions()
|
||||
|
||||
def _init_captions(self) -> None:
|
||||
"""Initialize caption objects for message display."""
|
||||
for i in range(self.max_lines):
|
||||
caption = mcrfpy.Caption(
|
||||
"",
|
||||
self.font,
|
||||
self.x + 5,
|
||||
self.y + 5 + i * self.line_height
|
||||
)
|
||||
caption.fill_color = mcrfpy.Color(*COLOR_MSG_DEFAULT)
|
||||
self.captions.append(caption)
|
||||
|
||||
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
|
||||
"""Add the message log to a scene."""
|
||||
ui.append(self.frame)
|
||||
for caption in self.captions:
|
||||
ui.append(caption)
|
||||
|
||||
def add_message(self, text: str,
|
||||
color: Tuple[int, int, int, int] = COLOR_MSG_DEFAULT) -> None:
|
||||
"""
|
||||
Add a message to the log.
|
||||
|
||||
Args:
|
||||
text: Message text
|
||||
color: Text color as (R, G, B, A)
|
||||
"""
|
||||
self.messages.append(Message(text, color))
|
||||
|
||||
# Trim old messages
|
||||
if len(self.messages) > 100:
|
||||
self.messages = self.messages[-100:]
|
||||
|
||||
# Update display
|
||||
self._update_display()
|
||||
|
||||
def _update_display(self) -> None:
|
||||
"""Update the displayed messages."""
|
||||
# Get the most recent messages
|
||||
recent = self.messages[-self.max_lines:]
|
||||
|
||||
for i, caption in enumerate(self.captions):
|
||||
if i < len(recent):
|
||||
msg = recent[i]
|
||||
caption.text = msg.text
|
||||
caption.fill_color = mcrfpy.Color(*msg.color)
|
||||
else:
|
||||
caption.text = ""
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all messages."""
|
||||
self.messages.clear()
|
||||
self._update_display()
|
||||
|
||||
|
||||
class LevelDisplay:
|
||||
"""Simple display showing current dungeon level."""
|
||||
|
||||
def __init__(self, x: int = LEVEL_DISPLAY_X, y: int = LEVEL_DISPLAY_Y,
|
||||
font: mcrfpy.Font = None):
|
||||
"""
|
||||
Create a level display.
|
||||
|
||||
Args:
|
||||
x: X position
|
||||
y: Y position
|
||||
font: Font for the text
|
||||
"""
|
||||
self.font = font or mcrfpy.default_font
|
||||
|
||||
self.caption = mcrfpy.Caption("Level: 1", self.font, x, y)
|
||||
self.caption.fill_color = mcrfpy.Color(*COLOR_TEXT)
|
||||
|
||||
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
|
||||
"""Add to a scene."""
|
||||
ui.append(self.caption)
|
||||
|
||||
def update(self, level: int) -> None:
|
||||
"""Update the displayed level."""
|
||||
self.caption.text = f"Dungeon Level: {level}"
|
||||
|
||||
|
||||
class GameUI:
|
||||
"""
|
||||
Container for all UI elements.
|
||||
|
||||
Provides a single point of access for updating the entire UI.
|
||||
"""
|
||||
|
||||
def __init__(self, font: mcrfpy.Font = None):
|
||||
"""
|
||||
Create the game UI.
|
||||
|
||||
Args:
|
||||
font: Font for all UI elements
|
||||
"""
|
||||
self.font = font or mcrfpy.default_font
|
||||
|
||||
# Create UI components
|
||||
self.health_bar = HealthBar(font=self.font)
|
||||
self.message_log = MessageLog(font=self.font)
|
||||
self.level_display = LevelDisplay(font=self.font)
|
||||
|
||||
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
|
||||
"""Add all UI elements to a scene."""
|
||||
self.health_bar.add_to_scene(ui)
|
||||
self.message_log.add_to_scene(ui)
|
||||
self.level_display.add_to_scene(ui)
|
||||
|
||||
def update_hp(self, current_hp: int, max_hp: int) -> None:
|
||||
"""Update the health bar."""
|
||||
self.health_bar.update(current_hp, max_hp)
|
||||
|
||||
def add_message(self, text: str,
|
||||
color: Tuple[int, int, int, int] = COLOR_MSG_DEFAULT) -> None:
|
||||
"""Add a message to the log."""
|
||||
self.message_log.add_message(text, color)
|
||||
|
||||
def update_level(self, level: int) -> None:
|
||||
"""Update the dungeon level display."""
|
||||
self.level_display.update(level)
|
||||
|
||||
def clear_messages(self) -> None:
|
||||
"""Clear the message log."""
|
||||
self.message_log.clear()
|
||||
|
||||
|
||||
class DeathScreen:
|
||||
"""Game over screen shown when player dies."""
|
||||
|
||||
def __init__(self, font: mcrfpy.Font = None):
|
||||
"""
|
||||
Create the death screen.
|
||||
|
||||
Args:
|
||||
font: Font for text
|
||||
"""
|
||||
self.font = font or mcrfpy.default_font
|
||||
self.elements: List = []
|
||||
|
||||
# Semi-transparent overlay
|
||||
self.overlay = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
self.overlay.fill_color = mcrfpy.Color(0, 0, 0, 180)
|
||||
self.elements.append(self.overlay)
|
||||
|
||||
# Death message
|
||||
self.death_text = mcrfpy.Caption(
|
||||
"YOU HAVE DIED",
|
||||
self.font,
|
||||
362, 300
|
||||
)
|
||||
self.death_text.fill_color = mcrfpy.Color(255, 0, 0, 255)
|
||||
self.death_text.outline = 2
|
||||
self.death_text.outline_color = mcrfpy.Color(0, 0, 0, 255)
|
||||
self.elements.append(self.death_text)
|
||||
|
||||
# Restart prompt
|
||||
self.restart_text = mcrfpy.Caption(
|
||||
"Press R to restart",
|
||||
self.font,
|
||||
400, 400
|
||||
)
|
||||
self.restart_text.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
||||
self.elements.append(self.restart_text)
|
||||
|
||||
def add_to_scene(self, ui: mcrfpy.UICollection) -> None:
|
||||
"""Add death screen elements to a scene."""
|
||||
for element in self.elements:
|
||||
ui.append(element)
|
||||
|
||||
def remove_from_scene(self, ui: mcrfpy.UICollection) -> None:
|
||||
"""Remove death screen elements from a scene."""
|
||||
for element in self.elements:
|
||||
try:
|
||||
ui.remove(element)
|
||||
except (ValueError, RuntimeError):
|
||||
pass
|
||||
176
docs/templates/minimal/game.py
vendored
|
|
@ -1,176 +0,0 @@
|
|||
"""
|
||||
McRogueFace Minimal Template
|
||||
============================
|
||||
|
||||
A starting point for simple roguelike prototypes.
|
||||
|
||||
This template demonstrates:
|
||||
- Scene object pattern (preferred OOP approach)
|
||||
- Grid-based movement with boundary checking
|
||||
- Keyboard input handling
|
||||
- Entity positioning on a grid
|
||||
|
||||
Usage:
|
||||
Place this file in your McRogueFace scripts directory and run McRogueFace.
|
||||
Use arrow keys to move the @ symbol. Press Escape to exit.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# =============================================================================
|
||||
# CONSTANTS
|
||||
# =============================================================================
|
||||
|
||||
# Grid dimensions (in tiles)
|
||||
GRID_WIDTH: int = 20
|
||||
GRID_HEIGHT: int = 15
|
||||
|
||||
# Tile size in pixels (must match your sprite sheet)
|
||||
TILE_SIZE: int = 16
|
||||
|
||||
# CP437 sprite indices (standard roguelike character mapping)
|
||||
# In CP437, character codes map to sprite indices: '@' = 64, '.' = 46, etc.
|
||||
SPRITE_PLAYER: int = 64 # '@' symbol
|
||||
SPRITE_FLOOR: int = 46 # '.' symbol
|
||||
|
||||
# Colors (RGBA tuples)
|
||||
COLOR_BACKGROUND: tuple[int, int, int] = (20, 20, 30)
|
||||
|
||||
# =============================================================================
|
||||
# GAME STATE
|
||||
# =============================================================================
|
||||
|
||||
# Player position in grid coordinates
|
||||
player_x: int = GRID_WIDTH // 2
|
||||
player_y: int = GRID_HEIGHT // 2
|
||||
|
||||
# Reference to player entity (set during setup)
|
||||
player_entity: mcrfpy.Entity = None
|
||||
|
||||
# =============================================================================
|
||||
# MOVEMENT LOGIC
|
||||
# =============================================================================
|
||||
|
||||
def try_move(dx: int, dy: int) -> bool:
|
||||
"""
|
||||
Attempt to move the player by (dx, dy) tiles.
|
||||
|
||||
Args:
|
||||
dx: Horizontal movement (-1 = left, +1 = right, 0 = none)
|
||||
dy: Vertical movement (-1 = up, +1 = down, 0 = none)
|
||||
|
||||
Returns:
|
||||
True if movement succeeded, False if blocked by boundary
|
||||
"""
|
||||
global player_x, player_y
|
||||
|
||||
new_x = player_x + dx
|
||||
new_y = player_y + dy
|
||||
|
||||
# Boundary checking: ensure player stays within grid
|
||||
if 0 <= new_x < GRID_WIDTH and 0 <= new_y < GRID_HEIGHT:
|
||||
player_x = new_x
|
||||
player_y = new_y
|
||||
|
||||
# Update the entity's position on the grid
|
||||
player_entity.x = player_x
|
||||
player_entity.y = player_y
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# =============================================================================
|
||||
# INPUT HANDLING
|
||||
# =============================================================================
|
||||
|
||||
def handle_keypress(key: str, action: str) -> None:
|
||||
"""
|
||||
Handle keyboard input for the game scene.
|
||||
|
||||
Args:
|
||||
key: The key that was pressed (e.g., "Up", "Down", "Escape", "a", "W")
|
||||
action: Either "start" (key pressed) or "end" (key released)
|
||||
|
||||
Note:
|
||||
We only process on "start" to avoid double-triggering on key release.
|
||||
"""
|
||||
if action != "start":
|
||||
return
|
||||
|
||||
# Movement keys (both arrow keys and WASD)
|
||||
if key == "Up" or key == "W" or key == "w":
|
||||
try_move(0, -1)
|
||||
elif key == "Down" or key == "S" or key == "s":
|
||||
try_move(0, 1)
|
||||
elif key == "Left" or key == "A" or key == "a":
|
||||
try_move(-1, 0)
|
||||
elif key == "Right" or key == "D" or key == "d":
|
||||
try_move(1, 0)
|
||||
|
||||
# Exit on Escape
|
||||
elif key == "Escape":
|
||||
mcrfpy.exit()
|
||||
|
||||
# =============================================================================
|
||||
# SCENE SETUP
|
||||
# =============================================================================
|
||||
|
||||
def setup_game() -> mcrfpy.Scene:
|
||||
"""
|
||||
Create and configure the game scene.
|
||||
|
||||
Returns:
|
||||
The configured Scene object, ready to be activated.
|
||||
"""
|
||||
global player_entity
|
||||
|
||||
# Create the scene using the OOP pattern (preferred over createScene)
|
||||
scene = mcrfpy.Scene("game")
|
||||
|
||||
# Load the sprite sheet texture
|
||||
# Adjust the path and tile size to match your assets
|
||||
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", TILE_SIZE, TILE_SIZE)
|
||||
|
||||
# Create the game grid
|
||||
# Grid(pos, size, grid_size) where:
|
||||
# pos = pixel position on screen
|
||||
# size = pixel dimensions of the grid display
|
||||
# grid_size = number of tiles (columns, rows)
|
||||
grid = mcrfpy.Grid(
|
||||
pos=(32, 32),
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
texture=texture
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(*COLOR_BACKGROUND)
|
||||
|
||||
# Fill the grid with floor tiles
|
||||
for x in range(GRID_WIDTH):
|
||||
for y in range(GRID_HEIGHT):
|
||||
point = grid.at(x, y)
|
||||
point.tilesprite = SPRITE_FLOOR
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
# Create the player entity
|
||||
player_entity = mcrfpy.Entity(
|
||||
pos=(player_x, player_y),
|
||||
texture=texture,
|
||||
sprite_index=SPRITE_PLAYER
|
||||
)
|
||||
grid.entities.append(player_entity)
|
||||
|
||||
# Add the grid to the scene's UI
|
||||
scene.children.append(grid)
|
||||
|
||||
# Set up keyboard input handler for this scene
|
||||
scene.on_key = handle_keypress
|
||||
|
||||
return scene
|
||||
|
||||
# =============================================================================
|
||||
# MAIN ENTRY POINT
|
||||
# =============================================================================
|
||||
|
||||
# Create and activate the game scene
|
||||
game_scene = setup_game()
|
||||
game_scene.activate()
|
||||
138
docs/templates/roguelike/constants.py
vendored
|
|
@ -1,138 +0,0 @@
|
|||
"""
|
||||
constants.py - Roguelike Template Constants
|
||||
|
||||
This module defines all the constants used throughout the roguelike template,
|
||||
including sprite indices for CP437 tileset, colors for FOV system, and
|
||||
game configuration values.
|
||||
|
||||
CP437 is the classic IBM PC character set commonly used in traditional roguelikes.
|
||||
The sprite indices correspond to ASCII character codes in a CP437 tileset.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# =============================================================================
|
||||
# SPRITE INDICES (CP437 Character Codes)
|
||||
# =============================================================================
|
||||
# These indices correspond to characters in a CP437-style tileset.
|
||||
# The default McRogueFace tileset uses 16x16 sprites arranged in a grid.
|
||||
|
||||
# Terrain sprites
|
||||
SPRITE_FLOOR = 46 # '.' - Standard floor tile
|
||||
SPRITE_WALL = 35 # '#' - Wall/obstacle tile
|
||||
SPRITE_DOOR_CLOSED = 43 # '+' - Closed door
|
||||
SPRITE_DOOR_OPEN = 47 # '/' - Open door
|
||||
SPRITE_STAIRS_DOWN = 62 # '>' - Stairs going down
|
||||
SPRITE_STAIRS_UP = 60 # '<' - Stairs going up
|
||||
|
||||
# Player sprite
|
||||
SPRITE_PLAYER = 64 # '@' - The classic roguelike player symbol
|
||||
|
||||
# Enemy sprites
|
||||
SPRITE_ORC = 111 # 'o' - Orc enemy
|
||||
SPRITE_TROLL = 84 # 'T' - Troll enemy
|
||||
SPRITE_GOBLIN = 103 # 'g' - Goblin enemy
|
||||
SPRITE_RAT = 114 # 'r' - Giant rat
|
||||
SPRITE_SNAKE = 115 # 's' - Snake
|
||||
SPRITE_ZOMBIE = 90 # 'Z' - Zombie
|
||||
|
||||
# Item sprites
|
||||
SPRITE_POTION = 33 # '!' - Potion
|
||||
SPRITE_SCROLL = 63 # '?' - Scroll
|
||||
SPRITE_GOLD = 36 # '$' - Gold/treasure
|
||||
SPRITE_WEAPON = 41 # ')' - Weapon
|
||||
SPRITE_ARMOR = 91 # '[' - Armor
|
||||
SPRITE_RING = 61 # '=' - Ring
|
||||
|
||||
# =============================================================================
|
||||
# FOV/VISIBILITY COLORS
|
||||
# =============================================================================
|
||||
# These colors are applied as overlays to grid tiles to create the fog of war
|
||||
# effect. The alpha channel determines how much of the original tile shows through.
|
||||
|
||||
# Fully visible - no overlay (alpha = 0 means completely transparent overlay)
|
||||
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0)
|
||||
|
||||
# Previously explored but not currently visible - dim blue-gray overlay
|
||||
# This creates the "memory" effect where you can see the map layout
|
||||
# but not current enemy positions
|
||||
COLOR_EXPLORED = mcrfpy.Color(50, 50, 80, 180)
|
||||
|
||||
# Never seen - completely black (alpha = 255 means fully opaque)
|
||||
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255)
|
||||
|
||||
# =============================================================================
|
||||
# TILE COLORS
|
||||
# =============================================================================
|
||||
# Base colors for different tile types (applied to the tile's color property)
|
||||
|
||||
COLOR_FLOOR = mcrfpy.Color(50, 50, 50) # Dark gray floor
|
||||
COLOR_WALL = mcrfpy.Color(100, 100, 100) # Lighter gray walls
|
||||
COLOR_FLOOR_LIT = mcrfpy.Color(100, 90, 70) # Warm lit floor
|
||||
COLOR_WALL_LIT = mcrfpy.Color(130, 110, 80) # Warm lit walls
|
||||
|
||||
# =============================================================================
|
||||
# ENTITY COLORS
|
||||
# =============================================================================
|
||||
# Colors applied to entity sprites
|
||||
|
||||
COLOR_PLAYER = mcrfpy.Color(255, 255, 255) # White player
|
||||
COLOR_ORC = mcrfpy.Color(63, 127, 63) # Green orc
|
||||
COLOR_TROLL = mcrfpy.Color(0, 127, 0) # Darker green troll
|
||||
COLOR_GOBLIN = mcrfpy.Color(127, 127, 0) # Yellow-green goblin
|
||||
|
||||
# =============================================================================
|
||||
# GAME CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Map dimensions (in tiles)
|
||||
MAP_WIDTH = 80
|
||||
MAP_HEIGHT = 45
|
||||
|
||||
# Room generation parameters
|
||||
ROOM_MIN_SIZE = 6 # Minimum room dimension
|
||||
ROOM_MAX_SIZE = 12 # Maximum room dimension
|
||||
MAX_ROOMS = 30 # Maximum number of rooms to generate
|
||||
|
||||
# FOV settings
|
||||
FOV_RADIUS = 8 # How far the player can see
|
||||
|
||||
# Display settings
|
||||
GRID_PIXEL_WIDTH = 1024 # Grid display width in pixels
|
||||
GRID_PIXEL_HEIGHT = 768 # Grid display height in pixels
|
||||
|
||||
# Sprite size (should match your tileset)
|
||||
SPRITE_WIDTH = 16
|
||||
SPRITE_HEIGHT = 16
|
||||
|
||||
# =============================================================================
|
||||
# ENEMY DEFINITIONS
|
||||
# =============================================================================
|
||||
# Dictionary of enemy types with their properties for easy spawning
|
||||
|
||||
ENEMY_TYPES = {
|
||||
"orc": {
|
||||
"sprite": SPRITE_ORC,
|
||||
"color": COLOR_ORC,
|
||||
"name": "Orc",
|
||||
"hp": 10,
|
||||
"power": 3,
|
||||
"defense": 0,
|
||||
},
|
||||
"troll": {
|
||||
"sprite": SPRITE_TROLL,
|
||||
"color": COLOR_TROLL,
|
||||
"name": "Troll",
|
||||
"hp": 16,
|
||||
"power": 4,
|
||||
"defense": 1,
|
||||
},
|
||||
"goblin": {
|
||||
"sprite": SPRITE_GOBLIN,
|
||||
"color": COLOR_GOBLIN,
|
||||
"name": "Goblin",
|
||||
"hp": 6,
|
||||
"power": 2,
|
||||
"defense": 0,
|
||||
},
|
||||
}
|
||||
340
docs/templates/roguelike/dungeon.py
vendored
|
|
@ -1,340 +0,0 @@
|
|||
"""
|
||||
dungeon.py - Procedural Dungeon Generation
|
||||
|
||||
This module provides classic roguelike dungeon generation using the
|
||||
"rooms and corridors" algorithm:
|
||||
|
||||
1. Generate random non-overlapping rectangular rooms
|
||||
2. Connect rooms with L-shaped corridors
|
||||
3. Mark tiles as walkable/transparent based on terrain type
|
||||
|
||||
The algorithm is simple but effective, producing dungeons similar to
|
||||
the original Rogue game.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import random
|
||||
from typing import Iterator, Tuple, List, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import mcrfpy
|
||||
|
||||
from constants import (
|
||||
MAP_WIDTH, MAP_HEIGHT,
|
||||
ROOM_MIN_SIZE, ROOM_MAX_SIZE, MAX_ROOMS,
|
||||
SPRITE_FLOOR, SPRITE_WALL,
|
||||
COLOR_FLOOR, COLOR_WALL,
|
||||
)
|
||||
|
||||
|
||||
class RectangularRoom:
|
||||
"""
|
||||
A rectangular room in the dungeon.
|
||||
|
||||
This class represents a single room and provides utilities for
|
||||
working with room geometry. Rooms are defined by their top-left
|
||||
corner (x1, y1) and bottom-right corner (x2, y2).
|
||||
|
||||
Attributes:
|
||||
x1, y1: Top-left corner coordinates
|
||||
x2, y2: Bottom-right corner coordinates
|
||||
"""
|
||||
|
||||
def __init__(self, x: int, y: int, width: int, height: int) -> None:
|
||||
"""
|
||||
Create a new rectangular room.
|
||||
|
||||
Args:
|
||||
x: X coordinate of the top-left corner
|
||||
y: Y coordinate of the top-left corner
|
||||
width: Width of the room in tiles
|
||||
height: Height of the room in tiles
|
||||
"""
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
def center(self) -> Tuple[int, int]:
|
||||
"""
|
||||
Return the center coordinates of the room.
|
||||
|
||||
This is useful for connecting rooms with corridors and
|
||||
for placing the player in the starting room.
|
||||
|
||||
Returns:
|
||||
Tuple of (center_x, center_y)
|
||||
"""
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return center_x, center_y
|
||||
|
||||
@property
|
||||
def inner(self) -> Tuple[slice, slice]:
|
||||
"""
|
||||
Return the inner area of the room as a pair of slices.
|
||||
|
||||
The inner area excludes the walls (1 tile border), giving
|
||||
the floor area where entities can be placed.
|
||||
|
||||
Returns:
|
||||
Tuple of (x_slice, y_slice) for array indexing
|
||||
"""
|
||||
# Add 1 to exclude the walls on all sides
|
||||
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
|
||||
|
||||
def intersects(self, other: RectangularRoom) -> bool:
|
||||
"""
|
||||
Check if this room overlaps with another room.
|
||||
|
||||
Used during generation to ensure rooms don't overlap.
|
||||
|
||||
Args:
|
||||
other: Another RectangularRoom to check against
|
||||
|
||||
Returns:
|
||||
True if the rooms overlap, False otherwise
|
||||
"""
|
||||
return (
|
||||
self.x1 <= other.x2
|
||||
and self.x2 >= other.x1
|
||||
and self.y1 <= other.y2
|
||||
and self.y2 >= other.y1
|
||||
)
|
||||
|
||||
def inner_tiles(self) -> Iterator[Tuple[int, int]]:
|
||||
"""
|
||||
Iterate over all floor tile coordinates in the room.
|
||||
|
||||
Yields coordinates for the interior of the room (excluding walls).
|
||||
|
||||
Yields:
|
||||
Tuples of (x, y) coordinates
|
||||
"""
|
||||
for x in range(self.x1 + 1, self.x2):
|
||||
for y in range(self.y1 + 1, self.y2):
|
||||
yield x, y
|
||||
|
||||
|
||||
def tunnel_between(
|
||||
start: Tuple[int, int],
|
||||
end: Tuple[int, int]
|
||||
) -> Iterator[Tuple[int, int]]:
|
||||
"""
|
||||
Generate an L-shaped tunnel between two points.
|
||||
|
||||
The tunnel goes horizontally first, then vertically (or vice versa,
|
||||
chosen randomly). This creates the classic roguelike corridor style.
|
||||
|
||||
Args:
|
||||
start: Starting (x, y) coordinates
|
||||
end: Ending (x, y) coordinates
|
||||
|
||||
Yields:
|
||||
Tuples of (x, y) coordinates for each tile in the tunnel
|
||||
"""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
# Randomly choose whether to go horizontal-first or vertical-first
|
||||
if random.random() < 0.5:
|
||||
# Horizontal first, then vertical
|
||||
corner_x, corner_y = x2, y1
|
||||
else:
|
||||
# Vertical first, then horizontal
|
||||
corner_x, corner_y = x1, y2
|
||||
|
||||
# Generate the horizontal segment
|
||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||
yield x, y1
|
||||
|
||||
# Generate the vertical segment
|
||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||
yield corner_x, y
|
||||
|
||||
# Generate to the endpoint (if needed)
|
||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||
yield x, corner_y
|
||||
|
||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||
yield x2, y
|
||||
|
||||
|
||||
def generate_dungeon(
|
||||
max_rooms: int = MAX_ROOMS,
|
||||
room_min_size: int = ROOM_MIN_SIZE,
|
||||
room_max_size: int = ROOM_MAX_SIZE,
|
||||
map_width: int = MAP_WIDTH,
|
||||
map_height: int = MAP_HEIGHT,
|
||||
) -> List[RectangularRoom]:
|
||||
"""
|
||||
Generate a dungeon using the rooms-and-corridors algorithm.
|
||||
|
||||
This function creates a list of non-overlapping rooms. The actual
|
||||
tile data should be applied to a Grid using populate_grid().
|
||||
|
||||
Algorithm:
|
||||
1. Try to place MAX_ROOMS rooms randomly
|
||||
2. Reject rooms that overlap existing rooms
|
||||
3. Connect each new room to the previous room with a corridor
|
||||
|
||||
Args:
|
||||
max_rooms: Maximum number of rooms to generate
|
||||
room_min_size: Minimum room dimension
|
||||
room_max_size: Maximum room dimension
|
||||
map_width: Width of the dungeon in tiles
|
||||
map_height: Height of the dungeon in tiles
|
||||
|
||||
Returns:
|
||||
List of RectangularRoom objects representing the dungeon layout
|
||||
"""
|
||||
rooms: List[RectangularRoom] = []
|
||||
|
||||
for _ in range(max_rooms):
|
||||
# Random room dimensions
|
||||
room_width = random.randint(room_min_size, room_max_size)
|
||||
room_height = random.randint(room_min_size, room_max_size)
|
||||
|
||||
# Random position (ensuring room fits within map bounds)
|
||||
x = random.randint(0, map_width - room_width - 1)
|
||||
y = random.randint(0, map_height - room_height - 1)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
# Check if this room overlaps with any existing room
|
||||
if any(new_room.intersects(other) for other in rooms):
|
||||
continue # Skip this room, try again
|
||||
|
||||
# Room is valid, add it
|
||||
rooms.append(new_room)
|
||||
|
||||
return rooms
|
||||
|
||||
|
||||
def populate_grid(grid: mcrfpy.Grid, rooms: List[RectangularRoom]) -> None:
|
||||
"""
|
||||
Apply dungeon layout to a McRogueFace Grid.
|
||||
|
||||
This function:
|
||||
1. Fills the entire grid with walls
|
||||
2. Carves out floor tiles for each room
|
||||
3. Carves corridors connecting adjacent rooms
|
||||
4. Sets walkable/transparent flags appropriately
|
||||
|
||||
Args:
|
||||
grid: The McRogueFace Grid to populate
|
||||
rooms: List of RectangularRoom objects from generate_dungeon()
|
||||
"""
|
||||
grid_width, grid_height = grid.grid_size
|
||||
|
||||
# Step 1: Fill entire map with walls
|
||||
for x in range(grid_width):
|
||||
for y in range(grid_height):
|
||||
point = grid.at(x, y)
|
||||
point.tilesprite = SPRITE_WALL
|
||||
point.walkable = False
|
||||
point.transparent = False
|
||||
point.color = COLOR_WALL
|
||||
|
||||
# Step 2: Carve out rooms
|
||||
for room in rooms:
|
||||
for x, y in room.inner_tiles():
|
||||
# Bounds check (room might extend past grid)
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
point.tilesprite = SPRITE_FLOOR
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
point.color = COLOR_FLOOR
|
||||
|
||||
# Step 3: Carve corridors between adjacent rooms
|
||||
for i in range(1, len(rooms)):
|
||||
# Connect each room to the previous room
|
||||
start = rooms[i - 1].center
|
||||
end = rooms[i].center
|
||||
|
||||
for x, y in tunnel_between(start, end):
|
||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
||||
point = grid.at(x, y)
|
||||
point.tilesprite = SPRITE_FLOOR
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
point.color = COLOR_FLOOR
|
||||
|
||||
|
||||
def get_random_floor_position(
|
||||
grid: mcrfpy.Grid,
|
||||
rooms: List[RectangularRoom],
|
||||
exclude_first_room: bool = False
|
||||
) -> Tuple[int, int]:
|
||||
"""
|
||||
Get a random walkable floor position for entity placement.
|
||||
|
||||
This is useful for placing enemies, items, or other entities
|
||||
in valid floor locations.
|
||||
|
||||
Args:
|
||||
grid: The populated Grid to search
|
||||
rooms: List of rooms (used for faster random selection)
|
||||
exclude_first_room: If True, won't return positions from the
|
||||
first room (where the player usually starts)
|
||||
|
||||
Returns:
|
||||
Tuple of (x, y) coordinates of a walkable floor tile
|
||||
"""
|
||||
available_rooms = rooms[1:] if exclude_first_room and len(rooms) > 1 else rooms
|
||||
|
||||
if not available_rooms:
|
||||
# Fallback: find any walkable tile
|
||||
grid_width, grid_height = grid.grid_size
|
||||
walkable_tiles = []
|
||||
for x in range(grid_width):
|
||||
for y in range(grid_height):
|
||||
if grid.at(x, y).walkable:
|
||||
walkable_tiles.append((x, y))
|
||||
return random.choice(walkable_tiles) if walkable_tiles else (1, 1)
|
||||
|
||||
# Pick a random room and a random position within it
|
||||
room = random.choice(available_rooms)
|
||||
floor_tiles = list(room.inner_tiles())
|
||||
return random.choice(floor_tiles)
|
||||
|
||||
|
||||
def get_spawn_positions(
|
||||
rooms: List[RectangularRoom],
|
||||
count: int,
|
||||
exclude_first_room: bool = True
|
||||
) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
Get multiple spawn positions for enemies.
|
||||
|
||||
Distributes enemies across different rooms for better gameplay.
|
||||
|
||||
Args:
|
||||
rooms: List of rooms from dungeon generation
|
||||
count: Number of positions to generate
|
||||
exclude_first_room: If True, won't spawn in the player's starting room
|
||||
|
||||
Returns:
|
||||
List of (x, y) coordinate tuples
|
||||
"""
|
||||
available_rooms = rooms[1:] if exclude_first_room and len(rooms) > 1 else rooms
|
||||
|
||||
if not available_rooms:
|
||||
return []
|
||||
|
||||
positions = []
|
||||
for i in range(count):
|
||||
# Cycle through rooms to distribute enemies
|
||||
room = available_rooms[i % len(available_rooms)]
|
||||
floor_tiles = list(room.inner_tiles())
|
||||
|
||||
# Try to avoid placing on the same tile
|
||||
available_tiles = [t for t in floor_tiles if t not in positions]
|
||||
if available_tiles:
|
||||
positions.append(random.choice(available_tiles))
|
||||
elif floor_tiles:
|
||||
positions.append(random.choice(floor_tiles))
|
||||
|
||||
return positions
|
||||
364
docs/templates/roguelike/entities.py
vendored
|
|
@ -1,364 +0,0 @@
|
|||
"""
|
||||
entities.py - Entity Management for Roguelike Template
|
||||
|
||||
This module provides entity creation and management utilities for the
|
||||
roguelike template. Entities in McRogueFace are game objects that exist
|
||||
on a Grid, such as the player, enemies, items, and NPCs.
|
||||
|
||||
The module includes:
|
||||
- Entity factory functions for creating common entity types
|
||||
- Helper functions for entity management
|
||||
- Simple data containers for entity stats (for future expansion)
|
||||
|
||||
Note: McRogueFace entities are simple position + sprite objects. For
|
||||
complex game logic like AI, combat, and inventory, you'll want to wrap
|
||||
them in Python classes that reference the underlying Entity.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Tuple, Optional, List, Dict, Any, TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import mcrfpy
|
||||
|
||||
from constants import (
|
||||
SPRITE_PLAYER, SPRITE_ORC, SPRITE_TROLL, SPRITE_GOBLIN,
|
||||
COLOR_PLAYER, COLOR_ORC, COLOR_TROLL, COLOR_GOBLIN,
|
||||
ENEMY_TYPES,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityStats:
|
||||
"""
|
||||
Optional stats container for game entities.
|
||||
|
||||
This dataclass can be used to track stats for entities that need them.
|
||||
Attach it to your entity wrapper class for combat, leveling, etc.
|
||||
|
||||
Attributes:
|
||||
hp: Current hit points
|
||||
max_hp: Maximum hit points
|
||||
power: Attack power
|
||||
defense: Damage reduction
|
||||
name: Display name for the entity
|
||||
"""
|
||||
hp: int = 10
|
||||
max_hp: int = 10
|
||||
power: int = 3
|
||||
defense: int = 0
|
||||
name: str = "Unknown"
|
||||
|
||||
@property
|
||||
def is_alive(self) -> bool:
|
||||
"""Check if the entity is still alive."""
|
||||
return self.hp > 0
|
||||
|
||||
def take_damage(self, amount: int) -> int:
|
||||
"""
|
||||
Apply damage, accounting for defense.
|
||||
|
||||
Args:
|
||||
amount: Raw damage amount
|
||||
|
||||
Returns:
|
||||
Actual damage dealt after defense
|
||||
"""
|
||||
actual_damage = max(0, amount - self.defense)
|
||||
self.hp = max(0, self.hp - actual_damage)
|
||||
return actual_damage
|
||||
|
||||
def heal(self, amount: int) -> int:
|
||||
"""
|
||||
Heal the entity.
|
||||
|
||||
Args:
|
||||
amount: Amount to heal
|
||||
|
||||
Returns:
|
||||
Actual amount healed (may be less if near max HP)
|
||||
"""
|
||||
old_hp = self.hp
|
||||
self.hp = min(self.max_hp, self.hp + amount)
|
||||
return self.hp - old_hp
|
||||
|
||||
|
||||
def create_player(
|
||||
grid: mcrfpy.Grid,
|
||||
texture: mcrfpy.Texture,
|
||||
x: int,
|
||||
y: int
|
||||
) -> mcrfpy.Entity:
|
||||
"""
|
||||
Create and place the player entity on the grid.
|
||||
|
||||
The player uses the classic '@' symbol (sprite index 64 in CP437).
|
||||
|
||||
Args:
|
||||
grid: The Grid to place the player on
|
||||
texture: The texture/tileset to use
|
||||
x: Starting X position
|
||||
y: Starting Y position
|
||||
|
||||
Returns:
|
||||
The created player Entity
|
||||
"""
|
||||
import mcrfpy
|
||||
|
||||
player = mcrfpy.Entity(
|
||||
pos=(x, y),
|
||||
texture=texture,
|
||||
sprite_index=SPRITE_PLAYER
|
||||
)
|
||||
grid.entities.append(player)
|
||||
|
||||
return player
|
||||
|
||||
|
||||
def create_enemy(
|
||||
grid: mcrfpy.Grid,
|
||||
texture: mcrfpy.Texture,
|
||||
x: int,
|
||||
y: int,
|
||||
enemy_type: str = "orc"
|
||||
) -> Tuple[mcrfpy.Entity, EntityStats]:
|
||||
"""
|
||||
Create an enemy entity with associated stats.
|
||||
|
||||
Enemy types are defined in constants.py. Currently available:
|
||||
- "orc": Standard enemy, balanced stats
|
||||
- "troll": Tough enemy, high HP and power
|
||||
- "goblin": Weak enemy, low stats
|
||||
|
||||
Args:
|
||||
grid: The Grid to place the enemy on
|
||||
texture: The texture/tileset to use
|
||||
x: X position
|
||||
y: Y position
|
||||
enemy_type: Key from ENEMY_TYPES dict
|
||||
|
||||
Returns:
|
||||
Tuple of (Entity, EntityStats) for the created enemy
|
||||
"""
|
||||
import mcrfpy
|
||||
|
||||
# Get enemy definition, default to orc if not found
|
||||
enemy_def = ENEMY_TYPES.get(enemy_type, ENEMY_TYPES["orc"])
|
||||
|
||||
entity = mcrfpy.Entity(
|
||||
pos=(x, y),
|
||||
texture=texture,
|
||||
sprite_index=enemy_def["sprite"]
|
||||
)
|
||||
grid.entities.append(entity)
|
||||
|
||||
stats = EntityStats(
|
||||
hp=enemy_def["hp"],
|
||||
max_hp=enemy_def["hp"],
|
||||
power=enemy_def["power"],
|
||||
defense=enemy_def["defense"],
|
||||
name=enemy_def["name"]
|
||||
)
|
||||
|
||||
return entity, stats
|
||||
|
||||
|
||||
def create_enemies_in_rooms(
|
||||
grid: mcrfpy.Grid,
|
||||
texture: mcrfpy.Texture,
|
||||
rooms: list,
|
||||
enemies_per_room: int = 2,
|
||||
skip_first_room: bool = True
|
||||
) -> List[Tuple[mcrfpy.Entity, EntityStats]]:
|
||||
"""
|
||||
Populate dungeon rooms with enemies.
|
||||
|
||||
This helper function places random enemies throughout the dungeon,
|
||||
typically skipping the first room (where the player starts).
|
||||
|
||||
Args:
|
||||
grid: The Grid to populate
|
||||
texture: The texture/tileset to use
|
||||
rooms: List of RectangularRoom objects from dungeon generation
|
||||
enemies_per_room: Maximum enemies to spawn per room
|
||||
skip_first_room: If True, don't spawn enemies in the first room
|
||||
|
||||
Returns:
|
||||
List of (Entity, EntityStats) tuples for all created enemies
|
||||
"""
|
||||
import random
|
||||
|
||||
enemies = []
|
||||
enemy_type_keys = list(ENEMY_TYPES.keys())
|
||||
|
||||
# Iterate through rooms, optionally skipping the first
|
||||
rooms_to_populate = rooms[1:] if skip_first_room else rooms
|
||||
|
||||
for room in rooms_to_populate:
|
||||
# Random number of enemies (0 to enemies_per_room)
|
||||
num_enemies = random.randint(0, enemies_per_room)
|
||||
|
||||
# Get available floor tiles in this room
|
||||
floor_tiles = list(room.inner_tiles())
|
||||
|
||||
for _ in range(num_enemies):
|
||||
if not floor_tiles:
|
||||
break
|
||||
|
||||
# Pick a random position and remove it from available
|
||||
pos = random.choice(floor_tiles)
|
||||
floor_tiles.remove(pos)
|
||||
|
||||
# Pick a random enemy type (weighted toward weaker enemies)
|
||||
if random.random() < 0.8:
|
||||
enemy_type = "orc" # 80% orcs
|
||||
else:
|
||||
enemy_type = "troll" # 20% trolls
|
||||
|
||||
x, y = pos
|
||||
entity, stats = create_enemy(grid, texture, x, y, enemy_type)
|
||||
enemies.append((entity, stats))
|
||||
|
||||
return enemies
|
||||
|
||||
|
||||
def get_blocking_entity_at(
|
||||
entities: List[mcrfpy.Entity],
|
||||
x: int,
|
||||
y: int
|
||||
) -> Optional[mcrfpy.Entity]:
|
||||
"""
|
||||
Check if there's a blocking entity at the given position.
|
||||
|
||||
Useful for collision detection - checks if an entity exists at
|
||||
the target position before moving there.
|
||||
|
||||
Args:
|
||||
entities: List of entities to check
|
||||
x: X coordinate to check
|
||||
y: Y coordinate to check
|
||||
|
||||
Returns:
|
||||
The entity at that position, or None if empty
|
||||
"""
|
||||
for entity in entities:
|
||||
if entity.pos[0] == x and entity.pos[1] == y:
|
||||
return entity
|
||||
return None
|
||||
|
||||
|
||||
def move_entity(
|
||||
entity: mcrfpy.Entity,
|
||||
grid: mcrfpy.Grid,
|
||||
dx: int,
|
||||
dy: int,
|
||||
entities: List[mcrfpy.Entity] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Attempt to move an entity by a delta.
|
||||
|
||||
Checks for:
|
||||
- Grid bounds
|
||||
- Walkable terrain
|
||||
- Other blocking entities (if entities list provided)
|
||||
|
||||
Args:
|
||||
entity: The entity to move
|
||||
grid: The grid for terrain collision
|
||||
dx: Delta X (-1, 0, or 1 typically)
|
||||
dy: Delta Y (-1, 0, or 1 typically)
|
||||
entities: Optional list of entities to check for collision
|
||||
|
||||
Returns:
|
||||
True if movement succeeded, False otherwise
|
||||
"""
|
||||
dest_x = entity.pos[0] + dx
|
||||
dest_y = entity.pos[1] + dy
|
||||
|
||||
# Check grid bounds
|
||||
grid_width, grid_height = grid.grid_size
|
||||
if not (0 <= dest_x < grid_width and 0 <= dest_y < grid_height):
|
||||
return False
|
||||
|
||||
# Check if tile is walkable
|
||||
if not grid.at(dest_x, dest_y).walkable:
|
||||
return False
|
||||
|
||||
# Check for blocking entities
|
||||
if entities and get_blocking_entity_at(entities, dest_x, dest_y):
|
||||
return False
|
||||
|
||||
# Move is valid
|
||||
entity.pos = (dest_x, dest_y)
|
||||
return True
|
||||
|
||||
|
||||
def distance_between(
|
||||
entity1: mcrfpy.Entity,
|
||||
entity2: mcrfpy.Entity
|
||||
) -> float:
|
||||
"""
|
||||
Calculate the Chebyshev distance between two entities.
|
||||
|
||||
Chebyshev distance (also called chessboard distance) counts
|
||||
diagonal moves as 1, which is standard for roguelikes.
|
||||
|
||||
Args:
|
||||
entity1: First entity
|
||||
entity2: Second entity
|
||||
|
||||
Returns:
|
||||
Distance in tiles (diagonal = 1)
|
||||
"""
|
||||
dx = abs(entity1.pos[0] - entity2.pos[0])
|
||||
dy = abs(entity1.pos[1] - entity2.pos[1])
|
||||
return max(dx, dy)
|
||||
|
||||
|
||||
def entities_in_radius(
|
||||
center: mcrfpy.Entity,
|
||||
entities: List[mcrfpy.Entity],
|
||||
radius: float
|
||||
) -> List[mcrfpy.Entity]:
|
||||
"""
|
||||
Find all entities within a given radius of a center entity.
|
||||
|
||||
Uses Chebyshev distance for roguelike-style radius.
|
||||
|
||||
Args:
|
||||
center: The entity to search around
|
||||
entities: List of entities to check
|
||||
radius: Maximum distance in tiles
|
||||
|
||||
Returns:
|
||||
List of entities within the radius (excluding center)
|
||||
"""
|
||||
nearby = []
|
||||
for entity in entities:
|
||||
if entity is not center:
|
||||
if distance_between(center, entity) <= radius:
|
||||
nearby.append(entity)
|
||||
return nearby
|
||||
|
||||
|
||||
def remove_entity(
|
||||
entity: mcrfpy.Entity,
|
||||
grid: mcrfpy.Grid
|
||||
) -> bool:
|
||||
"""
|
||||
Remove an entity from a grid.
|
||||
|
||||
Args:
|
||||
entity: The entity to remove
|
||||
grid: The grid containing the entity
|
||||
|
||||
Returns:
|
||||
True if removal succeeded, False otherwise
|
||||
"""
|
||||
try:
|
||||
idx = entity.index()
|
||||
grid.entities.remove(idx)
|
||||
return True
|
||||
except (ValueError, AttributeError):
|
||||
return False
|
||||
290
docs/templates/roguelike/game.py
vendored
|
|
@ -1,290 +0,0 @@
|
|||
"""
|
||||
game.py - Roguelike Template Main Entry Point
|
||||
|
||||
A minimal but complete roguelike starter using McRogueFace.
|
||||
|
||||
This template demonstrates:
|
||||
- Scene and grid setup
|
||||
- Procedural dungeon generation
|
||||
- Player entity with keyboard movement
|
||||
- Enemy entities (static, no AI)
|
||||
- Field of view using TCOD via Entity.update_visibility()
|
||||
- FOV visualization with grid color overlays
|
||||
|
||||
Run with: ./mcrogueface
|
||||
|
||||
Controls:
|
||||
- Arrow keys / WASD: Move player
|
||||
- Escape: Quit game
|
||||
|
||||
The template is designed to be extended. Good next steps:
|
||||
- Add enemy AI (chase player, pathfinding)
|
||||
- Implement combat system
|
||||
- Add items and inventory
|
||||
- Add multiple dungeon levels
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
from typing import List, Tuple
|
||||
|
||||
# Import our template modules
|
||||
from constants import (
|
||||
MAP_WIDTH, MAP_HEIGHT,
|
||||
SPRITE_WIDTH, SPRITE_HEIGHT,
|
||||
FOV_RADIUS,
|
||||
COLOR_VISIBLE, COLOR_EXPLORED, COLOR_UNKNOWN,
|
||||
SPRITE_PLAYER,
|
||||
)
|
||||
from dungeon import generate_dungeon, populate_grid, RectangularRoom
|
||||
from entities import (
|
||||
create_player,
|
||||
create_enemies_in_rooms,
|
||||
move_entity,
|
||||
EntityStats,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GAME STATE
|
||||
# =============================================================================
|
||||
# Global game state - in a larger game, you'd use a proper state management
|
||||
# system, but for a template this keeps things simple and visible.
|
||||
|
||||
class GameState:
|
||||
"""Container for all game state."""
|
||||
|
||||
def __init__(self):
|
||||
# Core game objects (set during initialization)
|
||||
self.grid: mcrfpy.Grid = None
|
||||
self.player: mcrfpy.Entity = None
|
||||
self.rooms: List[RectangularRoom] = []
|
||||
self.enemies: List[Tuple[mcrfpy.Entity, EntityStats]] = []
|
||||
|
||||
# Texture reference
|
||||
self.texture: mcrfpy.Texture = None
|
||||
|
||||
|
||||
# Global game state instance
|
||||
game = GameState()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# FOV (FIELD OF VIEW) SYSTEM
|
||||
# =============================================================================
|
||||
|
||||
def update_fov() -> None:
|
||||
"""
|
||||
Update the field of view based on player position.
|
||||
|
||||
This function:
|
||||
1. Calls update_visibility() on the player entity to compute FOV using TCOD
|
||||
2. Applies color overlays to tiles based on visibility state
|
||||
|
||||
The FOV creates the classic roguelike effect where:
|
||||
- Visible tiles are fully bright (no overlay)
|
||||
- Previously seen tiles are dimmed (remembered layout)
|
||||
- Never-seen tiles are completely dark
|
||||
|
||||
TCOD handles the actual FOV computation based on the grid's
|
||||
walkable and transparent flags set during dungeon generation.
|
||||
"""
|
||||
if not game.player or not game.grid:
|
||||
return
|
||||
|
||||
# Tell McRogueFace/TCOD to recompute visibility from player position
|
||||
game.player.update_visibility()
|
||||
|
||||
grid_width, grid_height = game.grid.grid_size
|
||||
|
||||
# Apply visibility colors to each tile
|
||||
for x in range(grid_width):
|
||||
for y in range(grid_height):
|
||||
point = game.grid.at(x, y)
|
||||
|
||||
# Get the player's visibility state for this tile
|
||||
state = game.player.at(x, y)
|
||||
|
||||
if state.visible:
|
||||
# Currently visible - no overlay (full brightness)
|
||||
point.color_overlay = COLOR_VISIBLE
|
||||
elif state.discovered:
|
||||
# Previously seen - dimmed overlay (memory)
|
||||
point.color_overlay = COLOR_EXPLORED
|
||||
else:
|
||||
# Never seen - completely dark
|
||||
point.color_overlay = COLOR_UNKNOWN
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# INPUT HANDLING
|
||||
# =============================================================================
|
||||
|
||||
def handle_keys(key: str, state: str) -> None:
|
||||
"""
|
||||
Handle keyboard input for player movement and game controls.
|
||||
|
||||
This is the main input handler registered with McRogueFace.
|
||||
It processes key events and updates game state accordingly.
|
||||
|
||||
Args:
|
||||
key: The key that was pressed (e.g., "W", "Up", "Escape")
|
||||
state: Either "start" (key pressed) or "end" (key released)
|
||||
"""
|
||||
# Only process key press events, not releases
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
# Movement deltas: (dx, dy)
|
||||
movement = {
|
||||
# Arrow keys
|
||||
"Up": (0, -1),
|
||||
"Down": (0, 1),
|
||||
"Left": (-1, 0),
|
||||
"Right": (1, 0),
|
||||
# WASD keys
|
||||
"W": (0, -1),
|
||||
"S": (0, 1),
|
||||
"A": (-1, 0),
|
||||
"D": (1, 0),
|
||||
# Numpad (for diagonal movement if desired)
|
||||
"Numpad8": (0, -1),
|
||||
"Numpad2": (0, 1),
|
||||
"Numpad4": (-1, 0),
|
||||
"Numpad6": (1, 0),
|
||||
"Numpad7": (-1, -1),
|
||||
"Numpad9": (1, -1),
|
||||
"Numpad1": (-1, 1),
|
||||
"Numpad3": (1, 1),
|
||||
}
|
||||
|
||||
if key in movement:
|
||||
dx, dy = movement[key]
|
||||
|
||||
# Get list of all entity objects for collision checking
|
||||
all_entities = [e for e, _ in game.enemies]
|
||||
|
||||
# Attempt to move the player
|
||||
if move_entity(game.player, game.grid, dx, dy, all_entities):
|
||||
# Movement succeeded - update FOV
|
||||
update_fov()
|
||||
|
||||
# Center camera on player
|
||||
px, py = game.player.pos
|
||||
game.grid.center = (px, py)
|
||||
|
||||
elif key == "Escape":
|
||||
# Quit the game
|
||||
mcrfpy.exit()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GAME INITIALIZATION
|
||||
# =============================================================================
|
||||
|
||||
def initialize_game() -> None:
|
||||
"""
|
||||
Set up the game world.
|
||||
|
||||
This function:
|
||||
1. Creates the scene and loads resources
|
||||
2. Generates the dungeon layout
|
||||
3. Creates and places all entities
|
||||
4. Initializes the FOV system
|
||||
5. Sets up input handling
|
||||
"""
|
||||
# Create the game scene
|
||||
mcrfpy.createScene("game")
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
|
||||
# Load the tileset texture
|
||||
# The default McRogueFace texture works great for roguelikes
|
||||
game.texture = mcrfpy.Texture(
|
||||
"assets/kenney_tinydungeon.png",
|
||||
SPRITE_WIDTH,
|
||||
SPRITE_HEIGHT
|
||||
)
|
||||
|
||||
# Create the grid (tile-based game world)
|
||||
# Using keyword arguments for clarity - this is the preferred style
|
||||
game.grid = mcrfpy.Grid(
|
||||
pos=(0, 0), # Screen position in pixels
|
||||
size=(1024, 768), # Display size in pixels
|
||||
grid_size=(MAP_WIDTH, MAP_HEIGHT), # Map size in tiles
|
||||
texture=game.texture
|
||||
)
|
||||
ui.append(game.grid)
|
||||
|
||||
# Generate dungeon layout
|
||||
game.rooms = generate_dungeon()
|
||||
|
||||
# Apply dungeon to grid (sets tiles, walkable flags, etc.)
|
||||
populate_grid(game.grid, game.rooms)
|
||||
|
||||
# Place player in the center of the first room
|
||||
if game.rooms:
|
||||
start_x, start_y = game.rooms[0].center
|
||||
else:
|
||||
# Fallback if no rooms generated
|
||||
start_x, start_y = MAP_WIDTH // 2, MAP_HEIGHT // 2
|
||||
|
||||
game.player = create_player(
|
||||
grid=game.grid,
|
||||
texture=game.texture,
|
||||
x=start_x,
|
||||
y=start_y
|
||||
)
|
||||
|
||||
# Center camera on player
|
||||
game.grid.center = (start_x, start_y)
|
||||
|
||||
# Spawn enemies in other rooms
|
||||
game.enemies = create_enemies_in_rooms(
|
||||
grid=game.grid,
|
||||
texture=game.texture,
|
||||
rooms=game.rooms,
|
||||
enemies_per_room=2,
|
||||
skip_first_room=True
|
||||
)
|
||||
|
||||
# Initial FOV calculation
|
||||
update_fov()
|
||||
|
||||
# Register input handler
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Switch to game scene
|
||||
mcrfpy.setScene("game")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN ENTRY POINT
|
||||
# =============================================================================
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
Main entry point for the roguelike template.
|
||||
|
||||
This function is called when the script starts. It initializes
|
||||
the game and McRogueFace handles the game loop automatically.
|
||||
"""
|
||||
initialize_game()
|
||||
|
||||
# Display welcome message
|
||||
print("=" * 50)
|
||||
print(" ROGUELIKE TEMPLATE")
|
||||
print("=" * 50)
|
||||
print("Controls:")
|
||||
print(" Arrow keys / WASD - Move")
|
||||
print(" Escape - Quit")
|
||||
print()
|
||||
print(f"Dungeon generated with {len(game.rooms)} rooms")
|
||||
print(f"Enemies spawned: {len(game.enemies)}")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
# Run the game
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
else:
|
||||
# McRogueFace runs game.py directly, not as __main__
|
||||
main()
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
"""McRogueFace - Part 0: Setting Up McRogueFace
|
||||
|
||||
Documentation: https://mcrogueface.github.io/tutorial/part_00_setup
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_00_setup/part_00_setup.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Create a Scene object - this is the preferred approach
|
||||
scene = mcrfpy.Scene("hello")
|
||||
|
||||
# Create a caption to display text
|
||||
title = mcrfpy.Caption(
|
||||
pos=(512, 300),
|
||||
text="Hello, Roguelike!"
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.font_size = 32
|
||||
|
||||
# Add the caption to the scene's UI collection
|
||||
scene.children.append(title)
|
||||
|
||||
# Activate the scene to display it
|
||||
scene.activate()
|
||||
|
||||
# Note: There is no run() function!
|
||||
# The engine is already running - your script is imported by it.
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
"""McRogueFace - Part 1: The '@' and the Dungeon Grid
|
||||
|
||||
Documentation: https://mcrogueface.github.io/tutorial/part_01_grid_movement
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_01_grid_movement/part_01_grid_movement.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Sprite indices for CP437 tileset
|
||||
SPRITE_AT = 64 # '@' - player character
|
||||
SPRITE_FLOOR = 46 # '.' - floor tile
|
||||
|
||||
# Grid dimensions (in tiles)
|
||||
GRID_WIDTH = 20
|
||||
GRID_HEIGHT = 15
|
||||
|
||||
# Create the scene
|
||||
scene = mcrfpy.Scene("game")
|
||||
|
||||
# Load the texture (sprite sheet)
|
||||
# Parameters: path, sprite_width, sprite_height
|
||||
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
|
||||
# Create the grid
|
||||
# The grid displays tiles and contains entities
|
||||
grid = mcrfpy.Grid(
|
||||
pos=(100, 80), # Position on screen (pixels)
|
||||
size=(640, 480), # Display size (pixels)
|
||||
zoom = 2.0,
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT), # Size in tiles
|
||||
texture=texture
|
||||
)
|
||||
|
||||
# Fill the grid with floor tiles
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
cell = grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
|
||||
# Create the player entity at the center of the grid
|
||||
player = mcrfpy.Entity(
|
||||
grid_pos=(GRID_WIDTH // 2, GRID_HEIGHT // 2), # Grid coordinates, not pixels!
|
||||
texture=texture,
|
||||
sprite_index=SPRITE_AT
|
||||
)
|
||||
|
||||
# Add the player to the grid
|
||||
# Option 1: Use the grid parameter in constructor
|
||||
# player = mcrfpy.Entity(grid_pos=(10, 7), texture=texture, sprite_index=SPRITE_AT, grid=grid)
|
||||
|
||||
# Option 2: Append to grid.entities (what we will use)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Add the grid to the scene
|
||||
scene.children.append(grid)
|
||||
|
||||
# Add a title caption
|
||||
title = mcrfpy.Caption(
|
||||
pos=(100, 20),
|
||||
text="Part 1: Grid Movement - Use Arrow Keys or WASD"
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.font_size = 18
|
||||
scene.children.append(title)
|
||||
|
||||
# Add a position display
|
||||
pos_display = mcrfpy.Caption(
|
||||
pos=(100, 50),
|
||||
text=f"Player Position: ({player.x}, {player.y})"
|
||||
)
|
||||
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||
pos_display.font_size = 16
|
||||
scene.children.append(pos_display)
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
"""Handle keyboard input to move the player.
|
||||
|
||||
Args:
|
||||
key: The key that was pressed (e.g., "W", "Up", "Space")
|
||||
action: Either "start" (key pressed) or "end" (key released)
|
||||
"""
|
||||
# Only respond to key press, not release
|
||||
if action != "start":
|
||||
return
|
||||
|
||||
# Get current player position
|
||||
px, py = int(player.x), int(player.y)
|
||||
|
||||
# Calculate new position based on key
|
||||
if key == "W" or key == "Up":
|
||||
py -= 1 # Up decreases Y
|
||||
elif key == "S" or key == "Down":
|
||||
py += 1 # Down increases Y
|
||||
elif key == "A" or key == "Left":
|
||||
px -= 1 # Left decreases X
|
||||
elif key == "D" or key == "Right":
|
||||
px += 1 # Right increases X
|
||||
elif key == "Escape":
|
||||
mcrfpy.exit()
|
||||
return
|
||||
|
||||
# Update player position
|
||||
player.x = px
|
||||
player.y = py
|
||||
|
||||
# Update the position display
|
||||
pos_display.text = f"Player Position: ({player.x}, {player.y})"
|
||||
|
||||
# Set the key handler on the scene
|
||||
# This is the preferred approach - works on ANY scene, not just the active one
|
||||
scene.on_key = handle_keys
|
||||
|
||||
# Activate the scene
|
||||
scene.activate()
|
||||
|
||||
print("Part 1 loaded! Use WASD or Arrow keys to move.")
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
"""McRogueFace - Part 2: Walls, Floors, and Collision
|
||||
|
||||
Documentation: https://mcrogueface.github.io/tutorial/part_02_tiles_collision
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_02_tiles_collision/part_02_tiles_collision.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
|
||||
# Sprite indices for CP437 tileset
|
||||
SPRITE_WALL = 35 # '#' - wall
|
||||
SPRITE_FLOOR = 46 # '.' - floor
|
||||
SPRITE_PLAYER = 64 # '@' - player
|
||||
|
||||
# Grid dimensions
|
||||
GRID_WIDTH = 30
|
||||
GRID_HEIGHT = 20
|
||||
|
||||
# =============================================================================
|
||||
# Map Creation
|
||||
# =============================================================================
|
||||
|
||||
def create_map(grid: mcrfpy.Grid) -> None:
|
||||
"""Fill the grid with walls and floors.
|
||||
|
||||
Creates a simple room with walls around the edges and floor in the middle.
|
||||
"""
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
cell = grid.at(x, y)
|
||||
|
||||
# Place walls around the edges
|
||||
if x == 0 or x == GRID_WIDTH - 1 or y == 0 or y == GRID_HEIGHT - 1:
|
||||
cell.tilesprite = SPRITE_WALL
|
||||
cell.walkable = False
|
||||
else:
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
cell.walkable = True
|
||||
|
||||
# Add some interior walls to make it interesting
|
||||
# Vertical wall
|
||||
for y in range(5, 15):
|
||||
cell = grid.at(10, y)
|
||||
cell.tilesprite = SPRITE_WALL
|
||||
cell.walkable = False
|
||||
|
||||
# Horizontal wall
|
||||
for x in range(15, 25):
|
||||
cell = grid.at(x, 10)
|
||||
cell.tilesprite = SPRITE_WALL
|
||||
cell.walkable = False
|
||||
|
||||
# Leave gaps for doorways
|
||||
grid.at(10, 10).tilesprite = SPRITE_FLOOR
|
||||
grid.at(10, 10).walkable = True
|
||||
grid.at(20, 10).tilesprite = SPRITE_FLOOR
|
||||
grid.at(20, 10).walkable = True
|
||||
|
||||
# =============================================================================
|
||||
# Collision Detection
|
||||
# =============================================================================
|
||||
|
||||
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
|
||||
"""Check if a position is valid for movement.
|
||||
|
||||
Args:
|
||||
grid: The game grid
|
||||
x: Target X coordinate (in tiles)
|
||||
y: Target Y coordinate (in tiles)
|
||||
|
||||
Returns:
|
||||
True if the position is walkable, False otherwise
|
||||
"""
|
||||
# Check grid bounds first
|
||||
if x < 0 or x >= GRID_WIDTH:
|
||||
return False
|
||||
if y < 0 or y >= GRID_HEIGHT:
|
||||
return False
|
||||
|
||||
# Check if the tile is walkable
|
||||
cell = grid.at(x, y)
|
||||
return cell.walkable
|
||||
|
||||
# =============================================================================
|
||||
# Game Setup
|
||||
# =============================================================================
|
||||
|
||||
# Create the scene
|
||||
scene = mcrfpy.Scene("game")
|
||||
|
||||
# Load texture
|
||||
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
|
||||
# Create the grid
|
||||
grid = mcrfpy.Grid(
|
||||
pos=(80, 100),
|
||||
size=(720, 480),
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
texture=texture,
|
||||
zoom=1.5
|
||||
)
|
||||
|
||||
# Build the map
|
||||
create_map(grid)
|
||||
|
||||
# Create the player in the center of the left room
|
||||
player = mcrfpy.Entity(
|
||||
grid_pos=(5, 10),
|
||||
texture=texture,
|
||||
sprite_index=SPRITE_PLAYER
|
||||
)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Add grid to scene
|
||||
scene.children.append(grid)
|
||||
|
||||
# =============================================================================
|
||||
# UI Elements
|
||||
# =============================================================================
|
||||
|
||||
title = mcrfpy.Caption(
|
||||
pos=(80, 20),
|
||||
text="Part 2: Walls and Collision"
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.font_size = 24
|
||||
scene.children.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption(
|
||||
pos=(80, 55),
|
||||
text="WASD or Arrow Keys to move | Walls block movement"
|
||||
)
|
||||
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||
instructions.font_size = 16
|
||||
scene.children.append(instructions)
|
||||
|
||||
pos_display = mcrfpy.Caption(
|
||||
pos=(80, 600),
|
||||
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||
)
|
||||
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||
pos_display.font_size = 16
|
||||
scene.children.append(pos_display)
|
||||
|
||||
status_display = mcrfpy.Caption(
|
||||
pos=(400, 600),
|
||||
text="Status: Ready"
|
||||
)
|
||||
status_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||
status_display.font_size = 16
|
||||
scene.children.append(status_display)
|
||||
|
||||
# =============================================================================
|
||||
# Input Handling
|
||||
# =============================================================================
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
"""Handle keyboard input with collision detection."""
|
||||
if action != "start":
|
||||
return
|
||||
|
||||
# Get current position
|
||||
px, py = int(player.x), int(player.y)
|
||||
|
||||
# Calculate intended new position
|
||||
new_x, new_y = px, py
|
||||
|
||||
if key == "W" or key == "Up":
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
new_x += 1
|
||||
elif key == "Escape":
|
||||
mcrfpy.exit()
|
||||
return
|
||||
else:
|
||||
return # Ignore other keys
|
||||
|
||||
# Check collision before moving
|
||||
if can_move_to(grid, new_x, new_y):
|
||||
player.x = new_x
|
||||
player.y = new_y
|
||||
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||
status_display.text = "Status: Moved"
|
||||
status_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||
else:
|
||||
status_display.text = "Status: Blocked!"
|
||||
status_display.fill_color = mcrfpy.Color(200, 100, 100)
|
||||
|
||||
scene.on_key = handle_keys
|
||||
|
||||
# =============================================================================
|
||||
# Start the Game
|
||||
# =============================================================================
|
||||
|
||||
scene.activate()
|
||||
print("Part 2 loaded! Try walking into walls.")
|
||||
|
|
@ -1,356 +0,0 @@
|
|||
"""McRogueFace - Part 3: Procedural Dungeon Generation
|
||||
|
||||
Documentation: https://mcrogueface.github.io/tutorial/part_03_dungeon_generation
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_03_dungeon_generation/part_03_dungeon_generation.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
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
|
||||
# Sprite indices for CP437 tileset
|
||||
SPRITE_WALL = 35 # '#' - wall
|
||||
SPRITE_FLOOR = 46 # '.' - floor
|
||||
SPRITE_PLAYER = 64 # '@' - player
|
||||
|
||||
# Grid dimensions
|
||||
GRID_WIDTH = 50
|
||||
GRID_HEIGHT = 35
|
||||
|
||||
# Room generation parameters
|
||||
ROOM_MIN_SIZE = 6
|
||||
ROOM_MAX_SIZE = 12
|
||||
MAX_ROOMS = 8
|
||||
|
||||
# =============================================================================
|
||||
# Room Class
|
||||
# =============================================================================
|
||||
|
||||
class RectangularRoom:
|
||||
"""A rectangular room with its position and size."""
|
||||
|
||||
def __init__(self, x: int, y: int, width: int, height: int):
|
||||
"""Create a new room.
|
||||
|
||||
Args:
|
||||
x: Left edge X coordinate
|
||||
y: Top edge Y coordinate
|
||||
width: Room width in tiles
|
||||
height: Room height in tiles
|
||||
"""
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
def center(self) -> tuple[int, int]:
|
||||
"""Return the center coordinates of the room."""
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return center_x, center_y
|
||||
|
||||
@property
|
||||
def inner(self) -> tuple[slice, slice]:
|
||||
"""Return the inner area of the room (excluding walls).
|
||||
|
||||
The inner area is one tile smaller on each side to leave room
|
||||
for walls between adjacent rooms.
|
||||
"""
|
||||
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
|
||||
|
||||
def intersects(self, other: "RectangularRoom") -> bool:
|
||||
"""Check if this room overlaps with another room.
|
||||
|
||||
Args:
|
||||
other: Another RectangularRoom to check against
|
||||
|
||||
Returns:
|
||||
True if the rooms overlap, False otherwise
|
||||
"""
|
||||
return (
|
||||
self.x1 <= other.x2 and
|
||||
self.x2 >= other.x1 and
|
||||
self.y1 <= other.y2 and
|
||||
self.y2 >= other.y1
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon Generation
|
||||
# =============================================================================
|
||||
|
||||
def fill_with_walls(grid: mcrfpy.Grid) -> None:
|
||||
"""Fill the entire grid with wall tiles."""
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
cell = grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_WALL
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
|
||||
def carve_room(grid: mcrfpy.Grid, room: RectangularRoom) -> None:
|
||||
"""Carve out a room by setting its inner tiles to floor.
|
||||
|
||||
Args:
|
||||
grid: The game grid
|
||||
room: The room to carve
|
||||
"""
|
||||
inner_x, inner_y = room.inner
|
||||
for y in range(inner_y.start, inner_y.stop):
|
||||
for x in range(inner_x.start, inner_x.stop):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
cell = grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
def carve_tunnel_horizontal(grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
|
||||
"""Carve a horizontal tunnel.
|
||||
|
||||
Args:
|
||||
grid: The game grid
|
||||
x1: Starting X coordinate
|
||||
x2: Ending X coordinate
|
||||
y: Y coordinate of the tunnel
|
||||
"""
|
||||
start_x = min(x1, x2)
|
||||
end_x = max(x1, x2)
|
||||
for x in range(start_x, end_x + 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
cell = grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
def carve_tunnel_vertical(grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
|
||||
"""Carve a vertical tunnel.
|
||||
|
||||
Args:
|
||||
grid: The game grid
|
||||
y1: Starting Y coordinate
|
||||
y2: Ending Y coordinate
|
||||
x: X coordinate of the tunnel
|
||||
"""
|
||||
start_y = min(y1, y2)
|
||||
end_y = max(y1, y2)
|
||||
for y in range(start_y, end_y + 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
cell = grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
def carve_l_tunnel(
|
||||
grid: mcrfpy.Grid,
|
||||
start: tuple[int, int],
|
||||
end: tuple[int, int]
|
||||
) -> None:
|
||||
"""Carve an L-shaped tunnel between two points.
|
||||
|
||||
Randomly chooses to go horizontal-then-vertical or vertical-then-horizontal.
|
||||
|
||||
Args:
|
||||
grid: The game grid
|
||||
start: Starting (x, y) coordinates
|
||||
end: Ending (x, y) coordinates
|
||||
"""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
# Randomly choose whether to go horizontal or vertical first
|
||||
if random.random() < 0.5:
|
||||
# Horizontal first, then vertical
|
||||
carve_tunnel_horizontal(grid, x1, x2, y1)
|
||||
carve_tunnel_vertical(grid, y1, y2, x2)
|
||||
else:
|
||||
# Vertical first, then horizontal
|
||||
carve_tunnel_vertical(grid, y1, y2, x1)
|
||||
carve_tunnel_horizontal(grid, x1, x2, y2)
|
||||
|
||||
def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]:
|
||||
"""Generate a dungeon with rooms and tunnels.
|
||||
|
||||
Args:
|
||||
grid: The game grid to generate the dungeon in
|
||||
|
||||
Returns:
|
||||
The (x, y) coordinates where the player should start
|
||||
"""
|
||||
# Start with all walls
|
||||
fill_with_walls(grid)
|
||||
|
||||
rooms: list[RectangularRoom] = []
|
||||
|
||||
for _ in range(MAX_ROOMS):
|
||||
# Random room dimensions
|
||||
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
|
||||
# Random position (leaving 1-tile border)
|
||||
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
# Check for overlap with existing rooms
|
||||
overlaps = False
|
||||
for other_room in rooms:
|
||||
if new_room.intersects(other_room):
|
||||
overlaps = True
|
||||
break
|
||||
|
||||
if overlaps:
|
||||
continue # Skip this room, try another
|
||||
|
||||
# No overlap - carve out the room
|
||||
carve_room(grid, new_room)
|
||||
|
||||
# Connect to previous room with a tunnel
|
||||
if rooms:
|
||||
# Tunnel from this room's center to the previous room's center
|
||||
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||
|
||||
rooms.append(new_room)
|
||||
|
||||
# Return the center of the first room as the player start position
|
||||
if rooms:
|
||||
return rooms[0].center
|
||||
else:
|
||||
# Fallback if no rooms were generated
|
||||
return GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||
|
||||
# =============================================================================
|
||||
# Collision Detection
|
||||
# =============================================================================
|
||||
|
||||
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
|
||||
"""Check if a position is valid for movement."""
|
||||
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
|
||||
return False
|
||||
return grid.at(x, y).walkable
|
||||
|
||||
# =============================================================================
|
||||
# Game Setup
|
||||
# =============================================================================
|
||||
|
||||
# Create the scene
|
||||
scene = mcrfpy.Scene("game")
|
||||
|
||||
# Load texture
|
||||
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
|
||||
# Create the grid
|
||||
grid = mcrfpy.Grid(
|
||||
pos=(50, 80),
|
||||
size=(800, 560),
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
texture=texture,
|
||||
zoom=1.0
|
||||
)
|
||||
|
||||
# Generate the dungeon and get player start position
|
||||
player_start_x, player_start_y = generate_dungeon(grid)
|
||||
|
||||
# Create the player at the starting position
|
||||
player = mcrfpy.Entity(
|
||||
grid_pos=(player_start_x, player_start_y),
|
||||
texture=texture,
|
||||
sprite_index=SPRITE_PLAYER
|
||||
)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Add grid to scene
|
||||
scene.children.append(grid)
|
||||
|
||||
# =============================================================================
|
||||
# UI Elements
|
||||
# =============================================================================
|
||||
|
||||
title = mcrfpy.Caption(
|
||||
pos=(50, 15),
|
||||
text="Part 3: Procedural Dungeon Generation"
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.font_size = 24
|
||||
scene.children.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption(
|
||||
pos=(50, 50),
|
||||
text="WASD/Arrows: Move | R: Regenerate dungeon | Escape: Quit"
|
||||
)
|
||||
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||
instructions.font_size = 16
|
||||
scene.children.append(instructions)
|
||||
|
||||
pos_display = mcrfpy.Caption(
|
||||
pos=(50, 660),
|
||||
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||
)
|
||||
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||
pos_display.font_size = 16
|
||||
scene.children.append(pos_display)
|
||||
|
||||
room_display = mcrfpy.Caption(
|
||||
pos=(400, 660),
|
||||
text="Press R to regenerate the dungeon"
|
||||
)
|
||||
room_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||
room_display.font_size = 16
|
||||
scene.children.append(room_display)
|
||||
|
||||
# =============================================================================
|
||||
# Input Handling
|
||||
# =============================================================================
|
||||
|
||||
def regenerate_dungeon() -> None:
|
||||
"""Generate a new dungeon and reposition the player."""
|
||||
new_x, new_y = generate_dungeon(grid)
|
||||
player.x = new_x
|
||||
player.y = new_y
|
||||
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||
room_display.text = "New dungeon generated!"
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
"""Handle keyboard input."""
|
||||
if action != "start":
|
||||
return
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
if key == "W" or key == "Up":
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
new_x += 1
|
||||
elif key == "R":
|
||||
regenerate_dungeon()
|
||||
return
|
||||
elif key == "Escape":
|
||||
mcrfpy.exit()
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
if can_move_to(grid, new_x, new_y):
|
||||
player.x = new_x
|
||||
player.y = new_y
|
||||
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||
|
||||
scene.on_key = handle_keys
|
||||
|
||||
# =============================================================================
|
||||
# Start the Game
|
||||
# =============================================================================
|
||||
|
||||
scene.activate()
|
||||
print("Part 3 loaded! Explore the dungeon or press R to regenerate.")
|
||||
|
|
@ -1,363 +0,0 @@
|
|||
"""McRogueFace - Part 4: Field of View
|
||||
|
||||
Documentation: https://mcrogueface.github.io/tutorial/part_04_fov
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_04_fov/part_04_fov.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
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
|
||||
# Sprite indices for CP437 tileset
|
||||
SPRITE_WALL = 35 # '#' - wall
|
||||
SPRITE_FLOOR = 46 # '.' - floor
|
||||
SPRITE_PLAYER = 64 # '@' - player
|
||||
|
||||
# Grid dimensions
|
||||
GRID_WIDTH = 50
|
||||
GRID_HEIGHT = 35
|
||||
|
||||
# Room generation parameters
|
||||
ROOM_MIN_SIZE = 6
|
||||
ROOM_MAX_SIZE = 12
|
||||
MAX_ROOMS = 8
|
||||
|
||||
# FOV settings
|
||||
FOV_RADIUS = 8
|
||||
|
||||
# Visibility colors (applied as overlays)
|
||||
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0) # Fully transparent - show tile
|
||||
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180) # Dark blue tint - dimmed
|
||||
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255) # Solid black - hidden
|
||||
|
||||
# =============================================================================
|
||||
# Room Class (from Part 3)
|
||||
# =============================================================================
|
||||
|
||||
class RectangularRoom:
|
||||
"""A rectangular room with its position and size."""
|
||||
|
||||
def __init__(self, x: int, y: int, width: int, height: int):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
def center(self) -> tuple[int, int]:
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return center_x, center_y
|
||||
|
||||
@property
|
||||
def inner(self) -> tuple[slice, slice]:
|
||||
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
|
||||
|
||||
def intersects(self, other: "RectangularRoom") -> bool:
|
||||
return (
|
||||
self.x1 <= other.x2 and
|
||||
self.x2 >= other.x1 and
|
||||
self.y1 <= other.y2 and
|
||||
self.y2 >= other.y1
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Exploration Tracking
|
||||
# =============================================================================
|
||||
|
||||
# Track which tiles have been discovered (seen at least once)
|
||||
explored: list[list[bool]] = []
|
||||
|
||||
def init_explored() -> None:
|
||||
"""Initialize the explored array to all False."""
|
||||
global explored
|
||||
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||
|
||||
def mark_explored(x: int, y: int) -> None:
|
||||
"""Mark a tile as explored."""
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
explored[y][x] = True
|
||||
|
||||
def is_explored(x: int, y: int) -> bool:
|
||||
"""Check if a tile has been explored."""
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
return explored[y][x]
|
||||
return False
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon Generation (from Part 3, with transparent property)
|
||||
# =============================================================================
|
||||
|
||||
def fill_with_walls(grid: mcrfpy.Grid) -> None:
|
||||
"""Fill the entire grid with wall tiles."""
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
cell = grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_WALL
|
||||
cell.walkable = False
|
||||
cell.transparent = False # Walls block line of sight
|
||||
|
||||
def carve_room(grid: mcrfpy.Grid, room: RectangularRoom) -> None:
|
||||
"""Carve out a room by setting its inner tiles to floor."""
|
||||
inner_x, inner_y = room.inner
|
||||
for y in range(inner_y.start, inner_y.stop):
|
||||
for x in range(inner_x.start, inner_x.stop):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
cell = grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
cell.walkable = True
|
||||
cell.transparent = True # Floors allow line of sight
|
||||
|
||||
def carve_tunnel_horizontal(grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
|
||||
"""Carve a horizontal tunnel."""
|
||||
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
cell = grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
def carve_tunnel_vertical(grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
|
||||
"""Carve a vertical tunnel."""
|
||||
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
cell = grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
def carve_l_tunnel(
|
||||
grid: mcrfpy.Grid,
|
||||
start: tuple[int, int],
|
||||
end: tuple[int, int]
|
||||
) -> None:
|
||||
"""Carve an L-shaped tunnel between two points."""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
if random.random() < 0.5:
|
||||
carve_tunnel_horizontal(grid, x1, x2, y1)
|
||||
carve_tunnel_vertical(grid, y1, y2, x2)
|
||||
else:
|
||||
carve_tunnel_vertical(grid, y1, y2, x1)
|
||||
carve_tunnel_horizontal(grid, x1, x2, y2)
|
||||
|
||||
def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]:
|
||||
"""Generate a dungeon with rooms and tunnels."""
|
||||
fill_with_walls(grid)
|
||||
init_explored() # Reset exploration when generating new dungeon
|
||||
|
||||
rooms: list[RectangularRoom] = []
|
||||
|
||||
for _ in range(MAX_ROOMS):
|
||||
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
overlaps = False
|
||||
for other_room in rooms:
|
||||
if new_room.intersects(other_room):
|
||||
overlaps = True
|
||||
break
|
||||
|
||||
if overlaps:
|
||||
continue
|
||||
|
||||
carve_room(grid, new_room)
|
||||
if rooms:
|
||||
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||
rooms.append(new_room)
|
||||
|
||||
if rooms:
|
||||
return rooms[0].center
|
||||
return GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||
|
||||
# =============================================================================
|
||||
# Field of View
|
||||
# =============================================================================
|
||||
|
||||
def update_fov(grid: mcrfpy.Grid, fov_layer, player_x: int, player_y: int) -> None:
|
||||
"""Update the field of view visualization.
|
||||
|
||||
Args:
|
||||
grid: The game grid
|
||||
fov_layer: The ColorLayer for FOV visualization
|
||||
player_x: Player's X position
|
||||
player_y: Player's Y position
|
||||
"""
|
||||
# Compute FOV from player position
|
||||
grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||
|
||||
# Update each tile's visibility
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if grid.is_in_fov(x, y):
|
||||
# Currently visible - mark as explored and show clearly
|
||||
mark_explored(x, y)
|
||||
fov_layer.set(x, y, COLOR_VISIBLE)
|
||||
elif is_explored(x, y):
|
||||
# Previously seen but not currently visible - show dimmed
|
||||
fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||
else:
|
||||
# Never seen - hide completely
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
|
||||
# =============================================================================
|
||||
# Collision Detection
|
||||
# =============================================================================
|
||||
|
||||
def can_move_to(grid: mcrfpy.Grid, x: int, y: int) -> bool:
|
||||
"""Check if a position is valid for movement."""
|
||||
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
|
||||
return False
|
||||
return grid.at(x, y).walkable
|
||||
|
||||
# =============================================================================
|
||||
# Game Setup
|
||||
# =============================================================================
|
||||
|
||||
# Create the scene
|
||||
scene = mcrfpy.Scene("game")
|
||||
|
||||
# Load texture
|
||||
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
|
||||
# Create the grid
|
||||
grid = mcrfpy.Grid(
|
||||
pos=(50, 80),
|
||||
size=(800, 560),
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
texture=texture,
|
||||
zoom=1.0
|
||||
)
|
||||
|
||||
# Generate the dungeon
|
||||
player_start_x, player_start_y = generate_dungeon(grid)
|
||||
|
||||
# Add a color layer for FOV visualization (below entities)
|
||||
fov_layer = grid.add_layer("color", z_index=-1)
|
||||
|
||||
# Initialize the FOV layer to all black (unknown)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
|
||||
# Create the player
|
||||
player = mcrfpy.Entity(
|
||||
grid_pos=(player_start_x, player_start_y),
|
||||
texture=texture,
|
||||
sprite_index=SPRITE_PLAYER
|
||||
)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Calculate initial FOV
|
||||
update_fov(grid, fov_layer, player_start_x, player_start_y)
|
||||
|
||||
# Add grid to scene
|
||||
scene.children.append(grid)
|
||||
|
||||
# =============================================================================
|
||||
# UI Elements
|
||||
# =============================================================================
|
||||
|
||||
title = mcrfpy.Caption(
|
||||
pos=(50, 15),
|
||||
text="Part 4: Field of View"
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.font_size = 24
|
||||
scene.children.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption(
|
||||
pos=(50, 50),
|
||||
text="WASD/Arrows: Move | R: Regenerate | Escape: Quit"
|
||||
)
|
||||
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||
instructions.font_size = 16
|
||||
scene.children.append(instructions)
|
||||
|
||||
pos_display = mcrfpy.Caption(
|
||||
pos=(50, 660),
|
||||
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||
)
|
||||
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||
pos_display.font_size = 16
|
||||
scene.children.append(pos_display)
|
||||
|
||||
fov_display = mcrfpy.Caption(
|
||||
pos=(400, 660),
|
||||
text=f"FOV Radius: {FOV_RADIUS}"
|
||||
)
|
||||
fov_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||
fov_display.font_size = 16
|
||||
scene.children.append(fov_display)
|
||||
|
||||
# =============================================================================
|
||||
# Input Handling
|
||||
# =============================================================================
|
||||
|
||||
def regenerate_dungeon() -> None:
|
||||
"""Generate a new dungeon and reposition the player."""
|
||||
new_x, new_y = generate_dungeon(grid)
|
||||
player.x = new_x
|
||||
player.y = new_y
|
||||
|
||||
# Reset FOV layer to unknown
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
|
||||
# Calculate new FOV
|
||||
update_fov(grid, fov_layer, new_x, new_y)
|
||||
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
"""Handle keyboard input."""
|
||||
if action != "start":
|
||||
return
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
if key == "W" or key == "Up":
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
new_x += 1
|
||||
elif key == "R":
|
||||
regenerate_dungeon()
|
||||
return
|
||||
elif key == "Escape":
|
||||
mcrfpy.exit()
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
if can_move_to(grid, new_x, new_y):
|
||||
player.x = new_x
|
||||
player.y = new_y
|
||||
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||
|
||||
# Update FOV after movement
|
||||
update_fov(grid, fov_layer, new_x, new_y)
|
||||
|
||||
scene.on_key = handle_keys
|
||||
|
||||
# =============================================================================
|
||||
# Start the Game
|
||||
# =============================================================================
|
||||
|
||||
scene.activate()
|
||||
print("Part 4 loaded! Explore the dungeon - watch the fog of war!")
|
||||
|
|
@ -1,685 +0,0 @@
|
|||
"""McRogueFace - Part 5: Placing Enemies
|
||||
|
||||
Documentation: https://mcrogueface.github.io/tutorial/part_05_enemies
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_05_enemies/part_05_enemies.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
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
|
||||
# Sprite indices for CP437 tileset
|
||||
SPRITE_WALL = 35 # '#' - wall
|
||||
SPRITE_FLOOR = 46 # '.' - floor
|
||||
SPRITE_PLAYER = 64 # '@' - player
|
||||
|
||||
# Enemy sprites (lowercase letters in CP437)
|
||||
SPRITE_GOBLIN = 103 # 'g'
|
||||
SPRITE_ORC = 111 # 'o'
|
||||
SPRITE_TROLL = 116 # 't'
|
||||
|
||||
# Grid dimensions
|
||||
GRID_WIDTH = 50
|
||||
GRID_HEIGHT = 35
|
||||
|
||||
# Room generation parameters
|
||||
ROOM_MIN_SIZE = 6
|
||||
ROOM_MAX_SIZE = 12
|
||||
MAX_ROOMS = 8
|
||||
|
||||
# Enemy spawn parameters
|
||||
MAX_ENEMIES_PER_ROOM = 3
|
||||
|
||||
# FOV settings
|
||||
FOV_RADIUS = 8
|
||||
|
||||
# Visibility colors
|
||||
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0)
|
||||
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180)
|
||||
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255)
|
||||
|
||||
# =============================================================================
|
||||
# Enemy Data
|
||||
# =============================================================================
|
||||
|
||||
# Enemy templates - stats for each enemy type
|
||||
ENEMY_TEMPLATES = {
|
||||
"goblin": {
|
||||
"sprite": SPRITE_GOBLIN,
|
||||
"hp": 6,
|
||||
"max_hp": 6,
|
||||
"attack": 3,
|
||||
"defense": 0,
|
||||
"color": mcrfpy.Color(100, 200, 100) # Greenish
|
||||
},
|
||||
"orc": {
|
||||
"sprite": SPRITE_ORC,
|
||||
"hp": 10,
|
||||
"max_hp": 10,
|
||||
"attack": 4,
|
||||
"defense": 1,
|
||||
"color": mcrfpy.Color(100, 150, 100) # Darker green
|
||||
},
|
||||
"troll": {
|
||||
"sprite": SPRITE_TROLL,
|
||||
"hp": 16,
|
||||
"max_hp": 16,
|
||||
"attack": 6,
|
||||
"defense": 2,
|
||||
"color": mcrfpy.Color(50, 150, 50) # Dark green
|
||||
}
|
||||
}
|
||||
|
||||
# Global storage for entity data
|
||||
# Maps entity objects to their data dictionaries
|
||||
entity_data: dict = {}
|
||||
|
||||
# Global references
|
||||
player = None
|
||||
grid = None
|
||||
fov_layer = None
|
||||
|
||||
# =============================================================================
|
||||
# Room Class (from Part 3)
|
||||
# =============================================================================
|
||||
|
||||
class RectangularRoom:
|
||||
"""A rectangular room with its position and size."""
|
||||
|
||||
def __init__(self, x: int, y: int, width: int, height: int):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
def center(self) -> tuple[int, int]:
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return center_x, center_y
|
||||
|
||||
@property
|
||||
def inner(self) -> tuple[slice, slice]:
|
||||
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
|
||||
|
||||
def intersects(self, other: "RectangularRoom") -> bool:
|
||||
return (
|
||||
self.x1 <= other.x2 and
|
||||
self.x2 >= other.x1 and
|
||||
self.y1 <= other.y2 and
|
||||
self.y2 >= other.y1
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Exploration Tracking (from Part 4)
|
||||
# =============================================================================
|
||||
|
||||
explored: list[list[bool]] = []
|
||||
|
||||
def init_explored() -> None:
|
||||
"""Initialize the explored array to all False."""
|
||||
global explored
|
||||
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||
|
||||
def mark_explored(x: int, y: int) -> None:
|
||||
"""Mark a tile as explored."""
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
explored[y][x] = True
|
||||
|
||||
def is_explored(x: int, y: int) -> bool:
|
||||
"""Check if a tile has been explored."""
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
return explored[y][x]
|
||||
return False
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon Generation (from Part 4)
|
||||
# =============================================================================
|
||||
|
||||
def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
|
||||
"""Fill the entire grid with wall tiles."""
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
cell = target_grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_WALL
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
|
||||
def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None:
|
||||
"""Carve out a room by setting its inner tiles to floor."""
|
||||
inner_x, inner_y = room.inner
|
||||
for y in range(inner_y.start, inner_y.stop):
|
||||
for x in range(inner_x.start, inner_x.stop):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
cell = target_grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
|
||||
"""Carve a horizontal tunnel."""
|
||||
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
cell = target_grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
|
||||
"""Carve a vertical tunnel."""
|
||||
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
cell = target_grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
def carve_l_tunnel(
|
||||
target_grid: mcrfpy.Grid,
|
||||
start: tuple[int, int],
|
||||
end: tuple[int, int]
|
||||
) -> None:
|
||||
"""Carve an L-shaped tunnel between two points."""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
if random.random() < 0.5:
|
||||
carve_tunnel_horizontal(target_grid, x1, x2, y1)
|
||||
carve_tunnel_vertical(target_grid, y1, y2, x2)
|
||||
else:
|
||||
carve_tunnel_vertical(target_grid, y1, y2, x1)
|
||||
carve_tunnel_horizontal(target_grid, x1, x2, y2)
|
||||
|
||||
# =============================================================================
|
||||
# Enemy Management
|
||||
# =============================================================================
|
||||
|
||||
def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, texture: mcrfpy.Texture) -> mcrfpy.Entity:
|
||||
"""Spawn an enemy at the given position.
|
||||
|
||||
Args:
|
||||
target_grid: The game grid
|
||||
x: X position in tiles
|
||||
y: Y position in tiles
|
||||
enemy_type: Type of enemy ("goblin", "orc", or "troll")
|
||||
texture: The texture to use for the sprite
|
||||
|
||||
Returns:
|
||||
The created enemy Entity
|
||||
"""
|
||||
template = ENEMY_TEMPLATES[enemy_type]
|
||||
|
||||
enemy = mcrfpy.Entity(
|
||||
grid_pos=(x, y),
|
||||
texture=texture,
|
||||
sprite_index=template["sprite"]
|
||||
)
|
||||
|
||||
|
||||
# Start hidden until player sees them
|
||||
enemy.visible = False
|
||||
|
||||
# Add to grid
|
||||
target_grid.entities.append(enemy)
|
||||
|
||||
# Store enemy data
|
||||
entity_data[enemy] = {
|
||||
"type": enemy_type,
|
||||
"name": enemy_type.capitalize(),
|
||||
"hp": template["hp"],
|
||||
"max_hp": template["max_hp"],
|
||||
"attack": template["attack"],
|
||||
"defense": template["defense"],
|
||||
"is_player": False
|
||||
}
|
||||
|
||||
return enemy
|
||||
|
||||
def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, texture: mcrfpy.Texture) -> None:
|
||||
"""Spawn random enemies in a room.
|
||||
|
||||
Args:
|
||||
target_grid: The game grid
|
||||
room: The room to spawn enemies in
|
||||
texture: The texture to use for sprites
|
||||
"""
|
||||
# Random number of enemies (0 to MAX_ENEMIES_PER_ROOM)
|
||||
num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM)
|
||||
|
||||
for _ in range(num_enemies):
|
||||
# Random position within the room's inner area
|
||||
inner_x, inner_y = room.inner
|
||||
x = random.randint(inner_x.start, inner_x.stop - 1)
|
||||
y = random.randint(inner_y.start, inner_y.stop - 1)
|
||||
|
||||
# Check if position is already occupied
|
||||
if get_blocking_entity_at(target_grid, x, y) is not None:
|
||||
continue # Skip this spawn attempt
|
||||
|
||||
# Choose enemy type based on weighted random
|
||||
roll = random.random()
|
||||
if roll < 0.6:
|
||||
enemy_type = "goblin" # 60% chance
|
||||
elif roll < 0.9:
|
||||
enemy_type = "orc" # 30% chance
|
||||
else:
|
||||
enemy_type = "troll" # 10% chance
|
||||
|
||||
spawn_enemy(target_grid, x, y, enemy_type, texture)
|
||||
|
||||
def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int) -> mcrfpy.Entity | None:
|
||||
"""Get any entity that blocks movement at the given position.
|
||||
|
||||
Args:
|
||||
target_grid: The game grid
|
||||
x: X position to check
|
||||
y: Y position to check
|
||||
|
||||
Returns:
|
||||
The blocking entity, or None if no entity blocks this position
|
||||
"""
|
||||
for entity in target_grid.entities:
|
||||
if int(entity.x) == x and int(entity.y) == y:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def clear_enemies(target_grid: mcrfpy.Grid) -> None:
|
||||
"""Remove all enemies from the grid."""
|
||||
global entity_data
|
||||
|
||||
# Get list of enemies to remove (not the player)
|
||||
enemies_to_remove = []
|
||||
for entity in target_grid.entities:
|
||||
if entity in entity_data and not entity_data[entity].get("is_player", False):
|
||||
enemies_to_remove.append(entity)
|
||||
|
||||
# Remove from grid and entity_data
|
||||
for enemy in enemies_to_remove:
|
||||
# Find and remove from grid.entities
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == enemy:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
# Remove from entity_data
|
||||
if enemy in entity_data:
|
||||
del entity_data[enemy]
|
||||
|
||||
# =============================================================================
|
||||
# Entity Visibility
|
||||
# =============================================================================
|
||||
|
||||
def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
||||
"""Update visibility of all entities based on FOV.
|
||||
|
||||
Entities outside the player's field of view are hidden.
|
||||
"""
|
||||
global player
|
||||
|
||||
for entity in target_grid.entities:
|
||||
# Player is always visible
|
||||
if entity == player:
|
||||
entity.visible = True
|
||||
continue
|
||||
|
||||
# Other entities are only visible if in FOV
|
||||
ex, ey = int(entity.x), int(entity.y)
|
||||
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||
|
||||
# =============================================================================
|
||||
# Field of View (from Part 4)
|
||||
# =============================================================================
|
||||
|
||||
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||
"""Update the field of view visualization."""
|
||||
# Compute FOV from player position
|
||||
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||
|
||||
# Update each tile's visibility
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if target_grid.is_in_fov(x, y):
|
||||
mark_explored(x, y)
|
||||
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||
elif is_explored(x, y):
|
||||
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||
else:
|
||||
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
|
||||
# Update entity visibility
|
||||
update_entity_visibility(target_grid)
|
||||
|
||||
# =============================================================================
|
||||
# Collision Detection
|
||||
# =============================================================================
|
||||
|
||||
def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int) -> bool:
|
||||
"""Check if a position is valid for movement.
|
||||
|
||||
A position is valid if:
|
||||
1. It is within grid bounds
|
||||
2. The tile is walkable
|
||||
3. No entity is blocking it
|
||||
"""
|
||||
# Check bounds
|
||||
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
|
||||
return False
|
||||
|
||||
# Check tile walkability
|
||||
if not target_grid.at(x, y).walkable:
|
||||
return False
|
||||
|
||||
# Check for blocking entities
|
||||
if get_blocking_entity_at(target_grid, x, y) is not None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon Generation with Enemies
|
||||
# =============================================================================
|
||||
|
||||
def generate_dungeon(target_grid: mcrfpy.Grid, texture: mcrfpy.Texture) -> tuple[int, int]:
|
||||
"""Generate a dungeon with rooms, tunnels, and enemies.
|
||||
|
||||
Args:
|
||||
target_grid: The game grid
|
||||
texture: The texture for entity sprites
|
||||
|
||||
Returns:
|
||||
The (x, y) coordinates where the player should start
|
||||
"""
|
||||
# Clear any existing enemies
|
||||
clear_enemies(target_grid)
|
||||
|
||||
# Fill with walls
|
||||
fill_with_walls(target_grid)
|
||||
init_explored()
|
||||
|
||||
rooms: list[RectangularRoom] = []
|
||||
|
||||
for _ in range(MAX_ROOMS):
|
||||
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
overlaps = False
|
||||
for other_room in rooms:
|
||||
if new_room.intersects(other_room):
|
||||
overlaps = True
|
||||
break
|
||||
|
||||
if overlaps:
|
||||
continue
|
||||
|
||||
carve_room(target_grid, new_room)
|
||||
|
||||
if rooms:
|
||||
carve_l_tunnel(target_grid, new_room.center, rooms[-1].center)
|
||||
# Spawn enemies in all rooms except the first (player starting room)
|
||||
spawn_enemies_in_room(target_grid, new_room, texture)
|
||||
|
||||
rooms.append(new_room)
|
||||
|
||||
if rooms:
|
||||
return rooms[0].center
|
||||
return GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||
|
||||
# =============================================================================
|
||||
# Game Setup
|
||||
# =============================================================================
|
||||
|
||||
# Create the scene
|
||||
scene = mcrfpy.Scene("game")
|
||||
|
||||
# Load texture
|
||||
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
|
||||
# Create the grid
|
||||
grid = mcrfpy.Grid(
|
||||
pos=(50, 80),
|
||||
size=(800, 560),
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
texture=texture,
|
||||
zoom=1.0
|
||||
)
|
||||
|
||||
# Generate the dungeon (without player first to get starting position)
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
|
||||
rooms: list[RectangularRoom] = []
|
||||
|
||||
for _ in range(MAX_ROOMS):
|
||||
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
overlaps = False
|
||||
for other_room in rooms:
|
||||
if new_room.intersects(other_room):
|
||||
overlaps = True
|
||||
break
|
||||
|
||||
if overlaps:
|
||||
continue
|
||||
|
||||
carve_room(grid, new_room)
|
||||
|
||||
if rooms:
|
||||
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||
|
||||
rooms.append(new_room)
|
||||
|
||||
# Get player starting position
|
||||
if rooms:
|
||||
player_start_x, player_start_y = rooms[0].center
|
||||
else:
|
||||
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||
|
||||
# Add FOV layer
|
||||
fov_layer = grid.add_layer("color", z_index=-1)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
|
||||
# Create the player
|
||||
player = mcrfpy.Entity(
|
||||
grid_pos=(player_start_x, player_start_y),
|
||||
texture=texture,
|
||||
sprite_index=SPRITE_PLAYER
|
||||
)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Store player data
|
||||
entity_data[player] = {
|
||||
"type": "player",
|
||||
"name": "Player",
|
||||
"hp": 30,
|
||||
"max_hp": 30,
|
||||
"attack": 5,
|
||||
"defense": 2,
|
||||
"is_player": True
|
||||
}
|
||||
|
||||
# Now spawn enemies in rooms (except the first one)
|
||||
for i, room in enumerate(rooms):
|
||||
if i == 0:
|
||||
continue # Skip player's starting room
|
||||
spawn_enemies_in_room(grid, room, texture)
|
||||
|
||||
# Calculate initial FOV
|
||||
update_fov(grid, fov_layer, player_start_x, player_start_y)
|
||||
|
||||
# Add grid to scene
|
||||
scene.children.append(grid)
|
||||
|
||||
# =============================================================================
|
||||
# UI Elements
|
||||
# =============================================================================
|
||||
|
||||
title = mcrfpy.Caption(
|
||||
pos=(50, 15),
|
||||
text="Part 5: Placing Enemies"
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.font_size = 24
|
||||
scene.children.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption(
|
||||
pos=(50, 50),
|
||||
text="WASD/Arrows: Move | R: Regenerate | Escape: Quit"
|
||||
)
|
||||
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||
instructions.font_size = 16
|
||||
scene.children.append(instructions)
|
||||
|
||||
pos_display = mcrfpy.Caption(
|
||||
pos=(50, 660),
|
||||
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||
)
|
||||
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||
pos_display.font_size = 16
|
||||
scene.children.append(pos_display)
|
||||
|
||||
status_display = mcrfpy.Caption(
|
||||
pos=(400, 660),
|
||||
text="Explore the dungeon..."
|
||||
)
|
||||
status_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||
status_display.font_size = 16
|
||||
scene.children.append(status_display)
|
||||
|
||||
# =============================================================================
|
||||
# Input Handling
|
||||
# =============================================================================
|
||||
|
||||
def regenerate_dungeon() -> None:
|
||||
"""Generate a new dungeon and reposition the player."""
|
||||
global player, grid, fov_layer, rooms
|
||||
|
||||
# Clear enemies
|
||||
clear_enemies(grid)
|
||||
|
||||
# Regenerate dungeon structure
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
|
||||
rooms = []
|
||||
|
||||
for _ in range(MAX_ROOMS):
|
||||
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
overlaps = False
|
||||
for other_room in rooms:
|
||||
if new_room.intersects(other_room):
|
||||
overlaps = True
|
||||
break
|
||||
|
||||
if overlaps:
|
||||
continue
|
||||
|
||||
carve_room(grid, new_room)
|
||||
|
||||
if rooms:
|
||||
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||
|
||||
rooms.append(new_room)
|
||||
|
||||
# Reposition player
|
||||
if rooms:
|
||||
new_x, new_y = rooms[0].center
|
||||
else:
|
||||
new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||
|
||||
player.x = new_x
|
||||
player.y = new_y
|
||||
|
||||
# Spawn new enemies
|
||||
for i, room in enumerate(rooms):
|
||||
if i == 0:
|
||||
continue
|
||||
spawn_enemies_in_room(grid, room, texture)
|
||||
|
||||
# Reset FOV layer
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
|
||||
# Update FOV
|
||||
update_fov(grid, fov_layer, new_x, new_y)
|
||||
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||
status_display.text = "New dungeon generated!"
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
"""Handle keyboard input."""
|
||||
global player, grid, fov_layer
|
||||
|
||||
if action != "start":
|
||||
return
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
if key == "W" or key == "Up":
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
new_x += 1
|
||||
elif key == "R":
|
||||
regenerate_dungeon()
|
||||
return
|
||||
elif key == "Escape":
|
||||
mcrfpy.exit()
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
# Check for blocking entity (potential combat target)
|
||||
blocker = get_blocking_entity_at(grid, new_x, new_y)
|
||||
if blocker is not None and blocker != player:
|
||||
# For now, just report that we bumped into an enemy
|
||||
if blocker in entity_data:
|
||||
enemy_name = entity_data[blocker]["name"]
|
||||
status_display.text = f"A {enemy_name} blocks your path!"
|
||||
status_display.fill_color = mcrfpy.Color(200, 150, 100)
|
||||
return
|
||||
|
||||
# Check if we can move
|
||||
if can_move_to(grid, new_x, new_y):
|
||||
player.x = new_x
|
||||
player.y = new_y
|
||||
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||
status_display.text = "Exploring..."
|
||||
status_display.fill_color = mcrfpy.Color(100, 200, 100)
|
||||
|
||||
# Update FOV after movement
|
||||
update_fov(grid, fov_layer, new_x, new_y)
|
||||
|
||||
scene.on_key = handle_keys
|
||||
|
||||
# =============================================================================
|
||||
# Start the Game
|
||||
# =============================================================================
|
||||
|
||||
scene.activate()
|
||||
print("Part 5 loaded! Enemies lurk in the dungeon...")
|
||||
|
|
@ -1,940 +0,0 @@
|
|||
"""McRogueFace - Part 6: Combat System
|
||||
|
||||
Documentation: https://mcrogueface.github.io/tutorial/part_06_combat
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/tutorials/part_06_combat/part_06_combat.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
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
|
||||
# Sprite indices for CP437 tileset
|
||||
SPRITE_WALL = 35 # '#' - wall
|
||||
SPRITE_FLOOR = 46 # '.' - floor
|
||||
SPRITE_PLAYER = 64 # '@' - player
|
||||
SPRITE_CORPSE = 37 # '%' - remains
|
||||
|
||||
# Enemy sprites
|
||||
SPRITE_GOBLIN = 103 # 'g'
|
||||
SPRITE_ORC = 111 # 'o'
|
||||
SPRITE_TROLL = 116 # 't'
|
||||
|
||||
# Grid dimensions
|
||||
GRID_WIDTH = 50
|
||||
GRID_HEIGHT = 35
|
||||
|
||||
# Room generation parameters
|
||||
ROOM_MIN_SIZE = 6
|
||||
ROOM_MAX_SIZE = 12
|
||||
MAX_ROOMS = 8
|
||||
|
||||
# Enemy spawn parameters
|
||||
MAX_ENEMIES_PER_ROOM = 3
|
||||
|
||||
# FOV settings
|
||||
FOV_RADIUS = 8
|
||||
|
||||
# Visibility colors
|
||||
COLOR_VISIBLE = mcrfpy.Color(0, 0, 0, 0)
|
||||
COLOR_DISCOVERED = mcrfpy.Color(0, 0, 40, 180)
|
||||
COLOR_UNKNOWN = mcrfpy.Color(0, 0, 0, 255)
|
||||
|
||||
# Message log settings
|
||||
MAX_MESSAGES = 5
|
||||
|
||||
# =============================================================================
|
||||
# Fighter Component
|
||||
# =============================================================================
|
||||
|
||||
@dataclass
|
||||
class Fighter:
|
||||
"""Combat stats for an entity."""
|
||||
hp: int
|
||||
max_hp: int
|
||||
attack: int
|
||||
defense: int
|
||||
name: str
|
||||
is_player: bool = False
|
||||
|
||||
@property
|
||||
def is_alive(self) -> bool:
|
||||
"""Check if this fighter is still alive."""
|
||||
return self.hp > 0
|
||||
|
||||
def take_damage(self, amount: int) -> int:
|
||||
"""Apply damage and return actual damage taken."""
|
||||
actual_damage = min(self.hp, amount)
|
||||
self.hp -= actual_damage
|
||||
return actual_damage
|
||||
|
||||
def heal(self, amount: int) -> int:
|
||||
"""Heal and return actual amount healed."""
|
||||
actual_heal = min(self.max_hp - self.hp, amount)
|
||||
self.hp += actual_heal
|
||||
return actual_heal
|
||||
|
||||
# =============================================================================
|
||||
# Enemy Templates
|
||||
# =============================================================================
|
||||
|
||||
ENEMY_TEMPLATES = {
|
||||
"goblin": {
|
||||
"sprite": SPRITE_GOBLIN,
|
||||
"hp": 6,
|
||||
"attack": 3,
|
||||
"defense": 0,
|
||||
"color": mcrfpy.Color(100, 200, 100)
|
||||
},
|
||||
"orc": {
|
||||
"sprite": SPRITE_ORC,
|
||||
"hp": 10,
|
||||
"attack": 4,
|
||||
"defense": 1,
|
||||
"color": mcrfpy.Color(100, 150, 100)
|
||||
},
|
||||
"troll": {
|
||||
"sprite": SPRITE_TROLL,
|
||||
"hp": 16,
|
||||
"attack": 6,
|
||||
"defense": 2,
|
||||
"color": mcrfpy.Color(50, 150, 50)
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Global State
|
||||
# =============================================================================
|
||||
|
||||
# Entity data storage
|
||||
entity_data: dict[mcrfpy.Entity, Fighter] = {}
|
||||
|
||||
# Global references
|
||||
player: Optional[mcrfpy.Entity] = None
|
||||
grid: Optional[mcrfpy.Grid] = None
|
||||
fov_layer = None
|
||||
texture: Optional[mcrfpy.Texture] = None
|
||||
|
||||
# Game state
|
||||
game_over: bool = False
|
||||
|
||||
# Message log
|
||||
messages: list[tuple[str, mcrfpy.Color]] = []
|
||||
|
||||
# =============================================================================
|
||||
# Room Class
|
||||
# =============================================================================
|
||||
|
||||
class RectangularRoom:
|
||||
"""A rectangular room with its position and size."""
|
||||
|
||||
def __init__(self, x: int, y: int, width: int, height: int):
|
||||
self.x1 = x
|
||||
self.y1 = y
|
||||
self.x2 = x + width
|
||||
self.y2 = y + height
|
||||
|
||||
@property
|
||||
def center(self) -> tuple[int, int]:
|
||||
center_x = (self.x1 + self.x2) // 2
|
||||
center_y = (self.y1 + self.y2) // 2
|
||||
return center_x, center_y
|
||||
|
||||
@property
|
||||
def inner(self) -> tuple[slice, slice]:
|
||||
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
|
||||
|
||||
def intersects(self, other: "RectangularRoom") -> bool:
|
||||
return (
|
||||
self.x1 <= other.x2 and
|
||||
self.x2 >= other.x1 and
|
||||
self.y1 <= other.y2 and
|
||||
self.y2 >= other.y1
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Exploration Tracking
|
||||
# =============================================================================
|
||||
|
||||
explored: list[list[bool]] = []
|
||||
|
||||
def init_explored() -> None:
|
||||
"""Initialize the explored array to all False."""
|
||||
global explored
|
||||
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||
|
||||
def mark_explored(x: int, y: int) -> None:
|
||||
"""Mark a tile as explored."""
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
explored[y][x] = True
|
||||
|
||||
def is_explored(x: int, y: int) -> bool:
|
||||
"""Check if a tile has been explored."""
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
return explored[y][x]
|
||||
return False
|
||||
|
||||
# =============================================================================
|
||||
# Message Log
|
||||
# =============================================================================
|
||||
|
||||
def add_message(text: str, color: mcrfpy.Color = None) -> None:
|
||||
"""Add a message to the log.
|
||||
|
||||
Args:
|
||||
text: The message text
|
||||
color: Optional color (defaults to white)
|
||||
"""
|
||||
if color is None:
|
||||
color = mcrfpy.Color(255, 255, 255)
|
||||
|
||||
messages.append((text, color))
|
||||
|
||||
# Keep only the most recent messages
|
||||
while len(messages) > MAX_MESSAGES:
|
||||
messages.pop(0)
|
||||
|
||||
# Update the message display
|
||||
update_message_display()
|
||||
|
||||
def update_message_display() -> None:
|
||||
"""Update the message log UI."""
|
||||
if message_log_caption is None:
|
||||
return
|
||||
|
||||
# Combine messages into a single string
|
||||
lines = []
|
||||
for text, color in messages:
|
||||
lines.append(text)
|
||||
|
||||
message_log_caption.text = "\n".join(lines)
|
||||
|
||||
def clear_messages() -> None:
|
||||
"""Clear all messages."""
|
||||
global messages
|
||||
messages = []
|
||||
update_message_display()
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon Generation
|
||||
# =============================================================================
|
||||
|
||||
def fill_with_walls(target_grid: mcrfpy.Grid) -> None:
|
||||
"""Fill the entire grid with wall tiles."""
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
cell = target_grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_WALL
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
|
||||
def carve_room(target_grid: mcrfpy.Grid, room: RectangularRoom) -> None:
|
||||
"""Carve out a room by setting its inner tiles to floor."""
|
||||
inner_x, inner_y = room.inner
|
||||
for y in range(inner_y.start, inner_y.stop):
|
||||
for x in range(inner_x.start, inner_x.stop):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
cell = target_grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
def carve_tunnel_horizontal(target_grid: mcrfpy.Grid, x1: int, x2: int, y: int) -> None:
|
||||
"""Carve a horizontal tunnel."""
|
||||
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
cell = target_grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
def carve_tunnel_vertical(target_grid: mcrfpy.Grid, y1: int, y2: int, x: int) -> None:
|
||||
"""Carve a vertical tunnel."""
|
||||
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
cell = target_grid.at(x, y)
|
||||
cell.tilesprite = SPRITE_FLOOR
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
def carve_l_tunnel(
|
||||
target_grid: mcrfpy.Grid,
|
||||
start: tuple[int, int],
|
||||
end: tuple[int, int]
|
||||
) -> None:
|
||||
"""Carve an L-shaped tunnel between two points."""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
if random.random() < 0.5:
|
||||
carve_tunnel_horizontal(target_grid, x1, x2, y1)
|
||||
carve_tunnel_vertical(target_grid, y1, y2, x2)
|
||||
else:
|
||||
carve_tunnel_vertical(target_grid, y1, y2, x1)
|
||||
carve_tunnel_horizontal(target_grid, x1, x2, y2)
|
||||
|
||||
# =============================================================================
|
||||
# Entity Management
|
||||
# =============================================================================
|
||||
|
||||
def spawn_enemy(target_grid: mcrfpy.Grid, x: int, y: int, enemy_type: str, tex: mcrfpy.Texture) -> mcrfpy.Entity:
|
||||
"""Spawn an enemy at the given position."""
|
||||
template = ENEMY_TEMPLATES[enemy_type]
|
||||
|
||||
enemy = mcrfpy.Entity(
|
||||
grid_pos=(x, y),
|
||||
texture=tex,
|
||||
sprite_index=template["sprite"]
|
||||
)
|
||||
|
||||
enemy.visible = False
|
||||
|
||||
target_grid.entities.append(enemy)
|
||||
|
||||
# Create Fighter component for this enemy
|
||||
entity_data[enemy] = Fighter(
|
||||
hp=template["hp"],
|
||||
max_hp=template["hp"],
|
||||
attack=template["attack"],
|
||||
defense=template["defense"],
|
||||
name=enemy_type.capitalize(),
|
||||
is_player=False
|
||||
)
|
||||
|
||||
return enemy
|
||||
|
||||
def spawn_enemies_in_room(target_grid: mcrfpy.Grid, room: RectangularRoom, tex: mcrfpy.Texture) -> None:
|
||||
"""Spawn random enemies in a room."""
|
||||
num_enemies = random.randint(0, MAX_ENEMIES_PER_ROOM)
|
||||
|
||||
for _ in range(num_enemies):
|
||||
inner_x, inner_y = room.inner
|
||||
x = random.randint(inner_x.start, inner_x.stop - 1)
|
||||
y = random.randint(inner_y.start, inner_y.stop - 1)
|
||||
|
||||
if get_entity_at(target_grid, x, y) is not None:
|
||||
continue
|
||||
|
||||
roll = random.random()
|
||||
if roll < 0.6:
|
||||
enemy_type = "goblin"
|
||||
elif roll < 0.9:
|
||||
enemy_type = "orc"
|
||||
else:
|
||||
enemy_type = "troll"
|
||||
|
||||
spawn_enemy(target_grid, x, y, enemy_type, tex)
|
||||
|
||||
def get_entity_at(target_grid: mcrfpy.Grid, x: int, y: int) -> Optional[mcrfpy.Entity]:
|
||||
"""Get any entity at the given position."""
|
||||
for entity in target_grid.entities:
|
||||
if int(entity.x) == x and int(entity.y) == y:
|
||||
# Check if this entity is alive (or is a non-Fighter entity)
|
||||
if entity in entity_data:
|
||||
if entity_data[entity].is_alive:
|
||||
return entity
|
||||
else:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mcrfpy.Entity = None) -> Optional[mcrfpy.Entity]:
|
||||
"""Get any living entity that blocks movement at the given position."""
|
||||
for entity in target_grid.entities:
|
||||
if entity == exclude:
|
||||
continue
|
||||
if int(entity.x) == x and int(entity.y) == y:
|
||||
if entity in entity_data and entity_data[entity].is_alive:
|
||||
return entity
|
||||
return None
|
||||
|
||||
def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
"""Remove an entity from the grid and data storage."""
|
||||
# Find and remove from grid
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
|
||||
# Remove from entity data
|
||||
if entity in entity_data:
|
||||
del entity_data[entity]
|
||||
|
||||
def clear_enemies(target_grid: mcrfpy.Grid) -> None:
|
||||
"""Remove all enemies from the grid."""
|
||||
enemies_to_remove = []
|
||||
|
||||
for entity in target_grid.entities:
|
||||
if entity in entity_data and not entity_data[entity].is_player:
|
||||
enemies_to_remove.append(entity)
|
||||
|
||||
for enemy in enemies_to_remove:
|
||||
remove_entity(target_grid, enemy)
|
||||
|
||||
# =============================================================================
|
||||
# Combat System
|
||||
# =============================================================================
|
||||
|
||||
def calculate_damage(attacker: Fighter, defender: Fighter) -> int:
|
||||
"""Calculate damage dealt from attacker to defender.
|
||||
|
||||
Args:
|
||||
attacker: The attacking Fighter
|
||||
defender: The defending Fighter
|
||||
|
||||
Returns:
|
||||
The amount of damage to deal (minimum 0)
|
||||
"""
|
||||
damage = max(0, attacker.attack - defender.defense)
|
||||
return damage
|
||||
|
||||
def perform_attack(attacker_entity: mcrfpy.Entity, defender_entity: mcrfpy.Entity) -> None:
|
||||
"""Execute an attack from one entity to another.
|
||||
|
||||
Args:
|
||||
attacker_entity: The entity performing the attack
|
||||
defender_entity: The entity being attacked
|
||||
"""
|
||||
global game_over
|
||||
|
||||
attacker = entity_data.get(attacker_entity)
|
||||
defender = entity_data.get(defender_entity)
|
||||
|
||||
if attacker is None or defender is None:
|
||||
return
|
||||
|
||||
# Calculate and apply damage
|
||||
damage = calculate_damage(attacker, defender)
|
||||
defender.take_damage(damage)
|
||||
|
||||
# Generate combat message
|
||||
if damage > 0:
|
||||
if attacker.is_player:
|
||||
add_message(
|
||||
f"You hit the {defender.name} for {damage} damage!",
|
||||
mcrfpy.Color(200, 200, 200)
|
||||
)
|
||||
else:
|
||||
add_message(
|
||||
f"The {attacker.name} hits you for {damage} damage!",
|
||||
mcrfpy.Color(255, 150, 150)
|
||||
)
|
||||
else:
|
||||
if attacker.is_player:
|
||||
add_message(
|
||||
f"You hit the {defender.name} but deal no damage.",
|
||||
mcrfpy.Color(150, 150, 150)
|
||||
)
|
||||
else:
|
||||
add_message(
|
||||
f"The {attacker.name} hits you but deals no damage.",
|
||||
mcrfpy.Color(150, 150, 200)
|
||||
)
|
||||
|
||||
# Check for death
|
||||
if not defender.is_alive:
|
||||
handle_death(defender_entity, defender)
|
||||
|
||||
def handle_death(entity: mcrfpy.Entity, fighter: Fighter) -> None:
|
||||
"""Handle the death of an entity.
|
||||
|
||||
Args:
|
||||
entity: The entity that died
|
||||
fighter: The Fighter component of the dead entity
|
||||
"""
|
||||
global game_over, grid
|
||||
|
||||
if fighter.is_player:
|
||||
# Player death
|
||||
add_message("You have died!", mcrfpy.Color(255, 50, 50))
|
||||
add_message("Press R to restart or Escape to quit.", mcrfpy.Color(200, 200, 200))
|
||||
game_over = True
|
||||
|
||||
# Change player sprite to corpse
|
||||
entity.sprite_index = SPRITE_CORPSE
|
||||
else:
|
||||
# Enemy death
|
||||
add_message(f"The {fighter.name} dies!", mcrfpy.Color(100, 255, 100))
|
||||
|
||||
# Replace with corpse
|
||||
entity.sprite_index = SPRITE_CORPSE
|
||||
|
||||
# Mark as dead (hp is already 0)
|
||||
# Remove blocking but keep visual corpse
|
||||
# Actually remove the entity and its data
|
||||
remove_entity(grid, entity)
|
||||
|
||||
# Update HP display
|
||||
update_hp_display()
|
||||
|
||||
# =============================================================================
|
||||
# Field of View
|
||||
# =============================================================================
|
||||
|
||||
def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
||||
"""Update visibility of all entities based on FOV."""
|
||||
global player
|
||||
|
||||
for entity in target_grid.entities:
|
||||
if entity == player:
|
||||
entity.visible = True
|
||||
continue
|
||||
|
||||
ex, ey = int(entity.x), int(entity.y)
|
||||
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||
|
||||
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||
"""Update the field of view visualization."""
|
||||
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if target_grid.is_in_fov(x, y):
|
||||
mark_explored(x, y)
|
||||
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||
elif is_explored(x, y):
|
||||
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||
else:
|
||||
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
|
||||
update_entity_visibility(target_grid)
|
||||
|
||||
# =============================================================================
|
||||
# Movement and Actions
|
||||
# =============================================================================
|
||||
|
||||
def can_move_to(target_grid: mcrfpy.Grid, x: int, y: int, mover: mcrfpy.Entity = None) -> bool:
|
||||
"""Check if a position is valid for movement."""
|
||||
if x < 0 or x >= GRID_WIDTH or y < 0 or y >= GRID_HEIGHT:
|
||||
return False
|
||||
|
||||
if not target_grid.at(x, y).walkable:
|
||||
return False
|
||||
|
||||
blocker = get_blocking_entity_at(target_grid, x, y, exclude=mover)
|
||||
if blocker is not None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def try_move_or_attack(dx: int, dy: int) -> None:
|
||||
"""Attempt to move the player or attack if blocked by enemy.
|
||||
|
||||
Args:
|
||||
dx: Change in X position (-1, 0, or 1)
|
||||
dy: Change in Y position (-1, 0, or 1)
|
||||
"""
|
||||
global player, grid, fov_layer, game_over
|
||||
|
||||
if game_over:
|
||||
return
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
target_x = px + dx
|
||||
target_y = py + dy
|
||||
|
||||
# Check bounds
|
||||
if target_x < 0 or target_x >= GRID_WIDTH or target_y < 0 or target_y >= GRID_HEIGHT:
|
||||
return
|
||||
|
||||
# Check for blocking entity
|
||||
blocker = get_blocking_entity_at(grid, target_x, target_y, exclude=player)
|
||||
|
||||
if blocker is not None:
|
||||
# Attack the blocking entity
|
||||
perform_attack(player, blocker)
|
||||
# After player attacks, enemies take their turn
|
||||
enemy_turn()
|
||||
elif grid.at(target_x, target_y).walkable:
|
||||
# Move to the empty tile
|
||||
player.x = target_x
|
||||
player.y = target_y
|
||||
pos_display.text = f"Position: ({target_x}, {target_y})"
|
||||
|
||||
# Update FOV after movement
|
||||
update_fov(grid, fov_layer, target_x, target_y)
|
||||
|
||||
# Enemies take their turn after player moves
|
||||
enemy_turn()
|
||||
|
||||
# Update HP display
|
||||
update_hp_display()
|
||||
|
||||
# =============================================================================
|
||||
# Enemy AI
|
||||
# =============================================================================
|
||||
|
||||
def enemy_turn() -> None:
|
||||
"""Execute enemy actions."""
|
||||
global player, grid, game_over
|
||||
|
||||
if game_over:
|
||||
return
|
||||
|
||||
player_x, player_y = int(player.x), int(player.y)
|
||||
|
||||
# Collect enemies that can act
|
||||
enemies = []
|
||||
for entity in grid.entities:
|
||||
if entity == player:
|
||||
continue
|
||||
if entity in entity_data and entity_data[entity].is_alive:
|
||||
enemies.append(entity)
|
||||
|
||||
for enemy in enemies:
|
||||
fighter = entity_data.get(enemy)
|
||||
if fighter is None or not fighter.is_alive:
|
||||
continue
|
||||
|
||||
ex, ey = int(enemy.x), int(enemy.y)
|
||||
|
||||
# Only act if in player's FOV (aware of player)
|
||||
if not grid.is_in_fov(ex, ey):
|
||||
continue
|
||||
|
||||
# Check if adjacent to player
|
||||
dx = player_x - ex
|
||||
dy = player_y - ey
|
||||
|
||||
if abs(dx) <= 1 and abs(dy) <= 1 and (dx != 0 or dy != 0):
|
||||
# Adjacent - attack!
|
||||
perform_attack(enemy, player)
|
||||
else:
|
||||
# Not adjacent - try to move toward player
|
||||
move_toward_player(enemy, ex, ey, player_x, player_y)
|
||||
|
||||
def move_toward_player(enemy: mcrfpy.Entity, ex: int, ey: int, px: int, py: int) -> None:
|
||||
"""Move an enemy one step toward the player.
|
||||
|
||||
Uses simple greedy movement - not true pathfinding.
|
||||
"""
|
||||
global grid
|
||||
|
||||
# Calculate direction to player
|
||||
dx = 0
|
||||
dy = 0
|
||||
|
||||
if px < ex:
|
||||
dx = -1
|
||||
elif px > ex:
|
||||
dx = 1
|
||||
|
||||
if py < ey:
|
||||
dy = -1
|
||||
elif py > ey:
|
||||
dy = 1
|
||||
|
||||
# Try to move in the desired direction
|
||||
# First try the combined direction
|
||||
new_x = ex + dx
|
||||
new_y = ey + dy
|
||||
|
||||
if can_move_to(grid, new_x, new_y, enemy):
|
||||
enemy.x = new_x
|
||||
enemy.y = new_y
|
||||
elif dx != 0 and can_move_to(grid, ex + dx, ey, enemy):
|
||||
# Try horizontal only
|
||||
enemy.x = ex + dx
|
||||
elif dy != 0 and can_move_to(grid, ex, ey + dy, enemy):
|
||||
# Try vertical only
|
||||
enemy.y = ey + dy
|
||||
# If all fail, enemy stays in place
|
||||
|
||||
# =============================================================================
|
||||
# UI Updates
|
||||
# =============================================================================
|
||||
|
||||
def update_hp_display() -> None:
|
||||
"""Update the HP display in the UI."""
|
||||
global player
|
||||
|
||||
if hp_display is None or player is None:
|
||||
return
|
||||
|
||||
if player in entity_data:
|
||||
fighter = entity_data[player]
|
||||
hp_display.text = f"HP: {fighter.hp}/{fighter.max_hp}"
|
||||
|
||||
# Color based on health percentage
|
||||
hp_percent = fighter.hp / fighter.max_hp
|
||||
if hp_percent > 0.6:
|
||||
hp_display.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
elif hp_percent > 0.3:
|
||||
hp_display.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
else:
|
||||
hp_display.fill_color = mcrfpy.Color(255, 100, 100)
|
||||
|
||||
# =============================================================================
|
||||
# Game Setup
|
||||
# =============================================================================
|
||||
|
||||
# Create the scene
|
||||
scene = mcrfpy.Scene("game")
|
||||
|
||||
# Load texture
|
||||
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
|
||||
# Create the grid
|
||||
grid = mcrfpy.Grid(
|
||||
pos=(50, 80),
|
||||
size=(800, 480),
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
texture=texture,
|
||||
zoom=1.0
|
||||
)
|
||||
|
||||
# Generate initial dungeon structure
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
|
||||
rooms: list[RectangularRoom] = []
|
||||
|
||||
for _ in range(MAX_ROOMS):
|
||||
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
overlaps = False
|
||||
for other_room in rooms:
|
||||
if new_room.intersects(other_room):
|
||||
overlaps = True
|
||||
break
|
||||
|
||||
if overlaps:
|
||||
continue
|
||||
|
||||
carve_room(grid, new_room)
|
||||
|
||||
if rooms:
|
||||
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||
|
||||
rooms.append(new_room)
|
||||
|
||||
# Get player starting position
|
||||
if rooms:
|
||||
player_start_x, player_start_y = rooms[0].center
|
||||
else:
|
||||
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||
|
||||
# Add FOV layer
|
||||
fov_layer = grid.add_layer("color", z_index=-1)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
|
||||
# Create the player
|
||||
player = mcrfpy.Entity(
|
||||
grid_pos=(player_start_x, player_start_y),
|
||||
texture=texture,
|
||||
sprite_index=SPRITE_PLAYER
|
||||
)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Create player Fighter component
|
||||
entity_data[player] = Fighter(
|
||||
hp=30,
|
||||
max_hp=30,
|
||||
attack=5,
|
||||
defense=2,
|
||||
name="Player",
|
||||
is_player=True
|
||||
)
|
||||
|
||||
# Spawn enemies in all rooms except the first
|
||||
for i, room in enumerate(rooms):
|
||||
if i == 0:
|
||||
continue
|
||||
spawn_enemies_in_room(grid, room, texture)
|
||||
|
||||
# Calculate initial FOV
|
||||
update_fov(grid, fov_layer, player_start_x, player_start_y)
|
||||
|
||||
# Add grid to scene
|
||||
scene.children.append(grid)
|
||||
|
||||
# =============================================================================
|
||||
# UI Elements
|
||||
# =============================================================================
|
||||
|
||||
title = mcrfpy.Caption(
|
||||
pos=(50, 15),
|
||||
text="Part 6: Combat System"
|
||||
)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.font_size = 24
|
||||
scene.children.append(title)
|
||||
|
||||
instructions = mcrfpy.Caption(
|
||||
pos=(50, 50),
|
||||
text="WASD/Arrows: Move/Attack | R: Restart | Escape: Quit"
|
||||
)
|
||||
instructions.fill_color = mcrfpy.Color(180, 180, 180)
|
||||
instructions.font_size = 16
|
||||
scene.children.append(instructions)
|
||||
|
||||
# Position display
|
||||
pos_display = mcrfpy.Caption(
|
||||
pos=(50, 580),
|
||||
text=f"Position: ({int(player.x)}, {int(player.y)})"
|
||||
)
|
||||
pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
||||
pos_display.font_size = 16
|
||||
scene.children.append(pos_display)
|
||||
|
||||
# HP display
|
||||
hp_display = mcrfpy.Caption(
|
||||
pos=(300, 580),
|
||||
text="HP: 30/30"
|
||||
)
|
||||
hp_display.fill_color = mcrfpy.Color(100, 255, 100)
|
||||
hp_display.font_size = 16
|
||||
scene.children.append(hp_display)
|
||||
|
||||
# Message log (positioned below the grid)
|
||||
message_log_caption = mcrfpy.Caption(
|
||||
pos=(50, 610),
|
||||
text=""
|
||||
)
|
||||
message_log_caption.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
message_log_caption.font_size = 14
|
||||
scene.children.append(message_log_caption)
|
||||
|
||||
# Initial message
|
||||
add_message("Welcome to the dungeon! Find and defeat the enemies.", mcrfpy.Color(100, 100, 255))
|
||||
|
||||
# =============================================================================
|
||||
# Input Handling
|
||||
# =============================================================================
|
||||
|
||||
def restart_game() -> None:
|
||||
"""Restart the game with a new dungeon."""
|
||||
global player, grid, fov_layer, game_over, entity_data, rooms
|
||||
|
||||
game_over = False
|
||||
|
||||
# Clear all entities and data
|
||||
entity_data.clear()
|
||||
|
||||
# Remove all entities from grid
|
||||
while len(grid.entities) > 0:
|
||||
grid.entities.remove(0)
|
||||
|
||||
# Regenerate dungeon
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
clear_messages()
|
||||
|
||||
rooms = []
|
||||
|
||||
for _ in range(MAX_ROOMS):
|
||||
room_width = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
room_height = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
|
||||
x = random.randint(1, GRID_WIDTH - room_width - 2)
|
||||
y = random.randint(1, GRID_HEIGHT - room_height - 2)
|
||||
|
||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||
|
||||
overlaps = False
|
||||
for other_room in rooms:
|
||||
if new_room.intersects(other_room):
|
||||
overlaps = True
|
||||
break
|
||||
|
||||
if overlaps:
|
||||
continue
|
||||
|
||||
carve_room(grid, new_room)
|
||||
|
||||
if rooms:
|
||||
carve_l_tunnel(grid, new_room.center, rooms[-1].center)
|
||||
|
||||
rooms.append(new_room)
|
||||
|
||||
# Get new player starting position
|
||||
if rooms:
|
||||
new_x, new_y = rooms[0].center
|
||||
else:
|
||||
new_x, new_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||
|
||||
# Recreate player
|
||||
player = mcrfpy.Entity(
|
||||
grid_pos=(new_x, new_y),
|
||||
texture=texture,
|
||||
sprite_index=SPRITE_PLAYER
|
||||
)
|
||||
grid.entities.append(player)
|
||||
|
||||
entity_data[player] = Fighter(
|
||||
hp=30,
|
||||
max_hp=30,
|
||||
attack=5,
|
||||
defense=2,
|
||||
name="Player",
|
||||
is_player=True
|
||||
)
|
||||
|
||||
# Spawn enemies
|
||||
for i, room in enumerate(rooms):
|
||||
if i == 0:
|
||||
continue
|
||||
spawn_enemies_in_room(grid, room, texture)
|
||||
|
||||
# Reset FOV layer
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
|
||||
# Update displays
|
||||
update_fov(grid, fov_layer, new_x, new_y)
|
||||
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||
update_hp_display()
|
||||
|
||||
add_message("A new adventure begins!", mcrfpy.Color(100, 100, 255))
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
"""Handle keyboard input."""
|
||||
global game_over
|
||||
|
||||
if action != "start":
|
||||
return
|
||||
|
||||
# Handle restart
|
||||
if key == "R":
|
||||
restart_game()
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
mcrfpy.exit()
|
||||
return
|
||||
|
||||
# Ignore other input if game is over
|
||||
if game_over:
|
||||
return
|
||||
|
||||
# Movement and attack
|
||||
if key == "W" or key == "Up":
|
||||
try_move_or_attack(0, -1)
|
||||
elif key == "S" or key == "Down":
|
||||
try_move_or_attack(0, 1)
|
||||
elif key == "A" or key == "Left":
|
||||
try_move_or_attack(-1, 0)
|
||||
elif key == "D" or key == "Right":
|
||||
try_move_or_attack(1, 0)
|
||||
|
||||
scene.on_key = handle_keys
|
||||
|
||||
# =============================================================================
|
||||
# Start the Game
|
||||
# =============================================================================
|
||||
|
||||
scene.activate()
|
||||
print("Part 6 loaded! Combat is now active. Good luck!")
|
||||