Compare commits
85 Commits
master
...
rebase-for
| Author | SHA1 | Date |
|---|---|---|
|
|
3325e4895e | |
|
|
705943abba | |
|
|
47b485ca7d | |
|
|
f2eaee95ec | |
|
|
494658e5c3 | |
|
|
6e820af8c4 | |
|
|
cf485ef327 | |
|
|
3a1432212f | |
|
|
38b6a3cade | |
|
|
1bbb0aa5b8 | |
|
|
bec2b3294d | |
|
|
9486104377 | |
|
|
5267287b05 | |
|
|
b8af8bc870 | |
|
|
0ef0a5d506 | |
|
|
a41d3d4a54 | |
|
|
6d4bc2989c | |
|
|
d5a7cbca85 | |
|
|
5d8510747c | |
|
|
06052c81c9 | |
|
|
6fe7b842ef | |
|
|
50d926fe37 | |
|
|
795701c986 | |
|
|
884a49a63a | |
|
|
c4d5a497d4 | |
|
|
ba97aebf3e | |
|
|
ac0ec4bb71 | |
|
|
a455c44b34 | |
|
|
96e78e6150 | |
|
|
0dd86056a8 | |
|
|
26cb410b8e | |
|
|
d09fc87499 | |
|
|
b022dfa6e8 | |
|
|
232ce34d54 | |
|
|
c1c17bab69 | |
|
|
e85861cbb2 | |
|
|
d6446e18ea | |
|
|
d3826804a0 | |
|
|
b4c49c4619 | |
|
|
76ac236be3 | |
|
|
97793fb26b | |
|
|
b3134f0890 | |
|
|
dfcc39dd43 | |
|
|
29ac89b489 | |
|
|
b4daac6e0c | |
|
|
3fd60d76ea | |
|
|
99fa92f8ba | |
|
|
9441f357df | |
|
|
34feb226e4 | |
|
|
486a1cd17c | |
|
|
8d9148b88d | |
|
|
f1798189f0 | |
|
|
5b168737ce | |
|
|
6875cb5fe1 | |
|
|
87483cc8ad | |
|
|
620def19f1 | |
|
|
c9b97b9b35 | |
|
|
8e59152a8f | |
|
|
fedfcd46a3 | |
|
|
c551c721ce | |
|
|
d74635ee4e | |
|
|
47e823d5b9 | |
|
|
6dbf8a5119 | |
|
|
a53ae29467 | |
|
|
257aa3c3d2 | |
|
|
a4b6c2c428 | |
|
|
b0ef1d2303 | |
|
|
b3f946ecb2 | |
|
|
6a4150ec05 | |
|
|
e295bfb742 | |
|
|
2ec97dfb1c | |
|
|
f89896176c | |
|
|
de753713d5 | |
|
|
c8124e84dc | |
|
|
a1e9129923 | |
|
|
f23dfbe4ba | |
|
|
1e9fd77a13 | |
|
|
2c1946c29b | |
|
|
6d05f8bc63 | |
|
|
a4d0efe334 | |
|
|
6a47bc1e28 | |
|
|
6a2c3c6c36 | |
|
|
a6f59085eb | |
|
|
d2499a67f8 | |
|
|
1784489dfb |
|
|
@ -9,25 +9,4 @@ obj
|
||||||
build
|
build
|
||||||
lib
|
lib
|
||||||
obj
|
obj
|
||||||
__pycache__
|
|
||||||
|
|
||||||
.cache/
|
|
||||||
7DRL2025 Release/
|
|
||||||
CMakeFiles/
|
|
||||||
Makefile
|
|
||||||
*.md
|
|
||||||
*.zip
|
|
||||||
__lib/
|
|
||||||
_oldscripts/
|
|
||||||
assets/
|
|
||||||
cellular_automata_fire/
|
|
||||||
*.txt
|
|
||||||
deps/
|
|
||||||
fetch_issues_txt.py
|
|
||||||
forest_fire_CA.py
|
|
||||||
mcrogueface.github.io
|
|
||||||
scripts/
|
|
||||||
test_*
|
|
||||||
|
|
||||||
tcod_reference
|
|
||||||
.archive
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"gitea": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "/home/john/Development/discord_for_claude/forgejo-mcp.linux.amd64",
|
|
||||||
"args": ["stdio", "--server", "https://gamedev.ffwf.net/gitea/", "--token", "f58ec698a5edee82db4960920b13d3f7d0d58d8e"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
573
CLAUDE.md
573
CLAUDE.md
|
|
@ -1,573 +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
|
|
||||||
- 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: Create task to document it
|
|
||||||
- When documentation misleads you: Create task to correct or expand it
|
|
||||||
- When implementing a feature: Update the Gitea wiki if appropriate
|
|
||||||
|
|
||||||
5. **Cross-Reference Everything**
|
|
||||||
- Commit messages should reference issue numbers (e.g., "Fixes #104", "Addresses #125")
|
|
||||||
- Issue comments should link to commits when work is done
|
|
||||||
- Wiki pages should reference relevant issues for implementation details
|
|
||||||
- Issues should link to each other when dependencies exist
|
|
||||||
|
|
||||||
### Workflow Pattern
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ 1. Check Gitea Issues & Wiki │
|
|
||||||
│ - Is there an existing issue for this? │
|
|
||||||
│ - What's the current status? │
|
|
||||||
│ - Are there related issues or blockers? │
|
|
||||||
└─────────────────┬───────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ 2. Create Issues (if needed) │
|
|
||||||
│ - Break work into granular tasks │
|
|
||||||
│ - Tag appropriately │
|
|
||||||
│ - Link dependencies │
|
|
||||||
└─────────────────┬───────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ 3. Do the Work │
|
|
||||||
│ - Implement/fix/document │
|
|
||||||
│ - Write tests first (TDD) │
|
|
||||||
│ - Add inline documentation │
|
|
||||||
└─────────────────┬───────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ 4. Update Gitea │
|
|
||||||
│ - Add notes to affected issues │
|
|
||||||
│ - Create follow-up issues for discovered work │
|
|
||||||
│ - Update wiki if architecture/APIs changed │
|
|
||||||
│ - Add documentation correction tasks │
|
|
||||||
└─────────────────┬───────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────┐
|
|
||||||
│ 5. Commit & Reference │
|
|
||||||
│ - Commit messages reference issue numbers │
|
|
||||||
│ - Close issues or update status │
|
|
||||||
│ - Add commit links to issue comments │
|
|
||||||
└─────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benefits of Gitea-First Approach
|
|
||||||
|
|
||||||
- **Reduced Context Switching**: Check brief issue descriptions instead of re-reading entire codebase
|
|
||||||
- **Better Planning**: Issues provide roadmap; avoid duplicate or contradictory work
|
|
||||||
- **Living Documentation**: Wiki and issues stay current as work progresses
|
|
||||||
- **Historical Context**: Issue comments capture why decisions were made
|
|
||||||
- **Efficiency**: MCP tools allow programmatic access to project state
|
|
||||||
|
|
||||||
### MCP Tools Available
|
|
||||||
|
|
||||||
Claude Code has access to Gitea MCP tools for:
|
|
||||||
- `list_repo_issues` - Query current issues with filtering
|
|
||||||
- `get_issue` - Get detailed issue information
|
|
||||||
- `create_issue` - Create new issues programmatically
|
|
||||||
- `create_issue_comment` - Add comments to issues
|
|
||||||
- `edit_issue` - Update issue status, title, body
|
|
||||||
- `add_issue_labels` - Tag issues appropriately
|
|
||||||
- `add_issue_dependency` / `add_issue_blocking` - Link related issues
|
|
||||||
- Plus wiki, milestone, and label management tools
|
|
||||||
|
|
||||||
Use these tools liberally to keep the project organized!
|
|
||||||
|
|
||||||
### Gitea Label System
|
|
||||||
|
|
||||||
**IMPORTANT**: Always apply appropriate labels when creating new issues!
|
|
||||||
|
|
||||||
The project uses a structured label system to organize issues:
|
|
||||||
|
|
||||||
**Label Categories:**
|
|
||||||
|
|
||||||
1. **System Labels** (identify affected codebase area):
|
|
||||||
- `system:rendering` - Rendering pipeline and visuals
|
|
||||||
- `system:ui-hierarchy` - UI component hierarchy and composition
|
|
||||||
- `system:grid` - Grid system and spatial containers
|
|
||||||
- `system:animation` - Animation and property interpolation
|
|
||||||
- `system:python-binding` - Python/C++ binding layer
|
|
||||||
- `system:input` - Input handling and events
|
|
||||||
- `system:performance` - Performance optimization and profiling
|
|
||||||
- `system:documentation` - Documentation infrastructure
|
|
||||||
|
|
||||||
2. **Priority Labels** (development timeline):
|
|
||||||
- `priority:tier1-active` - Current development focus - critical path to v1.0
|
|
||||||
- `priority:tier2-foundation` - Important foundation work - not blocking v1.0
|
|
||||||
- `priority:tier3-future` - Future features - deferred until after v1.0
|
|
||||||
|
|
||||||
3. **Type/Scope Labels** (effort and complexity):
|
|
||||||
- `Major Feature` - Significant time and effort required
|
|
||||||
- `Minor Feature` - Some effort required to create or overhaul functionality
|
|
||||||
- `Tiny Feature` - Quick and easy - a few lines or little interconnection
|
|
||||||
- `Bugfix` - Fixes incorrect behavior
|
|
||||||
- `Refactoring & Cleanup` - No new functionality, just improving codebase
|
|
||||||
- `Documentation` - Documentation work
|
|
||||||
- `Demo Target` - Functionality to demonstrate
|
|
||||||
|
|
||||||
4. **Workflow Labels** (current blockers/needs):
|
|
||||||
- `workflow:blocked` - Blocked by other work - waiting on dependencies
|
|
||||||
- `workflow:needs-documentation` - Needs documentation before or after implementation
|
|
||||||
- `workflow:needs-benchmark` - Needs performance testing and benchmarks
|
|
||||||
- `Alpha Release Requirement` - Blocker to 0.1 Alpha release
|
|
||||||
|
|
||||||
**When creating issues:**
|
|
||||||
- Apply at least one `system:*` label (what part of codebase)
|
|
||||||
- Apply one `priority:tier*` label (when to address it)
|
|
||||||
- Apply one type label (`Major Feature`, `Minor Feature`, `Tiny Feature`, or `Bugfix`)
|
|
||||||
- Apply `workflow:*` labels if applicable (blocked, needs docs, needs benchmarks)
|
|
||||||
|
|
||||||
**Example label combinations:**
|
|
||||||
- New rendering feature: `system:rendering`, `priority:tier2-foundation`, `Major Feature`
|
|
||||||
- Python API improvement: `system:python-binding`, `priority:tier1-active`, `Minor Feature`
|
|
||||||
- Performance work: `system:performance`, `priority:tier1-active`, `Major Feature`, `workflow:needs-benchmark`
|
|
||||||
|
|
||||||
**⚠️ CRITICAL BUG**: The Gitea MCP tool (v0.07) has a label application bug documented in `GITEA_MCP_LABEL_BUG_REPORT.md`:
|
|
||||||
- `add_issue_labels` and `replace_issue_labels` behave inconsistently
|
|
||||||
- Single ID arrays produce different results than multi-ID arrays for the SAME IDs
|
|
||||||
- Label IDs do not map reliably to actual labels
|
|
||||||
|
|
||||||
**Workaround Options:**
|
|
||||||
1. **Best**: Apply labels manually via web interface: `https://gamedev.ffwf.net/gitea/john/McRogueFace/issues/<number>`
|
|
||||||
2. **Automated**: Apply labels ONE AT A TIME using single-element arrays (slower but more reliable)
|
|
||||||
3. **Use single-ID mapping** (documented below)
|
|
||||||
|
|
||||||
**Label ID Reference** (for documentation purposes - see issue #131 for details):
|
|
||||||
```
|
|
||||||
1=Major Feature, 2=Alpha Release, 3=Bugfix, 4=Demo Target, 5=Documentation,
|
|
||||||
6=Minor Feature, 7=tier1-active, 8=tier2-foundation, 9=tier3-future,
|
|
||||||
10=Refactoring, 11=animation, 12=docs, 13=grid, 14=input, 15=performance,
|
|
||||||
16=python-binding, 17=rendering, 18=ui-hierarchy, 19=Tiny Feature,
|
|
||||||
20=blocked, 21=needs-benchmark, 22=needs-documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build the project (compiles to ./build directory)
|
|
||||||
make
|
|
||||||
|
|
||||||
# Or use the build script directly
|
|
||||||
./build.sh
|
|
||||||
|
|
||||||
# Run the game
|
|
||||||
make run
|
|
||||||
|
|
||||||
# Clean build artifacts
|
|
||||||
make clean
|
|
||||||
|
|
||||||
# The executable and all assets are in ./build/
|
|
||||||
cd build
|
|
||||||
./mcrogueface
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Architecture
|
|
||||||
|
|
||||||
McRogueFace is a C++ game engine with Python scripting support, designed for creating roguelike games. The architecture consists of:
|
|
||||||
|
|
||||||
### Core Engine (C++)
|
|
||||||
- **Entry Point**: `src/main.cpp` initializes the game engine
|
|
||||||
- **Scene System**: `Scene.h/cpp` manages game states
|
|
||||||
- **Entity System**: `UIEntity.h/cpp` provides game objects
|
|
||||||
- **Python Integration**: `McRFPy_API.h/cpp` exposes engine functionality to Python
|
|
||||||
- **UI Components**: `UIFrame`, `UICaption`, `UISprite`, `UIGrid` for rendering
|
|
||||||
|
|
||||||
### Game Logic (Python)
|
|
||||||
- **Main Script**: `src/scripts/game.py` contains game initialization and scene setup
|
|
||||||
- **Entity System**: `src/scripts/cos_entities.py` implements game entities (Player, Enemy, Boulder, etc.)
|
|
||||||
- **Level Generation**: `src/scripts/cos_level.py` uses BSP for procedural dungeon generation
|
|
||||||
- **Tile System**: `src/scripts/cos_tiles.py` implements Wave Function Collapse for tile placement
|
|
||||||
|
|
||||||
### Key Python API (`mcrfpy` module)
|
|
||||||
The C++ engine exposes these primary functions to Python:
|
|
||||||
- Scene Management: `createScene()`, `setScene()`, `sceneUI()`
|
|
||||||
- Entity Creation: `Entity()` with position and sprite properties
|
|
||||||
- Grid Management: `Grid()` for tilemap rendering
|
|
||||||
- Input Handling: `keypressScene()` for keyboard events
|
|
||||||
- Audio: `createSoundBuffer()`, `playSound()`, `setVolume()`
|
|
||||||
- Timers: `setTimer()`, `delTimer()` for event scheduling
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
### Running the Game
|
|
||||||
After building, the executable expects:
|
|
||||||
- `assets/` directory with sprites, fonts, and audio
|
|
||||||
- `scripts/` directory with Python game files
|
|
||||||
- Python 3.12 shared libraries in `./lib/`
|
|
||||||
|
|
||||||
### Modifying Game Logic
|
|
||||||
- Game scripts are in `src/scripts/`
|
|
||||||
- Main game entry is `game.py`
|
|
||||||
- Entity behavior in `cos_entities.py`
|
|
||||||
- Level generation in `cos_level.py`
|
|
||||||
|
|
||||||
### Adding New Features
|
|
||||||
1. C++ API additions go in `src/McRFPy_API.cpp`
|
|
||||||
2. Expose to Python using the existing binding pattern
|
|
||||||
3. Update Python scripts to use new functionality
|
|
||||||
|
|
||||||
## Testing Game Changes
|
|
||||||
|
|
||||||
Currently no automated test suite. Manual testing workflow:
|
|
||||||
1. Build with `make`
|
|
||||||
2. Run `make run` or `cd build && ./mcrogueface`
|
|
||||||
3. Test specific features through gameplay
|
|
||||||
4. Check console output for Python errors
|
|
||||||
|
|
||||||
### Quick Testing Commands
|
|
||||||
```bash
|
|
||||||
# Test basic functionality
|
|
||||||
make test
|
|
||||||
|
|
||||||
# Run in Python interactive mode
|
|
||||||
make python
|
|
||||||
|
|
||||||
# Test headless mode
|
|
||||||
cd build
|
|
||||||
./mcrogueface --headless -c "import mcrfpy; print('Headless test')"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Development Tasks
|
|
||||||
|
|
||||||
### Compiling McRogueFace
|
|
||||||
```bash
|
|
||||||
# Standard build (to ./build directory)
|
|
||||||
make
|
|
||||||
|
|
||||||
# Full rebuild
|
|
||||||
make clean && make
|
|
||||||
|
|
||||||
# Manual CMake build
|
|
||||||
mkdir build && cd build
|
|
||||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
|
||||||
make -j$(nproc)
|
|
||||||
|
|
||||||
# The library path issue: if linking fails, check that libraries are in __lib/
|
|
||||||
# CMakeLists.txt expects: link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running and Capturing Output
|
|
||||||
```bash
|
|
||||||
# Run with timeout and capture output
|
|
||||||
cd build
|
|
||||||
timeout 5 ./mcrogueface 2>&1 | tee output.log
|
|
||||||
|
|
||||||
# Run in background and kill after delay
|
|
||||||
./mcrogueface > output.txt 2>&1 & PID=$!; sleep 3; kill $PID 2>/dev/null
|
|
||||||
|
|
||||||
# Just capture first N lines (useful for crashes)
|
|
||||||
./mcrogueface 2>&1 | head -50
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debugging with GDB
|
|
||||||
```bash
|
|
||||||
# Interactive debugging
|
|
||||||
gdb ./mcrogueface
|
|
||||||
(gdb) run
|
|
||||||
(gdb) bt # backtrace after crash
|
|
||||||
|
|
||||||
# Batch mode debugging (non-interactive)
|
|
||||||
gdb -batch -ex run -ex where -ex quit ./mcrogueface 2>&1
|
|
||||||
|
|
||||||
# Get just the backtrace after a crash
|
|
||||||
gdb -batch -ex "run" -ex "bt" ./mcrogueface 2>&1 | head -50
|
|
||||||
|
|
||||||
# Debug with specific commands
|
|
||||||
echo -e "run\nbt 5\nquit\ny" | gdb ./mcrogueface 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing Different Python Scripts
|
|
||||||
```bash
|
|
||||||
# The game automatically runs build/scripts/game.py on startup
|
|
||||||
# To test different behavior:
|
|
||||||
|
|
||||||
# Option 1: Replace game.py temporarily
|
|
||||||
cd build
|
|
||||||
cp scripts/my_test_script.py scripts/game.py
|
|
||||||
./mcrogueface
|
|
||||||
|
|
||||||
# Option 2: Backup original and test
|
|
||||||
mv scripts/game.py scripts/game.py.bak
|
|
||||||
cp my_test.py scripts/game.py
|
|
||||||
./mcrogueface
|
|
||||||
mv scripts/game.py.bak scripts/game.py
|
|
||||||
|
|
||||||
# Option 3: For quick tests, create minimal game.py
|
|
||||||
echo 'import mcrfpy; print("Test"); mcrfpy.createScene("test")' > scripts/game.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Understanding Key Macros and Patterns
|
|
||||||
|
|
||||||
#### RET_PY_INSTANCE Macro (UIDrawable.h)
|
|
||||||
This macro handles converting C++ UI objects to their Python equivalents:
|
|
||||||
```cpp
|
|
||||||
RET_PY_INSTANCE(target);
|
|
||||||
// Expands to a switch on target->derived_type() that:
|
|
||||||
// 1. Allocates the correct Python object type (Frame, Caption, Sprite, Grid)
|
|
||||||
// 2. Sets the shared_ptr data member
|
|
||||||
// 3. Returns the PyObject*
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Collection Patterns
|
|
||||||
- `UICollection` wraps `std::vector<std::shared_ptr<UIDrawable>>`
|
|
||||||
- `UIEntityCollection` wraps `std::list<std::shared_ptr<UIEntity>>`
|
|
||||||
- Different containers require different iteration code (vector vs list)
|
|
||||||
|
|
||||||
#### Python Object Creation Patterns
|
|
||||||
```cpp
|
|
||||||
// Pattern 1: Using tp_alloc (most common)
|
|
||||||
auto o = (PyUIFrameObject*)type->tp_alloc(type, 0);
|
|
||||||
o->data = std::make_shared<UIFrame>();
|
|
||||||
|
|
||||||
// Pattern 2: Getting type from module
|
|
||||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
|
||||||
auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
|
|
||||||
|
|
||||||
// Pattern 3: Direct shared_ptr assignment
|
|
||||||
iterObj->data = self->data; // Shares the C++ object
|
|
||||||
```
|
|
||||||
|
|
||||||
### Working Directory Structure
|
|
||||||
```
|
|
||||||
build/
|
|
||||||
├── mcrogueface # The executable
|
|
||||||
├── scripts/
|
|
||||||
│ └── game.py # Auto-loaded Python script
|
|
||||||
├── assets/ # Copied from source during build
|
|
||||||
└── lib/ # Python libraries (copied from __lib/)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Quick Iteration Tips
|
|
||||||
- Keep a test script ready for quick experiments
|
|
||||||
- Use `timeout` to auto-kill hanging processes
|
|
||||||
- The game expects a window manager; use Xvfb for headless testing
|
|
||||||
- Python errors go to stderr, game output to stdout
|
|
||||||
- Segfaults usually mean Python type initialization issues
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
- The project uses SFML for graphics/audio and libtcod for roguelike utilities
|
|
||||||
- Python scripts are loaded at runtime from the `scripts/` directory
|
|
||||||
- Asset loading expects specific paths relative to the executable
|
|
||||||
- The game was created for 7DRL 2025 as "Crypt of Sokoban"
|
|
||||||
- Iterator implementations require careful handling of C++/Python boundaries
|
|
||||||
|
|
||||||
## Testing Guidelines
|
|
||||||
|
|
||||||
### Test-Driven Development
|
|
||||||
- **Always write tests first**: Create automation tests in `./tests/` for all bugs and new features
|
|
||||||
- **Practice TDD**: Write tests that fail to demonstrate the issue, then pass after the fix is applied
|
|
||||||
- **Close the loop**: Reproduce issue → change code → recompile → verify behavior change
|
|
||||||
|
|
||||||
### Two Types of Tests
|
|
||||||
|
|
||||||
#### 1. Direct Execution Tests (No Game Loop)
|
|
||||||
For tests that only need class initialization or direct code execution:
|
|
||||||
```python
|
|
||||||
# These tests can treat McRogueFace like a Python interpreter
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Test code here
|
|
||||||
result = mcrfpy.some_function()
|
|
||||||
assert result == expected_value
|
|
||||||
print("PASS" if condition else "FAIL")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Game Loop Tests (Timer-Based)
|
|
||||||
For tests requiring rendering, game state, or elapsed time:
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
from mcrfpy import automation
|
|
||||||
import sys
|
|
||||||
|
|
||||||
def run_test(runtime):
|
|
||||||
"""Timer callback - runs after game loop starts"""
|
|
||||||
# Now rendering is active, screenshots will work
|
|
||||||
automation.screenshot("test_result.png")
|
|
||||||
|
|
||||||
# Run your tests here
|
|
||||||
automation.click(100, 100)
|
|
||||||
|
|
||||||
# Always exit at the end
|
|
||||||
print("PASS" if success else "FAIL")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Set up the test scene
|
|
||||||
mcrfpy.createScene("test")
|
|
||||||
# ... add UI elements ...
|
|
||||||
|
|
||||||
# Schedule test to run after game loop starts
|
|
||||||
mcrfpy.setTimer("test", run_test, 100) # 0.1 seconds
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Testing Principles
|
|
||||||
- **Timer callbacks are essential**: Screenshots and UI interactions only work after the render loop starts
|
|
||||||
- **Use automation API**: Always create and examine screenshots when visual feedback is required
|
|
||||||
- **Exit properly**: Call `sys.exit()` at the end of timer-based tests to prevent hanging
|
|
||||||
- **Headless mode**: Use `--exec` flag for automated testing: `./mcrogueface --headless --exec tests/my_test.py`
|
|
||||||
|
|
||||||
### Example Test Pattern
|
|
||||||
```bash
|
|
||||||
# Run a test that requires game loop
|
|
||||||
./build/mcrogueface --headless --exec tests/issue_78_middle_click_test.py
|
|
||||||
|
|
||||||
# The test will:
|
|
||||||
# 1. Set up the scene during script execution
|
|
||||||
# 2. Register a timer callback
|
|
||||||
# 3. Game loop starts
|
|
||||||
# 4. Timer fires after 100ms
|
|
||||||
# 5. Test runs with full rendering available
|
|
||||||
# 6. Test takes screenshots and validates behavior
|
|
||||||
# 7. Test calls sys.exit() to terminate
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Best Practices
|
|
||||||
|
|
||||||
### Testing and Deployment
|
|
||||||
- **Keep tests in ./tests, not ./build/tests** - ./build gets shipped, and tests shouldn't be included
|
|
||||||
|
|
||||||
## Documentation Guidelines
|
|
||||||
|
|
||||||
### Documentation Macro System
|
|
||||||
|
|
||||||
**As of 2025-10-30, McRogueFace uses a macro-based documentation system** (`src/McRFPy_Doc.h`) that ensures consistent, complete docstrings across all Python bindings.
|
|
||||||
|
|
||||||
#### Include the Header
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#include "McRFPy_Doc.h"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Documenting Methods
|
|
||||||
|
|
||||||
For methods in PyMethodDef arrays, use `MCRF_METHOD`:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
{"method_name", (PyCFunction)Class::method, METH_VARARGS,
|
|
||||||
MCRF_METHOD(ClassName, method_name,
|
|
||||||
MCRF_SIG("(arg1: type, arg2: type)", "return_type"),
|
|
||||||
MCRF_DESC("Brief description of what the method does."),
|
|
||||||
MCRF_ARGS_START
|
|
||||||
MCRF_ARG("arg1", "Description of first argument")
|
|
||||||
MCRF_ARG("arg2", "Description of second argument")
|
|
||||||
MCRF_RETURNS("Description of return value")
|
|
||||||
MCRF_RAISES("ValueError", "Condition that raises this exception")
|
|
||||||
MCRF_NOTE("Important notes or caveats")
|
|
||||||
MCRF_LINK("docs/guide.md", "Related Documentation")
|
|
||||||
)},
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Documenting Properties
|
|
||||||
|
|
||||||
For properties in PyGetSetDef arrays, use `MCRF_PROPERTY`:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
{"property_name", (getter)getter_func, (setter)setter_func,
|
|
||||||
MCRF_PROPERTY(property_name,
|
|
||||||
"Brief description of the property. "
|
|
||||||
"Additional details about valid values, side effects, etc."
|
|
||||||
), NULL},
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Available Macros
|
|
||||||
|
|
||||||
- `MCRF_SIG(params, ret)` - Method signature
|
|
||||||
- `MCRF_DESC(text)` - Description paragraph
|
|
||||||
- `MCRF_ARGS_START` - Begin arguments section
|
|
||||||
- `MCRF_ARG(name, desc)` - Individual argument
|
|
||||||
- `MCRF_RETURNS(text)` - Return value description
|
|
||||||
- `MCRF_RAISES(exception, condition)` - Exception documentation
|
|
||||||
- `MCRF_NOTE(text)` - Important notes
|
|
||||||
- `MCRF_LINK(path, text)` - Reference to external documentation
|
|
||||||
|
|
||||||
#### Documentation Prose Guidelines
|
|
||||||
|
|
||||||
**Keep C++ docstrings concise** (1-2 sentences per section). For complex topics, use `MCRF_LINK` to reference external guides:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
MCRF_LINK("docs/animation-guide.md", "Animation System Tutorial")
|
|
||||||
```
|
|
||||||
|
|
||||||
**External documentation** (in `docs/`) can be verbose with examples, tutorials, and design rationale.
|
|
||||||
|
|
||||||
### Regenerating Documentation
|
|
||||||
|
|
||||||
After modifying C++ inline documentation with MCRF_* macros:
|
|
||||||
|
|
||||||
1. **Rebuild the project**: `make -j$(nproc)`
|
|
||||||
|
|
||||||
2. **Generate all documentation** (recommended - single command):
|
|
||||||
```bash
|
|
||||||
./tools/generate_all_docs.sh
|
|
||||||
```
|
|
||||||
This creates:
|
|
||||||
- `docs/api_reference_dynamic.html` - HTML API reference
|
|
||||||
- `docs/API_REFERENCE_DYNAMIC.md` - Markdown API reference
|
|
||||||
- `docs/mcrfpy.3` - Unix man page (section 3)
|
|
||||||
- `stubs/mcrfpy.pyi` - Type stubs for IDE support
|
|
||||||
|
|
||||||
3. **Or generate individually**:
|
|
||||||
```bash
|
|
||||||
# API docs (HTML + Markdown)
|
|
||||||
./build/mcrogueface --headless --exec tools/generate_dynamic_docs.py
|
|
||||||
|
|
||||||
# Type stubs (manually-maintained with @overload support)
|
|
||||||
./build/mcrogueface --headless --exec tools/generate_stubs_v2.py
|
|
||||||
|
|
||||||
# Man page (requires pandoc)
|
|
||||||
./tools/generate_man_page.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**System Requirements:**
|
|
||||||
- `pandoc` must be installed for man page generation: `sudo apt-get install pandoc`
|
|
||||||
|
|
||||||
### Important Notes
|
|
||||||
|
|
||||||
- **Single source of truth**: Documentation lives in C++ source files via MCRF_* macros
|
|
||||||
- **McRogueFace as Python interpreter**: Documentation scripts MUST be run using McRogueFace itself, not system Python
|
|
||||||
- **Use --headless --exec**: For non-interactive documentation generation
|
|
||||||
- **Link transformation**: `MCRF_LINK` references are transformed to appropriate format (HTML, Markdown, etc.)
|
|
||||||
- **No manual dictionaries**: The old hardcoded documentation system has been removed
|
|
||||||
|
|
||||||
### Documentation Pipeline Architecture
|
|
||||||
|
|
||||||
1. **C++ Source** → MCRF_* macros in PyMethodDef/PyGetSetDef arrays
|
|
||||||
2. **Compilation** → Macros expand to complete docstrings embedded in module
|
|
||||||
3. **Introspection** → Scripts use `dir()`, `getattr()`, `__doc__` to extract
|
|
||||||
4. **Generation** → HTML/Markdown/Stub files created with transformed links
|
|
||||||
5. **No drift**: Impossible for docs and code to disagree - they're the same file!
|
|
||||||
|
|
||||||
The macro system ensures complete, consistent documentation across all Python bindings.
|
|
||||||
- Close issues automatically in gitea by adding to the commit message "closes #X", where X is the issue number. This associates the issue closure with the specific commit, so granular commits are preferred. You should only use the MCP tool to close issues directly when discovering that the issue is already complete; when committing changes, always such "closes" (or the opposite, "reopens") references to related issues. If on a feature branch, the issue will be referenced by the commit, and when merged to master, the issue will be actually closed (or reopened).
|
|
||||||
|
|
@ -17,11 +17,20 @@ include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
|
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
|
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
|
||||||
|
|
||||||
|
# TODO: Move this into the WIN32 if block below (as 'else')
|
||||||
|
#include_directories(${CMAKE_SOURCE_DIR}/platform/linux)
|
||||||
|
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||||
|
|
||||||
# Collect all the source files
|
# Collect all the source files
|
||||||
file(GLOB_RECURSE SOURCES "src/*.cpp")
|
file(GLOB_RECURSE SOURCES "src/*.cpp")
|
||||||
|
|
||||||
# Create a list of libraries to link against
|
# Create a list of libraries to link against
|
||||||
set(LINK_LIBS
|
set(LINK_LIBS
|
||||||
|
m
|
||||||
|
dl
|
||||||
|
util
|
||||||
|
pthread
|
||||||
|
python3.12
|
||||||
sfml-graphics
|
sfml-graphics
|
||||||
sfml-window
|
sfml-window
|
||||||
sfml-system
|
sfml-system
|
||||||
|
|
@ -30,33 +39,19 @@ set(LINK_LIBS
|
||||||
|
|
||||||
# On Windows, add any additional libs and include directories
|
# On Windows, add any additional libs and include directories
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
# Windows-specific Python library name (no dots)
|
|
||||||
list(APPEND LINK_LIBS python312)
|
|
||||||
# Add the necessary Windows-specific libraries and include directories
|
# Add the necessary Windows-specific libraries and include directories
|
||||||
# include_directories(path_to_additional_includes)
|
# include_directories(path_to_additional_includes)
|
||||||
# link_directories(path_to_additional_libs)
|
# link_directories(path_to_additional_libs)
|
||||||
# list(APPEND LINK_LIBS additional_windows_libs)
|
# list(APPEND LINK_LIBS additional_windows_libs)
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
|
||||||
else()
|
|
||||||
# Unix/Linux specific libraries
|
|
||||||
list(APPEND LINK_LIBS python3.12 m dl util pthread)
|
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Add the directory where the linker should look for the libraries
|
# Add the directory where the linker should look for the libraries
|
||||||
#link_directories(${CMAKE_SOURCE_DIR}/deps_linux)
|
#link_directories(${CMAKE_SOURCE_DIR}/deps_linux)
|
||||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
link_directories(${CMAKE_SOURCE_DIR}/lib)
|
||||||
|
|
||||||
# Define the executable target before linking libraries
|
# Define the executable target before linking libraries
|
||||||
add_executable(mcrogueface ${SOURCES})
|
add_executable(mcrogueface ${SOURCES})
|
||||||
|
|
||||||
# On Windows, set subsystem to WINDOWS to hide console
|
|
||||||
if(WIN32)
|
|
||||||
set_target_properties(mcrogueface PROPERTIES
|
|
||||||
WIN32_EXECUTABLE TRUE
|
|
||||||
LINK_FLAGS "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Now the linker will find the libraries in the specified directory
|
# Now the linker will find the libraries in the specified directory
|
||||||
target_link_libraries(mcrogueface ${LINK_LIBS})
|
target_link_libraries(mcrogueface ${LINK_LIBS})
|
||||||
|
|
||||||
|
|
@ -73,28 +68,9 @@ add_custom_command(TARGET mcrogueface POST_BUILD
|
||||||
# Copy Python standard library to build directory
|
# Copy Python standard library to build directory
|
||||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib)
|
${CMAKE_SOURCE_DIR}/lib $<TARGET_FILE_DIR:mcrogueface>/lib)
|
||||||
|
|
||||||
# On Windows, copy DLLs to executable directory
|
# rpath for including shared libraries
|
||||||
if(WIN32)
|
set_target_properties(mcrogueface PROPERTIES
|
||||||
# Copy all DLL files from lib to the executable directory
|
INSTALL_RPATH "./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")
|
|
||||||
|
|
||||||
# Alternative: Copy specific DLLs if you want more control
|
|
||||||
# file(GLOB DLLS "${CMAKE_SOURCE_DIR}/__lib/*.dll")
|
|
||||||
# foreach(DLL ${DLLS})
|
|
||||||
# add_custom_command(TARGET mcrogueface POST_BUILD
|
|
||||||
# COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
|
||||||
# ${DLL} $<TARGET_FILE_DIR:mcrogueface>)
|
|
||||||
# endforeach()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# rpath for including shared libraries (Linux/Unix only)
|
|
||||||
if(NOT WIN32)
|
|
||||||
set_target_properties(mcrogueface PROPERTIES
|
|
||||||
INSTALL_RPATH "$ORIGIN/./lib")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
|
|
|
||||||
54
GNUmakefile
54
GNUmakefile
|
|
@ -1,54 +0,0 @@
|
||||||
# Convenience Makefile wrapper for McRogueFace
|
|
||||||
# This delegates to CMake build in the build directory
|
|
||||||
|
|
||||||
.PHONY: all build clean run test dist help
|
|
||||||
|
|
||||||
# Default target
|
|
||||||
all: build
|
|
||||||
|
|
||||||
# Build the project
|
|
||||||
build:
|
|
||||||
@./build.sh
|
|
||||||
|
|
||||||
# Clean build artifacts
|
|
||||||
clean:
|
|
||||||
@./clean.sh
|
|
||||||
|
|
||||||
# Run the game
|
|
||||||
run: build
|
|
||||||
@cd build && ./mcrogueface
|
|
||||||
|
|
||||||
# Run in Python mode
|
|
||||||
python: build
|
|
||||||
@cd build && ./mcrogueface -i
|
|
||||||
|
|
||||||
# Test basic functionality
|
|
||||||
test: build
|
|
||||||
@echo "Testing McRogueFace..."
|
|
||||||
@cd build && ./mcrogueface -V
|
|
||||||
@cd build && ./mcrogueface -c "print('Test passed')"
|
|
||||||
@cd build && ./mcrogueface --headless -c "import mcrfpy; print('mcrfpy imported successfully')"
|
|
||||||
|
|
||||||
# Create distribution archive
|
|
||||||
dist: build
|
|
||||||
@echo "Creating distribution archive..."
|
|
||||||
@cd build && zip -r ../McRogueFace-$$(date +%Y%m%d).zip . -x "*.o" "CMakeFiles/*" "Makefile" "*.cmake"
|
|
||||||
@echo "Distribution archive created: McRogueFace-$$(date +%Y%m%d).zip"
|
|
||||||
|
|
||||||
# Show help
|
|
||||||
help:
|
|
||||||
@echo "McRogueFace Build System"
|
|
||||||
@echo "======================="
|
|
||||||
@echo ""
|
|
||||||
@echo "Available targets:"
|
|
||||||
@echo " make - Build the project (default)"
|
|
||||||
@echo " make build - Build the project"
|
|
||||||
@echo " make clean - Remove all build artifacts"
|
|
||||||
@echo " make run - Build and run the game"
|
|
||||||
@echo " make python - Build and run in Python interactive mode"
|
|
||||||
@echo " make test - Run basic tests"
|
|
||||||
@echo " make dist - Create distribution archive"
|
|
||||||
@echo " make help - Show this help message"
|
|
||||||
@echo ""
|
|
||||||
@echo "Build output goes to: ./build/"
|
|
||||||
@echo "Distribution archives are created in project root"
|
|
||||||
161
README.md
161
README.md
|
|
@ -1,145 +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*
|
*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
|
* C++ first, Python close behind.
|
||||||
* Animate sprites with multiple frames. Smooth transitions for positions, sizes, zoom, and camera
|
* Entity-Component system based on David Churchill's Memorial University COMP4300 course lectures available on Youtube.
|
||||||
* Simple GUI element system allows keyboard and mouse input, composition
|
* Graphics, particles and shaders provided by SFML.
|
||||||
* No compilation or installation necessary. The runtime is a full Python environment; "Zip And Ship"
|
* 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**:
|
* ✅ Initial Commit
|
||||||
|
* ✅ Integrate scene, action, entity, component system from COMP4300 engine
|
||||||
- The entire McRogueFace visual framework:
|
* ✅ Windows / Visual Studio project
|
||||||
- **Sprite**: an image file or one sprite from a shared sprite sheet
|
* ✅ Draw Sprites
|
||||||
- **Caption**: load a font, display text
|
* ✅ Play Sounds
|
||||||
- **Frame**: A rectangle; put other things on it to move or manage GUIs as modules
|
* ✅ Draw UI, spawn entity from Python code
|
||||||
- **Grid**: A 2D array of tiles with zoom + position control
|
* ❌ Python AI for entities (NPCs on set paths, enemies towards player)
|
||||||
- **Entity**: Lives on a Grid, displays a sprite, and can have a perspective or move along a path
|
* ✅ Walking / Collision
|
||||||
- **Animation**: Change any property on any of the above over time
|
* ❌ "Boards" (stairs / doors / walk off edge of screen)
|
||||||
|
* ❌ Cutscenes - interrupt normal controls, text scroll, character portraits
|
||||||
```bash
|
* ❌ Mouse integration - tooltips, zoom, click to select targets, cursors
|
||||||
# Clone and build
|
|
||||||
git clone <wherever you found this repo>
|
|
||||||
cd McRogueFace
|
|
||||||
make
|
|
||||||
|
|
||||||
# Run the example game
|
|
||||||
cd build
|
|
||||||
./mcrogueface
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example: Creating a Simple Scene
|
|
||||||
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Create a new scene
|
|
||||||
mcrfpy.createScene("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
|
|
||||||
mcrfpy.sceneUI("intro").append(caption)
|
|
||||||
|
|
||||||
# Switch to the scene
|
|
||||||
mcrfpy.setScene("intro")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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 46 open issues organized by system
|
|
||||||
|
|
||||||
### 📖 Development Guides
|
|
||||||
|
|
||||||
In the repository root:
|
|
||||||
|
|
||||||
- **[CLAUDE.md](CLAUDE.md)** - Build instructions, testing guidelines, common tasks
|
|
||||||
- **[ROADMAP.md](ROADMAP.md)** - Strategic vision and development phases
|
|
||||||
- **[roguelike_tutorial/](roguelike_tutorial/)** - Complete roguelike tutorial implementations
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
|
||||||
223
ROADMAP.md
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).*
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 181 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 674 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 MiB |
54
build.sh
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,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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
#ifndef __PLATFORM
|
|
||||||
#define __PLATFORM
|
|
||||||
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
|
|
||||||
std::wstring executable_path()
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
wchar_t buffer[MAX_PATH];
|
|
||||||
GetModuleFileName(NULL, buffer, MAX_PATH);
|
|
||||||
std::wstring exec_path = buffer;
|
|
||||||
*/
|
|
||||||
auto exec_path = std::filesystem::canonical("/proc/self/exe").parent_path();
|
|
||||||
return exec_path.wstring();
|
|
||||||
//size_t path_index = exec_path.find_last_of('/');
|
|
||||||
//return exec_path.substr(0, path_index);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
std::wstring executable_filename()
|
|
||||||
{
|
|
||||||
auto exec_path = std::filesystem::canonical("/proc/self/exe");
|
|
||||||
return exec_path.wstring();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::wstring working_path()
|
|
||||||
{
|
|
||||||
auto cwd = std::filesystem::current_path();
|
|
||||||
return cwd.wstring();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string narrow_string(std::wstring convertme)
|
|
||||||
{
|
|
||||||
//setup converter
|
|
||||||
using convert_type = std::codecvt_utf8<wchar_t>;
|
|
||||||
std::wstring_convert<convert_type, wchar_t> converter;
|
|
||||||
|
|
||||||
//use converter (.to_bytes: wstr->str, .from_bytes: str->wstr)
|
|
||||||
return converter.to_bytes(convertme);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
#ifndef __PLATFORM
|
|
||||||
#define __PLATFORM
|
|
||||||
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 0
|
|
||||||
#include <Windows.h>
|
|
||||||
|
|
||||||
std::wstring executable_path()
|
|
||||||
{
|
|
||||||
wchar_t buffer[MAX_PATH];
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::wstring executable_filename()
|
|
||||||
{
|
|
||||||
wchar_t buffer[MAX_PATH];
|
|
||||||
GetModuleFileName(NULL, buffer, MAX_PATH);
|
|
||||||
std::wstring exec_path = buffer;
|
|
||||||
return exec_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::wstring working_path()
|
|
||||||
{
|
|
||||||
auto cwd = std::filesystem::current_path();
|
|
||||||
return cwd.wstring();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string narrow_string(std::wstring convertme)
|
|
||||||
{
|
|
||||||
//setup converter
|
|
||||||
using convert_type = std::codecvt_utf8<wchar_t>;
|
|
||||||
std::wstring_convert<convert_type, wchar_t> converter;
|
|
||||||
|
|
||||||
//use converter (.to_bytes: wstr->str, .from_bytes: str->wstr)
|
|
||||||
return converter.to_bytes(convertme);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1070
docs/mcrfpy.3
1070
docs/mcrfpy.3
File diff suppressed because it is too large
Load Diff
|
|
@ -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: ...
|
|
||||||
Binary file not shown.
|
|
@ -1,393 +0,0 @@
|
||||||
# McRogueFace Tutorial Parts 6-8: Implementation Plan
|
|
||||||
|
|
||||||
**Date**: Monday, July 28, 2025
|
|
||||||
**Target Delivery**: Tuesday, July 29, 2025
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
This document outlines the implementation plan for Parts 6-8 of the McRogueFace roguelike tutorial, adapting the libtcod Python tutorial to McRogueFace's architecture. The key discovery is that Python classes can successfully inherit from `mcrfpy.Entity` and store custom attributes, enabling a clean, Pythonic implementation.
|
|
||||||
|
|
||||||
## Key Architectural Insights
|
|
||||||
|
|
||||||
### Entity Inheritance Works!
|
|
||||||
```python
|
|
||||||
class GameEntity(mcrfpy.Entity):
|
|
||||||
def __init__(self, x, y, **kwargs):
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
# Custom attributes work perfectly!
|
|
||||||
self.hp = 10
|
|
||||||
self.inventory = []
|
|
||||||
self.any_attribute = "works"
|
|
||||||
```
|
|
||||||
|
|
||||||
This completely changes our approach from wrapper patterns to direct inheritance.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 6: Doing (and Taking) Some Damage
|
|
||||||
|
|
||||||
### Overview
|
|
||||||
Implement a combat system with HP tracking, damage calculation, and death mechanics using entity inheritance.
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
#### 1. CombatEntity Base Class
|
|
||||||
```python
|
|
||||||
class CombatEntity(mcrfpy.Entity):
|
|
||||||
"""Base class for entities that can fight and take damage"""
|
|
||||||
def __init__(self, x, y, hp=10, defense=0, power=1, **kwargs):
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
# Combat stats as direct attributes
|
|
||||||
self.hp = hp
|
|
||||||
self.max_hp = hp
|
|
||||||
self.defense = defense
|
|
||||||
self.power = power
|
|
||||||
self.is_alive = True
|
|
||||||
self.blocks_movement = True
|
|
||||||
|
|
||||||
def calculate_damage(self, attacker):
|
|
||||||
"""Simple damage formula: power - defense"""
|
|
||||||
return max(0, attacker.power - self.defense)
|
|
||||||
|
|
||||||
def take_damage(self, damage, attacker=None):
|
|
||||||
"""Apply damage and handle death"""
|
|
||||||
self.hp = max(0, self.hp - damage)
|
|
||||||
|
|
||||||
if self.hp == 0 and self.is_alive:
|
|
||||||
self.is_alive = False
|
|
||||||
self.on_death(attacker)
|
|
||||||
|
|
||||||
def on_death(self, killer=None):
|
|
||||||
"""Handle death - override in subclasses"""
|
|
||||||
self.sprite_index = self.sprite_index + 180 # Corpse offset
|
|
||||||
self.blocks_movement = False
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Entity Types
|
|
||||||
```python
|
|
||||||
class PlayerEntity(CombatEntity):
|
|
||||||
"""Player: HP=30, Defense=2, Power=5"""
|
|
||||||
def __init__(self, x, y, **kwargs):
|
|
||||||
kwargs['sprite_index'] = 64 # Hero sprite
|
|
||||||
super().__init__(x=x, y=y, hp=30, defense=2, power=5, **kwargs)
|
|
||||||
self.entity_type = "player"
|
|
||||||
|
|
||||||
class OrcEntity(CombatEntity):
|
|
||||||
"""Orc: HP=10, Defense=0, Power=3"""
|
|
||||||
def __init__(self, x, y, **kwargs):
|
|
||||||
kwargs['sprite_index'] = 65 # Orc sprite
|
|
||||||
super().__init__(x=x, y=y, hp=10, defense=0, power=3, **kwargs)
|
|
||||||
self.entity_type = "orc"
|
|
||||||
|
|
||||||
class TrollEntity(CombatEntity):
|
|
||||||
"""Troll: HP=16, Defense=1, Power=4"""
|
|
||||||
def __init__(self, x, y, **kwargs):
|
|
||||||
kwargs['sprite_index'] = 66 # Troll sprite
|
|
||||||
super().__init__(x=x, y=y, hp=16, defense=1, power=4, **kwargs)
|
|
||||||
self.entity_type = "troll"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Combat Integration
|
|
||||||
- Extend `on_bump()` from Part 5 to include combat
|
|
||||||
- Add attack animations (quick bump toward target)
|
|
||||||
- Console messages initially, UI messages in Part 7
|
|
||||||
- Death changes sprite and removes blocking
|
|
||||||
|
|
||||||
### Key Differences from Original Tutorial
|
|
||||||
- No Fighter component - stats are direct attributes
|
|
||||||
- No AI component - behavior in entity methods
|
|
||||||
- Integrated animations for visual feedback
|
|
||||||
- Simpler architecture overall
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 7: Creating the Interface
|
|
||||||
|
|
||||||
### Overview
|
|
||||||
Add visual UI elements including health bars, message logs, and colored feedback for combat events.
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
#### 1. Health Bar
|
|
||||||
```python
|
|
||||||
class HealthBar:
|
|
||||||
"""Health bar that reads entity HP directly"""
|
|
||||||
def __init__(self, entity, pos=(10, 740), size=(200, 20)):
|
|
||||||
self.entity = entity # Direct reference!
|
|
||||||
|
|
||||||
# Background (dark red)
|
|
||||||
self.bg = mcrfpy.Frame(pos=pos, size=size)
|
|
||||||
self.bg.fill_color = mcrfpy.Color(64, 16, 16)
|
|
||||||
|
|
||||||
# Foreground (green)
|
|
||||||
self.fg = mcrfpy.Frame(pos=pos, size=size)
|
|
||||||
self.fg.fill_color = mcrfpy.Color(0, 96, 0)
|
|
||||||
|
|
||||||
# Text overlay
|
|
||||||
self.text = mcrfpy.Caption(
|
|
||||||
pos=(pos[0] + 5, pos[1] + 2),
|
|
||||||
text=f"HP: {entity.hp}/{entity.max_hp}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""Update based on entity's current HP"""
|
|
||||||
ratio = self.entity.hp / self.entity.max_hp
|
|
||||||
self.fg.w = int(self.bg.w * ratio)
|
|
||||||
self.text.text = f"HP: {self.entity.hp}/{self.entity.max_hp}"
|
|
||||||
|
|
||||||
# Color changes at low health
|
|
||||||
if ratio < 0.25:
|
|
||||||
self.fg.fill_color = mcrfpy.Color(196, 16, 16) # Red
|
|
||||||
elif ratio < 0.5:
|
|
||||||
self.fg.fill_color = mcrfpy.Color(196, 196, 16) # Yellow
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Message Log
|
|
||||||
```python
|
|
||||||
class MessageLog:
|
|
||||||
"""Scrolling message log for combat feedback"""
|
|
||||||
def __init__(self, pos=(10, 600), size=(400, 120), max_messages=6):
|
|
||||||
self.frame = mcrfpy.Frame(pos=pos, size=size)
|
|
||||||
self.messages = [] # List of (text, color) tuples
|
|
||||||
self.captions = [] # Pre-allocated Caption pool
|
|
||||||
|
|
||||||
def add_message(self, text, color=None):
|
|
||||||
"""Add message with optional color"""
|
|
||||||
# Handle duplicate detection (x2, x3, etc.)
|
|
||||||
# Update caption display
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Color System
|
|
||||||
```python
|
|
||||||
class Colors:
|
|
||||||
# Combat colors
|
|
||||||
PLAYER_ATTACK = mcrfpy.Color(224, 224, 224)
|
|
||||||
ENEMY_ATTACK = mcrfpy.Color(255, 192, 192)
|
|
||||||
PLAYER_DEATH = mcrfpy.Color(255, 48, 48)
|
|
||||||
ENEMY_DEATH = mcrfpy.Color(255, 160, 48)
|
|
||||||
HEALTH_RECOVERED = mcrfpy.Color(0, 255, 0)
|
|
||||||
```
|
|
||||||
|
|
||||||
### UI Layout
|
|
||||||
- Health bar at bottom of screen
|
|
||||||
- Message log above health bar
|
|
||||||
- Direct binding to entity attributes
|
|
||||||
- Real-time updates during gameplay
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 8: Items and Inventory
|
|
||||||
|
|
||||||
### Overview
|
|
||||||
Implement items as entities, inventory management, and a hotbar-style UI for item usage.
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
|
|
||||||
#### 1. Item Entities
|
|
||||||
```python
|
|
||||||
class ItemEntity(mcrfpy.Entity):
|
|
||||||
"""Base class for pickupable items"""
|
|
||||||
def __init__(self, x, y, name, sprite, **kwargs):
|
|
||||||
kwargs['sprite_index'] = sprite
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
self.item_name = name
|
|
||||||
self.blocks_movement = False
|
|
||||||
self.item_type = "generic"
|
|
||||||
|
|
||||||
class HealingPotion(ItemEntity):
|
|
||||||
"""Consumable healing item"""
|
|
||||||
def __init__(self, x, y, healing_amount=4):
|
|
||||||
super().__init__(x, y, "Healing Potion", sprite=33)
|
|
||||||
self.healing_amount = healing_amount
|
|
||||||
self.item_type = "consumable"
|
|
||||||
|
|
||||||
def use(self, user):
|
|
||||||
"""Use the potion - returns (success, message)"""
|
|
||||||
if hasattr(user, 'hp'):
|
|
||||||
healed = min(self.healing_amount, user.max_hp - user.hp)
|
|
||||||
if healed > 0:
|
|
||||||
user.hp += healed
|
|
||||||
return True, f"You heal {healed} HP!"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Inventory System
|
|
||||||
```python
|
|
||||||
class InventoryMixin:
|
|
||||||
"""Mixin for entities with inventory"""
|
|
||||||
def __init__(self, *args, capacity=10, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.inventory = []
|
|
||||||
self.inventory_capacity = capacity
|
|
||||||
|
|
||||||
def pickup_item(self, item):
|
|
||||||
"""Pick up an item entity"""
|
|
||||||
if len(self.inventory) >= self.inventory_capacity:
|
|
||||||
return False, "Inventory full!"
|
|
||||||
self.inventory.append(item)
|
|
||||||
item.die() # Remove from grid
|
|
||||||
return True, f"Picked up {item.item_name}."
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Inventory UI
|
|
||||||
```python
|
|
||||||
class InventoryDisplay:
|
|
||||||
"""Hotbar-style inventory display"""
|
|
||||||
def __init__(self, entity, pos=(200, 700), slots=10):
|
|
||||||
# Create slot frames and sprites
|
|
||||||
# Number keys 1-9, 0 for slots
|
|
||||||
# Highlight selected slot
|
|
||||||
# Update based on entity.inventory
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Features
|
|
||||||
- Items exist as entities on the grid
|
|
||||||
- Direct inventory attribute on player
|
|
||||||
- Hotkey-based usage (1-9, 0)
|
|
||||||
- Visual hotbar display
|
|
||||||
- Item effects (healing, future: damage boost, etc.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Timeline
|
|
||||||
|
|
||||||
### Tuesday Morning (Priority 1: Core Systems)
|
|
||||||
1. **8:00-9:30**: Implement CombatEntity and entity types
|
|
||||||
2. **9:30-10:30**: Add combat to bump interactions
|
|
||||||
3. **10:30-11:30**: Basic health display (text or simple bar)
|
|
||||||
4. **11:30-12:00**: ItemEntity and pickup system
|
|
||||||
|
|
||||||
### Tuesday Afternoon (Priority 2: Integration)
|
|
||||||
1. **1:00-2:00**: Message log implementation
|
|
||||||
2. **2:00-3:00**: Full health bar with colors
|
|
||||||
3. **3:00-4:00**: Inventory UI (hotbar)
|
|
||||||
4. **4:00-5:00**: Testing and bug fixes
|
|
||||||
|
|
||||||
### Tuesday Evening (Priority 3: Polish)
|
|
||||||
1. **5:00-6:00**: Combat animations and effects
|
|
||||||
2. **6:00-7:00**: Sound integration (use CoS splat sounds)
|
|
||||||
3. **7:00-8:00**: Additional item types
|
|
||||||
4. **8:00-9:00**: Documentation and cleanup
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### Automated Tests
|
|
||||||
```python
|
|
||||||
# tests/test_part6_combat.py
|
|
||||||
- Test damage calculation
|
|
||||||
- Test death mechanics
|
|
||||||
- Test combat messages
|
|
||||||
|
|
||||||
# tests/test_part7_ui.py
|
|
||||||
- Test health bar updates
|
|
||||||
- Test message log scrolling
|
|
||||||
- Test color system
|
|
||||||
|
|
||||||
# tests/test_part8_inventory.py
|
|
||||||
- Test item pickup/drop
|
|
||||||
- Test inventory capacity
|
|
||||||
- Test item usage
|
|
||||||
```
|
|
||||||
|
|
||||||
### Visual Tests
|
|
||||||
- Screenshot combat states
|
|
||||||
- Verify UI element positioning
|
|
||||||
- Check animation smoothness
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
```
|
|
||||||
roguelike_tutorial/
|
|
||||||
├── part_6.py # Combat implementation
|
|
||||||
├── part_7.py # UI enhancements
|
|
||||||
├── part_8.py # Inventory system
|
|
||||||
├── combat.py # Shared combat utilities
|
|
||||||
├── ui_components.py # Reusable UI classes
|
|
||||||
├── colors.py # Color definitions
|
|
||||||
└── items.py # Item definitions
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Risk Mitigation
|
|
||||||
|
|
||||||
### Potential Issues
|
|
||||||
1. **Performance**: Many UI updates per frame
|
|
||||||
- Solution: Update only on state changes
|
|
||||||
|
|
||||||
2. **Entity Collection Bugs**: Known segfault issues
|
|
||||||
- Solution: Use index-based access when needed
|
|
||||||
|
|
||||||
3. **Animation Timing**: Complex with turn-based combat
|
|
||||||
- Solution: Queue animations, process sequentially
|
|
||||||
|
|
||||||
### Fallback Options
|
|
||||||
1. Start with console messages, add UI later
|
|
||||||
2. Simple health numbers before bars
|
|
||||||
3. Basic inventory list before hotbar
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
### Part 6
|
|
||||||
- [x] Entities can have HP and take damage
|
|
||||||
- [x] Death changes sprite and walkability
|
|
||||||
- [x] Combat messages appear
|
|
||||||
- [x] Player can kill enemies
|
|
||||||
|
|
||||||
### Part 7
|
|
||||||
- [x] Health bar shows current/max HP
|
|
||||||
- [x] Messages appear in scrolling log
|
|
||||||
- [x] Colors differentiate message types
|
|
||||||
- [x] UI updates in real-time
|
|
||||||
|
|
||||||
### Part 8
|
|
||||||
- [x] Items can be picked up
|
|
||||||
- [x] Inventory has capacity limit
|
|
||||||
- [x] Items can be used/consumed
|
|
||||||
- [x] Hotbar shows inventory items
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes for Implementation
|
|
||||||
|
|
||||||
1. **Keep It Simple**: Start with minimum viable features
|
|
||||||
2. **Build Incrementally**: Test each component before integrating
|
|
||||||
3. **Use Part 5**: Leverage existing entity interaction system
|
|
||||||
4. **Document Well**: Clear comments for tutorial purposes
|
|
||||||
5. **Visual Feedback**: McRogueFace excels at animations - use them!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comparison with Original Tutorial
|
|
||||||
|
|
||||||
### What We Keep
|
|
||||||
- Same combat formula (power - defense)
|
|
||||||
- Same entity stats (Player, Orc, Troll)
|
|
||||||
- Same item types (healing potions to start)
|
|
||||||
- Same UI elements (health bar, message log)
|
|
||||||
|
|
||||||
### What's Different
|
|
||||||
- Direct inheritance instead of components
|
|
||||||
- Integrated animations and visual effects
|
|
||||||
- Hotbar inventory instead of menu
|
|
||||||
- Built-in sound support
|
|
||||||
- Cleaner architecture overall
|
|
||||||
|
|
||||||
### What's Better
|
|
||||||
- More Pythonic with real inheritance
|
|
||||||
- Better visual feedback
|
|
||||||
- Smoother animations
|
|
||||||
- Simpler to understand
|
|
||||||
- Leverages McRogueFace's strengths
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
This implementation plan leverages McRogueFace's support for Python entity inheritance to create a clean, intuitive tutorial series. By using direct attributes instead of components, we simplify the architecture while maintaining all the functionality of the original tutorial. The addition of animations, sound effects, and rich UI elements showcases McRogueFace's capabilities while keeping the code beginner-friendly.
|
|
||||||
|
|
||||||
The Tuesday delivery timeline is aggressive but achievable by focusing on core functionality first, then integration, then polish. The modular design allows for easy testing and incremental development.
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
# Simple TCOD Tutorial Part 1 - Drawing the player sprite and moving it around
|
|
||||||
|
|
||||||
This is Part 1 of the Simple TCOD Tutorial adapted for McRogueFace. It implements the sophisticated, refactored TCOD tutorial approach with professional architecture from day one.
|
|
||||||
|
|
||||||
## Running the Code
|
|
||||||
|
|
||||||
From your tutorial build directory (separate from the engine development build):
|
|
||||||
```bash
|
|
||||||
cd simple_tcod_tutorial/build
|
|
||||||
./mcrogueface scripts/main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: The `scripts` folder should be a symlink to your `simple_tcod_tutorial` directory.
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
### Package Structure
|
|
||||||
```
|
|
||||||
simple_tcod_tutorial/
|
|
||||||
├── main.py # Entry point - ties everything together
|
|
||||||
├── game/ # Game package with proper separation
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── entity.py # Entity class - all game objects
|
|
||||||
│ ├── engine.py # Engine class - game coordinator
|
|
||||||
│ ├── actions.py # Action classes - command pattern
|
|
||||||
│ └── input_handlers.py # Input handling - extensible system
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Concepts Demonstrated
|
|
||||||
|
|
||||||
1. **Entity-Centric Design**
|
|
||||||
- Everything in the game is an Entity
|
|
||||||
- Entities have position, appearance, and behavior
|
|
||||||
- Designed to scale to items, NPCs, and effects
|
|
||||||
|
|
||||||
2. **Action-Based Command Pattern**
|
|
||||||
- All player actions are Action objects
|
|
||||||
- Separates input from game logic
|
|
||||||
- Enables undo, replay, and AI using same system
|
|
||||||
|
|
||||||
3. **Professional Input Handling**
|
|
||||||
- BaseEventHandler for different input contexts
|
|
||||||
- Complete movement key support (arrows, numpad, vi, WASD)
|
|
||||||
- Ready for menus, targeting, and other modes
|
|
||||||
|
|
||||||
4. **Engine as Coordinator**
|
|
||||||
- Manages game state without becoming a god object
|
|
||||||
- Delegates to appropriate systems
|
|
||||||
- Clean boundaries between systems
|
|
||||||
|
|
||||||
5. **Type Safety**
|
|
||||||
- Full type annotations throughout
|
|
||||||
- Forward references with TYPE_CHECKING
|
|
||||||
- Modern Python best practices
|
|
||||||
|
|
||||||
## Differences from Vanilla McRogueFace Tutorial
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- Animation system (instant movement instead)
|
|
||||||
- Complex UI elements (focus on core mechanics)
|
|
||||||
- Real-time features (pure turn-based)
|
|
||||||
- Visual effects (camera following, smooth scrolling)
|
|
||||||
- Entity color property (sprites handle appearance)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Complete movement key support
|
|
||||||
- Professional architecture patterns
|
|
||||||
- Proper package structure
|
|
||||||
- Type annotations
|
|
||||||
- Action-based design
|
|
||||||
- Extensible handler system
|
|
||||||
- Proper exit handling (Escape/Q actually quits)
|
|
||||||
|
|
||||||
### Adapted
|
|
||||||
- Grid rendering with proper centering
|
|
||||||
- Simplified entity system (position + sprite ID)
|
|
||||||
- Using simple_tutorial.png sprite sheet (12 sprites)
|
|
||||||
- Floor tiles using ground sprites (indices 1 and 2)
|
|
||||||
- Direct sprite indices instead of character mapping
|
|
||||||
|
|
||||||
## Learning Objectives
|
|
||||||
|
|
||||||
Students completing Part 1 will understand:
|
|
||||||
- How to structure a game project professionally
|
|
||||||
- The value of entity-centric design
|
|
||||||
- Command pattern for game actions
|
|
||||||
- Input handling that scales to complex UIs
|
|
||||||
- Type-driven development in Python
|
|
||||||
- Architecture that grows without refactoring
|
|
||||||
|
|
||||||
## What's Next
|
|
||||||
|
|
||||||
Part 2 will add:
|
|
||||||
- The GameMap class for world representation
|
|
||||||
- Tile-based movement and collision
|
|
||||||
- Multiple entities in the world
|
|
||||||
- Basic terrain (walls and floors)
|
|
||||||
- Rendering order for entities
|
|
||||||
|
|
||||||
The architecture we've built in Part 1 makes these additions natural and painless, demonstrating the value of starting with good patterns.
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
# Simple TCOD Tutorial Part 2 - The generic Entity, the map, and walls
|
|
||||||
|
|
||||||
This is Part 2 of the Simple TCOD Tutorial adapted for McRogueFace. Building on Part 1's foundation, we now introduce proper world representation and collision detection.
|
|
||||||
|
|
||||||
## Running the Code
|
|
||||||
|
|
||||||
From your tutorial build directory:
|
|
||||||
```bash
|
|
||||||
cd simple_tcod_tutorial/build
|
|
||||||
./mcrogueface scripts/main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## New Architecture Components
|
|
||||||
|
|
||||||
### GameMap Class (`game/game_map.py`)
|
|
||||||
The GameMap inherits from `mcrfpy.Grid` and adds:
|
|
||||||
- **Tile Management**: Uses Grid's built-in point system with walkable property
|
|
||||||
- **Entity Container**: Manages entity lifecycle with `add_entity()` and `remove_entity()`
|
|
||||||
- **Spatial Queries**: `get_entities_at()`, `get_blocking_entity_at()`, `is_walkable()`
|
|
||||||
- **Direct Integration**: Leverages Grid's walkable and tilesprite properties
|
|
||||||
|
|
||||||
### Tiles System (`game/tiles.py`)
|
|
||||||
- **Simple Tile Types**: Using NamedTuple for clean tile definitions
|
|
||||||
- **Tile Types**: Floor (walkable) and Wall (blocks movement)
|
|
||||||
- **Grid Integration**: Maps directly to Grid point properties
|
|
||||||
- **Future-Ready**: Includes transparency for FOV system in Part 4
|
|
||||||
|
|
||||||
### Entity Placement System
|
|
||||||
- **Bidirectional References**: Entities know their map, maps track their entities
|
|
||||||
- **`place()` Method**: Handles all bookkeeping when entities move between maps
|
|
||||||
- **Lifecycle Management**: Automatic cleanup when entities leave maps
|
|
||||||
|
|
||||||
## Key Changes from Part 1
|
|
||||||
|
|
||||||
### Engine Updates
|
|
||||||
- Replaced direct grid management with GameMap
|
|
||||||
- Engine creates and configures the GameMap
|
|
||||||
- Player is placed using the new `place()` method
|
|
||||||
|
|
||||||
### Movement System
|
|
||||||
- MovementAction now checks `is_walkable()` before moving
|
|
||||||
- Collision detection for both walls and blocking entities
|
|
||||||
- Clean separation between validation and execution
|
|
||||||
|
|
||||||
### Visual Changes
|
|
||||||
- Walls rendered as trees (sprite index 3)
|
|
||||||
- Border of walls around the map edge
|
|
||||||
- Floor tiles still use alternating pattern
|
|
||||||
|
|
||||||
## Architectural Benefits
|
|
||||||
|
|
||||||
### McRogueFace Integration
|
|
||||||
- **No NumPy Dependency**: Uses Grid's native tile management
|
|
||||||
- **Direct Walkability**: Grid points have built-in walkable property
|
|
||||||
- **Unified System**: Visual and logical tile data in one place
|
|
||||||
|
|
||||||
### Separation of Concerns
|
|
||||||
- **GameMap**: Knows about tiles and spatial relationships
|
|
||||||
- **Engine**: Coordinates high-level game state
|
|
||||||
- **Entity**: Manages its own lifecycle through `place()`
|
|
||||||
- **Actions**: Validate their own preconditions
|
|
||||||
|
|
||||||
### Extensibility
|
|
||||||
- Easy to add new tile types
|
|
||||||
- Simple to implement different map generation
|
|
||||||
- Ready for FOV, pathfinding, and complex queries
|
|
||||||
- Entity system scales to items and NPCs
|
|
||||||
|
|
||||||
### Type Safety
|
|
||||||
- TYPE_CHECKING imports prevent circular dependencies
|
|
||||||
- Proper type hints throughout
|
|
||||||
- Forward references maintain clean architecture
|
|
||||||
|
|
||||||
## What's Next
|
|
||||||
|
|
||||||
Part 3 will add:
|
|
||||||
- Procedural dungeon generation
|
|
||||||
- Room and corridor creation
|
|
||||||
- Multiple entities in the world
|
|
||||||
- Foundation for enemy placement
|
|
||||||
|
|
||||||
The architecture established in Part 2 makes these additions straightforward, demonstrating the value of proper design from the beginning.
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
# Simple TCOD Tutorial Part 3 - Generating a dungeon
|
|
||||||
|
|
||||||
This is Part 3 of the Simple TCOD Tutorial adapted for McRogueFace. We now add procedural dungeon generation to create interesting, playable levels.
|
|
||||||
|
|
||||||
## Running the Code
|
|
||||||
|
|
||||||
From your tutorial build directory:
|
|
||||||
```bash
|
|
||||||
cd simple_tcod_tutorial/build
|
|
||||||
./mcrogueface scripts/main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## New Features
|
|
||||||
|
|
||||||
### Procedural Generation Module (`game/procgen.py`)
|
|
||||||
|
|
||||||
This dedicated module demonstrates separation of concerns - dungeon generation logic is kept separate from the game map implementation.
|
|
||||||
|
|
||||||
#### RectangularRoom Class
|
|
||||||
- **Clean Abstraction**: Represents a room with position and dimensions
|
|
||||||
- **Utility Properties**:
|
|
||||||
- `center` - Returns room center for connections
|
|
||||||
- `inner` - Returns slice objects for efficient carving
|
|
||||||
- **Intersection Detection**: `intersects()` method prevents overlapping rooms
|
|
||||||
|
|
||||||
#### Tunnel Generation
|
|
||||||
- **L-Shaped Corridors**: Simple but effective connection method
|
|
||||||
- **Iterator Pattern**: `tunnel_between()` yields coordinates efficiently
|
|
||||||
- **Random Variation**: 50/50 chance of horizontal-first vs vertical-first
|
|
||||||
|
|
||||||
#### Dungeon Generation Algorithm
|
|
||||||
```python
|
|
||||||
def generate_dungeon(max_rooms, room_min_size, room_max_size,
|
|
||||||
map_width, map_height, engine) -> GameMap:
|
|
||||||
```
|
|
||||||
- **Simple Algorithm**: Try to place random rooms, reject overlaps
|
|
||||||
- **Automatic Connection**: Each room connects to the previous one
|
|
||||||
- **Player Placement**: First room contains the player
|
|
||||||
- **Entity-Centric**: Uses `player.place()` for proper lifecycle
|
|
||||||
|
|
||||||
## Architecture Benefits
|
|
||||||
|
|
||||||
### Modular Design
|
|
||||||
- Generation logic separate from GameMap
|
|
||||||
- Easy to swap algorithms later
|
|
||||||
- Room class reusable for other features
|
|
||||||
|
|
||||||
### Forward Thinking
|
|
||||||
- Engine parameter anticipates entity spawning
|
|
||||||
- Room list available for future features
|
|
||||||
- Iterator-based tunnel generation is memory efficient
|
|
||||||
|
|
||||||
### Clean Integration
|
|
||||||
- Works seamlessly with existing entity placement
|
|
||||||
- Respects GameMap's tile management
|
|
||||||
- No special cases or hacks needed
|
|
||||||
|
|
||||||
## Visual Changes
|
|
||||||
|
|
||||||
- Map size increased to 80x45 for better dungeons
|
|
||||||
- Zoom reduced to 1.0 to see more of the map
|
|
||||||
- Random room layouts each time
|
|
||||||
- Connected rooms and corridors
|
|
||||||
|
|
||||||
## Algorithm Details
|
|
||||||
|
|
||||||
The generation follows these steps:
|
|
||||||
1. Start with a map filled with walls
|
|
||||||
2. Try to place up to `max_rooms` rooms
|
|
||||||
3. For each room attempt:
|
|
||||||
- Generate random size and position
|
|
||||||
- Check for intersections with existing rooms
|
|
||||||
- If valid, carve out the room
|
|
||||||
- Connect to previous room (if any)
|
|
||||||
4. Place player in center of first room
|
|
||||||
|
|
||||||
This simple algorithm creates playable dungeons while being easy to understand and modify.
|
|
||||||
|
|
||||||
## What's Next
|
|
||||||
|
|
||||||
Part 4 will add:
|
|
||||||
- Field of View (FOV) system
|
|
||||||
- Explored vs unexplored areas
|
|
||||||
- Light and dark tile rendering
|
|
||||||
- Torch radius around player
|
|
||||||
|
|
||||||
The modular dungeon generation makes it easy to add these visual features without touching the generation code.
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
# Part 4: Field of View and Exploration
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Part 4 introduces the Field of View (FOV) system, transforming our fully-visible dungeon into an atmospheric exploration experience. We leverage McRogueFace's built-in FOV capabilities and perspective system for efficient rendering.
|
|
||||||
|
|
||||||
## What's New in Part 4
|
|
||||||
|
|
||||||
### Field of View System
|
|
||||||
- **FOV Calculation**: Using `Grid.compute_fov()` with configurable radius
|
|
||||||
- **Perspective System**: Grid tracks which entity is the viewer
|
|
||||||
- **Visibility States**: Unexplored (black), explored (dark), visible (lit)
|
|
||||||
- **Automatic Updates**: FOV recalculates on player movement
|
|
||||||
|
|
||||||
### Implementation Details
|
|
||||||
|
|
||||||
#### FOV with McRogueFace's Grid
|
|
||||||
|
|
||||||
Unlike TCOD which uses numpy arrays for visibility tracking, McRogueFace's Grid has built-in FOV support:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# In GameMap.update_fov()
|
|
||||||
self.compute_fov(viewer_x, viewer_y, radius, light_walls=True, algorithm=mcrfpy.FOV_BASIC)
|
|
||||||
```
|
|
||||||
|
|
||||||
The Grid automatically:
|
|
||||||
- Tracks which tiles have been explored
|
|
||||||
- Applies appropriate color overlays (shroud, dark, light)
|
|
||||||
- Updates entity visibility based on FOV
|
|
||||||
|
|
||||||
#### Perspective System
|
|
||||||
|
|
||||||
McRogueFace uses a perspective-based rendering approach:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Set the viewer
|
|
||||||
self.game_map.perspective = self.player
|
|
||||||
|
|
||||||
# Grid automatically renders from this entity's viewpoint
|
|
||||||
```
|
|
||||||
|
|
||||||
This is more efficient than manually updating tile colors every turn.
|
|
||||||
|
|
||||||
#### Color Overlays
|
|
||||||
|
|
||||||
We define overlay colors but let the Grid handle application:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# In tiles.py
|
|
||||||
SHROUD = mcrfpy.Color(0, 0, 0, 255) # Unexplored
|
|
||||||
DARK = mcrfpy.Color(100, 100, 150, 128) # Explored but not visible
|
|
||||||
LIGHT = mcrfpy.Color(255, 255, 255, 0) # Currently visible
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Differences from TCOD
|
|
||||||
|
|
||||||
| TCOD Approach | McRogueFace Approach |
|
|
||||||
|---------------|----------------------|
|
|
||||||
| `visible` and `explored` numpy arrays | Grid's built-in FOV state |
|
|
||||||
| Manual tile color switching | Automatic overlay system |
|
|
||||||
| `tcod.map.compute_fov()` | `Grid.compute_fov()` |
|
|
||||||
| Render conditionals for each tile | Perspective-based rendering |
|
|
||||||
|
|
||||||
### Movement and FOV Updates
|
|
||||||
|
|
||||||
The action system now updates FOV after player movement:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# In MovementAction.perform()
|
|
||||||
if self.entity == engine.player:
|
|
||||||
engine.update_fov()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture Notes
|
|
||||||
|
|
||||||
### Why Grid Perspective?
|
|
||||||
|
|
||||||
The perspective system provides several benefits:
|
|
||||||
1. **Efficiency**: No per-tile color updates needed
|
|
||||||
2. **Flexibility**: Easy to switch viewpoints (for debugging or features)
|
|
||||||
3. **Automatic**: Grid handles all rendering details
|
|
||||||
4. **Clean**: Separates game logic from rendering concerns
|
|
||||||
|
|
||||||
### Entity Visibility
|
|
||||||
|
|
||||||
Entities automatically update their visibility state:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# After FOV calculation
|
|
||||||
self.player.update_visibility()
|
|
||||||
```
|
|
||||||
|
|
||||||
This ensures entities are only rendered when visible to the current perspective.
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
- `game/tiles.py`: Added FOV color overlay constants
|
|
||||||
- `game/game_map.py`: Added `update_fov()` method
|
|
||||||
- `game/engine.py`: Added FOV initialization and update method
|
|
||||||
- `game/actions.py`: Update FOV after player movement
|
|
||||||
- `main.py`: Updated part description
|
|
||||||
|
|
||||||
## What's Next
|
|
||||||
|
|
||||||
Part 5 will add enemies to our dungeon, introducing:
|
|
||||||
- Enemy entities with AI
|
|
||||||
- Combat system
|
|
||||||
- Turn-based gameplay
|
|
||||||
- Health and damage
|
|
||||||
|
|
||||||
The FOV system will make enemies appear and disappear as you explore, adding tension and strategy to the gameplay.
|
|
||||||
|
|
||||||
## Learning Points
|
|
||||||
|
|
||||||
1. **Leverage Framework Features**: Use McRogueFace's built-in systems rather than reimplementing
|
|
||||||
2. **Perspective-Based Design**: Think in terms of viewpoints, not global state
|
|
||||||
3. **Automatic Systems**: Let the framework handle rendering details
|
|
||||||
4. **Clean Integration**: FOV updates fit naturally into the action system
|
|
||||||
|
|
||||||
## Running Part 4
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd simple_tcod_tutorial/build
|
|
||||||
./mcrogueface scripts/main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
You'll now see:
|
|
||||||
- Black unexplored areas
|
|
||||||
- Dark blue tint on previously seen areas
|
|
||||||
- Full brightness only in your field of view
|
|
||||||
- Smooth exploration as you move through the dungeon
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
# Part 5: Placing Enemies and Fighting Them
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Part 5 brings our dungeon to life with enemies! We add rats and spiders that populate the rooms, implement a combat system with melee attacks, and handle entity death by turning creatures into gravestones.
|
|
||||||
|
|
||||||
## What's New in Part 5
|
|
||||||
|
|
||||||
### Actor System
|
|
||||||
- **Actor Class**: Extends Entity with combat stats (HP, defense, power)
|
|
||||||
- **Combat Properties**: Health tracking, damage calculation, alive status
|
|
||||||
- **Death Handling**: Entities become gravestones when killed
|
|
||||||
|
|
||||||
### Enemy Types
|
|
||||||
Using our sprite sheet, we have two enemy types:
|
|
||||||
- **Rat** (sprite 5): 10 HP, 0 defense, 3 power - Common enemy
|
|
||||||
- **Spider** (sprite 4): 16 HP, 1 defense, 4 power - Tougher enemy
|
|
||||||
|
|
||||||
### Combat System
|
|
||||||
|
|
||||||
#### Bump-to-Attack
|
|
||||||
When the player tries to move into an enemy:
|
|
||||||
```python
|
|
||||||
# In MovementAction.perform()
|
|
||||||
target = engine.game_map.get_blocking_entity_at(dest_x, dest_y)
|
|
||||||
if target:
|
|
||||||
if self.entity == engine.player:
|
|
||||||
from game.entity import Actor
|
|
||||||
if isinstance(target, Actor) and target != engine.player:
|
|
||||||
return MeleeAction(self.entity, self.dx, self.dy).perform(engine)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Damage Calculation
|
|
||||||
Simple formula with defense reduction:
|
|
||||||
```python
|
|
||||||
damage = attacker.power - target.defense
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Death System
|
|
||||||
Dead entities become gravestones:
|
|
||||||
```python
|
|
||||||
def die(self) -> None:
|
|
||||||
"""Handle death by becoming a gravestone."""
|
|
||||||
self.sprite_index = 6 # Tombstone sprite
|
|
||||||
self.blocks_movement = False
|
|
||||||
self.name = f"Grave of {self.name}"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Entity Factories
|
|
||||||
|
|
||||||
Factory functions create pre-configured entities:
|
|
||||||
```python
|
|
||||||
def rat(x: int, y: int, texture: mcrfpy.Texture) -> Actor:
|
|
||||||
return Actor(
|
|
||||||
x=x, y=y,
|
|
||||||
sprite_id=5, # Rat sprite
|
|
||||||
texture=texture,
|
|
||||||
name="Rat",
|
|
||||||
hp=10, defense=0, power=3,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dungeon Population
|
|
||||||
|
|
||||||
Enemies are placed randomly in rooms:
|
|
||||||
```python
|
|
||||||
def place_entities(room, dungeon, max_monsters, texture):
|
|
||||||
number_of_monsters = random.randint(0, max_monsters)
|
|
||||||
|
|
||||||
for _ in range(number_of_monsters):
|
|
||||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
|
||||||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
|
||||||
|
|
||||||
if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
|
|
||||||
# 80% rats, 20% spiders
|
|
||||||
if random.random() < 0.8:
|
|
||||||
monster = entity_factories.rat(x, y, texture)
|
|
||||||
else:
|
|
||||||
monster = entity_factories.spider(x, y, texture)
|
|
||||||
monster.place(x, y, dungeon)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Implementation Details
|
|
||||||
|
|
||||||
### FOV and Enemy Visibility
|
|
||||||
Enemies are automatically shown/hidden by the FOV system:
|
|
||||||
```python
|
|
||||||
def update_fov(self) -> None:
|
|
||||||
# Update visibility for all entities
|
|
||||||
for entity in self.game_map.entities:
|
|
||||||
entity.update_visibility()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Action System Extension
|
|
||||||
The action system now handles combat:
|
|
||||||
- **MovementAction**: Detects collision, triggers attack
|
|
||||||
- **MeleeAction**: New action for melee combat
|
|
||||||
- Actions remain decoupled from entity logic
|
|
||||||
|
|
||||||
### Gravestone System
|
|
||||||
Instead of removing dead entities:
|
|
||||||
- Sprite changes to tombstone (index 6)
|
|
||||||
- Name changes to "Grave of [Name]"
|
|
||||||
- No longer blocks movement
|
|
||||||
- Remains visible as dungeon decoration
|
|
||||||
|
|
||||||
## Architecture Notes
|
|
||||||
|
|
||||||
### Why Actor Extends Entity?
|
|
||||||
- Maintains entity hierarchy
|
|
||||||
- Combat stats only for creatures
|
|
||||||
- Future items/decorations won't have HP
|
|
||||||
- Clean separation of concerns
|
|
||||||
|
|
||||||
### Why Factory Functions?
|
|
||||||
- Centralized entity configuration
|
|
||||||
- Easy to add new enemy types
|
|
||||||
- Consistent stat management
|
|
||||||
- Type-safe entity creation
|
|
||||||
|
|
||||||
### Combat in Actions
|
|
||||||
Combat logic lives in actions, not entities:
|
|
||||||
- Entities store stats
|
|
||||||
- Actions perform combat
|
|
||||||
- Clean separation of data and behavior
|
|
||||||
- Extensible for future combat types
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
- `game/entity.py`: Added Actor class with combat stats and death handling
|
|
||||||
- `game/entity_factories.py`: New module with entity creation functions
|
|
||||||
- `game/actions.py`: Added MeleeAction for combat
|
|
||||||
- `game/procgen.py`: Added enemy placement in rooms
|
|
||||||
- `game/engine.py`: Updated to use Actor type and handle all entity visibility
|
|
||||||
- `main.py`: Updated to use entity factories and Part 5 description
|
|
||||||
|
|
||||||
## What's Next
|
|
||||||
|
|
||||||
Part 6 will enhance the combat experience with:
|
|
||||||
- Health display UI
|
|
||||||
- Game over conditions
|
|
||||||
- Combat messages window
|
|
||||||
- More strategic combat mechanics
|
|
||||||
|
|
||||||
## Learning Points
|
|
||||||
|
|
||||||
1. **Entity Specialization**: Use inheritance to add features to specific entity types
|
|
||||||
2. **Factory Pattern**: Centralize object creation for consistency
|
|
||||||
3. **State Transformation**: Dead entities become decorations, not deletions
|
|
||||||
4. **Action Extensions**: Combat fits naturally into the action system
|
|
||||||
5. **Automatic Systems**: FOV handles entity visibility without special code
|
|
||||||
|
|
||||||
## Running Part 5
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd simple_tcod_tutorial/build
|
|
||||||
./mcrogueface scripts/main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
You'll now encounter rats and spiders as you explore! Walk into them to attack. Dead enemies become gravestones that mark your battles.
|
|
||||||
|
|
||||||
## Sprite Adaptations
|
|
||||||
|
|
||||||
Following our sprite sheet (`sprite_sheet.md`), we made these thematic changes:
|
|
||||||
- Orcs → Rats (same stats, different sprite)
|
|
||||||
- Trolls → Spiders (same stats, different sprite)
|
|
||||||
- Corpses → Gravestones (all use same tombstone sprite)
|
|
||||||
|
|
||||||
The gameplay remains identical to the TCOD tutorial, just with different visual theming.
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
# Part 6: Doing (and Taking) Damage
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Part 6 transforms our basic combat into a complete gameplay loop with visual feedback, enemy AI, and win/lose conditions. We add a health bar, message log, enemy AI that pursues the player, and proper game over handling.
|
|
||||||
|
|
||||||
## What's New in Part 6
|
|
||||||
|
|
||||||
### User Interface Components
|
|
||||||
|
|
||||||
#### Health Bar
|
|
||||||
A visual representation of the player's current health:
|
|
||||||
```python
|
|
||||||
class HealthBar:
|
|
||||||
def create_ui(self) -> List[mcrfpy.UIDrawable]:
|
|
||||||
# Dark red background
|
|
||||||
self.background = mcrfpy.Frame(pos=(x, y), size=(width, height))
|
|
||||||
self.background.fill_color = mcrfpy.Color(100, 0, 0, 255)
|
|
||||||
|
|
||||||
# Bright colored bar (green/yellow/red based on HP)
|
|
||||||
self.bar = mcrfpy.Frame(pos=(x, y), size=(width, height))
|
|
||||||
|
|
||||||
# Text overlay showing HP numbers
|
|
||||||
self.text = mcrfpy.Caption(pos=(x+5, y+2),
|
|
||||||
text=f"HP: {hp}/{max_hp}")
|
|
||||||
```
|
|
||||||
|
|
||||||
The bar changes color based on health percentage:
|
|
||||||
- Green (>60% health)
|
|
||||||
- Yellow (30-60% health)
|
|
||||||
- Red (<30% health)
|
|
||||||
|
|
||||||
#### Message Log
|
|
||||||
A scrolling combat log that replaces console print statements:
|
|
||||||
```python
|
|
||||||
class MessageLog:
|
|
||||||
def __init__(self, max_messages: int = 5):
|
|
||||||
self.messages: deque[str] = deque(maxlen=max_messages)
|
|
||||||
|
|
||||||
def add_message(self, message: str) -> None:
|
|
||||||
self.messages.append(message)
|
|
||||||
self.update_display()
|
|
||||||
```
|
|
||||||
|
|
||||||
Messages include:
|
|
||||||
- Combat actions ("Rat attacks Player for 3 hit points.")
|
|
||||||
- Death notifications ("Spider is dead!")
|
|
||||||
- Game state changes ("You have died! Press Escape to quit.")
|
|
||||||
|
|
||||||
### Enemy AI System
|
|
||||||
|
|
||||||
#### Basic AI Component
|
|
||||||
Enemies now actively pursue and attack the player:
|
|
||||||
```python
|
|
||||||
class BasicAI:
|
|
||||||
def take_turn(self, engine: Engine) -> None:
|
|
||||||
distance = max(abs(dx), abs(dy)) # Chebyshev distance
|
|
||||||
|
|
||||||
if distance <= 1:
|
|
||||||
# Adjacent: Attack!
|
|
||||||
MeleeAction(self.entity, attack_dx, attack_dy).perform(engine)
|
|
||||||
elif distance <= 6:
|
|
||||||
# Can see player: Move closer
|
|
||||||
MovementAction(self.entity, move_dx, move_dy).perform(engine)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Turn-Based System
|
|
||||||
After each player action, all enemies take their turn:
|
|
||||||
```python
|
|
||||||
def handle_enemy_turns(self) -> None:
|
|
||||||
for entity in self.game_map.entities:
|
|
||||||
if isinstance(entity, Actor) and entity.ai and entity.is_alive:
|
|
||||||
entity.ai.take_turn(self)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Game Over Condition
|
|
||||||
|
|
||||||
When the player dies:
|
|
||||||
1. Game state flag is set (`engine.game_over = True`)
|
|
||||||
2. Player becomes a gravestone (sprite changes)
|
|
||||||
3. Input is restricted (only Escape works)
|
|
||||||
4. Death message appears in the message log
|
|
||||||
|
|
||||||
```python
|
|
||||||
def handle_player_death(self) -> None:
|
|
||||||
self.game_over = True
|
|
||||||
self.message_log.add_message("You have died! Press Escape to quit.")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture Improvements
|
|
||||||
|
|
||||||
### UI Module (`game/ui.py`)
|
|
||||||
Separates UI concerns from game logic:
|
|
||||||
- `MessageLog`: Manages combat messages
|
|
||||||
- `HealthBar`: Displays player health
|
|
||||||
- Clean interface for updating displays
|
|
||||||
|
|
||||||
### AI Module (`game/ai.py`)
|
|
||||||
Encapsulates enemy behavior:
|
|
||||||
- `BasicAI`: Simple pursue-and-attack behavior
|
|
||||||
- Extensible for different AI types
|
|
||||||
- Uses existing action system
|
|
||||||
|
|
||||||
### Turn Management
|
|
||||||
Player actions trigger enemy turns:
|
|
||||||
- Movement → Enemy turns
|
|
||||||
- Attack → Enemy turns
|
|
||||||
- Wait → Enemy turns
|
|
||||||
- Maintains turn-based feel
|
|
||||||
|
|
||||||
## Key Implementation Details
|
|
||||||
|
|
||||||
### UI Updates
|
|
||||||
Health bar updates occur:
|
|
||||||
- After player takes damage
|
|
||||||
- Automatically via `engine.update_ui()`
|
|
||||||
- Color changes based on HP percentage
|
|
||||||
|
|
||||||
### Message Flow
|
|
||||||
Combat messages follow this pattern:
|
|
||||||
1. Action generates message text
|
|
||||||
2. `engine.message_log.add_message(text)`
|
|
||||||
3. Message appears in UI Caption
|
|
||||||
4. Old messages scroll up
|
|
||||||
|
|
||||||
### AI Decision Making
|
|
||||||
Basic AI uses simple rules:
|
|
||||||
1. Check if player is adjacent → Attack
|
|
||||||
2. Check if player is visible (within 6 tiles) → Move toward
|
|
||||||
3. Otherwise → Do nothing
|
|
||||||
|
|
||||||
### Game State Management
|
|
||||||
The `game_over` flag prevents:
|
|
||||||
- Player movement
|
|
||||||
- Player attacks
|
|
||||||
- Player waiting
|
|
||||||
- But allows Escape to quit
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
- `game/ui.py`: New module for UI components
|
|
||||||
- `game/ai.py`: New module for enemy AI
|
|
||||||
- `game/engine.py`: Added UI setup, enemy turns, game over handling
|
|
||||||
- `game/entity.py`: Added AI component to Actor
|
|
||||||
- `game/entity_factories.py`: Attached AI to enemies
|
|
||||||
- `game/actions.py`: Integrated message log, added enemy turn triggers
|
|
||||||
- `main.py`: Updated part description
|
|
||||||
|
|
||||||
## What's Next
|
|
||||||
|
|
||||||
Part 7 will expand the user interface further with:
|
|
||||||
- More detailed entity inspection
|
|
||||||
- Possibly inventory display
|
|
||||||
- Additional UI panels
|
|
||||||
- Mouse interaction
|
|
||||||
|
|
||||||
## Learning Points
|
|
||||||
|
|
||||||
1. **UI Separation**: Keep UI logic separate from game logic
|
|
||||||
2. **Component Systems**: AI as a component allows different behaviors
|
|
||||||
3. **Turn-Based Flow**: Player action → Enemy reactions creates tactical gameplay
|
|
||||||
4. **Visual Feedback**: Health bars and message logs improve player understanding
|
|
||||||
5. **State Management**: Game over flag controls available actions
|
|
||||||
|
|
||||||
## Running Part 6
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd simple_tcod_tutorial/build
|
|
||||||
./mcrogueface scripts/main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
You'll now see:
|
|
||||||
- Health bar at the top showing your current HP
|
|
||||||
- Message log at the bottom showing combat events
|
|
||||||
- Enemies that chase you when you're nearby
|
|
||||||
- Enemies that attack when adjacent
|
|
||||||
- Death state when HP reaches 0
|
|
||||||
|
|
||||||
## Combat Strategy
|
|
||||||
|
|
||||||
With enemy AI active, combat becomes more tactical:
|
|
||||||
- Enemies pursue when they see you
|
|
||||||
- Fighting in corridors limits how many can attack
|
|
||||||
- Running away is sometimes the best option
|
|
||||||
- Health management becomes critical
|
|
||||||
|
|
||||||
The game now has a complete combat loop with clear win/lose conditions!
|
|
||||||
|
|
@ -1,204 +0,0 @@
|
||||||
# Part 7: Creating the User Interface
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Part 7 significantly enhances the user interface, transforming our roguelike from a basic game into a more polished experience. We add mouse interaction, help displays, information panels, and better visual feedback systems.
|
|
||||||
|
|
||||||
## What's New in Part 7
|
|
||||||
|
|
||||||
### Mouse Interaction
|
|
||||||
|
|
||||||
#### Click-to-Inspect System
|
|
||||||
Since McRogueFace doesn't have mouse motion events, we use click events to show entity information:
|
|
||||||
```python
|
|
||||||
def grid_click_handler(pixel_x, pixel_y, button, state):
|
|
||||||
# Convert pixel coordinates to grid coordinates
|
|
||||||
grid_x = int(pixel_x / (self.tile_size * self.zoom))
|
|
||||||
grid_y = int(pixel_y / (self.tile_size * self.zoom))
|
|
||||||
|
|
||||||
# Update hover display for this position
|
|
||||||
self.update_mouse_hover(grid_x, grid_y)
|
|
||||||
```
|
|
||||||
|
|
||||||
Click displays show:
|
|
||||||
- Entity names
|
|
||||||
- Current HP for living creatures
|
|
||||||
- Multiple entities if stacked (e.g., "Grave of Rat")
|
|
||||||
|
|
||||||
#### Mouse Handler Registration
|
|
||||||
The click handler is registered as a local function to avoid issues with bound methods:
|
|
||||||
```python
|
|
||||||
# Use a local function instead of a bound method
|
|
||||||
self.game_map.click = grid_click_handler
|
|
||||||
```
|
|
||||||
|
|
||||||
### Help System
|
|
||||||
|
|
||||||
#### Toggle Help Display
|
|
||||||
Press `?`, `H`, or `F1` to show/hide help:
|
|
||||||
```python
|
|
||||||
class HelpDisplay:
|
|
||||||
def toggle(self) -> None:
|
|
||||||
self.visible = not self.visible
|
|
||||||
self.panel.frame.visible = self.visible
|
|
||||||
```
|
|
||||||
|
|
||||||
The help panel includes:
|
|
||||||
- Movement controls for all input methods
|
|
||||||
- Combat instructions
|
|
||||||
- Mouse usage tips
|
|
||||||
- Gameplay strategies
|
|
||||||
|
|
||||||
### Information Panels
|
|
||||||
|
|
||||||
#### Player Stats Panel
|
|
||||||
Always-visible panel showing:
|
|
||||||
- Player name
|
|
||||||
- Current/Max HP
|
|
||||||
- Power and Defense stats
|
|
||||||
- Current grid position
|
|
||||||
|
|
||||||
```python
|
|
||||||
class InfoPanel:
|
|
||||||
def create_ui(self, title: str) -> List[mcrfpy.Drawable]:
|
|
||||||
# Semi-transparent background frame
|
|
||||||
self.frame = mcrfpy.Frame(pos=(x, y), size=(width, height))
|
|
||||||
self.frame.fill_color = mcrfpy.Color(20, 20, 40, 200)
|
|
||||||
|
|
||||||
# Title and content captions as children
|
|
||||||
self.frame.children.append(self.title_caption)
|
|
||||||
self.frame.children.append(self.content_caption)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Reusable Panel System
|
|
||||||
The `InfoPanel` class provides:
|
|
||||||
- Titled panels with borders
|
|
||||||
- Semi-transparent backgrounds
|
|
||||||
- Easy content updates
|
|
||||||
- Consistent visual style
|
|
||||||
|
|
||||||
### Enhanced UI Components
|
|
||||||
|
|
||||||
#### MouseHoverDisplay Class
|
|
||||||
Manages tooltip-style hover information:
|
|
||||||
- Follows mouse position
|
|
||||||
- Shows/hides automatically
|
|
||||||
- Offset to avoid cursor overlap
|
|
||||||
- Multiple entity support
|
|
||||||
|
|
||||||
#### UI Module Organization
|
|
||||||
Clean separation of UI components:
|
|
||||||
- `MessageLog`: Combat messages
|
|
||||||
- `HealthBar`: HP visualization
|
|
||||||
- `MouseHoverDisplay`: Entity inspection
|
|
||||||
- `InfoPanel`: Generic information display
|
|
||||||
- `HelpDisplay`: Keyboard controls
|
|
||||||
|
|
||||||
## Architecture Improvements
|
|
||||||
|
|
||||||
### UI Composition
|
|
||||||
Using McRogueFace's parent-child system:
|
|
||||||
```python
|
|
||||||
# Add caption as child of frame
|
|
||||||
self.frame.children.append(self.text_caption)
|
|
||||||
```
|
|
||||||
|
|
||||||
Benefits:
|
|
||||||
- Automatic relative positioning
|
|
||||||
- Group visibility control
|
|
||||||
- Clean hierarchy
|
|
||||||
|
|
||||||
### Event Handler Extensions
|
|
||||||
Input handler now manages:
|
|
||||||
- Keyboard input (existing)
|
|
||||||
- Mouse motion (new)
|
|
||||||
- Mouse clicks (prepared for future)
|
|
||||||
- UI toggles (help display)
|
|
||||||
|
|
||||||
### Dynamic Content Updates
|
|
||||||
All UI elements support real-time updates:
|
|
||||||
```python
|
|
||||||
def update_stats_panel(self) -> None:
|
|
||||||
stats_text = f"""Name: {self.player.name}
|
|
||||||
HP: {self.player.hp}/{self.player.max_hp}
|
|
||||||
Power: {self.player.power}
|
|
||||||
Defense: {self.player.defense}"""
|
|
||||||
self.stats_panel.update_content(stats_text)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Implementation Details
|
|
||||||
|
|
||||||
### Mouse Coordinate Conversion
|
|
||||||
Pixel to grid conversion:
|
|
||||||
```python
|
|
||||||
grid_x = int(x / (self.engine.tile_size * self.engine.zoom))
|
|
||||||
grid_y = int(y / (self.engine.tile_size * self.engine.zoom))
|
|
||||||
```
|
|
||||||
|
|
||||||
### Visibility Management
|
|
||||||
UI elements can be toggled:
|
|
||||||
- Help panel starts hidden
|
|
||||||
- Mouse hover hides when not over entities
|
|
||||||
- Panels can be shown/hidden dynamically
|
|
||||||
|
|
||||||
### Color and Transparency
|
|
||||||
UI uses semi-transparent overlays:
|
|
||||||
- Panel backgrounds: `Color(20, 20, 40, 200)`
|
|
||||||
- Hover tooltips: `Color(255, 255, 200, 255)`
|
|
||||||
- Borders and outlines for readability
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
- `game/ui.py`: Added MouseHoverDisplay, InfoPanel, HelpDisplay classes
|
|
||||||
- `game/engine.py`: Integrated new UI components, mouse hover handling
|
|
||||||
- `game/input_handlers.py`: Added mouse motion handling, help toggle
|
|
||||||
- `main.py`: Registered mouse handlers, updated part description
|
|
||||||
|
|
||||||
## What's Next
|
|
||||||
|
|
||||||
Part 8 will add items and inventory:
|
|
||||||
- Collectible items (potions, equipment)
|
|
||||||
- Inventory management UI
|
|
||||||
- Item usage mechanics
|
|
||||||
- Equipment system
|
|
||||||
|
|
||||||
## Learning Points
|
|
||||||
|
|
||||||
1. **UI Composition**: Use parent-child relationships for complex UI
|
|
||||||
2. **Event Delegation**: Separate input handling from UI updates
|
|
||||||
3. **Information Layers**: Multiple UI systems can coexist (hover, panels, help)
|
|
||||||
4. **Visual Polish**: Small touches like transparency and borders improve UX
|
|
||||||
5. **Reusable Components**: Generic panels can be specialized for different uses
|
|
||||||
|
|
||||||
## Running Part 7
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd simple_tcod_tutorial/build
|
|
||||||
./mcrogueface scripts/main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
New features to try:
|
|
||||||
- Click on entities to see their details
|
|
||||||
- Press ? or H to toggle help display
|
|
||||||
- Watch the stats panel update as you take damage
|
|
||||||
- See entity HP in hover tooltips
|
|
||||||
- Notice the visual polish in UI panels
|
|
||||||
|
|
||||||
## UI Design Principles
|
|
||||||
|
|
||||||
### Consistency
|
|
||||||
- All panels use similar visual style
|
|
||||||
- Consistent color scheme
|
|
||||||
- Uniform text sizing
|
|
||||||
|
|
||||||
### Non-Intrusive
|
|
||||||
- Semi-transparent panels don't block view
|
|
||||||
- Hover info appears near cursor
|
|
||||||
- Help can be toggled off
|
|
||||||
|
|
||||||
### Information Hierarchy
|
|
||||||
- Critical info (health) always visible
|
|
||||||
- Contextual info (hover) on demand
|
|
||||||
- Help info toggleable
|
|
||||||
|
|
||||||
The UI now provides a professional feel while maintaining the roguelike aesthetic!
|
|
||||||
|
|
@ -1,297 +0,0 @@
|
||||||
# Part 8: Items and Inventory
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Part 8 transforms our roguelike into a proper loot-driven game by adding items that can be collected, managed, and used. We implement a flexible inventory system with capacity limits, create consumable items like healing potions, and build UI for inventory management.
|
|
||||||
|
|
||||||
## What's New in Part 8
|
|
||||||
|
|
||||||
### Parent-Child Entity Architecture
|
|
||||||
|
|
||||||
#### Flexible Entity Ownership
|
|
||||||
Entities now have parent containers, allowing them to exist in different contexts:
|
|
||||||
```python
|
|
||||||
class Entity(mcrfpy.Entity):
|
|
||||||
def __init__(self, parent: Optional[Union[GameMap, Inventory]] = None):
|
|
||||||
self.parent = parent
|
|
||||||
|
|
||||||
@property
|
|
||||||
def gamemap(self) -> Optional[GameMap]:
|
|
||||||
"""Get the GameMap through the parent chain"""
|
|
||||||
if isinstance(self.parent, Inventory):
|
|
||||||
return self.parent.gamemap
|
|
||||||
return self.parent
|
|
||||||
```
|
|
||||||
|
|
||||||
Benefits:
|
|
||||||
- Items can exist in the world or in inventories
|
|
||||||
- Clean ownership transfer when picking up/dropping
|
|
||||||
- Automatic visibility management
|
|
||||||
|
|
||||||
### Inventory System
|
|
||||||
|
|
||||||
#### Container-Based Design
|
|
||||||
The inventory acts like a specialized entity container:
|
|
||||||
```python
|
|
||||||
class Inventory:
|
|
||||||
def __init__(self, capacity: int):
|
|
||||||
self.capacity = capacity
|
|
||||||
self.items: List[Item] = []
|
|
||||||
self.parent: Optional[Actor] = None
|
|
||||||
|
|
||||||
def add_item(self, item: Item) -> None:
|
|
||||||
if len(self.items) >= self.capacity:
|
|
||||||
raise Impossible("Your inventory is full.")
|
|
||||||
|
|
||||||
# Transfer ownership
|
|
||||||
self.items.append(item)
|
|
||||||
item.parent = self
|
|
||||||
item.visible = False # Hide from map
|
|
||||||
```
|
|
||||||
|
|
||||||
Features:
|
|
||||||
- Capacity limits (26 items for letter selection)
|
|
||||||
- Clean item transfer between world and inventory
|
|
||||||
- Automatic visual management
|
|
||||||
|
|
||||||
### Item System
|
|
||||||
|
|
||||||
#### Item Entity Class
|
|
||||||
Items are entities with consumable components:
|
|
||||||
```python
|
|
||||||
class Item(Entity):
|
|
||||||
def __init__(self, consumable: Optional = None):
|
|
||||||
super().__init__(blocks_movement=False)
|
|
||||||
self.consumable = consumable
|
|
||||||
if consumable:
|
|
||||||
consumable.parent = self
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Consumable Components
|
|
||||||
Modular system for item effects:
|
|
||||||
```python
|
|
||||||
class HealingConsumable(Consumable):
|
|
||||||
def activate(self, action: ItemAction) -> None:
|
|
||||||
if consumer.hp >= consumer.max_hp:
|
|
||||||
raise Impossible("You are already at full health.")
|
|
||||||
|
|
||||||
amount_recovered = min(self.amount, consumer.max_hp - consumer.hp)
|
|
||||||
consumer.hp += amount_recovered
|
|
||||||
self.consume() # Remove item after use
|
|
||||||
```
|
|
||||||
|
|
||||||
### Exception-Driven Feedback
|
|
||||||
|
|
||||||
#### Clean Error Handling
|
|
||||||
Using exceptions for user feedback:
|
|
||||||
```python
|
|
||||||
class Impossible(Exception):
|
|
||||||
"""Action cannot be performed"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class PickupAction(Action):
|
|
||||||
def perform(self, engine: Engine) -> None:
|
|
||||||
if not items_here:
|
|
||||||
raise Impossible("There is nothing here to pick up.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
inventory.add_item(item)
|
|
||||||
engine.message_log.add_message(f"You picked up the {item.name}!")
|
|
||||||
except Impossible as e:
|
|
||||||
engine.message_log.add_message(str(e))
|
|
||||||
```
|
|
||||||
|
|
||||||
Benefits:
|
|
||||||
- Consistent error messaging
|
|
||||||
- Clean control flow
|
|
||||||
- Centralized feedback handling
|
|
||||||
|
|
||||||
### Inventory UI
|
|
||||||
|
|
||||||
#### Modal Inventory Screen
|
|
||||||
Interactive inventory management:
|
|
||||||
```python
|
|
||||||
class InventoryEventHandler(BaseEventHandler):
|
|
||||||
def create_ui(self) -> None:
|
|
||||||
# Semi-transparent background
|
|
||||||
self.background = mcrfpy.Frame(pos=(100, 100), size=(400, 400))
|
|
||||||
self.background.fill_color = mcrfpy.Color(0, 0, 0, 200)
|
|
||||||
|
|
||||||
# List items with letter keys
|
|
||||||
for i, item in enumerate(inventory.items):
|
|
||||||
item_caption = mcrfpy.Caption(
|
|
||||||
pos=(20, 80 + i * 20),
|
|
||||||
text=f"{chr(ord('a') + i)}) {item.name}"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Features:
|
|
||||||
- Letter-based selection (a-z)
|
|
||||||
- Separate handlers for use/drop
|
|
||||||
- ESC to cancel
|
|
||||||
- Visual feedback
|
|
||||||
|
|
||||||
### Enhanced Actions
|
|
||||||
|
|
||||||
#### Item Actions
|
|
||||||
New actions for item management:
|
|
||||||
```python
|
|
||||||
class PickupAction(Action):
|
|
||||||
"""Pick up items at current location"""
|
|
||||||
|
|
||||||
class ItemAction(Action):
|
|
||||||
"""Base for item usage actions"""
|
|
||||||
|
|
||||||
class DropAction(ItemAction):
|
|
||||||
"""Drop item from inventory"""
|
|
||||||
```
|
|
||||||
|
|
||||||
Each action:
|
|
||||||
- Self-validates
|
|
||||||
- Provides feedback
|
|
||||||
- Triggers enemy turns
|
|
||||||
|
|
||||||
## Architecture Improvements
|
|
||||||
|
|
||||||
### Component Relationships
|
|
||||||
Parent-based component system:
|
|
||||||
```python
|
|
||||||
# Components know their parent
|
|
||||||
consumable.parent = item
|
|
||||||
item.parent = inventory
|
|
||||||
inventory.parent = actor
|
|
||||||
actor.parent = gamemap
|
|
||||||
gamemap.engine = engine
|
|
||||||
```
|
|
||||||
|
|
||||||
Benefits:
|
|
||||||
- Access to game context from any component
|
|
||||||
- Clean ownership transfer
|
|
||||||
- Simplified entity lifecycle
|
|
||||||
|
|
||||||
### Input Handler States
|
|
||||||
Modal UI through handler switching:
|
|
||||||
```python
|
|
||||||
# Main game
|
|
||||||
engine.current_handler = MainGameEventHandler(engine)
|
|
||||||
|
|
||||||
# Open inventory
|
|
||||||
engine.current_handler = InventoryActivateHandler(engine)
|
|
||||||
|
|
||||||
# Back to game
|
|
||||||
engine.current_handler = MainGameEventHandler(engine)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Entity Lifecycle Management
|
|
||||||
Proper creation and cleanup:
|
|
||||||
```python
|
|
||||||
# Item spawning
|
|
||||||
item = entity_factories.health_potion(x, y, texture)
|
|
||||||
item.place(x, y, dungeon)
|
|
||||||
|
|
||||||
# Pickup
|
|
||||||
inventory.add_item(item) # Removes from map
|
|
||||||
|
|
||||||
# Drop
|
|
||||||
inventory.drop(item) # Returns to map
|
|
||||||
|
|
||||||
# Death
|
|
||||||
actor.die() # Drops all items
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Implementation Details
|
|
||||||
|
|
||||||
### Visibility Management
|
|
||||||
Items hide/show based on container:
|
|
||||||
```python
|
|
||||||
def add_item(self, item):
|
|
||||||
item.visible = False # Hide when in inventory
|
|
||||||
|
|
||||||
def drop(self, item):
|
|
||||||
item.visible = True # Show when on map
|
|
||||||
```
|
|
||||||
|
|
||||||
### Inventory Capacity
|
|
||||||
Limited to alphabet keys:
|
|
||||||
```python
|
|
||||||
if len(inventory.items) >= 26:
|
|
||||||
raise Impossible("Your inventory is full.")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Item Generation
|
|
||||||
Procedural item placement:
|
|
||||||
```python
|
|
||||||
def place_entities(room, dungeon, max_monsters, max_items, texture):
|
|
||||||
# Place 0-2 items per room
|
|
||||||
number_of_items = random.randint(0, max_items)
|
|
||||||
|
|
||||||
for _ in range(number_of_items):
|
|
||||||
if space_available:
|
|
||||||
item = entity_factories.health_potion(x, y, texture)
|
|
||||||
item.place(x, y, dungeon)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
- `game/entity.py`: Added parent system, Item class, inventory to Actor
|
|
||||||
- `game/inventory.py`: New inventory container system
|
|
||||||
- `game/consumable.py`: New consumable component system
|
|
||||||
- `game/exceptions.py`: New Impossible exception
|
|
||||||
- `game/actions.py`: Added PickupAction, ItemAction, DropAction
|
|
||||||
- `game/input_handlers.py`: Added InventoryEventHandler classes
|
|
||||||
- `game/engine.py`: Added current_handler, inventory UI methods
|
|
||||||
- `game/procgen.py`: Added item generation
|
|
||||||
- `game/entity_factories.py`: Added health_potion factory
|
|
||||||
- `game/ui.py`: Updated help text with inventory controls
|
|
||||||
- `main.py`: Updated to Part 8, handler management
|
|
||||||
|
|
||||||
## What's Next
|
|
||||||
|
|
||||||
Part 9 will add ranged attacks and targeting:
|
|
||||||
- Targeting UI for selecting enemies
|
|
||||||
- Ranged damage items (lightning staff)
|
|
||||||
- Area-of-effect items (fireball staff)
|
|
||||||
- Confusion effects
|
|
||||||
|
|
||||||
## Learning Points
|
|
||||||
|
|
||||||
1. **Container Architecture**: Entity ownership through parent relationships
|
|
||||||
2. **Component Systems**: Modular, reusable components with parent references
|
|
||||||
3. **Exception Handling**: Clean error propagation and user feedback
|
|
||||||
4. **Modal UI**: State-based input handling for different screens
|
|
||||||
5. **Item Systems**: Flexible consumable architecture for varied effects
|
|
||||||
6. **Lifecycle Management**: Proper entity creation, transfer, and cleanup
|
|
||||||
|
|
||||||
## Running Part 8
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd simple_tcod_tutorial/build
|
|
||||||
./mcrogueface scripts/main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
New features to try:
|
|
||||||
- Press G to pick up healing potions
|
|
||||||
- Press I to open inventory and use items
|
|
||||||
- Press O to drop items from inventory
|
|
||||||
- Heal yourself when injured in combat
|
|
||||||
- Manage limited inventory space (26 slots)
|
|
||||||
- Items drop from dead enemies
|
|
||||||
|
|
||||||
## Design Principles
|
|
||||||
|
|
||||||
### Flexibility Through Composition
|
|
||||||
- Items gain behavior through consumable components
|
|
||||||
- Easy to add new item types
|
|
||||||
- Reusable effect system
|
|
||||||
|
|
||||||
### Clean Ownership Transfer
|
|
||||||
- Entities always have clear parent
|
|
||||||
- Automatic visibility management
|
|
||||||
- No orphaned entities
|
|
||||||
|
|
||||||
### User-Friendly Feedback
|
|
||||||
- Clear error messages
|
|
||||||
- Consistent UI patterns
|
|
||||||
- Intuitive controls
|
|
||||||
|
|
||||||
The inventory system provides the foundation for equipment, spells, and complex item interactions in future parts!
|
|
||||||
|
|
@ -1,625 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 5: Entity Interactions
|
|
||||||
|
|
||||||
This tutorial builds on Part 4 by adding:
|
|
||||||
- Entity class hierarchy (PlayerEntity, EnemyEntity, BoulderEntity, ButtonEntity)
|
|
||||||
- Non-blocking movement animations with destination tracking
|
|
||||||
- Bump interactions (combat, pushing)
|
|
||||||
- Step-on interactions (buttons, doors)
|
|
||||||
- Concurrent enemy AI with smooth animations
|
|
||||||
|
|
||||||
Key concepts:
|
|
||||||
- Entities inherit from mcrfpy.Entity for proper C++/Python integration
|
|
||||||
- Logic operates on destination positions during animations
|
|
||||||
- Player input is processed immediately, not blocked by animations
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Entity Classes - Inherit from mcrfpy.Entity
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class GameEntity(mcrfpy.Entity):
|
|
||||||
"""Base class for all game entities with interaction logic"""
|
|
||||||
def __init__(self, x, y, **kwargs):
|
|
||||||
# Extract grid before passing to parent
|
|
||||||
grid = kwargs.pop('grid', None)
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
|
|
||||||
# Current position is tracked by parent Entity.x/y
|
|
||||||
# Add destination tracking for animation system
|
|
||||||
self.dest_x = x
|
|
||||||
self.dest_y = y
|
|
||||||
self.is_moving = False
|
|
||||||
|
|
||||||
# Game properties
|
|
||||||
self.blocks_movement = True
|
|
||||||
self.hp = 10
|
|
||||||
self.max_hp = 10
|
|
||||||
self.entity_type = "generic"
|
|
||||||
|
|
||||||
# Add to grid if provided
|
|
||||||
if grid:
|
|
||||||
grid.entities.append(self)
|
|
||||||
|
|
||||||
def start_move(self, new_x, new_y, duration=0.2, callback=None):
|
|
||||||
"""Start animating movement to new position"""
|
|
||||||
self.dest_x = new_x
|
|
||||||
self.dest_y = new_y
|
|
||||||
self.is_moving = True
|
|
||||||
|
|
||||||
# Create animations for smooth movement
|
|
||||||
if callback:
|
|
||||||
# Only x animation needs callback since they run in parallel
|
|
||||||
anim_x = mcrfpy.Animation("x", float(new_x), duration, "easeInOutQuad", callback=callback)
|
|
||||||
else:
|
|
||||||
anim_x = mcrfpy.Animation("x", float(new_x), duration, "easeInOutQuad")
|
|
||||||
anim_y = mcrfpy.Animation("y", float(new_y), duration, "easeInOutQuad")
|
|
||||||
|
|
||||||
anim_x.start(self)
|
|
||||||
anim_y.start(self)
|
|
||||||
|
|
||||||
def get_position(self):
|
|
||||||
"""Get logical position (destination if moving, otherwise current)"""
|
|
||||||
if self.is_moving:
|
|
||||||
return (self.dest_x, self.dest_y)
|
|
||||||
return (int(self.x), int(self.y))
|
|
||||||
|
|
||||||
def on_bump(self, other):
|
|
||||||
"""Called when another entity tries to move into our space"""
|
|
||||||
return False # Block movement by default
|
|
||||||
|
|
||||||
def on_step(self, other):
|
|
||||||
"""Called when another entity steps on us (non-blocking)"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def take_damage(self, damage):
|
|
||||||
"""Apply damage and handle death"""
|
|
||||||
self.hp -= damage
|
|
||||||
if self.hp <= 0:
|
|
||||||
self.hp = 0
|
|
||||||
self.die()
|
|
||||||
|
|
||||||
def die(self):
|
|
||||||
"""Remove entity from grid"""
|
|
||||||
# The C++ die() method handles removal from grid
|
|
||||||
super().die()
|
|
||||||
|
|
||||||
class PlayerEntity(GameEntity):
|
|
||||||
"""The player character"""
|
|
||||||
def __init__(self, x, y, **kwargs):
|
|
||||||
kwargs['sprite_index'] = 64 # Hero sprite
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
self.damage = 3
|
|
||||||
self.entity_type = "player"
|
|
||||||
self.blocks_movement = True
|
|
||||||
|
|
||||||
def on_bump(self, other):
|
|
||||||
"""Player bumps into something"""
|
|
||||||
if other.entity_type == "enemy":
|
|
||||||
# Deal damage
|
|
||||||
other.take_damage(self.damage)
|
|
||||||
return False # Can't move into enemy space
|
|
||||||
elif other.entity_type == "boulder":
|
|
||||||
# Try to push
|
|
||||||
dx = self.dest_x - int(self.x)
|
|
||||||
dy = self.dest_y - int(self.y)
|
|
||||||
return other.try_push(dx, dy)
|
|
||||||
return False
|
|
||||||
|
|
||||||
class EnemyEntity(GameEntity):
|
|
||||||
"""Basic enemy with AI"""
|
|
||||||
def __init__(self, x, y, **kwargs):
|
|
||||||
kwargs['sprite_index'] = 65 # Enemy sprite
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
self.damage = 1
|
|
||||||
self.entity_type = "enemy"
|
|
||||||
self.ai_state = "wander"
|
|
||||||
self.hp = 5
|
|
||||||
self.max_hp = 5
|
|
||||||
|
|
||||||
def on_bump(self, other):
|
|
||||||
"""Enemy bumps into something"""
|
|
||||||
if other.entity_type == "player":
|
|
||||||
other.take_damage(self.damage)
|
|
||||||
return False
|
|
||||||
return False
|
|
||||||
|
|
||||||
def can_see_player(self, player_pos, grid):
|
|
||||||
"""Check if enemy can see the player position"""
|
|
||||||
# Simple check: within 6 tiles and has line of sight
|
|
||||||
mx, my = self.get_position()
|
|
||||||
px, py = player_pos
|
|
||||||
|
|
||||||
dist = abs(px - mx) + abs(py - my)
|
|
||||||
if dist > 6:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Use libtcod for line of sight
|
|
||||||
line = list(mcrfpy.libtcod.line(mx, my, px, py))
|
|
||||||
if len(line) > 7: # Too far
|
|
||||||
return False
|
|
||||||
for x, y in line[1:-1]: # Skip start and end points
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
if cell and not cell.transparent:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def ai_turn(self, grid, player):
|
|
||||||
"""Decide next move"""
|
|
||||||
px, py = player.get_position()
|
|
||||||
mx, my = self.get_position()
|
|
||||||
|
|
||||||
# Simple AI: move toward player if visible
|
|
||||||
if self.can_see_player((px, py), grid):
|
|
||||||
# Calculate direction toward player
|
|
||||||
dx = 0
|
|
||||||
dy = 0
|
|
||||||
if px > mx:
|
|
||||||
dx = 1
|
|
||||||
elif px < mx:
|
|
||||||
dx = -1
|
|
||||||
if py > my:
|
|
||||||
dy = 1
|
|
||||||
elif py < my:
|
|
||||||
dy = -1
|
|
||||||
|
|
||||||
# Prefer cardinal movement
|
|
||||||
if dx != 0 and dy != 0:
|
|
||||||
# Pick horizontal or vertical based on greater distance
|
|
||||||
if abs(px - mx) > abs(py - my):
|
|
||||||
dy = 0
|
|
||||||
else:
|
|
||||||
dx = 0
|
|
||||||
|
|
||||||
return (mx + dx, my + dy)
|
|
||||||
else:
|
|
||||||
# Random movement
|
|
||||||
dx, dy = random.choice([(0,1), (0,-1), (1,0), (-1,0)])
|
|
||||||
return (mx + dx, my + dy)
|
|
||||||
|
|
||||||
class BoulderEntity(GameEntity):
|
|
||||||
"""Pushable boulder"""
|
|
||||||
def __init__(self, x, y, **kwargs):
|
|
||||||
kwargs['sprite_index'] = 7 # Boulder sprite
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
self.entity_type = "boulder"
|
|
||||||
self.pushable = True
|
|
||||||
|
|
||||||
def try_push(self, dx, dy):
|
|
||||||
"""Attempt to push boulder in direction"""
|
|
||||||
new_x = int(self.x) + dx
|
|
||||||
new_y = int(self.y) + dy
|
|
||||||
|
|
||||||
# Check if destination is free
|
|
||||||
if can_move_to(new_x, new_y):
|
|
||||||
self.start_move(new_x, new_y)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
class ButtonEntity(GameEntity):
|
|
||||||
"""Pressure plate that triggers when stepped on"""
|
|
||||||
def __init__(self, x, y, target=None, **kwargs):
|
|
||||||
kwargs['sprite_index'] = 8 # Button sprite
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
self.blocks_movement = False # Can be walked over
|
|
||||||
self.entity_type = "button"
|
|
||||||
self.pressed = False
|
|
||||||
self.pressed_by = set() # Track who's pressing
|
|
||||||
self.target = target # Door or other triggerable
|
|
||||||
|
|
||||||
def on_step(self, other):
|
|
||||||
"""Activate when stepped on"""
|
|
||||||
if other not in self.pressed_by:
|
|
||||||
self.pressed_by.add(other)
|
|
||||||
if not self.pressed:
|
|
||||||
self.pressed = True
|
|
||||||
self.sprite_index = 9 # Pressed sprite
|
|
||||||
if self.target:
|
|
||||||
self.target.activate()
|
|
||||||
|
|
||||||
def on_leave(self, other):
|
|
||||||
"""Deactivate when entity leaves"""
|
|
||||||
if other in self.pressed_by:
|
|
||||||
self.pressed_by.remove(other)
|
|
||||||
if len(self.pressed_by) == 0 and self.pressed:
|
|
||||||
self.pressed = False
|
|
||||||
self.sprite_index = 8 # Unpressed sprite
|
|
||||||
if self.target:
|
|
||||||
self.target.deactivate()
|
|
||||||
|
|
||||||
class DoorEntity(GameEntity):
|
|
||||||
"""Door that can be opened by buttons"""
|
|
||||||
def __init__(self, x, y, **kwargs):
|
|
||||||
kwargs['sprite_index'] = 3 # Closed door sprite
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
self.entity_type = "door"
|
|
||||||
self.is_open = False
|
|
||||||
|
|
||||||
def activate(self):
|
|
||||||
"""Open the door"""
|
|
||||||
self.is_open = True
|
|
||||||
self.blocks_movement = False
|
|
||||||
self.sprite_index = 11 # Open door sprite
|
|
||||||
|
|
||||||
def deactivate(self):
|
|
||||||
"""Close the door"""
|
|
||||||
self.is_open = False
|
|
||||||
self.blocks_movement = True
|
|
||||||
self.sprite_index = 3 # Closed door sprite
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Global Game State
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Create and activate a new scene
|
|
||||||
mcrfpy.createScene("tutorial")
|
|
||||||
mcrfpy.setScene("tutorial")
|
|
||||||
|
|
||||||
# Load the texture
|
|
||||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
|
||||||
|
|
||||||
# Create a grid of tiles
|
|
||||||
grid_width, grid_height = 40, 30
|
|
||||||
zoom = 2.0
|
|
||||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
|
||||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
|
||||||
|
|
||||||
# Create the grid
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
pos=grid_position,
|
|
||||||
grid_size=(grid_width, grid_height),
|
|
||||||
texture=texture,
|
|
||||||
size=grid_size,
|
|
||||||
)
|
|
||||||
grid.zoom = zoom
|
|
||||||
|
|
||||||
# Define tile types
|
|
||||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
|
||||||
WALL_TILES = [3, 7, 11]
|
|
||||||
|
|
||||||
# Game state
|
|
||||||
player = None
|
|
||||||
enemies = []
|
|
||||||
all_entities = []
|
|
||||||
is_player_turn = True
|
|
||||||
move_duration = 0.2
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Dungeon Generation (from Part 3)
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
class Room:
|
|
||||||
def __init__(self, x, y, width, height):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + width
|
|
||||||
self.y2 = y + height
|
|
||||||
|
|
||||||
def center(self):
|
|
||||||
return ((self.x1 + self.x2) // 2, (self.y1 + self.y2) // 2)
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
|
|
||||||
self.y1 <= other.y2 and self.y2 >= other.y1)
|
|
||||||
|
|
||||||
def create_room(room):
|
|
||||||
"""Carve out a room in the grid"""
|
|
||||||
for x in range(room.x1 + 1, room.x2):
|
|
||||||
for y in range(room.y1 + 1, room.y2):
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
if cell:
|
|
||||||
cell.walkable = True
|
|
||||||
cell.transparent = True
|
|
||||||
cell.tilesprite = random.choice(FLOOR_TILES)
|
|
||||||
|
|
||||||
def create_l_shaped_hallway(x1, y1, x2, y2):
|
|
||||||
"""Create L-shaped hallway between two points"""
|
|
||||||
corner_x = x2
|
|
||||||
corner_y = y1
|
|
||||||
|
|
||||||
if random.random() < 0.5:
|
|
||||||
corner_x = x1
|
|
||||||
corner_y = y2
|
|
||||||
|
|
||||||
for x, y in mcrfpy.libtcod.line(x1, y1, corner_x, corner_y):
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
if cell:
|
|
||||||
cell.walkable = True
|
|
||||||
cell.transparent = True
|
|
||||||
cell.tilesprite = random.choice(FLOOR_TILES)
|
|
||||||
|
|
||||||
for x, y in mcrfpy.libtcod.line(corner_x, corner_y, x2, y2):
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
if cell:
|
|
||||||
cell.walkable = True
|
|
||||||
cell.transparent = True
|
|
||||||
cell.tilesprite = random.choice(FLOOR_TILES)
|
|
||||||
|
|
||||||
def generate_dungeon():
|
|
||||||
"""Generate a simple dungeon with rooms and hallways"""
|
|
||||||
# Initialize all cells as walls
|
|
||||||
for x in range(grid_width):
|
|
||||||
for y in range(grid_height):
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
if cell:
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.tilesprite = random.choice(WALL_TILES)
|
|
||||||
|
|
||||||
rooms = []
|
|
||||||
num_rooms = 0
|
|
||||||
|
|
||||||
for _ in range(30):
|
|
||||||
w = random.randint(4, 8)
|
|
||||||
h = random.randint(4, 8)
|
|
||||||
x = random.randint(0, grid_width - w - 1)
|
|
||||||
y = random.randint(0, grid_height - h - 1)
|
|
||||||
|
|
||||||
new_room = Room(x, y, w, h)
|
|
||||||
|
|
||||||
# Check if room intersects with existing rooms
|
|
||||||
if any(new_room.intersects(other_room) for other_room in rooms):
|
|
||||||
continue
|
|
||||||
|
|
||||||
create_room(new_room)
|
|
||||||
|
|
||||||
if num_rooms > 0:
|
|
||||||
# Connect to previous room
|
|
||||||
new_x, new_y = new_room.center()
|
|
||||||
prev_x, prev_y = rooms[num_rooms - 1].center()
|
|
||||||
create_l_shaped_hallway(prev_x, prev_y, new_x, new_y)
|
|
||||||
|
|
||||||
rooms.append(new_room)
|
|
||||||
num_rooms += 1
|
|
||||||
|
|
||||||
return rooms
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Entity Management
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
def get_entities_at(x, y):
|
|
||||||
"""Get all entities at a specific position (including moving ones)"""
|
|
||||||
entities = []
|
|
||||||
for entity in all_entities:
|
|
||||||
ex, ey = entity.get_position()
|
|
||||||
if ex == x and ey == y:
|
|
||||||
entities.append(entity)
|
|
||||||
return entities
|
|
||||||
|
|
||||||
def get_blocking_entity_at(x, y):
|
|
||||||
"""Get the first blocking entity at position"""
|
|
||||||
for entity in get_entities_at(x, y):
|
|
||||||
if entity.blocks_movement:
|
|
||||||
return entity
|
|
||||||
return None
|
|
||||||
|
|
||||||
def can_move_to(x, y):
|
|
||||||
"""Check if a position is valid for movement"""
|
|
||||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
|
||||||
return False
|
|
||||||
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
if not cell or not cell.walkable:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check for blocking entities
|
|
||||||
if get_blocking_entity_at(x, y):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def can_entity_move_to(entity, x, y):
|
|
||||||
"""Check if specific entity can move to position"""
|
|
||||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
|
||||||
return False
|
|
||||||
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
if not cell or not cell.walkable:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check for other blocking entities (not self)
|
|
||||||
blocker = get_blocking_entity_at(x, y)
|
|
||||||
if blocker and blocker != entity:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Turn Management
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
def process_player_move(key):
|
|
||||||
"""Handle player input with immediate response"""
|
|
||||||
global is_player_turn
|
|
||||||
|
|
||||||
if not is_player_turn or player.is_moving:
|
|
||||||
return # Not player's turn or still animating
|
|
||||||
|
|
||||||
px, py = player.get_position()
|
|
||||||
new_x, new_y = px, py
|
|
||||||
|
|
||||||
# Calculate movement direction
|
|
||||||
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
|
|
||||||
else:
|
|
||||||
return # Not a movement key
|
|
||||||
|
|
||||||
if new_x == px and new_y == py:
|
|
||||||
return # No movement
|
|
||||||
|
|
||||||
# Check what's at destination
|
|
||||||
cell = grid.at(new_x, new_y)
|
|
||||||
if not cell or not cell.walkable:
|
|
||||||
return # Can't move into walls
|
|
||||||
|
|
||||||
blocking_entity = get_blocking_entity_at(new_x, new_y)
|
|
||||||
|
|
||||||
if blocking_entity:
|
|
||||||
# Try bump interaction
|
|
||||||
if not player.on_bump(blocking_entity):
|
|
||||||
# Movement blocked, but turn still happens
|
|
||||||
is_player_turn = False
|
|
||||||
mcrfpy.setTimer("enemy_turn", process_enemy_turns, 50)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Movement is valid - start player animation
|
|
||||||
is_player_turn = False
|
|
||||||
player.start_move(new_x, new_y, duration=move_duration, callback=player_move_complete)
|
|
||||||
|
|
||||||
# Update grid center to follow player
|
|
||||||
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, move_duration, "linear")
|
|
||||||
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, move_duration, "linear")
|
|
||||||
grid_anim_x.start(grid)
|
|
||||||
grid_anim_y.start(grid)
|
|
||||||
|
|
||||||
# Start enemy turns after a short delay (so player sees their move start first)
|
|
||||||
mcrfpy.setTimer("enemy_turn", process_enemy_turns, 50)
|
|
||||||
|
|
||||||
def process_enemy_turns(timer_name):
|
|
||||||
"""Process all enemy AI decisions and start their animations"""
|
|
||||||
enemies_to_move = []
|
|
||||||
|
|
||||||
for enemy in enemies:
|
|
||||||
if enemy.hp <= 0: # Skip dead enemies
|
|
||||||
continue
|
|
||||||
|
|
||||||
if enemy.is_moving:
|
|
||||||
continue # Skip if still animating
|
|
||||||
|
|
||||||
# AI decides next move based on player's destination
|
|
||||||
target_x, target_y = enemy.ai_turn(grid, player)
|
|
||||||
|
|
||||||
# Check if move is valid
|
|
||||||
cell = grid.at(target_x, target_y)
|
|
||||||
if not cell or not cell.walkable:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check what's at the destination
|
|
||||||
blocking_entity = get_blocking_entity_at(target_x, target_y)
|
|
||||||
|
|
||||||
if blocking_entity and blocking_entity != enemy:
|
|
||||||
# Try bump interaction
|
|
||||||
enemy.on_bump(blocking_entity)
|
|
||||||
# Enemy doesn't move but still took its turn
|
|
||||||
else:
|
|
||||||
# Valid move - add to list
|
|
||||||
enemies_to_move.append((enemy, target_x, target_y))
|
|
||||||
|
|
||||||
# Start all enemy animations simultaneously
|
|
||||||
for enemy, tx, ty in enemies_to_move:
|
|
||||||
enemy.start_move(tx, ty, duration=move_duration)
|
|
||||||
|
|
||||||
def player_move_complete(anim, entity):
|
|
||||||
"""Called when player animation finishes"""
|
|
||||||
global is_player_turn
|
|
||||||
|
|
||||||
player.is_moving = False
|
|
||||||
|
|
||||||
# Check for step-on interactions at new position
|
|
||||||
for entity in get_entities_at(int(player.x), int(player.y)):
|
|
||||||
if entity != player and not entity.blocks_movement:
|
|
||||||
entity.on_step(player)
|
|
||||||
|
|
||||||
# Update FOV from new position
|
|
||||||
update_fov()
|
|
||||||
|
|
||||||
# Player's turn is ready again
|
|
||||||
is_player_turn = True
|
|
||||||
|
|
||||||
def update_fov():
|
|
||||||
"""Update field of view from player position"""
|
|
||||||
px, py = int(player.x), int(player.y)
|
|
||||||
grid.compute_fov(px, py, radius=8)
|
|
||||||
player.update_visibility()
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Input Handling
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
def handle_keys(key, state):
|
|
||||||
"""Handle keyboard input"""
|
|
||||||
if state == "start":
|
|
||||||
# Movement keys
|
|
||||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
|
||||||
process_player_move(key)
|
|
||||||
|
|
||||||
# Register the key handler
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Initialize Game
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Generate dungeon
|
|
||||||
rooms = generate_dungeon()
|
|
||||||
|
|
||||||
# Place player in first room
|
|
||||||
if rooms:
|
|
||||||
start_x, start_y = rooms[0].center()
|
|
||||||
player = PlayerEntity(start_x, start_y, grid=grid)
|
|
||||||
all_entities.append(player)
|
|
||||||
|
|
||||||
# Place enemies in other rooms
|
|
||||||
for i in range(1, min(6, len(rooms))):
|
|
||||||
room = rooms[i]
|
|
||||||
ex, ey = room.center()
|
|
||||||
enemy = EnemyEntity(ex, ey, grid=grid)
|
|
||||||
enemies.append(enemy)
|
|
||||||
all_entities.append(enemy)
|
|
||||||
|
|
||||||
# Place some boulders
|
|
||||||
for i in range(3):
|
|
||||||
room = random.choice(rooms[1:])
|
|
||||||
bx = random.randint(room.x1 + 1, room.x2 - 1)
|
|
||||||
by = random.randint(room.y1 + 1, room.y2 - 1)
|
|
||||||
if can_move_to(bx, by):
|
|
||||||
boulder = BoulderEntity(bx, by, grid=grid)
|
|
||||||
all_entities.append(boulder)
|
|
||||||
|
|
||||||
# Place a button and door in one of the rooms
|
|
||||||
if len(rooms) > 2:
|
|
||||||
button_room = rooms[-2]
|
|
||||||
door_room = rooms[-1]
|
|
||||||
|
|
||||||
# Place door at entrance to last room
|
|
||||||
dx, dy = door_room.center()
|
|
||||||
door = DoorEntity(dx, door_room.y1, grid=grid)
|
|
||||||
all_entities.append(door)
|
|
||||||
|
|
||||||
# Place button in second to last room
|
|
||||||
bx, by = button_room.center()
|
|
||||||
button = ButtonEntity(bx, by, target=door, grid=grid)
|
|
||||||
all_entities.append(button)
|
|
||||||
|
|
||||||
# Set grid perspective to player
|
|
||||||
grid.perspective = player
|
|
||||||
grid.center_x = (start_x + 0.5) * 16
|
|
||||||
grid.center_y = (start_y + 0.5) * 16
|
|
||||||
|
|
||||||
# Initial FOV calculation
|
|
||||||
update_fov()
|
|
||||||
|
|
||||||
# Add grid to scene
|
|
||||||
mcrfpy.sceneUI("tutorial").append(grid)
|
|
||||||
|
|
||||||
# Show instructions
|
|
||||||
title = mcrfpy.Caption((320, 10),
|
|
||||||
text="Part 5: Entity Interactions - WASD to move, bump enemies, push boulders!",
|
|
||||||
)
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(title)
|
|
||||||
|
|
||||||
print("Part 5: Entity Interactions - Tutorial loaded!")
|
|
||||||
print("- Bump into enemies to attack them")
|
|
||||||
print("- Push boulders by walking into them")
|
|
||||||
print("- Step on buttons to open doors")
|
|
||||||
print("- Enemies will pursue you when they can see you")
|
|
||||||
|
|
@ -1,253 +0,0 @@
|
||||||
# Part 0 - Setting Up McRogueFace
|
|
||||||
|
|
||||||
Welcome to the McRogueFace Roguelike Tutorial! This tutorial will teach you how to create a complete roguelike game using the McRogueFace game engine. Unlike traditional Python libraries, McRogueFace is a complete, portable game engine that includes everything you need to make and distribute games.
|
|
||||||
|
|
||||||
## What is McRogueFace?
|
|
||||||
|
|
||||||
McRogueFace is a high-performance game engine with Python scripting support. Think of it like Unity or Godot, but specifically designed for roguelikes and 2D games. It includes:
|
|
||||||
|
|
||||||
- A complete Python 3.12 runtime (no installation needed!)
|
|
||||||
- High-performance C++ rendering and entity management
|
|
||||||
- Built-in UI components and scene management
|
|
||||||
- Integrated audio system
|
|
||||||
- Professional sprite-based graphics
|
|
||||||
- Easy distribution - your players don't need Python installed!
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Before starting this tutorial, you should:
|
|
||||||
|
|
||||||
- Have basic Python knowledge (variables, functions, classes)
|
|
||||||
- Be comfortable editing text files
|
|
||||||
- Have a text editor (VS Code, Sublime Text, Notepad++, etc.)
|
|
||||||
|
|
||||||
That's it! Unlike other roguelike tutorials, you don't need Python installed - McRogueFace includes everything.
|
|
||||||
|
|
||||||
## Getting McRogueFace
|
|
||||||
|
|
||||||
### Step 1: Download the Engine
|
|
||||||
|
|
||||||
1. Visit the McRogueFace releases page
|
|
||||||
2. Download the version for your operating system:
|
|
||||||
- `McRogueFace-Windows.zip` for Windows
|
|
||||||
- `McRogueFace-MacOS.zip` for macOS
|
|
||||||
- `McRogueFace-Linux.zip` for Linux
|
|
||||||
|
|
||||||
### Step 2: Extract the Archive
|
|
||||||
|
|
||||||
Extract the downloaded archive to a folder where you want to develop your game. You should see this structure:
|
|
||||||
|
|
||||||
```
|
|
||||||
McRogueFace/
|
|
||||||
├── mcrogueface (or mcrogueface.exe on Windows)
|
|
||||||
├── scripts/
|
|
||||||
│ └── game.py
|
|
||||||
├── assets/
|
|
||||||
│ ├── sprites/
|
|
||||||
│ ├── fonts/
|
|
||||||
│ └── audio/
|
|
||||||
└── lib/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Run the Engine
|
|
||||||
|
|
||||||
Run the McRogueFace executable:
|
|
||||||
|
|
||||||
- **Windows**: Double-click `mcrogueface.exe`
|
|
||||||
- **Mac/Linux**: Open a terminal in the folder and run `./mcrogueface`
|
|
||||||
|
|
||||||
You should see a window open with the default McRogueFace demo. This shows the engine is working correctly!
|
|
||||||
|
|
||||||
## Your First McRogueFace Script
|
|
||||||
|
|
||||||
Let's modify the engine to display "Hello Roguelike!" instead of the default demo.
|
|
||||||
|
|
||||||
### Step 1: Open game.py
|
|
||||||
|
|
||||||
Open `scripts/game.py` in your text editor. You'll see the default demo code. Replace it entirely with:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Create a new scene called "hello"
|
|
||||||
mcrfpy.createScene("hello")
|
|
||||||
|
|
||||||
# Switch to our new scene
|
|
||||||
mcrfpy.setScene("hello")
|
|
||||||
|
|
||||||
# Get the UI container for our scene
|
|
||||||
ui = mcrfpy.sceneUI("hello")
|
|
||||||
|
|
||||||
# Create a text caption
|
|
||||||
caption = mcrfpy.Caption("Hello Roguelike!", 400, 300)
|
|
||||||
caption.font_size = 32
|
|
||||||
caption.fill_color = mcrfpy.Color(255, 255, 255) # White text
|
|
||||||
|
|
||||||
# Add the caption to our scene
|
|
||||||
ui.append(caption)
|
|
||||||
|
|
||||||
# Create a smaller instruction caption
|
|
||||||
instruction = mcrfpy.Caption("Press ESC to exit", 400, 350)
|
|
||||||
instruction.font_size = 16
|
|
||||||
instruction.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
|
|
||||||
ui.append(instruction)
|
|
||||||
|
|
||||||
# Set up a simple key handler
|
|
||||||
def handle_keys(key, state):
|
|
||||||
if state == "start" and key == "Escape":
|
|
||||||
mcrfpy.setScene(None) # This exits the game
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
print("Hello Roguelike is running!")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Save and Run
|
|
||||||
|
|
||||||
1. Save the file
|
|
||||||
2. If McRogueFace is still running, it will automatically reload!
|
|
||||||
3. If not, run the engine again
|
|
||||||
|
|
||||||
You should now see "Hello Roguelike!" displayed in the window.
|
|
||||||
|
|
||||||
### Step 3: Understanding the Code
|
|
||||||
|
|
||||||
Let's break down what we just wrote:
|
|
||||||
|
|
||||||
1. **Import mcrfpy**: This is McRogueFace's Python API
|
|
||||||
2. **Create a scene**: Scenes are like game states (menu, gameplay, inventory, etc.)
|
|
||||||
3. **UI elements**: We create Caption objects for text display
|
|
||||||
4. **Colors**: McRogueFace uses RGB colors (0-255 for each component)
|
|
||||||
5. **Input handling**: We set up a callback for keyboard input
|
|
||||||
6. **Scene switching**: Setting the scene to None exits the game
|
|
||||||
|
|
||||||
## Key Differences from Pure Python Development
|
|
||||||
|
|
||||||
### The Game Loop
|
|
||||||
|
|
||||||
Unlike typical Python scripts, McRogueFace runs your code inside its game loop:
|
|
||||||
|
|
||||||
1. The engine starts and loads `scripts/game.py`
|
|
||||||
2. Your script sets up scenes, UI elements, and callbacks
|
|
||||||
3. The engine runs at 60 FPS, handling rendering and input
|
|
||||||
4. Your callbacks are triggered by game events
|
|
||||||
|
|
||||||
### Hot Reloading
|
|
||||||
|
|
||||||
McRogueFace can reload your scripts while running! Just save your changes and the engine will reload automatically. This makes development incredibly fast.
|
|
||||||
|
|
||||||
### Asset Pipeline
|
|
||||||
|
|
||||||
McRogueFace includes a complete asset system:
|
|
||||||
|
|
||||||
- **Sprites**: Place images in `assets/sprites/`
|
|
||||||
- **Fonts**: TrueType fonts in `assets/fonts/`
|
|
||||||
- **Audio**: Sound effects and music in `assets/audio/`
|
|
||||||
|
|
||||||
We'll explore these in later lessons.
|
|
||||||
|
|
||||||
## Testing Your Setup
|
|
||||||
|
|
||||||
Let's create a more interactive test to ensure everything is working properly:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Create our test scene
|
|
||||||
mcrfpy.createScene("test")
|
|
||||||
mcrfpy.setScene("test")
|
|
||||||
ui = mcrfpy.sceneUI("test")
|
|
||||||
|
|
||||||
# Create a background frame
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(20, 20, 30) # Dark blue-gray
|
|
||||||
ui.append(background)
|
|
||||||
|
|
||||||
# Title text
|
|
||||||
title = mcrfpy.Caption("McRogueFace Setup Test", 512, 100)
|
|
||||||
title.font_size = 36
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
|
|
||||||
ui.append(title)
|
|
||||||
|
|
||||||
# Status text that will update
|
|
||||||
status_text = mcrfpy.Caption("Press any key to test input...", 512, 300)
|
|
||||||
status_text.font_size = 20
|
|
||||||
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
ui.append(status_text)
|
|
||||||
|
|
||||||
# Instructions
|
|
||||||
instructions = [
|
|
||||||
"Arrow Keys: Test movement input",
|
|
||||||
"Space: Test action input",
|
|
||||||
"Mouse Click: Test mouse input",
|
|
||||||
"ESC: Exit"
|
|
||||||
]
|
|
||||||
|
|
||||||
y_offset = 400
|
|
||||||
for instruction in instructions:
|
|
||||||
inst_caption = mcrfpy.Caption(instruction, 512, y_offset)
|
|
||||||
inst_caption.font_size = 16
|
|
||||||
inst_caption.fill_color = mcrfpy.Color(150, 150, 150)
|
|
||||||
ui.append(inst_caption)
|
|
||||||
y_offset += 30
|
|
||||||
|
|
||||||
# Input handler
|
|
||||||
def handle_input(key, state):
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
if key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
else:
|
|
||||||
status_text.text = f"You pressed: {key}"
|
|
||||||
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green
|
|
||||||
|
|
||||||
# Set up input handling
|
|
||||||
mcrfpy.keypressScene(handle_input)
|
|
||||||
|
|
||||||
print("Setup test is running! Try pressing different keys.")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Engine Won't Start
|
|
||||||
|
|
||||||
- **Windows**: Make sure you extracted all files, not just the .exe
|
|
||||||
- **Mac**: You may need to right-click and select "Open" the first time
|
|
||||||
- **Linux**: Make sure the file is executable: `chmod +x mcrogueface`
|
|
||||||
|
|
||||||
### Scripts Not Loading
|
|
||||||
|
|
||||||
- Ensure your script is named exactly `game.py` in the `scripts/` folder
|
|
||||||
- Check the console output for Python errors
|
|
||||||
- Make sure you're using Python 3 syntax
|
|
||||||
|
|
||||||
### Performance Issues
|
|
||||||
|
|
||||||
- McRogueFace should run smoothly at 60 FPS
|
|
||||||
- If not, check if your graphics drivers are updated
|
|
||||||
- The engine shows FPS in the window title
|
|
||||||
|
|
||||||
## What's Next?
|
|
||||||
|
|
||||||
Congratulations! You now have McRogueFace set up and running. You've learned:
|
|
||||||
|
|
||||||
- How to download and run the McRogueFace engine
|
|
||||||
- The basic structure of a McRogueFace project
|
|
||||||
- How to create scenes and UI elements
|
|
||||||
- How to handle keyboard input
|
|
||||||
- The development workflow with hot reloading
|
|
||||||
|
|
||||||
In Part 1, we'll create our player character and implement movement. We'll explore McRogueFace's entity system and learn how to create a game world.
|
|
||||||
|
|
||||||
## Why McRogueFace?
|
|
||||||
|
|
||||||
Before we continue, let's highlight why McRogueFace is excellent for roguelike development:
|
|
||||||
|
|
||||||
1. **No Installation Hassles**: Your players just download and run - no Python needed!
|
|
||||||
2. **Professional Performance**: C++ engine core means smooth gameplay even with hundreds of entities
|
|
||||||
3. **Built-in Features**: UI, audio, scenes, and animations are already there
|
|
||||||
4. **Easy Distribution**: Just zip your game folder and share it
|
|
||||||
5. **Rapid Development**: Hot reloading and Python scripting for quick iteration
|
|
||||||
|
|
||||||
Ready to make a roguelike? Let's continue to Part 1!
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Create a new scene called "hello"
|
|
||||||
mcrfpy.createScene("hello")
|
|
||||||
|
|
||||||
# Switch to our new scene
|
|
||||||
mcrfpy.setScene("hello")
|
|
||||||
|
|
||||||
# Get the UI container for our scene
|
|
||||||
ui = mcrfpy.sceneUI("hello")
|
|
||||||
|
|
||||||
# Create a text caption
|
|
||||||
caption = mcrfpy.Caption("Hello Roguelike!", 400, 300)
|
|
||||||
caption.font_size = 32
|
|
||||||
caption.fill_color = mcrfpy.Color(255, 255, 255) # White text
|
|
||||||
|
|
||||||
# Add the caption to our scene
|
|
||||||
ui.append(caption)
|
|
||||||
|
|
||||||
# Create a smaller instruction caption
|
|
||||||
instruction = mcrfpy.Caption("Press ESC to exit", 400, 350)
|
|
||||||
instruction.font_size = 16
|
|
||||||
instruction.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
|
|
||||||
ui.append(instruction)
|
|
||||||
|
|
||||||
# Set up a simple key handler
|
|
||||||
def handle_keys(key, state):
|
|
||||||
if state == "start" and key == "Escape":
|
|
||||||
mcrfpy.setScene(None) # This exits the game
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
print("Hello Roguelike is running!")
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Create our test scene
|
|
||||||
mcrfpy.createScene("test")
|
|
||||||
mcrfpy.setScene("test")
|
|
||||||
ui = mcrfpy.sceneUI("test")
|
|
||||||
|
|
||||||
# Create a background frame
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(20, 20, 30) # Dark blue-gray
|
|
||||||
ui.append(background)
|
|
||||||
|
|
||||||
# Title text
|
|
||||||
title = mcrfpy.Caption("McRogueFace Setup Test", 512, 100)
|
|
||||||
title.font_size = 36
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
|
|
||||||
ui.append(title)
|
|
||||||
|
|
||||||
# Status text that will update
|
|
||||||
status_text = mcrfpy.Caption("Press any key to test input...", 512, 300)
|
|
||||||
status_text.font_size = 20
|
|
||||||
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
ui.append(status_text)
|
|
||||||
|
|
||||||
# Instructions
|
|
||||||
instructions = [
|
|
||||||
"Arrow Keys: Test movement input",
|
|
||||||
"Space: Test action input",
|
|
||||||
"Mouse Click: Test mouse input",
|
|
||||||
"ESC: Exit"
|
|
||||||
]
|
|
||||||
|
|
||||||
y_offset = 400
|
|
||||||
for instruction in instructions:
|
|
||||||
inst_caption = mcrfpy.Caption(instruction, 512, y_offset)
|
|
||||||
inst_caption.font_size = 16
|
|
||||||
inst_caption.fill_color = mcrfpy.Color(150, 150, 150)
|
|
||||||
ui.append(inst_caption)
|
|
||||||
y_offset += 30
|
|
||||||
|
|
||||||
# Input handler
|
|
||||||
def handle_input(key, state):
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
if key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
else:
|
|
||||||
status_text.text = f"You pressed: {key}"
|
|
||||||
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green
|
|
||||||
|
|
||||||
# Set up input handling
|
|
||||||
mcrfpy.keypressScene(handle_input)
|
|
||||||
|
|
||||||
print("Setup test is running! Try pressing different keys.")
|
|
||||||
|
|
@ -1,457 +0,0 @@
|
||||||
# Part 1 - Drawing the '@' Symbol and Moving It Around
|
|
||||||
|
|
||||||
In Part 0, we set up McRogueFace and created a simple "Hello Roguelike" scene. Now it's time to create the foundation of our game: a player character that can move around the screen.
|
|
||||||
|
|
||||||
In traditional roguelikes, the player is represented by the '@' symbol. We'll honor that tradition while taking advantage of McRogueFace's powerful sprite-based rendering system.
|
|
||||||
|
|
||||||
## Understanding McRogueFace's Architecture
|
|
||||||
|
|
||||||
Before we dive into code, let's understand two key concepts in McRogueFace:
|
|
||||||
|
|
||||||
### Grid - The Game World
|
|
||||||
|
|
||||||
A `Grid` represents your game world. It's a 2D array of tiles where each tile can be:
|
|
||||||
- **Walkable or not** (for collision detection)
|
|
||||||
- **Transparent or not** (for field of view, which we'll cover later)
|
|
||||||
- **Have a visual appearance** (sprite index and color)
|
|
||||||
|
|
||||||
Think of the Grid as the dungeon floor, walls, and other static elements.
|
|
||||||
|
|
||||||
### Entity - Things That Move
|
|
||||||
|
|
||||||
An `Entity` represents anything that can move around on the Grid:
|
|
||||||
- The player character
|
|
||||||
- Monsters
|
|
||||||
- Items (if you want them to be thrown or moved)
|
|
||||||
- Projectiles
|
|
||||||
|
|
||||||
Entities exist "on top of" the Grid and automatically handle smooth movement animation between tiles.
|
|
||||||
|
|
||||||
## Creating Our Game World
|
|
||||||
|
|
||||||
Let's start by creating a simple room for our player to move around in. Create a new `game.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Define some constants for our tile types
|
|
||||||
FLOOR_TILE = 0
|
|
||||||
WALL_TILE = 1
|
|
||||||
PLAYER_SPRITE = 2
|
|
||||||
|
|
||||||
# Window configuration
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
# Configure window properties
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 1"
|
|
||||||
|
|
||||||
# Get the UI container for our scene
|
|
||||||
ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
# Create a dark background
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
ui.append(background)
|
|
||||||
```
|
|
||||||
|
|
||||||
Now we need to set up our tileset. For this tutorial, we'll use ASCII-style sprites. McRogueFace comes with a built-in ASCII tileset:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Load the ASCII tileset
|
|
||||||
# This tileset has characters mapped to sprite indices
|
|
||||||
# For example: @ = 64, # = 35, . = 46
|
|
||||||
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
# Create the game grid
|
|
||||||
# 50x30 tiles is a good size for a roguelike
|
|
||||||
GRID_WIDTH = 50
|
|
||||||
GRID_HEIGHT = 30
|
|
||||||
|
|
||||||
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
|
|
||||||
grid.position = (100, 100) # Position on screen
|
|
||||||
grid.size = (800, 480) # Size in pixels
|
|
||||||
|
|
||||||
# Add the grid to our UI
|
|
||||||
ui.append(grid)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Initializing the Game World
|
|
||||||
|
|
||||||
Now let's fill our grid with a simple room:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def create_room():
|
|
||||||
"""Create a room with walls around the edges"""
|
|
||||||
# Fill everything with floor tiles first
|
|
||||||
for y in range(GRID_HEIGHT):
|
|
||||||
for x in range(GRID_WIDTH):
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
cell.walkable = True
|
|
||||||
cell.transparent = True
|
|
||||||
cell.sprite_index = 46 # '.' character
|
|
||||||
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
|
|
||||||
|
|
||||||
# Create walls around the edges
|
|
||||||
for x in range(GRID_WIDTH):
|
|
||||||
# Top wall
|
|
||||||
cell = grid.at(x, 0)
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.sprite_index = 35 # '#' character
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
|
|
||||||
|
|
||||||
# Bottom wall
|
|
||||||
cell = grid.at(x, GRID_HEIGHT - 1)
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.sprite_index = 35 # '#' character
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100)
|
|
||||||
|
|
||||||
for y in range(GRID_HEIGHT):
|
|
||||||
# Left wall
|
|
||||||
cell = grid.at(0, y)
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.sprite_index = 35 # '#' character
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100)
|
|
||||||
|
|
||||||
# Right wall
|
|
||||||
cell = grid.at(GRID_WIDTH - 1, y)
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.sprite_index = 35 # '#' character
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100)
|
|
||||||
|
|
||||||
# Create the room
|
|
||||||
create_room()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Creating the Player
|
|
||||||
|
|
||||||
Now let's add our player character:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Create the player entity
|
|
||||||
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
|
|
||||||
player.sprite_index = 64 # '@' character
|
|
||||||
player.color = mcrfpy.Color(255, 255, 255) # White
|
|
||||||
|
|
||||||
# The entity is automatically added to the grid when we pass grid= parameter
|
|
||||||
# This is equivalent to: grid.entities.append(player)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Handling Input
|
|
||||||
|
|
||||||
McRogueFace uses a callback system for input. For a turn-based roguelike, we only care about key presses, not releases:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def handle_input(key, state):
|
|
||||||
"""Handle keyboard input for player movement"""
|
|
||||||
# Only process key presses, not releases
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
# Movement deltas
|
|
||||||
dx, dy = 0, 0
|
|
||||||
|
|
||||||
# Arrow keys
|
|
||||||
if key == "Up":
|
|
||||||
dy = -1
|
|
||||||
elif key == "Down":
|
|
||||||
dy = 1
|
|
||||||
elif key == "Left":
|
|
||||||
dx = -1
|
|
||||||
elif key == "Right":
|
|
||||||
dx = 1
|
|
||||||
|
|
||||||
# Numpad movement (for true roguelike feel!)
|
|
||||||
elif key == "Num7": # Northwest
|
|
||||||
dx, dy = -1, -1
|
|
||||||
elif key == "Num8": # North
|
|
||||||
dy = -1
|
|
||||||
elif key == "Num9": # Northeast
|
|
||||||
dx, dy = 1, -1
|
|
||||||
elif key == "Num4": # West
|
|
||||||
dx = -1
|
|
||||||
elif key == "Num6": # East
|
|
||||||
dx = 1
|
|
||||||
elif key == "Num1": # Southwest
|
|
||||||
dx, dy = -1, 1
|
|
||||||
elif key == "Num2": # South
|
|
||||||
dy = 1
|
|
||||||
elif key == "Num3": # Southeast
|
|
||||||
dx, dy = 1, 1
|
|
||||||
|
|
||||||
# Escape to quit
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
return
|
|
||||||
|
|
||||||
# If there's movement, try to move the player
|
|
||||||
if dx != 0 or dy != 0:
|
|
||||||
move_player(dx, dy)
|
|
||||||
|
|
||||||
# Register the input handler
|
|
||||||
mcrfpy.keypressScene(handle_input)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementing Movement with Collision Detection
|
|
||||||
|
|
||||||
Now let's implement the movement function with proper collision detection:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def move_player(dx, dy):
|
|
||||||
"""Move the player if the destination is walkable"""
|
|
||||||
# Calculate new position
|
|
||||||
new_x = player.x + dx
|
|
||||||
new_y = player.y + dy
|
|
||||||
|
|
||||||
# Check bounds
|
|
||||||
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check if the destination is walkable
|
|
||||||
destination = grid.at(new_x, new_y)
|
|
||||||
if destination.walkable:
|
|
||||||
# Move the player
|
|
||||||
player.x = new_x
|
|
||||||
player.y = new_y
|
|
||||||
# The entity will automatically animate to the new position!
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding Visual Polish
|
|
||||||
|
|
||||||
Let's add some UI elements to make our game look more polished:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Add a title
|
|
||||||
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
|
|
||||||
ui.append(title)
|
|
||||||
|
|
||||||
# Add instructions
|
|
||||||
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
|
|
||||||
ui.append(instructions)
|
|
||||||
|
|
||||||
# Add a status line at the bottom
|
|
||||||
status = mcrfpy.Caption("@ You", 100, 600)
|
|
||||||
status.font_size = 18
|
|
||||||
status.fill_color = mcrfpy.Color(255, 255, 255)
|
|
||||||
ui.append(status)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Complete Code
|
|
||||||
|
|
||||||
Here's the complete `game.py` for Part 1:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Window configuration
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 1"
|
|
||||||
|
|
||||||
# Get the UI container for our scene
|
|
||||||
ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
# Create a dark background
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
ui.append(background)
|
|
||||||
|
|
||||||
# Load the ASCII tileset
|
|
||||||
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
# Create the game grid
|
|
||||||
GRID_WIDTH = 50
|
|
||||||
GRID_HEIGHT = 30
|
|
||||||
|
|
||||||
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
|
|
||||||
grid.position = (100, 100)
|
|
||||||
grid.size = (800, 480)
|
|
||||||
ui.append(grid)
|
|
||||||
|
|
||||||
def create_room():
|
|
||||||
"""Create a room with walls around the edges"""
|
|
||||||
# Fill everything with floor tiles first
|
|
||||||
for y in range(GRID_HEIGHT):
|
|
||||||
for x in range(GRID_WIDTH):
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
cell.walkable = True
|
|
||||||
cell.transparent = True
|
|
||||||
cell.sprite_index = 46 # '.' character
|
|
||||||
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
|
|
||||||
|
|
||||||
# Create walls around the edges
|
|
||||||
for x in range(GRID_WIDTH):
|
|
||||||
# Top wall
|
|
||||||
cell = grid.at(x, 0)
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.sprite_index = 35 # '#' character
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
|
|
||||||
|
|
||||||
# Bottom wall
|
|
||||||
cell = grid.at(x, GRID_HEIGHT - 1)
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.sprite_index = 35 # '#' character
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100)
|
|
||||||
|
|
||||||
for y in range(GRID_HEIGHT):
|
|
||||||
# Left wall
|
|
||||||
cell = grid.at(0, y)
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.sprite_index = 35 # '#' character
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100)
|
|
||||||
|
|
||||||
# Right wall
|
|
||||||
cell = grid.at(GRID_WIDTH - 1, y)
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.sprite_index = 35 # '#' character
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100)
|
|
||||||
|
|
||||||
# Create the room
|
|
||||||
create_room()
|
|
||||||
|
|
||||||
# Create the player entity
|
|
||||||
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
|
|
||||||
player.sprite_index = 64 # '@' character
|
|
||||||
player.color = mcrfpy.Color(255, 255, 255) # White
|
|
||||||
|
|
||||||
def move_player(dx, dy):
|
|
||||||
"""Move the player if the destination is walkable"""
|
|
||||||
# Calculate new position
|
|
||||||
new_x = player.x + dx
|
|
||||||
new_y = player.y + dy
|
|
||||||
|
|
||||||
# Check bounds
|
|
||||||
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check if the destination is walkable
|
|
||||||
destination = grid.at(new_x, new_y)
|
|
||||||
if destination.walkable:
|
|
||||||
# Move the player
|
|
||||||
player.x = new_x
|
|
||||||
player.y = new_y
|
|
||||||
|
|
||||||
def handle_input(key, state):
|
|
||||||
"""Handle keyboard input for player movement"""
|
|
||||||
# Only process key presses, not releases
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
# Movement deltas
|
|
||||||
dx, dy = 0, 0
|
|
||||||
|
|
||||||
# Arrow keys
|
|
||||||
if key == "Up":
|
|
||||||
dy = -1
|
|
||||||
elif key == "Down":
|
|
||||||
dy = 1
|
|
||||||
elif key == "Left":
|
|
||||||
dx = -1
|
|
||||||
elif key == "Right":
|
|
||||||
dx = 1
|
|
||||||
|
|
||||||
# Numpad movement (for true roguelike feel!)
|
|
||||||
elif key == "Num7": # Northwest
|
|
||||||
dx, dy = -1, -1
|
|
||||||
elif key == "Num8": # North
|
|
||||||
dy = -1
|
|
||||||
elif key == "Num9": # Northeast
|
|
||||||
dx, dy = 1, -1
|
|
||||||
elif key == "Num4": # West
|
|
||||||
dx = -1
|
|
||||||
elif key == "Num6": # East
|
|
||||||
dx = 1
|
|
||||||
elif key == "Num1": # Southwest
|
|
||||||
dx, dy = -1, 1
|
|
||||||
elif key == "Num2": # South
|
|
||||||
dy = 1
|
|
||||||
elif key == "Num3": # Southeast
|
|
||||||
dx, dy = 1, 1
|
|
||||||
|
|
||||||
# Escape to quit
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
return
|
|
||||||
|
|
||||||
# If there's movement, try to move the player
|
|
||||||
if dx != 0 or dy != 0:
|
|
||||||
move_player(dx, dy)
|
|
||||||
|
|
||||||
# Register the input handler
|
|
||||||
mcrfpy.keypressScene(handle_input)
|
|
||||||
|
|
||||||
# Add UI elements
|
|
||||||
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
|
||||||
ui.append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
ui.append(instructions)
|
|
||||||
|
|
||||||
status = mcrfpy.Caption("@ You", 100, 600)
|
|
||||||
status.font_size = 18
|
|
||||||
status.fill_color = mcrfpy.Color(255, 255, 255)
|
|
||||||
ui.append(status)
|
|
||||||
|
|
||||||
print("Part 1: The @ symbol moves!")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Understanding What We've Built
|
|
||||||
|
|
||||||
Let's review the key concepts we've implemented:
|
|
||||||
|
|
||||||
1. **Grid-Entity Architecture**: The Grid represents our static world (floors and walls), while the Entity (player) moves on top of it.
|
|
||||||
|
|
||||||
2. **Collision Detection**: By checking the `walkable` property of grid cells, we prevent the player from walking through walls.
|
|
||||||
|
|
||||||
3. **Turn-Based Input**: By only responding to key presses (not releases), we've created true turn-based movement.
|
|
||||||
|
|
||||||
4. **Visual Feedback**: The Entity system automatically animates movement between tiles, giving smooth visual feedback.
|
|
||||||
|
|
||||||
## Exercises
|
|
||||||
|
|
||||||
Try these modifications to deepen your understanding:
|
|
||||||
|
|
||||||
1. **Add More Rooms**: Create multiple rooms connected by corridors
|
|
||||||
2. **Different Tile Types**: Add doors (walkable but different appearance)
|
|
||||||
3. **Sprint Movement**: Hold Shift to move multiple tiles at once
|
|
||||||
4. **Mouse Support**: Click a tile to pathfind to it (we'll cover pathfinding properly later)
|
|
||||||
|
|
||||||
## ASCII Sprite Reference
|
|
||||||
|
|
||||||
Here are some useful ASCII character indices for the default tileset:
|
|
||||||
- @ (player): 64
|
|
||||||
- # (wall): 35
|
|
||||||
- . (floor): 46
|
|
||||||
- + (door): 43
|
|
||||||
- ~ (water): 126
|
|
||||||
- % (item): 37
|
|
||||||
- ! (potion): 33
|
|
||||||
|
|
||||||
## What's Next?
|
|
||||||
|
|
||||||
In Part 2, we'll expand our world with:
|
|
||||||
- A proper Entity system for managing multiple objects
|
|
||||||
- NPCs that can also move around
|
|
||||||
- A more interesting map layout
|
|
||||||
- The beginning of our game architecture
|
|
||||||
|
|
||||||
The foundation is set - you have a player character that can move around a world with collision detection. This is the core of any roguelike game!
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Window configuration
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 1"
|
|
||||||
|
|
||||||
# Get the UI container for our scene
|
|
||||||
ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
# Create a dark background
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
ui.append(background)
|
|
||||||
|
|
||||||
# Load the ASCII tileset
|
|
||||||
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
# Create the game grid
|
|
||||||
GRID_WIDTH = 50
|
|
||||||
GRID_HEIGHT = 30
|
|
||||||
|
|
||||||
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
|
|
||||||
grid.position = (100, 100)
|
|
||||||
grid.size = (800, 480)
|
|
||||||
ui.append(grid)
|
|
||||||
|
|
||||||
def create_room():
|
|
||||||
"""Create a room with walls around the edges"""
|
|
||||||
# Fill everything with floor tiles first
|
|
||||||
for y in range(GRID_HEIGHT):
|
|
||||||
for x in range(GRID_WIDTH):
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
cell.walkable = True
|
|
||||||
cell.transparent = True
|
|
||||||
cell.sprite_index = 46 # '.' character
|
|
||||||
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
|
|
||||||
|
|
||||||
# Create walls around the edges
|
|
||||||
for x in range(GRID_WIDTH):
|
|
||||||
# Top wall
|
|
||||||
cell = grid.at(x, 0)
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.sprite_index = 35 # '#' character
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
|
|
||||||
|
|
||||||
# Bottom wall
|
|
||||||
cell = grid.at(x, GRID_HEIGHT - 1)
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.sprite_index = 35 # '#' character
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100)
|
|
||||||
|
|
||||||
for y in range(GRID_HEIGHT):
|
|
||||||
# Left wall
|
|
||||||
cell = grid.at(0, y)
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.sprite_index = 35 # '#' character
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100)
|
|
||||||
|
|
||||||
# Right wall
|
|
||||||
cell = grid.at(GRID_WIDTH - 1, y)
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.sprite_index = 35 # '#' character
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100)
|
|
||||||
|
|
||||||
# Create the room
|
|
||||||
create_room()
|
|
||||||
|
|
||||||
# Create the player entity
|
|
||||||
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
|
|
||||||
player.sprite_index = 64 # '@' character
|
|
||||||
player.color = mcrfpy.Color(255, 255, 255) # White
|
|
||||||
|
|
||||||
def move_player(dx, dy):
|
|
||||||
"""Move the player if the destination is walkable"""
|
|
||||||
# Calculate new position
|
|
||||||
new_x = player.x + dx
|
|
||||||
new_y = player.y + dy
|
|
||||||
|
|
||||||
# Check bounds
|
|
||||||
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check if the destination is walkable
|
|
||||||
destination = grid.at(new_x, new_y)
|
|
||||||
if destination.walkable:
|
|
||||||
# Move the player
|
|
||||||
player.x = new_x
|
|
||||||
player.y = new_y
|
|
||||||
|
|
||||||
def handle_input(key, state):
|
|
||||||
"""Handle keyboard input for player movement"""
|
|
||||||
# Only process key presses, not releases
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
# Movement deltas
|
|
||||||
dx, dy = 0, 0
|
|
||||||
|
|
||||||
# Arrow keys
|
|
||||||
if key == "Up":
|
|
||||||
dy = -1
|
|
||||||
elif key == "Down":
|
|
||||||
dy = 1
|
|
||||||
elif key == "Left":
|
|
||||||
dx = -1
|
|
||||||
elif key == "Right":
|
|
||||||
dx = 1
|
|
||||||
|
|
||||||
# Numpad movement (for true roguelike feel!)
|
|
||||||
elif key == "Num7": # Northwest
|
|
||||||
dx, dy = -1, -1
|
|
||||||
elif key == "Num8": # North
|
|
||||||
dy = -1
|
|
||||||
elif key == "Num9": # Northeast
|
|
||||||
dx, dy = 1, -1
|
|
||||||
elif key == "Num4": # West
|
|
||||||
dx = -1
|
|
||||||
elif key == "Num6": # East
|
|
||||||
dx = 1
|
|
||||||
elif key == "Num1": # Southwest
|
|
||||||
dx, dy = -1, 1
|
|
||||||
elif key == "Num2": # South
|
|
||||||
dy = 1
|
|
||||||
elif key == "Num3": # Southeast
|
|
||||||
dx, dy = 1, 1
|
|
||||||
|
|
||||||
# Escape to quit
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
return
|
|
||||||
|
|
||||||
# If there's movement, try to move the player
|
|
||||||
if dx != 0 or dy != 0:
|
|
||||||
move_player(dx, dy)
|
|
||||||
|
|
||||||
# Register the input handler
|
|
||||||
mcrfpy.keypressScene(handle_input)
|
|
||||||
|
|
||||||
# Add UI elements
|
|
||||||
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
|
||||||
ui.append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
ui.append(instructions)
|
|
||||||
|
|
||||||
status = mcrfpy.Caption("@ You", 100, 600)
|
|
||||||
status.font_size = 18
|
|
||||||
status.fill_color = mcrfpy.Color(255, 255, 255)
|
|
||||||
ui.append(status)
|
|
||||||
|
|
||||||
print("Part 1: The @ symbol moves!")
|
|
||||||
|
|
@ -1,562 +0,0 @@
|
||||||
# Part 2 - The Generic Entity, the Render Functions, and the Map
|
|
||||||
|
|
||||||
In Part 1, we created a player character that could move around a simple room. Now it's time to build a proper architecture for our roguelike. We'll create a flexible entity system, a proper map structure, and organize our code for future expansion.
|
|
||||||
|
|
||||||
## Understanding Game Architecture
|
|
||||||
|
|
||||||
Before diving into code, let's understand the architecture we're building:
|
|
||||||
|
|
||||||
1. **Entities**: Anything that can exist in the game world (player, monsters, items)
|
|
||||||
2. **Game Map**: The dungeon structure with tiles that can be walls or floors
|
|
||||||
3. **Game Engine**: Coordinates everything - entities, map, input, and rendering
|
|
||||||
|
|
||||||
In McRogueFace, we'll adapt these concepts to work with the engine's scene-based architecture.
|
|
||||||
|
|
||||||
## Creating a Flexible Entity System
|
|
||||||
|
|
||||||
While McRogueFace provides a built-in `Entity` class, we'll create a wrapper to add game-specific functionality:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class GameObject:
|
|
||||||
"""Base class for all game objects (player, monsters, items)"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.sprite_index = sprite_index
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks # Does this entity block movement?
|
|
||||||
self._entity = None # The McRogueFace entity
|
|
||||||
self.grid = None # Reference to the grid
|
|
||||||
|
|
||||||
def attach_to_grid(self, grid):
|
|
||||||
"""Attach this game object to a McRogueFace grid"""
|
|
||||||
self.grid = grid
|
|
||||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
|
||||||
self._entity.sprite_index = self.sprite_index
|
|
||||||
self._entity.color = self.color
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
"""Move by the given amount if possible"""
|
|
||||||
if not self.grid:
|
|
||||||
return
|
|
||||||
|
|
||||||
new_x = self.x + dx
|
|
||||||
new_y = self.y + dy
|
|
||||||
|
|
||||||
# Update our position
|
|
||||||
self.x = new_x
|
|
||||||
self.y = new_y
|
|
||||||
|
|
||||||
# Update the visual entity
|
|
||||||
if self._entity:
|
|
||||||
self._entity.x = new_x
|
|
||||||
self._entity.y = new_y
|
|
||||||
|
|
||||||
def destroy(self):
|
|
||||||
"""Remove this entity from the game"""
|
|
||||||
if self._entity and self.grid:
|
|
||||||
# Find and remove from grid's entity list
|
|
||||||
for i, entity in enumerate(self.grid.entities):
|
|
||||||
if entity == self._entity:
|
|
||||||
del self.grid.entities[i]
|
|
||||||
break
|
|
||||||
self._entity = None
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building the Game Map
|
|
||||||
|
|
||||||
Let's create a proper map class that manages our dungeon:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def __init__(self, width, height):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.grid = None
|
|
||||||
self.entities = [] # List of GameObjects
|
|
||||||
|
|
||||||
def create_grid(self, tileset):
|
|
||||||
"""Create the McRogueFace grid"""
|
|
||||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
|
||||||
self.grid.position = (100, 100)
|
|
||||||
self.grid.size = (800, 480)
|
|
||||||
|
|
||||||
# Initialize all tiles as walls
|
|
||||||
self.fill_with_walls()
|
|
||||||
|
|
||||||
return self.grid
|
|
||||||
|
|
||||||
def fill_with_walls(self):
|
|
||||||
"""Fill the entire map with wall tiles"""
|
|
||||||
for y in range(self.height):
|
|
||||||
for x in range(self.width):
|
|
||||||
self.set_tile(x, y, walkable=False, transparent=False,
|
|
||||||
sprite_index=35, color=(100, 100, 100))
|
|
||||||
|
|
||||||
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
|
||||||
"""Set properties for a specific tile"""
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
cell = self.grid.at(x, y)
|
|
||||||
cell.walkable = walkable
|
|
||||||
cell.transparent = transparent
|
|
||||||
cell.sprite_index = sprite_index
|
|
||||||
cell.color = mcrfpy.Color(*color)
|
|
||||||
|
|
||||||
def create_room(self, x1, y1, x2, y2):
|
|
||||||
"""Carve out a room in the map"""
|
|
||||||
# Make sure coordinates are in the right order
|
|
||||||
x1, x2 = min(x1, x2), max(x1, x2)
|
|
||||||
y1, y2 = min(y1, y2), max(y1, y2)
|
|
||||||
|
|
||||||
# Carve out floor tiles
|
|
||||||
for y in range(y1, y2 + 1):
|
|
||||||
for x in range(x1, x2 + 1):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(50, 50, 50))
|
|
||||||
|
|
||||||
def create_tunnel_h(self, x1, x2, y):
|
|
||||||
"""Create a horizontal tunnel"""
|
|
||||||
for x in range(min(x1, x2), max(x1, x2) + 1):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(50, 50, 50))
|
|
||||||
|
|
||||||
def create_tunnel_v(self, y1, y2, x):
|
|
||||||
"""Create a vertical tunnel"""
|
|
||||||
for y in range(min(y1, y2), max(y1, y2) + 1):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(50, 50, 50))
|
|
||||||
|
|
||||||
def is_blocked(self, x, y):
|
|
||||||
"""Check if a tile blocks movement"""
|
|
||||||
# Check map boundaries
|
|
||||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check if tile is walkable
|
|
||||||
if not self.grid.at(x, y).walkable:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check if any blocking entity is at this position
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_entity(self, entity):
|
|
||||||
"""Add a GameObject to the map"""
|
|
||||||
self.entities.append(entity)
|
|
||||||
entity.attach_to_grid(self.grid)
|
|
||||||
|
|
||||||
def get_blocking_entity_at(self, x, y):
|
|
||||||
"""Return any blocking entity at the given position"""
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return entity
|
|
||||||
return None
|
|
||||||
```
|
|
||||||
|
|
||||||
## Creating the Game Engine
|
|
||||||
|
|
||||||
Now let's build our game engine to tie everything together:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Engine:
|
|
||||||
"""Main game engine that manages game state"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.game_map = None
|
|
||||||
self.player = None
|
|
||||||
self.entities = []
|
|
||||||
|
|
||||||
# Create the game scene
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
# Configure window
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 2"
|
|
||||||
|
|
||||||
# Get UI container
|
|
||||||
self.ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
# Add background
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
self.ui.append(background)
|
|
||||||
|
|
||||||
# Load tileset
|
|
||||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
# Create the game world
|
|
||||||
self.setup_game()
|
|
||||||
|
|
||||||
# Setup input handling
|
|
||||||
self.setup_input()
|
|
||||||
|
|
||||||
# Add UI elements
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
def setup_game(self):
|
|
||||||
"""Initialize the game world"""
|
|
||||||
# Create the map
|
|
||||||
self.game_map = GameMap(50, 30)
|
|
||||||
grid = self.game_map.create_grid(self.tileset)
|
|
||||||
self.ui.append(grid)
|
|
||||||
|
|
||||||
# Create some rooms
|
|
||||||
self.game_map.create_room(10, 10, 20, 20)
|
|
||||||
self.game_map.create_room(30, 15, 40, 25)
|
|
||||||
self.game_map.create_room(15, 22, 25, 28)
|
|
||||||
|
|
||||||
# Connect rooms with tunnels
|
|
||||||
self.game_map.create_tunnel_h(20, 30, 15)
|
|
||||||
self.game_map.create_tunnel_v(20, 22, 20)
|
|
||||||
|
|
||||||
# Create player
|
|
||||||
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
|
|
||||||
self.game_map.add_entity(self.player)
|
|
||||||
|
|
||||||
# Create an NPC
|
|
||||||
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
|
|
||||||
self.game_map.add_entity(npc)
|
|
||||||
self.entities.append(npc)
|
|
||||||
|
|
||||||
# Create some items (non-blocking)
|
|
||||||
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
|
|
||||||
self.game_map.add_entity(potion)
|
|
||||||
self.entities.append(potion)
|
|
||||||
|
|
||||||
def handle_movement(self, dx, dy):
|
|
||||||
"""Handle player movement"""
|
|
||||||
new_x = self.player.x + dx
|
|
||||||
new_y = self.player.y + dy
|
|
||||||
|
|
||||||
# Check if movement is blocked
|
|
||||||
if not self.game_map.is_blocked(new_x, new_y):
|
|
||||||
self.player.move(dx, dy)
|
|
||||||
else:
|
|
||||||
# Check if we bumped into an entity
|
|
||||||
target = self.game_map.get_blocking_entity_at(new_x, new_y)
|
|
||||||
if target:
|
|
||||||
print(f"You bump into the {target.name}!")
|
|
||||||
|
|
||||||
def setup_input(self):
|
|
||||||
"""Setup keyboard input handling"""
|
|
||||||
def handle_keys(key, state):
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
# Movement keys
|
|
||||||
movement = {
|
|
||||||
"Up": (0, -1),
|
|
||||||
"Down": (0, 1),
|
|
||||||
"Left": (-1, 0),
|
|
||||||
"Right": (1, 0),
|
|
||||||
"Num7": (-1, -1),
|
|
||||||
"Num8": (0, -1),
|
|
||||||
"Num9": (1, -1),
|
|
||||||
"Num4": (-1, 0),
|
|
||||||
"Num6": (1, 0),
|
|
||||||
"Num1": (-1, 1),
|
|
||||||
"Num2": (0, 1),
|
|
||||||
"Num3": (1, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
if key in movement:
|
|
||||||
dx, dy = movement[key]
|
|
||||||
self.handle_movement(dx, dy)
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Setup UI elements"""
|
|
||||||
# Title
|
|
||||||
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
|
||||||
self.ui.append(title)
|
|
||||||
|
|
||||||
# Instructions
|
|
||||||
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
self.ui.append(instructions)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Putting It All Together
|
|
||||||
|
|
||||||
Here's the complete `game.py` file:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
class GameObject:
|
|
||||||
"""Base class for all game objects (player, monsters, items)"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.sprite_index = sprite_index
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks
|
|
||||||
self._entity = None
|
|
||||||
self.grid = None
|
|
||||||
|
|
||||||
def attach_to_grid(self, grid):
|
|
||||||
"""Attach this game object to a McRogueFace grid"""
|
|
||||||
self.grid = grid
|
|
||||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
|
||||||
self._entity.sprite_index = self.sprite_index
|
|
||||||
self._entity.color = mcrfpy.Color(*self.color)
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
"""Move by the given amount if possible"""
|
|
||||||
if not self.grid:
|
|
||||||
return
|
|
||||||
|
|
||||||
new_x = self.x + dx
|
|
||||||
new_y = self.y + dy
|
|
||||||
|
|
||||||
self.x = new_x
|
|
||||||
self.y = new_y
|
|
||||||
|
|
||||||
if self._entity:
|
|
||||||
self._entity.x = new_x
|
|
||||||
self._entity.y = new_y
|
|
||||||
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def __init__(self, width, height):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.grid = None
|
|
||||||
self.entities = []
|
|
||||||
|
|
||||||
def create_grid(self, tileset):
|
|
||||||
"""Create the McRogueFace grid"""
|
|
||||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
|
||||||
self.grid.position = (100, 100)
|
|
||||||
self.grid.size = (800, 480)
|
|
||||||
self.fill_with_walls()
|
|
||||||
return self.grid
|
|
||||||
|
|
||||||
def fill_with_walls(self):
|
|
||||||
"""Fill the entire map with wall tiles"""
|
|
||||||
for y in range(self.height):
|
|
||||||
for x in range(self.width):
|
|
||||||
self.set_tile(x, y, walkable=False, transparent=False,
|
|
||||||
sprite_index=35, color=(100, 100, 100))
|
|
||||||
|
|
||||||
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
|
||||||
"""Set properties for a specific tile"""
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
cell = self.grid.at(x, y)
|
|
||||||
cell.walkable = walkable
|
|
||||||
cell.transparent = transparent
|
|
||||||
cell.sprite_index = sprite_index
|
|
||||||
cell.color = mcrfpy.Color(*color)
|
|
||||||
|
|
||||||
def create_room(self, x1, y1, x2, y2):
|
|
||||||
"""Carve out a room in the map"""
|
|
||||||
x1, x2 = min(x1, x2), max(x1, x2)
|
|
||||||
y1, y2 = min(y1, y2), max(y1, y2)
|
|
||||||
|
|
||||||
for y in range(y1, y2 + 1):
|
|
||||||
for x in range(x1, x2 + 1):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(50, 50, 50))
|
|
||||||
|
|
||||||
def create_tunnel_h(self, x1, x2, y):
|
|
||||||
"""Create a horizontal tunnel"""
|
|
||||||
for x in range(min(x1, x2), max(x1, x2) + 1):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(50, 50, 50))
|
|
||||||
|
|
||||||
def create_tunnel_v(self, y1, y2, x):
|
|
||||||
"""Create a vertical tunnel"""
|
|
||||||
for y in range(min(y1, y2), max(y1, y2) + 1):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(50, 50, 50))
|
|
||||||
|
|
||||||
def is_blocked(self, x, y):
|
|
||||||
"""Check if a tile blocks movement"""
|
|
||||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if not self.grid.at(x, y).walkable:
|
|
||||||
return True
|
|
||||||
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_entity(self, entity):
|
|
||||||
"""Add a GameObject to the map"""
|
|
||||||
self.entities.append(entity)
|
|
||||||
entity.attach_to_grid(self.grid)
|
|
||||||
|
|
||||||
def get_blocking_entity_at(self, x, y):
|
|
||||||
"""Return any blocking entity at the given position"""
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return entity
|
|
||||||
return None
|
|
||||||
|
|
||||||
class Engine:
|
|
||||||
"""Main game engine that manages game state"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.game_map = None
|
|
||||||
self.player = None
|
|
||||||
self.entities = []
|
|
||||||
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 2"
|
|
||||||
|
|
||||||
self.ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
self.ui.append(background)
|
|
||||||
|
|
||||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
self.setup_game()
|
|
||||||
self.setup_input()
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
def setup_game(self):
|
|
||||||
"""Initialize the game world"""
|
|
||||||
self.game_map = GameMap(50, 30)
|
|
||||||
grid = self.game_map.create_grid(self.tileset)
|
|
||||||
self.ui.append(grid)
|
|
||||||
|
|
||||||
self.game_map.create_room(10, 10, 20, 20)
|
|
||||||
self.game_map.create_room(30, 15, 40, 25)
|
|
||||||
self.game_map.create_room(15, 22, 25, 28)
|
|
||||||
|
|
||||||
self.game_map.create_tunnel_h(20, 30, 15)
|
|
||||||
self.game_map.create_tunnel_v(20, 22, 20)
|
|
||||||
|
|
||||||
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
|
|
||||||
self.game_map.add_entity(self.player)
|
|
||||||
|
|
||||||
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
|
|
||||||
self.game_map.add_entity(npc)
|
|
||||||
self.entities.append(npc)
|
|
||||||
|
|
||||||
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
|
|
||||||
self.game_map.add_entity(potion)
|
|
||||||
self.entities.append(potion)
|
|
||||||
|
|
||||||
def handle_movement(self, dx, dy):
|
|
||||||
"""Handle player movement"""
|
|
||||||
new_x = self.player.x + dx
|
|
||||||
new_y = self.player.y + dy
|
|
||||||
|
|
||||||
if not self.game_map.is_blocked(new_x, new_y):
|
|
||||||
self.player.move(dx, dy)
|
|
||||||
else:
|
|
||||||
target = self.game_map.get_blocking_entity_at(new_x, new_y)
|
|
||||||
if target:
|
|
||||||
print(f"You bump into the {target.name}!")
|
|
||||||
|
|
||||||
def setup_input(self):
|
|
||||||
"""Setup keyboard input handling"""
|
|
||||||
def handle_keys(key, state):
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
movement = {
|
|
||||||
"Up": (0, -1), "Down": (0, 1),
|
|
||||||
"Left": (-1, 0), "Right": (1, 0),
|
|
||||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
|
||||||
"Num4": (-1, 0), "Num6": (1, 0),
|
|
||||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
if key in movement:
|
|
||||||
dx, dy = movement[key]
|
|
||||||
self.handle_movement(dx, dy)
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Setup UI elements"""
|
|
||||||
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
|
||||||
self.ui.append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
self.ui.append(instructions)
|
|
||||||
|
|
||||||
# Create and run the game
|
|
||||||
engine = Engine()
|
|
||||||
print("Part 2: Entities and Maps!")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Understanding the Architecture
|
|
||||||
|
|
||||||
### GameObject Class
|
|
||||||
Our `GameObject` class wraps McRogueFace's `Entity` and adds:
|
|
||||||
- Game logic properties (name, blocking)
|
|
||||||
- Position tracking independent of the visual entity
|
|
||||||
- Easy attachment/detachment from grids
|
|
||||||
|
|
||||||
### GameMap Class
|
|
||||||
The `GameMap` manages:
|
|
||||||
- The McRogueFace `Grid` for visual representation
|
|
||||||
- A list of all entities in the map
|
|
||||||
- Collision detection including entity blocking
|
|
||||||
- Map generation utilities (rooms, tunnels)
|
|
||||||
|
|
||||||
### Engine Class
|
|
||||||
The `Engine` coordinates everything:
|
|
||||||
- Scene and UI setup
|
|
||||||
- Game state management
|
|
||||||
- Input handling
|
|
||||||
- Entity-map interactions
|
|
||||||
|
|
||||||
## Key Improvements from Part 1
|
|
||||||
|
|
||||||
1. **Proper Entity Management**: Multiple entities can exist and interact
|
|
||||||
2. **Blocking Entities**: Some entities block movement, others don't
|
|
||||||
3. **Map Generation**: Tools for creating rooms and tunnels
|
|
||||||
4. **Collision System**: Checks both tiles and entities
|
|
||||||
5. **Organized Code**: Clear separation of concerns
|
|
||||||
|
|
||||||
## Exercises
|
|
||||||
|
|
||||||
1. **Add More Entity Types**: Create different sprites for monsters, items, and NPCs
|
|
||||||
2. **Entity Interactions**: Make items disappear when walked over
|
|
||||||
3. **Random Map Generation**: Place rooms and tunnels randomly
|
|
||||||
4. **Entity Properties**: Add health, damage, or other attributes to GameObjects
|
|
||||||
|
|
||||||
## What's Next?
|
|
||||||
|
|
||||||
In Part 3, we'll implement proper dungeon generation with:
|
|
||||||
- Procedurally generated rooms
|
|
||||||
- Smart tunnel routing
|
|
||||||
- Entity spawning
|
|
||||||
- The beginning of a real roguelike dungeon!
|
|
||||||
|
|
||||||
We now have a solid foundation with proper entity management and map structure. This architecture will serve us well as we add more complex features to our roguelike!
|
|
||||||
|
|
@ -1,217 +0,0 @@
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
class GameObject:
|
|
||||||
"""Base class for all game objects (player, monsters, items)"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.sprite_index = sprite_index
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks
|
|
||||||
self._entity = None
|
|
||||||
self.grid = None
|
|
||||||
|
|
||||||
def attach_to_grid(self, grid):
|
|
||||||
"""Attach this game object to a McRogueFace grid"""
|
|
||||||
self.grid = grid
|
|
||||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
|
||||||
self._entity.sprite_index = self.sprite_index
|
|
||||||
self._entity.color = mcrfpy.Color(*self.color)
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
"""Move by the given amount if possible"""
|
|
||||||
if not self.grid:
|
|
||||||
return
|
|
||||||
|
|
||||||
new_x = self.x + dx
|
|
||||||
new_y = self.y + dy
|
|
||||||
|
|
||||||
self.x = new_x
|
|
||||||
self.y = new_y
|
|
||||||
|
|
||||||
if self._entity:
|
|
||||||
self._entity.x = new_x
|
|
||||||
self._entity.y = new_y
|
|
||||||
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def __init__(self, width, height):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.grid = None
|
|
||||||
self.entities = []
|
|
||||||
|
|
||||||
def create_grid(self, tileset):
|
|
||||||
"""Create the McRogueFace grid"""
|
|
||||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
|
||||||
self.grid.position = (100, 100)
|
|
||||||
self.grid.size = (800, 480)
|
|
||||||
self.fill_with_walls()
|
|
||||||
return self.grid
|
|
||||||
|
|
||||||
def fill_with_walls(self):
|
|
||||||
"""Fill the entire map with wall tiles"""
|
|
||||||
for y in range(self.height):
|
|
||||||
for x in range(self.width):
|
|
||||||
self.set_tile(x, y, walkable=False, transparent=False,
|
|
||||||
sprite_index=35, color=(100, 100, 100))
|
|
||||||
|
|
||||||
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
|
||||||
"""Set properties for a specific tile"""
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
cell = self.grid.at(x, y)
|
|
||||||
cell.walkable = walkable
|
|
||||||
cell.transparent = transparent
|
|
||||||
cell.sprite_index = sprite_index
|
|
||||||
cell.color = mcrfpy.Color(*color)
|
|
||||||
|
|
||||||
def create_room(self, x1, y1, x2, y2):
|
|
||||||
"""Carve out a room in the map"""
|
|
||||||
x1, x2 = min(x1, x2), max(x1, x2)
|
|
||||||
y1, y2 = min(y1, y2), max(y1, y2)
|
|
||||||
|
|
||||||
for y in range(y1, y2 + 1):
|
|
||||||
for x in range(x1, x2 + 1):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(50, 50, 50))
|
|
||||||
|
|
||||||
def create_tunnel_h(self, x1, x2, y):
|
|
||||||
"""Create a horizontal tunnel"""
|
|
||||||
for x in range(min(x1, x2), max(x1, x2) + 1):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(50, 50, 50))
|
|
||||||
|
|
||||||
def create_tunnel_v(self, y1, y2, x):
|
|
||||||
"""Create a vertical tunnel"""
|
|
||||||
for y in range(min(y1, y2), max(y1, y2) + 1):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(50, 50, 50))
|
|
||||||
|
|
||||||
def is_blocked(self, x, y):
|
|
||||||
"""Check if a tile blocks movement"""
|
|
||||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if not self.grid.at(x, y).walkable:
|
|
||||||
return True
|
|
||||||
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_entity(self, entity):
|
|
||||||
"""Add a GameObject to the map"""
|
|
||||||
self.entities.append(entity)
|
|
||||||
entity.attach_to_grid(self.grid)
|
|
||||||
|
|
||||||
def get_blocking_entity_at(self, x, y):
|
|
||||||
"""Return any blocking entity at the given position"""
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return entity
|
|
||||||
return None
|
|
||||||
|
|
||||||
class Engine:
|
|
||||||
"""Main game engine that manages game state"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.game_map = None
|
|
||||||
self.player = None
|
|
||||||
self.entities = []
|
|
||||||
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 2"
|
|
||||||
|
|
||||||
self.ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
self.ui.append(background)
|
|
||||||
|
|
||||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
self.setup_game()
|
|
||||||
self.setup_input()
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
def setup_game(self):
|
|
||||||
"""Initialize the game world"""
|
|
||||||
self.game_map = GameMap(50, 30)
|
|
||||||
grid = self.game_map.create_grid(self.tileset)
|
|
||||||
self.ui.append(grid)
|
|
||||||
|
|
||||||
self.game_map.create_room(10, 10, 20, 20)
|
|
||||||
self.game_map.create_room(30, 15, 40, 25)
|
|
||||||
self.game_map.create_room(15, 22, 25, 28)
|
|
||||||
|
|
||||||
self.game_map.create_tunnel_h(20, 30, 15)
|
|
||||||
self.game_map.create_tunnel_v(20, 22, 20)
|
|
||||||
|
|
||||||
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
|
|
||||||
self.game_map.add_entity(self.player)
|
|
||||||
|
|
||||||
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
|
|
||||||
self.game_map.add_entity(npc)
|
|
||||||
self.entities.append(npc)
|
|
||||||
|
|
||||||
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
|
|
||||||
self.game_map.add_entity(potion)
|
|
||||||
self.entities.append(potion)
|
|
||||||
|
|
||||||
def handle_movement(self, dx, dy):
|
|
||||||
"""Handle player movement"""
|
|
||||||
new_x = self.player.x + dx
|
|
||||||
new_y = self.player.y + dy
|
|
||||||
|
|
||||||
if not self.game_map.is_blocked(new_x, new_y):
|
|
||||||
self.player.move(dx, dy)
|
|
||||||
else:
|
|
||||||
target = self.game_map.get_blocking_entity_at(new_x, new_y)
|
|
||||||
if target:
|
|
||||||
print(f"You bump into the {target.name}!")
|
|
||||||
|
|
||||||
def setup_input(self):
|
|
||||||
"""Setup keyboard input handling"""
|
|
||||||
def handle_keys(key, state):
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
movement = {
|
|
||||||
"Up": (0, -1), "Down": (0, 1),
|
|
||||||
"Left": (-1, 0), "Right": (1, 0),
|
|
||||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
|
||||||
"Num4": (-1, 0), "Num6": (1, 0),
|
|
||||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
if key in movement:
|
|
||||||
dx, dy = movement[key]
|
|
||||||
self.handle_movement(dx, dy)
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Setup UI elements"""
|
|
||||||
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
|
||||||
self.ui.append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
self.ui.append(instructions)
|
|
||||||
|
|
||||||
# Create and run the game
|
|
||||||
engine = Engine()
|
|
||||||
print("Part 2: Entities and Maps!")
|
|
||||||
|
|
@ -1,548 +0,0 @@
|
||||||
# Part 3 - Generating a Dungeon
|
|
||||||
|
|
||||||
In Parts 1 and 2, we created a player that could move around and interact with a hand-crafted dungeon. Now it's time to generate dungeons procedurally - a core feature of any roguelike game!
|
|
||||||
|
|
||||||
## The Plan
|
|
||||||
|
|
||||||
We'll create a dungeon generator that:
|
|
||||||
1. Places rectangular rooms randomly
|
|
||||||
2. Ensures rooms don't overlap
|
|
||||||
3. Connects rooms with tunnels
|
|
||||||
4. Places the player in the first room
|
|
||||||
|
|
||||||
This is a classic approach used by many roguelikes, and it creates interesting, playable dungeons.
|
|
||||||
|
|
||||||
## Creating a Room Class
|
|
||||||
|
|
||||||
First, let's create a class to represent rectangular rooms:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class RectangularRoom:
|
|
||||||
"""A rectangular room with its position and size"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, width, height):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + width
|
|
||||||
self.y2 = y + height
|
|
||||||
|
|
||||||
@property
|
|
||||||
def center(self):
|
|
||||||
"""Return the center coordinates of the room"""
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return center_x, center_y
|
|
||||||
|
|
||||||
@property
|
|
||||||
def inner(self):
|
|
||||||
"""Return the inner area of the room as a tuple of slices
|
|
||||||
|
|
||||||
This property returns the area inside the walls.
|
|
||||||
We'll add 1 to min coordinates and subtract 1 from max coordinates.
|
|
||||||
"""
|
|
||||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
"""Return True if this room overlaps with another RectangularRoom"""
|
|
||||||
return (
|
|
||||||
self.x1 <= other.x2
|
|
||||||
and self.x2 >= other.x1
|
|
||||||
and self.y1 <= other.y2
|
|
||||||
and self.y2 >= other.y1
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementing Tunnel Generation
|
|
||||||
|
|
||||||
Since McRogueFace doesn't include line-drawing algorithms, let's implement simple L-shaped tunnels:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def tunnel_between(start, end):
|
|
||||||
"""Return an L-shaped tunnel between two points"""
|
|
||||||
x1, y1 = start
|
|
||||||
x2, y2 = end
|
|
||||||
|
|
||||||
# Randomly decide whether to go horizontal first or vertical first
|
|
||||||
if random.random() < 0.5:
|
|
||||||
# Horizontal, then vertical
|
|
||||||
corner_x = x2
|
|
||||||
corner_y = y1
|
|
||||||
else:
|
|
||||||
# Vertical, then horizontal
|
|
||||||
corner_x = x1
|
|
||||||
corner_y = y2
|
|
||||||
|
|
||||||
# Generate the coordinates
|
|
||||||
# First line: from start to corner
|
|
||||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
|
||||||
yield x, y1
|
|
||||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
|
||||||
yield corner_x, y
|
|
||||||
|
|
||||||
# Second line: from corner to end
|
|
||||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
|
||||||
yield x, corner_y
|
|
||||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
|
||||||
yield x2, y
|
|
||||||
```
|
|
||||||
|
|
||||||
## The Dungeon Generator
|
|
||||||
|
|
||||||
Now let's update our GameMap class to generate dungeons:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import random
|
|
||||||
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def __init__(self, width, height):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.grid = None
|
|
||||||
self.entities = []
|
|
||||||
self.rooms = [] # Keep track of rooms for game logic
|
|
||||||
|
|
||||||
def generate_dungeon(
|
|
||||||
self,
|
|
||||||
max_rooms,
|
|
||||||
room_min_size,
|
|
||||||
room_max_size,
|
|
||||||
player
|
|
||||||
):
|
|
||||||
"""Generate a new dungeon map"""
|
|
||||||
# Start with everything as walls
|
|
||||||
self.fill_with_walls()
|
|
||||||
|
|
||||||
for r in range(max_rooms):
|
|
||||||
# Random width and height
|
|
||||||
room_width = random.randint(room_min_size, room_max_size)
|
|
||||||
room_height = random.randint(room_min_size, room_max_size)
|
|
||||||
|
|
||||||
# Random position without going out of bounds
|
|
||||||
x = random.randint(0, self.width - room_width - 1)
|
|
||||||
y = random.randint(0, self.height - room_height - 1)
|
|
||||||
|
|
||||||
# Create the room
|
|
||||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
|
||||||
|
|
||||||
# Check if it intersects with any existing room
|
|
||||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
|
||||||
continue # This room intersects, so go to the next attempt
|
|
||||||
|
|
||||||
# If we get here, it's a valid room
|
|
||||||
|
|
||||||
# Carve out this room
|
|
||||||
self.carve_room(new_room)
|
|
||||||
|
|
||||||
# Place the player in the center of the first room
|
|
||||||
if len(self.rooms) == 0:
|
|
||||||
player.x, player.y = new_room.center
|
|
||||||
if player._entity:
|
|
||||||
player._entity.x, player._entity.y = new_room.center
|
|
||||||
else:
|
|
||||||
# All rooms after the first:
|
|
||||||
# Tunnel between this room and the previous one
|
|
||||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
|
||||||
|
|
||||||
# Finally, append the new room to the list
|
|
||||||
self.rooms.append(new_room)
|
|
||||||
|
|
||||||
def carve_room(self, room):
|
|
||||||
"""Carve out a room"""
|
|
||||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
|
||||||
|
|
||||||
for y in range(inner_y1, inner_y2):
|
|
||||||
for x in range(inner_x1, inner_x2):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(50, 50, 50))
|
|
||||||
|
|
||||||
def carve_tunnel(self, start, end):
|
|
||||||
"""Carve a tunnel between two points"""
|
|
||||||
for x, y in tunnel_between(start, end):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(30, 30, 40)) # Slightly different color for tunnels
|
|
||||||
```
|
|
||||||
|
|
||||||
## Complete Code
|
|
||||||
|
|
||||||
Here's the complete `game.py` with procedural dungeon generation:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
class GameObject:
|
|
||||||
"""Base class for all game objects"""
|
|
||||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.sprite_index = sprite_index
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks
|
|
||||||
self._entity = None
|
|
||||||
self.grid = None
|
|
||||||
|
|
||||||
def attach_to_grid(self, grid):
|
|
||||||
"""Attach this game object to a McRogueFace grid"""
|
|
||||||
self.grid = grid
|
|
||||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
|
||||||
self._entity.sprite_index = self.sprite_index
|
|
||||||
self._entity.color = mcrfpy.Color(*self.color)
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
"""Move by the given amount"""
|
|
||||||
if not self.grid:
|
|
||||||
return
|
|
||||||
self.x += dx
|
|
||||||
self.y += dy
|
|
||||||
if self._entity:
|
|
||||||
self._entity.x = self.x
|
|
||||||
self._entity.y = self.y
|
|
||||||
|
|
||||||
class RectangularRoom:
|
|
||||||
"""A rectangular room with its position and size"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, width, height):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + width
|
|
||||||
self.y2 = y + height
|
|
||||||
|
|
||||||
@property
|
|
||||||
def center(self):
|
|
||||||
"""Return the center coordinates of the room"""
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return center_x, center_y
|
|
||||||
|
|
||||||
@property
|
|
||||||
def inner(self):
|
|
||||||
"""Return the inner area of the room"""
|
|
||||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
"""Return True if this room overlaps with another"""
|
|
||||||
return (
|
|
||||||
self.x1 <= other.x2
|
|
||||||
and self.x2 >= other.x1
|
|
||||||
and self.y1 <= other.y2
|
|
||||||
and self.y2 >= other.y1
|
|
||||||
)
|
|
||||||
|
|
||||||
def tunnel_between(start, end):
|
|
||||||
"""Return an L-shaped tunnel between two points"""
|
|
||||||
x1, y1 = start
|
|
||||||
x2, y2 = end
|
|
||||||
|
|
||||||
if random.random() < 0.5:
|
|
||||||
corner_x = x2
|
|
||||||
corner_y = y1
|
|
||||||
else:
|
|
||||||
corner_x = x1
|
|
||||||
corner_y = y2
|
|
||||||
|
|
||||||
# Generate the coordinates
|
|
||||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
|
||||||
yield x, y1
|
|
||||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
|
||||||
yield corner_x, y
|
|
||||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
|
||||||
yield x, corner_y
|
|
||||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
|
||||||
yield x2, y
|
|
||||||
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def __init__(self, width, height):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.grid = None
|
|
||||||
self.entities = []
|
|
||||||
self.rooms = []
|
|
||||||
|
|
||||||
def create_grid(self, tileset):
|
|
||||||
"""Create the McRogueFace grid"""
|
|
||||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
|
||||||
self.grid.position = (100, 100)
|
|
||||||
self.grid.size = (800, 480)
|
|
||||||
return self.grid
|
|
||||||
|
|
||||||
def fill_with_walls(self):
|
|
||||||
"""Fill the entire map with wall tiles"""
|
|
||||||
for y in range(self.height):
|
|
||||||
for x in range(self.width):
|
|
||||||
self.set_tile(x, y, walkable=False, transparent=False,
|
|
||||||
sprite_index=35, color=(100, 100, 100))
|
|
||||||
|
|
||||||
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
|
||||||
"""Set properties for a specific tile"""
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
cell = self.grid.at(x, y)
|
|
||||||
cell.walkable = walkable
|
|
||||||
cell.transparent = transparent
|
|
||||||
cell.sprite_index = sprite_index
|
|
||||||
cell.color = mcrfpy.Color(*color)
|
|
||||||
|
|
||||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
|
|
||||||
"""Generate a new dungeon map"""
|
|
||||||
self.fill_with_walls()
|
|
||||||
|
|
||||||
for r in range(max_rooms):
|
|
||||||
room_width = random.randint(room_min_size, room_max_size)
|
|
||||||
room_height = random.randint(room_min_size, room_max_size)
|
|
||||||
|
|
||||||
x = random.randint(0, self.width - room_width - 1)
|
|
||||||
y = random.randint(0, self.height - room_height - 1)
|
|
||||||
|
|
||||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
|
||||||
|
|
||||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.carve_room(new_room)
|
|
||||||
|
|
||||||
if len(self.rooms) == 0:
|
|
||||||
player.x, player.y = new_room.center
|
|
||||||
if player._entity:
|
|
||||||
player._entity.x, player._entity.y = new_room.center
|
|
||||||
else:
|
|
||||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
|
||||||
|
|
||||||
self.rooms.append(new_room)
|
|
||||||
|
|
||||||
def carve_room(self, room):
|
|
||||||
"""Carve out a room"""
|
|
||||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
|
||||||
|
|
||||||
for y in range(inner_y1, inner_y2):
|
|
||||||
for x in range(inner_x1, inner_x2):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(50, 50, 50))
|
|
||||||
|
|
||||||
def carve_tunnel(self, start, end):
|
|
||||||
"""Carve a tunnel between two points"""
|
|
||||||
for x, y in tunnel_between(start, end):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(30, 30, 40))
|
|
||||||
|
|
||||||
def is_blocked(self, x, y):
|
|
||||||
"""Check if a tile blocks movement"""
|
|
||||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
|
||||||
return True
|
|
||||||
if not self.grid.at(x, y).walkable:
|
|
||||||
return True
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_entity(self, entity):
|
|
||||||
"""Add a GameObject to the map"""
|
|
||||||
self.entities.append(entity)
|
|
||||||
entity.attach_to_grid(self.grid)
|
|
||||||
|
|
||||||
class Engine:
|
|
||||||
"""Main game engine"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.game_map = None
|
|
||||||
self.player = None
|
|
||||||
self.entities = []
|
|
||||||
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 3"
|
|
||||||
|
|
||||||
self.ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
self.ui.append(background)
|
|
||||||
|
|
||||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
self.setup_game()
|
|
||||||
self.setup_input()
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
def setup_game(self):
|
|
||||||
"""Initialize the game world"""
|
|
||||||
self.game_map = GameMap(80, 45)
|
|
||||||
grid = self.game_map.create_grid(self.tileset)
|
|
||||||
self.ui.append(grid)
|
|
||||||
|
|
||||||
# Create player (before dungeon generation)
|
|
||||||
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
|
||||||
|
|
||||||
# Generate the dungeon
|
|
||||||
self.game_map.generate_dungeon(
|
|
||||||
max_rooms=30,
|
|
||||||
room_min_size=6,
|
|
||||||
room_max_size=10,
|
|
||||||
player=self.player
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add player to map
|
|
||||||
self.game_map.add_entity(self.player)
|
|
||||||
|
|
||||||
# Add some monsters in random rooms
|
|
||||||
for i in range(5):
|
|
||||||
if i < len(self.game_map.rooms) - 1: # Don't spawn in first room
|
|
||||||
room = self.game_map.rooms[i + 1]
|
|
||||||
x, y = room.center
|
|
||||||
|
|
||||||
# Create an orc
|
|
||||||
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
|
||||||
self.game_map.add_entity(orc)
|
|
||||||
self.entities.append(orc)
|
|
||||||
|
|
||||||
def handle_movement(self, dx, dy):
|
|
||||||
"""Handle player movement"""
|
|
||||||
new_x = self.player.x + dx
|
|
||||||
new_y = self.player.y + dy
|
|
||||||
|
|
||||||
if not self.game_map.is_blocked(new_x, new_y):
|
|
||||||
self.player.move(dx, dy)
|
|
||||||
|
|
||||||
def setup_input(self):
|
|
||||||
"""Setup keyboard input handling"""
|
|
||||||
def handle_keys(key, state):
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
movement = {
|
|
||||||
"Up": (0, -1), "Down": (0, 1),
|
|
||||||
"Left": (-1, 0), "Right": (1, 0),
|
|
||||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
|
||||||
"Num4": (-1, 0), "Num6": (1, 0),
|
|
||||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
if key in movement:
|
|
||||||
dx, dy = movement[key]
|
|
||||||
self.handle_movement(dx, dy)
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
elif key == "Space":
|
|
||||||
# Regenerate the dungeon
|
|
||||||
self.regenerate_dungeon()
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
def regenerate_dungeon(self):
|
|
||||||
"""Generate a new dungeon"""
|
|
||||||
# Clear existing entities
|
|
||||||
self.game_map.entities.clear()
|
|
||||||
self.game_map.rooms.clear()
|
|
||||||
self.entities.clear()
|
|
||||||
|
|
||||||
# Clear the entity list in the grid
|
|
||||||
if self.game_map.grid:
|
|
||||||
self.game_map.grid.entities.clear()
|
|
||||||
|
|
||||||
# Regenerate
|
|
||||||
self.game_map.generate_dungeon(
|
|
||||||
max_rooms=30,
|
|
||||||
room_min_size=6,
|
|
||||||
room_max_size=10,
|
|
||||||
player=self.player
|
|
||||||
)
|
|
||||||
|
|
||||||
# Re-add player
|
|
||||||
self.game_map.add_entity(self.player)
|
|
||||||
|
|
||||||
# Add new monsters
|
|
||||||
for i in range(5):
|
|
||||||
if i < len(self.game_map.rooms) - 1:
|
|
||||||
room = self.game_map.rooms[i + 1]
|
|
||||||
x, y = room.center
|
|
||||||
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
|
||||||
self.game_map.add_entity(orc)
|
|
||||||
self.entities.append(orc)
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Setup UI elements"""
|
|
||||||
title = mcrfpy.Caption("Procedural Dungeon Generation", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
|
||||||
self.ui.append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption("Arrow keys to move, SPACE to regenerate, ESC to quit", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
self.ui.append(instructions)
|
|
||||||
|
|
||||||
# Create and run the game
|
|
||||||
engine = Engine()
|
|
||||||
print("Part 3: Procedural Dungeon Generation!")
|
|
||||||
print("Press SPACE to generate a new dungeon")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Understanding the Algorithm
|
|
||||||
|
|
||||||
Our dungeon generation algorithm is simple but effective:
|
|
||||||
|
|
||||||
1. **Start with solid walls** - The entire map begins filled with wall tiles
|
|
||||||
2. **Try to place rooms** - Generate random rooms and check for overlaps
|
|
||||||
3. **Connect with tunnels** - Each new room connects to the previous one
|
|
||||||
4. **Place entities** - The player starts in the first room, monsters in others
|
|
||||||
|
|
||||||
### Room Placement
|
|
||||||
|
|
||||||
The algorithm attempts to place `max_rooms` rooms, but may place fewer if many attempts result in overlapping rooms. This is called "rejection sampling" - we generate random rooms and reject ones that don't fit.
|
|
||||||
|
|
||||||
### Tunnel Design
|
|
||||||
|
|
||||||
Our L-shaped tunnels are simple but effective. They either go:
|
|
||||||
- Horizontal first, then vertical
|
|
||||||
- Vertical first, then horizontal
|
|
||||||
|
|
||||||
This creates variety while ensuring all rooms are connected.
|
|
||||||
|
|
||||||
## Experimenting with Parameters
|
|
||||||
|
|
||||||
Try adjusting these parameters to create different dungeon styles:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Sparse dungeon with large rooms
|
|
||||||
self.game_map.generate_dungeon(
|
|
||||||
max_rooms=10,
|
|
||||||
room_min_size=10,
|
|
||||||
room_max_size=15,
|
|
||||||
player=self.player
|
|
||||||
)
|
|
||||||
|
|
||||||
# Dense dungeon with small rooms
|
|
||||||
self.game_map.generate_dungeon(
|
|
||||||
max_rooms=50,
|
|
||||||
room_min_size=4,
|
|
||||||
room_max_size=6,
|
|
||||||
player=self.player
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Visual Enhancements
|
|
||||||
|
|
||||||
Notice how we gave tunnels a slightly different color:
|
|
||||||
- Rooms: `color=(50, 50, 50)` - Medium gray
|
|
||||||
- Tunnels: `color=(30, 30, 40)` - Darker with blue tint
|
|
||||||
|
|
||||||
This subtle difference helps players understand the dungeon layout.
|
|
||||||
|
|
||||||
## Exercises
|
|
||||||
|
|
||||||
1. **Different Room Shapes**: Create circular or cross-shaped rooms
|
|
||||||
2. **Better Tunnel Routing**: Implement A* pathfinding for more natural tunnels
|
|
||||||
3. **Room Types**: Create special rooms (treasure rooms, trap rooms)
|
|
||||||
4. **Dungeon Themes**: Use different tile sets and colors for different dungeon levels
|
|
||||||
|
|
||||||
## What's Next?
|
|
||||||
|
|
||||||
In Part 4, we'll implement Field of View (FOV) so the player can only see parts of the dungeon they've explored. This will add mystery and atmosphere to our procedurally generated dungeons!
|
|
||||||
|
|
||||||
Our dungeon generator is now creating unique, playable levels every time. The foundation of a true roguelike is taking shape!
|
|
||||||
|
|
@ -1,312 +0,0 @@
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
class GameObject:
|
|
||||||
"""Base class for all game objects"""
|
|
||||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.sprite_index = sprite_index
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks
|
|
||||||
self._entity = None
|
|
||||||
self.grid = None
|
|
||||||
|
|
||||||
def attach_to_grid(self, grid):
|
|
||||||
"""Attach this game object to a McRogueFace grid"""
|
|
||||||
self.grid = grid
|
|
||||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
|
||||||
self._entity.sprite_index = self.sprite_index
|
|
||||||
self._entity.color = mcrfpy.Color(*self.color)
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
"""Move by the given amount"""
|
|
||||||
if not self.grid:
|
|
||||||
return
|
|
||||||
self.x += dx
|
|
||||||
self.y += dy
|
|
||||||
if self._entity:
|
|
||||||
self._entity.x = self.x
|
|
||||||
self._entity.y = self.y
|
|
||||||
|
|
||||||
class RectangularRoom:
|
|
||||||
"""A rectangular room with its position and size"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, width, height):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + width
|
|
||||||
self.y2 = y + height
|
|
||||||
|
|
||||||
@property
|
|
||||||
def center(self):
|
|
||||||
"""Return the center coordinates of the room"""
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return center_x, center_y
|
|
||||||
|
|
||||||
@property
|
|
||||||
def inner(self):
|
|
||||||
"""Return the inner area of the room"""
|
|
||||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
"""Return True if this room overlaps with another"""
|
|
||||||
return (
|
|
||||||
self.x1 <= other.x2
|
|
||||||
and self.x2 >= other.x1
|
|
||||||
and self.y1 <= other.y2
|
|
||||||
and self.y2 >= other.y1
|
|
||||||
)
|
|
||||||
|
|
||||||
def tunnel_between(start, end):
|
|
||||||
"""Return an L-shaped tunnel between two points"""
|
|
||||||
x1, y1 = start
|
|
||||||
x2, y2 = end
|
|
||||||
|
|
||||||
if random.random() < 0.5:
|
|
||||||
corner_x = x2
|
|
||||||
corner_y = y1
|
|
||||||
else:
|
|
||||||
corner_x = x1
|
|
||||||
corner_y = y2
|
|
||||||
|
|
||||||
# Generate the coordinates
|
|
||||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
|
||||||
yield x, y1
|
|
||||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
|
||||||
yield corner_x, y
|
|
||||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
|
||||||
yield x, corner_y
|
|
||||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
|
||||||
yield x2, y
|
|
||||||
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def __init__(self, width, height):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.grid = None
|
|
||||||
self.entities = []
|
|
||||||
self.rooms = []
|
|
||||||
|
|
||||||
def create_grid(self, tileset):
|
|
||||||
"""Create the McRogueFace grid"""
|
|
||||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
|
||||||
self.grid.position = (100, 100)
|
|
||||||
self.grid.size = (800, 480)
|
|
||||||
return self.grid
|
|
||||||
|
|
||||||
def fill_with_walls(self):
|
|
||||||
"""Fill the entire map with wall tiles"""
|
|
||||||
for y in range(self.height):
|
|
||||||
for x in range(self.width):
|
|
||||||
self.set_tile(x, y, walkable=False, transparent=False,
|
|
||||||
sprite_index=35, color=(100, 100, 100))
|
|
||||||
|
|
||||||
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
|
||||||
"""Set properties for a specific tile"""
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
cell = self.grid.at(x, y)
|
|
||||||
cell.walkable = walkable
|
|
||||||
cell.transparent = transparent
|
|
||||||
cell.sprite_index = sprite_index
|
|
||||||
cell.color = mcrfpy.Color(*color)
|
|
||||||
|
|
||||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
|
|
||||||
"""Generate a new dungeon map"""
|
|
||||||
self.fill_with_walls()
|
|
||||||
|
|
||||||
for r in range(max_rooms):
|
|
||||||
room_width = random.randint(room_min_size, room_max_size)
|
|
||||||
room_height = random.randint(room_min_size, room_max_size)
|
|
||||||
|
|
||||||
x = random.randint(0, self.width - room_width - 1)
|
|
||||||
y = random.randint(0, self.height - room_height - 1)
|
|
||||||
|
|
||||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
|
||||||
|
|
||||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.carve_room(new_room)
|
|
||||||
|
|
||||||
if len(self.rooms) == 0:
|
|
||||||
player.x, player.y = new_room.center
|
|
||||||
if player._entity:
|
|
||||||
player._entity.x, player._entity.y = new_room.center
|
|
||||||
else:
|
|
||||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
|
||||||
|
|
||||||
self.rooms.append(new_room)
|
|
||||||
|
|
||||||
def carve_room(self, room):
|
|
||||||
"""Carve out a room"""
|
|
||||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
|
||||||
|
|
||||||
for y in range(inner_y1, inner_y2):
|
|
||||||
for x in range(inner_x1, inner_x2):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(50, 50, 50))
|
|
||||||
|
|
||||||
def carve_tunnel(self, start, end):
|
|
||||||
"""Carve a tunnel between two points"""
|
|
||||||
for x, y in tunnel_between(start, end):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, color=(30, 30, 40))
|
|
||||||
|
|
||||||
def is_blocked(self, x, y):
|
|
||||||
"""Check if a tile blocks movement"""
|
|
||||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
|
||||||
return True
|
|
||||||
if not self.grid.at(x, y).walkable:
|
|
||||||
return True
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_entity(self, entity):
|
|
||||||
"""Add a GameObject to the map"""
|
|
||||||
self.entities.append(entity)
|
|
||||||
entity.attach_to_grid(self.grid)
|
|
||||||
|
|
||||||
class Engine:
|
|
||||||
"""Main game engine"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.game_map = None
|
|
||||||
self.player = None
|
|
||||||
self.entities = []
|
|
||||||
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 3"
|
|
||||||
|
|
||||||
self.ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
self.ui.append(background)
|
|
||||||
|
|
||||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
self.setup_game()
|
|
||||||
self.setup_input()
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
def setup_game(self):
|
|
||||||
"""Initialize the game world"""
|
|
||||||
self.game_map = GameMap(80, 45)
|
|
||||||
grid = self.game_map.create_grid(self.tileset)
|
|
||||||
self.ui.append(grid)
|
|
||||||
|
|
||||||
# Create player (before dungeon generation)
|
|
||||||
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
|
||||||
|
|
||||||
# Generate the dungeon
|
|
||||||
self.game_map.generate_dungeon(
|
|
||||||
max_rooms=30,
|
|
||||||
room_min_size=6,
|
|
||||||
room_max_size=10,
|
|
||||||
player=self.player
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add player to map
|
|
||||||
self.game_map.add_entity(self.player)
|
|
||||||
|
|
||||||
# Add some monsters in random rooms
|
|
||||||
for i in range(5):
|
|
||||||
if i < len(self.game_map.rooms) - 1: # Don't spawn in first room
|
|
||||||
room = self.game_map.rooms[i + 1]
|
|
||||||
x, y = room.center
|
|
||||||
|
|
||||||
# Create an orc
|
|
||||||
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
|
||||||
self.game_map.add_entity(orc)
|
|
||||||
self.entities.append(orc)
|
|
||||||
|
|
||||||
def handle_movement(self, dx, dy):
|
|
||||||
"""Handle player movement"""
|
|
||||||
new_x = self.player.x + dx
|
|
||||||
new_y = self.player.y + dy
|
|
||||||
|
|
||||||
if not self.game_map.is_blocked(new_x, new_y):
|
|
||||||
self.player.move(dx, dy)
|
|
||||||
|
|
||||||
def setup_input(self):
|
|
||||||
"""Setup keyboard input handling"""
|
|
||||||
def handle_keys(key, state):
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
movement = {
|
|
||||||
"Up": (0, -1), "Down": (0, 1),
|
|
||||||
"Left": (-1, 0), "Right": (1, 0),
|
|
||||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
|
||||||
"Num4": (-1, 0), "Num6": (1, 0),
|
|
||||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
if key in movement:
|
|
||||||
dx, dy = movement[key]
|
|
||||||
self.handle_movement(dx, dy)
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
elif key == "Space":
|
|
||||||
# Regenerate the dungeon
|
|
||||||
self.regenerate_dungeon()
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
def regenerate_dungeon(self):
|
|
||||||
"""Generate a new dungeon"""
|
|
||||||
# Clear existing entities
|
|
||||||
self.game_map.entities.clear()
|
|
||||||
self.game_map.rooms.clear()
|
|
||||||
self.entities.clear()
|
|
||||||
|
|
||||||
# Clear the entity list in the grid
|
|
||||||
if self.game_map.grid:
|
|
||||||
self.game_map.grid.entities.clear()
|
|
||||||
|
|
||||||
# Regenerate
|
|
||||||
self.game_map.generate_dungeon(
|
|
||||||
max_rooms=30,
|
|
||||||
room_min_size=6,
|
|
||||||
room_max_size=10,
|
|
||||||
player=self.player
|
|
||||||
)
|
|
||||||
|
|
||||||
# Re-add player
|
|
||||||
self.game_map.add_entity(self.player)
|
|
||||||
|
|
||||||
# Add new monsters
|
|
||||||
for i in range(5):
|
|
||||||
if i < len(self.game_map.rooms) - 1:
|
|
||||||
room = self.game_map.rooms[i + 1]
|
|
||||||
x, y = room.center
|
|
||||||
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
|
||||||
self.game_map.add_entity(orc)
|
|
||||||
self.entities.append(orc)
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Setup UI elements"""
|
|
||||||
title = mcrfpy.Caption("Procedural Dungeon Generation", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
|
||||||
self.ui.append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption("Arrow keys to move, SPACE to regenerate, ESC to quit", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
self.ui.append(instructions)
|
|
||||||
|
|
||||||
# Create and run the game
|
|
||||||
engine = Engine()
|
|
||||||
print("Part 3: Procedural Dungeon Generation!")
|
|
||||||
print("Press SPACE to generate a new dungeon")
|
|
||||||
|
|
@ -1,520 +0,0 @@
|
||||||
# Part 4 - Field of View
|
|
||||||
|
|
||||||
One of the defining features of roguelikes is exploration and discovery. In Part 3, we could see the entire dungeon at once. Now we'll implement Field of View (FOV) so players can only see what their character can actually see, adding mystery and tactical depth to our game.
|
|
||||||
|
|
||||||
## Understanding Field of View
|
|
||||||
|
|
||||||
Field of View creates three distinct visibility states for each tile:
|
|
||||||
|
|
||||||
1. **Visible**: Currently in the player's line of sight
|
|
||||||
2. **Explored**: Previously seen but not currently visible
|
|
||||||
3. **Unexplored**: Never seen (completely hidden)
|
|
||||||
|
|
||||||
This creates the classic "fog of war" effect where you remember the layout of areas you've explored, but can't see current enemy positions unless they're in your view.
|
|
||||||
|
|
||||||
## McRogueFace's FOV System
|
|
||||||
|
|
||||||
Good news! McRogueFace includes built-in FOV support through its C++ engine. We just need to enable and configure it. The engine uses an efficient shadowcasting algorithm that provides smooth, realistic line-of-sight calculations.
|
|
||||||
|
|
||||||
Let's update our code to use FOV:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class GameObject:
|
|
||||||
"""Base class for all game objects"""
|
|
||||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.sprite_index = sprite_index
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks
|
|
||||||
self._entity = None
|
|
||||||
self.grid = None
|
|
||||||
|
|
||||||
def attach_to_grid(self, grid):
|
|
||||||
"""Attach this game object to a McRogueFace grid"""
|
|
||||||
self.grid = grid
|
|
||||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
|
||||||
self._entity.sprite_index = self.sprite_index
|
|
||||||
self._entity.color = mcrfpy.Color(*self.color)
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
"""Move by the given amount"""
|
|
||||||
if not self.grid:
|
|
||||||
return
|
|
||||||
self.x += dx
|
|
||||||
self.y += dy
|
|
||||||
if self._entity:
|
|
||||||
self._entity.x = self.x
|
|
||||||
self._entity.y = self.y
|
|
||||||
# Update FOV when player moves
|
|
||||||
if self.name == "Player":
|
|
||||||
self.update_fov()
|
|
||||||
|
|
||||||
def update_fov(self):
|
|
||||||
"""Update field of view from this entity's position"""
|
|
||||||
if self._entity and self.grid:
|
|
||||||
self._entity.update_fov(radius=8)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuring Visibility Rendering
|
|
||||||
|
|
||||||
McRogueFace automatically handles the rendering of visible/explored/unexplored tiles. We need to set up our grid to use perspective-based rendering:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def create_grid(self, tileset):
|
|
||||||
"""Create the McRogueFace grid"""
|
|
||||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
|
||||||
self.grid.position = (100, 100)
|
|
||||||
self.grid.size = (800, 480)
|
|
||||||
|
|
||||||
# Enable perspective rendering (0 = first entity = player)
|
|
||||||
self.grid.perspective = 0
|
|
||||||
|
|
||||||
return self.grid
|
|
||||||
```
|
|
||||||
|
|
||||||
## Visual Appearance Configuration
|
|
||||||
|
|
||||||
Let's define how our tiles look in different visibility states:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Color configurations for visibility states
|
|
||||||
COLORS_VISIBLE = {
|
|
||||||
'wall': (100, 100, 100), # Light gray
|
|
||||||
'floor': (50, 50, 50), # Dark gray
|
|
||||||
'tunnel': (30, 30, 40), # Dark blue-gray
|
|
||||||
}
|
|
||||||
|
|
||||||
COLORS_EXPLORED = {
|
|
||||||
'wall': (50, 50, 70), # Darker, bluish
|
|
||||||
'floor': (20, 20, 30), # Very dark
|
|
||||||
'tunnel': (15, 15, 25), # Almost black
|
|
||||||
}
|
|
||||||
|
|
||||||
# Update the tile-setting methods to store the tile type
|
|
||||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
|
||||||
"""Set properties for a specific tile"""
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
cell = self.grid.at(x, y)
|
|
||||||
cell.walkable = walkable
|
|
||||||
cell.transparent = transparent
|
|
||||||
cell.sprite_index = sprite_index
|
|
||||||
# Store both visible and explored colors
|
|
||||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
|
||||||
# The engine will automatically darken explored tiles
|
|
||||||
```
|
|
||||||
|
|
||||||
## Complete Implementation
|
|
||||||
|
|
||||||
Here's the complete updated `game.py` with FOV:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Color configurations for visibility
|
|
||||||
COLORS_VISIBLE = {
|
|
||||||
'wall': (100, 100, 100),
|
|
||||||
'floor': (50, 50, 50),
|
|
||||||
'tunnel': (30, 30, 40),
|
|
||||||
}
|
|
||||||
|
|
||||||
class GameObject:
|
|
||||||
"""Base class for all game objects"""
|
|
||||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.sprite_index = sprite_index
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks
|
|
||||||
self._entity = None
|
|
||||||
self.grid = None
|
|
||||||
|
|
||||||
def attach_to_grid(self, grid):
|
|
||||||
"""Attach this game object to a McRogueFace grid"""
|
|
||||||
self.grid = grid
|
|
||||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
|
||||||
self._entity.sprite_index = self.sprite_index
|
|
||||||
self._entity.color = mcrfpy.Color(*self.color)
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
"""Move by the given amount"""
|
|
||||||
if not self.grid:
|
|
||||||
return
|
|
||||||
self.x += dx
|
|
||||||
self.y += dy
|
|
||||||
if self._entity:
|
|
||||||
self._entity.x = self.x
|
|
||||||
self._entity.y = self.y
|
|
||||||
# Update FOV when player moves
|
|
||||||
if self.name == "Player":
|
|
||||||
self.update_fov()
|
|
||||||
|
|
||||||
def update_fov(self):
|
|
||||||
"""Update field of view from this entity's position"""
|
|
||||||
if self._entity and self.grid:
|
|
||||||
self._entity.update_fov(radius=8)
|
|
||||||
|
|
||||||
class RectangularRoom:
|
|
||||||
"""A rectangular room with its position and size"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, width, height):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + width
|
|
||||||
self.y2 = y + height
|
|
||||||
|
|
||||||
@property
|
|
||||||
def center(self):
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return center_x, center_y
|
|
||||||
|
|
||||||
@property
|
|
||||||
def inner(self):
|
|
||||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
return (
|
|
||||||
self.x1 <= other.x2
|
|
||||||
and self.x2 >= other.x1
|
|
||||||
and self.y1 <= other.y2
|
|
||||||
and self.y2 >= other.y1
|
|
||||||
)
|
|
||||||
|
|
||||||
def tunnel_between(start, end):
|
|
||||||
"""Return an L-shaped tunnel between two points"""
|
|
||||||
x1, y1 = start
|
|
||||||
x2, y2 = end
|
|
||||||
|
|
||||||
if random.random() < 0.5:
|
|
||||||
corner_x = x2
|
|
||||||
corner_y = y1
|
|
||||||
else:
|
|
||||||
corner_x = x1
|
|
||||||
corner_y = y2
|
|
||||||
|
|
||||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
|
||||||
yield x, y1
|
|
||||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
|
||||||
yield corner_x, y
|
|
||||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
|
||||||
yield x, corner_y
|
|
||||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
|
||||||
yield x2, y
|
|
||||||
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def __init__(self, width, height):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.grid = None
|
|
||||||
self.entities = []
|
|
||||||
self.rooms = []
|
|
||||||
|
|
||||||
def create_grid(self, tileset):
|
|
||||||
"""Create the McRogueFace grid"""
|
|
||||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
|
||||||
self.grid.position = (100, 100)
|
|
||||||
self.grid.size = (800, 480)
|
|
||||||
|
|
||||||
# Enable perspective rendering (0 = first entity = player)
|
|
||||||
self.grid.perspective = 0
|
|
||||||
|
|
||||||
return self.grid
|
|
||||||
|
|
||||||
def fill_with_walls(self):
|
|
||||||
"""Fill the entire map with wall tiles"""
|
|
||||||
for y in range(self.height):
|
|
||||||
for x in range(self.width):
|
|
||||||
self.set_tile(x, y, walkable=False, transparent=False,
|
|
||||||
sprite_index=35, tile_type='wall')
|
|
||||||
|
|
||||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
|
||||||
"""Set properties for a specific tile"""
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
cell = self.grid.at(x, y)
|
|
||||||
cell.walkable = walkable
|
|
||||||
cell.transparent = transparent
|
|
||||||
cell.sprite_index = sprite_index
|
|
||||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
|
||||||
|
|
||||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
|
|
||||||
"""Generate a new dungeon map"""
|
|
||||||
self.fill_with_walls()
|
|
||||||
|
|
||||||
for r in range(max_rooms):
|
|
||||||
room_width = random.randint(room_min_size, room_max_size)
|
|
||||||
room_height = random.randint(room_min_size, room_max_size)
|
|
||||||
|
|
||||||
x = random.randint(0, self.width - room_width - 1)
|
|
||||||
y = random.randint(0, self.height - room_height - 1)
|
|
||||||
|
|
||||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
|
||||||
|
|
||||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.carve_room(new_room)
|
|
||||||
|
|
||||||
if len(self.rooms) == 0:
|
|
||||||
player.x, player.y = new_room.center
|
|
||||||
if player._entity:
|
|
||||||
player._entity.x, player._entity.y = new_room.center
|
|
||||||
else:
|
|
||||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
|
||||||
|
|
||||||
self.rooms.append(new_room)
|
|
||||||
|
|
||||||
def carve_room(self, room):
|
|
||||||
"""Carve out a room"""
|
|
||||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
|
||||||
|
|
||||||
for y in range(inner_y1, inner_y2):
|
|
||||||
for x in range(inner_x1, inner_x2):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, tile_type='floor')
|
|
||||||
|
|
||||||
def carve_tunnel(self, start, end):
|
|
||||||
"""Carve a tunnel between two points"""
|
|
||||||
for x, y in tunnel_between(start, end):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, tile_type='tunnel')
|
|
||||||
|
|
||||||
def is_blocked(self, x, y):
|
|
||||||
"""Check if a tile blocks movement"""
|
|
||||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
|
||||||
return True
|
|
||||||
if not self.grid.at(x, y).walkable:
|
|
||||||
return True
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_entity(self, entity):
|
|
||||||
"""Add a GameObject to the map"""
|
|
||||||
self.entities.append(entity)
|
|
||||||
entity.attach_to_grid(self.grid)
|
|
||||||
|
|
||||||
class Engine:
|
|
||||||
"""Main game engine"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.game_map = None
|
|
||||||
self.player = None
|
|
||||||
self.entities = []
|
|
||||||
self.fov_radius = 8
|
|
||||||
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 4"
|
|
||||||
|
|
||||||
self.ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
self.ui.append(background)
|
|
||||||
|
|
||||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
self.setup_game()
|
|
||||||
self.setup_input()
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
def setup_game(self):
|
|
||||||
"""Initialize the game world"""
|
|
||||||
self.game_map = GameMap(80, 45)
|
|
||||||
grid = self.game_map.create_grid(self.tileset)
|
|
||||||
self.ui.append(grid)
|
|
||||||
|
|
||||||
# Create player
|
|
||||||
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
|
||||||
|
|
||||||
# Generate the dungeon
|
|
||||||
self.game_map.generate_dungeon(
|
|
||||||
max_rooms=30,
|
|
||||||
room_min_size=6,
|
|
||||||
room_max_size=10,
|
|
||||||
player=self.player
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add player to map
|
|
||||||
self.game_map.add_entity(self.player)
|
|
||||||
|
|
||||||
# Add monsters in random rooms
|
|
||||||
for i in range(10):
|
|
||||||
if i < len(self.game_map.rooms) - 1:
|
|
||||||
room = self.game_map.rooms[i + 1]
|
|
||||||
x, y = room.center
|
|
||||||
|
|
||||||
# Randomly offset from center
|
|
||||||
x += random.randint(-2, 2)
|
|
||||||
y += random.randint(-2, 2)
|
|
||||||
|
|
||||||
# Make sure position is walkable
|
|
||||||
if self.game_map.grid.at(x, y).walkable:
|
|
||||||
if i % 2 == 0:
|
|
||||||
# Create an orc
|
|
||||||
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
|
||||||
self.game_map.add_entity(orc)
|
|
||||||
self.entities.append(orc)
|
|
||||||
else:
|
|
||||||
# Create a troll
|
|
||||||
troll = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
|
||||||
self.game_map.add_entity(troll)
|
|
||||||
self.entities.append(troll)
|
|
||||||
|
|
||||||
# Initial FOV calculation
|
|
||||||
self.player.update_fov()
|
|
||||||
|
|
||||||
def handle_movement(self, dx, dy):
|
|
||||||
"""Handle player movement"""
|
|
||||||
new_x = self.player.x + dx
|
|
||||||
new_y = self.player.y + dy
|
|
||||||
|
|
||||||
if not self.game_map.is_blocked(new_x, new_y):
|
|
||||||
self.player.move(dx, dy)
|
|
||||||
|
|
||||||
def setup_input(self):
|
|
||||||
"""Setup keyboard input handling"""
|
|
||||||
def handle_keys(key, state):
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
movement = {
|
|
||||||
"Up": (0, -1), "Down": (0, 1),
|
|
||||||
"Left": (-1, 0), "Right": (1, 0),
|
|
||||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
|
||||||
"Num4": (-1, 0), "Num6": (1, 0),
|
|
||||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
if key in movement:
|
|
||||||
dx, dy = movement[key]
|
|
||||||
self.handle_movement(dx, dy)
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
elif key == "v":
|
|
||||||
# Toggle FOV on/off
|
|
||||||
if self.game_map.grid.perspective == 0:
|
|
||||||
self.game_map.grid.perspective = -1 # Omniscient
|
|
||||||
print("FOV disabled - omniscient view")
|
|
||||||
else:
|
|
||||||
self.game_map.grid.perspective = 0 # Player perspective
|
|
||||||
print("FOV enabled - player perspective")
|
|
||||||
elif key == "Plus" or key == "Equals":
|
|
||||||
# Increase FOV radius
|
|
||||||
self.fov_radius = min(self.fov_radius + 1, 20)
|
|
||||||
self.player._entity.update_fov(radius=self.fov_radius)
|
|
||||||
print(f"FOV radius: {self.fov_radius}")
|
|
||||||
elif key == "Minus":
|
|
||||||
# Decrease FOV radius
|
|
||||||
self.fov_radius = max(self.fov_radius - 1, 3)
|
|
||||||
self.player._entity.update_fov(radius=self.fov_radius)
|
|
||||||
print(f"FOV radius: {self.fov_radius}")
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Setup UI elements"""
|
|
||||||
title = mcrfpy.Caption("Field of View", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
|
||||||
self.ui.append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption("Arrow keys to move | V to toggle FOV | +/- to adjust radius | ESC to quit", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
self.ui.append(instructions)
|
|
||||||
|
|
||||||
# FOV indicator
|
|
||||||
self.fov_text = mcrfpy.Caption(f"FOV Radius: {self.fov_radius}", 900, 100)
|
|
||||||
self.fov_text.font_size = 14
|
|
||||||
self.fov_text.fill_color = mcrfpy.Color(150, 200, 255)
|
|
||||||
self.ui.append(self.fov_text)
|
|
||||||
|
|
||||||
# Create and run the game
|
|
||||||
engine = Engine()
|
|
||||||
print("Part 4: Field of View!")
|
|
||||||
print("Press V to toggle FOV on/off")
|
|
||||||
print("Press +/- to adjust FOV radius")
|
|
||||||
```
|
|
||||||
|
|
||||||
## How FOV Works
|
|
||||||
|
|
||||||
McRogueFace's built-in FOV system uses a shadowcasting algorithm that:
|
|
||||||
|
|
||||||
1. **Casts rays** from the player's position to tiles within the radius
|
|
||||||
2. **Checks transparency** along each ray path
|
|
||||||
3. **Marks tiles as visible** if the ray reaches them unobstructed
|
|
||||||
4. **Remembers explored tiles** automatically
|
|
||||||
|
|
||||||
The engine handles all the complex calculations in C++ for optimal performance.
|
|
||||||
|
|
||||||
## Visibility States in Detail
|
|
||||||
|
|
||||||
### Visible Tiles
|
|
||||||
- Currently in the player's line of sight
|
|
||||||
- Rendered at full brightness
|
|
||||||
- Show current entity positions
|
|
||||||
|
|
||||||
### Explored Tiles
|
|
||||||
- Previously seen but not currently visible
|
|
||||||
- Rendered darker/muted
|
|
||||||
- Show remembered terrain but not entities
|
|
||||||
|
|
||||||
### Unexplored Tiles
|
|
||||||
- Never been in the player's FOV
|
|
||||||
- Rendered as black/invisible
|
|
||||||
- Complete mystery to the player
|
|
||||||
|
|
||||||
## FOV Parameters
|
|
||||||
|
|
||||||
You can customize FOV behavior:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Basic FOV update
|
|
||||||
entity.update_fov(radius=8)
|
|
||||||
|
|
||||||
# The grid's perspective property controls rendering:
|
|
||||||
grid.perspective = 0 # Use first entity's FOV (player)
|
|
||||||
grid.perspective = 1 # Use second entity's FOV
|
|
||||||
grid.perspective = -1 # Omniscient (no FOV, see everything)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
McRogueFace's C++ FOV implementation is highly optimized:
|
|
||||||
- Uses efficient shadowcasting algorithm
|
|
||||||
- Only recalculates when needed
|
|
||||||
- Handles large maps smoothly
|
|
||||||
- Automatically culls entities outside FOV
|
|
||||||
|
|
||||||
## Visual Polish
|
|
||||||
|
|
||||||
The engine automatically handles visual transitions:
|
|
||||||
- Smooth color changes between visibility states
|
|
||||||
- Entities fade in/out of view
|
|
||||||
- Explored areas remain visible but dimmed
|
|
||||||
|
|
||||||
## Exercises
|
|
||||||
|
|
||||||
1. **Variable Vision**: Give different entities different FOV radii
|
|
||||||
2. **Light Sources**: Create torches that expand local FOV
|
|
||||||
3. **Blind Spots**: Add pillars that create interesting shadows
|
|
||||||
4. **X-Ray Vision**: Temporary power-up to see through walls
|
|
||||||
|
|
||||||
## What's Next?
|
|
||||||
|
|
||||||
In Part 5, we'll place enemies throughout the dungeon and implement basic interactions. With FOV in place, enemies will appear and disappear as you explore, creating tension and surprise!
|
|
||||||
|
|
||||||
Field of View transforms our dungeon from a tactical puzzle into a mysterious world to explore. The fog of war adds atmosphere and gameplay depth that's essential to the roguelike experience.
|
|
||||||
|
|
@ -1,334 +0,0 @@
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Color configurations for visibility
|
|
||||||
COLORS_VISIBLE = {
|
|
||||||
'wall': (100, 100, 100),
|
|
||||||
'floor': (50, 50, 50),
|
|
||||||
'tunnel': (30, 30, 40),
|
|
||||||
}
|
|
||||||
|
|
||||||
class GameObject:
|
|
||||||
"""Base class for all game objects"""
|
|
||||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.sprite_index = sprite_index
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks
|
|
||||||
self._entity = None
|
|
||||||
self.grid = None
|
|
||||||
|
|
||||||
def attach_to_grid(self, grid):
|
|
||||||
"""Attach this game object to a McRogueFace grid"""
|
|
||||||
self.grid = grid
|
|
||||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
|
||||||
self._entity.sprite_index = self.sprite_index
|
|
||||||
self._entity.color = mcrfpy.Color(*self.color)
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
"""Move by the given amount"""
|
|
||||||
if not self.grid:
|
|
||||||
return
|
|
||||||
self.x += dx
|
|
||||||
self.y += dy
|
|
||||||
if self._entity:
|
|
||||||
self._entity.x = self.x
|
|
||||||
self._entity.y = self.y
|
|
||||||
# Update FOV when player moves
|
|
||||||
if self.name == "Player":
|
|
||||||
self.update_fov()
|
|
||||||
|
|
||||||
def update_fov(self):
|
|
||||||
"""Update field of view from this entity's position"""
|
|
||||||
if self._entity and self.grid:
|
|
||||||
self._entity.update_fov(radius=8)
|
|
||||||
|
|
||||||
class RectangularRoom:
|
|
||||||
"""A rectangular room with its position and size"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, width, height):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + width
|
|
||||||
self.y2 = y + height
|
|
||||||
|
|
||||||
@property
|
|
||||||
def center(self):
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return center_x, center_y
|
|
||||||
|
|
||||||
@property
|
|
||||||
def inner(self):
|
|
||||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
return (
|
|
||||||
self.x1 <= other.x2
|
|
||||||
and self.x2 >= other.x1
|
|
||||||
and self.y1 <= other.y2
|
|
||||||
and self.y2 >= other.y1
|
|
||||||
)
|
|
||||||
|
|
||||||
def tunnel_between(start, end):
|
|
||||||
"""Return an L-shaped tunnel between two points"""
|
|
||||||
x1, y1 = start
|
|
||||||
x2, y2 = end
|
|
||||||
|
|
||||||
if random.random() < 0.5:
|
|
||||||
corner_x = x2
|
|
||||||
corner_y = y1
|
|
||||||
else:
|
|
||||||
corner_x = x1
|
|
||||||
corner_y = y2
|
|
||||||
|
|
||||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
|
||||||
yield x, y1
|
|
||||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
|
||||||
yield corner_x, y
|
|
||||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
|
||||||
yield x, corner_y
|
|
||||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
|
||||||
yield x2, y
|
|
||||||
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def __init__(self, width, height):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.grid = None
|
|
||||||
self.entities = []
|
|
||||||
self.rooms = []
|
|
||||||
|
|
||||||
def create_grid(self, tileset):
|
|
||||||
"""Create the McRogueFace grid"""
|
|
||||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
|
||||||
self.grid.position = (100, 100)
|
|
||||||
self.grid.size = (800, 480)
|
|
||||||
|
|
||||||
# Enable perspective rendering (0 = first entity = player)
|
|
||||||
self.grid.perspective = 0
|
|
||||||
|
|
||||||
return self.grid
|
|
||||||
|
|
||||||
def fill_with_walls(self):
|
|
||||||
"""Fill the entire map with wall tiles"""
|
|
||||||
for y in range(self.height):
|
|
||||||
for x in range(self.width):
|
|
||||||
self.set_tile(x, y, walkable=False, transparent=False,
|
|
||||||
sprite_index=35, tile_type='wall')
|
|
||||||
|
|
||||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
|
||||||
"""Set properties for a specific tile"""
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
cell = self.grid.at(x, y)
|
|
||||||
cell.walkable = walkable
|
|
||||||
cell.transparent = transparent
|
|
||||||
cell.sprite_index = sprite_index
|
|
||||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
|
||||||
|
|
||||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
|
|
||||||
"""Generate a new dungeon map"""
|
|
||||||
self.fill_with_walls()
|
|
||||||
|
|
||||||
for r in range(max_rooms):
|
|
||||||
room_width = random.randint(room_min_size, room_max_size)
|
|
||||||
room_height = random.randint(room_min_size, room_max_size)
|
|
||||||
|
|
||||||
x = random.randint(0, self.width - room_width - 1)
|
|
||||||
y = random.randint(0, self.height - room_height - 1)
|
|
||||||
|
|
||||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
|
||||||
|
|
||||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.carve_room(new_room)
|
|
||||||
|
|
||||||
if len(self.rooms) == 0:
|
|
||||||
player.x, player.y = new_room.center
|
|
||||||
if player._entity:
|
|
||||||
player._entity.x, player._entity.y = new_room.center
|
|
||||||
else:
|
|
||||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
|
||||||
|
|
||||||
self.rooms.append(new_room)
|
|
||||||
|
|
||||||
def carve_room(self, room):
|
|
||||||
"""Carve out a room"""
|
|
||||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
|
||||||
|
|
||||||
for y in range(inner_y1, inner_y2):
|
|
||||||
for x in range(inner_x1, inner_x2):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, tile_type='floor')
|
|
||||||
|
|
||||||
def carve_tunnel(self, start, end):
|
|
||||||
"""Carve a tunnel between two points"""
|
|
||||||
for x, y in tunnel_between(start, end):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, tile_type='tunnel')
|
|
||||||
|
|
||||||
def is_blocked(self, x, y):
|
|
||||||
"""Check if a tile blocks movement"""
|
|
||||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
|
||||||
return True
|
|
||||||
if not self.grid.at(x, y).walkable:
|
|
||||||
return True
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_entity(self, entity):
|
|
||||||
"""Add a GameObject to the map"""
|
|
||||||
self.entities.append(entity)
|
|
||||||
entity.attach_to_grid(self.grid)
|
|
||||||
|
|
||||||
class Engine:
|
|
||||||
"""Main game engine"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.game_map = None
|
|
||||||
self.player = None
|
|
||||||
self.entities = []
|
|
||||||
self.fov_radius = 8
|
|
||||||
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 4"
|
|
||||||
|
|
||||||
self.ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
self.ui.append(background)
|
|
||||||
|
|
||||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
self.setup_game()
|
|
||||||
self.setup_input()
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
def setup_game(self):
|
|
||||||
"""Initialize the game world"""
|
|
||||||
self.game_map = GameMap(80, 45)
|
|
||||||
grid = self.game_map.create_grid(self.tileset)
|
|
||||||
self.ui.append(grid)
|
|
||||||
|
|
||||||
# Create player
|
|
||||||
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
|
||||||
|
|
||||||
# Generate the dungeon
|
|
||||||
self.game_map.generate_dungeon(
|
|
||||||
max_rooms=30,
|
|
||||||
room_min_size=6,
|
|
||||||
room_max_size=10,
|
|
||||||
player=self.player
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add player to map
|
|
||||||
self.game_map.add_entity(self.player)
|
|
||||||
|
|
||||||
# Add monsters in random rooms
|
|
||||||
for i in range(10):
|
|
||||||
if i < len(self.game_map.rooms) - 1:
|
|
||||||
room = self.game_map.rooms[i + 1]
|
|
||||||
x, y = room.center
|
|
||||||
|
|
||||||
# Randomly offset from center
|
|
||||||
x += random.randint(-2, 2)
|
|
||||||
y += random.randint(-2, 2)
|
|
||||||
|
|
||||||
# Make sure position is walkable
|
|
||||||
if self.game_map.grid.at(x, y).walkable:
|
|
||||||
if i % 2 == 0:
|
|
||||||
# Create an orc
|
|
||||||
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
|
||||||
self.game_map.add_entity(orc)
|
|
||||||
self.entities.append(orc)
|
|
||||||
else:
|
|
||||||
# Create a troll
|
|
||||||
troll = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
|
||||||
self.game_map.add_entity(troll)
|
|
||||||
self.entities.append(troll)
|
|
||||||
|
|
||||||
# Initial FOV calculation
|
|
||||||
self.player.update_fov()
|
|
||||||
|
|
||||||
def handle_movement(self, dx, dy):
|
|
||||||
"""Handle player movement"""
|
|
||||||
new_x = self.player.x + dx
|
|
||||||
new_y = self.player.y + dy
|
|
||||||
|
|
||||||
if not self.game_map.is_blocked(new_x, new_y):
|
|
||||||
self.player.move(dx, dy)
|
|
||||||
|
|
||||||
def setup_input(self):
|
|
||||||
"""Setup keyboard input handling"""
|
|
||||||
def handle_keys(key, state):
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
movement = {
|
|
||||||
"Up": (0, -1), "Down": (0, 1),
|
|
||||||
"Left": (-1, 0), "Right": (1, 0),
|
|
||||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
|
||||||
"Num4": (-1, 0), "Num6": (1, 0),
|
|
||||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
if key in movement:
|
|
||||||
dx, dy = movement[key]
|
|
||||||
self.handle_movement(dx, dy)
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
elif key == "v":
|
|
||||||
# Toggle FOV on/off
|
|
||||||
if self.game_map.grid.perspective == 0:
|
|
||||||
self.game_map.grid.perspective = -1 # Omniscient
|
|
||||||
print("FOV disabled - omniscient view")
|
|
||||||
else:
|
|
||||||
self.game_map.grid.perspective = 0 # Player perspective
|
|
||||||
print("FOV enabled - player perspective")
|
|
||||||
elif key == "Plus" or key == "Equals":
|
|
||||||
# Increase FOV radius
|
|
||||||
self.fov_radius = min(self.fov_radius + 1, 20)
|
|
||||||
self.player._entity.update_fov(radius=self.fov_radius)
|
|
||||||
print(f"FOV radius: {self.fov_radius}")
|
|
||||||
elif key == "Minus":
|
|
||||||
# Decrease FOV radius
|
|
||||||
self.fov_radius = max(self.fov_radius - 1, 3)
|
|
||||||
self.player._entity.update_fov(radius=self.fov_radius)
|
|
||||||
print(f"FOV radius: {self.fov_radius}")
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Setup UI elements"""
|
|
||||||
title = mcrfpy.Caption("Field of View", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
|
||||||
self.ui.append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption("Arrow keys to move | V to toggle FOV | +/- to adjust radius | ESC to quit", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
self.ui.append(instructions)
|
|
||||||
|
|
||||||
# FOV indicator
|
|
||||||
self.fov_text = mcrfpy.Caption(f"FOV Radius: {self.fov_radius}", 900, 100)
|
|
||||||
self.fov_text.font_size = 14
|
|
||||||
self.fov_text.fill_color = mcrfpy.Color(150, 200, 255)
|
|
||||||
self.ui.append(self.fov_text)
|
|
||||||
|
|
||||||
# Create and run the game
|
|
||||||
engine = Engine()
|
|
||||||
print("Part 4: Field of View!")
|
|
||||||
print("Press V to toggle FOV on/off")
|
|
||||||
print("Press +/- to adjust FOV radius")
|
|
||||||
|
|
@ -1,570 +0,0 @@
|
||||||
# Part 5 - Placing Enemies and Kicking Them (Harmlessly)
|
|
||||||
|
|
||||||
Now that we have Field of View working, it's time to populate our dungeon with enemies! In this part, we'll:
|
|
||||||
- Place enemies randomly in rooms
|
|
||||||
- Implement entity-to-entity collision detection
|
|
||||||
- Create basic interactions (bumping into enemies)
|
|
||||||
- Set the stage for combat in Part 6
|
|
||||||
|
|
||||||
## Enemy Spawning System
|
|
||||||
|
|
||||||
First, let's create a system to spawn enemies in our dungeon rooms. We'll avoid placing them in the first room (where the player starts) to give players a safe starting area.
|
|
||||||
|
|
||||||
```python
|
|
||||||
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
|
||||||
"""Spawn between 0 and max_enemies in a room"""
|
|
||||||
import random
|
|
||||||
|
|
||||||
number_of_enemies = random.randint(0, max_enemies)
|
|
||||||
|
|
||||||
for i in range(number_of_enemies):
|
|
||||||
# Try to find a valid position
|
|
||||||
attempts = 10
|
|
||||||
while attempts > 0:
|
|
||||||
# Random position within room bounds
|
|
||||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
|
||||||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
|
||||||
|
|
||||||
# Check if position is valid
|
|
||||||
if not game_map.is_blocked(x, y):
|
|
||||||
# 80% chance for orc, 20% for troll
|
|
||||||
if random.random() < 0.8:
|
|
||||||
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
|
||||||
else:
|
|
||||||
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
|
||||||
|
|
||||||
game_map.add_entity(enemy)
|
|
||||||
break
|
|
||||||
|
|
||||||
attempts -= 1
|
|
||||||
```
|
|
||||||
|
|
||||||
## Enhanced Collision Detection
|
|
||||||
|
|
||||||
We need to improve our collision detection to check for entities, not just walls:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def get_blocking_entity_at(self, x, y):
|
|
||||||
"""Return any blocking entity at the given position"""
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return entity
|
|
||||||
return None
|
|
||||||
|
|
||||||
def is_blocked(self, x, y):
|
|
||||||
"""Check if a tile blocks movement"""
|
|
||||||
# Check boundaries
|
|
||||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check walls
|
|
||||||
if not self.grid.at(x, y).walkable:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check entities
|
|
||||||
if self.get_blocking_entity_at(x, y):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
```
|
|
||||||
|
|
||||||
## Action System Introduction
|
|
||||||
|
|
||||||
Let's create a simple action system to handle different types of interactions:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class Action:
|
|
||||||
"""Base class for all actions"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class MovementAction(Action):
|
|
||||||
"""Action for moving an entity"""
|
|
||||||
def __init__(self, dx, dy):
|
|
||||||
self.dx = dx
|
|
||||||
self.dy = dy
|
|
||||||
|
|
||||||
class BumpAction(Action):
|
|
||||||
"""Action for bumping into something"""
|
|
||||||
def __init__(self, dx, dy, target=None):
|
|
||||||
self.dx = dx
|
|
||||||
self.dy = dy
|
|
||||||
self.target = target
|
|
||||||
|
|
||||||
class WaitAction(Action):
|
|
||||||
"""Action for waiting/skipping turn"""
|
|
||||||
pass
|
|
||||||
```
|
|
||||||
|
|
||||||
## Handling Player Actions
|
|
||||||
|
|
||||||
Now let's update our movement handling to support bumping into enemies:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def handle_player_turn(self, action):
|
|
||||||
"""Process the player's action"""
|
|
||||||
if isinstance(action, MovementAction):
|
|
||||||
dest_x = self.player.x + action.dx
|
|
||||||
dest_y = self.player.y + action.dy
|
|
||||||
|
|
||||||
# Check what's at the destination
|
|
||||||
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
|
||||||
|
|
||||||
if target:
|
|
||||||
# We bumped into something!
|
|
||||||
print(f"You kick the {target.name} in the shins, much to its annoyance!")
|
|
||||||
elif not self.game_map.is_blocked(dest_x, dest_y):
|
|
||||||
# Move the player
|
|
||||||
self.player.move(action.dx, action.dy)
|
|
||||||
# Update message
|
|
||||||
self.status_text.text = "Exploring the dungeon..."
|
|
||||||
else:
|
|
||||||
# Bumped into a wall
|
|
||||||
self.status_text.text = "Ouch! You bump into a wall."
|
|
||||||
|
|
||||||
elif isinstance(action, WaitAction):
|
|
||||||
self.status_text.text = "You wait..."
|
|
||||||
```
|
|
||||||
|
|
||||||
## Complete Updated Code
|
|
||||||
|
|
||||||
Here's the complete `game.py` with enemy placement and interactions:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Color configurations
|
|
||||||
COLORS_VISIBLE = {
|
|
||||||
'wall': (100, 100, 100),
|
|
||||||
'floor': (50, 50, 50),
|
|
||||||
'tunnel': (30, 30, 40),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Actions
|
|
||||||
class Action:
|
|
||||||
"""Base class for all actions"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class MovementAction(Action):
|
|
||||||
"""Action for moving an entity"""
|
|
||||||
def __init__(self, dx, dy):
|
|
||||||
self.dx = dx
|
|
||||||
self.dy = dy
|
|
||||||
|
|
||||||
class WaitAction(Action):
|
|
||||||
"""Action for waiting/skipping turn"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class GameObject:
|
|
||||||
"""Base class for all game objects"""
|
|
||||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.sprite_index = sprite_index
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks
|
|
||||||
self._entity = None
|
|
||||||
self.grid = None
|
|
||||||
|
|
||||||
def attach_to_grid(self, grid):
|
|
||||||
"""Attach this game object to a McRogueFace grid"""
|
|
||||||
self.grid = grid
|
|
||||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
|
||||||
self._entity.sprite_index = self.sprite_index
|
|
||||||
self._entity.color = mcrfpy.Color(*self.color)
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
"""Move by the given amount"""
|
|
||||||
if not self.grid:
|
|
||||||
return
|
|
||||||
self.x += dx
|
|
||||||
self.y += dy
|
|
||||||
if self._entity:
|
|
||||||
self._entity.x = self.x
|
|
||||||
self._entity.y = self.y
|
|
||||||
# Update FOV when player moves
|
|
||||||
if self.name == "Player":
|
|
||||||
self.update_fov()
|
|
||||||
|
|
||||||
def update_fov(self):
|
|
||||||
"""Update field of view from this entity's position"""
|
|
||||||
if self._entity and self.grid:
|
|
||||||
self._entity.update_fov(radius=8)
|
|
||||||
|
|
||||||
class RectangularRoom:
|
|
||||||
"""A rectangular room with its position and size"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, width, height):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + width
|
|
||||||
self.y2 = y + height
|
|
||||||
|
|
||||||
@property
|
|
||||||
def center(self):
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return center_x, center_y
|
|
||||||
|
|
||||||
@property
|
|
||||||
def inner(self):
|
|
||||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
return (
|
|
||||||
self.x1 <= other.x2
|
|
||||||
and self.x2 >= other.x1
|
|
||||||
and self.y1 <= other.y2
|
|
||||||
and self.y2 >= other.y1
|
|
||||||
)
|
|
||||||
|
|
||||||
def tunnel_between(start, end):
|
|
||||||
"""Return an L-shaped tunnel between two points"""
|
|
||||||
x1, y1 = start
|
|
||||||
x2, y2 = end
|
|
||||||
|
|
||||||
if random.random() < 0.5:
|
|
||||||
corner_x = x2
|
|
||||||
corner_y = y1
|
|
||||||
else:
|
|
||||||
corner_x = x1
|
|
||||||
corner_y = y2
|
|
||||||
|
|
||||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
|
||||||
yield x, y1
|
|
||||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
|
||||||
yield corner_x, y
|
|
||||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
|
||||||
yield x, corner_y
|
|
||||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
|
||||||
yield x2, y
|
|
||||||
|
|
||||||
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
|
||||||
"""Spawn between 0 and max_enemies in a room"""
|
|
||||||
number_of_enemies = random.randint(0, max_enemies)
|
|
||||||
|
|
||||||
enemies_spawned = []
|
|
||||||
|
|
||||||
for i in range(number_of_enemies):
|
|
||||||
# Try to find a valid position
|
|
||||||
attempts = 10
|
|
||||||
while attempts > 0:
|
|
||||||
# Random position within room bounds
|
|
||||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
|
||||||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
|
||||||
|
|
||||||
# Check if position is valid
|
|
||||||
if not game_map.is_blocked(x, y):
|
|
||||||
# 80% chance for orc, 20% for troll
|
|
||||||
if random.random() < 0.8:
|
|
||||||
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
|
||||||
else:
|
|
||||||
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
|
||||||
|
|
||||||
game_map.add_entity(enemy)
|
|
||||||
enemies_spawned.append(enemy)
|
|
||||||
break
|
|
||||||
|
|
||||||
attempts -= 1
|
|
||||||
|
|
||||||
return enemies_spawned
|
|
||||||
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def __init__(self, width, height):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.grid = None
|
|
||||||
self.entities = []
|
|
||||||
self.rooms = []
|
|
||||||
|
|
||||||
def create_grid(self, tileset):
|
|
||||||
"""Create the McRogueFace grid"""
|
|
||||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
|
||||||
self.grid.position = (100, 100)
|
|
||||||
self.grid.size = (800, 480)
|
|
||||||
|
|
||||||
# Enable perspective rendering
|
|
||||||
self.grid.perspective = 0
|
|
||||||
|
|
||||||
return self.grid
|
|
||||||
|
|
||||||
def fill_with_walls(self):
|
|
||||||
"""Fill the entire map with wall tiles"""
|
|
||||||
for y in range(self.height):
|
|
||||||
for x in range(self.width):
|
|
||||||
self.set_tile(x, y, walkable=False, transparent=False,
|
|
||||||
sprite_index=35, tile_type='wall')
|
|
||||||
|
|
||||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
|
||||||
"""Set properties for a specific tile"""
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
cell = self.grid.at(x, y)
|
|
||||||
cell.walkable = walkable
|
|
||||||
cell.transparent = transparent
|
|
||||||
cell.sprite_index = sprite_index
|
|
||||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
|
||||||
|
|
||||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
|
|
||||||
"""Generate a new dungeon map"""
|
|
||||||
self.fill_with_walls()
|
|
||||||
|
|
||||||
for r in range(max_rooms):
|
|
||||||
room_width = random.randint(room_min_size, room_max_size)
|
|
||||||
room_height = random.randint(room_min_size, room_max_size)
|
|
||||||
|
|
||||||
x = random.randint(0, self.width - room_width - 1)
|
|
||||||
y = random.randint(0, self.height - room_height - 1)
|
|
||||||
|
|
||||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
|
||||||
|
|
||||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.carve_room(new_room)
|
|
||||||
|
|
||||||
if len(self.rooms) == 0:
|
|
||||||
# First room - place player
|
|
||||||
player.x, player.y = new_room.center
|
|
||||||
if player._entity:
|
|
||||||
player._entity.x, player._entity.y = new_room.center
|
|
||||||
else:
|
|
||||||
# All other rooms - add tunnel and enemies
|
|
||||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
|
||||||
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
|
|
||||||
|
|
||||||
self.rooms.append(new_room)
|
|
||||||
|
|
||||||
def carve_room(self, room):
|
|
||||||
"""Carve out a room"""
|
|
||||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
|
||||||
|
|
||||||
for y in range(inner_y1, inner_y2):
|
|
||||||
for x in range(inner_x1, inner_x2):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, tile_type='floor')
|
|
||||||
|
|
||||||
def carve_tunnel(self, start, end):
|
|
||||||
"""Carve a tunnel between two points"""
|
|
||||||
for x, y in tunnel_between(start, end):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, tile_type='tunnel')
|
|
||||||
|
|
||||||
def get_blocking_entity_at(self, x, y):
|
|
||||||
"""Return any blocking entity at the given position"""
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return entity
|
|
||||||
return None
|
|
||||||
|
|
||||||
def is_blocked(self, x, y):
|
|
||||||
"""Check if a tile blocks movement"""
|
|
||||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if not self.grid.at(x, y).walkable:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if self.get_blocking_entity_at(x, y):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_entity(self, entity):
|
|
||||||
"""Add a GameObject to the map"""
|
|
||||||
self.entities.append(entity)
|
|
||||||
entity.attach_to_grid(self.grid)
|
|
||||||
|
|
||||||
class Engine:
|
|
||||||
"""Main game engine"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.game_map = None
|
|
||||||
self.player = None
|
|
||||||
self.entities = []
|
|
||||||
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 5"
|
|
||||||
|
|
||||||
self.ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
self.ui.append(background)
|
|
||||||
|
|
||||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
self.setup_game()
|
|
||||||
self.setup_input()
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
def setup_game(self):
|
|
||||||
"""Initialize the game world"""
|
|
||||||
self.game_map = GameMap(80, 45)
|
|
||||||
grid = self.game_map.create_grid(self.tileset)
|
|
||||||
self.ui.append(grid)
|
|
||||||
|
|
||||||
# Create player
|
|
||||||
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
|
||||||
|
|
||||||
# Generate the dungeon
|
|
||||||
self.game_map.generate_dungeon(
|
|
||||||
max_rooms=30,
|
|
||||||
room_min_size=6,
|
|
||||||
room_max_size=10,
|
|
||||||
player=self.player,
|
|
||||||
max_enemies_per_room=2
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add player to map
|
|
||||||
self.game_map.add_entity(self.player)
|
|
||||||
|
|
||||||
# Store reference to all entities
|
|
||||||
self.entities = [e for e in self.game_map.entities if e != self.player]
|
|
||||||
|
|
||||||
# Initial FOV calculation
|
|
||||||
self.player.update_fov()
|
|
||||||
|
|
||||||
def handle_player_turn(self, action):
|
|
||||||
"""Process the player's action"""
|
|
||||||
if isinstance(action, MovementAction):
|
|
||||||
dest_x = self.player.x + action.dx
|
|
||||||
dest_y = self.player.y + action.dy
|
|
||||||
|
|
||||||
# Check what's at the destination
|
|
||||||
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
|
||||||
|
|
||||||
if target:
|
|
||||||
# We bumped into something!
|
|
||||||
print(f"You kick the {target.name} in the shins, much to its annoyance!")
|
|
||||||
self.status_text.text = f"You kick the {target.name}!"
|
|
||||||
elif not self.game_map.is_blocked(dest_x, dest_y):
|
|
||||||
# Move the player
|
|
||||||
self.player.move(action.dx, action.dy)
|
|
||||||
self.status_text.text = ""
|
|
||||||
else:
|
|
||||||
# Bumped into a wall
|
|
||||||
self.status_text.text = "Blocked!"
|
|
||||||
|
|
||||||
elif isinstance(action, WaitAction):
|
|
||||||
self.status_text.text = "You wait..."
|
|
||||||
|
|
||||||
def setup_input(self):
|
|
||||||
"""Setup keyboard input handling"""
|
|
||||||
def handle_keys(key, state):
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
action = None
|
|
||||||
|
|
||||||
# Movement keys
|
|
||||||
movement = {
|
|
||||||
"Up": (0, -1), "Down": (0, 1),
|
|
||||||
"Left": (-1, 0), "Right": (1, 0),
|
|
||||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
|
||||||
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
|
|
||||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
if key in movement:
|
|
||||||
dx, dy = movement[key]
|
|
||||||
if dx == 0 and dy == 0:
|
|
||||||
action = WaitAction()
|
|
||||||
else:
|
|
||||||
action = MovementAction(dx, dy)
|
|
||||||
elif key == "Period":
|
|
||||||
action = WaitAction()
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Process the action
|
|
||||||
if action:
|
|
||||||
self.handle_player_turn(action)
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Setup UI elements"""
|
|
||||||
title = mcrfpy.Caption("Placing Enemies", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
|
||||||
self.ui.append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption("Arrow keys to move | . to wait | Bump into enemies! | ESC to quit", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
self.ui.append(instructions)
|
|
||||||
|
|
||||||
# Status text
|
|
||||||
self.status_text = mcrfpy.Caption("", 512, 600)
|
|
||||||
self.status_text.font_size = 18
|
|
||||||
self.status_text.fill_color = mcrfpy.Color(255, 200, 200)
|
|
||||||
self.ui.append(self.status_text)
|
|
||||||
|
|
||||||
# Entity count
|
|
||||||
entity_count = len(self.entities)
|
|
||||||
count_text = mcrfpy.Caption(f"Enemies: {entity_count}", 900, 100)
|
|
||||||
count_text.font_size = 14
|
|
||||||
count_text.fill_color = mcrfpy.Color(150, 150, 255)
|
|
||||||
self.ui.append(count_text)
|
|
||||||
|
|
||||||
# Create and run the game
|
|
||||||
engine = Engine()
|
|
||||||
print("Part 5: Placing Enemies!")
|
|
||||||
print("Try bumping into enemies - combat coming in Part 6!")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Understanding Entity Interactions
|
|
||||||
|
|
||||||
### Collision Detection
|
|
||||||
Our system now checks three things when the player tries to move:
|
|
||||||
1. **Map boundaries** - Can't move outside the map
|
|
||||||
2. **Wall tiles** - Can't walk through walls
|
|
||||||
3. **Blocking entities** - Can't walk through enemies
|
|
||||||
|
|
||||||
### The Action System
|
|
||||||
We've introduced a simple action system that will grow in Part 6:
|
|
||||||
- `Action` - Base class for all actions
|
|
||||||
- `MovementAction` - Represents attempted movement
|
|
||||||
- `WaitAction` - Skip a turn (important for turn-based games)
|
|
||||||
|
|
||||||
### Entity Spawning
|
|
||||||
Enemies are placed randomly in rooms with these rules:
|
|
||||||
- Never in the first room (player's starting room)
|
|
||||||
- Random number between 0 and max per room
|
|
||||||
- 80% orcs, 20% trolls
|
|
||||||
- Must be placed on walkable, unoccupied tiles
|
|
||||||
|
|
||||||
## Visual Feedback
|
|
||||||
|
|
||||||
With FOV enabled, enemies will appear and disappear as you explore:
|
|
||||||
- Enemies in sight are fully visible
|
|
||||||
- Enemies in explored but dark areas are hidden
|
|
||||||
- Creates tension and surprise encounters
|
|
||||||
|
|
||||||
## Exercises
|
|
||||||
|
|
||||||
1. **More Enemy Types**: Add different sprites and names (goblins, skeletons)
|
|
||||||
2. **Enemy Density**: Adjust spawn rates based on dungeon depth
|
|
||||||
3. **Special Rooms**: Create rooms with guaranteed enemies or treasures
|
|
||||||
4. **Better Feedback**: Add sound effects or visual effects for bumping
|
|
||||||
|
|
||||||
## What's Next?
|
|
||||||
|
|
||||||
In Part 6, we'll transform those harmless kicks into a real combat system! We'll add:
|
|
||||||
- Health points for all entities
|
|
||||||
- Damage calculations
|
|
||||||
- Death and corpses
|
|
||||||
- Combat messages
|
|
||||||
- The beginning of a real roguelike!
|
|
||||||
|
|
||||||
Right now our enemies are just obstacles. Soon they'll fight back!
|
|
||||||
|
|
@ -1,388 +0,0 @@
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Color configurations
|
|
||||||
COLORS_VISIBLE = {
|
|
||||||
'wall': (100, 100, 100),
|
|
||||||
'floor': (50, 50, 50),
|
|
||||||
'tunnel': (30, 30, 40),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Actions
|
|
||||||
class Action:
|
|
||||||
"""Base class for all actions"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class MovementAction(Action):
|
|
||||||
"""Action for moving an entity"""
|
|
||||||
def __init__(self, dx, dy):
|
|
||||||
self.dx = dx
|
|
||||||
self.dy = dy
|
|
||||||
|
|
||||||
class WaitAction(Action):
|
|
||||||
"""Action for waiting/skipping turn"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class GameObject:
|
|
||||||
"""Base class for all game objects"""
|
|
||||||
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.sprite_index = sprite_index
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks
|
|
||||||
self._entity = None
|
|
||||||
self.grid = None
|
|
||||||
|
|
||||||
def attach_to_grid(self, grid):
|
|
||||||
"""Attach this game object to a McRogueFace grid"""
|
|
||||||
self.grid = grid
|
|
||||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
|
||||||
self._entity.sprite_index = self.sprite_index
|
|
||||||
self._entity.color = mcrfpy.Color(*self.color)
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
"""Move by the given amount"""
|
|
||||||
if not self.grid:
|
|
||||||
return
|
|
||||||
self.x += dx
|
|
||||||
self.y += dy
|
|
||||||
if self._entity:
|
|
||||||
self._entity.x = self.x
|
|
||||||
self._entity.y = self.y
|
|
||||||
# Update FOV when player moves
|
|
||||||
if self.name == "Player":
|
|
||||||
self.update_fov()
|
|
||||||
|
|
||||||
def update_fov(self):
|
|
||||||
"""Update field of view from this entity's position"""
|
|
||||||
if self._entity and self.grid:
|
|
||||||
self._entity.update_fov(radius=8)
|
|
||||||
|
|
||||||
class RectangularRoom:
|
|
||||||
"""A rectangular room with its position and size"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, width, height):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + width
|
|
||||||
self.y2 = y + height
|
|
||||||
|
|
||||||
@property
|
|
||||||
def center(self):
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return center_x, center_y
|
|
||||||
|
|
||||||
@property
|
|
||||||
def inner(self):
|
|
||||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
return (
|
|
||||||
self.x1 <= other.x2
|
|
||||||
and self.x2 >= other.x1
|
|
||||||
and self.y1 <= other.y2
|
|
||||||
and self.y2 >= other.y1
|
|
||||||
)
|
|
||||||
|
|
||||||
def tunnel_between(start, end):
|
|
||||||
"""Return an L-shaped tunnel between two points"""
|
|
||||||
x1, y1 = start
|
|
||||||
x2, y2 = end
|
|
||||||
|
|
||||||
if random.random() < 0.5:
|
|
||||||
corner_x = x2
|
|
||||||
corner_y = y1
|
|
||||||
else:
|
|
||||||
corner_x = x1
|
|
||||||
corner_y = y2
|
|
||||||
|
|
||||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
|
||||||
yield x, y1
|
|
||||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
|
||||||
yield corner_x, y
|
|
||||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
|
||||||
yield x, corner_y
|
|
||||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
|
||||||
yield x2, y
|
|
||||||
|
|
||||||
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
|
||||||
"""Spawn between 0 and max_enemies in a room"""
|
|
||||||
number_of_enemies = random.randint(0, max_enemies)
|
|
||||||
|
|
||||||
enemies_spawned = []
|
|
||||||
|
|
||||||
for i in range(number_of_enemies):
|
|
||||||
# Try to find a valid position
|
|
||||||
attempts = 10
|
|
||||||
while attempts > 0:
|
|
||||||
# Random position within room bounds
|
|
||||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
|
||||||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
|
||||||
|
|
||||||
# Check if position is valid
|
|
||||||
if not game_map.is_blocked(x, y):
|
|
||||||
# 80% chance for orc, 20% for troll
|
|
||||||
if random.random() < 0.8:
|
|
||||||
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
|
||||||
else:
|
|
||||||
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
|
||||||
|
|
||||||
game_map.add_entity(enemy)
|
|
||||||
enemies_spawned.append(enemy)
|
|
||||||
break
|
|
||||||
|
|
||||||
attempts -= 1
|
|
||||||
|
|
||||||
return enemies_spawned
|
|
||||||
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def __init__(self, width, height):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.grid = None
|
|
||||||
self.entities = []
|
|
||||||
self.rooms = []
|
|
||||||
|
|
||||||
def create_grid(self, tileset):
|
|
||||||
"""Create the McRogueFace grid"""
|
|
||||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
|
||||||
self.grid.position = (100, 100)
|
|
||||||
self.grid.size = (800, 480)
|
|
||||||
|
|
||||||
# Enable perspective rendering
|
|
||||||
self.grid.perspective = 0
|
|
||||||
|
|
||||||
return self.grid
|
|
||||||
|
|
||||||
def fill_with_walls(self):
|
|
||||||
"""Fill the entire map with wall tiles"""
|
|
||||||
for y in range(self.height):
|
|
||||||
for x in range(self.width):
|
|
||||||
self.set_tile(x, y, walkable=False, transparent=False,
|
|
||||||
sprite_index=35, tile_type='wall')
|
|
||||||
|
|
||||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
|
||||||
"""Set properties for a specific tile"""
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
cell = self.grid.at(x, y)
|
|
||||||
cell.walkable = walkable
|
|
||||||
cell.transparent = transparent
|
|
||||||
cell.sprite_index = sprite_index
|
|
||||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
|
||||||
|
|
||||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
|
|
||||||
"""Generate a new dungeon map"""
|
|
||||||
self.fill_with_walls()
|
|
||||||
|
|
||||||
for r in range(max_rooms):
|
|
||||||
room_width = random.randint(room_min_size, room_max_size)
|
|
||||||
room_height = random.randint(room_min_size, room_max_size)
|
|
||||||
|
|
||||||
x = random.randint(0, self.width - room_width - 1)
|
|
||||||
y = random.randint(0, self.height - room_height - 1)
|
|
||||||
|
|
||||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
|
||||||
|
|
||||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.carve_room(new_room)
|
|
||||||
|
|
||||||
if len(self.rooms) == 0:
|
|
||||||
# First room - place player
|
|
||||||
player.x, player.y = new_room.center
|
|
||||||
if player._entity:
|
|
||||||
player._entity.x, player._entity.y = new_room.center
|
|
||||||
else:
|
|
||||||
# All other rooms - add tunnel and enemies
|
|
||||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
|
||||||
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
|
|
||||||
|
|
||||||
self.rooms.append(new_room)
|
|
||||||
|
|
||||||
def carve_room(self, room):
|
|
||||||
"""Carve out a room"""
|
|
||||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
|
||||||
|
|
||||||
for y in range(inner_y1, inner_y2):
|
|
||||||
for x in range(inner_x1, inner_x2):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, tile_type='floor')
|
|
||||||
|
|
||||||
def carve_tunnel(self, start, end):
|
|
||||||
"""Carve a tunnel between two points"""
|
|
||||||
for x, y in tunnel_between(start, end):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, tile_type='tunnel')
|
|
||||||
|
|
||||||
def get_blocking_entity_at(self, x, y):
|
|
||||||
"""Return any blocking entity at the given position"""
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return entity
|
|
||||||
return None
|
|
||||||
|
|
||||||
def is_blocked(self, x, y):
|
|
||||||
"""Check if a tile blocks movement"""
|
|
||||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if not self.grid.at(x, y).walkable:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if self.get_blocking_entity_at(x, y):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_entity(self, entity):
|
|
||||||
"""Add a GameObject to the map"""
|
|
||||||
self.entities.append(entity)
|
|
||||||
entity.attach_to_grid(self.grid)
|
|
||||||
|
|
||||||
class Engine:
|
|
||||||
"""Main game engine"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.game_map = None
|
|
||||||
self.player = None
|
|
||||||
self.entities = []
|
|
||||||
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 5"
|
|
||||||
|
|
||||||
self.ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
self.ui.append(background)
|
|
||||||
|
|
||||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
self.setup_game()
|
|
||||||
self.setup_input()
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
def setup_game(self):
|
|
||||||
"""Initialize the game world"""
|
|
||||||
self.game_map = GameMap(80, 45)
|
|
||||||
grid = self.game_map.create_grid(self.tileset)
|
|
||||||
self.ui.append(grid)
|
|
||||||
|
|
||||||
# Create player
|
|
||||||
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
|
||||||
|
|
||||||
# Generate the dungeon
|
|
||||||
self.game_map.generate_dungeon(
|
|
||||||
max_rooms=30,
|
|
||||||
room_min_size=6,
|
|
||||||
room_max_size=10,
|
|
||||||
player=self.player,
|
|
||||||
max_enemies_per_room=2
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add player to map
|
|
||||||
self.game_map.add_entity(self.player)
|
|
||||||
|
|
||||||
# Store reference to all entities
|
|
||||||
self.entities = [e for e in self.game_map.entities if e != self.player]
|
|
||||||
|
|
||||||
# Initial FOV calculation
|
|
||||||
self.player.update_fov()
|
|
||||||
|
|
||||||
def handle_player_turn(self, action):
|
|
||||||
"""Process the player's action"""
|
|
||||||
if isinstance(action, MovementAction):
|
|
||||||
dest_x = self.player.x + action.dx
|
|
||||||
dest_y = self.player.y + action.dy
|
|
||||||
|
|
||||||
# Check what's at the destination
|
|
||||||
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
|
||||||
|
|
||||||
if target:
|
|
||||||
# We bumped into something!
|
|
||||||
print(f"You kick the {target.name} in the shins, much to its annoyance!")
|
|
||||||
self.status_text.text = f"You kick the {target.name}!"
|
|
||||||
elif not self.game_map.is_blocked(dest_x, dest_y):
|
|
||||||
# Move the player
|
|
||||||
self.player.move(action.dx, action.dy)
|
|
||||||
self.status_text.text = ""
|
|
||||||
else:
|
|
||||||
# Bumped into a wall
|
|
||||||
self.status_text.text = "Blocked!"
|
|
||||||
|
|
||||||
elif isinstance(action, WaitAction):
|
|
||||||
self.status_text.text = "You wait..."
|
|
||||||
|
|
||||||
def setup_input(self):
|
|
||||||
"""Setup keyboard input handling"""
|
|
||||||
def handle_keys(key, state):
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
action = None
|
|
||||||
|
|
||||||
# Movement keys
|
|
||||||
movement = {
|
|
||||||
"Up": (0, -1), "Down": (0, 1),
|
|
||||||
"Left": (-1, 0), "Right": (1, 0),
|
|
||||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
|
||||||
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
|
|
||||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
if key in movement:
|
|
||||||
dx, dy = movement[key]
|
|
||||||
if dx == 0 and dy == 0:
|
|
||||||
action = WaitAction()
|
|
||||||
else:
|
|
||||||
action = MovementAction(dx, dy)
|
|
||||||
elif key == "Period":
|
|
||||||
action = WaitAction()
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Process the action
|
|
||||||
if action:
|
|
||||||
self.handle_player_turn(action)
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Setup UI elements"""
|
|
||||||
title = mcrfpy.Caption("Placing Enemies", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
|
||||||
self.ui.append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption("Arrow keys to move | . to wait | Bump into enemies! | ESC to quit", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
self.ui.append(instructions)
|
|
||||||
|
|
||||||
# Status text
|
|
||||||
self.status_text = mcrfpy.Caption("", 512, 600)
|
|
||||||
self.status_text.font_size = 18
|
|
||||||
self.status_text.fill_color = mcrfpy.Color(255, 200, 200)
|
|
||||||
self.ui.append(self.status_text)
|
|
||||||
|
|
||||||
# Entity count
|
|
||||||
entity_count = len(self.entities)
|
|
||||||
count_text = mcrfpy.Caption(f"Enemies: {entity_count}", 900, 100)
|
|
||||||
count_text.font_size = 14
|
|
||||||
count_text.fill_color = mcrfpy.Color(150, 150, 255)
|
|
||||||
self.ui.append(count_text)
|
|
||||||
|
|
||||||
# Create and run the game
|
|
||||||
engine = Engine()
|
|
||||||
print("Part 5: Placing Enemies!")
|
|
||||||
print("Try bumping into enemies - combat coming in Part 6!")
|
|
||||||
|
|
@ -1,743 +0,0 @@
|
||||||
# Part 6 - Doing (and Taking) Some Damage
|
|
||||||
|
|
||||||
It's time to turn our harmless kicks into real combat! In this part, we'll implement:
|
|
||||||
- Health points for all entities
|
|
||||||
- A damage calculation system
|
|
||||||
- Death and corpse mechanics
|
|
||||||
- Combat feedback messages
|
|
||||||
- The foundation of tactical roguelike combat
|
|
||||||
|
|
||||||
## Adding Combat Stats
|
|
||||||
|
|
||||||
First, let's enhance our GameObject class with combat capabilities:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class GameObject:
|
|
||||||
"""Base class for all game objects"""
|
|
||||||
def __init__(self, x, y, sprite_index, color, name,
|
|
||||||
blocks=False, hp=0, defense=0, power=0):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.sprite_index = sprite_index
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks
|
|
||||||
self._entity = None
|
|
||||||
self.grid = None
|
|
||||||
|
|
||||||
# Combat stats
|
|
||||||
self.max_hp = hp
|
|
||||||
self.hp = hp
|
|
||||||
self.defense = defense
|
|
||||||
self.power = power
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_alive(self):
|
|
||||||
"""Returns True if this entity can act"""
|
|
||||||
return self.hp > 0
|
|
||||||
|
|
||||||
def take_damage(self, amount):
|
|
||||||
"""Apply damage to this entity"""
|
|
||||||
damage = amount - self.defense
|
|
||||||
if damage > 0:
|
|
||||||
self.hp -= damage
|
|
||||||
|
|
||||||
# Check for death
|
|
||||||
if self.hp <= 0 and self.hp + damage > 0:
|
|
||||||
self.die()
|
|
||||||
|
|
||||||
return damage
|
|
||||||
|
|
||||||
def die(self):
|
|
||||||
"""Handle entity death"""
|
|
||||||
if self.name == "Player":
|
|
||||||
# Player death is special - we'll handle it differently
|
|
||||||
self.sprite_index = 64 # Stay as @ but change color
|
|
||||||
self.color = (127, 0, 0) # Dark red
|
|
||||||
if self._entity:
|
|
||||||
self._entity.color = mcrfpy.Color(127, 0, 0)
|
|
||||||
print("You have died!")
|
|
||||||
else:
|
|
||||||
# Enemy death
|
|
||||||
self.sprite_index = 37 # % character for corpse
|
|
||||||
self.color = (127, 0, 0) # Dark red
|
|
||||||
self.blocks = False # Corpses don't block
|
|
||||||
self.name = f"remains of {self.name}"
|
|
||||||
|
|
||||||
if self._entity:
|
|
||||||
self._entity.sprite_index = 37
|
|
||||||
self._entity.color = mcrfpy.Color(127, 0, 0)
|
|
||||||
```
|
|
||||||
|
|
||||||
## The Combat System
|
|
||||||
|
|
||||||
Now let's implement actual combat when entities bump into each other:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class MeleeAction(Action):
|
|
||||||
"""Action for melee attacks"""
|
|
||||||
def __init__(self, attacker, target):
|
|
||||||
self.attacker = attacker
|
|
||||||
self.target = target
|
|
||||||
|
|
||||||
def perform(self):
|
|
||||||
"""Execute the attack"""
|
|
||||||
if not self.target.is_alive:
|
|
||||||
return # Can't attack the dead
|
|
||||||
|
|
||||||
damage = self.attacker.power - self.target.defense
|
|
||||||
|
|
||||||
if damage > 0:
|
|
||||||
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
|
|
||||||
self.target.take_damage(damage)
|
|
||||||
else:
|
|
||||||
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
|
|
||||||
|
|
||||||
return attack_desc
|
|
||||||
```
|
|
||||||
|
|
||||||
## Entity Factories
|
|
||||||
|
|
||||||
Let's create factory functions for consistent entity creation:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def create_player(x, y):
|
|
||||||
"""Create the player entity"""
|
|
||||||
return GameObject(
|
|
||||||
x=x, y=y,
|
|
||||||
sprite_index=64, # @
|
|
||||||
color=(255, 255, 255),
|
|
||||||
name="Player",
|
|
||||||
blocks=True,
|
|
||||||
hp=30,
|
|
||||||
defense=2,
|
|
||||||
power=5
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_orc(x, y):
|
|
||||||
"""Create an orc enemy"""
|
|
||||||
return GameObject(
|
|
||||||
x=x, y=y,
|
|
||||||
sprite_index=111, # o
|
|
||||||
color=(63, 127, 63),
|
|
||||||
name="Orc",
|
|
||||||
blocks=True,
|
|
||||||
hp=10,
|
|
||||||
defense=0,
|
|
||||||
power=3
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_troll(x, y):
|
|
||||||
"""Create a troll enemy"""
|
|
||||||
return GameObject(
|
|
||||||
x=x, y=y,
|
|
||||||
sprite_index=84, # T
|
|
||||||
color=(0, 127, 0),
|
|
||||||
name="Troll",
|
|
||||||
blocks=True,
|
|
||||||
hp=16,
|
|
||||||
defense=1,
|
|
||||||
power=4
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## The Message Log
|
|
||||||
|
|
||||||
Combat needs feedback! Let's create a simple message log:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class MessageLog:
|
|
||||||
"""Manages game messages"""
|
|
||||||
def __init__(self, max_messages=5):
|
|
||||||
self.messages = []
|
|
||||||
self.max_messages = max_messages
|
|
||||||
|
|
||||||
def add_message(self, text, color=(255, 255, 255)):
|
|
||||||
"""Add a message to the log"""
|
|
||||||
self.messages.append((text, color))
|
|
||||||
# Keep only recent messages
|
|
||||||
if len(self.messages) > self.max_messages:
|
|
||||||
self.messages.pop(0)
|
|
||||||
|
|
||||||
def render(self, ui, x, y, line_height=20):
|
|
||||||
"""Render messages to the UI"""
|
|
||||||
for i, (text, color) in enumerate(self.messages):
|
|
||||||
caption = mcrfpy.Caption(text, x, y + i * line_height)
|
|
||||||
caption.font_size = 14
|
|
||||||
caption.fill_color = mcrfpy.Color(*color)
|
|
||||||
ui.append(caption)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Complete Implementation
|
|
||||||
|
|
||||||
Here's the complete `game.py` with combat:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Color configurations
|
|
||||||
COLORS_VISIBLE = {
|
|
||||||
'wall': (100, 100, 100),
|
|
||||||
'floor': (50, 50, 50),
|
|
||||||
'tunnel': (30, 30, 40),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Message colors
|
|
||||||
COLOR_PLAYER_ATK = (230, 230, 230)
|
|
||||||
COLOR_ENEMY_ATK = (255, 200, 200)
|
|
||||||
COLOR_PLAYER_DIE = (255, 100, 100)
|
|
||||||
COLOR_ENEMY_DIE = (255, 165, 0)
|
|
||||||
|
|
||||||
# Actions
|
|
||||||
class Action:
|
|
||||||
"""Base class for all actions"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class MovementAction(Action):
|
|
||||||
"""Action for moving an entity"""
|
|
||||||
def __init__(self, dx, dy):
|
|
||||||
self.dx = dx
|
|
||||||
self.dy = dy
|
|
||||||
|
|
||||||
class MeleeAction(Action):
|
|
||||||
"""Action for melee attacks"""
|
|
||||||
def __init__(self, attacker, target):
|
|
||||||
self.attacker = attacker
|
|
||||||
self.target = target
|
|
||||||
|
|
||||||
def perform(self):
|
|
||||||
"""Execute the attack"""
|
|
||||||
if not self.target.is_alive:
|
|
||||||
return None
|
|
||||||
|
|
||||||
damage = self.attacker.power - self.target.defense
|
|
||||||
|
|
||||||
if damage > 0:
|
|
||||||
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
|
|
||||||
self.target.take_damage(damage)
|
|
||||||
|
|
||||||
# Choose color based on attacker
|
|
||||||
if self.attacker.name == "Player":
|
|
||||||
color = COLOR_PLAYER_ATK
|
|
||||||
else:
|
|
||||||
color = COLOR_ENEMY_ATK
|
|
||||||
|
|
||||||
return attack_desc, color
|
|
||||||
else:
|
|
||||||
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
|
|
||||||
return attack_desc, (150, 150, 150)
|
|
||||||
|
|
||||||
class WaitAction(Action):
|
|
||||||
"""Action for waiting/skipping turn"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class GameObject:
|
|
||||||
"""Base class for all game objects"""
|
|
||||||
def __init__(self, x, y, sprite_index, color, name,
|
|
||||||
blocks=False, hp=0, defense=0, power=0):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.sprite_index = sprite_index
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks
|
|
||||||
self._entity = None
|
|
||||||
self.grid = None
|
|
||||||
|
|
||||||
# Combat stats
|
|
||||||
self.max_hp = hp
|
|
||||||
self.hp = hp
|
|
||||||
self.defense = defense
|
|
||||||
self.power = power
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_alive(self):
|
|
||||||
"""Returns True if this entity can act"""
|
|
||||||
return self.hp > 0
|
|
||||||
|
|
||||||
def attach_to_grid(self, grid):
|
|
||||||
"""Attach this game object to a McRogueFace grid"""
|
|
||||||
self.grid = grid
|
|
||||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
|
||||||
self._entity.sprite_index = self.sprite_index
|
|
||||||
self._entity.color = mcrfpy.Color(*self.color)
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
"""Move by the given amount"""
|
|
||||||
if not self.grid:
|
|
||||||
return
|
|
||||||
self.x += dx
|
|
||||||
self.y += dy
|
|
||||||
if self._entity:
|
|
||||||
self._entity.x = self.x
|
|
||||||
self._entity.y = self.y
|
|
||||||
# Update FOV when player moves
|
|
||||||
if self.name == "Player":
|
|
||||||
self.update_fov()
|
|
||||||
|
|
||||||
def update_fov(self):
|
|
||||||
"""Update field of view from this entity's position"""
|
|
||||||
if self._entity and self.grid:
|
|
||||||
self._entity.update_fov(radius=8)
|
|
||||||
|
|
||||||
def take_damage(self, amount):
|
|
||||||
"""Apply damage to this entity"""
|
|
||||||
self.hp -= amount
|
|
||||||
|
|
||||||
# Check for death
|
|
||||||
if self.hp <= 0:
|
|
||||||
self.die()
|
|
||||||
|
|
||||||
def die(self):
|
|
||||||
"""Handle entity death"""
|
|
||||||
if self.name == "Player":
|
|
||||||
# Player death
|
|
||||||
self.sprite_index = 64 # Stay as @
|
|
||||||
self.color = (127, 0, 0) # Dark red
|
|
||||||
if self._entity:
|
|
||||||
self._entity.color = mcrfpy.Color(127, 0, 0)
|
|
||||||
else:
|
|
||||||
# Enemy death
|
|
||||||
self.sprite_index = 37 # % character for corpse
|
|
||||||
self.color = (127, 0, 0) # Dark red
|
|
||||||
self.blocks = False # Corpses don't block
|
|
||||||
self.name = f"remains of {self.name}"
|
|
||||||
|
|
||||||
if self._entity:
|
|
||||||
self._entity.sprite_index = 37
|
|
||||||
self._entity.color = mcrfpy.Color(127, 0, 0)
|
|
||||||
|
|
||||||
# Entity factories
|
|
||||||
def create_player(x, y):
|
|
||||||
"""Create the player entity"""
|
|
||||||
return GameObject(
|
|
||||||
x=x, y=y,
|
|
||||||
sprite_index=64, # @
|
|
||||||
color=(255, 255, 255),
|
|
||||||
name="Player",
|
|
||||||
blocks=True,
|
|
||||||
hp=30,
|
|
||||||
defense=2,
|
|
||||||
power=5
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_orc(x, y):
|
|
||||||
"""Create an orc enemy"""
|
|
||||||
return GameObject(
|
|
||||||
x=x, y=y,
|
|
||||||
sprite_index=111, # o
|
|
||||||
color=(63, 127, 63),
|
|
||||||
name="Orc",
|
|
||||||
blocks=True,
|
|
||||||
hp=10,
|
|
||||||
defense=0,
|
|
||||||
power=3
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_troll(x, y):
|
|
||||||
"""Create a troll enemy"""
|
|
||||||
return GameObject(
|
|
||||||
x=x, y=y,
|
|
||||||
sprite_index=84, # T
|
|
||||||
color=(0, 127, 0),
|
|
||||||
name="Troll",
|
|
||||||
blocks=True,
|
|
||||||
hp=16,
|
|
||||||
defense=1,
|
|
||||||
power=4
|
|
||||||
)
|
|
||||||
|
|
||||||
class RectangularRoom:
|
|
||||||
"""A rectangular room with its position and size"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, width, height):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + width
|
|
||||||
self.y2 = y + height
|
|
||||||
|
|
||||||
@property
|
|
||||||
def center(self):
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return center_x, center_y
|
|
||||||
|
|
||||||
@property
|
|
||||||
def inner(self):
|
|
||||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
return (
|
|
||||||
self.x1 <= other.x2
|
|
||||||
and self.x2 >= other.x1
|
|
||||||
and self.y1 <= other.y2
|
|
||||||
and self.y2 >= other.y1
|
|
||||||
)
|
|
||||||
|
|
||||||
def tunnel_between(start, end):
|
|
||||||
"""Return an L-shaped tunnel between two points"""
|
|
||||||
x1, y1 = start
|
|
||||||
x2, y2 = end
|
|
||||||
|
|
||||||
if random.random() < 0.5:
|
|
||||||
corner_x = x2
|
|
||||||
corner_y = y1
|
|
||||||
else:
|
|
||||||
corner_x = x1
|
|
||||||
corner_y = y2
|
|
||||||
|
|
||||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
|
||||||
yield x, y1
|
|
||||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
|
||||||
yield corner_x, y
|
|
||||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
|
||||||
yield x, corner_y
|
|
||||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
|
||||||
yield x2, y
|
|
||||||
|
|
||||||
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
|
||||||
"""Spawn between 0 and max_enemies in a room"""
|
|
||||||
number_of_enemies = random.randint(0, max_enemies)
|
|
||||||
|
|
||||||
enemies_spawned = []
|
|
||||||
|
|
||||||
for i in range(number_of_enemies):
|
|
||||||
attempts = 10
|
|
||||||
while attempts > 0:
|
|
||||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
|
||||||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
|
||||||
|
|
||||||
if not game_map.is_blocked(x, y):
|
|
||||||
# 80% chance for orc, 20% for troll
|
|
||||||
if random.random() < 0.8:
|
|
||||||
enemy = create_orc(x, y)
|
|
||||||
else:
|
|
||||||
enemy = create_troll(x, y)
|
|
||||||
|
|
||||||
game_map.add_entity(enemy)
|
|
||||||
enemies_spawned.append(enemy)
|
|
||||||
break
|
|
||||||
|
|
||||||
attempts -= 1
|
|
||||||
|
|
||||||
return enemies_spawned
|
|
||||||
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def __init__(self, width, height):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.grid = None
|
|
||||||
self.entities = []
|
|
||||||
self.rooms = []
|
|
||||||
|
|
||||||
def create_grid(self, tileset):
|
|
||||||
"""Create the McRogueFace grid"""
|
|
||||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
|
||||||
self.grid.position = (100, 100)
|
|
||||||
self.grid.size = (800, 480)
|
|
||||||
|
|
||||||
# Enable perspective rendering
|
|
||||||
self.grid.perspective = 0
|
|
||||||
|
|
||||||
return self.grid
|
|
||||||
|
|
||||||
def fill_with_walls(self):
|
|
||||||
"""Fill the entire map with wall tiles"""
|
|
||||||
for y in range(self.height):
|
|
||||||
for x in range(self.width):
|
|
||||||
self.set_tile(x, y, walkable=False, transparent=False,
|
|
||||||
sprite_index=35, tile_type='wall')
|
|
||||||
|
|
||||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
|
||||||
"""Set properties for a specific tile"""
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
cell = self.grid.at(x, y)
|
|
||||||
cell.walkable = walkable
|
|
||||||
cell.transparent = transparent
|
|
||||||
cell.sprite_index = sprite_index
|
|
||||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
|
||||||
|
|
||||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
|
|
||||||
"""Generate a new dungeon map"""
|
|
||||||
self.fill_with_walls()
|
|
||||||
|
|
||||||
for r in range(max_rooms):
|
|
||||||
room_width = random.randint(room_min_size, room_max_size)
|
|
||||||
room_height = random.randint(room_min_size, room_max_size)
|
|
||||||
|
|
||||||
x = random.randint(0, self.width - room_width - 1)
|
|
||||||
y = random.randint(0, self.height - room_height - 1)
|
|
||||||
|
|
||||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
|
||||||
|
|
||||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.carve_room(new_room)
|
|
||||||
|
|
||||||
if len(self.rooms) == 0:
|
|
||||||
# First room - place player
|
|
||||||
player.x, player.y = new_room.center
|
|
||||||
if player._entity:
|
|
||||||
player._entity.x, player._entity.y = new_room.center
|
|
||||||
else:
|
|
||||||
# All other rooms - add tunnel and enemies
|
|
||||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
|
||||||
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
|
|
||||||
|
|
||||||
self.rooms.append(new_room)
|
|
||||||
|
|
||||||
def carve_room(self, room):
|
|
||||||
"""Carve out a room"""
|
|
||||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
|
||||||
|
|
||||||
for y in range(inner_y1, inner_y2):
|
|
||||||
for x in range(inner_x1, inner_x2):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, tile_type='floor')
|
|
||||||
|
|
||||||
def carve_tunnel(self, start, end):
|
|
||||||
"""Carve a tunnel between two points"""
|
|
||||||
for x, y in tunnel_between(start, end):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, tile_type='tunnel')
|
|
||||||
|
|
||||||
def get_blocking_entity_at(self, x, y):
|
|
||||||
"""Return any blocking entity at the given position"""
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return entity
|
|
||||||
return None
|
|
||||||
|
|
||||||
def is_blocked(self, x, y):
|
|
||||||
"""Check if a tile blocks movement"""
|
|
||||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if not self.grid.at(x, y).walkable:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if self.get_blocking_entity_at(x, y):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_entity(self, entity):
|
|
||||||
"""Add a GameObject to the map"""
|
|
||||||
self.entities.append(entity)
|
|
||||||
entity.attach_to_grid(self.grid)
|
|
||||||
|
|
||||||
class Engine:
|
|
||||||
"""Main game engine"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.game_map = None
|
|
||||||
self.player = None
|
|
||||||
self.entities = []
|
|
||||||
self.messages = [] # Simple message log
|
|
||||||
self.max_messages = 5
|
|
||||||
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 6"
|
|
||||||
|
|
||||||
self.ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
background = mcrfpy.Frame(0, 0, 1024, 768)
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
self.ui.append(background)
|
|
||||||
|
|
||||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
self.setup_game()
|
|
||||||
self.setup_input()
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
def add_message(self, text, color=(255, 255, 255)):
|
|
||||||
"""Add a message to the log"""
|
|
||||||
self.messages.append((text, color))
|
|
||||||
if len(self.messages) > self.max_messages:
|
|
||||||
self.messages.pop(0)
|
|
||||||
self.update_message_display()
|
|
||||||
|
|
||||||
def update_message_display(self):
|
|
||||||
"""Update the message display"""
|
|
||||||
# Clear old messages
|
|
||||||
for caption in self.message_captions:
|
|
||||||
# Remove from UI (McRogueFace doesn't have remove, so we hide it)
|
|
||||||
caption.text = ""
|
|
||||||
|
|
||||||
# Display current messages
|
|
||||||
for i, (text, color) in enumerate(self.messages):
|
|
||||||
if i < len(self.message_captions):
|
|
||||||
self.message_captions[i].text = text
|
|
||||||
self.message_captions[i].fill_color = mcrfpy.Color(*color)
|
|
||||||
|
|
||||||
def setup_game(self):
|
|
||||||
"""Initialize the game world"""
|
|
||||||
self.game_map = GameMap(80, 45)
|
|
||||||
grid = self.game_map.create_grid(self.tileset)
|
|
||||||
self.ui.append(grid)
|
|
||||||
|
|
||||||
# Create player
|
|
||||||
self.player = create_player(0, 0)
|
|
||||||
|
|
||||||
# Generate the dungeon
|
|
||||||
self.game_map.generate_dungeon(
|
|
||||||
max_rooms=30,
|
|
||||||
room_min_size=6,
|
|
||||||
room_max_size=10,
|
|
||||||
player=self.player,
|
|
||||||
max_enemies_per_room=2
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add player to map
|
|
||||||
self.game_map.add_entity(self.player)
|
|
||||||
|
|
||||||
# Store reference to all entities
|
|
||||||
self.entities = [e for e in self.game_map.entities if e != self.player]
|
|
||||||
|
|
||||||
# Initial FOV calculation
|
|
||||||
self.player.update_fov()
|
|
||||||
|
|
||||||
# Welcome message
|
|
||||||
self.add_message("Welcome to the dungeon!", (100, 100, 255))
|
|
||||||
|
|
||||||
def handle_player_turn(self, action):
|
|
||||||
"""Process the player's action"""
|
|
||||||
if not self.player.is_alive:
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(action, MovementAction):
|
|
||||||
dest_x = self.player.x + action.dx
|
|
||||||
dest_y = self.player.y + action.dy
|
|
||||||
|
|
||||||
# Check what's at the destination
|
|
||||||
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
|
||||||
|
|
||||||
if target:
|
|
||||||
# Attack!
|
|
||||||
attack = MeleeAction(self.player, target)
|
|
||||||
result = attack.perform()
|
|
||||||
if result:
|
|
||||||
text, color = result
|
|
||||||
self.add_message(text, color)
|
|
||||||
|
|
||||||
# Check if target died
|
|
||||||
if not target.is_alive:
|
|
||||||
death_msg = f"The {target.name.replace('remains of ', '')} is dead!"
|
|
||||||
self.add_message(death_msg, COLOR_ENEMY_DIE)
|
|
||||||
|
|
||||||
elif not self.game_map.is_blocked(dest_x, dest_y):
|
|
||||||
# Move the player
|
|
||||||
self.player.move(action.dx, action.dy)
|
|
||||||
|
|
||||||
elif isinstance(action, WaitAction):
|
|
||||||
pass # Do nothing
|
|
||||||
|
|
||||||
# Enemy turns
|
|
||||||
self.handle_enemy_turns()
|
|
||||||
|
|
||||||
def handle_enemy_turns(self):
|
|
||||||
"""Let all enemies take their turn"""
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.is_alive:
|
|
||||||
# Simple AI: if player is adjacent, attack. Otherwise, do nothing.
|
|
||||||
dx = entity.x - self.player.x
|
|
||||||
dy = entity.y - self.player.y
|
|
||||||
distance = abs(dx) + abs(dy)
|
|
||||||
|
|
||||||
if distance == 1: # Adjacent to player
|
|
||||||
attack = MeleeAction(entity, self.player)
|
|
||||||
result = attack.perform()
|
|
||||||
if result:
|
|
||||||
text, color = result
|
|
||||||
self.add_message(text, color)
|
|
||||||
|
|
||||||
# Check if player died
|
|
||||||
if not self.player.is_alive:
|
|
||||||
self.add_message("You have died!", COLOR_PLAYER_DIE)
|
|
||||||
|
|
||||||
def setup_input(self):
|
|
||||||
"""Setup keyboard input handling"""
|
|
||||||
def handle_keys(key, state):
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
action = None
|
|
||||||
|
|
||||||
# Movement keys
|
|
||||||
movement = {
|
|
||||||
"Up": (0, -1), "Down": (0, 1),
|
|
||||||
"Left": (-1, 0), "Right": (1, 0),
|
|
||||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
|
||||||
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
|
|
||||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
if key in movement:
|
|
||||||
dx, dy = movement[key]
|
|
||||||
if dx == 0 and dy == 0:
|
|
||||||
action = WaitAction()
|
|
||||||
else:
|
|
||||||
action = MovementAction(dx, dy)
|
|
||||||
elif key == "Period":
|
|
||||||
action = WaitAction()
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Process the action
|
|
||||||
if action:
|
|
||||||
self.handle_player_turn(action)
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Setup UI elements"""
|
|
||||||
title = mcrfpy.Caption("Combat System", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
|
||||||
self.ui.append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption("Attack enemies by bumping into them!", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
self.ui.append(instructions)
|
|
||||||
|
|
||||||
# Player stats
|
|
||||||
self.hp_text = mcrfpy.Caption(f"HP: {self.player.hp}/{self.player.max_hp}", 50, 100)
|
|
||||||
self.hp_text.font_size = 18
|
|
||||||
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
|
|
||||||
self.ui.append(self.hp_text)
|
|
||||||
|
|
||||||
# Message log
|
|
||||||
self.message_captions = []
|
|
||||||
for i in range(self.max_messages):
|
|
||||||
caption = mcrfpy.Caption("", 50, 620 + i * 20)
|
|
||||||
caption.font_size = 14
|
|
||||||
caption.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
self.ui.append(caption)
|
|
||||||
self.message_captions.append(caption)
|
|
||||||
|
|
||||||
# Timer to update HP display
|
|
||||||
def update_stats(dt):
|
|
||||||
self.hp_text.text = f"HP: {self.player.hp}/{self.player.max_hp}"
|
|
||||||
if self.player.hp <= 0:
|
|
||||||
self.hp_text.fill_color = mcrfpy.Color(127, 0, 0)
|
|
||||||
elif self.player.hp < self.player.max_hp // 3:
|
|
||||||
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
|
|
||||||
else:
|
|
||||||
self.hp_text.fill_color = mcrfpy.Color(0, 255, 0)
|
|
||||||
|
|
||||||
mcrfpy.setTimer("update_stats", update_stats, 100)
|
|
||||||
|
|
||||||
# Create and run the game
|
|
||||||
engine = Engine()
|
|
||||||
print("Part 6: Combat System!")
|
|
||||||
print("Attack enemies to defeat them, but watch your HP!")
|
|
||||||
|
|
@ -1,568 +0,0 @@
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Color configurations
|
|
||||||
COLORS_VISIBLE = {
|
|
||||||
'wall': (100, 100, 100),
|
|
||||||
'floor': (50, 50, 50),
|
|
||||||
'tunnel': (30, 30, 40),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Message colors
|
|
||||||
COLOR_PLAYER_ATK = (230, 230, 230)
|
|
||||||
COLOR_ENEMY_ATK = (255, 200, 200)
|
|
||||||
COLOR_PLAYER_DIE = (255, 100, 100)
|
|
||||||
COLOR_ENEMY_DIE = (255, 165, 0)
|
|
||||||
|
|
||||||
# Actions
|
|
||||||
class Action:
|
|
||||||
"""Base class for all actions"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class MovementAction(Action):
|
|
||||||
"""Action for moving an entity"""
|
|
||||||
def __init__(self, dx, dy):
|
|
||||||
self.dx = dx
|
|
||||||
self.dy = dy
|
|
||||||
|
|
||||||
class MeleeAction(Action):
|
|
||||||
"""Action for melee attacks"""
|
|
||||||
def __init__(self, attacker, target):
|
|
||||||
self.attacker = attacker
|
|
||||||
self.target = target
|
|
||||||
|
|
||||||
def perform(self):
|
|
||||||
"""Execute the attack"""
|
|
||||||
if not self.target.is_alive:
|
|
||||||
return None
|
|
||||||
|
|
||||||
damage = self.attacker.power - self.target.defense
|
|
||||||
|
|
||||||
if damage > 0:
|
|
||||||
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
|
|
||||||
self.target.take_damage(damage)
|
|
||||||
|
|
||||||
# Choose color based on attacker
|
|
||||||
if self.attacker.name == "Player":
|
|
||||||
color = COLOR_PLAYER_ATK
|
|
||||||
else:
|
|
||||||
color = COLOR_ENEMY_ATK
|
|
||||||
|
|
||||||
return attack_desc, color
|
|
||||||
else:
|
|
||||||
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
|
|
||||||
return attack_desc, (150, 150, 150)
|
|
||||||
|
|
||||||
class WaitAction(Action):
|
|
||||||
"""Action for waiting/skipping turn"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class GameObject:
|
|
||||||
"""Base class for all game objects"""
|
|
||||||
def __init__(self, x, y, sprite_index, color, name,
|
|
||||||
blocks=False, hp=0, defense=0, power=0):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.sprite_index = sprite_index
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks
|
|
||||||
self._entity = None
|
|
||||||
self.grid = None
|
|
||||||
|
|
||||||
# Combat stats
|
|
||||||
self.max_hp = hp
|
|
||||||
self.hp = hp
|
|
||||||
self.defense = defense
|
|
||||||
self.power = power
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_alive(self):
|
|
||||||
"""Returns True if this entity can act"""
|
|
||||||
return self.hp > 0
|
|
||||||
|
|
||||||
def attach_to_grid(self, grid):
|
|
||||||
"""Attach this game object to a McRogueFace grid"""
|
|
||||||
self.grid = grid
|
|
||||||
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
|
||||||
self._entity.sprite_index = self.sprite_index
|
|
||||||
self._entity.color = mcrfpy.Color(*self.color)
|
|
||||||
|
|
||||||
def move(self, dx, dy):
|
|
||||||
"""Move by the given amount"""
|
|
||||||
if not self.grid:
|
|
||||||
return
|
|
||||||
self.x += dx
|
|
||||||
self.y += dy
|
|
||||||
if self._entity:
|
|
||||||
self._entity.x = self.x
|
|
||||||
self._entity.y = self.y
|
|
||||||
# Update FOV when player moves
|
|
||||||
if self.name == "Player":
|
|
||||||
self.update_fov()
|
|
||||||
|
|
||||||
def update_fov(self):
|
|
||||||
"""Update field of view from this entity's position"""
|
|
||||||
if self._entity and self.grid:
|
|
||||||
self._entity.update_fov(radius=8)
|
|
||||||
|
|
||||||
def take_damage(self, amount):
|
|
||||||
"""Apply damage to this entity"""
|
|
||||||
self.hp -= amount
|
|
||||||
|
|
||||||
# Check for death
|
|
||||||
if self.hp <= 0:
|
|
||||||
self.die()
|
|
||||||
|
|
||||||
def die(self):
|
|
||||||
"""Handle entity death"""
|
|
||||||
if self.name == "Player":
|
|
||||||
# Player death
|
|
||||||
self.sprite_index = 64 # Stay as @
|
|
||||||
self.color = (127, 0, 0) # Dark red
|
|
||||||
if self._entity:
|
|
||||||
self._entity.color = mcrfpy.Color(127, 0, 0)
|
|
||||||
else:
|
|
||||||
# Enemy death
|
|
||||||
self.sprite_index = 37 # % character for corpse
|
|
||||||
self.color = (127, 0, 0) # Dark red
|
|
||||||
self.blocks = False # Corpses don't block
|
|
||||||
self.name = f"remains of {self.name}"
|
|
||||||
|
|
||||||
if self._entity:
|
|
||||||
self._entity.sprite_index = 37
|
|
||||||
self._entity.color = mcrfpy.Color(127, 0, 0)
|
|
||||||
|
|
||||||
# Entity factories
|
|
||||||
def create_player(x, y):
|
|
||||||
"""Create the player entity"""
|
|
||||||
return GameObject(
|
|
||||||
x=x, y=y,
|
|
||||||
sprite_index=64, # @
|
|
||||||
color=(255, 255, 255),
|
|
||||||
name="Player",
|
|
||||||
blocks=True,
|
|
||||||
hp=30,
|
|
||||||
defense=2,
|
|
||||||
power=5
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_orc(x, y):
|
|
||||||
"""Create an orc enemy"""
|
|
||||||
return GameObject(
|
|
||||||
x=x, y=y,
|
|
||||||
sprite_index=111, # o
|
|
||||||
color=(63, 127, 63),
|
|
||||||
name="Orc",
|
|
||||||
blocks=True,
|
|
||||||
hp=10,
|
|
||||||
defense=0,
|
|
||||||
power=3
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_troll(x, y):
|
|
||||||
"""Create a troll enemy"""
|
|
||||||
return GameObject(
|
|
||||||
x=x, y=y,
|
|
||||||
sprite_index=84, # T
|
|
||||||
color=(0, 127, 0),
|
|
||||||
name="Troll",
|
|
||||||
blocks=True,
|
|
||||||
hp=16,
|
|
||||||
defense=1,
|
|
||||||
power=4
|
|
||||||
)
|
|
||||||
|
|
||||||
class RectangularRoom:
|
|
||||||
"""A rectangular room with its position and size"""
|
|
||||||
|
|
||||||
def __init__(self, x, y, width, height):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + width
|
|
||||||
self.y2 = y + height
|
|
||||||
|
|
||||||
@property
|
|
||||||
def center(self):
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return center_x, center_y
|
|
||||||
|
|
||||||
@property
|
|
||||||
def inner(self):
|
|
||||||
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
return (
|
|
||||||
self.x1 <= other.x2
|
|
||||||
and self.x2 >= other.x1
|
|
||||||
and self.y1 <= other.y2
|
|
||||||
and self.y2 >= other.y1
|
|
||||||
)
|
|
||||||
|
|
||||||
def tunnel_between(start, end):
|
|
||||||
"""Return an L-shaped tunnel between two points"""
|
|
||||||
x1, y1 = start
|
|
||||||
x2, y2 = end
|
|
||||||
|
|
||||||
if random.random() < 0.5:
|
|
||||||
corner_x = x2
|
|
||||||
corner_y = y1
|
|
||||||
else:
|
|
||||||
corner_x = x1
|
|
||||||
corner_y = y2
|
|
||||||
|
|
||||||
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
|
||||||
yield x, y1
|
|
||||||
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
|
||||||
yield corner_x, y
|
|
||||||
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
|
||||||
yield x, corner_y
|
|
||||||
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
|
||||||
yield x2, y
|
|
||||||
|
|
||||||
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
|
||||||
"""Spawn between 0 and max_enemies in a room"""
|
|
||||||
number_of_enemies = random.randint(0, max_enemies)
|
|
||||||
|
|
||||||
enemies_spawned = []
|
|
||||||
|
|
||||||
for i in range(number_of_enemies):
|
|
||||||
attempts = 10
|
|
||||||
while attempts > 0:
|
|
||||||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
|
||||||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
|
||||||
|
|
||||||
if not game_map.is_blocked(x, y):
|
|
||||||
# 80% chance for orc, 20% for troll
|
|
||||||
if random.random() < 0.8:
|
|
||||||
enemy = create_orc(x, y)
|
|
||||||
else:
|
|
||||||
enemy = create_troll(x, y)
|
|
||||||
|
|
||||||
game_map.add_entity(enemy)
|
|
||||||
enemies_spawned.append(enemy)
|
|
||||||
break
|
|
||||||
|
|
||||||
attempts -= 1
|
|
||||||
|
|
||||||
return enemies_spawned
|
|
||||||
|
|
||||||
class GameMap:
|
|
||||||
"""Manages the game world"""
|
|
||||||
|
|
||||||
def __init__(self, width, height):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.grid = None
|
|
||||||
self.entities = []
|
|
||||||
self.rooms = []
|
|
||||||
|
|
||||||
def create_grid(self, tileset):
|
|
||||||
"""Create the McRogueFace grid"""
|
|
||||||
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
|
||||||
self.grid.position = (100, 100)
|
|
||||||
self.grid.size = (800, 480)
|
|
||||||
|
|
||||||
# Enable perspective rendering
|
|
||||||
self.grid.perspective = 0
|
|
||||||
|
|
||||||
return self.grid
|
|
||||||
|
|
||||||
def fill_with_walls(self):
|
|
||||||
"""Fill the entire map with wall tiles"""
|
|
||||||
for y in range(self.height):
|
|
||||||
for x in range(self.width):
|
|
||||||
self.set_tile(x, y, walkable=False, transparent=False,
|
|
||||||
sprite_index=35, tile_type='wall')
|
|
||||||
|
|
||||||
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
|
||||||
"""Set properties for a specific tile"""
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
cell = self.grid.at(x, y)
|
|
||||||
cell.walkable = walkable
|
|
||||||
cell.transparent = transparent
|
|
||||||
cell.sprite_index = sprite_index
|
|
||||||
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
|
||||||
|
|
||||||
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
|
|
||||||
"""Generate a new dungeon map"""
|
|
||||||
self.fill_with_walls()
|
|
||||||
|
|
||||||
for r in range(max_rooms):
|
|
||||||
room_width = random.randint(room_min_size, room_max_size)
|
|
||||||
room_height = random.randint(room_min_size, room_max_size)
|
|
||||||
|
|
||||||
x = random.randint(0, self.width - room_width - 1)
|
|
||||||
y = random.randint(0, self.height - room_height - 1)
|
|
||||||
|
|
||||||
new_room = RectangularRoom(x, y, room_width, room_height)
|
|
||||||
|
|
||||||
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.carve_room(new_room)
|
|
||||||
|
|
||||||
if len(self.rooms) == 0:
|
|
||||||
# First room - place player
|
|
||||||
player.x, player.y = new_room.center
|
|
||||||
if player._entity:
|
|
||||||
player._entity.x, player._entity.y = new_room.center
|
|
||||||
else:
|
|
||||||
# All other rooms - add tunnel and enemies
|
|
||||||
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
|
||||||
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
|
|
||||||
|
|
||||||
self.rooms.append(new_room)
|
|
||||||
|
|
||||||
def carve_room(self, room):
|
|
||||||
"""Carve out a room"""
|
|
||||||
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
|
||||||
|
|
||||||
for y in range(inner_y1, inner_y2):
|
|
||||||
for x in range(inner_x1, inner_x2):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, tile_type='floor')
|
|
||||||
|
|
||||||
def carve_tunnel(self, start, end):
|
|
||||||
"""Carve a tunnel between two points"""
|
|
||||||
for x, y in tunnel_between(start, end):
|
|
||||||
self.set_tile(x, y, walkable=True, transparent=True,
|
|
||||||
sprite_index=46, tile_type='tunnel')
|
|
||||||
|
|
||||||
def get_blocking_entity_at(self, x, y):
|
|
||||||
"""Return any blocking entity at the given position"""
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.blocks and entity.x == x and entity.y == y:
|
|
||||||
return entity
|
|
||||||
return None
|
|
||||||
|
|
||||||
def is_blocked(self, x, y):
|
|
||||||
"""Check if a tile blocks movement"""
|
|
||||||
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if not self.grid.at(x, y).walkable:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if self.get_blocking_entity_at(x, y):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_entity(self, entity):
|
|
||||||
"""Add a GameObject to the map"""
|
|
||||||
self.entities.append(entity)
|
|
||||||
entity.attach_to_grid(self.grid)
|
|
||||||
|
|
||||||
class Engine:
|
|
||||||
"""Main game engine"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.game_map = None
|
|
||||||
self.player = None
|
|
||||||
self.entities = []
|
|
||||||
self.messages = [] # Simple message log
|
|
||||||
self.max_messages = 5
|
|
||||||
|
|
||||||
mcrfpy.createScene("game")
|
|
||||||
mcrfpy.setScene("game")
|
|
||||||
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.title = "McRogueFace Roguelike - Part 6"
|
|
||||||
|
|
||||||
self.ui = mcrfpy.sceneUI("game")
|
|
||||||
|
|
||||||
background = mcrfpy.Frame((0, 0), (1024, 768))
|
|
||||||
background.fill_color = mcrfpy.Color(0, 0, 0)
|
|
||||||
self.ui.append(background)
|
|
||||||
|
|
||||||
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
|
||||||
|
|
||||||
self.setup_game()
|
|
||||||
self.setup_input()
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
def add_message(self, text, color=(255, 255, 255)):
|
|
||||||
"""Add a message to the log"""
|
|
||||||
self.messages.append((text, color))
|
|
||||||
if len(self.messages) > self.max_messages:
|
|
||||||
self.messages.pop(0)
|
|
||||||
self.update_message_display()
|
|
||||||
|
|
||||||
def update_message_display(self):
|
|
||||||
"""Update the message display"""
|
|
||||||
# Clear old messages
|
|
||||||
for caption in self.message_captions:
|
|
||||||
# Remove from UI (McRogueFace doesn't have remove, so we hide it)
|
|
||||||
caption.text = ""
|
|
||||||
|
|
||||||
# Display current messages
|
|
||||||
for i, (text, color) in enumerate(self.messages):
|
|
||||||
if i < len(self.message_captions):
|
|
||||||
self.message_captions[i].text = text
|
|
||||||
self.message_captions[i].fill_color = mcrfpy.Color(*color)
|
|
||||||
|
|
||||||
def setup_game(self):
|
|
||||||
"""Initialize the game world"""
|
|
||||||
self.game_map = GameMap(80, 45)
|
|
||||||
grid = self.game_map.create_grid(self.tileset)
|
|
||||||
self.ui.append(grid)
|
|
||||||
|
|
||||||
# Create player
|
|
||||||
self.player = create_player(0, 0)
|
|
||||||
|
|
||||||
# Generate the dungeon
|
|
||||||
self.game_map.generate_dungeon(
|
|
||||||
max_rooms=30,
|
|
||||||
room_min_size=6,
|
|
||||||
room_max_size=10,
|
|
||||||
player=self.player,
|
|
||||||
max_enemies_per_room=2
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add player to map
|
|
||||||
self.game_map.add_entity(self.player)
|
|
||||||
|
|
||||||
# Store reference to all entities
|
|
||||||
self.entities = [e for e in self.game_map.entities if e != self.player]
|
|
||||||
|
|
||||||
# Initial FOV calculation
|
|
||||||
self.player.update_fov()
|
|
||||||
|
|
||||||
# Welcome message
|
|
||||||
self.add_message("Welcome to the dungeon!", (100, 100, 255))
|
|
||||||
|
|
||||||
def handle_player_turn(self, action):
|
|
||||||
"""Process the player's action"""
|
|
||||||
if not self.player.is_alive:
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(action, MovementAction):
|
|
||||||
dest_x = self.player.x + action.dx
|
|
||||||
dest_y = self.player.y + action.dy
|
|
||||||
|
|
||||||
# Check what's at the destination
|
|
||||||
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
|
||||||
|
|
||||||
if target:
|
|
||||||
# Attack!
|
|
||||||
attack = MeleeAction(self.player, target)
|
|
||||||
result = attack.perform()
|
|
||||||
if result:
|
|
||||||
text, color = result
|
|
||||||
self.add_message(text, color)
|
|
||||||
|
|
||||||
# Check if target died
|
|
||||||
if not target.is_alive:
|
|
||||||
death_msg = f"The {target.name.replace('remains of ', '')} is dead!"
|
|
||||||
self.add_message(death_msg, COLOR_ENEMY_DIE)
|
|
||||||
|
|
||||||
elif not self.game_map.is_blocked(dest_x, dest_y):
|
|
||||||
# Move the player
|
|
||||||
self.player.move(action.dx, action.dy)
|
|
||||||
|
|
||||||
elif isinstance(action, WaitAction):
|
|
||||||
pass # Do nothing
|
|
||||||
|
|
||||||
# Enemy turns
|
|
||||||
self.handle_enemy_turns()
|
|
||||||
|
|
||||||
def handle_enemy_turns(self):
|
|
||||||
"""Let all enemies take their turn"""
|
|
||||||
for entity in self.entities:
|
|
||||||
if entity.is_alive:
|
|
||||||
# Simple AI: if player is adjacent, attack. Otherwise, do nothing.
|
|
||||||
dx = entity.x - self.player.x
|
|
||||||
dy = entity.y - self.player.y
|
|
||||||
distance = abs(dx) + abs(dy)
|
|
||||||
|
|
||||||
if distance == 1: # Adjacent to player
|
|
||||||
attack = MeleeAction(entity, self.player)
|
|
||||||
result = attack.perform()
|
|
||||||
if result:
|
|
||||||
text, color = result
|
|
||||||
self.add_message(text, color)
|
|
||||||
|
|
||||||
# Check if player died
|
|
||||||
if not self.player.is_alive:
|
|
||||||
self.add_message("You have died!", COLOR_PLAYER_DIE)
|
|
||||||
|
|
||||||
def setup_input(self):
|
|
||||||
"""Setup keyboard input handling"""
|
|
||||||
def handle_keys(key, state):
|
|
||||||
if state != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
action = None
|
|
||||||
|
|
||||||
# Movement keys
|
|
||||||
movement = {
|
|
||||||
"Up": (0, -1), "Down": (0, 1),
|
|
||||||
"Left": (-1, 0), "Right": (1, 0),
|
|
||||||
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
|
||||||
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
|
|
||||||
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
if key in movement:
|
|
||||||
dx, dy = movement[key]
|
|
||||||
if dx == 0 and dy == 0:
|
|
||||||
action = WaitAction()
|
|
||||||
else:
|
|
||||||
action = MovementAction(dx, dy)
|
|
||||||
elif key == "Period":
|
|
||||||
action = WaitAction()
|
|
||||||
elif key == "Escape":
|
|
||||||
mcrfpy.setScene(None)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Process the action
|
|
||||||
if action:
|
|
||||||
self.handle_player_turn(action)
|
|
||||||
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Setup UI elements"""
|
|
||||||
title = mcrfpy.Caption("Combat System", 512, 30)
|
|
||||||
title.font_size = 24
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 100)
|
|
||||||
self.ui.append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption("Attack enemies by bumping into them!", 512, 60)
|
|
||||||
instructions.font_size = 16
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
self.ui.append(instructions)
|
|
||||||
|
|
||||||
# Player stats
|
|
||||||
self.hp_text = mcrfpy.Caption(f"HP: {self.player.hp}/{self.player.max_hp}", 50, 100)
|
|
||||||
self.hp_text.font_size = 18
|
|
||||||
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
|
|
||||||
self.ui.append(self.hp_text)
|
|
||||||
|
|
||||||
# Message log
|
|
||||||
self.message_captions = []
|
|
||||||
for i in range(self.max_messages):
|
|
||||||
caption = mcrfpy.Caption("", 50, 620 + i * 20)
|
|
||||||
caption.font_size = 14
|
|
||||||
caption.fill_color = mcrfpy.Color(200, 200, 200)
|
|
||||||
self.ui.append(caption)
|
|
||||||
self.message_captions.append(caption)
|
|
||||||
|
|
||||||
# Timer to update HP display
|
|
||||||
def update_stats(dt):
|
|
||||||
self.hp_text.text = f"HP: {self.player.hp}/{self.player.max_hp}"
|
|
||||||
if self.player.hp <= 0:
|
|
||||||
self.hp_text.fill_color = mcrfpy.Color(127, 0, 0)
|
|
||||||
elif self.player.hp < self.player.max_hp // 3:
|
|
||||||
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
|
|
||||||
else:
|
|
||||||
self.hp_text.fill_color = mcrfpy.Color(0, 255, 0)
|
|
||||||
|
|
||||||
mcrfpy.setTimer("update_stats", update_stats, 100)
|
|
||||||
|
|
||||||
# Create and run the game
|
|
||||||
engine = Engine()
|
|
||||||
print("Part 6: Combat System!")
|
|
||||||
print("Attack enemies to defeat them, but watch your HP!")
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 0: Introduction to Scene, Texture, and Grid
|
|
||||||
|
|
||||||
This tutorial introduces the basic building blocks:
|
|
||||||
- Scene: A container for UI elements and game state
|
|
||||||
- Texture: Loading image assets for use in the game
|
|
||||||
- Grid: A tilemap component for rendering tile-based worlds
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Create and activate a new scene
|
|
||||||
mcrfpy.createScene("tutorial")
|
|
||||||
mcrfpy.setScene("tutorial")
|
|
||||||
|
|
||||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
|
||||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
|
||||||
|
|
||||||
# Create a grid of tiles
|
|
||||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
|
||||||
|
|
||||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
|
||||||
|
|
||||||
# calculating the size in pixels to fit the entire grid on-screen
|
|
||||||
zoom = 2.0
|
|
||||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
|
||||||
|
|
||||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
|
||||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
|
||||||
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
pos=grid_position,
|
|
||||||
grid_size=(grid_width, grid_height),
|
|
||||||
texture=texture,
|
|
||||||
size=grid_size, # height and width on screen
|
|
||||||
)
|
|
||||||
|
|
||||||
grid.zoom = zoom
|
|
||||||
grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile
|
|
||||||
|
|
||||||
# Define tile types
|
|
||||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
|
||||||
WALL_TILES = [3, 7, 11]
|
|
||||||
|
|
||||||
# Fill the grid with a simple pattern
|
|
||||||
for y in range(grid_height):
|
|
||||||
for x in range(grid_width):
|
|
||||||
# Create walls around the edges
|
|
||||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
|
||||||
tile_index = random.choice(WALL_TILES)
|
|
||||||
else:
|
|
||||||
# Fill interior with floor tiles
|
|
||||||
tile_index = random.choice(FLOOR_TILES)
|
|
||||||
|
|
||||||
# Set the tile at this position
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = tile_index
|
|
||||||
|
|
||||||
# Add the grid to the scene
|
|
||||||
mcrfpy.sceneUI("tutorial").append(grid)
|
|
||||||
|
|
||||||
# Add a title caption
|
|
||||||
title = mcrfpy.Caption((320, 10),
|
|
||||||
text="McRogueFace Tutorial - Part 0",
|
|
||||||
)
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(title)
|
|
||||||
|
|
||||||
# Add instructions
|
|
||||||
instructions = mcrfpy.Caption((280, 750),
|
|
||||||
text="Scene + Texture + Grid = Tilemap!",
|
|
||||||
)
|
|
||||||
instructions.font_size=18
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
|
||||||
|
|
||||||
print("Tutorial Part 0 loaded!")
|
|
||||||
print(f"Created a {grid.grid_size[0]}x{grid.grid_size[1]} grid")
|
|
||||||
print(f"Grid positioned at ({grid.x}, {grid.y})")
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 1: Entities and Keyboard Input
|
|
||||||
|
|
||||||
This tutorial builds on Part 0 by adding:
|
|
||||||
- Entity: A game object that can be placed in a grid
|
|
||||||
- Keyboard handling: Responding to key presses to move the entity
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Create and activate a new scene
|
|
||||||
mcrfpy.createScene("tutorial")
|
|
||||||
mcrfpy.setScene("tutorial")
|
|
||||||
|
|
||||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
|
||||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
|
||||||
|
|
||||||
# Load the hero sprite texture (32x32 sprite sheet)
|
|
||||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
|
||||||
|
|
||||||
# Create a grid of tiles
|
|
||||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
|
||||||
|
|
||||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
|
||||||
|
|
||||||
# calculating the size in pixels to fit the entire grid on-screen
|
|
||||||
zoom = 2.0
|
|
||||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
|
||||||
|
|
||||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
|
||||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
|
||||||
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
pos=grid_position,
|
|
||||||
grid_size=(grid_width, grid_height),
|
|
||||||
texture=texture,
|
|
||||||
size=grid_size, # height and width on screen
|
|
||||||
)
|
|
||||||
|
|
||||||
grid.zoom = zoom
|
|
||||||
grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile
|
|
||||||
|
|
||||||
# Define tile types
|
|
||||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
|
||||||
WALL_TILES = [3, 7, 11]
|
|
||||||
|
|
||||||
# Fill the grid with a simple pattern
|
|
||||||
for y in range(grid_height):
|
|
||||||
for x in range(grid_width):
|
|
||||||
# Create walls around the edges
|
|
||||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
|
||||||
tile_index = random.choice(WALL_TILES)
|
|
||||||
else:
|
|
||||||
# Fill interior with floor tiles
|
|
||||||
tile_index = random.choice(FLOOR_TILES)
|
|
||||||
|
|
||||||
# Set the tile at this position
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = tile_index
|
|
||||||
|
|
||||||
# Add the grid to the scene
|
|
||||||
mcrfpy.sceneUI("tutorial").append(grid)
|
|
||||||
|
|
||||||
# Create a player entity at position (4, 4)
|
|
||||||
player = mcrfpy.Entity(
|
|
||||||
(4, 4), # Entity positions are tile coordinates
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0 # Use the first sprite in the texture
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the player entity to the grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
|
|
||||||
# Define keyboard handler
|
|
||||||
def handle_keys(key, state):
|
|
||||||
"""Handle keyboard input to move the player"""
|
|
||||||
if state == "start": # Only respond to key press, not release
|
|
||||||
# Get current player position in grid coordinates
|
|
||||||
px, py = player.x, player.y
|
|
||||||
|
|
||||||
# Calculate new position based on key press
|
|
||||||
if key == "W" or key == "Up":
|
|
||||||
py -= 1
|
|
||||||
elif key == "S" or key == "Down":
|
|
||||||
py += 1
|
|
||||||
elif key == "A" or key == "Left":
|
|
||||||
px -= 1
|
|
||||||
elif key == "D" or key == "Right":
|
|
||||||
px += 1
|
|
||||||
|
|
||||||
# Update player position (no collision checking yet)
|
|
||||||
player.x = px
|
|
||||||
player.y = py
|
|
||||||
|
|
||||||
# Register the keyboard handler
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
# Add a title caption
|
|
||||||
title = mcrfpy.Caption((320, 10),
|
|
||||||
text="McRogueFace Tutorial - Part 1",
|
|
||||||
)
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(title)
|
|
||||||
|
|
||||||
# Add instructions
|
|
||||||
instructions = mcrfpy.Caption((200, 750),
|
|
||||||
text="Use WASD or Arrow Keys to move the hero!",
|
|
||||||
)
|
|
||||||
instructions.font_size=18
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
|
||||||
|
|
||||||
print("Tutorial Part 1 loaded!")
|
|
||||||
print(f"Player entity created at grid position (4, 4)")
|
|
||||||
print("Use WASD or Arrow keys to move!")
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 1: Entities and Keyboard Input
|
|
||||||
|
|
||||||
This tutorial builds on Part 0 by adding:
|
|
||||||
- Entity: A game object that can be placed in a grid
|
|
||||||
- Keyboard handling: Responding to key presses to move the entity
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Create and activate a new scene
|
|
||||||
mcrfpy.createScene("tutorial")
|
|
||||||
mcrfpy.setScene("tutorial")
|
|
||||||
|
|
||||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
|
||||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
|
||||||
|
|
||||||
# Load the hero sprite texture (32x32 sprite sheet)
|
|
||||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
|
||||||
|
|
||||||
# Create a grid of tiles
|
|
||||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
|
||||||
|
|
||||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
|
||||||
|
|
||||||
# calculating the size in pixels to fit the entire grid on-screen
|
|
||||||
zoom = 2.0
|
|
||||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
|
||||||
|
|
||||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
|
||||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
|
||||||
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
pos=grid_position,
|
|
||||||
grid_size=(grid_width, grid_height),
|
|
||||||
texture=texture,
|
|
||||||
size=grid_size, # height and width on screen
|
|
||||||
)
|
|
||||||
|
|
||||||
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
|
||||||
|
|
||||||
# Define tile types
|
|
||||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
|
||||||
WALL_TILES = [3, 7, 11]
|
|
||||||
|
|
||||||
# Fill the grid with a simple pattern
|
|
||||||
for y in range(grid_height):
|
|
||||||
for x in range(grid_width):
|
|
||||||
# Create walls around the edges
|
|
||||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
|
||||||
tile_index = random.choice(WALL_TILES)
|
|
||||||
else:
|
|
||||||
# Fill interior with floor tiles
|
|
||||||
tile_index = random.choice(FLOOR_TILES)
|
|
||||||
|
|
||||||
# Set the tile at this position
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = tile_index
|
|
||||||
|
|
||||||
# Add the grid to the scene
|
|
||||||
mcrfpy.sceneUI("tutorial").append(grid)
|
|
||||||
|
|
||||||
# Create a player entity at position (4, 4)
|
|
||||||
player = mcrfpy.Entity(
|
|
||||||
(4, 4), # Entity positions are tile coordinates
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0 # Use the first sprite in the texture
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the player entity to the grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
|
||||||
|
|
||||||
# Define keyboard handler
|
|
||||||
def handle_keys(key, state):
|
|
||||||
"""Handle keyboard input to move the player"""
|
|
||||||
if state == "start": # Only respond to key press, not release
|
|
||||||
# Get current player position in grid coordinates
|
|
||||||
px, py = player.x, player.y
|
|
||||||
|
|
||||||
# Calculate new position based on key press
|
|
||||||
if key == "W" or key == "Up":
|
|
||||||
py -= 1
|
|
||||||
elif key == "S" or key == "Down":
|
|
||||||
py += 1
|
|
||||||
elif key == "A" or key == "Left":
|
|
||||||
px -= 1
|
|
||||||
elif key == "D" or key == "Right":
|
|
||||||
px += 1
|
|
||||||
|
|
||||||
# Update player position (no collision checking yet)
|
|
||||||
player.x = px
|
|
||||||
player.y = py
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
|
||||||
|
|
||||||
# Register the keyboard handler
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
# Add a title caption
|
|
||||||
title = mcrfpy.Caption((320, 10),
|
|
||||||
text="McRogueFace Tutorial - Part 1",
|
|
||||||
)
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(title)
|
|
||||||
|
|
||||||
# Add instructions
|
|
||||||
instructions = mcrfpy.Caption((200, 750),
|
|
||||||
text="Use WASD or Arrow Keys to move the hero!",
|
|
||||||
)
|
|
||||||
instructions.font_size=18
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
|
||||||
|
|
||||||
print("Tutorial Part 1 loaded!")
|
|
||||||
print(f"Player entity created at grid position (4, 4)")
|
|
||||||
print("Use WASD or Arrow keys to move!")
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 2: Animated Movement
|
|
||||||
|
|
||||||
This tutorial builds on Part 1 by adding:
|
|
||||||
- Animation system for smooth movement
|
|
||||||
- Movement that takes 0.5 seconds per tile
|
|
||||||
- Input blocking during movement animation
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Create and activate a new scene
|
|
||||||
mcrfpy.createScene("tutorial")
|
|
||||||
mcrfpy.setScene("tutorial")
|
|
||||||
|
|
||||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
|
||||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
|
||||||
|
|
||||||
# Load the hero sprite texture (32x32 sprite sheet)
|
|
||||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
|
||||||
|
|
||||||
# Create a grid of tiles
|
|
||||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
|
||||||
|
|
||||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
|
||||||
|
|
||||||
# calculating the size in pixels to fit the entire grid on-screen
|
|
||||||
zoom = 2.0
|
|
||||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
|
||||||
|
|
||||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
|
||||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
|
||||||
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
pos=grid_position,
|
|
||||||
grid_size=(grid_width, grid_height),
|
|
||||||
texture=texture,
|
|
||||||
size=grid_size, # height and width on screen
|
|
||||||
)
|
|
||||||
|
|
||||||
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
|
||||||
|
|
||||||
# Define tile types
|
|
||||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
|
||||||
WALL_TILES = [3, 7, 11]
|
|
||||||
|
|
||||||
# Fill the grid with a simple pattern
|
|
||||||
for y in range(grid_height):
|
|
||||||
for x in range(grid_width):
|
|
||||||
# Create walls around the edges
|
|
||||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
|
||||||
tile_index = random.choice(WALL_TILES)
|
|
||||||
else:
|
|
||||||
# Fill interior with floor tiles
|
|
||||||
tile_index = random.choice(FLOOR_TILES)
|
|
||||||
|
|
||||||
# Set the tile at this position
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = tile_index
|
|
||||||
|
|
||||||
# Add the grid to the scene
|
|
||||||
mcrfpy.sceneUI("tutorial").append(grid)
|
|
||||||
|
|
||||||
# Create a player entity at position (4, 4)
|
|
||||||
player = mcrfpy.Entity(
|
|
||||||
(4, 4), # Entity positions are tile coordinates
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0 # Use the first sprite in the texture
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the player entity to the grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
|
||||||
|
|
||||||
# Movement state tracking
|
|
||||||
is_moving = False
|
|
||||||
move_animations = [] # Track active animations
|
|
||||||
|
|
||||||
# Animation completion callback
|
|
||||||
def movement_complete(runtime):
|
|
||||||
"""Called when movement animation completes"""
|
|
||||||
global is_moving
|
|
||||||
is_moving = False
|
|
||||||
# Ensure grid is centered on final position
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
|
||||||
|
|
||||||
motion_speed = 0.30 # seconds per tile
|
|
||||||
# Define keyboard handler
|
|
||||||
def handle_keys(key, state):
|
|
||||||
"""Handle keyboard input to move the player"""
|
|
||||||
global is_moving, move_animations
|
|
||||||
|
|
||||||
if state == "start" and not is_moving: # Only respond to key press when not moving
|
|
||||||
# Get current player position in grid coordinates
|
|
||||||
px, py = player.x, player.y
|
|
||||||
new_x, new_y = px, py
|
|
||||||
|
|
||||||
# Calculate new position based on key press
|
|
||||||
if key == "W" or key == "Up":
|
|
||||||
new_y -= 1
|
|
||||||
elif key == "S" or key == "Down":
|
|
||||||
new_y += 1
|
|
||||||
elif key == "A" or key == "Left":
|
|
||||||
new_x -= 1
|
|
||||||
elif key == "D" or key == "Right":
|
|
||||||
new_x += 1
|
|
||||||
|
|
||||||
# If position changed, start movement animation
|
|
||||||
if new_x != px or new_y != py:
|
|
||||||
is_moving = True
|
|
||||||
|
|
||||||
# Create animations for player position
|
|
||||||
anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad")
|
|
||||||
anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
|
||||||
anim_x.start(player)
|
|
||||||
anim_y.start(player)
|
|
||||||
|
|
||||||
# Animate grid center to follow player
|
|
||||||
center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
|
||||||
center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
|
||||||
center_x.start(grid)
|
|
||||||
center_y.start(grid)
|
|
||||||
|
|
||||||
# Set a timer to mark movement as complete
|
|
||||||
mcrfpy.setTimer("move_complete", movement_complete, 500)
|
|
||||||
|
|
||||||
# Register the keyboard handler
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
# Add a title caption
|
|
||||||
title = mcrfpy.Caption((320, 10),
|
|
||||||
text="McRogueFace Tutorial - Part 2",
|
|
||||||
)
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(title)
|
|
||||||
|
|
||||||
# Add instructions
|
|
||||||
instructions = mcrfpy.Caption((150, 750),
|
|
||||||
text="Smooth movement! Each step takes 0.5 seconds.",
|
|
||||||
)
|
|
||||||
instructions.font_size=18
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
|
||||||
|
|
||||||
print("Tutorial Part 2 loaded!")
|
|
||||||
print(f"Player entity created at grid position (4, 4)")
|
|
||||||
print("Movement is now animated over 0.5 seconds per tile!")
|
|
||||||
print("Use WASD or Arrow keys to move!")
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 2: Enhanced with Single Move Queue
|
|
||||||
|
|
||||||
This tutorial builds on Part 2 by adding:
|
|
||||||
- Single queued move system for responsive input
|
|
||||||
- Debug display showing position and queue status
|
|
||||||
- Smooth continuous movement when keys are held
|
|
||||||
- Animation callbacks to prevent race conditions
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Create and activate a new scene
|
|
||||||
mcrfpy.createScene("tutorial")
|
|
||||||
mcrfpy.setScene("tutorial")
|
|
||||||
|
|
||||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
|
||||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
|
||||||
|
|
||||||
# Load the hero sprite texture (32x32 sprite sheet)
|
|
||||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
|
||||||
|
|
||||||
# Create a grid of tiles
|
|
||||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
|
||||||
|
|
||||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
|
||||||
|
|
||||||
# calculating the size in pixels to fit the entire grid on-screen
|
|
||||||
zoom = 2.0
|
|
||||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
|
||||||
|
|
||||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
|
||||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
|
||||||
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
pos=grid_position,
|
|
||||||
grid_size=(grid_width, grid_height),
|
|
||||||
texture=texture,
|
|
||||||
size=grid_size, # height and width on screen
|
|
||||||
)
|
|
||||||
|
|
||||||
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
|
||||||
|
|
||||||
# Define tile types
|
|
||||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
|
||||||
WALL_TILES = [3, 7, 11]
|
|
||||||
|
|
||||||
# Fill the grid with a simple pattern
|
|
||||||
for y in range(grid_height):
|
|
||||||
for x in range(grid_width):
|
|
||||||
# Create walls around the edges
|
|
||||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
|
||||||
tile_index = random.choice(WALL_TILES)
|
|
||||||
else:
|
|
||||||
# Fill interior with floor tiles
|
|
||||||
tile_index = random.choice(FLOOR_TILES)
|
|
||||||
|
|
||||||
# Set the tile at this position
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = tile_index
|
|
||||||
|
|
||||||
# Add the grid to the scene
|
|
||||||
mcrfpy.sceneUI("tutorial").append(grid)
|
|
||||||
|
|
||||||
# Create a player entity at position (4, 4)
|
|
||||||
player = mcrfpy.Entity(
|
|
||||||
(4, 4), # Entity positions are tile coordinates
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0 # Use the first sprite in the texture
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the player entity to the grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
|
||||||
|
|
||||||
# Movement state tracking
|
|
||||||
is_moving = False
|
|
||||||
move_queue = [] # List to store queued moves (max 1 item)
|
|
||||||
#last_position = (4, 4) # Track last position
|
|
||||||
current_destination = None # Track where we're currently moving to
|
|
||||||
current_move = None # Track current move direction
|
|
||||||
|
|
||||||
# Store animation references
|
|
||||||
player_anim_x = None
|
|
||||||
player_anim_y = None
|
|
||||||
grid_anim_x = None
|
|
||||||
grid_anim_y = None
|
|
||||||
|
|
||||||
# Debug display caption
|
|
||||||
debug_caption = mcrfpy.Caption((10, 40),
|
|
||||||
text="Last: (4, 4) | Queue: 0 | Dest: None",
|
|
||||||
)
|
|
||||||
debug_caption.font_size = 16
|
|
||||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
|
||||||
|
|
||||||
# Additional debug caption for movement state
|
|
||||||
move_debug_caption = mcrfpy.Caption((10, 60),
|
|
||||||
text="Moving: False | Current: None | Queued: None",
|
|
||||||
)
|
|
||||||
move_debug_caption.font_size = 16
|
|
||||||
move_debug_caption.fill_color = mcrfpy.Color(255, 200, 0, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(move_debug_caption)
|
|
||||||
|
|
||||||
def key_to_direction(key):
|
|
||||||
"""Convert key to direction string"""
|
|
||||||
if key == "W" or key == "Up":
|
|
||||||
return "Up"
|
|
||||||
elif key == "S" or key == "Down":
|
|
||||||
return "Down"
|
|
||||||
elif key == "A" or key == "Left":
|
|
||||||
return "Left"
|
|
||||||
elif key == "D" or key == "Right":
|
|
||||||
return "Right"
|
|
||||||
return None
|
|
||||||
|
|
||||||
def update_debug_display():
|
|
||||||
"""Update the debug caption with current state"""
|
|
||||||
queue_count = len(move_queue)
|
|
||||||
dest_text = f"({current_destination[0]}, {current_destination[1]})" if current_destination else "None"
|
|
||||||
debug_caption.text = f"Last: ({player.x}, {player.y}) | Queue: {queue_count} | Dest: {dest_text}"
|
|
||||||
|
|
||||||
# Update movement state debug
|
|
||||||
current_dir = key_to_direction(current_move) if current_move else "None"
|
|
||||||
queued_dir = key_to_direction(move_queue[0]) if move_queue else "None"
|
|
||||||
move_debug_caption.text = f"Moving: {is_moving} | Current: {current_dir} | Queued: {queued_dir}"
|
|
||||||
|
|
||||||
# Animation completion callback
|
|
||||||
def movement_complete(anim, target):
|
|
||||||
"""Called when movement animation completes"""
|
|
||||||
global is_moving, move_queue, current_destination, current_move
|
|
||||||
global player_anim_x, player_anim_y
|
|
||||||
print(f"In callback for animation: {anim=} {target=}")
|
|
||||||
# Clear movement state
|
|
||||||
is_moving = False
|
|
||||||
current_move = None
|
|
||||||
current_destination = None
|
|
||||||
# Clear animation references
|
|
||||||
player_anim_x = None
|
|
||||||
player_anim_y = None
|
|
||||||
|
|
||||||
# Update last position to where we actually are now
|
|
||||||
#last_position = (int(player.x), int(player.y))
|
|
||||||
|
|
||||||
# Ensure grid is centered on final position
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
|
||||||
|
|
||||||
# Check if there's a queued move
|
|
||||||
if move_queue:
|
|
||||||
# Pop the next move from the queue
|
|
||||||
next_move = move_queue.pop(0)
|
|
||||||
print(f"Processing queued move: {next_move}")
|
|
||||||
# Process it like a fresh input
|
|
||||||
process_move(next_move)
|
|
||||||
|
|
||||||
update_debug_display()
|
|
||||||
|
|
||||||
motion_speed = 0.30 # seconds per tile
|
|
||||||
|
|
||||||
def process_move(key):
|
|
||||||
"""Process a move based on the key"""
|
|
||||||
global is_moving, current_move, current_destination, move_queue
|
|
||||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
|
||||||
|
|
||||||
# If already moving, just update the queue
|
|
||||||
if is_moving:
|
|
||||||
print(f"process_move processing {key=} as a queued move (is_moving = True)")
|
|
||||||
# Clear queue and add new move (only keep 1 queued move)
|
|
||||||
move_queue.clear()
|
|
||||||
move_queue.append(key)
|
|
||||||
update_debug_display()
|
|
||||||
return
|
|
||||||
print(f"process_move processing {key=} as a new, immediate animation (is_moving = False)")
|
|
||||||
# Calculate new position from current position
|
|
||||||
px, py = int(player.x), int(player.y)
|
|
||||||
new_x, new_y = px, py
|
|
||||||
|
|
||||||
# Calculate new position based on key press (only one tile movement)
|
|
||||||
if key == "W" or key == "Up":
|
|
||||||
new_y -= 1
|
|
||||||
elif key == "S" or key == "Down":
|
|
||||||
new_y += 1
|
|
||||||
elif key == "A" or key == "Left":
|
|
||||||
new_x -= 1
|
|
||||||
elif key == "D" or key == "Right":
|
|
||||||
new_x += 1
|
|
||||||
|
|
||||||
# Start the move if position changed
|
|
||||||
if new_x != px or new_y != py:
|
|
||||||
is_moving = True
|
|
||||||
current_move = key
|
|
||||||
current_destination = (new_x, new_y)
|
|
||||||
# only animate a single axis, same callback from either
|
|
||||||
if new_x != px:
|
|
||||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_x.start(player)
|
|
||||||
elif new_y != py:
|
|
||||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_y.start(player)
|
|
||||||
|
|
||||||
# Animate grid center to follow player
|
|
||||||
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_x.start(grid)
|
|
||||||
grid_anim_y.start(grid)
|
|
||||||
|
|
||||||
update_debug_display()
|
|
||||||
|
|
||||||
# Define keyboard handler
|
|
||||||
def handle_keys(key, state):
|
|
||||||
"""Handle keyboard input to move the player"""
|
|
||||||
if state == "start":
|
|
||||||
# Only process movement keys
|
|
||||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
|
||||||
print(f"handle_keys producing actual input: {key=}")
|
|
||||||
process_move(key)
|
|
||||||
|
|
||||||
|
|
||||||
# Register the keyboard handler
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
# Add a title caption
|
|
||||||
title = mcrfpy.Caption((320, 10),
|
|
||||||
text="McRogueFace Tutorial - Part 2 Enhanced",
|
|
||||||
)
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(title)
|
|
||||||
|
|
||||||
# Add instructions
|
|
||||||
instructions = mcrfpy.Caption((150, 750),
|
|
||||||
text="One-move queue system with animation callbacks!",
|
|
||||||
)
|
|
||||||
instructions.font_size=18
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
|
||||||
|
|
||||||
print("Tutorial Part 2 Enhanced loaded!")
|
|
||||||
print(f"Player entity created at grid position (4, 4)")
|
|
||||||
print("Movement now uses animation callbacks to prevent race conditions!")
|
|
||||||
print("Use WASD or Arrow keys to move!")
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 2: Animated Movement
|
|
||||||
|
|
||||||
This tutorial builds on Part 1 by adding:
|
|
||||||
- Animation system for smooth movement
|
|
||||||
- Movement that takes 0.5 seconds per tile
|
|
||||||
- Input blocking during movement animation
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Create and activate a new scene
|
|
||||||
mcrfpy.createScene("tutorial")
|
|
||||||
mcrfpy.setScene("tutorial")
|
|
||||||
|
|
||||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
|
||||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
|
||||||
|
|
||||||
# Load the hero sprite texture (32x32 sprite sheet)
|
|
||||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
|
||||||
|
|
||||||
# Create a grid of tiles
|
|
||||||
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
|
|
||||||
|
|
||||||
grid_width, grid_height = 25, 20 # width, height in number of tiles
|
|
||||||
|
|
||||||
# calculating the size in pixels to fit the entire grid on-screen
|
|
||||||
zoom = 2.0
|
|
||||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
|
||||||
|
|
||||||
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
|
|
||||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
|
||||||
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
pos=grid_position,
|
|
||||||
grid_size=(grid_width, grid_height),
|
|
||||||
texture=texture,
|
|
||||||
size=grid_size, # height and width on screen
|
|
||||||
)
|
|
||||||
|
|
||||||
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
|
|
||||||
|
|
||||||
# Define tile types
|
|
||||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
|
||||||
WALL_TILES = [3, 7, 11]
|
|
||||||
|
|
||||||
# Fill the grid with a simple pattern
|
|
||||||
for y in range(grid_height):
|
|
||||||
for x in range(grid_width):
|
|
||||||
# Create walls around the edges
|
|
||||||
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
|
|
||||||
tile_index = random.choice(WALL_TILES)
|
|
||||||
else:
|
|
||||||
# Fill interior with floor tiles
|
|
||||||
tile_index = random.choice(FLOOR_TILES)
|
|
||||||
|
|
||||||
# Set the tile at this position
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = tile_index
|
|
||||||
|
|
||||||
# Add the grid to the scene
|
|
||||||
mcrfpy.sceneUI("tutorial").append(grid)
|
|
||||||
|
|
||||||
# Create a player entity at position (4, 4)
|
|
||||||
player = mcrfpy.Entity(
|
|
||||||
(4, 4), # Entity positions are tile coordinates
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0 # Use the first sprite in the texture
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the player entity to the grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
|
|
||||||
|
|
||||||
# Movement state tracking
|
|
||||||
is_moving = False
|
|
||||||
move_animations = [] # Track active animations
|
|
||||||
|
|
||||||
# Animation completion callback
|
|
||||||
def movement_complete(runtime):
|
|
||||||
"""Called when movement animation completes"""
|
|
||||||
global is_moving
|
|
||||||
is_moving = False
|
|
||||||
# Ensure grid is centered on final position
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
|
||||||
|
|
||||||
motion_speed = 0.30 # seconds per tile
|
|
||||||
# Define keyboard handler
|
|
||||||
def handle_keys(key, state):
|
|
||||||
"""Handle keyboard input to move the player"""
|
|
||||||
global is_moving, move_animations
|
|
||||||
|
|
||||||
if state == "start" and not is_moving: # Only respond to key press when not moving
|
|
||||||
# Get current player position in grid coordinates
|
|
||||||
px, py = player.x, player.y
|
|
||||||
new_x, new_y = px, py
|
|
||||||
|
|
||||||
# Calculate new position based on key press
|
|
||||||
if key == "W" or key == "Up":
|
|
||||||
new_y -= 1
|
|
||||||
elif key == "S" or key == "Down":
|
|
||||||
new_y += 1
|
|
||||||
elif key == "A" or key == "Left":
|
|
||||||
new_x -= 1
|
|
||||||
elif key == "D" or key == "Right":
|
|
||||||
new_x += 1
|
|
||||||
|
|
||||||
# If position changed, start movement animation
|
|
||||||
if new_x != px or new_y != py:
|
|
||||||
is_moving = True
|
|
||||||
|
|
||||||
# Create animations for player position
|
|
||||||
anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad")
|
|
||||||
anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
|
||||||
anim_x.start(player)
|
|
||||||
anim_y.start(player)
|
|
||||||
|
|
||||||
# Animate grid center to follow player
|
|
||||||
center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
|
||||||
center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
|
||||||
center_x.start(grid)
|
|
||||||
center_y.start(grid)
|
|
||||||
|
|
||||||
# Set a timer to mark movement as complete
|
|
||||||
mcrfpy.setTimer("move_complete", movement_complete, 500)
|
|
||||||
|
|
||||||
# Register the keyboard handler
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
# Add a title caption
|
|
||||||
title = mcrfpy.Caption((320, 10),
|
|
||||||
text="McRogueFace Tutorial - Part 2",
|
|
||||||
)
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(title)
|
|
||||||
|
|
||||||
# Add instructions
|
|
||||||
instructions = mcrfpy.Caption((150, 750),
|
|
||||||
"Smooth movement! Each step takes 0.5 seconds.",
|
|
||||||
)
|
|
||||||
instructions.font_size=18
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
|
||||||
|
|
||||||
print("Tutorial Part 2 loaded!")
|
|
||||||
print(f"Player entity created at grid position (4, 4)")
|
|
||||||
print("Movement is now animated over 0.5 seconds per tile!")
|
|
||||||
print("Use WASD or Arrow keys to move!")
|
|
||||||
|
|
@ -1,313 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 3: Procedural Dungeon Generation
|
|
||||||
|
|
||||||
This tutorial builds on Part 2 by adding:
|
|
||||||
- Binary Space Partition (BSP) dungeon generation
|
|
||||||
- Rooms connected by hallways using libtcod.line()
|
|
||||||
- Walkable/non-walkable terrain
|
|
||||||
- Player spawning in a valid location
|
|
||||||
- Wall tiles that block movement
|
|
||||||
|
|
||||||
Key code references:
|
|
||||||
- src/scripts/cos_level.py (lines 7-15, 184-217, 218-224) - BSP algorithm
|
|
||||||
- mcrfpy.libtcod.line() for smooth hallway generation
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Create and activate a new scene
|
|
||||||
mcrfpy.createScene("tutorial")
|
|
||||||
mcrfpy.setScene("tutorial")
|
|
||||||
|
|
||||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
|
||||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
|
||||||
|
|
||||||
# Load the hero sprite texture
|
|
||||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
|
||||||
|
|
||||||
# Create a grid of tiles
|
|
||||||
grid_width, grid_height = 40, 30 # Larger grid for dungeon
|
|
||||||
|
|
||||||
# Calculate the size in pixels to fit the entire grid on-screen
|
|
||||||
zoom = 2.0
|
|
||||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
|
||||||
|
|
||||||
# Calculate the position to center the grid on the screen
|
|
||||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
|
||||||
|
|
||||||
# Create the grid with a TCODMap for pathfinding/FOV
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
pos=grid_position,
|
|
||||||
grid_size=(grid_width, grid_height),
|
|
||||||
texture=texture,
|
|
||||||
size=grid_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
grid.zoom = zoom
|
|
||||||
|
|
||||||
# Define tile types
|
|
||||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
|
||||||
WALL_TILES = [3, 7, 11]
|
|
||||||
|
|
||||||
# Room class for BSP
|
|
||||||
class Room:
|
|
||||||
def __init__(self, x, y, w, h):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + w
|
|
||||||
self.y2 = y + h
|
|
||||||
self.w = w
|
|
||||||
self.h = h
|
|
||||||
|
|
||||||
def center(self):
|
|
||||||
"""Return the center coordinates of the room"""
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return (center_x, center_y)
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
"""Return True if this room overlaps with another"""
|
|
||||||
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
|
|
||||||
self.y1 <= other.y2 and self.y2 >= other.y1)
|
|
||||||
|
|
||||||
# Dungeon generation functions
|
|
||||||
def carve_room(room):
|
|
||||||
"""Carve out a room in the grid - referenced from cos_level.py lines 117-120"""
|
|
||||||
# Using individual updates for now (batch updates would be more efficient)
|
|
||||||
for x in range(room.x1, room.x2):
|
|
||||||
for y in range(room.y1, room.y2):
|
|
||||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(FLOOR_TILES)
|
|
||||||
point.walkable = True
|
|
||||||
point.transparent = True
|
|
||||||
|
|
||||||
def carve_hallway(x1, y1, x2, y2):
|
|
||||||
"""Carve a hallway between two points using libtcod.line()
|
|
||||||
Referenced from cos_level.py lines 184-217, improved with libtcod.line()
|
|
||||||
"""
|
|
||||||
# Get all points along the line
|
|
||||||
|
|
||||||
# Simple solution: works if your characters have diagonal movement
|
|
||||||
#points = mcrfpy.libtcod.line(x1, y1, x2, y2)
|
|
||||||
|
|
||||||
# We don't, so we're going to carve a path with an elbow in it
|
|
||||||
points = []
|
|
||||||
if random.choice([True, False]):
|
|
||||||
# x1,y1 -> x2,y1 -> x2,y2
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y1, x2, y1))
|
|
||||||
points.extend(mcrfpy.libtcod.line(x2, y1, x2, y2))
|
|
||||||
else:
|
|
||||||
# x1,y1 -> x1,y2 -> x2,y2
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y1, x1, y2))
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y2, x2, y2))
|
|
||||||
|
|
||||||
|
|
||||||
# Carve out each point
|
|
||||||
for x, y in points:
|
|
||||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(FLOOR_TILES)
|
|
||||||
point.walkable = True
|
|
||||||
point.transparent = True
|
|
||||||
|
|
||||||
def generate_dungeon(max_rooms=10, room_min_size=4, room_max_size=10):
|
|
||||||
"""Generate a dungeon using simplified BSP approach
|
|
||||||
Referenced from cos_level.py lines 218-224
|
|
||||||
"""
|
|
||||||
rooms = []
|
|
||||||
|
|
||||||
# First, fill everything with walls
|
|
||||||
for y in range(grid_height):
|
|
||||||
for x in range(grid_width):
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(WALL_TILES)
|
|
||||||
point.walkable = False
|
|
||||||
point.transparent = False
|
|
||||||
|
|
||||||
# Generate rooms
|
|
||||||
for _ in range(max_rooms):
|
|
||||||
# Random room size
|
|
||||||
w = random.randint(room_min_size, room_max_size)
|
|
||||||
h = random.randint(room_min_size, room_max_size)
|
|
||||||
|
|
||||||
# Random position (with margin from edges)
|
|
||||||
x = random.randint(1, grid_width - w - 1)
|
|
||||||
y = random.randint(1, grid_height - h - 1)
|
|
||||||
|
|
||||||
new_room = Room(x, y, w, h)
|
|
||||||
|
|
||||||
# Check if it overlaps with existing rooms
|
|
||||||
failed = False
|
|
||||||
for other_room in rooms:
|
|
||||||
if new_room.intersects(other_room):
|
|
||||||
failed = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not failed:
|
|
||||||
# Carve out the room
|
|
||||||
carve_room(new_room)
|
|
||||||
|
|
||||||
# If not the first room, connect to previous room
|
|
||||||
if rooms:
|
|
||||||
# Get centers
|
|
||||||
prev_x, prev_y = rooms[-1].center()
|
|
||||||
new_x, new_y = new_room.center()
|
|
||||||
|
|
||||||
# Carve hallway using libtcod.line()
|
|
||||||
carve_hallway(prev_x, prev_y, new_x, new_y)
|
|
||||||
|
|
||||||
rooms.append(new_room)
|
|
||||||
|
|
||||||
return rooms
|
|
||||||
|
|
||||||
# Generate the dungeon
|
|
||||||
rooms = generate_dungeon(max_rooms=8, room_min_size=4, room_max_size=8)
|
|
||||||
|
|
||||||
# Add the grid to the scene
|
|
||||||
mcrfpy.sceneUI("tutorial").append(grid)
|
|
||||||
|
|
||||||
# Spawn player in the first room
|
|
||||||
if rooms:
|
|
||||||
spawn_x, spawn_y = rooms[0].center()
|
|
||||||
else:
|
|
||||||
# Fallback spawn position
|
|
||||||
spawn_x, spawn_y = 4, 4
|
|
||||||
|
|
||||||
# Create a player entity at the spawn position
|
|
||||||
player = mcrfpy.Entity(
|
|
||||||
(spawn_x, spawn_y),
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the player entity to the grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
|
||||||
|
|
||||||
# Movement state tracking (from Part 2)
|
|
||||||
is_moving = False
|
|
||||||
move_queue = []
|
|
||||||
current_destination = None
|
|
||||||
current_move = None
|
|
||||||
|
|
||||||
# Store animation references
|
|
||||||
player_anim_x = None
|
|
||||||
player_anim_y = None
|
|
||||||
grid_anim_x = None
|
|
||||||
grid_anim_y = None
|
|
||||||
|
|
||||||
def movement_complete(anim, target):
|
|
||||||
"""Called when movement animation completes"""
|
|
||||||
global is_moving, move_queue, current_destination, current_move
|
|
||||||
global player_anim_x, player_anim_y
|
|
||||||
|
|
||||||
is_moving = False
|
|
||||||
current_move = None
|
|
||||||
current_destination = None
|
|
||||||
player_anim_x = None
|
|
||||||
player_anim_y = None
|
|
||||||
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
|
||||||
|
|
||||||
if move_queue:
|
|
||||||
next_move = move_queue.pop(0)
|
|
||||||
process_move(next_move)
|
|
||||||
|
|
||||||
motion_speed = 0.20 # Slightly faster for dungeon exploration
|
|
||||||
|
|
||||||
def can_move_to(x, y):
|
|
||||||
"""Check if a position is valid for movement"""
|
|
||||||
# Boundary check
|
|
||||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Walkability check
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point and point.walkable:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def process_move(key):
|
|
||||||
"""Process a move based on the key"""
|
|
||||||
global is_moving, current_move, current_destination, move_queue
|
|
||||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
|
||||||
|
|
||||||
if is_moving:
|
|
||||||
move_queue.clear()
|
|
||||||
move_queue.append(key)
|
|
||||||
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
|
|
||||||
|
|
||||||
# Check if we can move to the new position
|
|
||||||
if new_x != px or new_y != py:
|
|
||||||
if can_move_to(new_x, new_y):
|
|
||||||
is_moving = True
|
|
||||||
current_move = key
|
|
||||||
current_destination = (new_x, new_y)
|
|
||||||
|
|
||||||
if new_x != px:
|
|
||||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_x.start(player)
|
|
||||||
elif new_y != py:
|
|
||||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_y.start(player)
|
|
||||||
|
|
||||||
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_x.start(grid)
|
|
||||||
grid_anim_y.start(grid)
|
|
||||||
else:
|
|
||||||
# Play a "bump" sound or visual feedback here
|
|
||||||
print(f"Can't move to ({new_x}, {new_y}) - blocked!")
|
|
||||||
|
|
||||||
def handle_keys(key, state):
|
|
||||||
"""Handle keyboard input to move the player"""
|
|
||||||
if state == "start":
|
|
||||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
|
||||||
process_move(key)
|
|
||||||
|
|
||||||
# Register the keyboard handler
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
# Add UI elements
|
|
||||||
title = mcrfpy.Caption((320, 10),
|
|
||||||
text="McRogueFace Tutorial - Part 3: Dungeon Generation",
|
|
||||||
)
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption((150, 750),
|
|
||||||
text=f"Procedural dungeon with {len(rooms)} rooms connected by hallways!",
|
|
||||||
)
|
|
||||||
instructions.font_size = 18
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
|
||||||
|
|
||||||
# Debug info
|
|
||||||
debug_caption = mcrfpy.Caption((10, 40),
|
|
||||||
text=f"Grid: {grid_width}x{grid_height} | Player spawned at ({spawn_x}, {spawn_y})",
|
|
||||||
)
|
|
||||||
debug_caption.font_size = 16
|
|
||||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
|
||||||
|
|
||||||
print("Tutorial Part 3 loaded!")
|
|
||||||
print(f"Generated dungeon with {len(rooms)} rooms")
|
|
||||||
print(f"Player spawned at ({spawn_x}, {spawn_y})")
|
|
||||||
print("Walls now block movement!")
|
|
||||||
print("Use WASD or Arrow keys to explore the dungeon!")
|
|
||||||
|
|
@ -1,366 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 4: Field of View
|
|
||||||
|
|
||||||
This tutorial builds on Part 3 by adding:
|
|
||||||
- Field of view calculation using grid.compute_fov()
|
|
||||||
- Entity perspective rendering with grid.perspective
|
|
||||||
- Three visibility states: unexplored (black), explored (dark), visible (lit)
|
|
||||||
- Memory of previously seen areas
|
|
||||||
- Enemy entity to demonstrate perspective switching
|
|
||||||
|
|
||||||
Key code references:
|
|
||||||
- tests/unit/test_tcod_fov_entities.py (lines 89-118) - FOV with multiple entities
|
|
||||||
- ROADMAP.md (lines 216-229) - FOV system implementation details
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Create and activate a new scene
|
|
||||||
mcrfpy.createScene("tutorial")
|
|
||||||
mcrfpy.setScene("tutorial")
|
|
||||||
|
|
||||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
|
||||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
|
||||||
|
|
||||||
# Load the hero sprite texture
|
|
||||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
|
||||||
|
|
||||||
# Create a grid of tiles
|
|
||||||
grid_width, grid_height = 40, 30
|
|
||||||
|
|
||||||
# Calculate the size in pixels
|
|
||||||
zoom = 2.0
|
|
||||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
|
||||||
|
|
||||||
# Calculate the position to center the grid on the screen
|
|
||||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
|
||||||
|
|
||||||
# Create the grid with a TCODMap for pathfinding/FOV
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
pos=grid_position,
|
|
||||||
grid_size=(grid_width, grid_height),
|
|
||||||
texture=texture,
|
|
||||||
size=grid_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
grid.zoom = zoom
|
|
||||||
|
|
||||||
# Define tile types
|
|
||||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
|
||||||
WALL_TILES = [3, 7, 11]
|
|
||||||
|
|
||||||
# Room class for BSP
|
|
||||||
class Room:
|
|
||||||
def __init__(self, x, y, w, h):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + w
|
|
||||||
self.y2 = y + h
|
|
||||||
self.w = w
|
|
||||||
self.h = h
|
|
||||||
|
|
||||||
def center(self):
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return (center_x, center_y)
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
|
|
||||||
self.y1 <= other.y2 and self.y2 >= other.y1)
|
|
||||||
|
|
||||||
# Dungeon generation functions (from Part 3)
|
|
||||||
def carve_room(room):
|
|
||||||
for x in range(room.x1, room.x2):
|
|
||||||
for y in range(room.y1, room.y2):
|
|
||||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(FLOOR_TILES)
|
|
||||||
point.walkable = True
|
|
||||||
point.transparent = True
|
|
||||||
|
|
||||||
def carve_hallway(x1, y1, x2, y2):
|
|
||||||
#points = mcrfpy.libtcod.line(x1, y1, x2, y2)
|
|
||||||
points = []
|
|
||||||
if random.choice([True, False]):
|
|
||||||
# x1,y1 -> x2,y1 -> x2,y2
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y1, x2, y1))
|
|
||||||
points.extend(mcrfpy.libtcod.line(x2, y1, x2, y2))
|
|
||||||
else:
|
|
||||||
# x1,y1 -> x1,y2 -> x2,y2
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y1, x1, y2))
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y2, x2, y2))
|
|
||||||
|
|
||||||
for x, y in points:
|
|
||||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(FLOOR_TILES)
|
|
||||||
point.walkable = True
|
|
||||||
point.transparent = True
|
|
||||||
|
|
||||||
def generate_dungeon(max_rooms=10, room_min_size=4, room_max_size=10):
|
|
||||||
rooms = []
|
|
||||||
|
|
||||||
# Fill with walls
|
|
||||||
for y in range(grid_height):
|
|
||||||
for x in range(grid_width):
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(WALL_TILES)
|
|
||||||
point.walkable = False
|
|
||||||
point.transparent = False
|
|
||||||
|
|
||||||
# Generate rooms
|
|
||||||
for _ in range(max_rooms):
|
|
||||||
w = random.randint(room_min_size, room_max_size)
|
|
||||||
h = random.randint(room_min_size, room_max_size)
|
|
||||||
x = random.randint(1, grid_width - w - 1)
|
|
||||||
y = random.randint(1, grid_height - h - 1)
|
|
||||||
|
|
||||||
new_room = Room(x, y, w, h)
|
|
||||||
|
|
||||||
failed = False
|
|
||||||
for other_room in rooms:
|
|
||||||
if new_room.intersects(other_room):
|
|
||||||
failed = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not failed:
|
|
||||||
carve_room(new_room)
|
|
||||||
|
|
||||||
if rooms:
|
|
||||||
prev_x, prev_y = rooms[-1].center()
|
|
||||||
new_x, new_y = new_room.center()
|
|
||||||
carve_hallway(prev_x, prev_y, new_x, new_y)
|
|
||||||
|
|
||||||
rooms.append(new_room)
|
|
||||||
|
|
||||||
return rooms
|
|
||||||
|
|
||||||
# Generate the dungeon
|
|
||||||
rooms = generate_dungeon(max_rooms=8, room_min_size=4, room_max_size=8)
|
|
||||||
|
|
||||||
# Add the grid to the scene
|
|
||||||
mcrfpy.sceneUI("tutorial").append(grid)
|
|
||||||
|
|
||||||
# Spawn player in the first room
|
|
||||||
if rooms:
|
|
||||||
spawn_x, spawn_y = rooms[0].center()
|
|
||||||
else:
|
|
||||||
spawn_x, spawn_y = 4, 4
|
|
||||||
|
|
||||||
# Create a player entity
|
|
||||||
player = mcrfpy.Entity(
|
|
||||||
(spawn_x, spawn_y),
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the player entity to the grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
|
|
||||||
# Create an enemy entity in another room (to demonstrate perspective switching)
|
|
||||||
enemy = None
|
|
||||||
if len(rooms) > 1:
|
|
||||||
enemy_x, enemy_y = rooms[1].center()
|
|
||||||
enemy = mcrfpy.Entity(
|
|
||||||
(enemy_x, enemy_y),
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0 # Enemy sprite
|
|
||||||
)
|
|
||||||
grid.entities.append(enemy)
|
|
||||||
|
|
||||||
# Set the grid perspective to the player by default
|
|
||||||
# Note: The new perspective system uses entity references directly
|
|
||||||
grid.perspective = player
|
|
||||||
|
|
||||||
# Initial FOV computation
|
|
||||||
def update_fov():
|
|
||||||
"""Update field of view from current perspective
|
|
||||||
Referenced from test_tcod_fov_entities.py lines 89-118
|
|
||||||
"""
|
|
||||||
if grid.perspective == player:
|
|
||||||
grid.compute_fov(int(player.x), int(player.y), radius=8, algorithm=0)
|
|
||||||
player.update_visibility()
|
|
||||||
elif enemy and grid.perspective == enemy:
|
|
||||||
grid.compute_fov(int(enemy.x), int(enemy.y), radius=6, algorithm=0)
|
|
||||||
enemy.update_visibility()
|
|
||||||
|
|
||||||
# Perform initial FOV calculation
|
|
||||||
update_fov()
|
|
||||||
|
|
||||||
# Center grid on current perspective
|
|
||||||
def center_on_perspective():
|
|
||||||
if grid.perspective == player:
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
|
||||||
elif enemy and grid.perspective == enemy:
|
|
||||||
grid.center = (enemy.x + 0.5) * 16, (enemy.y + 0.5) * 16
|
|
||||||
|
|
||||||
center_on_perspective()
|
|
||||||
|
|
||||||
# Movement state tracking (from Part 3)
|
|
||||||
is_moving = False
|
|
||||||
move_queue = []
|
|
||||||
current_destination = None
|
|
||||||
current_move = None
|
|
||||||
|
|
||||||
# Store animation references
|
|
||||||
player_anim_x = None
|
|
||||||
player_anim_y = None
|
|
||||||
grid_anim_x = None
|
|
||||||
grid_anim_y = None
|
|
||||||
|
|
||||||
def movement_complete(anim, target):
|
|
||||||
"""Called when movement animation completes"""
|
|
||||||
global is_moving, move_queue, current_destination, current_move
|
|
||||||
global player_anim_x, player_anim_y
|
|
||||||
|
|
||||||
is_moving = False
|
|
||||||
current_move = None
|
|
||||||
current_destination = None
|
|
||||||
player_anim_x = None
|
|
||||||
player_anim_y = None
|
|
||||||
|
|
||||||
# Update FOV after movement
|
|
||||||
update_fov()
|
|
||||||
center_on_perspective()
|
|
||||||
|
|
||||||
if move_queue:
|
|
||||||
next_move = move_queue.pop(0)
|
|
||||||
process_move(next_move)
|
|
||||||
|
|
||||||
motion_speed = 0.20
|
|
||||||
|
|
||||||
def can_move_to(x, y):
|
|
||||||
"""Check if a position is valid for movement"""
|
|
||||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
|
||||||
return False
|
|
||||||
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point and point.walkable:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def process_move(key):
|
|
||||||
"""Process a move based on the key"""
|
|
||||||
global is_moving, current_move, current_destination, move_queue
|
|
||||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
|
||||||
|
|
||||||
# Only allow player movement when in player perspective
|
|
||||||
if grid.perspective != player:
|
|
||||||
return
|
|
||||||
|
|
||||||
if is_moving:
|
|
||||||
move_queue.clear()
|
|
||||||
move_queue.append(key)
|
|
||||||
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
|
|
||||||
|
|
||||||
if new_x != px or new_y != py:
|
|
||||||
if can_move_to(new_x, new_y):
|
|
||||||
is_moving = True
|
|
||||||
current_move = key
|
|
||||||
current_destination = (new_x, new_y)
|
|
||||||
|
|
||||||
if new_x != px:
|
|
||||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_x.start(player)
|
|
||||||
elif new_y != py:
|
|
||||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_y.start(player)
|
|
||||||
|
|
||||||
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_x.start(grid)
|
|
||||||
grid_anim_y.start(grid)
|
|
||||||
|
|
||||||
def handle_keys(key, state):
|
|
||||||
"""Handle keyboard input"""
|
|
||||||
if state == "start":
|
|
||||||
# Movement keys
|
|
||||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
|
||||||
process_move(key)
|
|
||||||
|
|
||||||
# Perspective switching
|
|
||||||
elif key == "Tab":
|
|
||||||
# Switch perspective between player and enemy
|
|
||||||
if enemy:
|
|
||||||
if grid.perspective == player:
|
|
||||||
grid.perspective = enemy
|
|
||||||
print("Switched to enemy perspective")
|
|
||||||
else:
|
|
||||||
grid.perspective = player
|
|
||||||
print("Switched to player perspective")
|
|
||||||
|
|
||||||
# Update FOV and camera for new perspective
|
|
||||||
update_fov()
|
|
||||||
center_on_perspective()
|
|
||||||
|
|
||||||
# Register the keyboard handler
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
# Add UI elements
|
|
||||||
title = mcrfpy.Caption((320, 10),
|
|
||||||
text="McRogueFace Tutorial - Part 4: Field of View",
|
|
||||||
)
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption((150, 720),
|
|
||||||
text="Use WASD/Arrows to move. Press Tab to switch perspective!",
|
|
||||||
)
|
|
||||||
instructions.font_size = 18
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
|
||||||
|
|
||||||
# FOV info
|
|
||||||
fov_caption = mcrfpy.Caption((150, 745),
|
|
||||||
text="FOV: Player (radius 8) | Enemy visible in other room",
|
|
||||||
)
|
|
||||||
fov_caption.font_size = 16
|
|
||||||
fov_caption.fill_color = mcrfpy.Color(100, 200, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(fov_caption)
|
|
||||||
|
|
||||||
# Debug info
|
|
||||||
debug_caption = mcrfpy.Caption((10, 40),
|
|
||||||
text=f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Perspective: Player",
|
|
||||||
)
|
|
||||||
debug_caption.font_size = 16
|
|
||||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
|
||||||
|
|
||||||
# Update function for perspective display
|
|
||||||
def update_perspective_display():
|
|
||||||
current_perspective = "Player" if grid.perspective == player else "Enemy"
|
|
||||||
debug_caption.text = f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Perspective: {current_perspective}"
|
|
||||||
|
|
||||||
if grid.perspective == player:
|
|
||||||
fov_caption.text = "FOV: Player (radius 8) | Tab to switch perspective"
|
|
||||||
else:
|
|
||||||
fov_caption.text = "FOV: Enemy (radius 6) | Tab to switch perspective"
|
|
||||||
|
|
||||||
# Timer to update display
|
|
||||||
def update_display(runtime):
|
|
||||||
update_perspective_display()
|
|
||||||
|
|
||||||
mcrfpy.setTimer("display_update", update_display, 100)
|
|
||||||
|
|
||||||
print("Tutorial Part 4 loaded!")
|
|
||||||
print("Field of View system active!")
|
|
||||||
print("- Unexplored areas are black")
|
|
||||||
print("- Previously seen areas are dark")
|
|
||||||
print("- Currently visible areas are lit")
|
|
||||||
print("Press Tab to switch between player and enemy perspective!")
|
|
||||||
print("Use WASD or Arrow keys to move!")
|
|
||||||
|
|
@ -1,363 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 5: Interacting with other entities
|
|
||||||
|
|
||||||
This tutorial builds on Part 4 by adding:
|
|
||||||
- Subclassing mcrfpy.Entity
|
|
||||||
- Non-blocking movement animations with destination tracking
|
|
||||||
- Bump interactions (combat, pushing)
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Create and activate a new scene
|
|
||||||
mcrfpy.createScene("tutorial")
|
|
||||||
mcrfpy.setScene("tutorial")
|
|
||||||
|
|
||||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
|
||||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
|
||||||
|
|
||||||
# Load the hero sprite texture
|
|
||||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
|
||||||
|
|
||||||
# Create a grid of tiles
|
|
||||||
grid_width, grid_height = 40, 30
|
|
||||||
|
|
||||||
# Calculate the size in pixels
|
|
||||||
zoom = 2.0
|
|
||||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
|
||||||
|
|
||||||
# Calculate the position to center the grid on the screen
|
|
||||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
|
||||||
|
|
||||||
# Create the grid with a TCODMap for pathfinding/FOV
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
pos=grid_position,
|
|
||||||
grid_size=(grid_width, grid_height),
|
|
||||||
texture=texture,
|
|
||||||
size=grid_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
grid.zoom = zoom
|
|
||||||
|
|
||||||
# Define tile types
|
|
||||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
|
||||||
WALL_TILES = [3, 7, 11]
|
|
||||||
|
|
||||||
# Room class for BSP
|
|
||||||
class Room:
|
|
||||||
def __init__(self, x, y, w, h):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + w
|
|
||||||
self.y2 = y + h
|
|
||||||
self.w = w
|
|
||||||
self.h = h
|
|
||||||
|
|
||||||
def center(self):
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return (center_x, center_y)
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
|
|
||||||
self.y1 <= other.y2 and self.y2 >= other.y1)
|
|
||||||
|
|
||||||
# Dungeon generation functions (from Part 3)
|
|
||||||
def carve_room(room):
|
|
||||||
for x in range(room.x1, room.x2):
|
|
||||||
for y in range(room.y1, room.y2):
|
|
||||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(FLOOR_TILES)
|
|
||||||
point.walkable = True
|
|
||||||
point.transparent = True
|
|
||||||
|
|
||||||
def carve_hallway(x1, y1, x2, y2):
|
|
||||||
#points = mcrfpy.libtcod.line(x1, y1, x2, y2)
|
|
||||||
points = []
|
|
||||||
if random.choice([True, False]):
|
|
||||||
# x1,y1 -> x2,y1 -> x2,y2
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y1, x2, y1))
|
|
||||||
points.extend(mcrfpy.libtcod.line(x2, y1, x2, y2))
|
|
||||||
else:
|
|
||||||
# x1,y1 -> x1,y2 -> x2,y2
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y1, x1, y2))
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y2, x2, y2))
|
|
||||||
|
|
||||||
for x, y in points:
|
|
||||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(FLOOR_TILES)
|
|
||||||
point.walkable = True
|
|
||||||
point.transparent = True
|
|
||||||
|
|
||||||
def generate_dungeon(max_rooms=10, room_min_size=4, room_max_size=10):
|
|
||||||
rooms = []
|
|
||||||
|
|
||||||
# Fill with walls
|
|
||||||
for y in range(grid_height):
|
|
||||||
for x in range(grid_width):
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(WALL_TILES)
|
|
||||||
point.walkable = False
|
|
||||||
point.transparent = False
|
|
||||||
|
|
||||||
# Generate rooms
|
|
||||||
for _ in range(max_rooms):
|
|
||||||
w = random.randint(room_min_size, room_max_size)
|
|
||||||
h = random.randint(room_min_size, room_max_size)
|
|
||||||
x = random.randint(1, grid_width - w - 1)
|
|
||||||
y = random.randint(1, grid_height - h - 1)
|
|
||||||
|
|
||||||
new_room = Room(x, y, w, h)
|
|
||||||
|
|
||||||
failed = False
|
|
||||||
for other_room in rooms:
|
|
||||||
if new_room.intersects(other_room):
|
|
||||||
failed = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not failed:
|
|
||||||
carve_room(new_room)
|
|
||||||
|
|
||||||
if rooms:
|
|
||||||
prev_x, prev_y = rooms[-1].center()
|
|
||||||
new_x, new_y = new_room.center()
|
|
||||||
carve_hallway(prev_x, prev_y, new_x, new_y)
|
|
||||||
|
|
||||||
rooms.append(new_room)
|
|
||||||
|
|
||||||
return rooms
|
|
||||||
|
|
||||||
# Generate the dungeon
|
|
||||||
rooms = generate_dungeon(max_rooms=8, room_min_size=4, room_max_size=8)
|
|
||||||
|
|
||||||
# Add the grid to the scene
|
|
||||||
mcrfpy.sceneUI("tutorial").append(grid)
|
|
||||||
|
|
||||||
# Spawn player in the first room
|
|
||||||
if rooms:
|
|
||||||
spawn_x, spawn_y = rooms[0].center()
|
|
||||||
else:
|
|
||||||
spawn_x, spawn_y = 4, 4
|
|
||||||
|
|
||||||
class GameEntity(mcrfpy.Entity):
|
|
||||||
"""An entity whose default behavior is to prevent others from moving into its tile."""
|
|
||||||
|
|
||||||
def __init__(self, x, y, walkable=False, **kwargs):
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
self.walkable = walkable
|
|
||||||
self.dest_x = x
|
|
||||||
self.dest_y = y
|
|
||||||
self.is_moving = False
|
|
||||||
|
|
||||||
def get_position(self):
|
|
||||||
"""Get logical position (destination if moving, otherwise current)"""
|
|
||||||
if self.is_moving:
|
|
||||||
return (self.dest_x, self.dest_y)
|
|
||||||
return (int(self.x), int(self.y))
|
|
||||||
|
|
||||||
def on_bump(self, other):
|
|
||||||
return self.walkable # allow other's motion to proceed if entity is walkable
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<{self.__class__.__name__} x={self.x}, y={self.y}, sprite_index={self.sprite_index}>"
|
|
||||||
|
|
||||||
class BumpableEntity(GameEntity):
|
|
||||||
def __init__(self, x, y, **kwargs):
|
|
||||||
super().__init__(x, y, **kwargs)
|
|
||||||
|
|
||||||
def on_bump(self, other):
|
|
||||||
print(f"Watch it, {other}! You bumped into {self}!")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Create a player entity
|
|
||||||
player = GameEntity(
|
|
||||||
spawn_x, spawn_y,
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the player entity to the grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
for r in rooms:
|
|
||||||
enemy_x, enemy_y = r.center()
|
|
||||||
enemy = BumpableEntity(
|
|
||||||
enemy_x, enemy_y,
|
|
||||||
grid=grid,
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0 # Enemy sprite
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set the grid perspective to the player by default
|
|
||||||
# Note: The new perspective system uses entity references directly
|
|
||||||
grid.perspective = player
|
|
||||||
|
|
||||||
# Initial FOV computation
|
|
||||||
def update_fov():
|
|
||||||
"""Update field of view from current perspective
|
|
||||||
Referenced from test_tcod_fov_entities.py lines 89-118
|
|
||||||
"""
|
|
||||||
if grid.perspective == player:
|
|
||||||
grid.compute_fov(int(player.x), int(player.y), radius=8, algorithm=0)
|
|
||||||
player.update_visibility()
|
|
||||||
elif enemy and grid.perspective == enemy:
|
|
||||||
grid.compute_fov(int(enemy.x), int(enemy.y), radius=6, algorithm=0)
|
|
||||||
enemy.update_visibility()
|
|
||||||
|
|
||||||
# Perform initial FOV calculation
|
|
||||||
update_fov()
|
|
||||||
|
|
||||||
# Center grid on current perspective
|
|
||||||
def center_on_perspective():
|
|
||||||
if grid.perspective == player:
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
|
||||||
elif enemy and grid.perspective == enemy:
|
|
||||||
grid.center = (enemy.x + 0.5) * 16, (enemy.y + 0.5) * 16
|
|
||||||
|
|
||||||
center_on_perspective()
|
|
||||||
|
|
||||||
# Movement state tracking (from Part 3)
|
|
||||||
#is_moving = False # make it an entity property
|
|
||||||
move_queue = []
|
|
||||||
current_destination = None
|
|
||||||
current_move = None
|
|
||||||
|
|
||||||
# Store animation references
|
|
||||||
player_anim_x = None
|
|
||||||
player_anim_y = None
|
|
||||||
grid_anim_x = None
|
|
||||||
grid_anim_y = None
|
|
||||||
|
|
||||||
def movement_complete(anim, target):
|
|
||||||
"""Called when movement animation completes"""
|
|
||||||
global move_queue, current_destination, current_move
|
|
||||||
global player_anim_x, player_anim_y
|
|
||||||
|
|
||||||
player.is_moving = False
|
|
||||||
current_move = None
|
|
||||||
current_destination = None
|
|
||||||
player_anim_x = None
|
|
||||||
player_anim_y = None
|
|
||||||
|
|
||||||
# Update FOV after movement
|
|
||||||
update_fov()
|
|
||||||
center_on_perspective()
|
|
||||||
|
|
||||||
if move_queue:
|
|
||||||
next_move = move_queue.pop(0)
|
|
||||||
process_move(next_move)
|
|
||||||
|
|
||||||
motion_speed = 0.20
|
|
||||||
|
|
||||||
def can_move_to(x, y):
|
|
||||||
"""Check if a position is valid for movement"""
|
|
||||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
|
||||||
return False
|
|
||||||
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point and point.walkable:
|
|
||||||
for e in grid.entities:
|
|
||||||
if not e.walkable and (x, y) == e.get_position(): # blocking the way
|
|
||||||
e.on_bump(player)
|
|
||||||
return False
|
|
||||||
return True # all checks passed, no collision
|
|
||||||
return False
|
|
||||||
|
|
||||||
def process_move(key):
|
|
||||||
"""Process a move based on the key"""
|
|
||||||
global current_move, current_destination, move_queue
|
|
||||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
|
||||||
|
|
||||||
# Only allow player movement when in player perspective
|
|
||||||
if grid.perspective != player:
|
|
||||||
return
|
|
||||||
|
|
||||||
if player.is_moving:
|
|
||||||
move_queue.clear()
|
|
||||||
move_queue.append(key)
|
|
||||||
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
|
|
||||||
|
|
||||||
if new_x != px or new_y != py:
|
|
||||||
if can_move_to(new_x, new_y):
|
|
||||||
player.is_moving = True
|
|
||||||
current_move = key
|
|
||||||
current_destination = (new_x, new_y)
|
|
||||||
|
|
||||||
if new_x != px:
|
|
||||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_x.start(player)
|
|
||||||
elif new_y != py:
|
|
||||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_y.start(player)
|
|
||||||
|
|
||||||
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_x.start(grid)
|
|
||||||
grid_anim_y.start(grid)
|
|
||||||
|
|
||||||
def handle_keys(key, state):
|
|
||||||
"""Handle keyboard input"""
|
|
||||||
if state == "start":
|
|
||||||
# Movement keys
|
|
||||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
|
||||||
process_move(key)
|
|
||||||
|
|
||||||
# Register the keyboard handler
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
# Add UI elements
|
|
||||||
title = mcrfpy.Caption((320, 10),
|
|
||||||
text="McRogueFace Tutorial - Part 5: Entity Collision",
|
|
||||||
)
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption((150, 720),
|
|
||||||
text="Use WASD/Arrows to move. Try to bump into the other entity!",
|
|
||||||
)
|
|
||||||
instructions.font_size = 18
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
|
||||||
|
|
||||||
# Debug info
|
|
||||||
debug_caption = mcrfpy.Caption((10, 40),
|
|
||||||
text=f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Perspective: Player",
|
|
||||||
)
|
|
||||||
debug_caption.font_size = 16
|
|
||||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
|
||||||
|
|
||||||
# Update function for perspective display
|
|
||||||
def update_perspective_display():
|
|
||||||
current_perspective = "Player" if grid.perspective == player else "Enemy"
|
|
||||||
debug_caption.text = f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Perspective: {current_perspective}"
|
|
||||||
|
|
||||||
# Timer to update display
|
|
||||||
def update_display(runtime):
|
|
||||||
update_perspective_display()
|
|
||||||
|
|
||||||
mcrfpy.setTimer("display_update", update_display, 100)
|
|
||||||
|
|
||||||
print("Tutorial Part 4 loaded!")
|
|
||||||
print("Field of View system active!")
|
|
||||||
print("- Unexplored areas are black")
|
|
||||||
print("- Previously seen areas are dark")
|
|
||||||
print("- Currently visible areas are lit")
|
|
||||||
print("Press Tab to switch between player and enemy perspective!")
|
|
||||||
print("Use WASD or Arrow keys to move!")
|
|
||||||
|
|
@ -1,645 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 6: Turn-based enemy movement
|
|
||||||
|
|
||||||
This tutorial builds on Part 5 by adding:
|
|
||||||
- Turn cycles where enemies move after the player
|
|
||||||
- Enemy AI that pursues or wanders
|
|
||||||
- Shared collision detection for all entities
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Create and activate a new scene
|
|
||||||
mcrfpy.createScene("tutorial")
|
|
||||||
mcrfpy.setScene("tutorial")
|
|
||||||
|
|
||||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
|
||||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
|
||||||
|
|
||||||
# Load the hero sprite texture
|
|
||||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
|
||||||
|
|
||||||
# Create a grid of tiles
|
|
||||||
grid_width, grid_height = 40, 30
|
|
||||||
|
|
||||||
# Calculate the size in pixels
|
|
||||||
zoom = 2.0
|
|
||||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
|
||||||
|
|
||||||
# Calculate the position to center the grid on the screen
|
|
||||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
|
||||||
|
|
||||||
# Create the grid with a TCODMap for pathfinding/FOV
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
pos=grid_position,
|
|
||||||
grid_size=(grid_width, grid_height),
|
|
||||||
texture=texture,
|
|
||||||
size=grid_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
grid.zoom = zoom
|
|
||||||
|
|
||||||
# Define tile types
|
|
||||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
|
||||||
WALL_TILES = [3, 7, 11]
|
|
||||||
|
|
||||||
# Room class for BSP
|
|
||||||
class Room:
|
|
||||||
def __init__(self, x, y, w, h):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + w
|
|
||||||
self.y2 = y + h
|
|
||||||
self.w = w
|
|
||||||
self.h = h
|
|
||||||
|
|
||||||
def center(self):
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return (center_x, center_y)
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
|
|
||||||
self.y1 <= other.y2 and self.y2 >= other.y1)
|
|
||||||
|
|
||||||
# Dungeon generation functions (from Part 3)
|
|
||||||
def carve_room(room):
|
|
||||||
for x in range(room.x1, room.x2):
|
|
||||||
for y in range(room.y1, room.y2):
|
|
||||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(FLOOR_TILES)
|
|
||||||
point.walkable = True
|
|
||||||
point.transparent = True
|
|
||||||
|
|
||||||
def carve_hallway(x1, y1, x2, y2):
|
|
||||||
#points = mcrfpy.libtcod.line(x1, y1, x2, y2)
|
|
||||||
points = []
|
|
||||||
if random.choice([True, False]):
|
|
||||||
# x1,y1 -> x2,y1 -> x2,y2
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y1, x2, y1))
|
|
||||||
points.extend(mcrfpy.libtcod.line(x2, y1, x2, y2))
|
|
||||||
else:
|
|
||||||
# x1,y1 -> x1,y2 -> x2,y2
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y1, x1, y2))
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y2, x2, y2))
|
|
||||||
|
|
||||||
for x, y in points:
|
|
||||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(FLOOR_TILES)
|
|
||||||
point.walkable = True
|
|
||||||
point.transparent = True
|
|
||||||
|
|
||||||
def generate_dungeon(max_rooms=10, room_min_size=4, room_max_size=10):
|
|
||||||
rooms = []
|
|
||||||
|
|
||||||
# Fill with walls
|
|
||||||
for y in range(grid_height):
|
|
||||||
for x in range(grid_width):
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(WALL_TILES)
|
|
||||||
point.walkable = False
|
|
||||||
point.transparent = False
|
|
||||||
|
|
||||||
# Generate rooms
|
|
||||||
for _ in range(max_rooms):
|
|
||||||
w = random.randint(room_min_size, room_max_size)
|
|
||||||
h = random.randint(room_min_size, room_max_size)
|
|
||||||
x = random.randint(1, grid_width - w - 1)
|
|
||||||
y = random.randint(1, grid_height - h - 1)
|
|
||||||
|
|
||||||
new_room = Room(x, y, w, h)
|
|
||||||
|
|
||||||
failed = False
|
|
||||||
for other_room in rooms:
|
|
||||||
if new_room.intersects(other_room):
|
|
||||||
failed = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not failed:
|
|
||||||
carve_room(new_room)
|
|
||||||
|
|
||||||
if rooms:
|
|
||||||
prev_x, prev_y = rooms[-1].center()
|
|
||||||
new_x, new_y = new_room.center()
|
|
||||||
carve_hallway(prev_x, prev_y, new_x, new_y)
|
|
||||||
|
|
||||||
rooms.append(new_room)
|
|
||||||
|
|
||||||
return rooms
|
|
||||||
|
|
||||||
# Generate the dungeon
|
|
||||||
rooms = generate_dungeon(max_rooms=8, room_min_size=4, room_max_size=8)
|
|
||||||
|
|
||||||
# Add the grid to the scene
|
|
||||||
mcrfpy.sceneUI("tutorial").append(grid)
|
|
||||||
|
|
||||||
# Spawn player in the first room
|
|
||||||
if rooms:
|
|
||||||
spawn_x, spawn_y = rooms[0].center()
|
|
||||||
else:
|
|
||||||
spawn_x, spawn_y = 4, 4
|
|
||||||
|
|
||||||
class GameEntity(mcrfpy.Entity):
|
|
||||||
"""An entity whose default behavior is to prevent others from moving into its tile."""
|
|
||||||
|
|
||||||
def __init__(self, x, y, walkable=False, **kwargs):
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
self.walkable = walkable
|
|
||||||
self.dest_x = x
|
|
||||||
self.dest_y = y
|
|
||||||
self.is_moving = False
|
|
||||||
|
|
||||||
def get_position(self):
|
|
||||||
"""Get logical position (destination if moving, otherwise current)"""
|
|
||||||
if self.is_moving:
|
|
||||||
return (self.dest_x, self.dest_y)
|
|
||||||
return (int(self.x), int(self.y))
|
|
||||||
|
|
||||||
def on_bump(self, other):
|
|
||||||
return self.walkable # allow other's motion to proceed if entity is walkable
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<{self.__class__.__name__} x={self.x}, y={self.y}, sprite_index={self.sprite_index}>"
|
|
||||||
|
|
||||||
class CombatEntity(GameEntity):
|
|
||||||
def __init__(self, x, y, hp=10, damage=(1,3), **kwargs):
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
self.hp = hp
|
|
||||||
self.damage = damage
|
|
||||||
|
|
||||||
def is_dead(self):
|
|
||||||
return self.hp <= 0
|
|
||||||
|
|
||||||
def start_move(self, new_x, new_y, duration=0.2, callback=None):
|
|
||||||
"""Start animating movement to new position"""
|
|
||||||
self.dest_x = new_x
|
|
||||||
self.dest_y = new_y
|
|
||||||
self.is_moving = True
|
|
||||||
|
|
||||||
# Define completion callback that resets is_moving
|
|
||||||
def movement_done(anim, entity):
|
|
||||||
self.is_moving = False
|
|
||||||
if callback:
|
|
||||||
callback(anim, entity)
|
|
||||||
|
|
||||||
# Create animations for smooth movement
|
|
||||||
anim_x = mcrfpy.Animation("x", float(new_x), duration, "easeInOutQuad", callback=movement_done)
|
|
||||||
anim_y = mcrfpy.Animation("y", float(new_y), duration, "easeInOutQuad")
|
|
||||||
|
|
||||||
anim_x.start(self)
|
|
||||||
anim_y.start(self)
|
|
||||||
|
|
||||||
def can_see(self, target_x, target_y):
|
|
||||||
"""Check if this entity can see the target position"""
|
|
||||||
mx, my = self.get_position()
|
|
||||||
|
|
||||||
# Simple distance check first
|
|
||||||
dist = abs(target_x - mx) + abs(target_y - my)
|
|
||||||
if dist > 6:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Line of sight check
|
|
||||||
line = list(mcrfpy.libtcod.line(mx, my, target_x, target_y))
|
|
||||||
for x, y in line[1:-1]: # Skip start and end
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
if cell and not cell.transparent:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def ai_turn(self, player_pos):
|
|
||||||
"""Decide next move"""
|
|
||||||
mx, my = self.get_position()
|
|
||||||
px, py = player_pos
|
|
||||||
|
|
||||||
# Simple AI: move toward player if visible
|
|
||||||
if self.can_see(px, py):
|
|
||||||
# Calculate direction toward player
|
|
||||||
dx = 0
|
|
||||||
dy = 0
|
|
||||||
if px > mx:
|
|
||||||
dx = 1
|
|
||||||
elif px < mx:
|
|
||||||
dx = -1
|
|
||||||
if py > my:
|
|
||||||
dy = 1
|
|
||||||
elif py < my:
|
|
||||||
dy = -1
|
|
||||||
|
|
||||||
# Prefer cardinal movement
|
|
||||||
if dx != 0 and dy != 0:
|
|
||||||
# Pick horizontal or vertical based on greater distance
|
|
||||||
if abs(px - mx) > abs(py - my):
|
|
||||||
dy = 0
|
|
||||||
else:
|
|
||||||
dx = 0
|
|
||||||
|
|
||||||
return (mx + dx, my + dy)
|
|
||||||
else:
|
|
||||||
# Random wander
|
|
||||||
dx, dy = random.choice([(0,1), (0,-1), (1,0), (-1,0)])
|
|
||||||
return (mx + dx, my + dy)
|
|
||||||
|
|
||||||
def ai_turn_dijkstra(self):
|
|
||||||
"""Decide next move using precomputed Dijkstra map"""
|
|
||||||
mx, my = self.get_position()
|
|
||||||
|
|
||||||
# Get current distance to player
|
|
||||||
current_dist = grid.get_dijkstra_distance(mx, my)
|
|
||||||
if current_dist is None or current_dist > 20:
|
|
||||||
# Too far or unreachable - random wander
|
|
||||||
dx, dy = random.choice([(0,1), (0,-1), (1,0), (-1,0)])
|
|
||||||
return (mx + dx, my + dy)
|
|
||||||
|
|
||||||
# Check all adjacent cells for best move
|
|
||||||
best_moves = []
|
|
||||||
for dx, dy in [(0,1), (0,-1), (1,0), (-1,0)]:
|
|
||||||
nx, ny = mx + dx, my + dy
|
|
||||||
|
|
||||||
# Skip if out of bounds
|
|
||||||
if nx < 0 or nx >= grid_width or ny < 0 or ny >= grid_height:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip if not walkable
|
|
||||||
cell = grid.at(nx, ny)
|
|
||||||
if not cell or not cell.walkable:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Get distance from this cell
|
|
||||||
dist = grid.get_dijkstra_distance(nx, ny)
|
|
||||||
if dist is not None:
|
|
||||||
best_moves.append((dist, nx, ny))
|
|
||||||
|
|
||||||
if best_moves:
|
|
||||||
# Sort by distance
|
|
||||||
best_moves.sort()
|
|
||||||
|
|
||||||
# If multiple moves have the same best distance, pick randomly
|
|
||||||
best_dist = best_moves[0][0]
|
|
||||||
equal_moves = [(nx, ny) for dist, nx, ny in best_moves if dist == best_dist]
|
|
||||||
|
|
||||||
if len(equal_moves) > 1:
|
|
||||||
# Random choice among equally good moves
|
|
||||||
nx, ny = random.choice(equal_moves)
|
|
||||||
else:
|
|
||||||
_, nx, ny = best_moves[0]
|
|
||||||
|
|
||||||
return (nx, ny)
|
|
||||||
else:
|
|
||||||
# No valid moves
|
|
||||||
return (mx, my)
|
|
||||||
|
|
||||||
# Create a player entity
|
|
||||||
player = CombatEntity(
|
|
||||||
spawn_x, spawn_y,
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the player entity to the grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
|
|
||||||
# Track all enemies
|
|
||||||
enemies = []
|
|
||||||
|
|
||||||
# Spawn enemies in other rooms
|
|
||||||
for i, room in enumerate(rooms[1:], 1): # Skip first room (player spawn)
|
|
||||||
if i <= 3: # Limit to 3 enemies for now
|
|
||||||
enemy_x, enemy_y = room.center()
|
|
||||||
enemy = CombatEntity(
|
|
||||||
enemy_x, enemy_y,
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0 # Enemy sprite (borrow player's)
|
|
||||||
)
|
|
||||||
grid.entities.append(enemy)
|
|
||||||
enemies.append(enemy)
|
|
||||||
|
|
||||||
# Set the grid perspective to the player by default
|
|
||||||
# Note: The new perspective system uses entity references directly
|
|
||||||
grid.perspective = player
|
|
||||||
|
|
||||||
# Initial FOV computation
|
|
||||||
def update_fov():
|
|
||||||
"""Update field of view from current perspective"""
|
|
||||||
if grid.perspective == player:
|
|
||||||
grid.compute_fov(int(player.x), int(player.y), radius=8, algorithm=0)
|
|
||||||
player.update_visibility()
|
|
||||||
|
|
||||||
# Perform initial FOV calculation
|
|
||||||
update_fov()
|
|
||||||
|
|
||||||
# Center grid on current perspective
|
|
||||||
def center_on_perspective():
|
|
||||||
if grid.perspective == player:
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
|
||||||
|
|
||||||
center_on_perspective()
|
|
||||||
|
|
||||||
# Movement state tracking (from Part 3)
|
|
||||||
#is_moving = False # make it an entity property
|
|
||||||
move_queue = []
|
|
||||||
current_destination = None
|
|
||||||
current_move = None
|
|
||||||
|
|
||||||
# Store animation references
|
|
||||||
player_anim_x = None
|
|
||||||
player_anim_y = None
|
|
||||||
grid_anim_x = None
|
|
||||||
grid_anim_y = None
|
|
||||||
|
|
||||||
def movement_complete(anim, target):
|
|
||||||
"""Called when movement animation completes"""
|
|
||||||
global move_queue, current_destination, current_move
|
|
||||||
global player_anim_x, player_anim_y, is_player_turn
|
|
||||||
|
|
||||||
player.is_moving = False
|
|
||||||
current_move = None
|
|
||||||
current_destination = None
|
|
||||||
player_anim_x = None
|
|
||||||
player_anim_y = None
|
|
||||||
|
|
||||||
# Update FOV after movement
|
|
||||||
update_fov()
|
|
||||||
center_on_perspective()
|
|
||||||
|
|
||||||
# Player turn complete, start enemy turns and queued player move simultaneously
|
|
||||||
is_player_turn = False
|
|
||||||
process_enemy_turns_and_player_queue()
|
|
||||||
|
|
||||||
motion_speed = 0.20
|
|
||||||
is_player_turn = True # Track whose turn it is
|
|
||||||
|
|
||||||
def get_blocking_entity_at(x, y):
|
|
||||||
"""Get blocking entity at position"""
|
|
||||||
for e in grid.entities:
|
|
||||||
if not e.walkable and (x, y) == e.get_position():
|
|
||||||
return e
|
|
||||||
return None
|
|
||||||
|
|
||||||
def can_move_to(x, y, mover=None):
|
|
||||||
"""Check if a position is valid for movement"""
|
|
||||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
|
||||||
return False
|
|
||||||
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if not point or not point.walkable:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check for blocking entities
|
|
||||||
blocker = get_blocking_entity_at(x, y)
|
|
||||||
if blocker and blocker != mover:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def process_enemy_turns_and_player_queue():
|
|
||||||
"""Process all enemy AI decisions and player's queued move simultaneously"""
|
|
||||||
global is_player_turn, move_queue
|
|
||||||
|
|
||||||
# Compute Dijkstra map once for all enemies (if using Dijkstra)
|
|
||||||
if USE_DIJKSTRA:
|
|
||||||
px, py = player.get_position()
|
|
||||||
grid.compute_dijkstra(px, py, diagonal_cost=1.41)
|
|
||||||
|
|
||||||
enemies_to_move = []
|
|
||||||
claimed_positions = set() # Track where enemies plan to move
|
|
||||||
|
|
||||||
# Collect all enemy moves
|
|
||||||
for i, enemy in enumerate(enemies):
|
|
||||||
if enemy.is_dead():
|
|
||||||
continue
|
|
||||||
|
|
||||||
# AI decides next move
|
|
||||||
if USE_DIJKSTRA:
|
|
||||||
target_x, target_y = enemy.ai_turn_dijkstra()
|
|
||||||
else:
|
|
||||||
target_x, target_y = enemy.ai_turn(player.get_position())
|
|
||||||
|
|
||||||
# Check if move is valid and not claimed by another enemy
|
|
||||||
if can_move_to(target_x, target_y, enemy) and (target_x, target_y) not in claimed_positions:
|
|
||||||
enemies_to_move.append((enemy, target_x, target_y))
|
|
||||||
claimed_positions.add((target_x, target_y))
|
|
||||||
|
|
||||||
# Start all enemy animations simultaneously
|
|
||||||
any_enemy_moved = False
|
|
||||||
if enemies_to_move:
|
|
||||||
for enemy, tx, ty in enemies_to_move:
|
|
||||||
enemy.start_move(tx, ty, duration=motion_speed)
|
|
||||||
any_enemy_moved = True
|
|
||||||
|
|
||||||
# Process player's queued move at the same time
|
|
||||||
if move_queue:
|
|
||||||
next_move = move_queue.pop(0)
|
|
||||||
process_player_queued_move(next_move)
|
|
||||||
else:
|
|
||||||
# No queued move, set up callback to return control when animations finish
|
|
||||||
if any_enemy_moved:
|
|
||||||
# Wait for animations to complete
|
|
||||||
mcrfpy.setTimer("turn_complete", check_turn_complete, int(motion_speed * 1000) + 50)
|
|
||||||
else:
|
|
||||||
# No animations, return control immediately
|
|
||||||
is_player_turn = True
|
|
||||||
|
|
||||||
def process_player_queued_move(key):
|
|
||||||
"""Process player's queued move during enemy turn"""
|
|
||||||
global current_move, current_destination
|
|
||||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
if new_x != px or new_y != py:
|
|
||||||
# Check destination at animation end time (considering enemy destinations)
|
|
||||||
future_blocker = get_future_blocking_entity_at(new_x, new_y)
|
|
||||||
|
|
||||||
if future_blocker:
|
|
||||||
# Will bump at destination
|
|
||||||
# Schedule bump for when animations complete
|
|
||||||
mcrfpy.setTimer("delayed_bump", lambda t: handle_delayed_bump(future_blocker), int(motion_speed * 1000))
|
|
||||||
elif can_move_to(new_x, new_y, player):
|
|
||||||
# Valid move, start animation
|
|
||||||
player.is_moving = True
|
|
||||||
current_move = key
|
|
||||||
current_destination = (new_x, new_y)
|
|
||||||
player.dest_x = new_x
|
|
||||||
player.dest_y = new_y
|
|
||||||
|
|
||||||
# Player animation with callback
|
|
||||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=player_queued_move_complete)
|
|
||||||
player_anim_x.start(player)
|
|
||||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
|
||||||
player_anim_y.start(player)
|
|
||||||
|
|
||||||
# Move camera with player
|
|
||||||
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_x.start(grid)
|
|
||||||
grid_anim_y.start(grid)
|
|
||||||
else:
|
|
||||||
# Blocked by wall, wait for turn to complete
|
|
||||||
mcrfpy.setTimer("turn_complete", check_turn_complete, int(motion_speed * 1000) + 50)
|
|
||||||
|
|
||||||
def get_future_blocking_entity_at(x, y):
|
|
||||||
"""Get entity that will be blocking at position after current animations"""
|
|
||||||
for e in grid.entities:
|
|
||||||
if not e.walkable and (x, y) == (e.dest_x, e.dest_y):
|
|
||||||
return e
|
|
||||||
return None
|
|
||||||
|
|
||||||
def handle_delayed_bump(entity):
|
|
||||||
"""Handle bump after animations complete"""
|
|
||||||
global is_player_turn
|
|
||||||
entity.on_bump(player)
|
|
||||||
is_player_turn = True
|
|
||||||
|
|
||||||
def player_queued_move_complete(anim, target):
|
|
||||||
"""Called when player's queued movement completes"""
|
|
||||||
global is_player_turn
|
|
||||||
player.is_moving = False
|
|
||||||
update_fov()
|
|
||||||
center_on_perspective()
|
|
||||||
is_player_turn = True
|
|
||||||
|
|
||||||
def check_turn_complete(timer_name):
|
|
||||||
"""Check if all animations are complete"""
|
|
||||||
global is_player_turn
|
|
||||||
|
|
||||||
# Check if any entity is still moving
|
|
||||||
if player.is_moving:
|
|
||||||
mcrfpy.setTimer("turn_complete", check_turn_complete, 50)
|
|
||||||
return
|
|
||||||
|
|
||||||
for enemy in enemies:
|
|
||||||
if enemy.is_moving:
|
|
||||||
mcrfpy.setTimer("turn_complete", check_turn_complete, 50)
|
|
||||||
return
|
|
||||||
|
|
||||||
# All done
|
|
||||||
is_player_turn = True
|
|
||||||
|
|
||||||
def process_move(key):
|
|
||||||
"""Process a move based on the key"""
|
|
||||||
global current_move, current_destination, move_queue
|
|
||||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y, is_player_turn
|
|
||||||
|
|
||||||
# Only allow player movement on player's turn
|
|
||||||
if not is_player_turn:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Only allow player movement when in player perspective
|
|
||||||
if grid.perspective != player:
|
|
||||||
return
|
|
||||||
|
|
||||||
if player.is_moving:
|
|
||||||
move_queue.clear()
|
|
||||||
move_queue.append(key)
|
|
||||||
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
|
|
||||||
|
|
||||||
if new_x != px or new_y != py:
|
|
||||||
# Check what's at destination
|
|
||||||
blocker = get_blocking_entity_at(new_x, new_y)
|
|
||||||
|
|
||||||
if blocker:
|
|
||||||
# Bump interaction (combat will go here later)
|
|
||||||
blocker.on_bump(player)
|
|
||||||
# Still counts as a turn
|
|
||||||
is_player_turn = False
|
|
||||||
process_enemy_turns_and_player_queue()
|
|
||||||
elif can_move_to(new_x, new_y, player):
|
|
||||||
player.is_moving = True
|
|
||||||
current_move = key
|
|
||||||
current_destination = (new_x, new_y)
|
|
||||||
player.dest_x = new_x
|
|
||||||
player.dest_y = new_y
|
|
||||||
|
|
||||||
# Start player move animation
|
|
||||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_x.start(player)
|
|
||||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
|
||||||
player_anim_y.start(player)
|
|
||||||
|
|
||||||
# Move camera with player
|
|
||||||
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_x.start(grid)
|
|
||||||
grid_anim_y.start(grid)
|
|
||||||
|
|
||||||
def handle_keys(key, state):
|
|
||||||
"""Handle keyboard input"""
|
|
||||||
if state == "start":
|
|
||||||
# Movement keys
|
|
||||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
|
||||||
process_move(key)
|
|
||||||
|
|
||||||
# Register the keyboard handler
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
# Add UI elements
|
|
||||||
title = mcrfpy.Caption((320, 10),
|
|
||||||
text="McRogueFace Tutorial - Part 6: Turn-based Movement",
|
|
||||||
)
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption((150, 720),
|
|
||||||
text="Use WASD/Arrows to move. Enemies move after you!",
|
|
||||||
)
|
|
||||||
instructions.font_size = 18
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
|
||||||
|
|
||||||
# Debug info
|
|
||||||
debug_caption = mcrfpy.Caption((10, 40),
|
|
||||||
text=f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Enemies: {len(enemies)}",
|
|
||||||
)
|
|
||||||
debug_caption.font_size = 16
|
|
||||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
|
||||||
|
|
||||||
# Update function for turn display
|
|
||||||
def update_turn_display():
|
|
||||||
turn_text = "Player" if is_player_turn else "Enemy"
|
|
||||||
alive_enemies = sum(1 for e in enemies if not e.is_dead())
|
|
||||||
debug_caption.text = f"Grid: {grid_width}x{grid_height} | Turn: {turn_text} | Enemies: {alive_enemies}/{len(enemies)}"
|
|
||||||
|
|
||||||
# Configuration toggle
|
|
||||||
USE_DIJKSTRA = True # Set to False to use old line-of-sight AI
|
|
||||||
|
|
||||||
# Timer to update display
|
|
||||||
def update_display(runtime):
|
|
||||||
update_turn_display()
|
|
||||||
|
|
||||||
mcrfpy.setTimer("display_update", update_display, 100)
|
|
||||||
|
|
||||||
print("Tutorial Part 6 loaded!")
|
|
||||||
print("Turn-based movement system active!")
|
|
||||||
print(f"Using {'Dijkstra' if USE_DIJKSTRA else 'Line-of-sight'} AI pathfinding")
|
|
||||||
print("- Enemies move after the player")
|
|
||||||
print("- Enemies pursue when they can see you" if not USE_DIJKSTRA else "- Enemies use optimal pathfinding")
|
|
||||||
print("- Enemies wander when they can't" if not USE_DIJKSTRA else "- All enemies share one pathfinding map")
|
|
||||||
print("Use WASD or Arrow keys to move!")
|
|
||||||
|
|
@ -1,582 +0,0 @@
|
||||||
"""
|
|
||||||
McRogueFace Tutorial - Part 6: Turn-based enemy movement
|
|
||||||
|
|
||||||
This tutorial builds on Part 5 by adding:
|
|
||||||
- Turn cycles where enemies move after the player
|
|
||||||
- Enemy AI that pursues or wanders
|
|
||||||
- Shared collision detection for all entities
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Create and activate a new scene
|
|
||||||
mcrfpy.createScene("tutorial")
|
|
||||||
mcrfpy.setScene("tutorial")
|
|
||||||
|
|
||||||
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
|
|
||||||
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
|
|
||||||
|
|
||||||
# Load the hero sprite texture
|
|
||||||
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
|
|
||||||
|
|
||||||
# Create a grid of tiles
|
|
||||||
grid_width, grid_height = 40, 30
|
|
||||||
|
|
||||||
# Calculate the size in pixels
|
|
||||||
zoom = 2.0
|
|
||||||
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
|
|
||||||
|
|
||||||
# Calculate the position to center the grid on the screen
|
|
||||||
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
|
|
||||||
|
|
||||||
# Create the grid with a TCODMap for pathfinding/FOV
|
|
||||||
grid = mcrfpy.Grid(
|
|
||||||
pos=grid_position,
|
|
||||||
grid_size=(grid_width, grid_height),
|
|
||||||
texture=texture,
|
|
||||||
size=grid_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
grid.zoom = zoom
|
|
||||||
|
|
||||||
# Define tile types
|
|
||||||
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
|
|
||||||
WALL_TILES = [3, 7, 11]
|
|
||||||
|
|
||||||
# Room class for BSP
|
|
||||||
class Room:
|
|
||||||
def __init__(self, x, y, w, h):
|
|
||||||
self.x1 = x
|
|
||||||
self.y1 = y
|
|
||||||
self.x2 = x + w
|
|
||||||
self.y2 = y + h
|
|
||||||
self.w = w
|
|
||||||
self.h = h
|
|
||||||
|
|
||||||
def center(self):
|
|
||||||
center_x = (self.x1 + self.x2) // 2
|
|
||||||
center_y = (self.y1 + self.y2) // 2
|
|
||||||
return (center_x, center_y)
|
|
||||||
|
|
||||||
def intersects(self, other):
|
|
||||||
return (self.x1 <= other.x2 and self.x2 >= other.x1 and
|
|
||||||
self.y1 <= other.y2 and self.y2 >= other.y1)
|
|
||||||
|
|
||||||
# Dungeon generation functions (from Part 3)
|
|
||||||
def carve_room(room):
|
|
||||||
for x in range(room.x1, room.x2):
|
|
||||||
for y in range(room.y1, room.y2):
|
|
||||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(FLOOR_TILES)
|
|
||||||
point.walkable = True
|
|
||||||
point.transparent = True
|
|
||||||
|
|
||||||
def carve_hallway(x1, y1, x2, y2):
|
|
||||||
#points = mcrfpy.libtcod.line(x1, y1, x2, y2)
|
|
||||||
points = []
|
|
||||||
if random.choice([True, False]):
|
|
||||||
# x1,y1 -> x2,y1 -> x2,y2
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y1, x2, y1))
|
|
||||||
points.extend(mcrfpy.libtcod.line(x2, y1, x2, y2))
|
|
||||||
else:
|
|
||||||
# x1,y1 -> x1,y2 -> x2,y2
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y1, x1, y2))
|
|
||||||
points.extend(mcrfpy.libtcod.line(x1, y2, x2, y2))
|
|
||||||
|
|
||||||
for x, y in points:
|
|
||||||
if 0 <= x < grid_width and 0 <= y < grid_height:
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(FLOOR_TILES)
|
|
||||||
point.walkable = True
|
|
||||||
point.transparent = True
|
|
||||||
|
|
||||||
def generate_dungeon(max_rooms=10, room_min_size=4, room_max_size=10):
|
|
||||||
rooms = []
|
|
||||||
|
|
||||||
# Fill with walls
|
|
||||||
for y in range(grid_height):
|
|
||||||
for x in range(grid_width):
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if point:
|
|
||||||
point.tilesprite = random.choice(WALL_TILES)
|
|
||||||
point.walkable = False
|
|
||||||
point.transparent = False
|
|
||||||
|
|
||||||
# Generate rooms
|
|
||||||
for _ in range(max_rooms):
|
|
||||||
w = random.randint(room_min_size, room_max_size)
|
|
||||||
h = random.randint(room_min_size, room_max_size)
|
|
||||||
x = random.randint(1, grid_width - w - 1)
|
|
||||||
y = random.randint(1, grid_height - h - 1)
|
|
||||||
|
|
||||||
new_room = Room(x, y, w, h)
|
|
||||||
|
|
||||||
failed = False
|
|
||||||
for other_room in rooms:
|
|
||||||
if new_room.intersects(other_room):
|
|
||||||
failed = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not failed:
|
|
||||||
carve_room(new_room)
|
|
||||||
|
|
||||||
if rooms:
|
|
||||||
prev_x, prev_y = rooms[-1].center()
|
|
||||||
new_x, new_y = new_room.center()
|
|
||||||
carve_hallway(prev_x, prev_y, new_x, new_y)
|
|
||||||
|
|
||||||
rooms.append(new_room)
|
|
||||||
|
|
||||||
return rooms
|
|
||||||
|
|
||||||
# Generate the dungeon
|
|
||||||
rooms = generate_dungeon(max_rooms=8, room_min_size=4, room_max_size=8)
|
|
||||||
|
|
||||||
# Add the grid to the scene
|
|
||||||
mcrfpy.sceneUI("tutorial").append(grid)
|
|
||||||
|
|
||||||
# Spawn player in the first room
|
|
||||||
if rooms:
|
|
||||||
spawn_x, spawn_y = rooms[0].center()
|
|
||||||
else:
|
|
||||||
spawn_x, spawn_y = 4, 4
|
|
||||||
|
|
||||||
class GameEntity(mcrfpy.Entity):
|
|
||||||
"""An entity whose default behavior is to prevent others from moving into its tile."""
|
|
||||||
|
|
||||||
def __init__(self, x, y, walkable=False, **kwargs):
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
self.walkable = walkable
|
|
||||||
self.dest_x = x
|
|
||||||
self.dest_y = y
|
|
||||||
self.is_moving = False
|
|
||||||
|
|
||||||
def get_position(self):
|
|
||||||
"""Get logical position (destination if moving, otherwise current)"""
|
|
||||||
if self.is_moving:
|
|
||||||
return (self.dest_x, self.dest_y)
|
|
||||||
return (int(self.x), int(self.y))
|
|
||||||
|
|
||||||
def on_bump(self, other):
|
|
||||||
return self.walkable # allow other's motion to proceed if entity is walkable
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<{self.__class__.__name__} x={self.x}, y={self.y}, sprite_index={self.sprite_index}>"
|
|
||||||
|
|
||||||
class CombatEntity(GameEntity):
|
|
||||||
def __init__(self, x, y, hp=10, damage=(1,3), **kwargs):
|
|
||||||
super().__init__(x=x, y=y, **kwargs)
|
|
||||||
self.hp = hp
|
|
||||||
self.damage = damage
|
|
||||||
|
|
||||||
def is_dead(self):
|
|
||||||
return self.hp <= 0
|
|
||||||
|
|
||||||
def start_move(self, new_x, new_y, duration=0.2, callback=None):
|
|
||||||
"""Start animating movement to new position"""
|
|
||||||
self.dest_x = new_x
|
|
||||||
self.dest_y = new_y
|
|
||||||
self.is_moving = True
|
|
||||||
|
|
||||||
# Define completion callback that resets is_moving
|
|
||||||
def movement_done(anim, entity):
|
|
||||||
self.is_moving = False
|
|
||||||
if callback:
|
|
||||||
callback(anim, entity)
|
|
||||||
|
|
||||||
# Create animations for smooth movement
|
|
||||||
anim_x = mcrfpy.Animation("x", float(new_x), duration, "easeInOutQuad", callback=movement_done)
|
|
||||||
anim_y = mcrfpy.Animation("y", float(new_y), duration, "easeInOutQuad")
|
|
||||||
|
|
||||||
anim_x.start(self)
|
|
||||||
anim_y.start(self)
|
|
||||||
|
|
||||||
def can_see(self, target_x, target_y):
|
|
||||||
"""Check if this entity can see the target position"""
|
|
||||||
mx, my = self.get_position()
|
|
||||||
|
|
||||||
# Simple distance check first
|
|
||||||
dist = abs(target_x - mx) + abs(target_y - my)
|
|
||||||
if dist > 6:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Line of sight check
|
|
||||||
line = list(mcrfpy.libtcod.line(mx, my, target_x, target_y))
|
|
||||||
for x, y in line[1:-1]: # Skip start and end
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
if cell and not cell.transparent:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def ai_turn(self, player_pos):
|
|
||||||
"""Decide next move"""
|
|
||||||
mx, my = self.get_position()
|
|
||||||
px, py = player_pos
|
|
||||||
|
|
||||||
# Simple AI: move toward player if visible
|
|
||||||
if self.can_see(px, py):
|
|
||||||
# Calculate direction toward player
|
|
||||||
dx = 0
|
|
||||||
dy = 0
|
|
||||||
if px > mx:
|
|
||||||
dx = 1
|
|
||||||
elif px < mx:
|
|
||||||
dx = -1
|
|
||||||
if py > my:
|
|
||||||
dy = 1
|
|
||||||
elif py < my:
|
|
||||||
dy = -1
|
|
||||||
|
|
||||||
# Prefer cardinal movement
|
|
||||||
if dx != 0 and dy != 0:
|
|
||||||
# Pick horizontal or vertical based on greater distance
|
|
||||||
if abs(px - mx) > abs(py - my):
|
|
||||||
dy = 0
|
|
||||||
else:
|
|
||||||
dx = 0
|
|
||||||
|
|
||||||
return (mx + dx, my + dy)
|
|
||||||
else:
|
|
||||||
# Random wander
|
|
||||||
dx, dy = random.choice([(0,1), (0,-1), (1,0), (-1,0)])
|
|
||||||
return (mx + dx, my + dy)
|
|
||||||
|
|
||||||
# Create a player entity
|
|
||||||
player = CombatEntity(
|
|
||||||
spawn_x, spawn_y,
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the player entity to the grid
|
|
||||||
grid.entities.append(player)
|
|
||||||
|
|
||||||
# Track all enemies
|
|
||||||
enemies = []
|
|
||||||
|
|
||||||
# Spawn enemies in other rooms
|
|
||||||
for i, room in enumerate(rooms[1:], 1): # Skip first room (player spawn)
|
|
||||||
if i <= 3: # Limit to 3 enemies for now
|
|
||||||
enemy_x, enemy_y = room.center()
|
|
||||||
enemy = CombatEntity(
|
|
||||||
enemy_x, enemy_y,
|
|
||||||
texture=hero_texture,
|
|
||||||
sprite_index=0 # Enemy sprite (borrow player's)
|
|
||||||
)
|
|
||||||
grid.entities.append(enemy)
|
|
||||||
enemies.append(enemy)
|
|
||||||
|
|
||||||
# Set the grid perspective to the player by default
|
|
||||||
# Note: The new perspective system uses entity references directly
|
|
||||||
grid.perspective = player
|
|
||||||
|
|
||||||
# Initial FOV computation
|
|
||||||
def update_fov():
|
|
||||||
"""Update field of view from current perspective"""
|
|
||||||
if grid.perspective == player:
|
|
||||||
grid.compute_fov(int(player.x), int(player.y), radius=8, algorithm=0)
|
|
||||||
player.update_visibility()
|
|
||||||
|
|
||||||
# Perform initial FOV calculation
|
|
||||||
update_fov()
|
|
||||||
|
|
||||||
# Center grid on current perspective
|
|
||||||
def center_on_perspective():
|
|
||||||
if grid.perspective == player:
|
|
||||||
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
|
|
||||||
|
|
||||||
center_on_perspective()
|
|
||||||
|
|
||||||
# Movement state tracking (from Part 3)
|
|
||||||
#is_moving = False # make it an entity property
|
|
||||||
move_queue = []
|
|
||||||
current_destination = None
|
|
||||||
current_move = None
|
|
||||||
|
|
||||||
# Store animation references
|
|
||||||
player_anim_x = None
|
|
||||||
player_anim_y = None
|
|
||||||
grid_anim_x = None
|
|
||||||
grid_anim_y = None
|
|
||||||
|
|
||||||
def movement_complete(anim, target):
|
|
||||||
"""Called when movement animation completes"""
|
|
||||||
global move_queue, current_destination, current_move
|
|
||||||
global player_anim_x, player_anim_y, is_player_turn
|
|
||||||
|
|
||||||
player.is_moving = False
|
|
||||||
current_move = None
|
|
||||||
current_destination = None
|
|
||||||
player_anim_x = None
|
|
||||||
player_anim_y = None
|
|
||||||
|
|
||||||
# Update FOV after movement
|
|
||||||
update_fov()
|
|
||||||
center_on_perspective()
|
|
||||||
|
|
||||||
# Player turn complete, start enemy turns and queued player move simultaneously
|
|
||||||
is_player_turn = False
|
|
||||||
process_enemy_turns_and_player_queue()
|
|
||||||
|
|
||||||
motion_speed = 0.20
|
|
||||||
is_player_turn = True # Track whose turn it is
|
|
||||||
|
|
||||||
def get_blocking_entity_at(x, y):
|
|
||||||
"""Get blocking entity at position"""
|
|
||||||
for e in grid.entities:
|
|
||||||
if not e.walkable and (x, y) == e.get_position():
|
|
||||||
return e
|
|
||||||
return None
|
|
||||||
|
|
||||||
def can_move_to(x, y, mover=None):
|
|
||||||
"""Check if a position is valid for movement"""
|
|
||||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
|
||||||
return False
|
|
||||||
|
|
||||||
point = grid.at(x, y)
|
|
||||||
if not point or not point.walkable:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check for blocking entities
|
|
||||||
blocker = get_blocking_entity_at(x, y)
|
|
||||||
if blocker and blocker != mover:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def process_enemy_turns_and_player_queue():
|
|
||||||
"""Process all enemy AI decisions and player's queued move simultaneously"""
|
|
||||||
global is_player_turn, move_queue
|
|
||||||
|
|
||||||
enemies_to_move = []
|
|
||||||
|
|
||||||
# Collect all enemy moves
|
|
||||||
for i, enemy in enumerate(enemies):
|
|
||||||
if enemy.is_dead():
|
|
||||||
continue
|
|
||||||
|
|
||||||
# AI decides next move based on player's position
|
|
||||||
target_x, target_y = enemy.ai_turn(player.get_position())
|
|
||||||
|
|
||||||
# Check if move is valid
|
|
||||||
if can_move_to(target_x, target_y, enemy):
|
|
||||||
enemies_to_move.append((enemy, target_x, target_y))
|
|
||||||
|
|
||||||
# Start all enemy animations simultaneously
|
|
||||||
any_enemy_moved = False
|
|
||||||
if enemies_to_move:
|
|
||||||
for enemy, tx, ty in enemies_to_move:
|
|
||||||
enemy.start_move(tx, ty, duration=motion_speed)
|
|
||||||
any_enemy_moved = True
|
|
||||||
|
|
||||||
# Process player's queued move at the same time
|
|
||||||
if move_queue:
|
|
||||||
next_move = move_queue.pop(0)
|
|
||||||
process_player_queued_move(next_move)
|
|
||||||
else:
|
|
||||||
# No queued move, set up callback to return control when animations finish
|
|
||||||
if any_enemy_moved:
|
|
||||||
# Wait for animations to complete
|
|
||||||
mcrfpy.setTimer("turn_complete", check_turn_complete, int(motion_speed * 1000) + 50)
|
|
||||||
else:
|
|
||||||
# No animations, return control immediately
|
|
||||||
is_player_turn = True
|
|
||||||
|
|
||||||
def process_player_queued_move(key):
|
|
||||||
"""Process player's queued move during enemy turn"""
|
|
||||||
global current_move, current_destination
|
|
||||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
if new_x != px or new_y != py:
|
|
||||||
# Check destination at animation end time (considering enemy destinations)
|
|
||||||
future_blocker = get_future_blocking_entity_at(new_x, new_y)
|
|
||||||
|
|
||||||
if future_blocker:
|
|
||||||
# Will bump at destination
|
|
||||||
# Schedule bump for when animations complete
|
|
||||||
mcrfpy.setTimer("delayed_bump", lambda t: handle_delayed_bump(future_blocker), int(motion_speed * 1000))
|
|
||||||
elif can_move_to(new_x, new_y, player):
|
|
||||||
# Valid move, start animation
|
|
||||||
player.is_moving = True
|
|
||||||
current_move = key
|
|
||||||
current_destination = (new_x, new_y)
|
|
||||||
player.dest_x = new_x
|
|
||||||
player.dest_y = new_y
|
|
||||||
|
|
||||||
# Player animation with callback
|
|
||||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=player_queued_move_complete)
|
|
||||||
player_anim_x.start(player)
|
|
||||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
|
||||||
player_anim_y.start(player)
|
|
||||||
|
|
||||||
# Move camera with player
|
|
||||||
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_x.start(grid)
|
|
||||||
grid_anim_y.start(grid)
|
|
||||||
else:
|
|
||||||
# Blocked by wall, wait for turn to complete
|
|
||||||
mcrfpy.setTimer("turn_complete", check_turn_complete, int(motion_speed * 1000) + 50)
|
|
||||||
|
|
||||||
def get_future_blocking_entity_at(x, y):
|
|
||||||
"""Get entity that will be blocking at position after current animations"""
|
|
||||||
for e in grid.entities:
|
|
||||||
if not e.walkable and (x, y) == (e.dest_x, e.dest_y):
|
|
||||||
return e
|
|
||||||
return None
|
|
||||||
|
|
||||||
def handle_delayed_bump(entity):
|
|
||||||
"""Handle bump after animations complete"""
|
|
||||||
global is_player_turn
|
|
||||||
entity.on_bump(player)
|
|
||||||
is_player_turn = True
|
|
||||||
|
|
||||||
def player_queued_move_complete(anim, target):
|
|
||||||
"""Called when player's queued movement completes"""
|
|
||||||
global is_player_turn
|
|
||||||
player.is_moving = False
|
|
||||||
update_fov()
|
|
||||||
center_on_perspective()
|
|
||||||
is_player_turn = True
|
|
||||||
|
|
||||||
def check_turn_complete(timer_name):
|
|
||||||
"""Check if all animations are complete"""
|
|
||||||
global is_player_turn
|
|
||||||
|
|
||||||
# Check if any entity is still moving
|
|
||||||
if player.is_moving:
|
|
||||||
mcrfpy.setTimer("turn_complete", check_turn_complete, 50)
|
|
||||||
return
|
|
||||||
|
|
||||||
for enemy in enemies:
|
|
||||||
if enemy.is_moving:
|
|
||||||
mcrfpy.setTimer("turn_complete", check_turn_complete, 50)
|
|
||||||
return
|
|
||||||
|
|
||||||
# All done
|
|
||||||
is_player_turn = True
|
|
||||||
|
|
||||||
def process_move(key):
|
|
||||||
"""Process a move based on the key"""
|
|
||||||
global current_move, current_destination, move_queue
|
|
||||||
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y, is_player_turn
|
|
||||||
|
|
||||||
# Only allow player movement on player's turn
|
|
||||||
if not is_player_turn:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Only allow player movement when in player perspective
|
|
||||||
if grid.perspective != player:
|
|
||||||
return
|
|
||||||
|
|
||||||
if player.is_moving:
|
|
||||||
move_queue.clear()
|
|
||||||
move_queue.append(key)
|
|
||||||
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
|
|
||||||
|
|
||||||
if new_x != px or new_y != py:
|
|
||||||
# Check what's at destination
|
|
||||||
blocker = get_blocking_entity_at(new_x, new_y)
|
|
||||||
|
|
||||||
if blocker:
|
|
||||||
# Bump interaction (combat will go here later)
|
|
||||||
blocker.on_bump(player)
|
|
||||||
# Still counts as a turn
|
|
||||||
is_player_turn = False
|
|
||||||
process_enemy_turns_and_player_queue()
|
|
||||||
elif can_move_to(new_x, new_y, player):
|
|
||||||
player.is_moving = True
|
|
||||||
current_move = key
|
|
||||||
current_destination = (new_x, new_y)
|
|
||||||
player.dest_x = new_x
|
|
||||||
player.dest_y = new_y
|
|
||||||
|
|
||||||
# Start player move animation
|
|
||||||
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
|
|
||||||
player_anim_x.start(player)
|
|
||||||
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
|
|
||||||
player_anim_y.start(player)
|
|
||||||
|
|
||||||
# Move camera with player
|
|
||||||
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
|
|
||||||
grid_anim_x.start(grid)
|
|
||||||
grid_anim_y.start(grid)
|
|
||||||
|
|
||||||
def handle_keys(key, state):
|
|
||||||
"""Handle keyboard input"""
|
|
||||||
if state == "start":
|
|
||||||
# Movement keys
|
|
||||||
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
|
|
||||||
process_move(key)
|
|
||||||
|
|
||||||
# Register the keyboard handler
|
|
||||||
mcrfpy.keypressScene(handle_keys)
|
|
||||||
|
|
||||||
# Add UI elements
|
|
||||||
title = mcrfpy.Caption((320, 10),
|
|
||||||
text="McRogueFace Tutorial - Part 6: Turn-based Movement",
|
|
||||||
)
|
|
||||||
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(title)
|
|
||||||
|
|
||||||
instructions = mcrfpy.Caption((150, 720),
|
|
||||||
text="Use WASD/Arrows to move. Enemies move after you!",
|
|
||||||
)
|
|
||||||
instructions.font_size = 18
|
|
||||||
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(instructions)
|
|
||||||
|
|
||||||
# Debug info
|
|
||||||
debug_caption = mcrfpy.Caption((10, 40),
|
|
||||||
text=f"Grid: {grid_width}x{grid_height} | Rooms: {len(rooms)} | Enemies: {len(enemies)}",
|
|
||||||
)
|
|
||||||
debug_caption.font_size = 16
|
|
||||||
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
|
|
||||||
mcrfpy.sceneUI("tutorial").append(debug_caption)
|
|
||||||
|
|
||||||
# Update function for turn display
|
|
||||||
def update_turn_display():
|
|
||||||
turn_text = "Player" if is_player_turn else "Enemy"
|
|
||||||
alive_enemies = sum(1 for e in enemies if not e.is_dead())
|
|
||||||
debug_caption.text = f"Grid: {grid_width}x{grid_height} | Turn: {turn_text} | Enemies: {alive_enemies}/{len(enemies)}"
|
|
||||||
|
|
||||||
# Timer to update display
|
|
||||||
def update_display(runtime):
|
|
||||||
update_turn_display()
|
|
||||||
|
|
||||||
mcrfpy.setTimer("display_update", update_display, 100)
|
|
||||||
|
|
||||||
print("Tutorial Part 6 loaded!")
|
|
||||||
print("Turn-based movement system active!")
|
|
||||||
print("- Enemies move after the player")
|
|
||||||
print("- Enemies pursue when they can see you")
|
|
||||||
print("- Enemies wander when they can't")
|
|
||||||
print("Use WASD or Arrow keys to move!")
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
|
|
@ -0,0 +1,50 @@
|
||||||
|
class GridPoint:
|
||||||
|
def __init__(self, color, walkable, tilesprite, transparent, visible, discovered, color_overlay, tile_overlay, uisprite):
|
||||||
|
self.color = color
|
||||||
|
self.walkable = walkable
|
||||||
|
self.tilesprite = tilesprite
|
||||||
|
self.transparent = transparent
|
||||||
|
self.visible = visible
|
||||||
|
self.discovered = discovered
|
||||||
|
self.color_overlay = color_overlay
|
||||||
|
self.tile_overlay = tile_overlay
|
||||||
|
self.uisprite = uisprite
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<GridPoint {self.color}, {self.tilesprite}/{self.uisprite} {'W' if self.walkable else '-'}{'T' if self.transparent else '-'}{'V' if self.visible else '-'}{'D' if self.discovered else '-'} {self.color_overlay}/{self.tile_overlay}>"
|
||||||
|
|
||||||
|
class Grid:
|
||||||
|
def __init__(self, title, gx, gy, gs, x, y, w, h, visible=False):
|
||||||
|
self.title = title
|
||||||
|
self.grid_x = gx
|
||||||
|
self.grid_y = gy
|
||||||
|
self.grid_size = gs
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.w = w
|
||||||
|
self.h = h
|
||||||
|
self.visible = visible
|
||||||
|
|
||||||
|
self.points = []
|
||||||
|
self.entities = []
|
||||||
|
|
||||||
|
def at(self, x, y):
|
||||||
|
if not (x > 0 and y > 0 and x < self.grid_x and y < self.grid_y): return None
|
||||||
|
return self.points[y * self.grid_y + x]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Grid {self.grid_x}x{self.grid_y}, grid_size={self.grid_size}, (({self.x},{self.y}), ({self.w}, {self.h})), visible={self.visible}>"
|
||||||
|
|
||||||
|
|
||||||
|
# CGrid(Grid* _g, int _ti, int _si, int _x, int _y, bool _v)
|
||||||
|
class Entity:
|
||||||
|
def __init__(self, parent, tex_index, sprite_index, x, y, visible=True):
|
||||||
|
self.parent = parent
|
||||||
|
self.tex_index = tex_index
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.visible = visible
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Entity on grid {repr(self.parent)}@({self.x},{self.y}), TI={self.tex_index}, SI={self.sprite_index}, visible={self.visible}>"
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import mcrfpy
|
||||||
|
BLACK = (0, 0, 0)
|
||||||
|
WHITE = (255, 255, 255)
|
||||||
|
RED, GREEN, BLUE = (255, 0, 0), (0, 255, 0), (0, 0, 255)
|
||||||
|
DARKRED, DARKGREEN, DARKBLUE = (192, 0, 0), (0, 192, 0), (0, 0, 192)
|
||||||
|
class MusicScene:
|
||||||
|
def __init__(self, ui_name = "demobox1", grid_name = "demogrid"):
|
||||||
|
# Texture & Sound Loading
|
||||||
|
print("Load textures")
|
||||||
|
mcrfpy.createTexture("./assets/test_portraits.png", 32, 8, 8) #0 - portraits
|
||||||
|
mcrfpy.createTexture("./assets/alives_other.png", 16, 64, 64) #1 - TinyWorld NPCs
|
||||||
|
mcrfpy.createTexture("./assets/alives_other.png", 32, 32, 32) #2 - TinyWorld NPCs - 2x2 sprite
|
||||||
|
# {"createSoundBuffer", McRFPy_API::_createSoundBuffer, METH_VARARGS, "(filename)"},
|
||||||
|
#{"loadMusic", McRFPy_API::_loadMusic, METH_VARARGS, "(filename)"},
|
||||||
|
#{"setMusicVolume", McRFPy_API::_setMusicVolume, METH_VARARGS, "(int)"},
|
||||||
|
#{"setSoundVolume", McRFPy_API::_setSoundVolume, METH_VARARGS, "(int)"},
|
||||||
|
#{"playSound", McRFPy_API::_playSound, METH_VARARGS, "(int)"},
|
||||||
|
#{"getMusicVolume", McRFPy_API::_getMusicVolume, METH_VARARGS, ""},
|
||||||
|
#{"getSoundVolume", McRFPy_API::_getSoundVolume, METH_VARARGS, ""},
|
||||||
|
|
||||||
|
mcrfpy.loadMusic("./assets/ultima.ogg")
|
||||||
|
mcrfpy.createSoundBuffer("./assets/boom.wav")
|
||||||
|
self.ui_name = ui_name
|
||||||
|
self.grid_name = grid_name
|
||||||
|
|
||||||
|
print("Create UI")
|
||||||
|
# Create dialog UI
|
||||||
|
mcrfpy.createMenu(ui_name, 20, 540, 500, 200)
|
||||||
|
mcrfpy.createCaption(ui_name, f"Music Volume: {mcrfpy.getMusicVolume()}", 24, RED)
|
||||||
|
mcrfpy.createCaption(ui_name, f"SFX Volume: {mcrfpy.getSoundVolume()}", 24, RED)
|
||||||
|
#mcrfpy.createButton(ui_name, 250, 20, 100, 50, DARKBLUE, (0, 0, 0), "clicky", "testaction")
|
||||||
|
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKRED, (0, 0, 0), "Music+", "mvol+")
|
||||||
|
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKGREEN, (0, 0, 0), "Music-", "mvol-")
|
||||||
|
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKBLUE, GREEN, "SFX+", "svol+")
|
||||||
|
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKBLUE, RED, "SFX-", "svol-")
|
||||||
|
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKRED, (0, 0, 0), "REPL", "startrepl")
|
||||||
|
mcrfpy.createSprite(ui_name, 1, 0, 20, 40, 3.0)
|
||||||
|
|
||||||
|
print("Create UI 2")
|
||||||
|
entitymenu = "entitytestmenu"
|
||||||
|
|
||||||
|
mcrfpy.createMenu(entitymenu, 840, 20, 20, 500)
|
||||||
|
mcrfpy.createButton(entitymenu, 0, 10, 150, 40, DARKBLUE, BLACK, "PlayM", "playm")
|
||||||
|
mcrfpy.createButton(entitymenu, 0, 60, 150, 40, DARKBLUE, BLACK, "StopM", "stopm")
|
||||||
|
mcrfpy.createButton(entitymenu, 0, 110, 150, 40, DARKBLUE, BLACK, "SFX", "boom")
|
||||||
|
print("Make UIs visible")
|
||||||
|
self.menus = mcrfpy.listMenus()
|
||||||
|
self.menus[0].visible = True
|
||||||
|
self.menus[1].w = 170
|
||||||
|
self.menus[1].visible = True
|
||||||
|
mcrfpy.modMenu(self.menus[0])
|
||||||
|
mcrfpy.modMenu(self.menus[1])
|
||||||
|
self.mvol = mcrfpy.getMusicVolume()
|
||||||
|
self.svol = mcrfpy.getSoundVolume()
|
||||||
|
mcrfpy.registerPyAction("mvol+", lambda: self.setmvol(self.mvol+10))
|
||||||
|
mcrfpy.registerPyAction("mvol-", lambda: self.setmvol(self.mvol-10))
|
||||||
|
mcrfpy.registerPyAction("svol+", lambda: self.setsvol(self.svol+10))
|
||||||
|
mcrfpy.registerPyAction("svol-", lambda: self.setsvol(self.svol-10))
|
||||||
|
mcrfpy.registerPyAction("playm", lambda: None)
|
||||||
|
mcrfpy.registerPyAction("stopm", lambda: None)
|
||||||
|
mcrfpy.registerPyAction("boom", lambda: mcrfpy.playSound(0))
|
||||||
|
|
||||||
|
def setmvol(self, v):
|
||||||
|
mcrfpy.setMusicVolume(int(v))
|
||||||
|
self.menus[0].captions[0].text = f"Music Volume: {mcrfpy.getMusicVolume():.1f}"
|
||||||
|
mcrfpy.modMenu(self.menus[0])
|
||||||
|
self.mvol = mcrfpy.getMusicVolume()
|
||||||
|
|
||||||
|
def setsvol(self, v):
|
||||||
|
mcrfpy.setSoundVolume(int(v))
|
||||||
|
self.menus[0].captions[1].text = f"Sound Volume: {mcrfpy.getSoundVolume():.1f}"
|
||||||
|
mcrfpy.modMenu(self.menus[0])
|
||||||
|
self.svol = mcrfpy.getSoundVolume()
|
||||||
|
|
||||||
|
scene = None
|
||||||
|
def start():
|
||||||
|
global scene
|
||||||
|
scene = MusicScene()
|
||||||
|
|
||||||
|
|
@ -0,0 +1,575 @@
|
||||||
|
import UIMenu
|
||||||
|
import Grid
|
||||||
|
import mcrfpy
|
||||||
|
from random import randint, choice
|
||||||
|
from pprint import pprint
|
||||||
|
#print("TestScene imported")
|
||||||
|
BLACK = (0, 0, 0)
|
||||||
|
WHITE = (255, 255, 255)
|
||||||
|
RED, GREEN, BLUE = (255, 0, 0), (0, 255, 0), (0, 0, 255)
|
||||||
|
DARKRED, DARKGREEN, DARKBLUE = (192, 0, 0), (0, 192, 0), (0, 0, 192)
|
||||||
|
|
||||||
|
animations_in_progress = 0
|
||||||
|
|
||||||
|
# don't load grid over and over, use the global scene
|
||||||
|
scene = None
|
||||||
|
|
||||||
|
class TestEntity:
|
||||||
|
def __init__(self, grid, label, tex_index, basesprite, x, y, texture_width=64, walk_frames=5, attack_frames=6, do_fov=False):
|
||||||
|
self.grid = grid
|
||||||
|
self.basesprite = basesprite
|
||||||
|
self.texture_width = texture_width
|
||||||
|
self.walk_frames = walk_frames
|
||||||
|
self.attack_frames = attack_frames
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.facing_direction = 0
|
||||||
|
self.do_fov = do_fov
|
||||||
|
self.label = label
|
||||||
|
self.inventory = []
|
||||||
|
#print(f"Calling C++ with: {repr((self.grid, label, tex_index, self.basesprite, x, y, self))}")
|
||||||
|
grids = mcrfpy.listGrids()
|
||||||
|
for g in grids:
|
||||||
|
if g.title == self.grid:
|
||||||
|
self.entity_index = len(g.entities)
|
||||||
|
mcrfpy.createEntity(self.grid, label, tex_index, self.basesprite, x, y, self)
|
||||||
|
|
||||||
|
def ai_act(self):
|
||||||
|
return # no AI motion
|
||||||
|
#if self.label == "player": return
|
||||||
|
self.move(randint(-1, 1), randint(-1, 1))
|
||||||
|
scene.actors += 1
|
||||||
|
|
||||||
|
def player_act(self):
|
||||||
|
#print("I'M INTERVENING")
|
||||||
|
mcrfpy.unlockPlayerInput()
|
||||||
|
scene.updatehints()
|
||||||
|
|
||||||
|
def die(self):
|
||||||
|
#self.x = -2
|
||||||
|
#self.y = -2
|
||||||
|
self.move(-1000,-1000)
|
||||||
|
self.animate(0,animove=(-1000,-1000))
|
||||||
|
self.x = -1000
|
||||||
|
self.y = -1000
|
||||||
|
self.label = "dead"
|
||||||
|
|
||||||
|
def interact(self, initiator, callback):
|
||||||
|
print(f"Interacted with {self.label}. ", end='')
|
||||||
|
if self.label == 'item':
|
||||||
|
print("'taking' item.")
|
||||||
|
callback()
|
||||||
|
self.die()
|
||||||
|
else:
|
||||||
|
print("blocking movement.")
|
||||||
|
|
||||||
|
def move(self, dx, dy, force=False):
|
||||||
|
# select animation direction
|
||||||
|
# prefer left or right for diagonals.
|
||||||
|
#grids = mcrfpy.listGrids()
|
||||||
|
for g in scene.grids:
|
||||||
|
if g.title == self.grid:
|
||||||
|
if not force and g.at(self.x + dx, self.y + dy) is None or not g.at(self.x + dx, self.y + dy).walkable:
|
||||||
|
#print("Blocked at target location.")
|
||||||
|
return
|
||||||
|
if not force: # entities can be stepped on when force=True (like collecting items!)
|
||||||
|
for entity in scene.tes:
|
||||||
|
if (entity.x, entity.y) == (self.x + dx, self.y + dy):
|
||||||
|
print(f"Blocked by entity {entity.label} at ({entity.x}, {entity.y})")
|
||||||
|
return entity.interact(self, lambda: self.move(dx, dy, force=True))
|
||||||
|
if self.label == "player":
|
||||||
|
mcrfpy.lockPlayerInput()
|
||||||
|
scene.updatehints()
|
||||||
|
if (dx == 0 and dy == 0):
|
||||||
|
direction = self.facing_direction # TODO, jump straight to computer turn
|
||||||
|
elif (dx):
|
||||||
|
direction = 2 if dx == +1 else 3
|
||||||
|
else:
|
||||||
|
direction = 0 if dy == +1 else 1
|
||||||
|
self.animate(direction, move=True, animove=(self.x + dx, self.y+dy))
|
||||||
|
self.facing_direction = direction
|
||||||
|
if (self.do_fov): mcrfpy.refreshFov()
|
||||||
|
|
||||||
|
|
||||||
|
def animate(self, direction, attacking=False, move=False, animove=None):
|
||||||
|
start_sprite = self.basesprite + (self.texture_width * (direction + (4 if attacking else 0)))
|
||||||
|
animation_frames = [start_sprite + i for i in range((self.attack_frames if attacking else self.walk_frames))]
|
||||||
|
mcrfpy.createAnimation(
|
||||||
|
0.15, # duration, seconds
|
||||||
|
self.grid, # parent: a UIMenu or Grid key
|
||||||
|
"entity", # target type: 'menu', 'grid', 'caption', 'button', 'sprite', or 'entity'
|
||||||
|
self.entity_index, # target id: integer index for menu or grid objs; None for grid/menu
|
||||||
|
"sprite", # field: 'position', 'size', 'bgcolor', 'textcolor', or 'sprite'
|
||||||
|
self.animation_done, #callback: callable once animation is complete
|
||||||
|
False, #loop: repeat indefinitely
|
||||||
|
animation_frames # values: iterable of frames for 'sprite', lerp target for others
|
||||||
|
)
|
||||||
|
#global animations_in_progress
|
||||||
|
#animations_in_progress += 1
|
||||||
|
if move:
|
||||||
|
pos = [self.x, self.y]
|
||||||
|
if (direction == 0): pos[1] += 1
|
||||||
|
elif (direction == 1): pos[1] -= 1
|
||||||
|
elif (direction == 2): pos[0] += 1
|
||||||
|
elif (direction == 3): pos[0] -= 1
|
||||||
|
if not animove:
|
||||||
|
self.x, self.y = pos
|
||||||
|
animove = pos
|
||||||
|
else:
|
||||||
|
pos = animove
|
||||||
|
self.x, self.y = animove
|
||||||
|
#scene.move_entity(self.grid, self.entity_index, pos)
|
||||||
|
#for g in mcrfpy.listGrids():
|
||||||
|
for g in scene.grids:
|
||||||
|
if g.title == self.grid:
|
||||||
|
g.entities[self.entity_index].x = pos[0]
|
||||||
|
g.entities[self.entity_index].y = pos[1]
|
||||||
|
mcrfpy.modGrid(g, True)
|
||||||
|
if animove:
|
||||||
|
mcrfpy.createAnimation(
|
||||||
|
0.25, # duration, seconds
|
||||||
|
self.grid, # parent: a UIMenu or Grid key
|
||||||
|
"entity", # target type: 'menu', 'grid', 'caption', 'button', 'sprite', or 'entity'
|
||||||
|
self.entity_index, # target id: integer index for menu or grid objs; None for grid/menu
|
||||||
|
"position", # field: 'position', 'size', 'bgcolor', 'textcolor', or 'sprite'
|
||||||
|
self.animation_done, #callback: callable once animation is complete
|
||||||
|
False, #loop: repeat indefinitely
|
||||||
|
animove # values: iterable of frames for 'sprite', lerp target for others
|
||||||
|
)
|
||||||
|
#animations_in_progress += 1
|
||||||
|
|
||||||
|
|
||||||
|
def animation_done(self):
|
||||||
|
#global animations_in_progress
|
||||||
|
#animations_in_progress -= 1
|
||||||
|
scene.actors -= 1
|
||||||
|
#print(f"{self} done animating - {scene.actors} remaining")
|
||||||
|
if scene.actors <= 0:
|
||||||
|
scene.actors = 0
|
||||||
|
mcrfpy.unlockPlayerInput()
|
||||||
|
scene.updatehints()
|
||||||
|
|
||||||
|
class TestItemEntity(TestEntity):
|
||||||
|
def __init__(self, grid, label, tex_index, basesprite, x, y, texture_width=64, walk_frames=5, attack_frames=6, do_fov=False, item="Nothing"):
|
||||||
|
super().__init__(grid, label, tex_index, basesprite, x, y, texture_width, walk_frames, attack_frames, do_fov)
|
||||||
|
self.item = item
|
||||||
|
|
||||||
|
def interact(self, initiator, callback):
|
||||||
|
if self.label == 'dead': return super().interact(initiator, callback)
|
||||||
|
print(f"Interacted with {self.label}, an item. Adding {self.item} to {initiator.label}'s inventory")
|
||||||
|
initiator.inventory.append(self.item)
|
||||||
|
callback()
|
||||||
|
scene.itemguis()
|
||||||
|
self.die()
|
||||||
|
|
||||||
|
class TestDoorEntity(TestEntity):
|
||||||
|
def __init__(self, grid, label, tex_index, basesprite, x, y, texture_width=64, walk_frames=5, attack_frames=6, do_fov=False, key="Nothing"):
|
||||||
|
super().__init__(grid, label, tex_index, basesprite, x, y, texture_width, walk_frames, attack_frames, do_fov)
|
||||||
|
self.key = key
|
||||||
|
|
||||||
|
def interact(self, initiator, callback):
|
||||||
|
if self.label == 'dead': return super().interact(initiator, callback)
|
||||||
|
print(f"Interacted with {self.label}, a Door. ", end='')
|
||||||
|
if self.key in initiator.inventory:
|
||||||
|
initiator.inventory.remove(self.key)
|
||||||
|
print("Taking key & passing.")
|
||||||
|
callback()
|
||||||
|
scene.itemguis()
|
||||||
|
self.die()
|
||||||
|
else:
|
||||||
|
print("The door is locked.")
|
||||||
|
|
||||||
|
class TestScene:
|
||||||
|
def __init__(self, ui_name = "demobox1", grid_name = "demogrid"):
|
||||||
|
# Texture & Sound Loading
|
||||||
|
self.actors = 0
|
||||||
|
#print("Load textures")
|
||||||
|
mcrfpy.createTexture("./assets/test_portraits.png", 32, 8, 8) #0 - portraits
|
||||||
|
mcrfpy.createTexture("./assets/alives_other.png", 16, 64, 64) #1 - TinyWorld NPCs
|
||||||
|
mcrfpy.createTexture("./assets/alives_other.png", 32, 32, 32) #2 - TinyWorld NPCs - 2x2 sprite
|
||||||
|
mcrfpy.createTexture("./assets/custom_player.png", 16, 5, 13) #3 - player
|
||||||
|
mcrfpy.createTexture("./assets/Sprite-0001.png", 80, 10, 10) #4 - LGJ2023 keycard + other icons
|
||||||
|
mcrfpy.createTexture("./assets/tiny_keycards.png", 16, 8, 8) #5 - tiny keycards (ground items)
|
||||||
|
self.ui_name = ui_name
|
||||||
|
self.grid_name = grid_name
|
||||||
|
|
||||||
|
# Menu index = 0
|
||||||
|
#print("Create UI")
|
||||||
|
# Create dialog UI
|
||||||
|
mcrfpy.createMenu(ui_name, 20, 540, 500, 200)
|
||||||
|
#mcrfpy.createCaption(ui_name, "Hello There", 18, BLACK)
|
||||||
|
mcrfpy.createCaption(ui_name, "", 24, RED)
|
||||||
|
#mcrfpy.createButton(ui_name, 250, 20, 100, 50, DARKBLUE, (0, 0, 0), "clicky", "testaction")
|
||||||
|
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKRED, (0, 0, 0), "REPL", "startrepl")
|
||||||
|
##mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKGREEN, (0, 0, 0), "map gen", "gridgen")
|
||||||
|
#mcrfpy.createButton(ui_name, 250, 20, 100, 50, DARKGREEN, (0, 0, 0), "mapL", "gridgen2")
|
||||||
|
#mcrfpy.createButton(ui_name, 250, 20, 100, 50, DARKBLUE, (192, 0, 0), "a_menu", "animtest")
|
||||||
|
#mcrfpy.createButton(ui_name, 250, 20, 100, 50, DARKRED, GREEN, "a_spr", "animtest2")
|
||||||
|
#mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKBLUE, GREEN, "Next sp", "nextsp")
|
||||||
|
#mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKBLUE, RED, "Prev sp", "prevsp")
|
||||||
|
#mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKBLUE, DARKGREEN, "+16 sp", "skipsp")
|
||||||
|
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKGREEN, (0, 0, 0), "Next", "nextsp")
|
||||||
|
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKBLUE, (0, 0, 0), "Prev", "prevsp")
|
||||||
|
mcrfpy.createSprite(ui_name, 4, 1, 10, 10, 2.0)
|
||||||
|
|
||||||
|
# Menu index = 1
|
||||||
|
#print("Create UI 2")
|
||||||
|
entitymenu = "entitytestmenu"
|
||||||
|
mcrfpy.createMenu(entitymenu, 840, 20, 20, 500)
|
||||||
|
mcrfpy.createButton(entitymenu, 0, 10, 150, 40, DARKBLUE, BLACK, "Up", "test_up")
|
||||||
|
mcrfpy.createButton(entitymenu, 0, 60, 150, 40, DARKBLUE, BLACK, "Down", "test_down")
|
||||||
|
mcrfpy.createButton(entitymenu, 0, 110, 150, 40, DARKBLUE, BLACK, "Left", "test_left")
|
||||||
|
mcrfpy.createButton(entitymenu, 0, 160, 150, 40, DARKBLUE, BLACK, "Right", "test_right")
|
||||||
|
mcrfpy.createButton(entitymenu, 0, 210, 150, 40, DARKBLUE, BLACK, "Attack", "test_attack")
|
||||||
|
mcrfpy.createButton(entitymenu, 0, 210, 150, 40, DARKBLUE, RED, "TE", "testent")
|
||||||
|
|
||||||
|
# Menu index = 2
|
||||||
|
mcrfpy.createMenu( "gridtitlemenu", 0, -10, 0, 0)
|
||||||
|
mcrfpy.createCaption("gridtitlemenu", "<grid name>", 18, WHITE)
|
||||||
|
#mcrfpy.createCaption("gridtitlemenu", "<camstate>", 16, WHITE)
|
||||||
|
|
||||||
|
# Menu index = 3
|
||||||
|
mcrfpy.createMenu( "hintsmenu", 0, 505, 0, 0)
|
||||||
|
mcrfpy.createCaption("hintsmenu", "<hintline>", 16, WHITE)
|
||||||
|
|
||||||
|
# Menu index = 4
|
||||||
|
# menu names must be created in alphabetical order (?!) - thanks, C++ hash map
|
||||||
|
mcrfpy.createMenu( "i", 600, 20, 0, 0)
|
||||||
|
#mcrfpy.createMenu( "camstatemenu", 600, 20, 0, 0)
|
||||||
|
mcrfpy.createCaption("i", "<camstate>", 16, WHITE)
|
||||||
|
|
||||||
|
# Menu index = 5
|
||||||
|
mcrfpy.createMenu( "j", 600, 500, 0, 0)
|
||||||
|
mcrfpy.createButton( "j", 0, 0, 80, 40, DARKBLUE, WHITE, "Recenter", "activatecamfollow")
|
||||||
|
|
||||||
|
# Menu index = 6, 7, 8, 9, 10: keycard sprites
|
||||||
|
mcrfpy.createMenu("k", 540, 540, 80, 80) #6
|
||||||
|
mcrfpy.createSprite("k", 4, 0, 10, 10, 1.0)
|
||||||
|
|
||||||
|
mcrfpy.createMenu("l", 540 + (80 * 1), 540, 80, 80) #7
|
||||||
|
mcrfpy.createSprite("l", 4, 1, 10, 10, 1.0)
|
||||||
|
|
||||||
|
mcrfpy.createMenu("m", 540 + (80 * 2), 540, 80, 80) #8
|
||||||
|
mcrfpy.createSprite("m", 4, 2, 10, 10, 1.0)
|
||||||
|
|
||||||
|
mcrfpy.createMenu("n", 540 + (80 * 3), 540, 80, 80) #9
|
||||||
|
mcrfpy.createSprite("n", 4, 3, 10, 10, 1.0)
|
||||||
|
|
||||||
|
mcrfpy.createMenu("o", 540 + (80 * 4), 540, 80, 80) #10
|
||||||
|
mcrfpy.createSprite("o", 4, 4, 10, 10, 1.0)
|
||||||
|
|
||||||
|
mcrfpy.createMenu("p", 20, 20, 40, 40) #11
|
||||||
|
#mcrfpy.createButton("p", 0, 0, 130, 40, DARKGREEN, (0, 0, 0), "Register", "keyregistration")
|
||||||
|
mcrfpy.createButton("p", 0, 0, 130, 40, DARKGREEN, (0, 0, 0), "Register", "startrepl")
|
||||||
|
mcrfpy.registerPyAction("keyregistration", keyregistration)
|
||||||
|
#print("Make UIs visible")
|
||||||
|
self.menus = mcrfpy.listMenus()
|
||||||
|
self.menus[0].visible = True
|
||||||
|
self.menus[1].w = 170
|
||||||
|
self.menus[1].visible = True
|
||||||
|
self.menus[2].visible = True
|
||||||
|
|
||||||
|
for mn in range(2, 6):
|
||||||
|
self.menus[mn].bgcolor = BLACK
|
||||||
|
self.menus[mn].visible = True
|
||||||
|
mcrfpy.modMenu(self.menus[mn])
|
||||||
|
|
||||||
|
for mn in range(6, 11):
|
||||||
|
self.menus[mn].bgcolor = BLACK
|
||||||
|
self.menus[mn].visible = False
|
||||||
|
mcrfpy.modMenu(self.menus[mn])
|
||||||
|
|
||||||
|
self.menus[11].visible=True
|
||||||
|
mcrfpy.modMenu(self.menus[11])
|
||||||
|
|
||||||
|
#self.menus[2].bgcolor = BLACK
|
||||||
|
#self.menus[3].visible = True
|
||||||
|
#self.menus[3].bgcolor = BLACK
|
||||||
|
#self.menus[4].visible = True
|
||||||
|
#self.menus[4].bgcolor = BLACK
|
||||||
|
#self.menus[5].visible = True
|
||||||
|
#mcrfpy.modMenu(self.menus[0])
|
||||||
|
#mcrfpy.modMenu(self.menus[1])
|
||||||
|
#mcrfpy.modMenu(self.menus[2])
|
||||||
|
#mcrfpy.modMenu(self.menus[3])
|
||||||
|
#mcrfpy.modMenu(self.menus[4])
|
||||||
|
#mcrfpy.modMenu(self.menus[5])
|
||||||
|
#pprint(mcrfpy.listMenus())
|
||||||
|
#print(f"UI 1 gave back this sprite: {self.menus[0].sprites}")
|
||||||
|
|
||||||
|
#print("Create grid")
|
||||||
|
# create grid (title, gx, gy, gs, x, y, w, h)
|
||||||
|
mcrfpy.createGrid(grid_name, 100, 100, 16, 20, 20, 800, 500)
|
||||||
|
self.grids = mcrfpy.listGrids()
|
||||||
|
#print(self.grids)
|
||||||
|
|
||||||
|
#print("Create entities")
|
||||||
|
#mcrfpy.createEntity("demogrid", "dragon", 2, 545, 10, 10, lambda: None)
|
||||||
|
#mcrfpy.createEntity("demogrid", "tinyenemy", 1, 1538, 3, 6, lambda: None)
|
||||||
|
|
||||||
|
#print("Create fancy entity")
|
||||||
|
self.player = TestEntity("demogrid", "player", 3, 20, 17, 3, 5, walk_frames=4, attack_frames=5, do_fov=True)
|
||||||
|
self.tes = [
|
||||||
|
TestEntity("demogrid", "classtest", 1, 1538, 5, 7, 64, walk_frames=5, attack_frames=6),
|
||||||
|
TestEntity("demogrid", "classtest", 1, 1545, 7, 9, 64, walk_frames=5, attack_frames=6),
|
||||||
|
TestEntity("demogrid", "classtest", 1, 1552, 9, 11, 64, walk_frames=5, attack_frames=6),
|
||||||
|
TestEntity("demogrid", "classtest", 1, 1566, 11, 13, 64, walk_frames=4, attack_frames=6),
|
||||||
|
#TestEntity("demogrid", "item", 1, 1573, 13, 15, 64, walk_frames=4, attack_frames=6),
|
||||||
|
TestEntity("demogrid", "classtest", 1, 1583, 15, 17, 64, walk_frames=4, attack_frames=6),
|
||||||
|
TestEntity("demogrid", "classtest", 1, 130, 9, 7, 64, walk_frames=5, attack_frames=6),
|
||||||
|
TestEntity("demogrid", "classtest", 1, 136, 11, 9, 64, walk_frames=5, attack_frames=6),
|
||||||
|
TestEntity("demogrid", "classtest", 1, 143, 13, 11, 64, walk_frames=5, attack_frames=6),
|
||||||
|
TestEntity("demogrid", "classtest", 1, 158, 15, 13, 64, walk_frames=5, attack_frames=6),
|
||||||
|
TestEntity("demogrid", "classtest", 1, 165, 17, 15, 64, walk_frames=5, attack_frames=6),
|
||||||
|
self.player,
|
||||||
|
|
||||||
|
TestItemEntity("demogrid", "GreenKeyCard", 5, 0, 19, 3, texture_width=64,
|
||||||
|
walk_frames=5, attack_frames=6, do_fov=False, item="Green Keycard"),
|
||||||
|
TestItemEntity("demogrid", "BlueKeyCard", 5, 1, 21, 3, texture_width=64,
|
||||||
|
walk_frames=5, attack_frames=6, do_fov=False, item="Blue Keycard"),
|
||||||
|
TestItemEntity("demogrid", "RedKeyCard", 5, 2, 23, 3, texture_width=64,
|
||||||
|
walk_frames=5, attack_frames=6, do_fov=False, item="Red Keycard"),
|
||||||
|
TestItemEntity("demogrid", "OrangeKeyCard", 5, 3, 25, 3, texture_width=64,
|
||||||
|
walk_frames=5, attack_frames=6, do_fov=False, item="Orange Keycard"),
|
||||||
|
TestItemEntity("demogrid", "DevilKeyCard", 5, 4, 27, 3, texture_width=64,
|
||||||
|
walk_frames=5, attack_frames=6, do_fov=False, item="Devil Keycard"),
|
||||||
|
|
||||||
|
TestDoorEntity("demogrid", "GreenKeyDoor", 5, 8, 19, 5, texture_width=64,
|
||||||
|
walk_frames=5, attack_frames=6, do_fov=False, key="Green Keycard"),
|
||||||
|
TestDoorEntity("demogrid", "BlueKeyDoor", 5, 9, 21, 5, texture_width=64,
|
||||||
|
walk_frames=5, attack_frames=6, do_fov=False, key="Blue Keycard"),
|
||||||
|
TestDoorEntity("demogrid", "RedKeyDoor", 5, 10, 23, 5, texture_width=64,
|
||||||
|
walk_frames=5, attack_frames=6, do_fov=False, key="Red Keycard"),
|
||||||
|
TestDoorEntity("demogrid", "OrangeKeyDoor", 5, 11, 25, 5, texture_width=64,
|
||||||
|
walk_frames=5, attack_frames=6, do_fov=False, key="Orange Keycard"),
|
||||||
|
TestDoorEntity("demogrid", "DevilKeyDoor", 5, 12, 27, 5, texture_width=64,
|
||||||
|
walk_frames=5, attack_frames=6, do_fov=False, key="Devil Keycard")
|
||||||
|
]
|
||||||
|
self.grids = mcrfpy.listGrids()
|
||||||
|
|
||||||
|
self.entity_direction = 0
|
||||||
|
mcrfpy.registerPyAction("test_down", lambda: [te.animate(0, False, True) for te in self.tes])
|
||||||
|
mcrfpy.registerPyAction("test_up", lambda: [te.animate(1, False, True) for te in self.tes])
|
||||||
|
mcrfpy.registerPyAction("test_right", lambda: [te.animate(2, False, True) for te in self.tes])
|
||||||
|
mcrfpy.registerPyAction("test_left", lambda: [te.animate(3, False, True) for te in self.tes])
|
||||||
|
mcrfpy.registerPyAction("test_attack", lambda: [te.animate(0, True) for te in self.tes])
|
||||||
|
mcrfpy.registerPyAction("testent", lambda: [te.animate(2, True) for te in self.tes])
|
||||||
|
mcrfpy.registerPyAction("activatecamfollow", lambda: mcrfpy.camFollow(True))
|
||||||
|
|
||||||
|
# Button behavior
|
||||||
|
self.clicks = 0
|
||||||
|
self.sprite_index = 0
|
||||||
|
#mcrfpy.registerPyAction("testaction", self.click)
|
||||||
|
mcrfpy.registerPyAction("gridgen", self.gridgen)
|
||||||
|
#mcrfpy.registerPyAction("gridgen2", lambda: self.gridgen())
|
||||||
|
#mcrfpy.registerPyAction("animtest", lambda: self.createAnimation())
|
||||||
|
#mcrfpy.registerPyAction("animtest2", lambda: self.createAnimation2())
|
||||||
|
mcrfpy.registerPyAction("nextsp", lambda: self.changesprite(1))
|
||||||
|
mcrfpy.registerPyAction("prevsp", lambda: self.changesprite(-1))
|
||||||
|
mcrfpy.registerPyAction("skipsp", lambda: self.changesprite(16))
|
||||||
|
mcrfpy.unlockPlayerInput()
|
||||||
|
mcrfpy.setActiveGrid("demogrid")
|
||||||
|
self.gridgen()
|
||||||
|
self.updatehints()
|
||||||
|
mcrfpy.camFollow(True)
|
||||||
|
|
||||||
|
def itemguis(self):
|
||||||
|
print(self.player.inventory)
|
||||||
|
items = ["Green Keycard", "Blue Keycard", "Red Keycard", "Orange Keycard", "Devil Keycard"]
|
||||||
|
for mn in range(6, 11):
|
||||||
|
self.menus[mn].visible = items[mn-6] in self.player.inventory
|
||||||
|
mcrfpy.modMenu(self.menus[mn])
|
||||||
|
|
||||||
|
def animate_entity(self, direction, attacking=False):
|
||||||
|
if direction is None:
|
||||||
|
direction = self.entity_direction
|
||||||
|
else:
|
||||||
|
self.entity_direction = direction
|
||||||
|
|
||||||
|
dragon_sprite = 545 + (32 * (direction + (4 if attacking else 0)))
|
||||||
|
dragon_animation = [dragon_sprite + i for i in range((5 if attacking else 4))]
|
||||||
|
mcrfpy.createAnimation(
|
||||||
|
1.0, # duration, seconds
|
||||||
|
"demogrid", # parent: a UIMenu or Grid key
|
||||||
|
"entity", # target type: 'menu', 'grid', 'caption', 'button', 'sprite', or 'entity'
|
||||||
|
0, # target id: integer index for menu or grid objs; None for grid/menu
|
||||||
|
"sprite", # field: 'position', 'size', 'bgcolor', 'textcolor', or 'sprite'
|
||||||
|
lambda: self.animation_done("demobox1"), #callback: callable once animation is complete
|
||||||
|
False, #loop: repeat indefinitely
|
||||||
|
dragon_animation # values: iterable of frames for 'sprite', lerp target for others
|
||||||
|
)
|
||||||
|
|
||||||
|
orc_sprite = 1538 + (64 * (direction + (4 if attacking else 0)))
|
||||||
|
orc_animation = [orc_sprite + i for i in range((5 if attacking else 4))]
|
||||||
|
mcrfpy.createAnimation(
|
||||||
|
1.0, # duration, seconds
|
||||||
|
"demogrid", # parent: a UIMenu or Grid key
|
||||||
|
"entity", # target type: 'menu', 'grid', 'caption', 'button', 'sprite', or 'entity'
|
||||||
|
1, # target id: integer index for menu or grid objs; None for grid/menu
|
||||||
|
"sprite", # field: 'position', 'size', 'bgcolor', 'textcolor', or 'sprite'
|
||||||
|
lambda: self.animation_done("demobox1"), #callback: callable once animation is complete
|
||||||
|
False, #loop: repeat indefinitely
|
||||||
|
orc_animation # values: iterable of frames for 'sprite', lerp target for others
|
||||||
|
)
|
||||||
|
|
||||||
|
#def move_entity(self, targetgrid, entity_index, position):
|
||||||
|
# for i, g in enumerate(self.grids):
|
||||||
|
# if g.title == targetgrid:
|
||||||
|
# g.entities[entity_index].x = position[0]
|
||||||
|
# g.entities[entity_index].y = position[1]
|
||||||
|
# #pts = g.points
|
||||||
|
# g.visible = True
|
||||||
|
# mcrfpy.modGrid(g)
|
||||||
|
# self.grids = mcrfpy.listGrids()
|
||||||
|
# #self.grids[i].points = pts
|
||||||
|
# return
|
||||||
|
|
||||||
|
|
||||||
|
def changesprite(self, n):
|
||||||
|
self.sprite_index += n
|
||||||
|
self.menus[0].captions[0].text = f"Sprite #{self.sprite_index}"
|
||||||
|
self.menus[0].sprites[0].sprite_index = self.sprite_index
|
||||||
|
mcrfpy.modMenu(self.menus[0])
|
||||||
|
|
||||||
|
def click(self):
|
||||||
|
self.clicks += 1
|
||||||
|
self.menus[0].captions[0].text = f"Clicks: {self.clicks}"
|
||||||
|
self.menus[0].sprites[0].sprite_index = randint(0, 3)
|
||||||
|
mcrfpy.modMenu(self.menus[0])
|
||||||
|
|
||||||
|
def updatehints(self):
|
||||||
|
self.menus[2].captions[0].text=mcrfpy.activeGrid()
|
||||||
|
mcrfpy.modMenu(self.menus[2])
|
||||||
|
|
||||||
|
self.menus[3].captions[0].text=mcrfpy.inputMode()
|
||||||
|
mcrfpy.modMenu(self.menus[3])
|
||||||
|
#self.menus[4].captions[0].text=f"follow: {mcrfpy.camFollow()}"
|
||||||
|
|
||||||
|
self.menus[4].captions[0].text="following" if mcrfpy.camFollow() else "free"
|
||||||
|
mcrfpy.modMenu(self.menus[4])
|
||||||
|
|
||||||
|
self.menus[5].visible = not mcrfpy.camFollow()
|
||||||
|
mcrfpy.modMenu(self.menus[5])
|
||||||
|
|
||||||
|
|
||||||
|
def gridgen(self):
|
||||||
|
|
||||||
|
#print(f"[Python] modifying {len(self.grids[0].points)} grid points")
|
||||||
|
for p in self.grids[0].points:
|
||||||
|
#p.color = (randint(0, 255), randint(64, 192), 0)
|
||||||
|
p.color = (0,0,0)
|
||||||
|
p.walkable = False
|
||||||
|
p.transparent = False
|
||||||
|
|
||||||
|
room_centers = [(randint(0, self.grids[0].grid_x-1), randint(0, self.grids[0].grid_y-1)) for i in range(20)] + [(17, 3), (20,10) + (20,5)]
|
||||||
|
#room_centers.append((3, 5))
|
||||||
|
for r in room_centers:
|
||||||
|
# random hallway
|
||||||
|
target = choice(room_centers)
|
||||||
|
length1 = abs(target[0] - r[0])
|
||||||
|
|
||||||
|
xbase = min(target[0], r[0])
|
||||||
|
for x in range(length1):
|
||||||
|
gpoint = self.grids[0].at(x, r[1])
|
||||||
|
if gpoint is None: continue
|
||||||
|
gpoint.walkable = True
|
||||||
|
gpoint.transparent = True
|
||||||
|
gpoint.color = (192, 192, 192)
|
||||||
|
|
||||||
|
length2 = abs(target[1] - r[1])
|
||||||
|
ybase = min(target[1], r[1])
|
||||||
|
for y in range(length2):
|
||||||
|
gpoint = self.grids[0].at(r[0], y)
|
||||||
|
if gpoint is None: continue
|
||||||
|
gpoint.walkable = True
|
||||||
|
gpoint.transparent = True
|
||||||
|
gpoint.color = (192, 192, 192)
|
||||||
|
|
||||||
|
for r in room_centers:
|
||||||
|
#print(r)
|
||||||
|
room_color = (randint(16, 24)*8, randint(16, 24)*8, randint(16, 24)*8)
|
||||||
|
#self.grids[0].at(r[0], r[1]).walkable = True
|
||||||
|
#self.grids[0].at(r[0], r[1]).color = room_color
|
||||||
|
halfx, halfy = randint(2, 11), randint(2,11)
|
||||||
|
for p_x in range(r[0] - halfx, r[0] + halfx):
|
||||||
|
for p_y in range(r[1] - halfy, r[1] + halfy):
|
||||||
|
gpoint = self.grids[0].at(p_x, p_y)
|
||||||
|
if gpoint is None: continue
|
||||||
|
gpoint.walkable = True
|
||||||
|
gpoint.transparent = True
|
||||||
|
gpoint.color = room_color
|
||||||
|
#print()
|
||||||
|
|
||||||
|
#print("[Python] Modifying:")
|
||||||
|
self.grids[0].at(10, 10).color = (255, 255, 255)
|
||||||
|
self.grids[0].at(10, 10).walkable = False
|
||||||
|
self.grids[0].visible = True
|
||||||
|
mcrfpy.modGrid(self.grids[0])
|
||||||
|
#self.grids = mcrfpy.listGrids()
|
||||||
|
#print(f"Sent grid: {repr(self.grids[0])}")
|
||||||
|
#print(f"Received grid: {repr(mcrfpy.listGrids()[0])}")
|
||||||
|
|
||||||
|
def animation_done(self, s):
|
||||||
|
print(f"The `{s}` animation completed.")
|
||||||
|
#self.menus = mcrfpy.listMenus()
|
||||||
|
|
||||||
|
# if (!PyArg_ParseTuple(args, "fsssiOOO", &duration, &parent, &target_type, &target_id, &field, &callback, &loop_obj, &values_obj)) return NULL;
|
||||||
|
def createAnimation(self):
|
||||||
|
print(self.menus)
|
||||||
|
self.menus = mcrfpy.listMenus()
|
||||||
|
self.menus[0].w = 500
|
||||||
|
self.menus[0].h = 200
|
||||||
|
print(self.menus)
|
||||||
|
mcrfpy.modMenu(self.menus[0])
|
||||||
|
print(self.menus)
|
||||||
|
mcrfpy.createAnimation(
|
||||||
|
3.0, # duration, seconds
|
||||||
|
"demobox1", # parent: a UIMenu or Grid key
|
||||||
|
"menu", # target type: 'menu', 'grid', 'caption', 'button', 'sprite', or 'entity'
|
||||||
|
0, # target id: integer index for menu or grid objs; None for grid/menu
|
||||||
|
"size", # field: 'position', 'size', 'bgcolor', 'textcolor', or 'sprite'
|
||||||
|
lambda: self.animation_done("demobox1"), #callback: callable once animation is complete
|
||||||
|
False, #loop: repeat indefinitely
|
||||||
|
[150, 100] # values: iterable of frames for 'sprite', lerp target for others
|
||||||
|
)
|
||||||
|
|
||||||
|
def createAnimation2(self):
|
||||||
|
mcrfpy.createAnimation(
|
||||||
|
5,
|
||||||
|
"demobox1",
|
||||||
|
"sprite",
|
||||||
|
0,
|
||||||
|
"sprite",
|
||||||
|
lambda: self.animation_done("sprite change"),
|
||||||
|
False,
|
||||||
|
[0, 1, 2, 1, 2, 0]
|
||||||
|
)
|
||||||
|
|
||||||
|
def keytest():
|
||||||
|
print("Key tested.")
|
||||||
|
|
||||||
|
def keyregistration():
|
||||||
|
print("Registering 'keytest'")
|
||||||
|
mcrfpy.registerPyAction("keytest", keytest)
|
||||||
|
print("Registering input")
|
||||||
|
print(mcrfpy.registerInputAction(15, "keytest")) # 15 = P
|
||||||
|
|
||||||
|
mcrfpy.registerPyAction("player_move_up", lambda: scene.player.move(0, -1))
|
||||||
|
mcrfpy.registerPyAction("player_move_left", lambda: scene.player.move(-1, 0))
|
||||||
|
mcrfpy.registerPyAction("player_move_down", lambda: scene.player.move(0, 1))
|
||||||
|
mcrfpy.registerPyAction("player_move_right", lambda: scene.player.move(1, 0))
|
||||||
|
|
||||||
|
mcrfpy.registerInputAction(ord('w') - ord('a'), "player_move_up")
|
||||||
|
mcrfpy.registerInputAction(ord('a') - ord('a'), "player_move_left")
|
||||||
|
mcrfpy.registerInputAction(ord('s') - ord('a'), "player_move_down")
|
||||||
|
mcrfpy.registerInputAction(ord('d') - ord('a'), "player_move_right")
|
||||||
|
|
||||||
|
|
||||||
|
def start():
|
||||||
|
global scene
|
||||||
|
#print("TestScene.start called")
|
||||||
|
scene = TestScene()
|
||||||
|
mcrfpy.refreshFov()
|
||||||
|
|
||||||
|
|
||||||
|
scene.updatehints()
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
class Caption:
|
||||||
|
def __init__(self, text, textsize, color):
|
||||||
|
self.text = text
|
||||||
|
self.textsize = textsize
|
||||||
|
self.color = color
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Caption text={self.text}, textsize={self.textsize}, color={self.color}>"
|
||||||
|
|
||||||
|
class Button:
|
||||||
|
def __init__(self, x, y, w, h, bgcolor, textcolor, text, actioncode):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.w = w
|
||||||
|
self.h = h
|
||||||
|
self.bgcolor = bgcolor
|
||||||
|
self.textcolor = textcolor
|
||||||
|
self.text = text
|
||||||
|
self.actioncode = actioncode
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Button ({self.x}, {self.y}, {self.w}, {self.h}), bgcolor={self.bgcolor}, textcolor={self.textcolor}, actioncode={self.actioncode}>"
|
||||||
|
|
||||||
|
class Sprite:
|
||||||
|
def __init__(self, tex_index, sprite_index, x, y):
|
||||||
|
self.tex_index = tex_index
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Sprite tex_index={self.tex_index}, self.sprite_index={self.sprite_index}, x={self.x}, y={self.y}>"
|
||||||
|
|
||||||
|
class UIMenu:
|
||||||
|
def __init__(self, title, x, y, w, h, bgcolor, visible=False):
|
||||||
|
self.title = title
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.w = w
|
||||||
|
self.h = h
|
||||||
|
self.bgcolor = bgcolor
|
||||||
|
self.visible = visible
|
||||||
|
self.captions = []
|
||||||
|
self.buttons = []
|
||||||
|
self.sprites = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<UIMenu title={repr(self.title)}, x={self.x}, y={self.y}, w={self.w}, h={self.h}, bgcolor={self.bgcolor}, visible={self.visible}, {len(self.captions)} captions, {len(self.buttons)} buttons, {len(self.sprites)} sprites>"
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
print("[Python] Attempting import")
|
||||||
|
import scriptable
|
||||||
|
|
||||||
|
print(f"[Python] calling fibonacci(8): {scriptable.fibonacci(8)}")
|
||||||
|
print(f"[Python] calling fibonacci(15): {scriptable.fibonacci(15)}")
|
||||||
|
|
||||||
|
import venv
|
||||||
|
print("[Python] Importing library installed with pip")
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import mcrfpy
|
||||||
|
mcrfpy.createTexture("./assets/test_portraits.png", 32, 8, 8)
|
||||||
|
from random import choice, randint
|
||||||
|
|
||||||
|
box_colors = [
|
||||||
|
(0, 0, 192),
|
||||||
|
(0, 192, 0),
|
||||||
|
(192, 0, 0),
|
||||||
|
(192, 192, 0),
|
||||||
|
(0, 192, 192),
|
||||||
|
(192, 0, 192)
|
||||||
|
]
|
||||||
|
|
||||||
|
text_colors = [
|
||||||
|
(0, 0, 255),
|
||||||
|
(0, 255, 0),
|
||||||
|
(255, 0, 0),
|
||||||
|
(255, 255, 0),
|
||||||
|
(0, 255, 255),
|
||||||
|
(255, 0, 255)
|
||||||
|
]
|
||||||
|
|
||||||
|
test_x = 500
|
||||||
|
test_y = 10
|
||||||
|
for i in range(40):
|
||||||
|
ui_name = f"test{i}"
|
||||||
|
mcrfpy.createMenu(ui_name, test_x, test_y, 400, 200)
|
||||||
|
mcrfpy.createCaption(ui_name, "Hello There", 18, choice(text_colors))
|
||||||
|
mcrfpy.createButton(ui_name, 250, 20, 100, 50, choice(box_colors), (0, 0, 0), "asdf", "testaction")
|
||||||
|
|
||||||
|
mcrfpy.createSprite(ui_name, 0, randint(0, 3), 650, 60, 5.0)
|
||||||
|
|
||||||
|
test_x -= 50
|
||||||
|
test_y += 50
|
||||||
|
if (test_x <= 50):
|
||||||
|
test_x = 500
|
||||||
|
#print(test_x)
|
||||||
118
src/ActionCode.h
118
src/ActionCode.h
|
|
@ -11,10 +11,10 @@ public:
|
||||||
const static int WHEEL_NUM = 4;
|
const static int WHEEL_NUM = 4;
|
||||||
const static int WHEEL_NEG = 2;
|
const static int WHEEL_NEG = 2;
|
||||||
const static int WHEEL_DEL = 1;
|
const static int WHEEL_DEL = 1;
|
||||||
static int keycode(const sf::Keyboard::Key& k) { return KEY + (int)k; }
|
static int keycode(sf::Keyboard::Key& k) { return KEY + (int)k; }
|
||||||
static int keycode(const sf::Mouse::Button& b) { return MOUSEBUTTON + (int)b; }
|
static int keycode(sf::Mouse::Button& b) { return MOUSEBUTTON + (int)b; }
|
||||||
//static int keycode(sf::Mouse::Wheel& w, float d) { return MOUSEWHEEL + (((int)w)<<12) + int(d*16) + 512; }
|
//static int keycode(sf::Mouse::Wheel& w, float d) { return MOUSEWHEEL + (((int)w)<<12) + int(d*16) + 512; }
|
||||||
static int keycode(const sf::Mouse::Wheel& w, float d) {
|
static int keycode(sf::Mouse::Wheel& w, float d) {
|
||||||
int neg = 0;
|
int neg = 0;
|
||||||
if (d < 0) { neg = 1; }
|
if (d < 0) { neg = 1; }
|
||||||
return MOUSEWHEEL + (w * WHEEL_NUM) + (neg * WHEEL_NEG) + 1;
|
return MOUSEWHEEL + (w * WHEEL_NUM) + (neg * WHEEL_NEG) + 1;
|
||||||
|
|
@ -31,116 +31,4 @@ public:
|
||||||
if (a & WHEEL_NEG) factor = -1;
|
if (a & WHEEL_NEG) factor = -1;
|
||||||
return (a & WHEEL_DEL) * factor;
|
return (a & WHEEL_DEL) * factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string key_str(const sf::Keyboard::Key& keycode)
|
|
||||||
{
|
|
||||||
switch(keycode)
|
|
||||||
{
|
|
||||||
case sf::Keyboard::Key::Unknown: return "Unknown"; break;
|
|
||||||
case sf::Keyboard::Key::A: return "A"; break;
|
|
||||||
case sf::Keyboard::Key::B: return "B"; break;
|
|
||||||
case sf::Keyboard::Key::C: return "C"; break;
|
|
||||||
case sf::Keyboard::Key::D: return "D"; break;
|
|
||||||
case sf::Keyboard::Key::E: return "E"; break;
|
|
||||||
case sf::Keyboard::Key::F: return "F"; break;
|
|
||||||
case sf::Keyboard::Key::G: return "G"; break;
|
|
||||||
case sf::Keyboard::Key::H: return "H"; break;
|
|
||||||
case sf::Keyboard::Key::I: return "I"; break;
|
|
||||||
case sf::Keyboard::Key::J: return "J"; break;
|
|
||||||
case sf::Keyboard::Key::K: return "K"; break;
|
|
||||||
case sf::Keyboard::Key::L: return "L"; break;
|
|
||||||
case sf::Keyboard::Key::M: return "M"; break;
|
|
||||||
case sf::Keyboard::Key::N: return "N"; break;
|
|
||||||
case sf::Keyboard::Key::O: return "O"; break;
|
|
||||||
case sf::Keyboard::Key::P: return "P"; break;
|
|
||||||
case sf::Keyboard::Key::Q: return "Q"; break;
|
|
||||||
case sf::Keyboard::Key::R: return "R"; break;
|
|
||||||
case sf::Keyboard::Key::S: return "S"; break;
|
|
||||||
case sf::Keyboard::Key::T: return "T"; break;
|
|
||||||
case sf::Keyboard::Key::U: return "U"; break;
|
|
||||||
case sf::Keyboard::Key::V: return "V"; break;
|
|
||||||
case sf::Keyboard::Key::W: return "W"; break;
|
|
||||||
case sf::Keyboard::Key::X: return "X"; break;
|
|
||||||
case sf::Keyboard::Key::Y: return "Y"; break;
|
|
||||||
case sf::Keyboard::Key::Z: return "Z"; break;
|
|
||||||
case sf::Keyboard::Key::Num0: return "Num0"; break;
|
|
||||||
case sf::Keyboard::Key::Num1: return "Num1"; break;
|
|
||||||
case sf::Keyboard::Key::Num2: return "Num2"; break;
|
|
||||||
case sf::Keyboard::Key::Num3: return "Num3"; break;
|
|
||||||
case sf::Keyboard::Key::Num4: return "Num4"; break;
|
|
||||||
case sf::Keyboard::Key::Num5: return "Num5"; break;
|
|
||||||
case sf::Keyboard::Key::Num6: return "Num6"; break;
|
|
||||||
case sf::Keyboard::Key::Num7: return "Num7"; break;
|
|
||||||
case sf::Keyboard::Key::Num8: return "Num8"; break;
|
|
||||||
case sf::Keyboard::Key::Num9: return "Num9"; break;
|
|
||||||
case sf::Keyboard::Key::Escape: return "Escape"; break;
|
|
||||||
case sf::Keyboard::Key::LControl: return "LControl"; break;
|
|
||||||
case sf::Keyboard::Key::LShift: return "LShift"; break;
|
|
||||||
case sf::Keyboard::Key::LAlt: return "LAlt"; break;
|
|
||||||
case sf::Keyboard::Key::LSystem: return "LSystem"; break;
|
|
||||||
case sf::Keyboard::Key::RControl: return "RControl"; break;
|
|
||||||
case sf::Keyboard::Key::RShift: return "RShift"; break;
|
|
||||||
case sf::Keyboard::Key::RAlt: return "RAlt"; break;
|
|
||||||
case sf::Keyboard::Key::RSystem: return "RSystem"; break;
|
|
||||||
case sf::Keyboard::Key::Menu: return "Menu"; break;
|
|
||||||
case sf::Keyboard::Key::LBracket: return "LBracket"; break;
|
|
||||||
case sf::Keyboard::Key::RBracket: return "RBracket"; break;
|
|
||||||
case sf::Keyboard::Key::Semicolon: return "Semicolon"; break;
|
|
||||||
case sf::Keyboard::Key::Comma: return "Comma"; break;
|
|
||||||
case sf::Keyboard::Key::Period: return "Period"; break;
|
|
||||||
case sf::Keyboard::Key::Apostrophe: return "Apostrophe"; break;
|
|
||||||
case sf::Keyboard::Key::Slash: return "Slash"; break;
|
|
||||||
case sf::Keyboard::Key::Backslash: return "Backslash"; break;
|
|
||||||
case sf::Keyboard::Key::Grave: return "Grave"; break;
|
|
||||||
case sf::Keyboard::Key::Equal: return "Equal"; break;
|
|
||||||
case sf::Keyboard::Key::Hyphen: return "Hyphen"; break;
|
|
||||||
case sf::Keyboard::Key::Space: return "Space"; break;
|
|
||||||
case sf::Keyboard::Key::Enter: return "Enter"; break;
|
|
||||||
case sf::Keyboard::Key::Backspace: return "Backspace"; break;
|
|
||||||
case sf::Keyboard::Key::Tab: return "Tab"; break;
|
|
||||||
case sf::Keyboard::Key::PageUp: return "PageUp"; break;
|
|
||||||
case sf::Keyboard::Key::PageDown: return "PageDown"; break;
|
|
||||||
case sf::Keyboard::Key::End: return "End"; break;
|
|
||||||
case sf::Keyboard::Key::Home: return "Home"; break;
|
|
||||||
case sf::Keyboard::Key::Insert: return "Insert"; break;
|
|
||||||
case sf::Keyboard::Key::Delete: return "Delete"; break;
|
|
||||||
case sf::Keyboard::Key::Add: return "Add"; break;
|
|
||||||
case sf::Keyboard::Key::Subtract: return "Subtract"; break;
|
|
||||||
case sf::Keyboard::Key::Multiply: return "Multiply"; break;
|
|
||||||
case sf::Keyboard::Key::Divide: return "Divide"; break;
|
|
||||||
case sf::Keyboard::Key::Left: return "Left"; break;
|
|
||||||
case sf::Keyboard::Key::Right: return "Right"; break;
|
|
||||||
case sf::Keyboard::Key::Up: return "Up"; break;
|
|
||||||
case sf::Keyboard::Key::Down: return "Down"; break;
|
|
||||||
case sf::Keyboard::Key::Numpad0: return "Numpad0"; break;
|
|
||||||
case sf::Keyboard::Key::Numpad1: return "Numpad1"; break;
|
|
||||||
case sf::Keyboard::Key::Numpad2: return "Numpad2"; break;
|
|
||||||
case sf::Keyboard::Key::Numpad3: return "Numpad3"; break;
|
|
||||||
case sf::Keyboard::Key::Numpad4: return "Numpad4"; break;
|
|
||||||
case sf::Keyboard::Key::Numpad5: return "Numpad5"; break;
|
|
||||||
case sf::Keyboard::Key::Numpad6: return "Numpad6"; break;
|
|
||||||
case sf::Keyboard::Key::Numpad7: return "Numpad7"; break;
|
|
||||||
case sf::Keyboard::Key::Numpad8: return "Numpad8"; break;
|
|
||||||
case sf::Keyboard::Key::Numpad9: return "Numpad9"; break;
|
|
||||||
case sf::Keyboard::Key::F1: return "F1"; break;
|
|
||||||
case sf::Keyboard::Key::F2: return "F2"; break;
|
|
||||||
case sf::Keyboard::Key::F3: return "F3"; break;
|
|
||||||
case sf::Keyboard::Key::F4: return "F4"; break;
|
|
||||||
case sf::Keyboard::Key::F5: return "F5"; break;
|
|
||||||
case sf::Keyboard::Key::F6: return "F6"; break;
|
|
||||||
case sf::Keyboard::Key::F7: return "F7"; break;
|
|
||||||
case sf::Keyboard::Key::F8: return "F8"; break;
|
|
||||||
case sf::Keyboard::Key::F9: return "F9"; break;
|
|
||||||
case sf::Keyboard::Key::F10: return "F10"; break;
|
|
||||||
case sf::Keyboard::Key::F11: return "F11"; break;
|
|
||||||
case sf::Keyboard::Key::F12: return "F12"; break;
|
|
||||||
case sf::Keyboard::Key::F13: return "F13"; break;
|
|
||||||
case sf::Keyboard::Key::F14: return "F14"; break;
|
|
||||||
case sf::Keyboard::Key::F15: return "F15"; break;
|
|
||||||
case sf::Keyboard::Key::Pause: return "Pause"; break;
|
|
||||||
default:
|
|
||||||
return "Any";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,675 +1,124 @@
|
||||||
#include "Animation.h"
|
#include "Animation.h"
|
||||||
#include "UIDrawable.h"
|
|
||||||
#include "UIEntity.h"
|
|
||||||
#include "PyAnimation.h"
|
|
||||||
#include "McRFPy_API.h"
|
|
||||||
#include "PythonObjectCache.h"
|
|
||||||
#include <cmath>
|
|
||||||
#include <algorithm>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
#ifndef M_PI
|
Animation::Animation(float _d, std::function<void()> _cb, bool _l)
|
||||||
#define M_PI 3.14159265358979323846
|
:duration(_d), callback(_cb), loop(_l), elapsed(0.0f) {}
|
||||||
#endif
|
|
||||||
|
|
||||||
// Forward declaration of PyAnimation type
|
// linear interpolation constructor
|
||||||
namespace mcrfpydef {
|
template<typename T>
|
||||||
extern PyTypeObject PyAnimationType;
|
LerpAnimation<T>::LerpAnimation(float _d, T _ev, T _sv, std::function<void()> _cb, std::function<void(T)> _w, bool _l)
|
||||||
}
|
:Animation(_d, _cb, _l), //duration(_d), target(_t), callback(_cb), loop(_l),elapsed(0.0f),
|
||||||
|
startvalue(_sv), endvalue(_ev), write(_w) {}
|
||||||
// Animation implementation
|
|
||||||
Animation::Animation(const std::string& targetProperty,
|
// discrete values constructor
|
||||||
const AnimationValue& targetValue,
|
template<typename T>
|
||||||
float duration,
|
DiscreteAnimation<T>::DiscreteAnimation(float _d, std::vector<T> _v, std::function<void()> _cb, std::function<void(T)> _w, bool _l)
|
||||||
EasingFunction easingFunc,
|
:Animation(_d, _cb, _l), //duration(_d), target(_t), callback(_cb), loop(_l), elapsed(0.0f),
|
||||||
bool delta,
|
index(0), nonelapsed(0.0f), values(_v), write(_w) {
|
||||||
PyObject* callback)
|
timestep = _d / _v.size();
|
||||||
: targetProperty(targetProperty)
|
|
||||||
, targetValue(targetValue)
|
|
||||||
, duration(duration)
|
|
||||||
, easingFunc(easingFunc)
|
|
||||||
, delta(delta)
|
|
||||||
, pythonCallback(callback)
|
|
||||||
{
|
|
||||||
// Increase reference count for Python callback
|
|
||||||
if (pythonCallback) {
|
|
||||||
Py_INCREF(pythonCallback);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* // don't call virtual functions (like cancel()) from base class destructor
|
||||||
|
* // child classes destructors' are called first anyway
|
||||||
Animation::~Animation() {
|
Animation::~Animation() {
|
||||||
// Decrease reference count for Python callback if we still own it
|
// deconstructor sets target to desired end state (no partial values)
|
||||||
PyObject* callback = pythonCallback;
|
cancel();
|
||||||
if (callback) {
|
}
|
||||||
pythonCallback = nullptr;
|
*/
|
||||||
|
|
||||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
template<>
|
||||||
Py_DECREF(callback);
|
void LerpAnimation<std::string>::lerp() {
|
||||||
PyGILState_Release(gstate);
|
//*(std::string*)target = ;
|
||||||
}
|
write(endvalue.substr(0, endvalue.length() * (elapsed / duration)));
|
||||||
|
|
||||||
// Clean up cache entry
|
|
||||||
if (serial_number != 0) {
|
|
||||||
PythonObjectCache::getInstance().remove(serial_number);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Animation::start(std::shared_ptr<UIDrawable> target) {
|
template<>
|
||||||
if (!target) return;
|
void LerpAnimation<int>::lerp() {
|
||||||
|
int delta = endvalue - startvalue;
|
||||||
targetWeak = target;
|
//*(int*)target = ;
|
||||||
elapsed = 0.0f;
|
write(startvalue + (elapsed / duration * delta));
|
||||||
callbackTriggered = false; // Reset callback state
|
|
||||||
|
|
||||||
// Capture start value from target
|
|
||||||
std::visit([this, &target](const auto& targetVal) {
|
|
||||||
using T = std::decay_t<decltype(targetVal)>;
|
|
||||||
|
|
||||||
if constexpr (std::is_same_v<T, float>) {
|
|
||||||
float value;
|
|
||||||
if (target->getProperty(targetProperty, value)) {
|
|
||||||
startValue = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, int>) {
|
|
||||||
int value;
|
|
||||||
if (target->getProperty(targetProperty, value)) {
|
|
||||||
startValue = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, std::vector<int>>) {
|
|
||||||
// For sprite animation, get current sprite index
|
|
||||||
int value;
|
|
||||||
if (target->getProperty(targetProperty, value)) {
|
|
||||||
startValue = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
|
||||||
sf::Color value;
|
|
||||||
if (target->getProperty(targetProperty, value)) {
|
|
||||||
startValue = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
|
||||||
sf::Vector2f value;
|
|
||||||
if (target->getProperty(targetProperty, value)) {
|
|
||||||
startValue = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, std::string>) {
|
|
||||||
std::string value;
|
|
||||||
if (target->getProperty(targetProperty, value)) {
|
|
||||||
startValue = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, targetValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Animation::startEntity(std::shared_ptr<UIEntity> target) {
|
template<>
|
||||||
if (!target) return;
|
void LerpAnimation<float>::lerp() {
|
||||||
|
int delta = endvalue - startvalue;
|
||||||
entityTargetWeak = target;
|
//*(float*)target = ;
|
||||||
elapsed = 0.0f;
|
write(startvalue + (elapsed / duration * delta));
|
||||||
callbackTriggered = false; // Reset callback state
|
|
||||||
|
|
||||||
// Capture the starting value from the entity
|
|
||||||
std::visit([this, target](const auto& val) {
|
|
||||||
using T = std::decay_t<decltype(val)>;
|
|
||||||
|
|
||||||
if constexpr (std::is_same_v<T, float>) {
|
|
||||||
float value = 0.0f;
|
|
||||||
if (target->getProperty(targetProperty, value)) {
|
|
||||||
startValue = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, int>) {
|
|
||||||
// For entities, we might need to handle sprite_index differently
|
|
||||||
if (targetProperty == "sprite_index" || targetProperty == "sprite_number") {
|
|
||||||
startValue = target->sprite.getSpriteIndex();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Entities don't support other types yet
|
|
||||||
}, targetValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Animation::hasValidTarget() const {
|
template<>
|
||||||
return !targetWeak.expired() || !entityTargetWeak.expired();
|
void LerpAnimation<sf::Vector2f>::lerp() {
|
||||||
|
//std::cout << "sf::Vector2f implementation of lerp." << std::endl;
|
||||||
|
int delta_x = endvalue.x - startvalue.x;
|
||||||
|
int delta_y = endvalue.y - startvalue.y;
|
||||||
|
//std::cout << "Start: " << startvalue.x << ", " << startvalue.y << "; End: " << endvalue.x << ", " << endvalue.y << std::endl;
|
||||||
|
//std::cout << "Delta: " << delta_x << ", " << delta_y << std::endl;
|
||||||
|
//((sf::Vector2f*)target)->x = startvalue.x + (elapsed / duration * delta_x);
|
||||||
|
//((sf::Vector2f*)target)->y = startvalue.y + (elapsed / duration * delta_y);
|
||||||
|
write(sf::Vector2f(startvalue.x + (elapsed / duration * delta_x),
|
||||||
|
startvalue.y + (elapsed / duration * delta_y)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Animation::clearCallback() {
|
template<>
|
||||||
// Safely clear the callback when PyAnimation is being destroyed
|
void LerpAnimation<sf::Vector2i>::lerp() {
|
||||||
PyObject* callback = pythonCallback;
|
int delta_x = endvalue.x - startvalue.y;
|
||||||
if (callback) {
|
int delta_y = endvalue.y - startvalue.y;
|
||||||
pythonCallback = nullptr;
|
//((sf::Vector2i*)target)->x = startvalue.x + (elapsed / duration * delta_x);
|
||||||
callbackTriggered = true; // Prevent future triggering
|
//((sf::Vector2i*)target)->y = startvalue.y + (elapsed / duration * delta_y);
|
||||||
|
write(sf::Vector2i(startvalue.x + (elapsed / duration * delta_x),
|
||||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
startvalue.y + (elapsed / duration * delta_y)));
|
||||||
Py_DECREF(callback);
|
|
||||||
PyGILState_Release(gstate);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Animation::complete() {
|
template<typename T>
|
||||||
// Jump to end of animation
|
void LerpAnimation<T>::step(float delta) {
|
||||||
elapsed = duration;
|
if (complete) return;
|
||||||
|
elapsed += delta;
|
||||||
// Apply final value
|
//std::cout << "LerpAnimation step function. Elapsed: " << elapsed <<std::endl;
|
||||||
if (auto target = targetWeak.lock()) {
|
lerp();
|
||||||
AnimationValue finalValue = interpolate(1.0f);
|
if (isDone()) { callback(); complete = true; cancel(); }; //use the exact value, not my math
|
||||||
applyValue(target.get(), finalValue);
|
|
||||||
}
|
|
||||||
else if (auto entity = entityTargetWeak.lock()) {
|
|
||||||
AnimationValue finalValue = interpolate(1.0f);
|
|
||||||
applyValue(entity.get(), finalValue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Animation::update(float deltaTime) {
|
template<typename T>
|
||||||
// Try to lock weak_ptr to get shared_ptr
|
void DiscreteAnimation<T>::step(float delta)
|
||||||
std::shared_ptr<UIDrawable> target = targetWeak.lock();
|
{
|
||||||
std::shared_ptr<UIEntity> entity = entityTargetWeak.lock();
|
if (complete) return;
|
||||||
|
nonelapsed += delta;
|
||||||
// If both are null, target was destroyed
|
//std::cout << "Nonelapsed: " << nonelapsed << " elapsed (pre-add): " << elapsed << " timestep: " << timestep << " duration: " << duration << " index: " << index << std::endl;
|
||||||
if (!target && !entity) {
|
if (nonelapsed < timestep) return;
|
||||||
return false; // Remove this animation
|
//std::cout << "values size: " << values.size() << " isDone(): " << isDone() << std::endl;
|
||||||
}
|
if (elapsed > duration && !complete) {callback(); complete = true; return; }
|
||||||
|
elapsed += nonelapsed; // or should it be += timestep?
|
||||||
if (isComplete()) {
|
if (index == values.size() - 1) return;
|
||||||
return false;
|
nonelapsed = 0; // or should it -= timestep?
|
||||||
}
|
index++;
|
||||||
|
//*(T*)target = values[index];
|
||||||
elapsed += deltaTime;
|
write(values[index]);
|
||||||
elapsed = std::min(elapsed, duration);
|
|
||||||
|
|
||||||
// Calculate easing value (0.0 to 1.0)
|
|
||||||
float t = duration > 0 ? elapsed / duration : 1.0f;
|
|
||||||
float easedT = easingFunc(t);
|
|
||||||
|
|
||||||
// Get interpolated value
|
|
||||||
AnimationValue currentValue = interpolate(easedT);
|
|
||||||
|
|
||||||
// Apply to whichever target is valid
|
|
||||||
if (target) {
|
|
||||||
applyValue(target.get(), currentValue);
|
|
||||||
} else if (entity) {
|
|
||||||
applyValue(entity.get(), currentValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger callback when animation completes
|
|
||||||
// Check pythonCallback again in case it was cleared during update
|
|
||||||
if (isComplete() && !callbackTriggered && pythonCallback) {
|
|
||||||
triggerCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
return !isComplete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimationValue Animation::getCurrentValue() const {
|
template<typename T>
|
||||||
float t = duration > 0 ? elapsed / duration : 1.0f;
|
void LerpAnimation<T>::cancel() {
|
||||||
float easedT = easingFunc(t);
|
//*(T*)target = endvalue;
|
||||||
return interpolate(easedT);
|
write(endvalue);
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimationValue Animation::interpolate(float t) const {
|
template<typename T>
|
||||||
// Visit the variant to perform type-specific interpolation
|
void DiscreteAnimation<T>::cancel() {
|
||||||
return std::visit([this, t](const auto& target) -> AnimationValue {
|
//*(T*)target = values[values.size() - 1];
|
||||||
using T = std::decay_t<decltype(target)>;
|
write(values[values.size() - 1]);
|
||||||
|
|
||||||
if constexpr (std::is_same_v<T, float>) {
|
|
||||||
// Interpolate float
|
|
||||||
const float* start = std::get_if<float>(&startValue);
|
|
||||||
if (!start) return target; // Type mismatch
|
|
||||||
|
|
||||||
if (delta) {
|
|
||||||
return *start + target * t;
|
|
||||||
} else {
|
|
||||||
return *start + (target - *start) * t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, int>) {
|
|
||||||
// Interpolate integer
|
|
||||||
const int* start = std::get_if<int>(&startValue);
|
|
||||||
if (!start) return target;
|
|
||||||
|
|
||||||
float result;
|
|
||||||
if (delta) {
|
|
||||||
result = *start + target * t;
|
|
||||||
} else {
|
|
||||||
result = *start + (target - *start) * t;
|
|
||||||
}
|
|
||||||
return static_cast<int>(std::round(result));
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, std::vector<int>>) {
|
|
||||||
// For sprite animation, interpolate through the list
|
|
||||||
if (target.empty()) return target;
|
|
||||||
|
|
||||||
// Map t to an index in the vector
|
|
||||||
size_t index = static_cast<size_t>(t * (target.size() - 1));
|
|
||||||
index = std::min(index, target.size() - 1);
|
|
||||||
return static_cast<int>(target[index]);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
|
||||||
// Interpolate color
|
|
||||||
const sf::Color* start = std::get_if<sf::Color>(&startValue);
|
|
||||||
if (!start) return target;
|
|
||||||
|
|
||||||
sf::Color result;
|
|
||||||
if (delta) {
|
|
||||||
result.r = std::clamp(start->r + target.r * t, 0.0f, 255.0f);
|
|
||||||
result.g = std::clamp(start->g + target.g * t, 0.0f, 255.0f);
|
|
||||||
result.b = std::clamp(start->b + target.b * t, 0.0f, 255.0f);
|
|
||||||
result.a = std::clamp(start->a + target.a * t, 0.0f, 255.0f);
|
|
||||||
} else {
|
|
||||||
result.r = start->r + (target.r - start->r) * t;
|
|
||||||
result.g = start->g + (target.g - start->g) * t;
|
|
||||||
result.b = start->b + (target.b - start->b) * t;
|
|
||||||
result.a = start->a + (target.a - start->a) * t;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
|
||||||
// Interpolate vector
|
|
||||||
const sf::Vector2f* start = std::get_if<sf::Vector2f>(&startValue);
|
|
||||||
if (!start) return target;
|
|
||||||
|
|
||||||
if (delta) {
|
|
||||||
return sf::Vector2f(start->x + target.x * t,
|
|
||||||
start->y + target.y * t);
|
|
||||||
} else {
|
|
||||||
return sf::Vector2f(start->x + (target.x - start->x) * t,
|
|
||||||
start->y + (target.y - start->y) * t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, std::string>) {
|
|
||||||
// For text, show characters based on t
|
|
||||||
const std::string* start = std::get_if<std::string>(&startValue);
|
|
||||||
if (!start) return target;
|
|
||||||
|
|
||||||
// If delta mode, append characters from target
|
|
||||||
if (delta) {
|
|
||||||
size_t chars = static_cast<size_t>(target.length() * t);
|
|
||||||
return *start + target.substr(0, chars);
|
|
||||||
} else {
|
|
||||||
// Transition from start text to target text
|
|
||||||
if (t < 0.5f) {
|
|
||||||
// First half: remove characters from start
|
|
||||||
size_t chars = static_cast<size_t>(start->length() * (1.0f - t * 2.0f));
|
|
||||||
return start->substr(0, chars);
|
|
||||||
} else {
|
|
||||||
// Second half: add characters to target
|
|
||||||
size_t chars = static_cast<size_t>(target.length() * ((t - 0.5f) * 2.0f));
|
|
||||||
return target.substr(0, chars);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return target; // Fallback
|
|
||||||
}, targetValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Animation::applyValue(UIDrawable* target, const AnimationValue& value) {
|
bool Animation::isDone() {
|
||||||
if (!target) return;
|
return elapsed + Animation::EPSILON >= duration;
|
||||||
|
|
||||||
std::visit([this, target](const auto& val) {
|
|
||||||
using T = std::decay_t<decltype(val)>;
|
|
||||||
|
|
||||||
if constexpr (std::is_same_v<T, float>) {
|
|
||||||
target->setProperty(targetProperty, val);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, int>) {
|
|
||||||
target->setProperty(targetProperty, val);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
|
||||||
target->setProperty(targetProperty, val);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
|
||||||
target->setProperty(targetProperty, val);
|
|
||||||
}
|
|
||||||
else if constexpr (std::is_same_v<T, std::string>) {
|
|
||||||
target->setProperty(targetProperty, val);
|
|
||||||
}
|
|
||||||
}, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Animation::applyValue(UIEntity* entity, const AnimationValue& value) {
|
namespace animation_template_implementations {
|
||||||
if (!entity) return;
|
// instantiate to compile concrete templates
|
||||||
|
//LerpAnimation<sf::Vector2f> implement_vector2f;
|
||||||
|
|
||||||
std::visit([this, entity](const auto& val) {
|
auto implement_v2f_const = LerpAnimation<sf::Vector2<float>>(4.0, sf::Vector2<float>(), sf::Vector2f(1,1), [](){}, [](sf::Vector2f v){}, false);
|
||||||
using T = std::decay_t<decltype(val)>;
|
auto implement_disc_i = DiscreteAnimation<int>(3.0, std::vector<int>{0},[](){},[](int){},false);
|
||||||
|
//LerpAnimation<sf::Vector2i> implement_vector2i;
|
||||||
if constexpr (std::is_same_v<T, float>) {
|
//LerpAnimation<int> implment_int;
|
||||||
entity->setProperty(targetProperty, val);
|
//LerpAnimation<std::string> implment_string;
|
||||||
}
|
//LerpAnimation<float> implement_float;
|
||||||
else if constexpr (std::is_same_v<T, int>) {
|
//DiscreteAnimation<int> implement_int_discrete;
|
||||||
entity->setProperty(targetProperty, val);
|
|
||||||
}
|
|
||||||
// Entities don't support other types yet
|
|
||||||
}, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Animation::triggerCallback() {
|
|
||||||
if (!pythonCallback) return;
|
|
||||||
|
|
||||||
// Ensure we only trigger once
|
|
||||||
if (callbackTriggered) return;
|
|
||||||
callbackTriggered = true;
|
|
||||||
|
|
||||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
|
||||||
|
|
||||||
// TODO: In future, create PyAnimation wrapper for this animation
|
|
||||||
// For now, pass None for both parameters
|
|
||||||
PyObject* args = PyTuple_New(2);
|
|
||||||
Py_INCREF(Py_None);
|
|
||||||
Py_INCREF(Py_None);
|
|
||||||
PyTuple_SetItem(args, 0, Py_None); // animation parameter
|
|
||||||
PyTuple_SetItem(args, 1, Py_None); // target parameter
|
|
||||||
|
|
||||||
PyObject* result = PyObject_CallObject(pythonCallback, args);
|
|
||||||
Py_DECREF(args);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
// Print error but don't crash
|
|
||||||
PyErr_Print();
|
|
||||||
PyErr_Clear(); // Clear the error state
|
|
||||||
} else {
|
|
||||||
Py_DECREF(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
PyGILState_Release(gstate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Easing functions implementation
|
|
||||||
namespace EasingFunctions {
|
|
||||||
|
|
||||||
float linear(float t) {
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeIn(float t) {
|
|
||||||
return t * t;
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeOut(float t) {
|
|
||||||
return t * (2.0f - t);
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeInOut(float t) {
|
|
||||||
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quadratic
|
|
||||||
float easeInQuad(float t) {
|
|
||||||
return t * t;
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeOutQuad(float t) {
|
|
||||||
return t * (2.0f - t);
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeInOutQuad(float t) {
|
|
||||||
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cubic
|
|
||||||
float easeInCubic(float t) {
|
|
||||||
return t * t * t;
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeOutCubic(float t) {
|
|
||||||
float t1 = t - 1.0f;
|
|
||||||
return t1 * t1 * t1 + 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeInOutCubic(float t) {
|
|
||||||
return t < 0.5f ? 4.0f * t * t * t : (t - 1.0f) * (2.0f * t - 2.0f) * (2.0f * t - 2.0f) + 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quartic
|
|
||||||
float easeInQuart(float t) {
|
|
||||||
return t * t * t * t;
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeOutQuart(float t) {
|
|
||||||
float t1 = t - 1.0f;
|
|
||||||
return 1.0f - t1 * t1 * t1 * t1;
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeInOutQuart(float t) {
|
|
||||||
return t < 0.5f ? 8.0f * t * t * t * t : 1.0f - 8.0f * (t - 1.0f) * (t - 1.0f) * (t - 1.0f) * (t - 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sine
|
|
||||||
float easeInSine(float t) {
|
|
||||||
return 1.0f - std::cos(t * M_PI / 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeOutSine(float t) {
|
|
||||||
return std::sin(t * M_PI / 2.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeInOutSine(float t) {
|
|
||||||
return 0.5f * (1.0f - std::cos(M_PI * t));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exponential
|
|
||||||
float easeInExpo(float t) {
|
|
||||||
return t == 0.0f ? 0.0f : std::pow(2.0f, 10.0f * (t - 1.0f));
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeOutExpo(float t) {
|
|
||||||
return t == 1.0f ? 1.0f : 1.0f - std::pow(2.0f, -10.0f * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeInOutExpo(float t) {
|
|
||||||
if (t == 0.0f) return 0.0f;
|
|
||||||
if (t == 1.0f) return 1.0f;
|
|
||||||
if (t < 0.5f) {
|
|
||||||
return 0.5f * std::pow(2.0f, 20.0f * t - 10.0f);
|
|
||||||
} else {
|
|
||||||
return 1.0f - 0.5f * std::pow(2.0f, -20.0f * t + 10.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Circular
|
|
||||||
float easeInCirc(float t) {
|
|
||||||
return 1.0f - std::sqrt(1.0f - t * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeOutCirc(float t) {
|
|
||||||
float t1 = t - 1.0f;
|
|
||||||
return std::sqrt(1.0f - t1 * t1);
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeInOutCirc(float t) {
|
|
||||||
if (t < 0.5f) {
|
|
||||||
return 0.5f * (1.0f - std::sqrt(1.0f - 4.0f * t * t));
|
|
||||||
} else {
|
|
||||||
return 0.5f * (std::sqrt(1.0f - (2.0f * t - 2.0f) * (2.0f * t - 2.0f)) + 1.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Elastic
|
|
||||||
float easeInElastic(float t) {
|
|
||||||
if (t == 0.0f) return 0.0f;
|
|
||||||
if (t == 1.0f) return 1.0f;
|
|
||||||
float p = 0.3f;
|
|
||||||
float a = 1.0f;
|
|
||||||
float s = p / 4.0f;
|
|
||||||
float t1 = t - 1.0f;
|
|
||||||
return -(a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p));
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeOutElastic(float t) {
|
|
||||||
if (t == 0.0f) return 0.0f;
|
|
||||||
if (t == 1.0f) return 1.0f;
|
|
||||||
float p = 0.3f;
|
|
||||||
float a = 1.0f;
|
|
||||||
float s = p / 4.0f;
|
|
||||||
return a * std::pow(2.0f, -10.0f * t) * std::sin((t - s) * (2.0f * M_PI) / p) + 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeInOutElastic(float t) {
|
|
||||||
if (t == 0.0f) return 0.0f;
|
|
||||||
if (t == 1.0f) return 1.0f;
|
|
||||||
float p = 0.45f;
|
|
||||||
float a = 1.0f;
|
|
||||||
float s = p / 4.0f;
|
|
||||||
|
|
||||||
if (t < 0.5f) {
|
|
||||||
float t1 = 2.0f * t - 1.0f;
|
|
||||||
return -0.5f * (a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p));
|
|
||||||
} else {
|
|
||||||
float t1 = 2.0f * t - 1.0f;
|
|
||||||
return a * std::pow(2.0f, -10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p) * 0.5f + 1.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Back (overshooting)
|
|
||||||
float easeInBack(float t) {
|
|
||||||
const float s = 1.70158f;
|
|
||||||
return t * t * ((s + 1.0f) * t - s);
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeOutBack(float t) {
|
|
||||||
const float s = 1.70158f;
|
|
||||||
float t1 = t - 1.0f;
|
|
||||||
return t1 * t1 * ((s + 1.0f) * t1 + s) + 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeInOutBack(float t) {
|
|
||||||
const float s = 1.70158f * 1.525f;
|
|
||||||
if (t < 0.5f) {
|
|
||||||
return 0.5f * (4.0f * t * t * ((s + 1.0f) * 2.0f * t - s));
|
|
||||||
} else {
|
|
||||||
float t1 = 2.0f * t - 2.0f;
|
|
||||||
return 0.5f * (t1 * t1 * ((s + 1.0f) * t1 + s) + 2.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bounce
|
|
||||||
float easeOutBounce(float t) {
|
|
||||||
if (t < 1.0f / 2.75f) {
|
|
||||||
return 7.5625f * t * t;
|
|
||||||
} else if (t < 2.0f / 2.75f) {
|
|
||||||
float t1 = t - 1.5f / 2.75f;
|
|
||||||
return 7.5625f * t1 * t1 + 0.75f;
|
|
||||||
} else if (t < 2.5f / 2.75f) {
|
|
||||||
float t1 = t - 2.25f / 2.75f;
|
|
||||||
return 7.5625f * t1 * t1 + 0.9375f;
|
|
||||||
} else {
|
|
||||||
float t1 = t - 2.625f / 2.75f;
|
|
||||||
return 7.5625f * t1 * t1 + 0.984375f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeInBounce(float t) {
|
|
||||||
return 1.0f - easeOutBounce(1.0f - t);
|
|
||||||
}
|
|
||||||
|
|
||||||
float easeInOutBounce(float t) {
|
|
||||||
if (t < 0.5f) {
|
|
||||||
return 0.5f * easeInBounce(2.0f * t);
|
|
||||||
} else {
|
|
||||||
return 0.5f * easeOutBounce(2.0f * t - 1.0f) + 0.5f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get easing function by name
|
|
||||||
EasingFunction getByName(const std::string& name) {
|
|
||||||
static std::unordered_map<std::string, EasingFunction> easingMap = {
|
|
||||||
{"linear", linear},
|
|
||||||
{"easeIn", easeIn},
|
|
||||||
{"easeOut", easeOut},
|
|
||||||
{"easeInOut", easeInOut},
|
|
||||||
{"easeInQuad", easeInQuad},
|
|
||||||
{"easeOutQuad", easeOutQuad},
|
|
||||||
{"easeInOutQuad", easeInOutQuad},
|
|
||||||
{"easeInCubic", easeInCubic},
|
|
||||||
{"easeOutCubic", easeOutCubic},
|
|
||||||
{"easeInOutCubic", easeInOutCubic},
|
|
||||||
{"easeInQuart", easeInQuart},
|
|
||||||
{"easeOutQuart", easeOutQuart},
|
|
||||||
{"easeInOutQuart", easeInOutQuart},
|
|
||||||
{"easeInSine", easeInSine},
|
|
||||||
{"easeOutSine", easeOutSine},
|
|
||||||
{"easeInOutSine", easeInOutSine},
|
|
||||||
{"easeInExpo", easeInExpo},
|
|
||||||
{"easeOutExpo", easeOutExpo},
|
|
||||||
{"easeInOutExpo", easeInOutExpo},
|
|
||||||
{"easeInCirc", easeInCirc},
|
|
||||||
{"easeOutCirc", easeOutCirc},
|
|
||||||
{"easeInOutCirc", easeInOutCirc},
|
|
||||||
{"easeInElastic", easeInElastic},
|
|
||||||
{"easeOutElastic", easeOutElastic},
|
|
||||||
{"easeInOutElastic", easeInOutElastic},
|
|
||||||
{"easeInBack", easeInBack},
|
|
||||||
{"easeOutBack", easeOutBack},
|
|
||||||
{"easeInOutBack", easeInOutBack},
|
|
||||||
{"easeInBounce", easeInBounce},
|
|
||||||
{"easeOutBounce", easeOutBounce},
|
|
||||||
{"easeInOutBounce", easeInOutBounce}
|
|
||||||
};
|
|
||||||
|
|
||||||
auto it = easingMap.find(name);
|
|
||||||
if (it != easingMap.end()) {
|
|
||||||
return it->second;
|
|
||||||
}
|
|
||||||
return linear; // Default to linear
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace EasingFunctions
|
|
||||||
|
|
||||||
// AnimationManager implementation
|
|
||||||
AnimationManager& AnimationManager::getInstance() {
|
|
||||||
static AnimationManager instance;
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) {
|
|
||||||
if (animation && animation->hasValidTarget()) {
|
|
||||||
if (isUpdating) {
|
|
||||||
// Defer adding during update to avoid iterator invalidation
|
|
||||||
pendingAnimations.push_back(animation);
|
|
||||||
} else {
|
|
||||||
activeAnimations.push_back(animation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AnimationManager::update(float deltaTime) {
|
|
||||||
// Set flag to defer new animations
|
|
||||||
isUpdating = true;
|
|
||||||
|
|
||||||
// Remove completed or invalid animations
|
|
||||||
activeAnimations.erase(
|
|
||||||
std::remove_if(activeAnimations.begin(), activeAnimations.end(),
|
|
||||||
[deltaTime](std::shared_ptr<Animation>& anim) {
|
|
||||||
return !anim || !anim->update(deltaTime);
|
|
||||||
}),
|
|
||||||
activeAnimations.end()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clear update flag
|
|
||||||
isUpdating = false;
|
|
||||||
|
|
||||||
// Add any animations that were created during update
|
|
||||||
if (!pendingAnimations.empty()) {
|
|
||||||
activeAnimations.insert(activeAnimations.end(),
|
|
||||||
pendingAnimations.begin(),
|
|
||||||
pendingAnimations.end());
|
|
||||||
pendingAnimations.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void AnimationManager::clear(bool completeAnimations) {
|
|
||||||
if (completeAnimations) {
|
|
||||||
// Complete all animations before clearing
|
|
||||||
for (auto& anim : activeAnimations) {
|
|
||||||
if (anim) {
|
|
||||||
anim->complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
activeAnimations.clear();
|
|
||||||
}
|
}
|
||||||
209
src/Animation.h
209
src/Animation.h
|
|
@ -1,175 +1,50 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include "Common.h"
|
||||||
#include <string>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
|
||||||
#include <variant>
|
|
||||||
#include <vector>
|
|
||||||
#include <SFML/Graphics.hpp>
|
|
||||||
#include "Python.h"
|
|
||||||
|
|
||||||
// Forward declarations
|
class Animation
|
||||||
class UIDrawable;
|
{
|
||||||
class UIEntity;
|
protected:
|
||||||
|
static constexpr float EPSILON = 0.05;
|
||||||
// Forward declare namespace
|
float duration, elapsed;
|
||||||
namespace EasingFunctions {
|
std::function<void()> callback;
|
||||||
float linear(float t);
|
bool loop;
|
||||||
}
|
bool complete=false;
|
||||||
|
|
||||||
// Easing function type
|
|
||||||
typedef std::function<float(float)> EasingFunction;
|
|
||||||
|
|
||||||
// Animation target value can be various types
|
|
||||||
typedef std::variant<
|
|
||||||
float, // Single float value
|
|
||||||
int, // Single integer value
|
|
||||||
std::vector<int>, // List of integers (for sprite animation)
|
|
||||||
sf::Color, // Color animation
|
|
||||||
sf::Vector2f, // Vector animation
|
|
||||||
std::string // String animation (for text)
|
|
||||||
> AnimationValue;
|
|
||||||
|
|
||||||
class Animation {
|
|
||||||
public:
|
public:
|
||||||
// Constructor
|
//Animation(float, T, T*, std::function<void()>, bool); // lerp
|
||||||
Animation(const std::string& targetProperty,
|
//Animation(float, std::vector<T>, T*, std::function<void()>, bool); // discrete
|
||||||
const AnimationValue& targetValue,
|
Animation(float, std::function<void()>, bool);
|
||||||
float duration,
|
//Animation() {};
|
||||||
EasingFunction easingFunc = EasingFunctions::linear,
|
virtual void step(float) = 0;
|
||||||
bool delta = false,
|
virtual void cancel() = 0;
|
||||||
PyObject* callback = nullptr);
|
bool isDone();
|
||||||
|
|
||||||
// Destructor - cleanup Python callback reference
|
|
||||||
~Animation();
|
|
||||||
|
|
||||||
// Apply this animation to a drawable
|
|
||||||
void start(std::shared_ptr<UIDrawable> target);
|
|
||||||
|
|
||||||
// Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable)
|
|
||||||
void startEntity(std::shared_ptr<UIEntity> target);
|
|
||||||
|
|
||||||
// Complete the animation immediately (jump to final value)
|
|
||||||
void complete();
|
|
||||||
|
|
||||||
// Update animation (called each frame)
|
|
||||||
// Returns true if animation is still running, false if complete
|
|
||||||
bool update(float deltaTime);
|
|
||||||
|
|
||||||
// Get current interpolated value
|
|
||||||
AnimationValue getCurrentValue() const;
|
|
||||||
|
|
||||||
// Check if animation has valid target
|
|
||||||
bool hasValidTarget() const;
|
|
||||||
|
|
||||||
// Clear the callback (called when PyAnimation is deallocated)
|
|
||||||
void clearCallback();
|
|
||||||
|
|
||||||
// Animation properties
|
|
||||||
std::string getTargetProperty() const { return targetProperty; }
|
|
||||||
float getDuration() const { return duration; }
|
|
||||||
float getElapsed() const { return elapsed; }
|
|
||||||
bool isComplete() const { return elapsed >= duration; }
|
|
||||||
bool isDelta() const { return delta; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::string targetProperty; // Property name to animate (e.g., "x", "color.r", "sprite_number")
|
|
||||||
AnimationValue startValue; // Starting value (captured when animation starts)
|
|
||||||
AnimationValue targetValue; // Target value to animate to
|
|
||||||
float duration; // Animation duration in seconds
|
|
||||||
float elapsed = 0.0f; // Elapsed time
|
|
||||||
EasingFunction easingFunc; // Easing function to use
|
|
||||||
bool delta; // If true, targetValue is relative to start
|
|
||||||
|
|
||||||
// RAII: Use weak_ptr for safe target tracking
|
|
||||||
std::weak_ptr<UIDrawable> targetWeak;
|
|
||||||
std::weak_ptr<UIEntity> entityTargetWeak;
|
|
||||||
|
|
||||||
// Callback support
|
|
||||||
PyObject* pythonCallback = nullptr; // Python callback function (we own a reference)
|
|
||||||
bool callbackTriggered = false; // Ensure callback only fires once
|
|
||||||
PyObject* pyAnimationWrapper = nullptr; // Weak reference to PyAnimation if created from Python
|
|
||||||
|
|
||||||
// Python object cache support
|
|
||||||
uint64_t serial_number = 0;
|
|
||||||
|
|
||||||
// Helper to interpolate between values
|
|
||||||
AnimationValue interpolate(float t) const;
|
|
||||||
|
|
||||||
// Helper to apply value to target
|
|
||||||
void applyValue(UIDrawable* target, const AnimationValue& value);
|
|
||||||
void applyValue(UIEntity* entity, const AnimationValue& value);
|
|
||||||
|
|
||||||
// Trigger callback when animation completes
|
|
||||||
void triggerCallback();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Easing functions library
|
template<typename T>
|
||||||
namespace EasingFunctions {
|
class LerpAnimation: public Animation
|
||||||
// Basic easing functions
|
{
|
||||||
float linear(float t);
|
T startvalue, endvalue;
|
||||||
float easeIn(float t);
|
std::function<void(T)> write;
|
||||||
float easeOut(float t);
|
void lerp();
|
||||||
float easeInOut(float t);
|
|
||||||
|
|
||||||
// Advanced easing functions
|
|
||||||
float easeInQuad(float t);
|
|
||||||
float easeOutQuad(float t);
|
|
||||||
float easeInOutQuad(float t);
|
|
||||||
|
|
||||||
float easeInCubic(float t);
|
|
||||||
float easeOutCubic(float t);
|
|
||||||
float easeInOutCubic(float t);
|
|
||||||
|
|
||||||
float easeInQuart(float t);
|
|
||||||
float easeOutQuart(float t);
|
|
||||||
float easeInOutQuart(float t);
|
|
||||||
|
|
||||||
float easeInSine(float t);
|
|
||||||
float easeOutSine(float t);
|
|
||||||
float easeInOutSine(float t);
|
|
||||||
|
|
||||||
float easeInExpo(float t);
|
|
||||||
float easeOutExpo(float t);
|
|
||||||
float easeInOutExpo(float t);
|
|
||||||
|
|
||||||
float easeInCirc(float t);
|
|
||||||
float easeOutCirc(float t);
|
|
||||||
float easeInOutCirc(float t);
|
|
||||||
|
|
||||||
float easeInElastic(float t);
|
|
||||||
float easeOutElastic(float t);
|
|
||||||
float easeInOutElastic(float t);
|
|
||||||
|
|
||||||
float easeInBack(float t);
|
|
||||||
float easeOutBack(float t);
|
|
||||||
float easeInOutBack(float t);
|
|
||||||
|
|
||||||
float easeInBounce(float t);
|
|
||||||
float easeOutBounce(float t);
|
|
||||||
float easeInOutBounce(float t);
|
|
||||||
|
|
||||||
// Get easing function by name
|
|
||||||
EasingFunction getByName(const std::string& name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animation manager to handle active animations
|
|
||||||
class AnimationManager {
|
|
||||||
public:
|
public:
|
||||||
static AnimationManager& getInstance();
|
~LerpAnimation() { cancel(); }
|
||||||
|
LerpAnimation(float, T, T, std::function<void()>, std::function<void(T)>, bool);
|
||||||
// Add an animation to be managed
|
//LerpAnimation() {};
|
||||||
void addAnimation(std::shared_ptr<Animation> animation);
|
void step(float) override final;
|
||||||
|
void cancel() override final;
|
||||||
// Update all animations
|
};
|
||||||
void update(float deltaTime);
|
|
||||||
|
template<typename T>
|
||||||
// Clear all animations (optionally completing them first)
|
class DiscreteAnimation: public Animation
|
||||||
void clear(bool completeAnimations = false);
|
{
|
||||||
|
std::vector<T> values;
|
||||||
private:
|
std::function<void(T)> write;
|
||||||
AnimationManager() = default;
|
float nonelapsed, timestep;
|
||||||
std::vector<std::shared_ptr<Animation>> activeAnimations;
|
int index;
|
||||||
std::vector<std::shared_ptr<Animation>> pendingAnimations; // Animations to add after update
|
public:
|
||||||
bool isUpdating = false; // Flag to track if we're in update loop
|
DiscreteAnimation(float, std::vector<T>, std::function<void()>, std::function<void(T)>, bool);
|
||||||
|
DiscreteAnimation() {};
|
||||||
|
~DiscreteAnimation() { cancel(); }
|
||||||
|
void step(float) override final;
|
||||||
|
void cancel() override final;
|
||||||
};
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#include "Button.h"
|
||||||
|
|
||||||
|
void Button::render(sf::RenderWindow & window)
|
||||||
|
{
|
||||||
|
window.draw(rect);
|
||||||
|
window.draw(caption);
|
||||||
|
}
|
||||||
|
|
||||||
|
Button::Button(int x, int y, int w, int h,
|
||||||
|
sf::Color _background, sf::Color _textcolor,
|
||||||
|
const char * _caption, sf::Font & font,
|
||||||
|
const char * _action)
|
||||||
|
{
|
||||||
|
rect.setPosition(sf::Vector2f(x, y));
|
||||||
|
rect.setSize(sf::Vector2f(w, h));
|
||||||
|
rect.setFillColor(_background);
|
||||||
|
|
||||||
|
caption.setFillColor(_textcolor);
|
||||||
|
caption.setPosition(sf::Vector2f(x, y));
|
||||||
|
caption.setString(_caption);
|
||||||
|
caption.setFont(font);
|
||||||
|
|
||||||
|
action = _action;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Common.h"
|
||||||
|
|
||||||
|
class Button
|
||||||
|
{
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
public:
|
||||||
|
// TODO / JankMode: setter & getter for these three fields
|
||||||
|
// were protected, but directly changing them should be...fine?
|
||||||
|
sf::RectangleShape rect;
|
||||||
|
sf::Text caption;
|
||||||
|
std::string action;
|
||||||
|
|
||||||
|
Button() {};
|
||||||
|
Button(int x, int y, int w, int h,
|
||||||
|
sf::Color _background, sf::Color _textcolor,
|
||||||
|
const char * _caption, sf::Font & font,
|
||||||
|
const char * _action);
|
||||||
|
void setPosition(sf::Vector2f v) { rect.setPosition(v); caption.setPosition(v); }
|
||||||
|
void setSize(sf::Vector2f & v) { rect.setSize(v); }
|
||||||
|
void setBackground(sf::Color c) { rect.setFillColor(c); }
|
||||||
|
void setCaption(std::string & s) { caption.setString(s); }
|
||||||
|
void setTextColor(sf::Color c) { caption.setFillColor(c); }
|
||||||
|
void render(sf::RenderWindow & window);
|
||||||
|
auto contains(sf::Vector2i p) { return rect.getGlobalBounds().contains(p.x, p.y); }
|
||||||
|
auto contains(sf::Vector2f rel, sf::Vector2i p) {
|
||||||
|
return rect.getGlobalBounds().contains(p.x - rel.x, p.y - rel.y);
|
||||||
|
}
|
||||||
|
auto getAction() { return action; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
};
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
#include "CommandLineParser.h"
|
|
||||||
#include <iostream>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
CommandLineParser::CommandLineParser(int argc, char* argv[])
|
|
||||||
: argc(argc), argv(argv) {}
|
|
||||||
|
|
||||||
CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& config) {
|
|
||||||
ParseResult result;
|
|
||||||
current_arg = 1; // Reset for each parse
|
|
||||||
|
|
||||||
// Detect if running as Python interpreter
|
|
||||||
std::filesystem::path exec_name = std::filesystem::path(argv[0]).filename();
|
|
||||||
if (exec_name.string().find("python") == 0) {
|
|
||||||
config.headless = true;
|
|
||||||
config.python_mode = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (current_arg < argc) {
|
|
||||||
std::string arg = argv[current_arg];
|
|
||||||
|
|
||||||
// Handle Python-style single-letter flags
|
|
||||||
if (arg == "-h" || arg == "--help") {
|
|
||||||
print_help();
|
|
||||||
result.should_exit = true;
|
|
||||||
result.exit_code = 0;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arg == "-V" || arg == "--version") {
|
|
||||||
print_version();
|
|
||||||
result.should_exit = true;
|
|
||||||
result.exit_code = 0;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Python execution modes
|
|
||||||
if (arg == "-c") {
|
|
||||||
config.python_mode = true;
|
|
||||||
current_arg++;
|
|
||||||
if (current_arg >= argc) {
|
|
||||||
std::cerr << "Argument expected for the -c option" << std::endl;
|
|
||||||
result.should_exit = true;
|
|
||||||
result.exit_code = 1;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
config.python_command = argv[current_arg];
|
|
||||||
current_arg++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arg == "-m") {
|
|
||||||
config.python_mode = true;
|
|
||||||
current_arg++;
|
|
||||||
if (current_arg >= argc) {
|
|
||||||
std::cerr << "Argument expected for the -m option" << std::endl;
|
|
||||||
result.should_exit = true;
|
|
||||||
result.exit_code = 1;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
config.python_module = argv[current_arg];
|
|
||||||
current_arg++;
|
|
||||||
// Collect remaining args as module args
|
|
||||||
while (current_arg < argc) {
|
|
||||||
config.script_args.push_back(argv[current_arg]);
|
|
||||||
current_arg++;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arg == "-i") {
|
|
||||||
config.interactive_mode = true;
|
|
||||||
config.python_mode = true;
|
|
||||||
current_arg++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// McRogueFace specific flags
|
|
||||||
if (arg == "--headless") {
|
|
||||||
config.headless = true;
|
|
||||||
config.audio_enabled = false;
|
|
||||||
current_arg++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arg == "--audio-off") {
|
|
||||||
config.audio_enabled = false;
|
|
||||||
current_arg++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arg == "--audio-on") {
|
|
||||||
config.audio_enabled = true;
|
|
||||||
current_arg++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arg == "--screenshot") {
|
|
||||||
config.take_screenshot = true;
|
|
||||||
current_arg++;
|
|
||||||
if (current_arg < argc && argv[current_arg][0] != '-') {
|
|
||||||
config.screenshot_path = argv[current_arg];
|
|
||||||
current_arg++;
|
|
||||||
} else {
|
|
||||||
config.screenshot_path = "screenshot.png";
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arg == "--exec") {
|
|
||||||
current_arg++;
|
|
||||||
if (current_arg >= argc) {
|
|
||||||
std::cerr << "Argument expected for the --exec option" << std::endl;
|
|
||||||
result.should_exit = true;
|
|
||||||
result.exit_code = 1;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
config.exec_scripts.push_back(argv[current_arg]);
|
|
||||||
config.python_mode = true;
|
|
||||||
current_arg++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no flags matched, treat as positional argument (script name)
|
|
||||||
if (arg[0] != '-') {
|
|
||||||
config.script_path = arg;
|
|
||||||
config.python_mode = true;
|
|
||||||
current_arg++;
|
|
||||||
// Remaining args are script args
|
|
||||||
while (current_arg < argc) {
|
|
||||||
config.script_args.push_back(argv[current_arg]);
|
|
||||||
current_arg++;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown flag
|
|
||||||
std::cerr << "Unknown option: " << arg << std::endl;
|
|
||||||
result.should_exit = true;
|
|
||||||
result.exit_code = 1;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CommandLineParser::print_help() {
|
|
||||||
std::cout << "usage: mcrogueface [option] ... [-c cmd | -m mod | file | -] [arg] ...\n"
|
|
||||||
<< "Options:\n"
|
|
||||||
<< " -c cmd : program passed in as string (terminates option list)\n"
|
|
||||||
<< " -h : print this help message and exit (also --help)\n"
|
|
||||||
<< " -i : inspect interactively after running script\n"
|
|
||||||
<< " -m mod : run library module as a script (terminates option list)\n"
|
|
||||||
<< " -V : print the Python version number and exit (also --version)\n"
|
|
||||||
<< "\n"
|
|
||||||
<< "McRogueFace specific options:\n"
|
|
||||||
<< " --exec file : execute script before main program (can be used multiple times)\n"
|
|
||||||
<< " --headless : run without creating a window (implies --audio-off)\n"
|
|
||||||
<< " --audio-off : disable audio\n"
|
|
||||||
<< " --audio-on : enable audio (even in headless mode)\n"
|
|
||||||
<< " --screenshot [path] : take a screenshot in headless mode\n"
|
|
||||||
<< "\n"
|
|
||||||
<< "Arguments:\n"
|
|
||||||
<< " file : program read from script file\n"
|
|
||||||
<< " - : program read from stdin\n"
|
|
||||||
<< " arg ...: arguments passed to program in sys.argv[1:]\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
void CommandLineParser::print_version() {
|
|
||||||
std::cout << "Python 3.12.0 (McRogueFace embedded)\n";
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
#ifndef COMMAND_LINE_PARSER_H
|
|
||||||
#define COMMAND_LINE_PARSER_H
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include "McRogueFaceConfig.h"
|
|
||||||
|
|
||||||
class CommandLineParser {
|
|
||||||
public:
|
|
||||||
struct ParseResult {
|
|
||||||
bool should_exit = false;
|
|
||||||
int exit_code = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
CommandLineParser(int argc, char* argv[]);
|
|
||||||
ParseResult parse(McRogueFaceConfig& config);
|
|
||||||
|
|
||||||
private:
|
|
||||||
int argc;
|
|
||||||
char** argv;
|
|
||||||
int current_arg = 1; // Skip program name
|
|
||||||
|
|
||||||
bool has_flag(const std::string& short_flag, const std::string& long_flag = "");
|
|
||||||
std::string get_next_arg(const std::string& flag_name);
|
|
||||||
void parse_positional_args(McRogueFaceConfig& config);
|
|
||||||
void print_help();
|
|
||||||
void print_version();
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // COMMAND_LINE_PARSER_H
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Common.h"
|
||||||
|
#include "IndexSprite.h"
|
||||||
|
#include "Grid.h"
|
||||||
|
//#include "Item.h"
|
||||||
|
#include "Python.h"
|
||||||
|
#include <list>
|
||||||
|
|
||||||
|
class CGrid
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool visible;
|
||||||
|
int x, y;
|
||||||
|
IndexSprite indexsprite;
|
||||||
|
Grid* grid;
|
||||||
|
CGrid(Grid* _g, int _ti, int _si, int _x, int _y, bool _v)
|
||||||
|
: visible(_v), x(_x), y(_y), grid(_g), indexsprite(_ti, _si, _x, _y, 1.0) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CInventory
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
//std::list<std::shared_ptr<Item>>;
|
||||||
|
int x;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CBehavior
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PyObject* object;
|
||||||
|
CBehavior(PyObject* p): object(p) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
class CCombatant
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
int hp;
|
||||||
|
int maxhp;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CCaster
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
int mp;
|
||||||
|
int maxmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CLevel
|
||||||
|
{
|
||||||
|
int constitution; // +HP, resist effects
|
||||||
|
int strength; // +damage, block/parry
|
||||||
|
int dexterity; // +speed, dodge
|
||||||
|
int intelligence; // +MP, spell resist
|
||||||
|
int wisdom; // +damage, deflect
|
||||||
|
int luck; // crit, loot
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
class CTransform
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Vec2 pos = { 0.0, 0.0 };
|
||||||
|
Vec2 velocity = { 0.0, 0.0 };
|
||||||
|
float angle = 0;
|
||||||
|
|
||||||
|
CTransform(const Vec2 & p, const Vec2 & v, float a)
|
||||||
|
: pos(p), velocity(v), angle(a) {}
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
class CShape
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
sf::CircleShape circle;
|
||||||
|
|
||||||
|
CShape(float radius, int points, const sf::Color & fill, const sf::Color & outline, float thickness)
|
||||||
|
: circle(radius, points)
|
||||||
|
{
|
||||||
|
circle.setFillColor(fill);
|
||||||
|
circle.setOutlineColor(outline);
|
||||||
|
circle.setOutlineThickness(thickness);
|
||||||
|
circle.setOrigin(radius, radius);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CCollision
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
float radius = 0;
|
||||||
|
CCollision(float r)
|
||||||
|
: radius(r) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CScore
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
int score = 0;
|
||||||
|
CScore(int s)
|
||||||
|
: score(s) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CLifespan
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
int remaining = 0;
|
||||||
|
int total = 0;
|
||||||
|
CLifespan(int t)
|
||||||
|
: remaining(t), total(t) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CInput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool up = false;
|
||||||
|
bool left = false;
|
||||||
|
bool right = false;
|
||||||
|
bool down = false;
|
||||||
|
bool fire = false;
|
||||||
|
|
||||||
|
CInput() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CSteer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
sf::Vector2f position;
|
||||||
|
sf::Vector2f velocity;
|
||||||
|
float v_max;
|
||||||
|
float dv_max;
|
||||||
|
float theta_max;
|
||||||
|
float dtheta_max;
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
#include "Entity.h"
|
||||||
|
|
||||||
|
Entity::Entity(const size_t i, const std::string & t)
|
||||||
|
: m_id(i), m_tag(t) {}
|
||||||
|
|
||||||
|
bool Entity::isActive() const
|
||||||
|
{
|
||||||
|
return m_active;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string & Entity::tag() const
|
||||||
|
{
|
||||||
|
return m_tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t Entity::id() const
|
||||||
|
{
|
||||||
|
return m_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Entity::destroy()
|
||||||
|
{
|
||||||
|
m_active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Common.h"
|
||||||
|
|
||||||
|
#include "Components.h"
|
||||||
|
|
||||||
|
class Entity
|
||||||
|
{
|
||||||
|
friend class EntityManager;
|
||||||
|
|
||||||
|
bool m_active = true;
|
||||||
|
size_t m_id = 0;
|
||||||
|
std::string m_tag = "default";
|
||||||
|
|
||||||
|
//constructor and destructor
|
||||||
|
Entity(const size_t id, const std::string & t);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// component pointers
|
||||||
|
//std::shared_ptr<CTransform> cTransform;
|
||||||
|
//std::shared_ptr<CShape> cShape;
|
||||||
|
//std::shared_ptr<CCollision> cCollision;
|
||||||
|
//std::shared_ptr<CInput> cInput;
|
||||||
|
//std::shared_ptr<CScore> cScore;
|
||||||
|
//std::shared_ptr<CLifespan> cLifespan;
|
||||||
|
std::shared_ptr<CGrid> cGrid;
|
||||||
|
std::shared_ptr<CInventory> cInventory;
|
||||||
|
std::shared_ptr<CBehavior> cBehavior;
|
||||||
|
|
||||||
|
//private member access functions
|
||||||
|
bool isActive() const;
|
||||||
|
const std::string & tag() const;
|
||||||
|
const size_t id() const;
|
||||||
|
void destroy();
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
#include "EntityManager.h"
|
||||||
|
|
||||||
|
EntityManager::EntityManager()
|
||||||
|
:m_totalEntities(0) {}
|
||||||
|
|
||||||
|
void EntityManager::update()
|
||||||
|
{
|
||||||
|
//TODO: add entities from m_entitiesToAdd to all vector / tag map
|
||||||
|
removeDeadEntities(m_entities);
|
||||||
|
|
||||||
|
// C++17 way of iterating!
|
||||||
|
for (auto& [tag, entityVec] : m_entityMap)
|
||||||
|
{
|
||||||
|
removeDeadEntities(entityVec);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& e : m_entitiesToAdd)
|
||||||
|
{
|
||||||
|
m_entities.push_back(e);
|
||||||
|
m_entityMap[e->tag()].push_back(e);
|
||||||
|
}
|
||||||
|
//if (m_entitiesToAdd.size())
|
||||||
|
// m_entitiesToAdd.erase(m_entitiesToAdd.begin(), m_entitiesToAdd.end());
|
||||||
|
m_entitiesToAdd = EntityVec();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void EntityManager::removeDeadEntities(EntityVec & vec)
|
||||||
|
{
|
||||||
|
EntityVec survivors; // New vector
|
||||||
|
for (auto& e : m_entities){
|
||||||
|
if (e->isActive()) survivors.push_back(e); // populate new vector
|
||||||
|
else if (e->cGrid) { // erase vector from grid
|
||||||
|
for( auto it = e->cGrid->grid->entities.begin(); it != e->cGrid->grid->entities.end(); it++){
|
||||||
|
if( *it == e ){
|
||||||
|
e->cGrid->grid->entities.erase( it );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//std::cout << "All entities: " << m_entities.size() << " Survivors: " << survivors.size() << std::endl;
|
||||||
|
m_entities = survivors; // point to new vector
|
||||||
|
for (auto& [tag, entityVec] : m_entityMap)
|
||||||
|
{
|
||||||
|
EntityVec tag_survivors; // New vector
|
||||||
|
for (auto& e : m_entityMap[tag])
|
||||||
|
{
|
||||||
|
if (e->isActive()) tag_survivors.push_back(e); // populate new vector
|
||||||
|
}
|
||||||
|
m_entityMap[tag] = tag_survivors; // point to new vector
|
||||||
|
//std::cout << tag << " entities: " << m_entityMap[tag].size() << " Survivors: " << tag_survivors.size() << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<Entity> EntityManager::addEntity(const std::string & tag)
|
||||||
|
{
|
||||||
|
// create the entity shared pointer
|
||||||
|
auto entity = std::shared_ptr<Entity>(new Entity(m_totalEntities++, tag));
|
||||||
|
m_entitiesToAdd.push_back(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntityVec & EntityManager::getEntities()
|
||||||
|
{
|
||||||
|
return m_entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntityVec & EntityManager::getEntities(const std::string & tag)
|
||||||
|
{
|
||||||
|
return m_entityMap[tag];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Common.h"
|
||||||
|
#include "Entity.h"
|
||||||
|
|
||||||
|
typedef std::vector<std::shared_ptr<Entity>> EntityVec;
|
||||||
|
typedef std::map<std::string, EntityVec> EntityMap;
|
||||||
|
|
||||||
|
class EntityManager
|
||||||
|
{
|
||||||
|
EntityVec m_entities;
|
||||||
|
EntityVec m_entitiesToAdd;
|
||||||
|
EntityMap m_entityMap;
|
||||||
|
size_t m_totalEntities;
|
||||||
|
|
||||||
|
void removeDeadEntities(EntityVec & vec);
|
||||||
|
|
||||||
|
public:
|
||||||
|
EntityManager();
|
||||||
|
void update();
|
||||||
|
std::shared_ptr<Entity> addEntity(const std::string & tag);
|
||||||
|
const EntityVec & getEntities();
|
||||||
|
const EntityVec & getEntities(const std::string & tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -1,408 +1,123 @@
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
|
#include "MenuScene.h"
|
||||||
|
//#include "UITestScene.h"
|
||||||
#include "ActionCode.h"
|
#include "ActionCode.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PyScene.h"
|
#include "PythonScene.h"
|
||||||
#include "UITestScene.h"
|
#include "UITestScene.h"
|
||||||
#include "Resources.h"
|
#include "Resources.h"
|
||||||
#include "Animation.h"
|
|
||||||
#include "Timer.h"
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
|
GameEngine::GameEngine()
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
|
||||||
: config(cfg), headless(cfg.headless)
|
|
||||||
{
|
{
|
||||||
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
|
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
|
||||||
Resources::game = this;
|
Resources::game = this;
|
||||||
window_title = "McRogueFace Engine";
|
window_title = "McRogueFace - r/RoguelikeDev Tutorial Run";
|
||||||
|
window.create(sf::VideoMode(1024, 768), window_title);
|
||||||
// Initialize rendering based on headless mode
|
visible = window.getDefaultView();
|
||||||
if (headless) {
|
window.setFramerateLimit(30);
|
||||||
headless_renderer = std::make_unique<HeadlessRenderer>();
|
|
||||||
if (!headless_renderer->init(1024, 768)) {
|
|
||||||
throw std::runtime_error("Failed to initialize headless renderer");
|
|
||||||
}
|
|
||||||
render_target = &headless_renderer->getRenderTarget();
|
|
||||||
} else {
|
|
||||||
window = std::make_unique<sf::RenderWindow>();
|
|
||||||
window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close | sf::Style::Resize);
|
|
||||||
window->setFramerateLimit(60);
|
|
||||||
render_target = window.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
visible = render_target->getDefaultView();
|
|
||||||
|
|
||||||
// Initialize the game view
|
|
||||||
gameView.setSize(static_cast<float>(gameResolution.x), static_cast<float>(gameResolution.y));
|
|
||||||
// Use integer center coordinates for pixel-perfect rendering
|
|
||||||
gameView.setCenter(std::floor(gameResolution.x / 2.0f), std::floor(gameResolution.y / 2.0f));
|
|
||||||
updateViewport();
|
|
||||||
scene = "uitest";
|
scene = "uitest";
|
||||||
|
//std::cout << "Constructing MenuScene" << std::endl;
|
||||||
|
scenes["menu"] = new MenuScene(this);
|
||||||
scenes["uitest"] = new UITestScene(this);
|
scenes["uitest"] = new UITestScene(this);
|
||||||
|
|
||||||
|
//std::cout << "Constructed MenuScene" <<std::endl;
|
||||||
|
//scenes["play"] = new UITestScene(this);
|
||||||
|
//api = new McRFPy_API(this);
|
||||||
|
|
||||||
McRFPy_API::game = this;
|
McRFPy_API::game = this;
|
||||||
|
McRFPy_API::api_init();
|
||||||
|
McRFPy_API::executePyString("import mcrfpy");
|
||||||
|
//McRFPy_API::executePyString("from UIMenu import *");
|
||||||
|
//McRFPy_API::executePyString("from Grid import *");
|
||||||
|
|
||||||
// Initialize profiler overlay
|
//scenes["py"] = new PythonScene(this, "TestScene");
|
||||||
profilerOverlay = new ProfilerOverlay(Resources::font);
|
|
||||||
|
|
||||||
// Only load game.py if no custom script/command/module/exec is specified
|
IndexSprite::game = this;
|
||||||
bool should_load_game = config.script_path.empty() &&
|
|
||||||
config.python_command.empty() &&
|
|
||||||
config.python_module.empty() &&
|
|
||||||
config.exec_scripts.empty() &&
|
|
||||||
!config.interactive_mode &&
|
|
||||||
!config.python_mode;
|
|
||||||
|
|
||||||
if (should_load_game) {
|
|
||||||
if (!Py_IsInitialized()) {
|
|
||||||
McRFPy_API::api_init();
|
|
||||||
}
|
|
||||||
McRFPy_API::executePyString("import mcrfpy");
|
|
||||||
McRFPy_API::executeScript("scripts/game.py");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute any --exec scripts in order
|
|
||||||
if (!config.exec_scripts.empty()) {
|
|
||||||
if (!Py_IsInitialized()) {
|
|
||||||
McRFPy_API::api_init();
|
|
||||||
}
|
|
||||||
McRFPy_API::executePyString("import mcrfpy");
|
|
||||||
|
|
||||||
for (const auto& exec_script : config.exec_scripts) {
|
|
||||||
std::cout << "Executing script: " << exec_script << std::endl;
|
|
||||||
McRFPy_API::executeScript(exec_script.string());
|
|
||||||
}
|
|
||||||
std::cout << "All --exec scripts completed" << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
clock.restart();
|
clock.restart();
|
||||||
runtime.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
GameEngine::~GameEngine()
|
|
||||||
{
|
|
||||||
cleanup();
|
|
||||||
for (auto& [name, scene] : scenes) {
|
|
||||||
delete scene;
|
|
||||||
}
|
|
||||||
delete profilerOverlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::cleanup()
|
|
||||||
{
|
|
||||||
if (cleaned_up) return;
|
|
||||||
cleaned_up = true;
|
|
||||||
|
|
||||||
// Clear all animations first (RAII handles invalidation)
|
|
||||||
AnimationManager::getInstance().clear();
|
|
||||||
|
|
||||||
// Clear Python references before destroying C++ objects
|
|
||||||
// Clear all timers (they hold Python callables)
|
|
||||||
timers.clear();
|
|
||||||
|
|
||||||
// Clear McRFPy_API's reference to this game engine
|
|
||||||
if (McRFPy_API::game == this) {
|
|
||||||
McRFPy_API::game = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force close the window if it's still open
|
|
||||||
if (window && window->isOpen()) {
|
|
||||||
window->close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scene* GameEngine::currentScene() { return scenes[scene]; }
|
Scene* GameEngine::currentScene() { return scenes[scene]; }
|
||||||
void GameEngine::changeScene(std::string s)
|
void GameEngine::changeScene(std::string s) { std::cout << "Current scene is now '" << s << "'\n"; scene = s; }
|
||||||
{
|
|
||||||
changeScene(s, TransitionType::None, 0.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::changeScene(std::string sceneName, TransitionType transitionType, float duration)
|
|
||||||
{
|
|
||||||
if (scenes.find(sceneName) == scenes.end())
|
|
||||||
{
|
|
||||||
std::cout << "Attempted to change to a scene that doesn't exist (`" << sceneName << "`)" << std::endl;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transitionType == TransitionType::None || duration <= 0.0f)
|
|
||||||
{
|
|
||||||
// Immediate scene change
|
|
||||||
std::string old_scene = scene;
|
|
||||||
scene = sceneName;
|
|
||||||
|
|
||||||
// Trigger Python scene lifecycle events
|
|
||||||
McRFPy_API::triggerSceneChange(old_scene, sceneName);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Start transition
|
|
||||||
transition.start(transitionType, scene, sceneName, duration);
|
|
||||||
|
|
||||||
// Render current scene to texture
|
|
||||||
sf::RenderTarget* original_target = render_target;
|
|
||||||
render_target = transition.oldSceneTexture.get();
|
|
||||||
transition.oldSceneTexture->clear();
|
|
||||||
currentScene()->render();
|
|
||||||
transition.oldSceneTexture->display();
|
|
||||||
|
|
||||||
// Change to new scene
|
|
||||||
std::string old_scene = scene;
|
|
||||||
scene = sceneName;
|
|
||||||
|
|
||||||
// Render new scene to texture
|
|
||||||
render_target = transition.newSceneTexture.get();
|
|
||||||
transition.newSceneTexture->clear();
|
|
||||||
currentScene()->render();
|
|
||||||
transition.newSceneTexture->display();
|
|
||||||
|
|
||||||
// Restore original render target and scene
|
|
||||||
render_target = original_target;
|
|
||||||
scene = old_scene;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void GameEngine::quit() { running = false; }
|
void GameEngine::quit() { running = false; }
|
||||||
void GameEngine::setPause(bool p) { paused = p; }
|
void GameEngine::setPause(bool p) { paused = p; }
|
||||||
sf::Font & GameEngine::getFont() { /*return font; */ return Resources::font; }
|
sf::Font & GameEngine::getFont() { /*return font; */ return Resources::font; }
|
||||||
sf::RenderWindow & GameEngine::getWindow() {
|
sf::RenderWindow & GameEngine::getWindow() { return window; }
|
||||||
if (!window) {
|
|
||||||
throw std::runtime_error("Window not available in headless mode");
|
|
||||||
}
|
|
||||||
return *window;
|
|
||||||
}
|
|
||||||
|
|
||||||
sf::RenderTarget & GameEngine::getRenderTarget() {
|
|
||||||
return *render_target;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::createScene(std::string s) { scenes[s] = new PyScene(this); }
|
|
||||||
|
|
||||||
void GameEngine::setWindowScale(float multiplier)
|
|
||||||
{
|
|
||||||
if (!headless && window) {
|
|
||||||
window->setSize(sf::Vector2u(gameResolution.x * multiplier, gameResolution.y * multiplier));
|
|
||||||
updateViewport();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::run()
|
void GameEngine::run()
|
||||||
{
|
{
|
||||||
//std::cout << "GameEngine::run() starting main loop..." << std::endl;
|
|
||||||
float fps = 0.0;
|
float fps = 0.0;
|
||||||
frameTime = 0.016f; // Initialize to ~60 FPS
|
|
||||||
clock.restart();
|
clock.restart();
|
||||||
while (running)
|
while (running)
|
||||||
{
|
{
|
||||||
// Reset per-frame metrics
|
|
||||||
metrics.resetPerFrame();
|
|
||||||
|
|
||||||
currentScene()->update();
|
currentScene()->update();
|
||||||
testTimers();
|
sUserInput();
|
||||||
|
|
||||||
// Update Python scenes
|
|
||||||
{
|
|
||||||
ScopedTimer pyTimer(metrics.pythonScriptTime);
|
|
||||||
McRFPy_API::updatePythonScenes(frameTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update animations (only if frameTime is valid)
|
|
||||||
if (frameTime > 0.0f && frameTime < 1.0f) {
|
|
||||||
ScopedTimer animTimer(metrics.animationTime);
|
|
||||||
AnimationManager::getInstance().update(frameTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!headless) {
|
|
||||||
sUserInput();
|
|
||||||
}
|
|
||||||
if (!paused)
|
if (!paused)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
currentScene()->sRender();
|
||||||
// Handle scene transitions
|
|
||||||
if (transition.type != TransitionType::None)
|
|
||||||
{
|
|
||||||
transition.update(frameTime);
|
|
||||||
|
|
||||||
if (transition.isComplete())
|
|
||||||
{
|
|
||||||
// Transition complete - finalize scene change
|
|
||||||
scene = transition.toScene;
|
|
||||||
transition.type = TransitionType::None;
|
|
||||||
|
|
||||||
// Trigger Python scene lifecycle events
|
|
||||||
McRFPy_API::triggerSceneChange(transition.fromScene, transition.toScene);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Render transition
|
|
||||||
render_target->clear();
|
|
||||||
transition.render(*render_target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Normal scene rendering
|
|
||||||
currentScene()->render();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update and render profiler overlay (if enabled)
|
|
||||||
if (profilerOverlay && !headless) {
|
|
||||||
profilerOverlay->update(metrics);
|
|
||||||
profilerOverlay->render(*render_target);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display the frame
|
|
||||||
if (headless) {
|
|
||||||
headless_renderer->display();
|
|
||||||
// Take screenshot if requested
|
|
||||||
if (config.take_screenshot) {
|
|
||||||
headless_renderer->saveScreenshot(config.screenshot_path.empty() ? "screenshot.png" : config.screenshot_path);
|
|
||||||
config.take_screenshot = false; // Only take one screenshot
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window->display();
|
|
||||||
}
|
|
||||||
|
|
||||||
currentFrame++;
|
currentFrame++;
|
||||||
frameTime = clock.restart().asSeconds();
|
frameTime = clock.restart().asSeconds();
|
||||||
fps = 1 / frameTime;
|
fps = 1 / frameTime;
|
||||||
|
window.setTitle(window_title + " " + std::to_string(fps) + " FPS");
|
||||||
// Update profiling metrics
|
|
||||||
metrics.updateFrameTime(frameTime * 1000.0f); // Convert to milliseconds
|
|
||||||
|
|
||||||
int whole_fps = metrics.fps;
|
|
||||||
int tenth_fps = (metrics.fps * 10) % 10;
|
|
||||||
|
|
||||||
if (!headless && window) {
|
|
||||||
window->setTitle(window_title);
|
|
||||||
}
|
|
||||||
|
|
||||||
// In windowed mode, check if window was closed
|
|
||||||
if (!headless && window && !window->isOpen()) {
|
|
||||||
running = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In headless exec mode, auto-exit when no timers remain
|
|
||||||
if (config.auto_exit_after_exec && timers.empty()) {
|
|
||||||
running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up before exiting the run loop
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<Timer> GameEngine::getTimer(const std::string& name)
|
|
||||||
{
|
|
||||||
auto it = timers.find(name);
|
|
||||||
if (it != timers.end()) {
|
|
||||||
return it->second;
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
|
|
||||||
{
|
|
||||||
auto it = timers.find(name);
|
|
||||||
if (it != timers.end()) // overwrite existing
|
|
||||||
{
|
|
||||||
if (target == NULL || target == Py_None)
|
|
||||||
{
|
|
||||||
// Delete: Overwrite existing timer with one that calls None. This will be deleted in the next timer check
|
|
||||||
// see gitea issue #4: this allows for a timer to be deleted during its own call to itself
|
|
||||||
timers[name] = std::make_shared<Timer>(Py_None, 1000, runtime.getElapsedTime().asMilliseconds());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (target == NULL || target == Py_None)
|
|
||||||
{
|
|
||||||
std::cout << "Refusing to initialize timer to None. It's not an error, it's just pointless." << std::endl;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
timers[name] = std::make_shared<Timer>(target, interval, runtime.getElapsedTime().asMilliseconds());
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::testTimers()
|
|
||||||
{
|
|
||||||
int now = runtime.getElapsedTime().asMilliseconds();
|
|
||||||
auto it = timers.begin();
|
|
||||||
while (it != timers.end())
|
|
||||||
{
|
|
||||||
it->second->test(now);
|
|
||||||
|
|
||||||
// Remove timers that have been cancelled or are one-shot and fired
|
|
||||||
if (!it->second->getCallback() || it->second->getCallback() == Py_None)
|
|
||||||
{
|
|
||||||
it = timers.erase(it);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
it++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::processEvent(const sf::Event& event)
|
|
||||||
{
|
|
||||||
std::string actionType;
|
|
||||||
int actionCode = 0;
|
|
||||||
|
|
||||||
if (event.type == sf::Event::Closed) { running = false; return; }
|
|
||||||
|
|
||||||
// Handle F3 for profiler overlay toggle
|
|
||||||
if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::F3) {
|
|
||||||
if (profilerOverlay) {
|
|
||||||
profilerOverlay->toggle();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Handle window resize events
|
|
||||||
else if (event.type == sf::Event::Resized) {
|
|
||||||
// Update the viewport to handle the new window size
|
|
||||||
updateViewport();
|
|
||||||
|
|
||||||
// Notify Python scenes about the resize
|
|
||||||
McRFPy_API::triggerResize(event.size.width, event.size.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start";
|
|
||||||
else if (event.type == sf::Event::KeyReleased || event.type == sf::Event::MouseButtonReleased) actionType = "end";
|
|
||||||
|
|
||||||
if (event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseButtonReleased)
|
|
||||||
actionCode = ActionCode::keycode(event.mouseButton.button);
|
|
||||||
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)
|
|
||||||
actionCode = ActionCode::keycode(event.key.code);
|
|
||||||
else if (event.type == sf::Event::MouseWheelScrolled)
|
|
||||||
{
|
|
||||||
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
|
|
||||||
{
|
|
||||||
int delta = 1;
|
|
||||||
if (event.mouseWheelScroll.delta < 0) delta = -1;
|
|
||||||
actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (currentScene()->hasAction(actionCode))
|
|
||||||
{
|
|
||||||
std::string name = currentScene()->action(actionCode);
|
|
||||||
currentScene()->doAction(name, actionType);
|
|
||||||
}
|
|
||||||
else if (currentScene()->key_callable &&
|
|
||||||
(event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased))
|
|
||||||
{
|
|
||||||
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameEngine::sUserInput()
|
void GameEngine::sUserInput()
|
||||||
{
|
{
|
||||||
sf::Event event;
|
sf::Event event;
|
||||||
while (window && window->pollEvent(event))
|
while (window.pollEvent(event))
|
||||||
{
|
{
|
||||||
processEvent(event);
|
std::string actionType;
|
||||||
|
int actionCode = 0;
|
||||||
|
|
||||||
|
if (event.type == sf::Event::Closed) { running = false; continue; }
|
||||||
|
else if (event.type == sf::Event::Resized) {
|
||||||
|
sf::FloatRect area(0.f, 0.f, event.size.width, event.size.height);
|
||||||
|
visible = sf::View(area);
|
||||||
|
window.setView(visible);
|
||||||
|
//std::cout << "Visible area set to (0, 0, " << event.size.width << ", " << event.size.height <<")"<<std::endl;
|
||||||
|
actionType = "resize";
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start";
|
||||||
|
else if (event.type == sf::Event::KeyReleased || event.type == sf::Event::MouseButtonReleased) actionType = "end";
|
||||||
|
|
||||||
|
if (event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseButtonReleased)
|
||||||
|
actionCode = ActionCode::keycode(event.mouseButton.button);
|
||||||
|
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)
|
||||||
|
actionCode = ActionCode::keycode(event.key.code);
|
||||||
|
else if (event.type == sf::Event::MouseWheelScrolled)
|
||||||
|
{
|
||||||
|
// //sf::Mouse::Wheel w = event.MouseWheelScrollEvent.wheel;
|
||||||
|
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
|
||||||
|
{
|
||||||
|
int delta = 1;
|
||||||
|
if (event.mouseWheelScroll.delta < 0) delta = -1;
|
||||||
|
actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta );
|
||||||
|
/*
|
||||||
|
std::cout << "[GameEngine] Generated MouseWheel code w(" << (int)event.mouseWheelScroll.wheel << ") d(" << event.mouseWheelScroll.delta << ") D(" << delta << ") = " << actionCode << std::endl;
|
||||||
|
std::cout << " test decode: isMouseWheel=" << ActionCode::isMouseWheel(actionCode) << ", wheel=" << ActionCode::wheel(actionCode) << ", delta=" << ActionCode::delta(actionCode) << std::endl;
|
||||||
|
std::cout << " math test: actionCode && WHEEL_NEG -> " << (actionCode && ActionCode::WHEEL_NEG) << "; actionCode && WHEEL_DEL -> " << (actionCode && ActionCode::WHEEL_DEL) << ";" << std::endl;
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
// float d = event.MouseWheelScrollEvent.delta;
|
||||||
|
// actionCode = ActionCode::keycode(0, d);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
|
||||||
|
//std::cout << "Event produced action code " << actionCode << ": " << actionType << std::endl;
|
||||||
|
|
||||||
|
if (currentScene()->hasAction(actionCode))
|
||||||
|
{
|
||||||
|
std::string name = currentScene()->action(actionCode);
|
||||||
|
currentScene()->doAction(name, actionType);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//std::cout << "[GameEngine] Action not registered for input: " << actionCode << ": " << actionType << std::endl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -423,123 +138,3 @@ std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> GameEngine::scene_ui(s
|
||||||
if (scenes.count(target) == 0) return NULL;
|
if (scenes.count(target) == 0) return NULL;
|
||||||
return scenes[target]->ui_elements;
|
return scenes[target]->ui_elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameEngine::setWindowTitle(const std::string& title)
|
|
||||||
{
|
|
||||||
window_title = title;
|
|
||||||
if (!headless && window) {
|
|
||||||
window->setTitle(title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::setVSync(bool enabled)
|
|
||||||
{
|
|
||||||
vsync_enabled = enabled;
|
|
||||||
if (!headless && window) {
|
|
||||||
window->setVerticalSyncEnabled(enabled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::setFramerateLimit(unsigned int limit)
|
|
||||||
{
|
|
||||||
framerate_limit = limit;
|
|
||||||
if (!headless && window) {
|
|
||||||
window->setFramerateLimit(limit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::setGameResolution(unsigned int width, unsigned int height) {
|
|
||||||
gameResolution = sf::Vector2u(width, height);
|
|
||||||
gameView.setSize(static_cast<float>(width), static_cast<float>(height));
|
|
||||||
// Use integer center coordinates for pixel-perfect rendering
|
|
||||||
gameView.setCenter(std::floor(width / 2.0f), std::floor(height / 2.0f));
|
|
||||||
updateViewport();
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::setViewportMode(ViewportMode mode) {
|
|
||||||
viewportMode = mode;
|
|
||||||
updateViewport();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string GameEngine::getViewportModeString() const {
|
|
||||||
switch (viewportMode) {
|
|
||||||
case ViewportMode::Center: return "center";
|
|
||||||
case ViewportMode::Stretch: return "stretch";
|
|
||||||
case ViewportMode::Fit: return "fit";
|
|
||||||
}
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
void GameEngine::updateViewport() {
|
|
||||||
if (!render_target) return;
|
|
||||||
|
|
||||||
auto windowSize = render_target->getSize();
|
|
||||||
|
|
||||||
switch (viewportMode) {
|
|
||||||
case ViewportMode::Center: {
|
|
||||||
// 1:1 pixels, centered in window
|
|
||||||
float viewportWidth = std::min(static_cast<float>(gameResolution.x), static_cast<float>(windowSize.x));
|
|
||||||
float viewportHeight = std::min(static_cast<float>(gameResolution.y), static_cast<float>(windowSize.y));
|
|
||||||
|
|
||||||
// Floor offsets to ensure integer pixel alignment
|
|
||||||
float offsetX = std::floor((windowSize.x - viewportWidth) / 2.0f);
|
|
||||||
float offsetY = std::floor((windowSize.y - viewportHeight) / 2.0f);
|
|
||||||
|
|
||||||
gameView.setViewport(sf::FloatRect(
|
|
||||||
offsetX / windowSize.x,
|
|
||||||
offsetY / windowSize.y,
|
|
||||||
viewportWidth / windowSize.x,
|
|
||||||
viewportHeight / windowSize.y
|
|
||||||
));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case ViewportMode::Stretch: {
|
|
||||||
// Fill entire window, ignore aspect ratio
|
|
||||||
gameView.setViewport(sf::FloatRect(0, 0, 1, 1));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case ViewportMode::Fit: {
|
|
||||||
// Maintain aspect ratio with black bars
|
|
||||||
float windowAspect = static_cast<float>(windowSize.x) / windowSize.y;
|
|
||||||
float gameAspect = static_cast<float>(gameResolution.x) / gameResolution.y;
|
|
||||||
|
|
||||||
float viewportWidth, viewportHeight;
|
|
||||||
float offsetX = 0, offsetY = 0;
|
|
||||||
|
|
||||||
if (windowAspect > gameAspect) {
|
|
||||||
// Window is wider - black bars on sides
|
|
||||||
// Calculate viewport size in pixels and floor for pixel-perfect scaling
|
|
||||||
float pixelHeight = static_cast<float>(windowSize.y);
|
|
||||||
float pixelWidth = std::floor(pixelHeight * gameAspect);
|
|
||||||
|
|
||||||
viewportHeight = 1.0f;
|
|
||||||
viewportWidth = pixelWidth / windowSize.x;
|
|
||||||
offsetX = (1.0f - viewportWidth) / 2.0f;
|
|
||||||
} else {
|
|
||||||
// Window is taller - black bars on top/bottom
|
|
||||||
// Calculate viewport size in pixels and floor for pixel-perfect scaling
|
|
||||||
float pixelWidth = static_cast<float>(windowSize.x);
|
|
||||||
float pixelHeight = std::floor(pixelWidth / gameAspect);
|
|
||||||
|
|
||||||
viewportWidth = 1.0f;
|
|
||||||
viewportHeight = pixelHeight / windowSize.y;
|
|
||||||
offsetY = (1.0f - viewportHeight) / 2.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
gameView.setViewport(sf::FloatRect(offsetX, offsetY, viewportWidth, viewportHeight));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the view
|
|
||||||
render_target->setView(gameView);
|
|
||||||
}
|
|
||||||
|
|
||||||
sf::Vector2f GameEngine::windowToGameCoords(const sf::Vector2f& windowPos) const {
|
|
||||||
if (!render_target) return windowPos;
|
|
||||||
|
|
||||||
// Convert window coordinates to game coordinates using the view
|
|
||||||
return render_target->mapPixelToCoords(sf::Vector2i(windowPos), gameView);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
169
src/GameEngine.h
169
src/GameEngine.h
|
|
@ -1,37 +1,17 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
|
#include "Entity.h"
|
||||||
|
#include "EntityManager.h"
|
||||||
#include "Scene.h"
|
#include "Scene.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "IndexTexture.h"
|
#include "IndexTexture.h"
|
||||||
#include "Timer.h"
|
|
||||||
#include "PyCallable.h"
|
|
||||||
#include "McRogueFaceConfig.h"
|
|
||||||
#include "HeadlessRenderer.h"
|
|
||||||
#include "SceneTransition.h"
|
|
||||||
#include "Profiler.h"
|
|
||||||
#include <memory>
|
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
class GameEngine
|
class GameEngine
|
||||||
{
|
{
|
||||||
public:
|
sf::RenderWindow window;
|
||||||
// Forward declare nested class so private section can use it
|
|
||||||
class ProfilerOverlay;
|
|
||||||
|
|
||||||
// Viewport modes (moved here so private section can use it)
|
|
||||||
enum class ViewportMode {
|
|
||||||
Center, // 1:1 pixels, viewport centered in window
|
|
||||||
Stretch, // viewport size = window size, doesn't respect aspect ratio
|
|
||||||
Fit // maintains original aspect ratio, leaves black bars
|
|
||||||
};
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::unique_ptr<sf::RenderWindow> window;
|
|
||||||
std::unique_ptr<HeadlessRenderer> headless_renderer;
|
|
||||||
sf::RenderTarget* render_target;
|
|
||||||
|
|
||||||
sf::Font font;
|
sf::Font font;
|
||||||
|
std::string scene;
|
||||||
std::map<std::string, Scene*> scenes;
|
std::map<std::string, Scene*> scenes;
|
||||||
bool running = true;
|
bool running = true;
|
||||||
bool paused = false;
|
bool paused = false;
|
||||||
|
|
@ -41,135 +21,19 @@ private:
|
||||||
float frameTime;
|
float frameTime;
|
||||||
std::string window_title;
|
std::string window_title;
|
||||||
|
|
||||||
bool headless = false;
|
|
||||||
McRogueFaceConfig config;
|
|
||||||
bool cleaned_up = false;
|
|
||||||
|
|
||||||
// Window state tracking
|
|
||||||
bool vsync_enabled = false;
|
|
||||||
unsigned int framerate_limit = 60;
|
|
||||||
|
|
||||||
// Scene transition state
|
|
||||||
SceneTransition transition;
|
|
||||||
|
|
||||||
// Viewport system
|
|
||||||
sf::Vector2u gameResolution{1024, 768}; // Fixed game resolution
|
|
||||||
sf::View gameView; // View for the game content
|
|
||||||
ViewportMode viewportMode = ViewportMode::Fit;
|
|
||||||
|
|
||||||
// Profiling overlay
|
|
||||||
bool showProfilerOverlay = false; // F3 key toggles this
|
|
||||||
int overlayUpdateCounter = 0; // Only update overlay every N frames
|
|
||||||
ProfilerOverlay* profilerOverlay = nullptr; // The actual overlay renderer
|
|
||||||
|
|
||||||
void updateViewport();
|
|
||||||
|
|
||||||
void testTimers();
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
sf::Clock runtime;
|
|
||||||
std::map<std::string, std::shared_ptr<Timer>> timers;
|
|
||||||
std::string scene;
|
|
||||||
|
|
||||||
// Profiling metrics
|
|
||||||
struct ProfilingMetrics {
|
|
||||||
float frameTime = 0.0f; // Current frame time in milliseconds
|
|
||||||
float avgFrameTime = 0.0f; // Average frame time over last N frames
|
|
||||||
int fps = 0; // Frames per second
|
|
||||||
int drawCalls = 0; // Draw calls per frame
|
|
||||||
int uiElements = 0; // Number of UI elements rendered
|
|
||||||
int visibleElements = 0; // Number of visible elements
|
|
||||||
|
|
||||||
// Detailed timing breakdowns (added for profiling system)
|
|
||||||
float gridRenderTime = 0.0f; // Time spent rendering grids (ms)
|
|
||||||
float entityRenderTime = 0.0f; // Time spent rendering entities (ms)
|
|
||||||
float fovOverlayTime = 0.0f; // Time spent rendering FOV overlays (ms)
|
|
||||||
float pythonScriptTime = 0.0f; // Time spent in Python callbacks (ms)
|
|
||||||
float animationTime = 0.0f; // Time spent updating animations (ms)
|
|
||||||
|
|
||||||
// Grid-specific metrics
|
|
||||||
int gridCellsRendered = 0; // Number of grid cells drawn this frame
|
|
||||||
int entitiesRendered = 0; // Number of entities drawn this frame
|
|
||||||
int totalEntities = 0; // Total entities in scene
|
|
||||||
|
|
||||||
// Frame time history for averaging
|
|
||||||
static constexpr int HISTORY_SIZE = 60;
|
|
||||||
float frameTimeHistory[HISTORY_SIZE] = {0};
|
|
||||||
int historyIndex = 0;
|
|
||||||
|
|
||||||
void updateFrameTime(float deltaMs) {
|
|
||||||
frameTime = deltaMs;
|
|
||||||
frameTimeHistory[historyIndex] = deltaMs;
|
|
||||||
historyIndex = (historyIndex + 1) % HISTORY_SIZE;
|
|
||||||
|
|
||||||
// Calculate average
|
|
||||||
float sum = 0.0f;
|
|
||||||
for (int i = 0; i < HISTORY_SIZE; ++i) {
|
|
||||||
sum += frameTimeHistory[i];
|
|
||||||
}
|
|
||||||
avgFrameTime = sum / HISTORY_SIZE;
|
|
||||||
fps = avgFrameTime > 0 ? static_cast<int>(1000.0f / avgFrameTime) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void resetPerFrame() {
|
|
||||||
drawCalls = 0;
|
|
||||||
uiElements = 0;
|
|
||||||
visibleElements = 0;
|
|
||||||
|
|
||||||
// Reset per-frame timing metrics
|
|
||||||
gridRenderTime = 0.0f;
|
|
||||||
entityRenderTime = 0.0f;
|
|
||||||
fovOverlayTime = 0.0f;
|
|
||||||
pythonScriptTime = 0.0f;
|
|
||||||
animationTime = 0.0f;
|
|
||||||
|
|
||||||
// Reset per-frame counters
|
|
||||||
gridCellsRendered = 0;
|
|
||||||
entitiesRendered = 0;
|
|
||||||
totalEntities = 0;
|
|
||||||
}
|
|
||||||
} metrics;
|
|
||||||
|
|
||||||
GameEngine();
|
GameEngine();
|
||||||
GameEngine(const McRogueFaceConfig& cfg);
|
|
||||||
~GameEngine();
|
|
||||||
Scene* currentScene();
|
Scene* currentScene();
|
||||||
void changeScene(std::string);
|
void changeScene(std::string);
|
||||||
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
|
|
||||||
void createScene(std::string);
|
|
||||||
void quit();
|
void quit();
|
||||||
void setPause(bool);
|
void setPause(bool);
|
||||||
sf::Font & getFont();
|
sf::Font & getFont();
|
||||||
sf::RenderWindow & getWindow();
|
sf::RenderWindow & getWindow();
|
||||||
sf::RenderTarget & getRenderTarget();
|
|
||||||
sf::RenderTarget* getRenderTargetPtr() { return render_target; }
|
|
||||||
void run();
|
void run();
|
||||||
void sUserInput();
|
void sUserInput();
|
||||||
void cleanup(); // Clean up Python references before destruction
|
|
||||||
int getFrame() { return currentFrame; }
|
int getFrame() { return currentFrame; }
|
||||||
float getFrameTime() { return frameTime; }
|
float getFrameTime() { return frameTime; }
|
||||||
sf::View getView() { return visible; }
|
sf::View getView() { return visible; }
|
||||||
void manageTimer(std::string, PyObject*, int);
|
|
||||||
std::shared_ptr<Timer> getTimer(const std::string& name);
|
|
||||||
void setWindowScale(float);
|
|
||||||
bool isHeadless() const { return headless; }
|
|
||||||
void processEvent(const sf::Event& event);
|
|
||||||
|
|
||||||
// Window property accessors
|
|
||||||
const std::string& getWindowTitle() const { return window_title; }
|
|
||||||
void setWindowTitle(const std::string& title);
|
|
||||||
bool getVSync() const { return vsync_enabled; }
|
|
||||||
void setVSync(bool enabled);
|
|
||||||
unsigned int getFramerateLimit() const { return framerate_limit; }
|
|
||||||
void setFramerateLimit(unsigned int limit);
|
|
||||||
|
|
||||||
// Viewport system
|
|
||||||
void setGameResolution(unsigned int width, unsigned int height);
|
|
||||||
sf::Vector2u getGameResolution() const { return gameResolution; }
|
|
||||||
void setViewportMode(ViewportMode mode);
|
|
||||||
ViewportMode getViewportMode() const { return viewportMode; }
|
|
||||||
std::string getViewportModeString() const;
|
|
||||||
sf::Vector2f windowToGameCoords(const sf::Vector2f& windowPos) const;
|
|
||||||
|
|
||||||
// global textures for scripts to access
|
// global textures for scripts to access
|
||||||
std::vector<IndexTexture> textures;
|
std::vector<IndexTexture> textures;
|
||||||
|
|
@ -181,28 +45,3 @@ public:
|
||||||
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> scene_ui(std::string scene);
|
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> scene_ui(std::string scene);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Visual overlay that displays real-time profiling metrics
|
|
||||||
*/
|
|
||||||
class GameEngine::ProfilerOverlay {
|
|
||||||
private:
|
|
||||||
sf::Font& font;
|
|
||||||
sf::Text text;
|
|
||||||
sf::RectangleShape background;
|
|
||||||
bool visible;
|
|
||||||
int updateInterval;
|
|
||||||
int frameCounter;
|
|
||||||
|
|
||||||
sf::Color getPerformanceColor(float frameTimeMs);
|
|
||||||
std::string formatFloat(float value, int precision = 1);
|
|
||||||
std::string formatPercentage(float part, float total);
|
|
||||||
|
|
||||||
public:
|
|
||||||
ProfilerOverlay(sf::Font& fontRef);
|
|
||||||
void toggle();
|
|
||||||
void setVisible(bool vis);
|
|
||||||
bool isVisible() const;
|
|
||||||
void update(const ProfilingMetrics& metrics);
|
|
||||||
void render(sf::RenderTarget& target);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,340 @@
|
||||||
|
#include "Grid.h"
|
||||||
|
#include <cmath>
|
||||||
|
#include "Entity.h"
|
||||||
|
|
||||||
|
GridPoint::GridPoint():
|
||||||
|
color(0, 0, 0, 0), walkable(false), tilesprite(-1), transparent(false), visible(false), discovered(false), color_overlay(0,0,0,255), tile_overlay(-1), uisprite(-1)
|
||||||
|
{};
|
||||||
|
|
||||||
|
void Grid::setSprite(int ti)
|
||||||
|
{
|
||||||
|
int tx = ti % texture_width, ty = ti / texture_width;
|
||||||
|
sprite.setTextureRect(sf::IntRect(tx * grid_size, ty * grid_size, grid_size, grid_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid::Grid(int gx, int gy, int gs, int _x, int _y, int _w, int _h):
|
||||||
|
grid_size(gs),
|
||||||
|
grid_x(gx), grid_y(gy),
|
||||||
|
zoom(1.0f), center_x((gx/2) * gs), center_y((gy/2) * gs),
|
||||||
|
texture_width(12), texture_height(11), visible(false)
|
||||||
|
{
|
||||||
|
//grid_size = gs;
|
||||||
|
//zoom = 1.0f;
|
||||||
|
//grid_x = gx;
|
||||||
|
//grid_y = gy;
|
||||||
|
tcodmap = new TCODMap(gx, gy);
|
||||||
|
points.resize(gx*gy);
|
||||||
|
box.setSize(sf::Vector2f(_w, _h));
|
||||||
|
box.setPosition(sf::Vector2f(_x, _y));
|
||||||
|
box.setFillColor(sf::Color(0,0,0,0));
|
||||||
|
|
||||||
|
renderTexture.create(_w, _h);
|
||||||
|
|
||||||
|
texture.loadFromFile("./assets/kenney_tinydungeon.png");
|
||||||
|
texture.setSmooth(false);
|
||||||
|
sprite.setTexture(texture);
|
||||||
|
|
||||||
|
//output.setSize(box.getSize());
|
||||||
|
output.setTextureRect(
|
||||||
|
sf::IntRect(0, 0,
|
||||||
|
box.getSize().x, box.getSize().y));
|
||||||
|
output.setPosition(box.getPosition());
|
||||||
|
// textures are upside-down inside renderTexture
|
||||||
|
output.setTexture(renderTexture.getTexture());
|
||||||
|
|
||||||
|
// Show one texture at a time
|
||||||
|
sprite.setTexture(texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Grid::refreshTCODmap() {
|
||||||
|
//int total = 0, walkable = 0, transparent = 0;
|
||||||
|
for (int x = 0; x < grid_x; x++) {
|
||||||
|
for (int y = 0; y < grid_y; y++) {
|
||||||
|
auto p = at(x, y);
|
||||||
|
//total++; if (p.walkable) walkable++; if (p.transparent) transparent++;
|
||||||
|
tcodmap->setProperties(x, y, p.transparent, p.walkable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//std::cout << "Map refreshed: " << total << " squares, " << walkable << "walkable, " << transparent << " transparent" << std::endl;
|
||||||
|
}
|
||||||
|
void Grid::refreshTCODsight(int x, int y) {
|
||||||
|
tcodmap->computeFov(x,y, 0, true, FOV_PERMISSIVE_8);
|
||||||
|
for (int x = 0; x < grid_x; x++) {
|
||||||
|
for (int y = 0; y < grid_y; y++) {
|
||||||
|
auto& p = at(x, y);
|
||||||
|
if (p.visible && !tcodmap->isInFov(x, y)) {
|
||||||
|
p.discovered = true;
|
||||||
|
p.visible = false;
|
||||||
|
} else if (!p.visible && tcodmap->isInFov(x,y)) {
|
||||||
|
p.discovered = true;
|
||||||
|
p.visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Grid::inBounds(int x, int y) {
|
||||||
|
return (x >= 0 && y >= 0 && x < grid_x && y < grid_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Grid::screenToGrid(int sx, int sy, int& gx, int& gy) {
|
||||||
|
|
||||||
|
float center_x_sq = center_x / grid_size;
|
||||||
|
float center_y_sq = center_y / grid_size;
|
||||||
|
|
||||||
|
float width_sq = box.getSize().x / (grid_size * zoom);
|
||||||
|
float height_sq = box.getSize().y / (grid_size * zoom);
|
||||||
|
float left_edge = center_x_sq - (width_sq / 2.0);
|
||||||
|
float right_edge = center_x_sq + (width_sq / 2.0);
|
||||||
|
float top_edge = center_y_sq - (height_sq / 2.0);
|
||||||
|
float bottom_edge = center_y_sq + (height_sq / 2.0);
|
||||||
|
|
||||||
|
float grid_px = zoom * grid_size;
|
||||||
|
//std::cout << "##############################" <<
|
||||||
|
// "\nscreen coord: (" << sx << ", " << sy << ")" << std::endl;
|
||||||
|
|
||||||
|
sx -= box.getPosition().x;
|
||||||
|
sy -= box.getPosition().y;
|
||||||
|
|
||||||
|
//std::cout << "box coord: (" << sx << ", " << sy << ")" << std::endl;
|
||||||
|
float mouse_x_sq = sx / grid_px;
|
||||||
|
float mouse_y_sq = sy / grid_px;
|
||||||
|
|
||||||
|
float ans_x = mouse_x_sq + left_edge;
|
||||||
|
float ans_y = mouse_y_sq + top_edge;
|
||||||
|
|
||||||
|
// compare integer method with this (mostly working) one
|
||||||
|
//int diff_realpixel_x = box.getSize().x / 2.0 - sx;
|
||||||
|
//int diff_realpixel_y = box.getSize().y / 2.0 - sy;
|
||||||
|
int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom);
|
||||||
|
int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom);
|
||||||
|
|
||||||
|
std::cout << "Float method got ans (" << ans_x << ", " << ans_y << ")"
|
||||||
|
<< std::endl << "Int method px (" << left_spritepixels + (sx/zoom) << ", " <<
|
||||||
|
top_spritepixels + (sy/zoom) << ")" << std::endl <<
|
||||||
|
"Int grid (" << (left_spritepixels + (sx/zoom) ) / grid_size <<
|
||||||
|
", " << (top_spritepixels + (sy/zoom)) / grid_size << ")" <<
|
||||||
|
|
||||||
|
std::endl;
|
||||||
|
|
||||||
|
// casting float turns -0.5 to 0; I want any negative coord to be OOB
|
||||||
|
if (ans_x < 0) ans_x = -1;
|
||||||
|
if (ans_y < 0) ans_y = -1;
|
||||||
|
|
||||||
|
gx = ans_x;
|
||||||
|
gy = ans_y;
|
||||||
|
/*
|
||||||
|
std::cout <<
|
||||||
|
"C: (" << center_x << ", " << center_y << ")" << std::endl <<
|
||||||
|
"W: " << width_sq << " H: " << height_sq << std::endl <<
|
||||||
|
"L: " << left_edge << " T: " << top_edge << std::endl <<
|
||||||
|
"R: " << right_edge << " B: " << bottom_edge << std::endl <<
|
||||||
|
"Grid Px: " << grid_px << "( zoom: " << zoom << ")" << std::endl <<
|
||||||
|
"answer: G(" << ans_x << ", " << ans_y << ")" << std::endl <<
|
||||||
|
"##############################" <<
|
||||||
|
std::endl;
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
void Grid::renderPxToGrid(int sx, int sy, int& gx, int& gy) {
|
||||||
|
// just like screen px coversion, but no offset by grid's position
|
||||||
|
float center_x_sq = center_x / grid_size;
|
||||||
|
float center_y_sq = center_y / grid_size;
|
||||||
|
|
||||||
|
float width_sq = box.getSize().x / (grid_size * zoom);
|
||||||
|
float height_sq = box.getSize().y / (grid_size * zoom);
|
||||||
|
|
||||||
|
int width_px = box.getSize().x / 2.0;
|
||||||
|
int height_px = box.getSize().y / 2.0;
|
||||||
|
|
||||||
|
float left_edge = center_x_sq - (width_sq / 2.0);
|
||||||
|
float top_edge = center_y_sq - (height_sq / 2.0);
|
||||||
|
|
||||||
|
float grid_px = zoom * grid_size;
|
||||||
|
float sx_sq = sx / grid_px;
|
||||||
|
float sy_sq = sy / grid_px;
|
||||||
|
|
||||||
|
float ans_x = sx_sq + left_edge;
|
||||||
|
float ans_y = sy_sq + top_edge;
|
||||||
|
|
||||||
|
if (ans_x < 0) ans_x = -1;
|
||||||
|
if (ans_y < 0) ans_y = -1;
|
||||||
|
|
||||||
|
gx = ans_x;
|
||||||
|
gy = ans_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Grid::integerGrid(float fx, float fy, int& gx, int& gy) {
|
||||||
|
if (fx < 0) fx -= 0.5;
|
||||||
|
if (fy < 0) fy -= 0.5;
|
||||||
|
gx = fx;
|
||||||
|
gy = fy;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Grid::gridToRenderPx(int gx, int gy, int& sx, int& sy) {
|
||||||
|
// integer grid square (gx, gy) - what pixel (on rendertexture)
|
||||||
|
// should it's top left corner be at (the sprite's position)
|
||||||
|
|
||||||
|
// eff_gridsize = grid_size * zoom
|
||||||
|
// if center_x = 161, and grid_size is 16, that's 10 + 1/16 sprites
|
||||||
|
// center_x - (box.getSize().x / 2 / zoom) = left edge (in px)
|
||||||
|
// gx * eff_gridsize = pixel position without panning
|
||||||
|
// pixel_gx - left_edge = grid's render position
|
||||||
|
|
||||||
|
sx = (gx * grid_size * zoom) - (center_x - (box.getSize().x / 2.0 / zoom));
|
||||||
|
sy = (gy * grid_size * zoom) - (center_y - (box.getSize().y / 2.0 / zoom));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Grid::render(sf::RenderWindow & window)
|
||||||
|
{
|
||||||
|
renderTexture.clear();
|
||||||
|
//renderTexture.draw(box);
|
||||||
|
|
||||||
|
// sprites that are visible according to zoom, center_x, center_y, and box width
|
||||||
|
float center_x_sq = center_x / grid_size;
|
||||||
|
float center_y_sq = center_y / grid_size;
|
||||||
|
|
||||||
|
float width_sq = box.getSize().x / (grid_size * zoom);
|
||||||
|
float height_sq = box.getSize().y / (grid_size * zoom);
|
||||||
|
float left_edge = center_x_sq - (width_sq / 2.0);
|
||||||
|
//float right_edge = center_x_sq + (width_sq / 2.0);
|
||||||
|
float top_edge = center_y_sq - (height_sq / 2.0);
|
||||||
|
//float bottom_edge = center_y_sq + (height_sq / 2.0);
|
||||||
|
|
||||||
|
|
||||||
|
int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom);
|
||||||
|
int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom);
|
||||||
|
|
||||||
|
sprite.setScale(sf::Vector2f(zoom, zoom));
|
||||||
|
sf::RectangleShape r; // for colors and overlays
|
||||||
|
r.setSize(sf::Vector2f(grid_size * zoom, grid_size * zoom));
|
||||||
|
r.setOutlineThickness(0);
|
||||||
|
|
||||||
|
int x_limit = left_edge + width_sq + 2;
|
||||||
|
if (x_limit > grid_x) x_limit = grid_x;
|
||||||
|
|
||||||
|
int y_limit = top_edge + height_sq + 2;
|
||||||
|
if (y_limit > grid_y) y_limit = grid_y;
|
||||||
|
|
||||||
|
//for (float x = (left_edge >= 0 ? left_edge : 0);
|
||||||
|
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
||||||
|
x < x_limit; //x < view_width;
|
||||||
|
x+=1)
|
||||||
|
{
|
||||||
|
//for (float y = (top_edge >= 0 ? top_edge : 0);
|
||||||
|
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
||||||
|
y < y_limit; //y < view_height;
|
||||||
|
y+=1)
|
||||||
|
{
|
||||||
|
// Converting everything to integer pixels to avoid jitter
|
||||||
|
//auto pixel_pos = sf::Vector2f(
|
||||||
|
// (x - left_edge) * (zoom * grid_size),
|
||||||
|
// (y - top_edge) * (zoom * grid_size));
|
||||||
|
|
||||||
|
// This failed horribly:
|
||||||
|
//int gx, gy; integerGrid(x, y, gx, gy);
|
||||||
|
//int px_x, px_y; gridToRenderPx(gx, gy, px_x, px_y);
|
||||||
|
//auto pixel_pos = sf::Vector2f(px_x, px_y);
|
||||||
|
|
||||||
|
// this draws coherently, but the coordinates
|
||||||
|
// don't match up with the mouse cursor function
|
||||||
|
// jitter not eliminated
|
||||||
|
auto pixel_pos = sf::Vector2f(
|
||||||
|
(x*grid_size - left_spritepixels) * zoom,
|
||||||
|
(y*grid_size - top_spritepixels) * zoom );
|
||||||
|
|
||||||
|
auto gridpoint = at(std::floor(x), std::floor(y));
|
||||||
|
|
||||||
|
sprite.setPosition(pixel_pos);
|
||||||
|
|
||||||
|
r.setPosition(pixel_pos);
|
||||||
|
r.setFillColor(gridpoint.color);
|
||||||
|
renderTexture.draw(r);
|
||||||
|
|
||||||
|
// tilesprite
|
||||||
|
// if discovered but not visible, set opacity to 90%
|
||||||
|
// if not discovered... just don't draw it?
|
||||||
|
if (gridpoint.tilesprite != -1) {
|
||||||
|
setSprite(gridpoint.tilesprite);
|
||||||
|
renderTexture.draw(sprite);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto e : entities) {
|
||||||
|
auto drawent = e->cGrid->indexsprite.drawable();
|
||||||
|
drawent.setScale(zoom, zoom);
|
||||||
|
auto pixel_pos = sf::Vector2f(
|
||||||
|
(drawent.getPosition().x*grid_size - left_spritepixels) * zoom,
|
||||||
|
(drawent.getPosition().y*grid_size - top_spritepixels) * zoom );
|
||||||
|
drawent.setPosition(pixel_pos);
|
||||||
|
renderTexture.draw(drawent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop again and draw on top of entities
|
||||||
|
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
||||||
|
x < x_limit; //x < view_width;
|
||||||
|
x+=1)
|
||||||
|
{
|
||||||
|
//for (float y = (top_edge >= 0 ? top_edge : 0);
|
||||||
|
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
||||||
|
y < y_limit; //y < view_height;
|
||||||
|
y+=1)
|
||||||
|
{
|
||||||
|
|
||||||
|
auto pixel_pos = sf::Vector2f(
|
||||||
|
(x*grid_size - left_spritepixels) * zoom,
|
||||||
|
(y*grid_size - top_spritepixels) * zoom );
|
||||||
|
|
||||||
|
auto gridpoint = at(std::floor(x), std::floor(y));
|
||||||
|
|
||||||
|
sprite.setPosition(pixel_pos);
|
||||||
|
|
||||||
|
r.setPosition(pixel_pos);
|
||||||
|
|
||||||
|
// visible & discovered layers for testing purposes
|
||||||
|
if (!gridpoint.discovered) {
|
||||||
|
r.setFillColor(sf::Color(16, 16, 20, 192)); // 255 opacity for actual blackout
|
||||||
|
renderTexture.draw(r);
|
||||||
|
} else if (!gridpoint.visible) {
|
||||||
|
r.setFillColor(sf::Color(32, 32, 40, 128));
|
||||||
|
renderTexture.draw(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// overlay
|
||||||
|
|
||||||
|
// uisprite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// grid lines for testing & validation
|
||||||
|
/*
|
||||||
|
sf::Vertex line[] =
|
||||||
|
{
|
||||||
|
sf::Vertex(sf::Vector2f(0, 0), sf::Color::Red),
|
||||||
|
sf::Vertex(box.getSize(), sf::Color::Red),
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
renderTexture.draw(line, 2, sf::Lines);
|
||||||
|
sf::Vertex lineb[] =
|
||||||
|
{
|
||||||
|
sf::Vertex(sf::Vector2f(0, box.getSize().y), sf::Color::Blue),
|
||||||
|
sf::Vertex(sf::Vector2f(box.getSize().x, 0), sf::Color::Blue),
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
renderTexture.draw(lineb, 2, sf::Lines);
|
||||||
|
*/
|
||||||
|
|
||||||
|
// render to window
|
||||||
|
renderTexture.display();
|
||||||
|
window.draw(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
GridPoint& Grid::at(int x, int y)
|
||||||
|
{
|
||||||
|
return points[y * grid_x + x];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
#pragma once
|
||||||
|
#include "Common.h"
|
||||||
|
#include "libtcod.h"
|
||||||
|
|
||||||
|
//#include "Entity.h"
|
||||||
|
class Entity; // forward declare
|
||||||
|
|
||||||
|
class GridPoint
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Layers: color, walkable, tilesprite, transparent, visible, discovered, overlay, uisprite
|
||||||
|
sf::Color color;
|
||||||
|
bool walkable;
|
||||||
|
int tilesprite;
|
||||||
|
bool transparent, visible, discovered;
|
||||||
|
sf::Color color_overlay;
|
||||||
|
int tile_overlay, uisprite;
|
||||||
|
GridPoint();
|
||||||
|
};
|
||||||
|
|
||||||
|
class Grid
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
public:
|
||||||
|
Grid();
|
||||||
|
sf::RectangleShape box; // view on window
|
||||||
|
bool visible;
|
||||||
|
sf::Texture texture;
|
||||||
|
sf::Sprite sprite, output;
|
||||||
|
sf::RenderTexture renderTexture;
|
||||||
|
TCODMap* tcodmap;
|
||||||
|
void setSprite(int);
|
||||||
|
const int texture_width, texture_height;
|
||||||
|
auto contains(sf::Vector2i p) { return box.getGlobalBounds().contains(p.x, p.y); }
|
||||||
|
|
||||||
|
Grid(int gx, int gy, int gs, int _x, int _y, int _w, int _h);
|
||||||
|
int grid_x, grid_y; // rectangle map size (integer - sprites)
|
||||||
|
int grid_size; // pixel size of 1 sprite
|
||||||
|
float zoom;
|
||||||
|
int center_x, center_y; // center in 1.0x Pixels
|
||||||
|
|
||||||
|
std::vector<GridPoint> points; // grid visible contents
|
||||||
|
std::vector<std::shared_ptr<Entity>> entities;
|
||||||
|
void render(sf::RenderWindow&); // draw to screen
|
||||||
|
GridPoint& at(int, int);
|
||||||
|
bool inBounds(int, int);
|
||||||
|
void screenToGrid(int, int, int&, int&);
|
||||||
|
|
||||||
|
void renderPxToGrid(int, int, int&, int&);
|
||||||
|
void gridToRenderPx(int, int, int&, int&);
|
||||||
|
void integerGrid(float, float, int&, int&);
|
||||||
|
|
||||||
|
void refreshTCODmap();
|
||||||
|
void refreshTCODsight(int, int);
|
||||||
|
TCODDijkstra *dijkstra; //= new TCODDijkstra(myMap);
|
||||||
|
};
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
#include "HeadlessRenderer.h"
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
bool HeadlessRenderer::init(int width, int height) {
|
|
||||||
if (!render_texture.create(width, height)) {
|
|
||||||
std::cerr << "Failed to create headless render texture" << std::endl;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
sf::RenderTarget& HeadlessRenderer::getRenderTarget() {
|
|
||||||
return render_texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HeadlessRenderer::saveScreenshot(const std::string& path) {
|
|
||||||
sf::Image screenshot = render_texture.getTexture().copyToImage();
|
|
||||||
if (!screenshot.saveToFile(path)) {
|
|
||||||
std::cerr << "Failed to save screenshot to: " << path << std::endl;
|
|
||||||
} else {
|
|
||||||
std::cout << "Screenshot saved to: " << path << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HeadlessRenderer::display() {
|
|
||||||
render_texture.display();
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
#ifndef HEADLESS_RENDERER_H
|
|
||||||
#define HEADLESS_RENDERER_H
|
|
||||||
|
|
||||||
#include <SFML/Graphics.hpp>
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class HeadlessRenderer {
|
|
||||||
private:
|
|
||||||
sf::RenderTexture render_texture;
|
|
||||||
|
|
||||||
public:
|
|
||||||
bool init(int width = 1024, int height = 768);
|
|
||||||
sf::RenderTarget& getRenderTarget();
|
|
||||||
void saveScreenshot(const std::string& path);
|
|
||||||
void display(); // Finalize the current frame
|
|
||||||
bool isOpen() const { return true; } // Always "open" in headless mode
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // HEADLESS_RENDERER_H
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#include "IndexSprite.h"
|
||||||
|
#include "GameEngine.h"
|
||||||
|
|
||||||
|
//int texture_index, sprite_index, x, y;
|
||||||
|
|
||||||
|
GameEngine* IndexSprite::game;
|
||||||
|
|
||||||
|
sf::Sprite IndexSprite::drawable()
|
||||||
|
{
|
||||||
|
sf::Sprite s;
|
||||||
|
auto& tex = IndexSprite::game->textures[texture_index];
|
||||||
|
s.setTexture(tex.texture);
|
||||||
|
s.setScale(sf::Vector2f(scale, scale));
|
||||||
|
s.setPosition(sf::Vector2f(x, y));
|
||||||
|
//std::cout << "Drawable position: " << x << ", " << y << " -> " << s.getPosition().x << ", " << s.getPosition().y << std::endl;
|
||||||
|
s.setTextureRect(tex.spriteCoordinates(sprite_index));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexSprite::IndexSprite(int _ti, int _si, float _x, float _y, float _s):
|
||||||
|
texture_index(_ti), sprite_index(_si), x(_x), y(_y), scale(_s) {
|
||||||
|
//std::cout << "IndexSprite constructed with x, y " << _x << ", " << _y << std::endl;
|
||||||
|
//std::cout << " * Stored x, y " << x << ", " << y << std::endl;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
#pragma once
|
||||||
|
#include "Common.h"
|
||||||
|
class GameEngine; // forward declare
|
||||||
|
|
||||||
|
class IndexSprite {
|
||||||
|
public:
|
||||||
|
int texture_index, sprite_index;
|
||||||
|
float x, y;
|
||||||
|
float scale;
|
||||||
|
static GameEngine* game;
|
||||||
|
sf::Sprite drawable();
|
||||||
|
IndexSprite(int, int, float, float, float);
|
||||||
|
};
|
||||||
1824
src/McRFPy_API.cpp
1824
src/McRFPy_API.cpp
File diff suppressed because it is too large
Load Diff
162
src/McRFPy_API.h
162
src/McRFPy_API.h
|
|
@ -1,11 +1,19 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
|
#include "Entity.h"
|
||||||
|
//#include "EntityManager.h"
|
||||||
|
//#include "Scene.h"
|
||||||
|
//#include "GameEngine.h" // can't - need forward declaration
|
||||||
|
//#include "ActionCode.h"
|
||||||
#include "Python.h"
|
#include "Python.h"
|
||||||
|
#include "UIMenu.h"
|
||||||
|
#include "Grid.h"
|
||||||
|
#include "IndexSprite.h"
|
||||||
|
#include "EntityManager.h"
|
||||||
#include <list>
|
#include <list>
|
||||||
|
|
||||||
#include "PyFont.h"
|
// implementation required to link templates
|
||||||
#include "PyTexture.h"
|
#include "Animation.h"
|
||||||
#include "McRogueFaceConfig.h"
|
|
||||||
|
|
||||||
class GameEngine; // forward declared (circular members)
|
class GameEngine; // forward declared (circular members)
|
||||||
|
|
||||||
|
|
@ -16,30 +24,65 @@ private:
|
||||||
texture_width = 12, texture_height = 11, // w & h sprite/frame count
|
texture_width = 12, texture_height = 11, // w & h sprite/frame count
|
||||||
texture_sprite_count = 11 * 12; // t_width * t_height, minus blanks?
|
texture_sprite_count = 11 * 12; // t_width * t_height, minus blanks?
|
||||||
|
|
||||||
|
// TODO: this is wrong, load resources @ GameEngineSprite sprite;
|
||||||
|
// sf::Texture texture;
|
||||||
|
|
||||||
|
//std::vector<PyMethodDef> mcrfpyMethodsVector;
|
||||||
|
//static PyObject* PyInit_mcrfpy();
|
||||||
|
|
||||||
McRFPy_API();
|
McRFPy_API();
|
||||||
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static PyObject* mcrf_module;
|
inline static sf::Sprite sprite;
|
||||||
static std::shared_ptr<PyFont> default_font;
|
inline static sf::Texture texture;
|
||||||
static std::shared_ptr<PyTexture> default_texture;
|
static void setSpriteTexture(int);
|
||||||
//inline static sf::Sprite sprite;
|
|
||||||
//inline static sf::Texture texture;
|
|
||||||
//static void setSpriteTexture(int);
|
|
||||||
inline static GameEngine* game;
|
inline static GameEngine* game;
|
||||||
static void api_init();
|
static void api_init();
|
||||||
static void api_init(const McRogueFaceConfig& config, int argc, char** argv);
|
|
||||||
static PyStatus init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv);
|
|
||||||
static void api_shutdown();
|
static void api_shutdown();
|
||||||
// Python API functionality - use mcrfpy.* in scripts
|
// Python API functionality - use mcrfpy.* in scripts
|
||||||
//static PyObject* _drawSprite(PyObject*, PyObject*);
|
static PyObject* _drawSprite(PyObject*, PyObject*);
|
||||||
static void REPL_device(FILE * fp, const char *filename);
|
static void REPL_device(FILE * fp, const char *filename);
|
||||||
static void REPL();
|
static void REPL();
|
||||||
|
|
||||||
static std::vector<sf::SoundBuffer>* soundbuffers;
|
// Jank mode engage: let the API hold data for Python to hack on
|
||||||
static sf::Music* music;
|
static std::map<std::string, UIMenu*> menus;
|
||||||
static sf::Sound* sfx;
|
static EntityManager entities; // this is also kinda good, entities not on the current grid can still act (like monsters following you through doors??)
|
||||||
|
static std::map<std::string, Grid*> grids;
|
||||||
|
static std::list<Animation*> animations;
|
||||||
|
static std::vector<sf::SoundBuffer> soundbuffers;
|
||||||
|
static sf::Music music;
|
||||||
|
static sf::Sound sfx;
|
||||||
|
|
||||||
|
static std::shared_ptr<Entity> player;
|
||||||
|
|
||||||
|
static std::map<std::string, PyObject*> callbacks;
|
||||||
|
|
||||||
|
// Jank Python Method Exposures
|
||||||
|
static PyObject* _createMenu(PyObject*, PyObject*); // creates a new menu object in McRFPy_API::menus
|
||||||
|
static PyObject* _listMenus(PyObject*, PyObject*);
|
||||||
|
static PyObject* _modMenu(PyObject*, PyObject*);
|
||||||
|
|
||||||
|
static PyObject* _createCaption(PyObject*, PyObject*); // calls menu.add_caption
|
||||||
|
static PyObject* _createButton(PyObject*, PyObject*);
|
||||||
|
static PyObject* _createTexture(PyObject*, PyObject*);
|
||||||
|
static PyObject* _listTextures(PyObject*, PyObject*);
|
||||||
|
static PyObject* _createSprite(PyObject*, PyObject*);
|
||||||
|
|
||||||
|
// use _listMenus, probably will not implement
|
||||||
|
//static PyObject* _listCaptions(PyObject*, PyObject*);
|
||||||
|
//static PyObject* _listButtons(PyObject*, PyObject*);
|
||||||
|
|
||||||
|
static PyObject* _createEntity(PyObject*, PyObject*);
|
||||||
|
//static PyObject* _listEntities(PyObject*, PyObject*);
|
||||||
|
|
||||||
|
static PyObject* _createGrid(PyObject*, PyObject*);
|
||||||
|
static PyObject* _listGrids(PyObject*, PyObject*);
|
||||||
|
static PyObject* _modGrid(PyObject*, PyObject*);
|
||||||
|
|
||||||
|
static PyObject* _createAnimation(PyObject*, PyObject*);
|
||||||
|
|
||||||
|
static PyObject* _registerPyAction(PyObject*, PyObject*);
|
||||||
|
static PyObject* _registerInputAction(PyObject*, PyObject*);
|
||||||
|
|
||||||
static PyObject* _createSoundBuffer(PyObject*, PyObject*);
|
static PyObject* _createSoundBuffer(PyObject*, PyObject*);
|
||||||
static PyObject* _loadMusic(PyObject*, PyObject*);
|
static PyObject* _loadMusic(PyObject*, PyObject*);
|
||||||
|
|
@ -49,40 +92,71 @@ public:
|
||||||
static PyObject* _getMusicVolume(PyObject*, PyObject*);
|
static PyObject* _getMusicVolume(PyObject*, PyObject*);
|
||||||
static PyObject* _getSoundVolume(PyObject*, PyObject*);
|
static PyObject* _getSoundVolume(PyObject*, PyObject*);
|
||||||
|
|
||||||
|
// allow all player actions (items, menus, movement, combat)
|
||||||
|
static PyObject* _unlockPlayerInput(PyObject*, PyObject*);
|
||||||
|
// disallow player actions (animating enemy actions)
|
||||||
|
static PyObject* _lockPlayerInput(PyObject*, PyObject*);
|
||||||
|
// prompt C++/Grid Objects to callback with a target Entity or grid space
|
||||||
|
static PyObject* _requestGridTarget(PyObject*, PyObject*);
|
||||||
|
// string for labeling the map
|
||||||
|
static std::string active_grid;
|
||||||
|
static PyObject* _activeGrid(PyObject*, PyObject*);
|
||||||
|
static PyObject* _setActiveGrid(PyObject*, PyObject*);
|
||||||
|
// string for prompting input
|
||||||
|
static std::string input_mode;
|
||||||
|
static PyObject* _inputMode(PyObject*, PyObject*);
|
||||||
|
// turn cycle
|
||||||
|
static int turn_number;
|
||||||
|
static PyObject* _turnNumber(PyObject*, PyObject*);
|
||||||
|
static PyObject* _refreshFov(PyObject*, PyObject*);
|
||||||
|
static bool do_camfollow;
|
||||||
|
static void camFollow();
|
||||||
|
static PyObject* _camFollow(PyObject*, PyObject*);
|
||||||
|
|
||||||
static PyObject* _sceneUI(PyObject*, PyObject*);
|
static PyObject* _sceneUI(PyObject*, PyObject*);
|
||||||
|
|
||||||
// scene control
|
|
||||||
static PyObject* _setScene(PyObject*, PyObject*);
|
|
||||||
static PyObject* _currentScene(PyObject*, PyObject*);
|
|
||||||
static PyObject* _createScene(PyObject*, PyObject*);
|
|
||||||
static PyObject* _keypressScene(PyObject*, PyObject*);
|
|
||||||
|
|
||||||
// timer control
|
|
||||||
static PyObject* _setTimer(PyObject*, PyObject*);
|
|
||||||
static PyObject* _delTimer(PyObject*, PyObject*);
|
|
||||||
|
|
||||||
static PyObject* _exit(PyObject*, PyObject*);
|
|
||||||
static PyObject* _setScale(PyObject*, PyObject*);
|
|
||||||
|
|
||||||
// accept keyboard input from scene
|
// accept keyboard input from scene
|
||||||
static sf::Vector2i cursor_position;
|
static sf::Vector2i cursor_position;
|
||||||
|
static void player_input(int, int);
|
||||||
|
static void computerTurn();
|
||||||
|
static void playerTurn();
|
||||||
|
|
||||||
|
// Jank Functionality
|
||||||
|
static UIMenu* createMenu(int posx, int posy, int sizex, int sizey);
|
||||||
|
static void createCaption(std::string menukey, std::string text, int fontsize, sf::Color textcolor);
|
||||||
|
static void createButton(std::string menukey, int x, int y, int w, int h, sf::Color bgcolor, sf::Color textcolor, std::string caption, std::string action);
|
||||||
|
static void createSprite(std::string menukey, int ti, int si, float x, float y, float scale);
|
||||||
|
static int createTexture(std::string filename, int grid_size, int grid_width, int grid_height);
|
||||||
|
//static void playSound(const char * filename);
|
||||||
|
//static void playMusic(const char * filename);
|
||||||
|
|
||||||
|
static void doAction(std::string);
|
||||||
|
|
||||||
|
// McRFPy_API(GameEngine*);
|
||||||
|
|
||||||
|
// API functionality - use from C++ directly
|
||||||
|
|
||||||
|
//void spawnEntity(int tex_index, int grid_x, int grid_y, PyObject* script);
|
||||||
|
|
||||||
static void executeScript(std::string);
|
static void executeScript(std::string);
|
||||||
static void executePyString(std::string);
|
static void executePyString(std::string);
|
||||||
|
|
||||||
// Helper to mark scenes as needing z_index resort
|
|
||||||
static void markSceneNeedsSort();
|
|
||||||
|
|
||||||
// Name-based finding methods
|
|
||||||
static PyObject* _find(PyObject*, PyObject*);
|
|
||||||
static PyObject* _findAll(PyObject*, PyObject*);
|
|
||||||
|
|
||||||
// Profiling/metrics
|
|
||||||
static PyObject* _getMetrics(PyObject*, PyObject*);
|
|
||||||
|
|
||||||
// Scene lifecycle management for Python Scene objects
|
|
||||||
static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene);
|
|
||||||
static void updatePythonScenes(float dt);
|
|
||||||
static void triggerResize(int width, int height);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
static PyMethodDef mcrfpyMethods[] = {
|
||||||
|
{"drawSprite", McRFPy_API::_drawSprite, METH_VARARGS,
|
||||||
|
"Draw a sprite (index, x, y)"},
|
||||||
|
{NULL, NULL, 0, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
static PyModuleDef mcrfpyModule = {
|
||||||
|
PyModuleDef_HEAD_INIT, "mcrfpy", NULL, -1, mcrfpyMethods,
|
||||||
|
NULL, NULL, NULL, NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
// Module initializer fn, passed to PyImport_AppendInittab
|
||||||
|
PyObject* PyInit_mcrfpy()
|
||||||
|
{
|
||||||
|
return PyModule_Create(&mcrfpyModule);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue