Compare commits
7 commits
6d5e99a114
...
188b312af0
| Author | SHA1 | Date | |
|---|---|---|---|
| 188b312af0 | |||
| e7462e37a3 | |||
| 9ca79baec8 | |||
| e58b44ef82 | |||
| d73a207535 | |||
| c332772324 | |||
| 1805b985bd |
37 changed files with 2848 additions and 855 deletions
|
|
@ -17,6 +17,9 @@ option(MCRF_SDL2 "Build with SDL2+OpenGL ES 2 backend instead of SFML" OFF)
|
|||
# Playground mode - minimal scripts for web playground (REPL-focused)
|
||||
option(MCRF_PLAYGROUND "Build with minimal playground scripts instead of full game" OFF)
|
||||
|
||||
# Demo mode - self-contained demo game for web showcase
|
||||
option(MCRF_DEMO "Build with demo scripts (web showcase)" OFF)
|
||||
|
||||
# Game shell mode - fullscreen canvas, no REPL chrome (for itch.io / standalone web games)
|
||||
option(MCRF_GAME_SHELL "Use minimal game-only HTML shell (no REPL)" OFF)
|
||||
|
||||
|
|
@ -343,6 +346,7 @@ endif()
|
|||
set(MCRF_ASSETS_DIR "${CMAKE_SOURCE_DIR}/assets" CACHE PATH "Assets directory for WASM preloading")
|
||||
set(MCRF_SCRIPTS_DIR "${CMAKE_SOURCE_DIR}/src/scripts" CACHE PATH "Scripts directory for WASM preloading")
|
||||
set(MCRF_SCRIPTS_PLAYGROUND_DIR "${CMAKE_SOURCE_DIR}/src/scripts_playground" CACHE PATH "Playground scripts for WASM")
|
||||
set(MCRF_SCRIPTS_DEMO_DIR "${CMAKE_SOURCE_DIR}/src/scripts_demo" CACHE PATH "Demo scripts for WASM showcase")
|
||||
|
||||
# Emscripten-specific link options (use ports for zlib, bzip2, sqlite3)
|
||||
if(EMSCRIPTEN)
|
||||
|
|
@ -365,8 +369,8 @@ if(EMSCRIPTEN)
|
|||
-sALLOW_UNIMPLEMENTED_SYSCALLS=1
|
||||
# Preload Python stdlib into virtual filesystem at /lib/python3.14
|
||||
--preload-file=${CMAKE_SOURCE_DIR}/wasm_stdlib/lib@/lib
|
||||
# Preload game scripts into /scripts (use playground scripts if MCRF_PLAYGROUND is set)
|
||||
--preload-file=$<IF:$<BOOL:${MCRF_PLAYGROUND}>,${MCRF_SCRIPTS_PLAYGROUND_DIR},${MCRF_SCRIPTS_DIR}>@/scripts
|
||||
# Preload game scripts into /scripts (playground, demo, or full game)
|
||||
--preload-file=$<IF:$<BOOL:${MCRF_PLAYGROUND}>,${MCRF_SCRIPTS_PLAYGROUND_DIR},$<IF:$<BOOL:${MCRF_DEMO}>,${MCRF_SCRIPTS_DEMO_DIR},${MCRF_SCRIPTS_DIR}>>@/scripts
|
||||
# Preload assets
|
||||
--preload-file=${MCRF_ASSETS_DIR}@/assets
|
||||
# Use custom HTML shell - game shell (fullscreen) or playground shell (REPL)
|
||||
|
|
|
|||
327
Makefile
Normal file
327
Makefile
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
# McRogueFace Build Makefile
|
||||
# Usage:
|
||||
# make - Build for Linux (default)
|
||||
# make windows - Cross-compile for Windows using MinGW (release)
|
||||
# make windows-debug - Cross-compile for Windows with console & debug symbols
|
||||
# make clean - Clean Linux build
|
||||
# make clean-windows - Clean Windows build
|
||||
# make run - Run the Linux build
|
||||
#
|
||||
# WebAssembly / Emscripten:
|
||||
# make wasm - Build full game for web (requires emsdk activated)
|
||||
# make wasm-game - Build game for web with fullscreen canvas (no REPL)
|
||||
# make playground - Build minimal playground for web REPL
|
||||
# make serve - Serve wasm build locally on port 8080
|
||||
# make serve-game - Serve wasm-game build locally on port 8080
|
||||
# make clean-wasm - Clean Emscripten builds
|
||||
#
|
||||
# Packaging:
|
||||
# make package-windows-light - Windows with minimal stdlib (~5 MB)
|
||||
# make package-windows-full - Windows with full stdlib (~15 MB)
|
||||
# make package-linux-light - Linux with minimal stdlib
|
||||
# make package-linux-full - Linux with full stdlib
|
||||
# make package-all - All platform/preset combinations
|
||||
#
|
||||
# Release:
|
||||
# make version-bump NEXT_VERSION=x.y.z-suffix
|
||||
# Tags HEAD with current version, builds all packages, bumps to NEXT_VERSION
|
||||
|
||||
.PHONY: all linux windows windows-debug clean clean-windows clean-dist run
|
||||
.PHONY: wasm wasm-game playground serve serve-game serve-playground clean-wasm
|
||||
.PHONY: package-windows-light package-windows-full package-linux-light package-linux-full package-all
|
||||
.PHONY: version-bump
|
||||
.PHONY: debug debug-test asan asan-test valgrind-test massif-test analyze clean-debug
|
||||
|
||||
# Number of parallel jobs for compilation
|
||||
JOBS := $(shell nproc 2>/dev/null || echo 4)
|
||||
|
||||
all: linux
|
||||
|
||||
linux:
|
||||
@echo "Building McRogueFace for Linux..."
|
||||
@mkdir -p build
|
||||
@cd build && cmake .. -DCMAKE_BUILD_TYPE=Release && make -j$(JOBS)
|
||||
@echo "Build complete! Run with: ./build/mcrogueface"
|
||||
|
||||
windows:
|
||||
@echo "Cross-compiling McRogueFace for Windows..."
|
||||
@mkdir -p build-windows
|
||||
@cd build-windows && cmake .. \
|
||||
-DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release && make -j$(JOBS)
|
||||
@echo "Windows build complete! Output: build-windows/mcrogueface.exe"
|
||||
|
||||
windows-debug:
|
||||
@echo "Cross-compiling McRogueFace for Windows (debug with console)..."
|
||||
@mkdir -p build-windows-debug
|
||||
@cd build-windows-debug && cmake .. \
|
||||
-DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_WINDOWS_CONSOLE=ON && make -j$(JOBS)
|
||||
@echo "Windows debug build complete! Output: build-windows-debug/mcrogueface.exe"
|
||||
@echo "Run from cmd.exe to see console output"
|
||||
|
||||
clean:
|
||||
@echo "Cleaning Linux build..."
|
||||
@rm -rf build
|
||||
|
||||
clean-windows:
|
||||
@echo "Cleaning Windows builds..."
|
||||
@rm -rf build-windows build-windows-debug
|
||||
|
||||
clean-dist:
|
||||
@echo "Cleaning distribution packages..."
|
||||
@rm -rf dist
|
||||
|
||||
clean-all: clean clean-windows clean-wasm clean-debug clean-dist
|
||||
@echo "All builds and packages cleaned."
|
||||
|
||||
run: linux
|
||||
@cd build && ./mcrogueface
|
||||
|
||||
# Debug and sanitizer targets
|
||||
debug:
|
||||
@echo "Building McRogueFace with debug Python (pydebug assertions)..."
|
||||
@mkdir -p build-debug
|
||||
@cd build-debug && cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_DEBUG_PYTHON=ON && make -j$(JOBS)
|
||||
@echo "Debug build complete! Output: build-debug/mcrogueface"
|
||||
|
||||
debug-test: debug
|
||||
@echo "Running test suite with debug Python..."
|
||||
cd tests && MCRF_BUILD_DIR=../build-debug \
|
||||
MCRF_LIB_DIR=../__lib_debug \
|
||||
python3 run_tests.py -v
|
||||
|
||||
asan:
|
||||
@echo "Building McRogueFace with ASan + UBSan..."
|
||||
@mkdir -p build-asan
|
||||
@cd build-asan && cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_DEBUG_PYTHON=ON \
|
||||
-DMCRF_SANITIZE_ADDRESS=ON \
|
||||
-DMCRF_SANITIZE_UNDEFINED=ON && make -j$(JOBS)
|
||||
@echo "ASan build complete! Output: build-asan/mcrogueface"
|
||||
|
||||
asan-test: asan
|
||||
@echo "Running test suite under ASan + UBSan..."
|
||||
cd tests && MCRF_BUILD_DIR=../build-asan \
|
||||
MCRF_LIB_DIR=../__lib_debug \
|
||||
PYTHONMALLOC=malloc \
|
||||
ASAN_OPTIONS="detect_leaks=1:halt_on_error=1:print_summary=1" \
|
||||
LSAN_OPTIONS="suppressions=$(CURDIR)/sanitizers/asan.supp" \
|
||||
UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1" \
|
||||
python3 run_tests.py -v --sanitizer
|
||||
|
||||
valgrind-test: debug
|
||||
@echo "Running test suite under Valgrind memcheck..."
|
||||
cd tests && MCRF_BUILD_DIR=../build-debug \
|
||||
MCRF_LIB_DIR=../__lib_debug \
|
||||
MCRF_TIMEOUT_MULTIPLIER=50 \
|
||||
PYTHONMALLOC=malloc \
|
||||
python3 run_tests.py -v --valgrind
|
||||
|
||||
massif-test: debug
|
||||
@echo "Running heap profiling under Valgrind Massif..."
|
||||
@mkdir -p build-debug
|
||||
cd build-debug && valgrind --tool=massif \
|
||||
--massif-out-file=massif.out \
|
||||
--pages-as-heap=no \
|
||||
--detailed-freq=10 \
|
||||
--max-snapshots=100 \
|
||||
./mcrogueface --headless --exec ../tests/benchmarks/stress_test_suite.py
|
||||
@echo "Massif output: build-debug/massif.out"
|
||||
@echo "View with: ms_print build-debug/massif.out"
|
||||
|
||||
analyze:
|
||||
@echo "Running cppcheck static analysis..."
|
||||
cppcheck --enable=warning,performance,portability \
|
||||
--suppress=missingIncludeSystem \
|
||||
--suppress=unusedFunction \
|
||||
--suppress=noExplicitConstructor \
|
||||
--suppress=missingOverride \
|
||||
--inline-suppr \
|
||||
-I src/ -I deps/ -I deps/cpython -I deps/Python \
|
||||
-I src/platform -I src/3d -I src/tiled -I src/ldtk -I src/audio \
|
||||
--std=c++20 \
|
||||
--quiet \
|
||||
src/ 2>&1
|
||||
@echo "Static analysis complete."
|
||||
|
||||
clean-debug:
|
||||
@echo "Cleaning debug/sanitizer builds..."
|
||||
@rm -rf build-debug build-asan
|
||||
|
||||
# Packaging targets using tools/package.sh
|
||||
package-windows-light: windows
|
||||
@./tools/package.sh windows light
|
||||
|
||||
package-windows-full: windows
|
||||
@./tools/package.sh windows full
|
||||
|
||||
package-linux-light: linux
|
||||
@./tools/package.sh linux light
|
||||
|
||||
package-linux-full: linux
|
||||
@./tools/package.sh linux full
|
||||
|
||||
package-all: windows linux
|
||||
@./tools/package.sh all
|
||||
|
||||
# Legacy target for backwards compatibility
|
||||
package-windows: package-windows-full
|
||||
|
||||
# Emscripten / WebAssembly targets
|
||||
# Requires: source ~/emsdk/emsdk_env.sh (or wherever your emsdk is installed)
|
||||
#
|
||||
# For iterative development, configure once then rebuild:
|
||||
# source ~/emsdk/emsdk_env.sh && emmake make -C build-emscripten
|
||||
#
|
||||
wasm:
|
||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f build-emscripten/Makefile ]; then \
|
||||
echo "Configuring WebAssembly build (full game)..."; \
|
||||
mkdir -p build-emscripten; \
|
||||
cd build-emscripten && emcmake cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DMCRF_SDL2=ON; \
|
||||
fi
|
||||
@echo "Building McRogueFace for WebAssembly..."
|
||||
@emmake make -C build-emscripten -j$(JOBS)
|
||||
@echo "WebAssembly build complete! Files in build-emscripten/"
|
||||
@echo "Run 'make serve' to test locally"
|
||||
|
||||
playground:
|
||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f build-playground/Makefile ]; then \
|
||||
echo "Configuring Playground build..."; \
|
||||
mkdir -p build-playground; \
|
||||
cd build-playground && emcmake cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DMCRF_SDL2=ON \
|
||||
-DMCRF_PLAYGROUND=ON; \
|
||||
fi
|
||||
@echo "Building McRogueFace Playground for WebAssembly..."
|
||||
@emmake make -C build-playground -j$(JOBS)
|
||||
@echo "Playground build complete! Files in build-playground/"
|
||||
@echo "Run 'make serve-playground' to test locally"
|
||||
|
||||
serve:
|
||||
@echo "Serving WebAssembly build at http://localhost:8080"
|
||||
@echo "Press Ctrl+C to stop"
|
||||
@cd build-emscripten && python3 -m http.server 8080
|
||||
|
||||
serve-playground:
|
||||
@echo "Serving Playground build at http://localhost:8080"
|
||||
@echo "Press Ctrl+C to stop"
|
||||
@cd build-playground && python3 -m http.server 8080
|
||||
|
||||
wasm-game:
|
||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f build-wasm-game/Makefile ]; then \
|
||||
echo "Configuring WebAssembly game build (fullscreen, no REPL)..."; \
|
||||
mkdir -p build-wasm-game; \
|
||||
cd build-wasm-game && emcmake cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DMCRF_SDL2=ON \
|
||||
-DMCRF_GAME_SHELL=ON; \
|
||||
fi
|
||||
@echo "Building McRogueFace game for WebAssembly..."
|
||||
@emmake make -C build-wasm-game -j$(JOBS)
|
||||
@echo "Game build complete! Files in build-wasm-game/"
|
||||
@echo "Run 'make serve-game' to test locally"
|
||||
|
||||
serve-game:
|
||||
@echo "Serving game build at http://localhost:8080"
|
||||
@echo "Press Ctrl+C to stop"
|
||||
@cd build-wasm-game && python3 -m http.server 8080
|
||||
|
||||
wasm-demo:
|
||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f build-wasm-demo/Makefile ]; then \
|
||||
echo "Configuring WebAssembly demo build..."; \
|
||||
mkdir -p build-wasm-demo; \
|
||||
cd build-wasm-demo && emcmake cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DMCRF_SDL2=ON \
|
||||
-DMCRF_DEMO=ON \
|
||||
-DMCRF_GAME_SHELL=ON; \
|
||||
fi
|
||||
@echo "Building McRogueFace demo for WebAssembly..."
|
||||
@emmake make -C build-wasm-demo -j$(JOBS)
|
||||
@cp web/index.html build-wasm-demo/index.html
|
||||
@echo "Demo build complete! Files in build-wasm-demo/"
|
||||
@echo "Run 'make serve-demo' to test locally"
|
||||
|
||||
serve-demo:
|
||||
@echo "Serving demo build at http://localhost:8080"
|
||||
@echo "Press Ctrl+C to stop"
|
||||
@cd build-wasm-demo && python3 -m http.server 8080
|
||||
|
||||
clean-wasm:
|
||||
@echo "Cleaning Emscripten builds..."
|
||||
@rm -rf build-emscripten build-playground build-wasm-game build-wasm-demo
|
||||
|
||||
# Current version extracted from source
|
||||
CURRENT_VERSION := $(shell grep 'MCRFPY_VERSION' src/McRogueFaceVersion.h | sed 's/.*"\(.*\)"/\1/')
|
||||
|
||||
# Release workflow: tag current version, build all packages, bump to next version
|
||||
# Usage: make version-bump NEXT_VERSION=0.2.6-prerelease-7drl2026
|
||||
version-bump:
|
||||
ifndef NEXT_VERSION
|
||||
$(error Usage: make version-bump NEXT_VERSION=x.y.z-suffix)
|
||||
endif
|
||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
# git status (clean working dir check), but ignore modules/, because building submodules dirties their subdirs
|
||||
@if [ -n "$$(git status --porcelain | grep -v modules)" ]; then \
|
||||
echo "Error: Working tree is not clean. Commit or stash changes first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "=== Releasing $(CURRENT_VERSION) ==="
|
||||
@# Idempotent tag: ok if it already points at HEAD (resuming partial run)
|
||||
@if git rev-parse "$(CURRENT_VERSION)" >/dev/null 2>&1; then \
|
||||
TAG_COMMIT=$$(git rev-parse "$(CURRENT_VERSION)^{}"); \
|
||||
HEAD_COMMIT=$$(git rev-parse HEAD); \
|
||||
if [ "$$TAG_COMMIT" != "$$HEAD_COMMIT" ]; then \
|
||||
echo "Error: Tag $(CURRENT_VERSION) already exists but points to a different commit."; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo "Tag $(CURRENT_VERSION) already exists at HEAD (resuming)."; \
|
||||
else \
|
||||
git tag "$(CURRENT_VERSION)"; \
|
||||
fi
|
||||
$(MAKE) package-linux-full
|
||||
$(MAKE) package-windows-full
|
||||
$(MAKE) wasm
|
||||
@echo "Packaging WASM build..."
|
||||
@mkdir -p dist
|
||||
cd build-emscripten && zip -r ../dist/McRogueFace-$(CURRENT_VERSION)-WASM.zip \
|
||||
mcrogueface.html mcrogueface.js mcrogueface.wasm mcrogueface.data
|
||||
@echo ""
|
||||
@echo "Bumping version: $(CURRENT_VERSION) -> $(NEXT_VERSION)"
|
||||
@sed -i 's|MCRFPY_VERSION "$(CURRENT_VERSION)"|MCRFPY_VERSION "$(NEXT_VERSION)"|' src/McRogueFaceVersion.h
|
||||
@TAGGED_HASH=$$(git rev-parse --short HEAD); \
|
||||
git add src/McRogueFaceVersion.h && \
|
||||
git commit -m "Version bump: $(CURRENT_VERSION) ($$TAGGED_HASH) -> $(NEXT_VERSION)"
|
||||
@echo ""
|
||||
@echo "=== Release $(CURRENT_VERSION) complete ==="
|
||||
@echo "Tag: $(CURRENT_VERSION)"
|
||||
@echo "Next: $(NEXT_VERSION)"
|
||||
@echo "Packages:"
|
||||
@ls -lh dist/*$(CURRENT_VERSION)* 2>/dev/null
|
||||
238
docs/ISSUE_TRIAGE_2026-04.md
Normal file
238
docs/ISSUE_TRIAGE_2026-04.md
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
# McRogueFace Issue Triage — April 2026
|
||||
|
||||
**46 open issues** across #53–#304. Grouped by system, ordered by impact.
|
||||
|
||||
---
|
||||
|
||||
## Group 1: Render Cache Dirty Flags (Bugfix Cluster)
|
||||
|
||||
**4 issues — all quick-to-moderate fixes, high user-visible impact**
|
||||
|
||||
These are systemic bugs where Python property setters bypass the render cache invalidation system (#144). They cause stale frames when using `clip_children` or `cache_subtree`. Issue #291 is the umbrella audit; the other three are specific bugs it identified.
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #291 | Audit all Python property setters for missing markDirty() calls | Medium — systematic sweep of all tp_getset setters |
|
||||
| #290 | UIDrawable base x/y/pos setters don't propagate dirty flags to parent | Quick — add markCompositeDirty() call in set_float_member() |
|
||||
| #289 | Caption Python property setters don't call markDirty() | Quick — add markDirty() to text/font_size/fill_color setters |
|
||||
| #288 | UICollection mutations don't invalidate parent Frame's render cache | Quick — add markCompositeDirty() in append/remove/etc |
|
||||
|
||||
**Dependencies:** None external. #291 depends on #288–#290 being fixed first (or done together).
|
||||
|
||||
**Recommendation: Tackle first.** These are correctness bugs affecting every user of the caching system. The fixes are mechanical (add missing dirty-flag calls), low risk, and testable. One focused session can close all four.
|
||||
|
||||
---
|
||||
|
||||
## Group 2: Grid Dangling Pointer Bugs
|
||||
|
||||
**3 issues — moderate fixes, memory safety impact**
|
||||
|
||||
All three are the same class of bug: raw `UIGrid*` pointers in child objects that dangle when the parent grid is destroyed. Part of the broader memory safety audit (#279).
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #270 | GridLayer::parent_grid dangling raw pointer | Moderate — convert to weak_ptr or add invalidation |
|
||||
| #271 | UIGridPoint::parent_grid dangling raw pointer | Moderate — same pattern as #270 |
|
||||
| #277 | GridChunk::parent_grid dangling raw pointer | Moderate — same pattern as #270 |
|
||||
|
||||
**Dependencies:** These are the last 3 unfixed bugs from the memory safety audit (#279). Fixing all three would effectively close #279.
|
||||
|
||||
**Recommendation: Tackle second.** Same fix pattern applied three times. Closes the memory safety audit chapter.
|
||||
|
||||
---
|
||||
|
||||
## Group 3: Animation System Fixes
|
||||
|
||||
**2 issues — one bugfix, one feature**
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #256 | Animation system bypasses spatial hash updates for entity position | Moderate — hook animation property changes into spatial hash |
|
||||
| #218 | Color and Vector animation targets | Minor feature — compound property animation support |
|
||||
|
||||
**Dependencies:** #256 is independent. #218 is a nice-to-have that improves DX.
|
||||
|
||||
---
|
||||
|
||||
## Group 4: Grid Layer & Rendering Fixes
|
||||
|
||||
**2 issues — quick fixes**
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #257 | Grid layers with z_index of zero are on top of entities | Quick — change `>=0` to `>0` or similar in draw order |
|
||||
| #152 | Sparse Grid Layers | Major feature — default values + sub-grid chunk optimization |
|
||||
|
||||
**Dependencies:** #257 is standalone. #152 builds on the existing layer system.
|
||||
|
||||
---
|
||||
|
||||
## Group 5: API Cleanup & Consistency
|
||||
|
||||
**1 issue — quick, blocks v1.0**
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #304 | Remove camelCase module functions before 1.0 | Quick — delete 4 method entries from mcrfpyMethods[], update tests |
|
||||
|
||||
**Dependencies:** Snake_case aliases already added. This is a breaking change gated on the 1.0 release.
|
||||
|
||||
---
|
||||
|
||||
## Group 6: Multi-Tile Entity Rendering
|
||||
|
||||
**5 issues — parent + 4 children, all tier3-future**
|
||||
|
||||
Umbrella issue #233 with four sub-issues for different approaches to entities larger than one grid cell.
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #233 | Enhance Entity rendering and positioning capabilities (parent) | Meta/tracking |
|
||||
| #234 | Entity origin offset for oversized sprites | Minor — add pixel offset to entity draw position |
|
||||
| #235 | Texture display bounds for non-uniform sprite content | Minor — support non-cell-aligned sprite regions |
|
||||
| #236 | Multi-tile entities using oversized sprites | Minor — render single large sprite across cells |
|
||||
| #237 | Multi-tile entities using composite sprites | Major — multiple sprite indices per entity |
|
||||
|
||||
**Dependencies:** #234 is the simplest starting point. #236 and #237 build on #234/#235.
|
||||
|
||||
---
|
||||
|
||||
## Group 7: Memory Safety Audit Tail
|
||||
|
||||
**9 issues — testing/tooling infrastructure for the #279 audit**
|
||||
|
||||
These are the remaining items from the 7DRL 2026 post-mortem. The actual bugs are mostly fixed; these are about preventing regressions and improving the safety toolchain.
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #279 | Engine memory safety audit — meta/tracking | Meta — close when #270/#271/#277 done |
|
||||
| #287 | Regression tests for each bug from #258–#278 | Medium — write targeted test scripts |
|
||||
| #285 | CI pipeline for debug-test and asan-test | Medium — CI/CD configuration |
|
||||
| #286 | Re-enable ASan leak detection | Tiny — remove detect_leaks=0 suppression |
|
||||
| #284 | Valgrind Massif heap profiling target | Tiny — add Makefile target |
|
||||
| #283 | Atheris fuzzing harness for Python API | Major — significant new infrastructure |
|
||||
| #282 | Install modern Clang for TSan/fuzzing | Minor — toolchain upgrade |
|
||||
| #281 | Free-threaded CPython + TSan Makefile targets | Minor — Makefile additions |
|
||||
| #280 | Instrumented libtcod debug build | Minor — rebuild libtcod with sanitizers |
|
||||
|
||||
**Dependencies:** #286 depends on #266/#275 (both closed). #287 depends on the actual bugs being fixed. #283 depends on #282.
|
||||
|
||||
---
|
||||
|
||||
## Group 8: Grid Data Model Enhancements
|
||||
|
||||
**3 issues — foundation work for game data**
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #293 | DiscreteMap serialization via bytes | Minor — add bytes() and from_bytes() to DiscreteMap |
|
||||
| #294 | Entity.gridstate as DiscreteMap reference | Minor — refactor internal representation |
|
||||
| #149 | Reduce the size of UIGrid.cpp | Refactoring — break 1400+ line file into logical units |
|
||||
|
||||
**Dependencies:** #294 depends on #293. #149 is independent refactoring.
|
||||
|
||||
---
|
||||
|
||||
## Group 9: Performance Optimization
|
||||
|
||||
**4 issues — significant effort, needs benchmarks first**
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #255 | Tracking down performance improvement opportunities | Investigation — profiling session |
|
||||
| #117 | Memory Pool for Entities | Major — custom allocator |
|
||||
| #145 | TexturePool with power-of-2 RenderTexture reuse | Major — deferred from #144 |
|
||||
| #124 | Grid Point Animation | Major — per-tile animation system, needs design |
|
||||
|
||||
**Dependencies:** #255 should be done first to identify where optimization matters. #145 builds on the dirty-flag system (#144, closed). #124 is a large standalone feature.
|
||||
|
||||
---
|
||||
|
||||
## Group 10: WASM / Playground Tooling
|
||||
|
||||
**3 issues — all tier3-future**
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #238 | Emscripten debugging infrastructure (DWARF, source maps) | Minor — build config additions |
|
||||
| #239 | Automated WASM testing with headless browser | Major — new test infrastructure |
|
||||
| #240 | Developer troubleshooting docs for WASM deployments | Documentation — write guide |
|
||||
|
||||
**Dependencies:** #238 supports #239. #240 is standalone documentation.
|
||||
|
||||
---
|
||||
|
||||
## Group 11: LLM Agent Testbed
|
||||
|
||||
**3 issues — research/demo infrastructure**
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #55 | McRogueFace as Agent Simulation Environment | Major — umbrella/vision issue |
|
||||
| #154 | Grounded Multi-Agent Testbed | Major — research infrastructure |
|
||||
| #156 | Turn-based LLM Agent Orchestration | Major — orchestration layer |
|
||||
|
||||
**Dependencies:** #156 depends on #154. Both depend on #55 conceptually. These also depend on #53 (alternative input methods) and mature API stability.
|
||||
|
||||
---
|
||||
|
||||
## Group 12: Demo Games & Tutorials
|
||||
|
||||
**2 issues — showcase/marketing**
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #248 | Crypt of Sokoban Remaster (7DRL prep) | Major — full game remaster |
|
||||
| #167 | r/roguelikedev Tutorial Series Demo Game | Major — tutorial content + demo game |
|
||||
|
||||
**Dependencies:** Both benefit from a stable, well-documented API. #167 specifically needs the API to be settled.
|
||||
|
||||
---
|
||||
|
||||
## Group 13: Platform & Architecture (Far Future)
|
||||
|
||||
**5 issues — large features, mostly deferred**
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #70 | Package mcrfpy without embedded interpreter (wheels) | Major — significant build system rework |
|
||||
| #62 | Multiple Windows | Major — architectural change |
|
||||
| #67 | Grid Stitching / infinite world prototype | Major — new rendering/data infrastructure |
|
||||
| #54 | Jupyter Notebook Interface | Major — alternative rendering target |
|
||||
| #53 | Alternative Input Methods | Major — depends on #220 |
|
||||
|
||||
---
|
||||
|
||||
## Group 14: Concurrency
|
||||
|
||||
**1 issue — deferred**
|
||||
|
||||
| Issue | Title | Difficulty |
|
||||
|-------|-------|------------|
|
||||
| #220 | Secondary Concurrency Model: Subinterpreter Support | Major — Python 3.12+ subinterpreters |
|
||||
|
||||
**Dependencies:** Depends on free-threaded CPython work (#281).
|
||||
|
||||
---
|
||||
|
||||
## Summary by Priority
|
||||
|
||||
| Priority | Groups | Issue Count | Session Estimate |
|
||||
|----------|--------|-------------|-----------------|
|
||||
| **Do now** | G1 (dirty flags), G2 (dangling ptrs) | 7 | 1 session |
|
||||
| **Do soon** | G3 (animation), G4 (grid fixes), G5 (API cleanup) | 5 | 1 session |
|
||||
| **Foundation** | G7 (safety tests), G8 (grid data) | 12 | 2-3 sessions |
|
||||
| **When ready** | G6 (multi-tile), G9 (perf), G10 (WASM) | 12 | 3-4 sessions |
|
||||
| **Future** | G11 (LLM), G12 (demos), G13 (platform), G14 (concurrency) | 10 | unbounded |
|
||||
|
||||
## Recommended First Session
|
||||
|
||||
**Groups 1 + 2: Dirty flags + dangling pointers (7 issues)**
|
||||
|
||||
Rationale:
|
||||
- All are correctness/safety bugs, not features — fixes don't need design decisions
|
||||
- Dirty flag fixes (#288-#291) share the same mechanical pattern: add missing `markDirty()` or `markCompositeDirty()` calls
|
||||
- Dangling pointer fixes (#270, #271, #277) share the same pattern: convert `UIGrid*` to `weak_ptr<UIGrid>` or add invalidation on grid destruction
|
||||
- Closing these also effectively closes the meta issue #279
|
||||
- High confidence of completing all 7 in one session
|
||||
- Clears the way for performance work (Group 9) which depends on correct caching
|
||||
|
|
@ -75,30 +75,30 @@ pos_display.fill_color = mcrfpy.Color(200, 200, 100)
|
|||
pos_display.font_size = 16
|
||||
scene.children.append(pos_display)
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
def handle_keys(key, action) -> None:
|
||||
"""Handle keyboard input to move the player.
|
||||
|
||||
Args:
|
||||
key: The key that was pressed (e.g., "W", "Up", "Space")
|
||||
action: Either "start" (key pressed) or "end" (key released)
|
||||
key: A mcrfpy.Key enum value (e.g., Key.W, Key.UP)
|
||||
action: A mcrfpy.InputState enum value (PRESSED or RELEASED)
|
||||
"""
|
||||
# Only respond to key press, not release
|
||||
if action != "start":
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
# Get current player position
|
||||
px, py = int(player.x), int(player.y)
|
||||
|
||||
# Calculate new position based on key
|
||||
if key == "W" or key == "Up":
|
||||
if key == mcrfpy.Key.W or key == mcrfpy.Key.UP:
|
||||
py -= 1 # Up decreases Y
|
||||
elif key == "S" or key == "Down":
|
||||
elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN:
|
||||
py += 1 # Down increases Y
|
||||
elif key == "A" or key == "Left":
|
||||
elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT:
|
||||
px -= 1 # Left decreases X
|
||||
elif key == "D" or key == "Right":
|
||||
elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT:
|
||||
px += 1 # Right increases X
|
||||
elif key == "Escape":
|
||||
elif key == mcrfpy.Key.ESCAPE:
|
||||
mcrfpy.exit()
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -160,9 +160,9 @@ scene.children.append(status_display)
|
|||
# Input Handling
|
||||
# =============================================================================
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
def handle_keys(key, action) -> None:
|
||||
"""Handle keyboard input with collision detection."""
|
||||
if action != "start":
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
# Get current position
|
||||
|
|
@ -171,15 +171,15 @@ def handle_keys(key: str, action: str) -> None:
|
|||
# Calculate intended new position
|
||||
new_x, new_y = px, py
|
||||
|
||||
if key == "W" or key == "Up":
|
||||
if key == mcrfpy.Key.W or key == mcrfpy.Key.UP:
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN:
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT:
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT:
|
||||
new_x += 1
|
||||
elif key == "Escape":
|
||||
elif key == mcrfpy.Key.ESCAPE:
|
||||
mcrfpy.exit()
|
||||
return
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -316,26 +316,26 @@ def regenerate_dungeon() -> None:
|
|||
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||
room_display.text = "New dungeon generated!"
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
def handle_keys(key, action) -> None:
|
||||
"""Handle keyboard input."""
|
||||
if action != "start":
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
if key == "W" or key == "Up":
|
||||
if key == mcrfpy.Key.W or key == mcrfpy.Key.UP:
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN:
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT:
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT:
|
||||
new_x += 1
|
||||
elif key == "R":
|
||||
elif key == mcrfpy.Key.R:
|
||||
regenerate_dungeon()
|
||||
return
|
||||
elif key == "Escape":
|
||||
elif key == mcrfpy.Key.ESCAPE:
|
||||
mcrfpy.exit()
|
||||
return
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -71,24 +71,9 @@ class RectangularRoom:
|
|||
# Exploration Tracking
|
||||
# =============================================================================
|
||||
|
||||
# Track which tiles have been discovered (seen at least once)
|
||||
explored: list[list[bool]] = []
|
||||
|
||||
def init_explored() -> None:
|
||||
"""Initialize the explored array to all False."""
|
||||
global explored
|
||||
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||
|
||||
def mark_explored(x: int, y: int) -> None:
|
||||
"""Mark a tile as explored."""
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
explored[y][x] = True
|
||||
|
||||
def is_explored(x: int, y: int) -> bool:
|
||||
"""Check if a tile has been explored."""
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
return explored[y][x]
|
||||
return False
|
||||
# Note: The ColorLayer's draw_fov() method tracks exploration state
|
||||
# internally - tiles that have been visible at least once are rendered
|
||||
# with the 'discovered' color. No manual tracking needed!
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon Generation (from Part 3, with transparent property)
|
||||
|
|
@ -151,7 +136,6 @@ def carve_l_tunnel(
|
|||
def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]:
|
||||
"""Generate a dungeon with rooms and tunnels."""
|
||||
fill_with_walls(grid)
|
||||
init_explored() # Reset exploration when generating new dungeon
|
||||
|
||||
rooms: list[RectangularRoom] = []
|
||||
|
||||
|
|
@ -188,28 +172,25 @@ def generate_dungeon(grid: mcrfpy.Grid) -> tuple[int, int]:
|
|||
def update_fov(grid: mcrfpy.Grid, fov_layer, player_x: int, player_y: int) -> None:
|
||||
"""Update the field of view visualization.
|
||||
|
||||
Uses the ColorLayer's built-in draw_fov() method, which computes FOV
|
||||
via libtcod and paints visibility colors in a single call. The layer
|
||||
tracks explored state automatically.
|
||||
|
||||
Args:
|
||||
grid: The game grid
|
||||
fov_layer: The ColorLayer for FOV visualization
|
||||
player_x: Player's X position
|
||||
player_y: Player's Y position
|
||||
"""
|
||||
# Compute FOV from player position
|
||||
grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||
|
||||
# Update each tile's visibility
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if grid.is_in_fov(x, y):
|
||||
# Currently visible - mark as explored and show clearly
|
||||
mark_explored(x, y)
|
||||
fov_layer.set(x, y, COLOR_VISIBLE)
|
||||
elif is_explored(x, y):
|
||||
# Previously seen but not currently visible - show dimmed
|
||||
fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||
else:
|
||||
# Never seen - hide completely
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
# draw_fov computes FOV and paints the color layer in one step.
|
||||
# It tracks explored state internally so previously-seen tiles stay dimmed.
|
||||
fov_layer.draw_fov(
|
||||
(player_x, player_y),
|
||||
radius=FOV_RADIUS,
|
||||
visible=COLOR_VISIBLE,
|
||||
discovered=COLOR_DISCOVERED,
|
||||
unknown=COLOR_UNKNOWN
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Collision Detection
|
||||
|
|
@ -244,12 +225,12 @@ grid = mcrfpy.Grid(
|
|||
player_start_x, player_start_y = generate_dungeon(grid)
|
||||
|
||||
# Add a color layer for FOV visualization (below entities)
|
||||
fov_layer = grid.add_layer("color", z_index=-1)
|
||||
# Create the layer object, then attach it to the grid
|
||||
fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov")
|
||||
grid.add_layer(fov_layer)
|
||||
|
||||
# Initialize the FOV layer to all black (unknown)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
# Create the player
|
||||
player = mcrfpy.Entity(
|
||||
|
|
@ -312,34 +293,32 @@ def regenerate_dungeon() -> None:
|
|||
player.y = new_y
|
||||
|
||||
# Reset FOV layer to unknown
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
# Calculate new FOV
|
||||
update_fov(grid, fov_layer, new_x, new_y)
|
||||
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
def handle_keys(key, action) -> None:
|
||||
"""Handle keyboard input."""
|
||||
if action != "start":
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
if key == "W" or key == "Up":
|
||||
if key == mcrfpy.Key.W or key == mcrfpy.Key.UP:
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN:
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT:
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT:
|
||||
new_x += 1
|
||||
elif key == "R":
|
||||
elif key == mcrfpy.Key.R:
|
||||
regenerate_dungeon()
|
||||
return
|
||||
elif key == "Escape":
|
||||
elif key == mcrfpy.Key.ESCAPE:
|
||||
mcrfpy.exit()
|
||||
return
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -120,23 +120,8 @@ class RectangularRoom:
|
|||
# Exploration Tracking (from Part 4)
|
||||
# =============================================================================
|
||||
|
||||
explored: list[list[bool]] = []
|
||||
|
||||
def init_explored() -> None:
|
||||
"""Initialize the explored array to all False."""
|
||||
global explored
|
||||
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||
|
||||
def mark_explored(x: int, y: int) -> None:
|
||||
"""Mark a tile as explored."""
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
explored[y][x] = True
|
||||
|
||||
def is_explored(x: int, y: int) -> bool:
|
||||
"""Check if a tile has been explored."""
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
return explored[y][x]
|
||||
return False
|
||||
# Note: The ColorLayer's draw_fov() method tracks exploration state
|
||||
# internally - no manual tracking needed!
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon Generation (from Part 4)
|
||||
|
|
@ -293,20 +278,15 @@ def clear_enemies(target_grid: mcrfpy.Grid) -> None:
|
|||
"""Remove all enemies from the grid."""
|
||||
global entity_data
|
||||
|
||||
# Get list of enemies to remove (not the player)
|
||||
# Collect enemies to remove (not the player)
|
||||
enemies_to_remove = []
|
||||
for entity in target_grid.entities:
|
||||
if entity in entity_data and not entity_data[entity].get("is_player", False):
|
||||
enemies_to_remove.append(entity)
|
||||
|
||||
# Remove from grid and entity_data
|
||||
# Remove from grid (by entity reference) and from entity_data
|
||||
for enemy in enemies_to_remove:
|
||||
# Find and remove from grid.entities
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == enemy:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
# Remove from entity_data
|
||||
target_grid.entities.remove(enemy)
|
||||
if enemy in entity_data:
|
||||
del entity_data[enemy]
|
||||
|
||||
|
|
@ -329,7 +309,7 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
|||
|
||||
# Other entities are only visible if in FOV
|
||||
ex, ey = int(entity.x), int(entity.y)
|
||||
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||
entity.visible = target_grid.is_in_fov((ex, ey))
|
||||
|
||||
# =============================================================================
|
||||
# Field of View (from Part 4)
|
||||
|
|
@ -337,21 +317,18 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
|||
|
||||
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||
"""Update the field of view visualization."""
|
||||
# Compute FOV from player position
|
||||
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||
# draw_fov computes FOV and paints the color layer in one step.
|
||||
# It also tracks explored state internally.
|
||||
target_fov_layer.draw_fov(
|
||||
(player_x, player_y),
|
||||
radius=FOV_RADIUS,
|
||||
visible=COLOR_VISIBLE,
|
||||
discovered=COLOR_DISCOVERED,
|
||||
unknown=COLOR_UNKNOWN
|
||||
)
|
||||
|
||||
# Update each tile's visibility
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if target_grid.is_in_fov(x, y):
|
||||
mark_explored(x, y)
|
||||
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||
elif is_explored(x, y):
|
||||
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||
else:
|
||||
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
|
||||
# Update entity visibility
|
||||
# Update entity visibility (draw_fov calls compute_fov internally,
|
||||
# so is_in_fov() reflects the current FOV state)
|
||||
update_entity_visibility(target_grid)
|
||||
|
||||
# =============================================================================
|
||||
|
|
@ -399,7 +376,6 @@ def generate_dungeon(target_grid: mcrfpy.Grid, texture: mcrfpy.Texture) -> tuple
|
|||
|
||||
# Fill with walls
|
||||
fill_with_walls(target_grid)
|
||||
init_explored()
|
||||
|
||||
rooms: list[RectangularRoom] = []
|
||||
|
||||
|
|
@ -454,7 +430,6 @@ grid = mcrfpy.Grid(
|
|||
|
||||
# Generate the dungeon (without player first to get starting position)
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
|
||||
rooms: list[RectangularRoom] = []
|
||||
|
||||
|
|
@ -489,10 +464,9 @@ else:
|
|||
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||
|
||||
# Add FOV layer
|
||||
fov_layer = grid.add_layer("color", z_index=-1)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov")
|
||||
grid.add_layer(fov_layer)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
# Create the player
|
||||
player = mcrfpy.Entity(
|
||||
|
|
@ -574,7 +548,6 @@ def regenerate_dungeon() -> None:
|
|||
|
||||
# Regenerate dungeon structure
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
|
||||
rooms = []
|
||||
|
||||
|
|
@ -618,37 +591,35 @@ def regenerate_dungeon() -> None:
|
|||
spawn_enemies_in_room(grid, room, texture)
|
||||
|
||||
# Reset FOV layer
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
# Update FOV
|
||||
update_fov(grid, fov_layer, new_x, new_y)
|
||||
pos_display.text = f"Position: ({new_x}, {new_y})"
|
||||
status_display.text = "New dungeon generated!"
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
def handle_keys(key, action) -> None:
|
||||
"""Handle keyboard input."""
|
||||
global player, grid, fov_layer
|
||||
|
||||
if action != "start":
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
px, py = int(player.x), int(player.y)
|
||||
new_x, new_y = px, py
|
||||
|
||||
if key == "W" or key == "Up":
|
||||
if key == mcrfpy.Key.W or key == mcrfpy.Key.UP:
|
||||
new_y -= 1
|
||||
elif key == "S" or key == "Down":
|
||||
elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN:
|
||||
new_y += 1
|
||||
elif key == "A" or key == "Left":
|
||||
elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT:
|
||||
new_x -= 1
|
||||
elif key == "D" or key == "Right":
|
||||
elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT:
|
||||
new_x += 1
|
||||
elif key == "R":
|
||||
elif key == mcrfpy.Key.R:
|
||||
regenerate_dungeon()
|
||||
return
|
||||
elif key == "Escape":
|
||||
elif key == mcrfpy.Key.ESCAPE:
|
||||
mcrfpy.exit()
|
||||
return
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -159,27 +159,7 @@ class RectangularRoom:
|
|||
self.y2 >= other.y1
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Exploration Tracking
|
||||
# =============================================================================
|
||||
|
||||
explored: list[list[bool]] = []
|
||||
|
||||
def init_explored() -> None:
|
||||
"""Initialize the explored array to all False."""
|
||||
global explored
|
||||
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||
|
||||
def mark_explored(x: int, y: int) -> None:
|
||||
"""Mark a tile as explored."""
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
explored[y][x] = True
|
||||
|
||||
def is_explored(x: int, y: int) -> bool:
|
||||
"""Check if a tile has been explored."""
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
return explored[y][x]
|
||||
return False
|
||||
# Exploration tracking is handled internally by draw_fov()
|
||||
|
||||
# =============================================================================
|
||||
# Message Log
|
||||
|
|
@ -357,9 +337,9 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc
|
|||
def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
"""Remove an entity from the grid and data storage."""
|
||||
# Find and remove from grid
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
for e in target_grid.entities:
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
target_grid.entities.remove(e)
|
||||
break
|
||||
|
||||
# Remove from entity data
|
||||
|
|
@ -487,21 +467,19 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
|||
continue
|
||||
|
||||
ex, ey = int(entity.x), int(entity.y)
|
||||
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||
entity.visible = target_grid.is_in_fov((ex, ey))
|
||||
|
||||
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||
"""Update the field of view visualization."""
|
||||
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if target_grid.is_in_fov(x, y):
|
||||
mark_explored(x, y)
|
||||
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||
elif is_explored(x, y):
|
||||
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||
else:
|
||||
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
# draw_fov computes FOV and paints the color layer in one step,
|
||||
# and tracks exploration internally.
|
||||
target_fov_layer.draw_fov(
|
||||
(player_x, player_y),
|
||||
radius=FOV_RADIUS,
|
||||
visible=COLOR_VISIBLE,
|
||||
discovered=COLOR_DISCOVERED,
|
||||
unknown=COLOR_UNKNOWN
|
||||
)
|
||||
|
||||
update_entity_visibility(target_grid)
|
||||
|
||||
|
|
@ -595,7 +573,7 @@ def enemy_turn() -> None:
|
|||
ex, ey = int(enemy.x), int(enemy.y)
|
||||
|
||||
# Only act if in player's FOV (aware of player)
|
||||
if not grid.is_in_fov(ex, ey):
|
||||
if not grid.is_in_fov((ex, ey)):
|
||||
continue
|
||||
|
||||
# Check if adjacent to player
|
||||
|
|
@ -691,7 +669,6 @@ grid = mcrfpy.Grid(
|
|||
|
||||
# Generate initial dungeon structure
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
|
||||
rooms: list[RectangularRoom] = []
|
||||
|
||||
|
|
@ -726,10 +703,9 @@ else:
|
|||
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||
|
||||
# Add FOV layer
|
||||
fov_layer = grid.add_layer("color", z_index=-1)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov")
|
||||
grid.add_layer(fov_layer)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
# Create the player
|
||||
player = mcrfpy.Entity(
|
||||
|
|
@ -826,11 +802,10 @@ def restart_game() -> None:
|
|||
|
||||
# Remove all entities from grid
|
||||
while len(grid.entities) > 0:
|
||||
grid.entities.remove(0)
|
||||
grid.entities.remove(grid.entities[0])
|
||||
|
||||
# Regenerate dungeon
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
clear_messages()
|
||||
|
||||
rooms = []
|
||||
|
|
@ -889,9 +864,7 @@ def restart_game() -> None:
|
|||
spawn_enemies_in_room(grid, room, texture)
|
||||
|
||||
# Reset FOV layer
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
# Update displays
|
||||
update_fov(grid, fov_layer, new_x, new_y)
|
||||
|
|
@ -900,19 +873,19 @@ def restart_game() -> None:
|
|||
|
||||
add_message("A new adventure begins!", mcrfpy.Color(100, 100, 255))
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
def handle_keys(key, action) -> None:
|
||||
"""Handle keyboard input."""
|
||||
global game_over
|
||||
|
||||
if action != "start":
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
# Handle restart
|
||||
if key == "R":
|
||||
if key == mcrfpy.Key.R:
|
||||
restart_game()
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
if key == mcrfpy.Key.ESCAPE:
|
||||
mcrfpy.exit()
|
||||
return
|
||||
|
||||
|
|
@ -921,13 +894,13 @@ def handle_keys(key: str, action: str) -> None:
|
|||
return
|
||||
|
||||
# Movement and attack
|
||||
if key == "W" or key == "Up":
|
||||
if key == mcrfpy.Key.W or key == mcrfpy.Key.UP:
|
||||
try_move_or_attack(0, -1)
|
||||
elif key == "S" or key == "Down":
|
||||
elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN:
|
||||
try_move_or_attack(0, 1)
|
||||
elif key == "A" or key == "Left":
|
||||
elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT:
|
||||
try_move_or_attack(-1, 0)
|
||||
elif key == "D" or key == "Right":
|
||||
elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT:
|
||||
try_move_or_attack(1, 0)
|
||||
|
||||
scene.on_key = handle_keys
|
||||
|
|
|
|||
|
|
@ -405,24 +405,7 @@ class RectangularRoom:
|
|||
self.y2 >= other.y1
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Exploration Tracking
|
||||
# =============================================================================
|
||||
|
||||
explored: list[list[bool]] = []
|
||||
|
||||
def init_explored() -> None:
|
||||
global explored
|
||||
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||
|
||||
def mark_explored(x: int, y: int) -> None:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
explored[y][x] = True
|
||||
|
||||
def is_explored(x: int, y: int) -> bool:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
return explored[y][x]
|
||||
return False
|
||||
# Exploration tracking is handled internally by draw_fov()
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon Generation
|
||||
|
|
@ -546,10 +529,7 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc
|
|||
return None
|
||||
|
||||
def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
if entity in entity_data:
|
||||
del entity_data[entity]
|
||||
|
||||
|
|
@ -633,20 +613,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
|||
continue
|
||||
|
||||
ex, ey = int(entity.x), int(entity.y)
|
||||
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||
entity.visible = target_grid.is_in_fov((ex, ey))
|
||||
|
||||
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if target_grid.is_in_fov(x, y):
|
||||
mark_explored(x, y)
|
||||
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||
elif is_explored(x, y):
|
||||
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||
else:
|
||||
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
target_fov_layer.draw_fov(
|
||||
(player_x, player_y),
|
||||
radius=FOV_RADIUS,
|
||||
visible=COLOR_VISIBLE,
|
||||
discovered=COLOR_DISCOVERED,
|
||||
unknown=COLOR_UNKNOWN
|
||||
)
|
||||
|
||||
update_entity_visibility(target_grid)
|
||||
|
||||
|
|
@ -719,7 +695,7 @@ def enemy_turn() -> None:
|
|||
|
||||
ex, ey = int(enemy.x), int(enemy.y)
|
||||
|
||||
if not grid.is_in_fov(ex, ey):
|
||||
if not grid.is_in_fov((ex, ey)):
|
||||
continue
|
||||
|
||||
dx = player_x - ex
|
||||
|
|
@ -791,7 +767,6 @@ grid = mcrfpy.Grid(
|
|||
|
||||
# Generate initial dungeon structure
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
|
||||
rooms: list[RectangularRoom] = []
|
||||
|
||||
|
|
@ -826,10 +801,9 @@ else:
|
|||
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||
|
||||
# Add FOV layer
|
||||
fov_layer = grid.add_layer("color", z_index=-1)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov")
|
||||
grid.add_layer(fov_layer)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
# Create the player
|
||||
player = mcrfpy.Entity(
|
||||
|
|
@ -930,11 +904,11 @@ def restart_game() -> None:
|
|||
|
||||
entity_data.clear()
|
||||
|
||||
while len(grid.entities) > 0:
|
||||
grid.entities.remove(0)
|
||||
entities_to_clear = list(grid.entities)
|
||||
for e in entities_to_clear:
|
||||
grid.entities.remove(e)
|
||||
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
message_log.clear()
|
||||
|
||||
rooms = []
|
||||
|
|
@ -989,9 +963,7 @@ def restart_game() -> None:
|
|||
continue
|
||||
spawn_enemies_in_room(grid, room, texture)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
update_fov(grid, fov_layer, new_x, new_y)
|
||||
|
||||
|
|
@ -999,30 +971,30 @@ def restart_game() -> None:
|
|||
|
||||
update_ui()
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
def handle_keys(key, action) -> None:
|
||||
global game_over
|
||||
|
||||
if action != "start":
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
if key == "R":
|
||||
if key == mcrfpy.Key.R:
|
||||
restart_game()
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
if key == mcrfpy.Key.ESCAPE:
|
||||
mcrfpy.exit()
|
||||
return
|
||||
|
||||
if game_over:
|
||||
return
|
||||
|
||||
if key == "W" or key == "Up":
|
||||
if key == mcrfpy.Key.W or key == mcrfpy.Key.UP:
|
||||
try_move_or_attack(0, -1)
|
||||
elif key == "S" or key == "Down":
|
||||
elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN:
|
||||
try_move_or_attack(0, 1)
|
||||
elif key == "A" or key == "Left":
|
||||
elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT:
|
||||
try_move_or_attack(-1, 0)
|
||||
elif key == "D" or key == "Right":
|
||||
elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT:
|
||||
try_move_or_attack(1, 0)
|
||||
|
||||
scene.on_key = handle_keys
|
||||
|
|
|
|||
|
|
@ -436,24 +436,7 @@ class RectangularRoom:
|
|||
self.y2 >= other.y1
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Exploration Tracking
|
||||
# =============================================================================
|
||||
|
||||
explored: list[list[bool]] = []
|
||||
|
||||
def init_explored() -> None:
|
||||
global explored
|
||||
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||
|
||||
def mark_explored(x: int, y: int) -> None:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
explored[y][x] = True
|
||||
|
||||
def is_explored(x: int, y: int) -> bool:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
return explored[y][x]
|
||||
return False
|
||||
# Exploration tracking is handled internally by draw_fov()
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon Generation
|
||||
|
|
@ -711,10 +694,7 @@ def use_item(index: int) -> bool:
|
|||
|
||||
def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
"""Remove an item entity from the grid and item_data."""
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
|
||||
if entity in item_data:
|
||||
del item_data[entity]
|
||||
|
|
@ -751,10 +731,7 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc
|
|||
return None
|
||||
|
||||
def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
if entity in entity_data:
|
||||
del entity_data[entity]
|
||||
|
||||
|
|
@ -775,10 +752,7 @@ def clear_all_entities(target_grid: mcrfpy.Grid) -> None:
|
|||
if entity in item_data:
|
||||
del item_data[entity]
|
||||
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
|
||||
# =============================================================================
|
||||
# Combat System
|
||||
|
|
@ -852,20 +826,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
|||
continue
|
||||
|
||||
ex, ey = int(entity.x), int(entity.y)
|
||||
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||
entity.visible = target_grid.is_in_fov((ex, ey))
|
||||
|
||||
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if target_grid.is_in_fov(x, y):
|
||||
mark_explored(x, y)
|
||||
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||
elif is_explored(x, y):
|
||||
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||
else:
|
||||
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
target_fov_layer.draw_fov(
|
||||
(player_x, player_y),
|
||||
radius=FOV_RADIUS,
|
||||
visible=COLOR_VISIBLE,
|
||||
discovered=COLOR_DISCOVERED,
|
||||
unknown=COLOR_UNKNOWN
|
||||
)
|
||||
|
||||
update_entity_visibility(target_grid)
|
||||
|
||||
|
|
@ -938,7 +908,7 @@ def enemy_turn() -> None:
|
|||
|
||||
ex, ey = int(enemy.x), int(enemy.y)
|
||||
|
||||
if not grid.is_in_fov(ex, ey):
|
||||
if not grid.is_in_fov((ex, ey)):
|
||||
continue
|
||||
|
||||
dx = player_x - ex
|
||||
|
|
@ -1012,7 +982,6 @@ grid = mcrfpy.Grid(
|
|||
|
||||
# Generate initial dungeon
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
|
||||
rooms: list[RectangularRoom] = []
|
||||
|
||||
|
|
@ -1047,10 +1016,9 @@ else:
|
|||
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||
|
||||
# Add FOV layer
|
||||
fov_layer = grid.add_layer("color", z_index=-1)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov")
|
||||
grid.add_layer(fov_layer)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
# Create the player
|
||||
player = mcrfpy.Entity(
|
||||
|
|
@ -1156,11 +1124,10 @@ def restart_game() -> None:
|
|||
entity_data.clear()
|
||||
item_data.clear()
|
||||
|
||||
while len(grid.entities) > 0:
|
||||
grid.entities.remove(0)
|
||||
for e in list(grid.entities):
|
||||
grid.entities.remove(e)
|
||||
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
message_log.clear()
|
||||
|
||||
rooms = []
|
||||
|
|
@ -1219,9 +1186,7 @@ def restart_game() -> None:
|
|||
spawn_enemies_in_room(grid, room, texture)
|
||||
spawn_items_in_room(grid, room, texture)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
update_fov(grid, fov_layer, new_x, new_y)
|
||||
|
||||
|
|
@ -1229,17 +1194,17 @@ def restart_game() -> None:
|
|||
|
||||
update_ui()
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
def handle_keys(key, action) -> None:
|
||||
global game_over
|
||||
|
||||
if action != "start":
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
if key == "R":
|
||||
if key == mcrfpy.Key.R:
|
||||
restart_game()
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
if key == mcrfpy.Key.ESCAPE:
|
||||
mcrfpy.exit()
|
||||
return
|
||||
|
||||
|
|
@ -1247,20 +1212,20 @@ def handle_keys(key: str, action: str) -> None:
|
|||
return
|
||||
|
||||
# Movement
|
||||
if key == "W" or key == "Up":
|
||||
if key == mcrfpy.Key.W or key == mcrfpy.Key.UP:
|
||||
try_move_or_attack(0, -1)
|
||||
elif key == "S" or key == "Down":
|
||||
elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN:
|
||||
try_move_or_attack(0, 1)
|
||||
elif key == "A" or key == "Left":
|
||||
elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT:
|
||||
try_move_or_attack(-1, 0)
|
||||
elif key == "D" or key == "Right":
|
||||
elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT:
|
||||
try_move_or_attack(1, 0)
|
||||
# Pickup
|
||||
elif key == "G" or key == ",":
|
||||
elif key == mcrfpy.Key.G:
|
||||
pickup_item()
|
||||
# Use items by number key
|
||||
elif key in ["1", "2", "3", "4", "5"]:
|
||||
index = int(key) - 1
|
||||
elif key in [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5]:
|
||||
index = [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5].index(key)
|
||||
if use_item(index):
|
||||
enemy_turn() # Using an item takes a turn
|
||||
update_ui()
|
||||
|
|
|
|||
|
|
@ -457,24 +457,7 @@ class RectangularRoom:
|
|||
self.y2 >= other.y1
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Exploration Tracking
|
||||
# =============================================================================
|
||||
|
||||
explored: list[list[bool]] = []
|
||||
|
||||
def init_explored() -> None:
|
||||
global explored
|
||||
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||
|
||||
def mark_explored(x: int, y: int) -> None:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
explored[y][x] = True
|
||||
|
||||
def is_explored(x: int, y: int) -> bool:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
return explored[y][x]
|
||||
return False
|
||||
# Exploration tracking is handled internally by draw_fov()
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon Generation
|
||||
|
|
@ -635,18 +618,12 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc
|
|||
return None
|
||||
|
||||
def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
if entity in entity_data:
|
||||
del entity_data[entity]
|
||||
|
||||
def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
if entity in item_data:
|
||||
del item_data[entity]
|
||||
|
||||
|
|
@ -666,10 +643,7 @@ def clear_all_entities(target_grid: mcrfpy.Grid) -> None:
|
|||
if entity in item_data:
|
||||
del item_data[entity]
|
||||
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
|
||||
# =============================================================================
|
||||
# Targeting System
|
||||
|
|
@ -701,11 +675,7 @@ def exit_targeting_mode() -> None:
|
|||
global game_mode, target_cursor, grid
|
||||
|
||||
if target_cursor is not None:
|
||||
# Remove cursor from grid
|
||||
for i, e in enumerate(grid.entities):
|
||||
if e == target_cursor:
|
||||
grid.entities.remove(i)
|
||||
break
|
||||
grid.entities.remove(target_cursor)
|
||||
target_cursor = None
|
||||
|
||||
game_mode = GameMode.NORMAL
|
||||
|
|
@ -723,7 +693,7 @@ def move_cursor(dx: int, dy: int) -> None:
|
|||
return
|
||||
|
||||
# Check if position is in FOV (can only target visible tiles)
|
||||
if not grid.is_in_fov(new_x, new_y):
|
||||
if not grid.is_in_fov((new_x, new_y)):
|
||||
message_log.add("You cannot see that location.", COLOR_INVALID)
|
||||
return
|
||||
|
||||
|
|
@ -936,20 +906,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
|||
continue
|
||||
|
||||
ex, ey = int(entity.x), int(entity.y)
|
||||
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||
entity.visible = target_grid.is_in_fov((ex, ey))
|
||||
|
||||
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if target_grid.is_in_fov(x, y):
|
||||
mark_explored(x, y)
|
||||
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||
elif is_explored(x, y):
|
||||
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||
else:
|
||||
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
target_fov_layer.draw_fov(
|
||||
(player_x, player_y),
|
||||
radius=FOV_RADIUS,
|
||||
visible=COLOR_VISIBLE,
|
||||
discovered=COLOR_DISCOVERED,
|
||||
unknown=COLOR_UNKNOWN
|
||||
)
|
||||
|
||||
update_entity_visibility(target_grid)
|
||||
|
||||
|
|
@ -1022,7 +988,7 @@ def enemy_turn() -> None:
|
|||
|
||||
ex, ey = int(enemy.x), int(enemy.y)
|
||||
|
||||
if not grid.is_in_fov(ex, ey):
|
||||
if not grid.is_in_fov((ex, ey)):
|
||||
continue
|
||||
|
||||
dx = player_x - ex
|
||||
|
|
@ -1095,7 +1061,6 @@ grid = mcrfpy.Grid(
|
|||
|
||||
# Generate initial dungeon
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
|
||||
rooms: list[RectangularRoom] = []
|
||||
|
||||
|
|
@ -1130,10 +1095,9 @@ else:
|
|||
player_start_x, player_start_y = GRID_WIDTH // 2, GRID_HEIGHT // 2
|
||||
|
||||
# Add FOV layer
|
||||
fov_layer = grid.add_layer("color", z_index=-1)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov")
|
||||
grid.add_layer(fov_layer)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
# Create the player
|
||||
player = mcrfpy.Entity(
|
||||
|
|
@ -1247,11 +1211,10 @@ def restart_game() -> None:
|
|||
entity_data.clear()
|
||||
item_data.clear()
|
||||
|
||||
while len(grid.entities) > 0:
|
||||
grid.entities.remove(0)
|
||||
for e in list(grid.entities):
|
||||
grid.entities.remove(e)
|
||||
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
message_log.clear()
|
||||
|
||||
rooms = []
|
||||
|
|
@ -1309,9 +1272,7 @@ def restart_game() -> None:
|
|||
spawn_enemies_in_room(grid, room, texture)
|
||||
spawn_items_in_room(grid, room, texture)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
update_fov(grid, fov_layer, new_x, new_y)
|
||||
|
||||
|
|
@ -1320,18 +1281,18 @@ def restart_game() -> None:
|
|||
mode_display.update(game_mode)
|
||||
update_ui()
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
def handle_keys(key, action) -> None:
|
||||
global game_over, game_mode
|
||||
|
||||
if action != "start":
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
# Always allow restart and quit
|
||||
if key == "R":
|
||||
if key == mcrfpy.Key.R:
|
||||
restart_game()
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
if key == mcrfpy.Key.ESCAPE:
|
||||
if game_mode == GameMode.TARGETING:
|
||||
exit_targeting_mode()
|
||||
message_log.add("Targeting cancelled.", COLOR_INFO)
|
||||
|
|
@ -1349,41 +1310,41 @@ def handle_keys(key: str, action: str) -> None:
|
|||
else:
|
||||
handle_normal_input(key)
|
||||
|
||||
def handle_normal_input(key: str) -> None:
|
||||
def handle_normal_input(key) -> None:
|
||||
"""Handle input in normal game mode."""
|
||||
# Movement
|
||||
if key == "W" or key == "Up":
|
||||
if key == mcrfpy.Key.W or key == mcrfpy.Key.UP:
|
||||
try_move_or_attack(0, -1)
|
||||
elif key == "S" or key == "Down":
|
||||
elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN:
|
||||
try_move_or_attack(0, 1)
|
||||
elif key == "A" or key == "Left":
|
||||
elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT:
|
||||
try_move_or_attack(-1, 0)
|
||||
elif key == "D" or key == "Right":
|
||||
elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT:
|
||||
try_move_or_attack(1, 0)
|
||||
# Ranged attack (enter targeting mode)
|
||||
elif key == "F":
|
||||
elif key == mcrfpy.Key.F:
|
||||
enter_targeting_mode()
|
||||
# Pickup
|
||||
elif key == "G" or key == ",":
|
||||
elif key == mcrfpy.Key.G:
|
||||
pickup_item()
|
||||
# Use items
|
||||
elif key in ["1", "2", "3", "4", "5"]:
|
||||
index = int(key) - 1
|
||||
elif key in [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5]:
|
||||
index = [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5].index(key)
|
||||
if use_item(index):
|
||||
enemy_turn()
|
||||
update_ui()
|
||||
|
||||
def handle_targeting_input(key: str) -> None:
|
||||
def handle_targeting_input(key) -> None:
|
||||
"""Handle input in targeting mode."""
|
||||
if key == "Up" or key == "W":
|
||||
if key == mcrfpy.Key.UP or key == mcrfpy.Key.W:
|
||||
move_cursor(0, -1)
|
||||
elif key == "Down" or key == "S":
|
||||
elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S:
|
||||
move_cursor(0, 1)
|
||||
elif key == "Left" or key == "A":
|
||||
elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A:
|
||||
move_cursor(-1, 0)
|
||||
elif key == "Right" or key == "D":
|
||||
elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D:
|
||||
move_cursor(1, 0)
|
||||
elif key == "Return" or key == "Space":
|
||||
elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE:
|
||||
confirm_target()
|
||||
|
||||
scene.on_key = handle_keys
|
||||
|
|
|
|||
|
|
@ -511,24 +511,7 @@ class RectangularRoom:
|
|||
self.y2 >= other.y1
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Exploration Tracking
|
||||
# =============================================================================
|
||||
|
||||
explored: list[list[bool]] = []
|
||||
|
||||
def init_explored() -> None:
|
||||
global explored
|
||||
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||
|
||||
def mark_explored(x: int, y: int) -> None:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
explored[y][x] = True
|
||||
|
||||
def is_explored(x: int, y: int) -> bool:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
return explored[y][x]
|
||||
return False
|
||||
# Exploration tracking is handled internally by draw_fov()
|
||||
|
||||
# =============================================================================
|
||||
# Save/Load System
|
||||
|
|
@ -540,7 +523,7 @@ def save_game() -> bool:
|
|||
Returns:
|
||||
True if save succeeded, False otherwise
|
||||
"""
|
||||
global player, player_inventory, grid, explored, dungeon_level
|
||||
global player, player_inventory, grid, dungeon_level
|
||||
|
||||
try:
|
||||
# Collect tile data
|
||||
|
|
@ -592,7 +575,6 @@ def save_game() -> bool:
|
|||
"inventory": player_inventory.to_dict()
|
||||
},
|
||||
"tiles": tiles,
|
||||
"explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)],
|
||||
"enemies": enemies,
|
||||
"items": items_on_ground
|
||||
}
|
||||
|
|
@ -615,7 +597,7 @@ def load_game() -> bool:
|
|||
Returns:
|
||||
True if load succeeded, False otherwise
|
||||
"""
|
||||
global player, player_inventory, grid, explored, dungeon_level
|
||||
global player, player_inventory, grid, dungeon_level
|
||||
global entity_data, item_data, fov_layer, game_over
|
||||
|
||||
if not os.path.exists(SAVE_FILE):
|
||||
|
|
@ -629,8 +611,8 @@ def load_game() -> bool:
|
|||
entity_data.clear()
|
||||
item_data.clear()
|
||||
|
||||
while len(grid.entities) > 0:
|
||||
grid.entities.remove(0)
|
||||
for e in list(grid.entities):
|
||||
grid.entities.remove(e)
|
||||
|
||||
# Restore dungeon level
|
||||
dungeon_level = save_data.get("dungeon_level", 1)
|
||||
|
|
@ -645,10 +627,7 @@ def load_game() -> bool:
|
|||
cell.walkable = tile_data["walkable"]
|
||||
cell.transparent = tile_data["transparent"]
|
||||
|
||||
# Restore explored state
|
||||
global explored
|
||||
explored_data = save_data["explored"]
|
||||
explored = [[explored_data[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)]
|
||||
# Exploration state is tracked internally by draw_fov()
|
||||
|
||||
# Restore player
|
||||
player_data = save_data["player"]
|
||||
|
|
@ -695,9 +674,7 @@ def load_game() -> bool:
|
|||
item_data[item_entity] = Item.from_dict(item_entry["item"])
|
||||
|
||||
# Reset FOV layer
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
# Compute initial FOV
|
||||
update_fov(grid, fov_layer, int(player.x), int(player.y))
|
||||
|
|
@ -890,18 +867,12 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc
|
|||
return None
|
||||
|
||||
def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
if entity in entity_data:
|
||||
del entity_data[entity]
|
||||
|
||||
def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
if entity in item_data:
|
||||
del item_data[entity]
|
||||
|
||||
|
|
@ -931,10 +902,7 @@ def exit_targeting_mode() -> None:
|
|||
global game_mode, target_cursor, grid
|
||||
|
||||
if target_cursor is not None:
|
||||
for i, e in enumerate(grid.entities):
|
||||
if e == target_cursor:
|
||||
grid.entities.remove(i)
|
||||
break
|
||||
grid.entities.remove(target_cursor)
|
||||
target_cursor = None
|
||||
|
||||
game_mode = GameMode.NORMAL
|
||||
|
|
@ -949,7 +917,7 @@ def move_cursor(dx: int, dy: int) -> None:
|
|||
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
|
||||
return
|
||||
|
||||
if not grid.is_in_fov(new_x, new_y):
|
||||
if not grid.is_in_fov((new_x, new_y)):
|
||||
message_log.add("You cannot see that location.", COLOR_INVALID)
|
||||
return
|
||||
|
||||
|
|
@ -1144,20 +1112,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
|||
continue
|
||||
|
||||
ex, ey = int(entity.x), int(entity.y)
|
||||
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||
entity.visible = target_grid.is_in_fov((ex, ey))
|
||||
|
||||
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if target_grid.is_in_fov(x, y):
|
||||
mark_explored(x, y)
|
||||
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||
elif is_explored(x, y):
|
||||
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||
else:
|
||||
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
target_fov_layer.draw_fov(
|
||||
(player_x, player_y),
|
||||
radius=FOV_RADIUS,
|
||||
visible=COLOR_VISIBLE,
|
||||
discovered=COLOR_DISCOVERED,
|
||||
unknown=COLOR_UNKNOWN
|
||||
)
|
||||
|
||||
update_entity_visibility(target_grid)
|
||||
|
||||
|
|
@ -1230,7 +1194,7 @@ def enemy_turn() -> None:
|
|||
|
||||
ex, ey = int(enemy.x), int(enemy.y)
|
||||
|
||||
if not grid.is_in_fov(ex, ey):
|
||||
if not grid.is_in_fov((ex, ey)):
|
||||
continue
|
||||
|
||||
dx = player_x - ex
|
||||
|
|
@ -1299,11 +1263,10 @@ def generate_new_game() -> None:
|
|||
entity_data.clear()
|
||||
item_data.clear()
|
||||
|
||||
while len(grid.entities) > 0:
|
||||
grid.entities.remove(0)
|
||||
for e in list(grid.entities):
|
||||
grid.entities.remove(e)
|
||||
|
||||
fill_with_walls(grid)
|
||||
init_explored()
|
||||
message_log.clear()
|
||||
|
||||
rooms: list[RectangularRoom] = []
|
||||
|
|
@ -1361,9 +1324,7 @@ def generate_new_game() -> None:
|
|||
spawn_enemies_in_room(grid, room, texture)
|
||||
spawn_items_in_room(grid, room, texture)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
update_fov(grid, fov_layer, new_x, new_y)
|
||||
|
||||
|
|
@ -1390,10 +1351,9 @@ grid = mcrfpy.Grid(
|
|||
)
|
||||
|
||||
# Add FOV layer
|
||||
fov_layer = grid.add_layer("color", z_index=-1)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov")
|
||||
grid.add_layer(fov_layer)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
# Add grid to scene
|
||||
scene.children.append(grid)
|
||||
|
|
@ -1456,9 +1416,6 @@ message_log.add_to_scene(scene)
|
|||
# Initialize Game (Load or New)
|
||||
# =============================================================================
|
||||
|
||||
# Initialize explored array
|
||||
init_explored()
|
||||
|
||||
# Try to load existing save, otherwise generate new game
|
||||
if has_save_file():
|
||||
message_log.add("Found saved game. Loading...", COLOR_INFO)
|
||||
|
|
@ -1475,20 +1432,20 @@ else:
|
|||
# Input Handling
|
||||
# =============================================================================
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
def handle_keys(key, action) -> None:
|
||||
global game_over, game_mode
|
||||
|
||||
if action != "start":
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
# Always allow restart
|
||||
if key == "R":
|
||||
if key == mcrfpy.Key.R:
|
||||
delete_save()
|
||||
generate_new_game()
|
||||
message_log.add("A new adventure begins!", COLOR_INFO)
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
if key == mcrfpy.Key.ESCAPE:
|
||||
if game_mode == GameMode.TARGETING:
|
||||
exit_targeting_mode()
|
||||
message_log.add("Targeting cancelled.", COLOR_INFO)
|
||||
|
|
@ -1500,14 +1457,8 @@ def handle_keys(key: str, action: str) -> None:
|
|||
mcrfpy.exit()
|
||||
return
|
||||
|
||||
# Save game (Ctrl+S or just S when not moving)
|
||||
if key == "S" and game_mode == GameMode.NORMAL:
|
||||
# Check if this is meant to be a save (could add modifier check)
|
||||
# For simplicity, we will use a dedicated save key
|
||||
pass
|
||||
|
||||
# Dedicated save with period key
|
||||
if key == "Period" and game_mode == GameMode.NORMAL and not game_over:
|
||||
if key == mcrfpy.Key.PERIOD and game_mode == GameMode.NORMAL and not game_over:
|
||||
save_game()
|
||||
return
|
||||
|
||||
|
|
@ -1520,39 +1471,39 @@ def handle_keys(key: str, action: str) -> None:
|
|||
else:
|
||||
handle_normal_input(key)
|
||||
|
||||
def handle_normal_input(key: str) -> None:
|
||||
def handle_normal_input(key) -> None:
|
||||
# Movement
|
||||
if key == "W" or key == "Up":
|
||||
if key == mcrfpy.Key.W or key == mcrfpy.Key.UP:
|
||||
try_move_or_attack(0, -1)
|
||||
elif key == "S" or key == "Down":
|
||||
elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN:
|
||||
try_move_or_attack(0, 1)
|
||||
elif key == "A" or key == "Left":
|
||||
elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT:
|
||||
try_move_or_attack(-1, 0)
|
||||
elif key == "D" or key == "Right":
|
||||
elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT:
|
||||
try_move_or_attack(1, 0)
|
||||
# Ranged attack
|
||||
elif key == "F":
|
||||
elif key == mcrfpy.Key.F:
|
||||
enter_targeting_mode()
|
||||
# Pickup
|
||||
elif key == "G" or key == ",":
|
||||
elif key == mcrfpy.Key.G:
|
||||
pickup_item()
|
||||
# Use items
|
||||
elif key in ["1", "2", "3", "4", "5"]:
|
||||
index = int(key) - 1
|
||||
elif key in [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5]:
|
||||
index = [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5].index(key)
|
||||
if use_item(index):
|
||||
enemy_turn()
|
||||
update_ui()
|
||||
|
||||
def handle_targeting_input(key: str) -> None:
|
||||
if key == "Up" or key == "W":
|
||||
def handle_targeting_input(key) -> None:
|
||||
if key == mcrfpy.Key.UP or key == mcrfpy.Key.W:
|
||||
move_cursor(0, -1)
|
||||
elif key == "Down" or key == "S":
|
||||
elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S:
|
||||
move_cursor(0, 1)
|
||||
elif key == "Left" or key == "A":
|
||||
elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A:
|
||||
move_cursor(-1, 0)
|
||||
elif key == "Right" or key == "D":
|
||||
elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D:
|
||||
move_cursor(1, 0)
|
||||
elif key == "Return" or key == "Space":
|
||||
elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE:
|
||||
confirm_target()
|
||||
|
||||
scene.on_key = handle_keys
|
||||
|
|
|
|||
|
|
@ -567,24 +567,7 @@ class RectangularRoom:
|
|||
self.y2 >= other.y1
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Exploration Tracking
|
||||
# =============================================================================
|
||||
|
||||
explored: list[list[bool]] = []
|
||||
|
||||
def init_explored() -> None:
|
||||
global explored
|
||||
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||
|
||||
def mark_explored(x: int, y: int) -> None:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
explored[y][x] = True
|
||||
|
||||
def is_explored(x: int, y: int) -> bool:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
return explored[y][x]
|
||||
return False
|
||||
# Exploration tracking is handled automatically by ColorLayer.draw_fov()
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon Generation
|
||||
|
|
@ -821,18 +804,12 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc
|
|||
return None
|
||||
|
||||
def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
if entity in entity_data:
|
||||
del entity_data[entity]
|
||||
|
||||
def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
if entity in item_data:
|
||||
del item_data[entity]
|
||||
|
||||
|
|
@ -851,11 +828,7 @@ def clear_entities_except_player(target_grid: mcrfpy.Grid) -> None:
|
|||
del entity_data[entity]
|
||||
if entity in item_data:
|
||||
del item_data[entity]
|
||||
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
|
||||
# =============================================================================
|
||||
# Level Transition
|
||||
|
|
@ -883,7 +856,6 @@ def descend_stairs() -> bool:
|
|||
clear_entities_except_player(grid)
|
||||
|
||||
# Generate new dungeon
|
||||
init_explored()
|
||||
player_start = generate_dungeon(grid, dungeon_level)
|
||||
|
||||
# Move player to starting position
|
||||
|
|
@ -896,9 +868,7 @@ def descend_stairs() -> bool:
|
|||
spawn_entities_for_level(grid, texture, dungeon_level)
|
||||
|
||||
# Reset FOV
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
update_fov(grid, fov_layer, player_start[0], player_start[1])
|
||||
|
||||
|
|
@ -977,7 +947,7 @@ def spawn_entities_for_level(target_grid: mcrfpy.Grid, tex: mcrfpy.Texture, leve
|
|||
|
||||
def save_game() -> bool:
|
||||
"""Save the current game state to a JSON file."""
|
||||
global player, player_inventory, grid, explored, dungeon_level, stairs_position
|
||||
global player, player_inventory, grid, dungeon_level, stairs_position
|
||||
|
||||
try:
|
||||
tiles = []
|
||||
|
|
@ -1026,7 +996,6 @@ def save_game() -> bool:
|
|||
"inventory": player_inventory.to_dict()
|
||||
},
|
||||
"tiles": tiles,
|
||||
"explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)],
|
||||
"enemies": enemies,
|
||||
"items": items_on_ground
|
||||
}
|
||||
|
|
@ -1043,7 +1012,7 @@ def save_game() -> bool:
|
|||
|
||||
def load_game() -> bool:
|
||||
"""Load a saved game from JSON file."""
|
||||
global player, player_inventory, grid, explored, dungeon_level
|
||||
global player, player_inventory, grid, dungeon_level
|
||||
global entity_data, item_data, fov_layer, game_over, stairs_position
|
||||
|
||||
if not os.path.exists(SAVE_FILE):
|
||||
|
|
@ -1057,7 +1026,7 @@ def load_game() -> bool:
|
|||
item_data.clear()
|
||||
|
||||
while len(grid.entities) > 0:
|
||||
grid.entities.remove(0)
|
||||
grid.entities.pop(0)
|
||||
|
||||
dungeon_level = save_data.get("dungeon_level", 1)
|
||||
stairs_position = tuple(save_data.get("stairs_position", [0, 0]))
|
||||
|
|
@ -1071,10 +1040,6 @@ def load_game() -> bool:
|
|||
cell.walkable = tile_data["walkable"]
|
||||
cell.transparent = tile_data["transparent"]
|
||||
|
||||
global explored
|
||||
explored_data = save_data["explored"]
|
||||
explored = [[explored_data[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)]
|
||||
|
||||
player_data = save_data["player"]
|
||||
player = mcrfpy.Entity(
|
||||
grid_pos=(player_data["x"], player_data["y"]),
|
||||
|
|
@ -1114,9 +1079,7 @@ def load_game() -> bool:
|
|||
grid.entities.append(item_entity)
|
||||
item_data[item_entity] = Item.from_dict(item_entry["item"])
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
update_fov(grid, fov_layer, int(player.x), int(player.y))
|
||||
|
||||
|
|
@ -1168,10 +1131,7 @@ def exit_targeting_mode() -> None:
|
|||
global game_mode, target_cursor, grid
|
||||
|
||||
if target_cursor is not None:
|
||||
for i, e in enumerate(grid.entities):
|
||||
if e == target_cursor:
|
||||
grid.entities.remove(i)
|
||||
break
|
||||
grid.entities.remove(target_cursor)
|
||||
target_cursor = None
|
||||
|
||||
game_mode = GameMode.NORMAL
|
||||
|
|
@ -1186,7 +1146,7 @@ def move_cursor(dx: int, dy: int) -> None:
|
|||
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
|
||||
return
|
||||
|
||||
if not grid.is_in_fov(new_x, new_y):
|
||||
if not grid.is_in_fov((new_x, new_y)):
|
||||
message_log.add("You cannot see that location.", COLOR_INVALID)
|
||||
return
|
||||
|
||||
|
|
@ -1380,21 +1340,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
|||
continue
|
||||
|
||||
ex, ey = int(entity.x), int(entity.y)
|
||||
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||
entity.visible = target_grid.is_in_fov((ex, ey))
|
||||
|
||||
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if target_grid.is_in_fov(x, y):
|
||||
mark_explored(x, y)
|
||||
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||
elif is_explored(x, y):
|
||||
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||
else:
|
||||
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
|
||||
target_fov_layer.draw_fov(
|
||||
(player_x, player_y),
|
||||
radius=FOV_RADIUS,
|
||||
visible=COLOR_VISIBLE,
|
||||
discovered=COLOR_DISCOVERED,
|
||||
unknown=COLOR_UNKNOWN
|
||||
)
|
||||
update_entity_visibility(target_grid)
|
||||
|
||||
# =============================================================================
|
||||
|
|
@ -1466,7 +1421,7 @@ def enemy_turn() -> None:
|
|||
|
||||
ex, ey = int(enemy.x), int(enemy.y)
|
||||
|
||||
if not grid.is_in_fov(ex, ey):
|
||||
if not grid.is_in_fov((ex, ey)):
|
||||
continue
|
||||
|
||||
dx = player_x - ex
|
||||
|
|
@ -1535,9 +1490,8 @@ def generate_new_game() -> None:
|
|||
item_data.clear()
|
||||
|
||||
while len(grid.entities) > 0:
|
||||
grid.entities.remove(0)
|
||||
grid.entities.pop(0)
|
||||
|
||||
init_explored()
|
||||
message_log.clear()
|
||||
|
||||
player_start = generate_dungeon(grid, dungeon_level)
|
||||
|
|
@ -1562,9 +1516,7 @@ def generate_new_game() -> None:
|
|||
|
||||
spawn_entities_for_level(grid, texture, dungeon_level)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
update_fov(grid, fov_layer, player_start[0], player_start[1])
|
||||
|
||||
|
|
@ -1587,10 +1539,9 @@ grid = mcrfpy.Grid(
|
|||
zoom=1.0
|
||||
)
|
||||
|
||||
fov_layer = grid.add_layer("color", z_index=-1)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov")
|
||||
grid.add_layer(fov_layer)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
scene.children.append(grid)
|
||||
|
||||
|
|
@ -1639,8 +1590,6 @@ message_log.add_to_scene(scene)
|
|||
# Initialize Game
|
||||
# =============================================================================
|
||||
|
||||
init_explored()
|
||||
|
||||
if has_save_file():
|
||||
message_log.add("Found saved game. Loading...", COLOR_INFO)
|
||||
if not load_game():
|
||||
|
|
@ -1656,19 +1605,19 @@ else:
|
|||
# Input Handling
|
||||
# =============================================================================
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
def handle_keys(key, action) -> None:
|
||||
global game_over, game_mode
|
||||
|
||||
if action != "start":
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
if key == "R":
|
||||
if key == mcrfpy.Key.R:
|
||||
delete_save()
|
||||
generate_new_game()
|
||||
message_log.add("A new adventure begins!", COLOR_INFO)
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
if key == mcrfpy.Key.ESCAPE:
|
||||
if game_mode == GameMode.TARGETING:
|
||||
exit_targeting_mode()
|
||||
message_log.add("Targeting cancelled.", COLOR_INFO)
|
||||
|
|
@ -1679,7 +1628,7 @@ def handle_keys(key: str, action: str) -> None:
|
|||
mcrfpy.exit()
|
||||
return
|
||||
|
||||
if key == "Period" and game_mode == GameMode.NORMAL and not game_over:
|
||||
if key == mcrfpy.Key.PERIOD and game_mode == GameMode.NORMAL and not game_over:
|
||||
save_game()
|
||||
return
|
||||
|
||||
|
|
@ -1691,38 +1640,38 @@ def handle_keys(key: str, action: str) -> None:
|
|||
else:
|
||||
handle_normal_input(key)
|
||||
|
||||
def handle_normal_input(key: str) -> None:
|
||||
if key == "W" or key == "Up":
|
||||
def handle_normal_input(key) -> None:
|
||||
if key == mcrfpy.Key.W or key == mcrfpy.Key.UP:
|
||||
try_move_or_attack(0, -1)
|
||||
elif key == "S" or key == "Down":
|
||||
elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN:
|
||||
try_move_or_attack(0, 1)
|
||||
elif key == "A" or key == "Left":
|
||||
elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT:
|
||||
try_move_or_attack(-1, 0)
|
||||
elif key == "D" or key == "Right":
|
||||
elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT:
|
||||
try_move_or_attack(1, 0)
|
||||
elif key == "F":
|
||||
elif key == mcrfpy.Key.F:
|
||||
enter_targeting_mode()
|
||||
elif key == "G" or key == ",":
|
||||
elif key == mcrfpy.Key.G or key == mcrfpy.Key.COMMA:
|
||||
pickup_item()
|
||||
elif key == "Period" and mcrfpy.keypressed("LShift"):
|
||||
elif key == mcrfpy.Key.PERIOD and mcrfpy.keyboard.shift:
|
||||
# Shift+. (>) to descend stairs
|
||||
descend_stairs()
|
||||
elif key in ["1", "2", "3", "4", "5"]:
|
||||
index = int(key) - 1
|
||||
elif key in (mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5):
|
||||
index = [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5].index(key)
|
||||
if use_item(index):
|
||||
enemy_turn()
|
||||
update_ui()
|
||||
|
||||
def handle_targeting_input(key: str) -> None:
|
||||
if key == "Up" or key == "W":
|
||||
def handle_targeting_input(key) -> None:
|
||||
if key == mcrfpy.Key.UP or key == mcrfpy.Key.W:
|
||||
move_cursor(0, -1)
|
||||
elif key == "Down" or key == "S":
|
||||
elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S:
|
||||
move_cursor(0, 1)
|
||||
elif key == "Left" or key == "A":
|
||||
elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A:
|
||||
move_cursor(-1, 0)
|
||||
elif key == "Right" or key == "D":
|
||||
elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D:
|
||||
move_cursor(1, 0)
|
||||
elif key == "Return" or key == "Space":
|
||||
elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE:
|
||||
confirm_target()
|
||||
|
||||
scene.on_key = handle_keys
|
||||
|
|
|
|||
|
|
@ -723,24 +723,7 @@ class RectangularRoom:
|
|||
self.y2 >= other.y1
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Exploration Tracking
|
||||
# =============================================================================
|
||||
|
||||
explored: list[list[bool]] = []
|
||||
|
||||
def init_explored() -> None:
|
||||
global explored
|
||||
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||
|
||||
def mark_explored(x: int, y: int) -> None:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
explored[y][x] = True
|
||||
|
||||
def is_explored(x: int, y: int) -> bool:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
return explored[y][x]
|
||||
return False
|
||||
# Exploration tracking is handled automatically by ColorLayer.draw_fov()
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon Generation
|
||||
|
|
@ -972,18 +955,12 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc
|
|||
return None
|
||||
|
||||
def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
if entity in entity_data:
|
||||
del entity_data[entity]
|
||||
|
||||
def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
if entity in item_data:
|
||||
del item_data[entity]
|
||||
|
||||
|
|
@ -1001,11 +978,7 @@ def clear_entities_except_player(target_grid: mcrfpy.Grid) -> None:
|
|||
del entity_data[entity]
|
||||
if entity in item_data:
|
||||
del item_data[entity]
|
||||
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
|
||||
# =============================================================================
|
||||
# XP and Level Up
|
||||
|
|
@ -1059,7 +1032,6 @@ def descend_stairs() -> bool:
|
|||
|
||||
clear_entities_except_player(grid)
|
||||
|
||||
init_explored()
|
||||
player_start = generate_dungeon(grid, dungeon_level)
|
||||
|
||||
player.x = player_start[0]
|
||||
|
|
@ -1067,9 +1039,7 @@ def descend_stairs() -> bool:
|
|||
|
||||
spawn_entities_for_level(grid, texture, dungeon_level)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
update_fov(grid, fov_layer, player_start[0], player_start[1])
|
||||
|
||||
|
|
@ -1084,7 +1054,7 @@ def descend_stairs() -> bool:
|
|||
# =============================================================================
|
||||
|
||||
def save_game() -> bool:
|
||||
global player, player_inventory, grid, explored, dungeon_level, stairs_position
|
||||
global player, player_inventory, grid, dungeon_level, stairs_position
|
||||
|
||||
try:
|
||||
tiles = []
|
||||
|
|
@ -1133,7 +1103,6 @@ def save_game() -> bool:
|
|||
"inventory": player_inventory.to_dict()
|
||||
},
|
||||
"tiles": tiles,
|
||||
"explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)],
|
||||
"enemies": enemies,
|
||||
"items": items_on_ground
|
||||
}
|
||||
|
|
@ -1149,7 +1118,7 @@ def save_game() -> bool:
|
|||
return False
|
||||
|
||||
def load_game() -> bool:
|
||||
global player, player_inventory, grid, explored, dungeon_level
|
||||
global player, player_inventory, grid, dungeon_level
|
||||
global entity_data, item_data, fov_layer, game_over, stairs_position
|
||||
|
||||
if not os.path.exists(SAVE_FILE):
|
||||
|
|
@ -1163,7 +1132,7 @@ def load_game() -> bool:
|
|||
item_data.clear()
|
||||
|
||||
while len(grid.entities) > 0:
|
||||
grid.entities.remove(0)
|
||||
grid.entities.pop(0)
|
||||
|
||||
dungeon_level = save_data.get("dungeon_level", 1)
|
||||
stairs_position = tuple(save_data.get("stairs_position", [0, 0]))
|
||||
|
|
@ -1177,10 +1146,6 @@ def load_game() -> bool:
|
|||
cell.walkable = tile_data["walkable"]
|
||||
cell.transparent = tile_data["transparent"]
|
||||
|
||||
global explored
|
||||
explored_data = save_data["explored"]
|
||||
explored = [[explored_data[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)]
|
||||
|
||||
player_data = save_data["player"]
|
||||
player = mcrfpy.Entity(
|
||||
grid_pos=(player_data["x"], player_data["y"]),
|
||||
|
|
@ -1220,9 +1185,7 @@ def load_game() -> bool:
|
|||
grid.entities.append(item_entity)
|
||||
item_data[item_entity] = Item.from_dict(item_entry["item"])
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
update_fov(grid, fov_layer, int(player.x), int(player.y))
|
||||
|
||||
|
|
@ -1274,10 +1237,7 @@ def exit_targeting_mode() -> None:
|
|||
global game_mode, target_cursor, grid
|
||||
|
||||
if target_cursor is not None:
|
||||
for i, e in enumerate(grid.entities):
|
||||
if e == target_cursor:
|
||||
grid.entities.remove(i)
|
||||
break
|
||||
grid.entities.remove(target_cursor)
|
||||
target_cursor = None
|
||||
|
||||
game_mode = GameMode.NORMAL
|
||||
|
|
@ -1292,7 +1252,7 @@ def move_cursor(dx: int, dy: int) -> None:
|
|||
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
|
||||
return
|
||||
|
||||
if not grid.is_in_fov(new_x, new_y):
|
||||
if not grid.is_in_fov((new_x, new_y)):
|
||||
message_log.add("You cannot see that location.", COLOR_INVALID)
|
||||
return
|
||||
|
||||
|
|
@ -1488,21 +1448,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
|||
continue
|
||||
|
||||
ex, ey = int(entity.x), int(entity.y)
|
||||
entity.visible = target_grid.is_in_fov(ex, ey)
|
||||
entity.visible = target_grid.is_in_fov((ex, ey))
|
||||
|
||||
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if target_grid.is_in_fov(x, y):
|
||||
mark_explored(x, y)
|
||||
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||
elif is_explored(x, y):
|
||||
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||
else:
|
||||
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
|
||||
target_fov_layer.draw_fov(
|
||||
(player_x, player_y),
|
||||
radius=FOV_RADIUS,
|
||||
visible=COLOR_VISIBLE,
|
||||
discovered=COLOR_DISCOVERED,
|
||||
unknown=COLOR_UNKNOWN
|
||||
)
|
||||
update_entity_visibility(target_grid)
|
||||
|
||||
# =============================================================================
|
||||
|
|
@ -1574,7 +1529,7 @@ def enemy_turn() -> None:
|
|||
|
||||
ex, ey = int(enemy.x), int(enemy.y)
|
||||
|
||||
if not grid.is_in_fov(ex, ey):
|
||||
if not grid.is_in_fov((ex, ey)):
|
||||
continue
|
||||
|
||||
dx = player_x - ex
|
||||
|
|
@ -1644,9 +1599,8 @@ def generate_new_game() -> None:
|
|||
item_data.clear()
|
||||
|
||||
while len(grid.entities) > 0:
|
||||
grid.entities.remove(0)
|
||||
grid.entities.pop(0)
|
||||
|
||||
init_explored()
|
||||
message_log.clear()
|
||||
|
||||
player_start = generate_dungeon(grid, dungeon_level)
|
||||
|
|
@ -1673,9 +1627,7 @@ def generate_new_game() -> None:
|
|||
|
||||
spawn_entities_for_level(grid, texture, dungeon_level)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
update_fov(grid, fov_layer, player_start[0], player_start[1])
|
||||
|
||||
|
|
@ -1698,10 +1650,9 @@ grid = mcrfpy.Grid(
|
|||
zoom=1.0
|
||||
)
|
||||
|
||||
fov_layer = grid.add_layer("color", z_index=-1)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov")
|
||||
grid.add_layer(fov_layer)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
scene.children.append(grid)
|
||||
|
||||
|
|
@ -1756,8 +1707,6 @@ message_log.add_to_scene(scene)
|
|||
# Initialize Game
|
||||
# =============================================================================
|
||||
|
||||
init_explored()
|
||||
|
||||
if has_save_file():
|
||||
message_log.add("Found saved game. Loading...", COLOR_INFO)
|
||||
if not load_game():
|
||||
|
|
@ -1773,19 +1722,19 @@ else:
|
|||
# Input Handling
|
||||
# =============================================================================
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
def handle_keys(key, action) -> None:
|
||||
global game_over, game_mode
|
||||
|
||||
if action != "start":
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
if key == "R":
|
||||
if key == mcrfpy.Key.R:
|
||||
delete_save()
|
||||
generate_new_game()
|
||||
message_log.add("A new adventure begins!", COLOR_INFO)
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
if key == mcrfpy.Key.ESCAPE:
|
||||
if game_mode == GameMode.TARGETING:
|
||||
exit_targeting_mode()
|
||||
message_log.add("Targeting cancelled.", COLOR_INFO)
|
||||
|
|
@ -1796,7 +1745,7 @@ def handle_keys(key: str, action: str) -> None:
|
|||
mcrfpy.exit()
|
||||
return
|
||||
|
||||
if key == "Period" and game_mode == GameMode.NORMAL and not game_over:
|
||||
if key == mcrfpy.Key.PERIOD and game_mode == GameMode.NORMAL and not game_over:
|
||||
# Check for shift to descend
|
||||
descend_stairs()
|
||||
return
|
||||
|
|
@ -1809,35 +1758,35 @@ def handle_keys(key: str, action: str) -> None:
|
|||
else:
|
||||
handle_normal_input(key)
|
||||
|
||||
def handle_normal_input(key: str) -> None:
|
||||
if key == "W" or key == "Up":
|
||||
def handle_normal_input(key) -> None:
|
||||
if key == mcrfpy.Key.W or key == mcrfpy.Key.UP:
|
||||
try_move_or_attack(0, -1)
|
||||
elif key == "S" or key == "Down":
|
||||
elif key == mcrfpy.Key.S or key == mcrfpy.Key.DOWN:
|
||||
try_move_or_attack(0, 1)
|
||||
elif key == "A" or key == "Left":
|
||||
elif key == mcrfpy.Key.A or key == mcrfpy.Key.LEFT:
|
||||
try_move_or_attack(-1, 0)
|
||||
elif key == "D" or key == "Right":
|
||||
elif key == mcrfpy.Key.D or key == mcrfpy.Key.RIGHT:
|
||||
try_move_or_attack(1, 0)
|
||||
elif key == "F":
|
||||
elif key == mcrfpy.Key.F:
|
||||
enter_targeting_mode()
|
||||
elif key == "G" or key == ",":
|
||||
elif key == mcrfpy.Key.G or key == mcrfpy.Key.COMMA:
|
||||
pickup_item()
|
||||
elif key in ["1", "2", "3", "4", "5"]:
|
||||
index = int(key) - 1
|
||||
elif key in (mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5):
|
||||
index = [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5].index(key)
|
||||
if use_item(index):
|
||||
enemy_turn()
|
||||
update_ui()
|
||||
|
||||
def handle_targeting_input(key: str) -> None:
|
||||
if key == "Up" or key == "W":
|
||||
def handle_targeting_input(key) -> None:
|
||||
if key == mcrfpy.Key.UP or key == mcrfpy.Key.W:
|
||||
move_cursor(0, -1)
|
||||
elif key == "Down" or key == "S":
|
||||
elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S:
|
||||
move_cursor(0, 1)
|
||||
elif key == "Left" or key == "A":
|
||||
elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A:
|
||||
move_cursor(-1, 0)
|
||||
elif key == "Right" or key == "D":
|
||||
elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D:
|
||||
move_cursor(1, 0)
|
||||
elif key == "Return" or key == "Space":
|
||||
elif key == mcrfpy.Key.ENTER or key == mcrfpy.Key.SPACE:
|
||||
confirm_target()
|
||||
|
||||
scene.on_key = handle_keys
|
||||
|
|
|
|||
|
|
@ -801,24 +801,7 @@ class RectangularRoom:
|
|||
def intersects(self, other: "RectangularRoom") -> bool:
|
||||
return self.x1 <= other.x2 and self.x2 >= other.x1 and self.y1 <= other.y2 and self.y2 >= other.y1
|
||||
|
||||
# =============================================================================
|
||||
# Exploration Tracking
|
||||
# =============================================================================
|
||||
|
||||
explored: list[list[bool]] = []
|
||||
|
||||
def init_explored() -> None:
|
||||
global explored
|
||||
explored = [[False for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
|
||||
|
||||
def mark_explored(x: int, y: int) -> None:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
explored[y][x] = True
|
||||
|
||||
def is_explored(x: int, y: int) -> bool:
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
return explored[y][x]
|
||||
return False
|
||||
# Exploration tracking is handled automatically by ColorLayer.draw_fov()
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon Generation (abbreviated for space)
|
||||
|
|
@ -1014,18 +997,12 @@ def get_blocking_entity_at(target_grid: mcrfpy.Grid, x: int, y: int, exclude: mc
|
|||
return None
|
||||
|
||||
def remove_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
if entity in entity_data:
|
||||
del entity_data[entity]
|
||||
|
||||
def remove_item_entity(target_grid: mcrfpy.Grid, entity: mcrfpy.Entity) -> None:
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
if entity in item_data:
|
||||
del item_data[entity]
|
||||
|
||||
|
|
@ -1041,10 +1018,7 @@ def clear_entities_except_player(target_grid: mcrfpy.Grid) -> None:
|
|||
del entity_data[entity]
|
||||
if entity in item_data:
|
||||
del item_data[entity]
|
||||
for i, e in enumerate(target_grid.entities):
|
||||
if e == entity:
|
||||
target_grid.entities.remove(i)
|
||||
break
|
||||
target_grid.entities.remove(entity)
|
||||
|
||||
# =============================================================================
|
||||
# Equipment Actions
|
||||
|
|
@ -1174,14 +1148,11 @@ def descend_stairs() -> bool:
|
|||
|
||||
dungeon_level += 1
|
||||
clear_entities_except_player(grid)
|
||||
init_explored()
|
||||
player_start = generate_dungeon(grid, dungeon_level)
|
||||
player.x, player.y = player_start
|
||||
spawn_entities_for_level(grid, texture, dungeon_level)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
update_fov(grid, fov_layer, player_start[0], player_start[1])
|
||||
|
||||
message_log.add(f"You descend to level {dungeon_level}...", COLOR_DESCEND)
|
||||
|
|
@ -1194,7 +1165,7 @@ def descend_stairs() -> bool:
|
|||
# =============================================================================
|
||||
|
||||
def save_game() -> bool:
|
||||
global player, player_inventory, grid, explored, dungeon_level, stairs_position
|
||||
global player, player_inventory, grid, dungeon_level, stairs_position
|
||||
|
||||
try:
|
||||
tiles = []
|
||||
|
|
@ -1238,7 +1209,6 @@ def save_game() -> bool:
|
|||
"inventory": player_inventory.to_dict()
|
||||
},
|
||||
"tiles": tiles,
|
||||
"explored": [[explored[y][x] for x in range(GRID_WIDTH)] for y in range(GRID_HEIGHT)],
|
||||
"enemies": enemies,
|
||||
"items": items_on_ground
|
||||
}
|
||||
|
|
@ -1253,7 +1223,7 @@ def save_game() -> bool:
|
|||
return False
|
||||
|
||||
def load_game() -> bool:
|
||||
global player, player_inventory, grid, explored, dungeon_level
|
||||
global player, player_inventory, grid, dungeon_level
|
||||
global entity_data, item_data, fov_layer, game_over, stairs_position
|
||||
|
||||
if not os.path.exists(SAVE_FILE):
|
||||
|
|
@ -1266,7 +1236,7 @@ def load_game() -> bool:
|
|||
entity_data.clear()
|
||||
item_data.clear()
|
||||
while len(grid.entities) > 0:
|
||||
grid.entities.remove(0)
|
||||
grid.entities.pop(0)
|
||||
|
||||
dungeon_level = save_data.get("dungeon_level", 1)
|
||||
stairs_position = tuple(save_data.get("stairs_position", [0, 0]))
|
||||
|
|
@ -1280,9 +1250,6 @@ def load_game() -> bool:
|
|||
cell.walkable = tile_data["walkable"]
|
||||
cell.transparent = tile_data["transparent"]
|
||||
|
||||
global explored
|
||||
explored = save_data["explored"]
|
||||
|
||||
player_data = save_data["player"]
|
||||
player = mcrfpy.Entity(
|
||||
grid_pos=(player_data["x"], player_data["y"]),
|
||||
|
|
@ -1321,9 +1288,7 @@ def load_game() -> bool:
|
|||
grid.entities.append(item_entity)
|
||||
item_data[item_entity] = item
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
update_fov(grid, fov_layer, int(player.x), int(player.y))
|
||||
|
||||
game_over = False
|
||||
|
|
@ -1362,10 +1327,7 @@ def enter_targeting_mode() -> None:
|
|||
def exit_targeting_mode() -> None:
|
||||
global game_mode, target_cursor
|
||||
if target_cursor:
|
||||
for i, e in enumerate(grid.entities):
|
||||
if e == target_cursor:
|
||||
grid.entities.remove(i)
|
||||
break
|
||||
grid.entities.remove(target_cursor)
|
||||
target_cursor = None
|
||||
game_mode = GameMode.NORMAL
|
||||
mode_display.update(game_mode)
|
||||
|
|
@ -1375,7 +1337,7 @@ def move_cursor(dx: int, dy: int) -> None:
|
|||
new_x, new_y = target_x + dx, target_y + dy
|
||||
if not (0 <= new_x < GRID_WIDTH and 0 <= new_y < GRID_HEIGHT):
|
||||
return
|
||||
if not grid.is_in_fov(new_x, new_y):
|
||||
if not grid.is_in_fov((new_x, new_y)):
|
||||
message_log.add("Cannot see that location.", COLOR_INVALID)
|
||||
return
|
||||
distance = abs(new_x - int(player.x)) + abs(new_y - int(player.y))
|
||||
|
|
@ -1515,19 +1477,16 @@ def update_entity_visibility(target_grid: mcrfpy.Grid) -> None:
|
|||
if entity == player or entity == target_cursor:
|
||||
entity.visible = True
|
||||
else:
|
||||
entity.visible = target_grid.is_in_fov(int(entity.x), int(entity.y))
|
||||
entity.visible = target_grid.is_in_fov((int(entity.x), int(entity.y)))
|
||||
|
||||
def update_fov(target_grid: mcrfpy.Grid, target_fov_layer, player_x: int, player_y: int) -> None:
|
||||
target_grid.compute_fov(player_x, player_y, FOV_RADIUS, mcrfpy.FOV.SHADOW)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if target_grid.is_in_fov(x, y):
|
||||
mark_explored(x, y)
|
||||
target_fov_layer.set(x, y, COLOR_VISIBLE)
|
||||
elif is_explored(x, y):
|
||||
target_fov_layer.set(x, y, COLOR_DISCOVERED)
|
||||
else:
|
||||
target_fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
target_fov_layer.draw_fov(
|
||||
(player_x, player_y),
|
||||
radius=FOV_RADIUS,
|
||||
visible=COLOR_VISIBLE,
|
||||
discovered=COLOR_DISCOVERED,
|
||||
unknown=COLOR_UNKNOWN
|
||||
)
|
||||
update_entity_visibility(target_grid)
|
||||
|
||||
# =============================================================================
|
||||
|
|
@ -1577,7 +1536,7 @@ def enemy_turn() -> None:
|
|||
if not fighter.is_alive:
|
||||
continue
|
||||
ex, ey = int(entity.x), int(entity.y)
|
||||
if not grid.is_in_fov(ex, ey):
|
||||
if not grid.is_in_fov((ex, ey)):
|
||||
continue
|
||||
|
||||
dx, dy = px - ex, py - ey
|
||||
|
|
@ -1624,9 +1583,8 @@ def generate_new_game() -> None:
|
|||
entity_data.clear()
|
||||
item_data.clear()
|
||||
while len(grid.entities) > 0:
|
||||
grid.entities.remove(0)
|
||||
grid.entities.pop(0)
|
||||
|
||||
init_explored()
|
||||
message_log.clear()
|
||||
|
||||
player_start = generate_dungeon(grid, dungeon_level)
|
||||
|
|
@ -1642,9 +1600,7 @@ def generate_new_game() -> None:
|
|||
player_inventory = Inventory(capacity=10)
|
||||
spawn_entities_for_level(grid, texture, dungeon_level)
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
update_fov(grid, fov_layer, player_start[0], player_start[1])
|
||||
|
||||
mode_display.update(game_mode)
|
||||
|
|
@ -1666,10 +1622,9 @@ grid = mcrfpy.Grid(
|
|||
zoom=1.0
|
||||
)
|
||||
|
||||
fov_layer = grid.add_layer("color", z_index=-1)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
fov_layer.set(x, y, COLOR_UNKNOWN)
|
||||
fov_layer = mcrfpy.ColorLayer(z_index=-1, name="fov")
|
||||
grid.add_layer(fov_layer)
|
||||
fov_layer.fill(COLOR_UNKNOWN)
|
||||
|
||||
scene.children.append(grid)
|
||||
|
||||
|
|
@ -1706,8 +1661,6 @@ message_log = MessageLog(x=20, y=768 - UI_BOTTOM_HEIGHT + 10, width=990, height=
|
|||
message_log.add_to_scene(scene)
|
||||
|
||||
# Initialize
|
||||
init_explored()
|
||||
|
||||
if has_save_file():
|
||||
message_log.add("Loading saved game...", COLOR_INFO)
|
||||
if not load_game():
|
||||
|
|
@ -1722,19 +1675,19 @@ else:
|
|||
# Input Handling
|
||||
# =============================================================================
|
||||
|
||||
def handle_keys(key: str, action: str) -> None:
|
||||
def handle_keys(key, action) -> None:
|
||||
global game_over, game_mode
|
||||
|
||||
if action != "start":
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
if key == "R":
|
||||
if key == mcrfpy.Key.R:
|
||||
delete_save()
|
||||
generate_new_game()
|
||||
message_log.add("New adventure begins!", COLOR_INFO)
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
if key == mcrfpy.Key.ESCAPE:
|
||||
if game_mode == GameMode.TARGETING:
|
||||
exit_targeting_mode()
|
||||
message_log.add("Targeting cancelled.", COLOR_INFO)
|
||||
|
|
@ -1752,39 +1705,39 @@ def handle_keys(key: str, action: str) -> None:
|
|||
else:
|
||||
handle_normal_input(key)
|
||||
|
||||
def handle_normal_input(key: str) -> None:
|
||||
if key in ("W", "Up"):
|
||||
def handle_normal_input(key) -> None:
|
||||
if key in (mcrfpy.Key.W, mcrfpy.Key.UP):
|
||||
try_move_or_attack(0, -1)
|
||||
elif key in ("S", "Down"):
|
||||
elif key in (mcrfpy.Key.S, mcrfpy.Key.DOWN):
|
||||
try_move_or_attack(0, 1)
|
||||
elif key in ("A", "Left"):
|
||||
elif key in (mcrfpy.Key.A, mcrfpy.Key.LEFT):
|
||||
try_move_or_attack(-1, 0)
|
||||
elif key in ("D", "Right"):
|
||||
elif key in (mcrfpy.Key.D, mcrfpy.Key.RIGHT):
|
||||
try_move_or_attack(1, 0)
|
||||
elif key == "F":
|
||||
elif key == mcrfpy.Key.F:
|
||||
enter_targeting_mode()
|
||||
elif key in ("G", ","):
|
||||
elif key in (mcrfpy.Key.G, mcrfpy.Key.COMMA):
|
||||
pickup_item()
|
||||
elif key == "Period":
|
||||
elif key == mcrfpy.Key.PERIOD:
|
||||
descend_stairs()
|
||||
elif key == "E":
|
||||
elif key == mcrfpy.Key.E:
|
||||
message_log.add("Press 1-5 to equip an item from inventory.", COLOR_INFO)
|
||||
elif key in "12345":
|
||||
index = int(key) - 1
|
||||
elif key in (mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5):
|
||||
index = [mcrfpy.Key.NUM_1, mcrfpy.Key.NUM_2, mcrfpy.Key.NUM_3, mcrfpy.Key.NUM_4, mcrfpy.Key.NUM_5].index(key)
|
||||
if use_item(index):
|
||||
enemy_turn()
|
||||
update_ui()
|
||||
|
||||
def handle_targeting_input(key: str) -> None:
|
||||
if key in ("Up", "W"):
|
||||
def handle_targeting_input(key) -> None:
|
||||
if key in (mcrfpy.Key.UP, mcrfpy.Key.W):
|
||||
move_cursor(0, -1)
|
||||
elif key in ("Down", "S"):
|
||||
elif key in (mcrfpy.Key.DOWN, mcrfpy.Key.S):
|
||||
move_cursor(0, 1)
|
||||
elif key in ("Left", "A"):
|
||||
elif key in (mcrfpy.Key.LEFT, mcrfpy.Key.A):
|
||||
move_cursor(-1, 0)
|
||||
elif key in ("Right", "D"):
|
||||
elif key in (mcrfpy.Key.RIGHT, mcrfpy.Key.D):
|
||||
move_cursor(1, 0)
|
||||
elif key in ("Return", "Space"):
|
||||
elif key in (mcrfpy.Key.ENTER, mcrfpy.Key.SPACE):
|
||||
confirm_target()
|
||||
|
||||
scene.on_key = handle_keys
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# McRogueFace AddressSanitizer Suppression File
|
||||
#
|
||||
# Minimal — most CPython false positives are handled by:
|
||||
# CPython false positives are handled by:
|
||||
# - PYTHONMALLOC=malloc (bypasses pymalloc)
|
||||
# - detect_leaks=0 (CPython has intentional lifetime leaks)
|
||||
# - LSAN suppressions below (CPython has intentional lifetime leaks)
|
||||
#
|
||||
# Usage (via ASAN_OPTIONS or LSAN_OPTIONS):
|
||||
# LSAN_OPTIONS="suppressions=sanitizers/asan.supp"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public:
|
|||
|
||||
GridLayerType type;
|
||||
std::string name; // #150 - Layer name for GridPoint property access
|
||||
int z_index; // Negative = below entities, >= 0 = above entities
|
||||
int z_index; // <= 0 = below entities (ground level), > 0 = above entities
|
||||
int grid_x, grid_y; // Dimensions
|
||||
GridData* parent_grid; // Parent grid reference (#252: GridData, not UIGrid)
|
||||
bool visible; // Visibility flag
|
||||
|
|
|
|||
|
|
@ -269,15 +269,6 @@ static PyMethodDef mcrfpyMethods[] = {
|
|||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("This immediately closes the window and terminates the program.")
|
||||
)},
|
||||
{"setScale", McRFPy_API::_setScale, METH_VARARGS,
|
||||
MCRF_FUNCTION(setScale,
|
||||
MCRF_SIG("(multiplier: float)", "None"),
|
||||
MCRF_DESC("Deprecated: use Window.resolution instead. Scale the game window size."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("multiplier", "Scale factor (e.g., 2.0 for double size)")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("The internal resolution remains 1024x768, but the window is scaled. This is deprecated - use Window.resolution instead.")
|
||||
)},
|
||||
{"set_scale", McRFPy_API::_setScale, METH_VARARGS,
|
||||
MCRF_FUNCTION(set_scale,
|
||||
MCRF_SIG("(multiplier: float)", "None"),
|
||||
|
|
@ -298,16 +289,6 @@ static PyMethodDef mcrfpyMethods[] = {
|
|||
MCRF_RETURNS("Frame, Caption, Sprite, Grid, or Entity if found; None otherwise")
|
||||
MCRF_NOTE("Searches scene UI elements and entities within grids.")
|
||||
)},
|
||||
{"findAll", McRFPy_API::_findAll, METH_VARARGS,
|
||||
MCRF_FUNCTION(findAll,
|
||||
MCRF_SIG("(pattern: str, scene: str = None)", "list"),
|
||||
MCRF_DESC("Find all UI elements matching a name pattern. Prefer find_all()."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("pattern", "Name pattern with optional wildcards (* matches any characters)")
|
||||
MCRF_ARG("scene", "Scene to search in (default: current scene)")
|
||||
MCRF_RETURNS("list: All matching UI elements and entities")
|
||||
MCRF_NOTE("Example: find_all('enemy*') finds all elements starting with 'enemy'")
|
||||
)},
|
||||
{"find_all", McRFPy_API::_findAll, METH_VARARGS,
|
||||
MCRF_FUNCTION(find_all,
|
||||
MCRF_SIG("(pattern: str, scene: str = None)", "list"),
|
||||
|
|
@ -319,12 +300,6 @@ static PyMethodDef mcrfpyMethods[] = {
|
|||
MCRF_NOTE("Example: find_all('enemy*') finds all elements starting with 'enemy', find_all('*_button') finds all elements ending with '_button'")
|
||||
)},
|
||||
|
||||
{"getMetrics", McRFPy_API::_getMetrics, METH_NOARGS,
|
||||
MCRF_FUNCTION(getMetrics,
|
||||
MCRF_SIG("()", "dict"),
|
||||
MCRF_DESC("Get current performance metrics. Prefer get_metrics()."),
|
||||
MCRF_RETURNS("dict: Performance data with keys: frame_time, avg_frame_time, fps, draw_calls, ui_elements, visible_elements, current_frame, runtime")
|
||||
)},
|
||||
{"get_metrics", McRFPy_API::_getMetrics, METH_NOARGS,
|
||||
MCRF_FUNCTION(get_metrics,
|
||||
MCRF_SIG("()", "dict"),
|
||||
|
|
@ -332,15 +307,6 @@ static PyMethodDef mcrfpyMethods[] = {
|
|||
MCRF_RETURNS("dict: Performance data with keys: frame_time (last frame duration in seconds), avg_frame_time (average frame time), fps (frames per second), draw_calls (number of draw calls), ui_elements (total UI element count), visible_elements (visible element count), current_frame (frame counter), runtime (total runtime in seconds)")
|
||||
)},
|
||||
|
||||
{"setDevConsole", McRFPy_API::_setDevConsole, METH_VARARGS,
|
||||
MCRF_FUNCTION(setDevConsole,
|
||||
MCRF_SIG("(enabled: bool)", "None"),
|
||||
MCRF_DESC("Enable or disable the developer console overlay. Prefer set_dev_console()."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("enabled", "True to enable the console (default), False to disable")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("When disabled, the grave/tilde key will not open the console. Use this to ship games without debug features.")
|
||||
)},
|
||||
{"set_dev_console", McRFPy_API::_setDevConsole, METH_VARARGS,
|
||||
MCRF_FUNCTION(set_dev_console,
|
||||
MCRF_SIG("(enabled: bool)", "None"),
|
||||
|
|
|
|||
|
|
@ -404,6 +404,7 @@ int UIArc::set_center(PyUIArcObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setCenter(vec->data);
|
||||
self->data->markCompositeDirty(); // #291: position change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -417,6 +418,7 @@ int UIArc::set_radius(PyUIArcObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setRadius(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -430,6 +432,7 @@ int UIArc::set_start_angle(PyUIArcObject* self, PyObject* value, void* closure)
|
|||
return -1;
|
||||
}
|
||||
self->data->setStartAngle(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -443,6 +446,7 @@ int UIArc::set_end_angle(PyUIArcObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setEndAngle(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -462,6 +466,7 @@ int UIArc::set_color(PyUIArcObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setColor(color->data);
|
||||
self->data->markDirty(); // #291: color change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -475,6 +480,7 @@ int UIArc::set_thickness(PyUIArcObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setThickness(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -200,14 +200,22 @@ int UICaption::set_float_member(PyUICaptionObject* self, PyObject* value, void*
|
|||
PyErr_SetString(PyExc_TypeError, "Value must be a number (int or float)");
|
||||
return -1;
|
||||
}
|
||||
if (member_ptr == 0) //x
|
||||
if (member_ptr == 0) { //x
|
||||
self->data->text.setPosition(val, self->data->text.getPosition().y);
|
||||
else if (member_ptr == 1) //y
|
||||
self->data->markCompositeDirty(); // #289: position change invalidates parent cache
|
||||
}
|
||||
else if (member_ptr == 1) { //y
|
||||
self->data->text.setPosition(self->data->text.getPosition().x, val);
|
||||
else if (member_ptr == 4) //outline
|
||||
self->data->markCompositeDirty(); // #289: position change invalidates parent cache
|
||||
}
|
||||
else if (member_ptr == 4) { //outline
|
||||
self->data->text.setOutlineThickness(val);
|
||||
else if (member_ptr == 5) // character size
|
||||
self->data->markDirty(); // #289: content change invalidates own + parent cache
|
||||
}
|
||||
else if (member_ptr == 5) { // character size
|
||||
self->data->text.setCharacterSize(val);
|
||||
self->data->markDirty(); // #289: content change invalidates own + parent cache
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -219,6 +227,7 @@ PyObject* UICaption::get_vec_member(PyUICaptionObject* self, void* closure)
|
|||
int UICaption::set_vec_member(PyUICaptionObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
self->data->text.setPosition(PyVector::fromPy(value));
|
||||
self->data->markCompositeDirty(); // #289: position change invalidates parent cache
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -289,10 +298,12 @@ int UICaption::set_color_member(PyUICaptionObject* self, PyObject* value, void*
|
|||
if (member_ptr == 0)
|
||||
{
|
||||
self->data->text.setFillColor(sf::Color(r, g, b, a));
|
||||
self->data->markDirty(); // #289: color change invalidates own + parent cache
|
||||
}
|
||||
else if (member_ptr == 1)
|
||||
{
|
||||
self->data->text.setOutlineColor(sf::Color(r, g, b, a));
|
||||
self->data->markDirty(); // #289: color change invalidates own + parent cache
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -329,6 +340,7 @@ int UICaption::set_text(PyUICaptionObject* self, PyObject* value, void* closure)
|
|||
Py_DECREF(temp_bytes);
|
||||
}
|
||||
self->data->text.setString(Resources::caption_buffer);
|
||||
self->data->markDirty(); // #289: text change invalidates own + parent cache
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -337,6 +337,7 @@ int UICircle::set_radius(PyUICircleObject* self, PyObject* value, void* closure)
|
|||
return -1;
|
||||
}
|
||||
self->data->setRadius(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -355,6 +356,7 @@ int UICircle::set_center(PyUICircleObject* self, PyObject* value, void* closure)
|
|||
return -1;
|
||||
}
|
||||
self->data->setCenter(vec->data);
|
||||
self->data->markCompositeDirty(); // #291: position change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -382,6 +384,7 @@ int UICircle::set_fill_color(PyUICircleObject* self, PyObject* value, void* clos
|
|||
return -1;
|
||||
}
|
||||
self->data->setFillColor(color);
|
||||
self->data->markDirty(); // #291: color change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -409,6 +412,7 @@ int UICircle::set_outline_color(PyUICircleObject* self, PyObject* value, void* c
|
|||
return -1;
|
||||
}
|
||||
self->data->setOutlineColor(color);
|
||||
self->data->markDirty(); // #291: color change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -422,6 +426,7 @@ int UICircle::set_outline(PyUICircleObject* self, PyObject* value, void* closure
|
|||
return -1;
|
||||
}
|
||||
self->data->setOutline(static_cast<float>(PyFloat_AsDouble(value)));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -271,6 +271,11 @@ int UICollection::setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject
|
|||
// #122: Clear the parent before removing
|
||||
(*self->data)[index]->setParent(nullptr);
|
||||
self->data->erase(self->data->begin() + index);
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -302,6 +307,12 @@ int UICollection::setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject
|
|||
// Mark scene as needing resort after replacing element
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -494,7 +505,14 @@ int UICollection::ass_subscript(PyUICollectionObject* self, PyObject* key, PyObj
|
|||
|
||||
// Mark scene as needing resort after slice deletion
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
{
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
} else {
|
||||
// Assignment
|
||||
|
|
@ -564,7 +582,14 @@ int UICollection::ass_subscript(PyUICollectionObject* self, PyObject* key, PyObj
|
|||
|
||||
// Mark scene as needing resort after slice assignment
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
{
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
|
|
@ -635,6 +660,12 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
|
|||
// Mark scene as needing resort after adding element
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
|
@ -689,7 +720,12 @@ PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable)
|
|||
|
||||
// Mark scene as needing resort after adding elements
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
|
@ -717,6 +753,11 @@ PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o)
|
|||
(*it)->setParent(nullptr);
|
||||
vec->erase(it);
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
}
|
||||
|
|
@ -766,6 +807,12 @@ PyObject* UICollection::pop(PyUICollectionObject* self, PyObject* args)
|
|||
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
auto owner_ptr = self->owner.lock();
|
||||
if (owner_ptr) {
|
||||
owner_ptr->markContentDirty();
|
||||
}
|
||||
|
||||
// Convert to Python object and return
|
||||
return convertDrawableToPython(drawable);
|
||||
}
|
||||
|
|
@ -817,6 +864,12 @@ PyObject* UICollection::insert(PyUICollectionObject* self, PyObject* args)
|
|||
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// #288: Invalidate parent Frame's render cache
|
||||
auto owner_ptr2 = self->owner.lock();
|
||||
if (owner_ptr2) {
|
||||
owner_ptr2->markContentDirty();
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -545,10 +545,12 @@ int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure)
|
|||
case 0: // x
|
||||
drawable->position.x = val;
|
||||
drawable->onPositionChanged();
|
||||
drawable->markCompositeDirty(); // #290: position change invalidates parent cache
|
||||
break;
|
||||
case 1: // y
|
||||
drawable->position.y = val;
|
||||
drawable->onPositionChanged();
|
||||
drawable->markCompositeDirty(); // #290: position change invalidates parent cache
|
||||
break;
|
||||
case 2: // w
|
||||
case 3: // h
|
||||
|
|
@ -559,6 +561,7 @@ int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure)
|
|||
} else {
|
||||
drawable->resize(bounds.width, val);
|
||||
}
|
||||
drawable->markDirty(); // #290: size change invalidates own + parent cache
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
|
@ -638,6 +641,7 @@ int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) {
|
|||
|
||||
drawable->position = sf::Vector2f(x, y);
|
||||
drawable->onPositionChanged();
|
||||
drawable->markCompositeDirty(); // #290: position change invalidates parent cache
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -873,6 +877,7 @@ int UIDrawable::set_grid_pos(PyObject* self, PyObject* value, void* closure) {
|
|||
drawable->position.x = grid_x * cell_size.x;
|
||||
drawable->position.y = grid_y * cell_size.y;
|
||||
drawable->onPositionChanged();
|
||||
drawable->markCompositeDirty(); // #290: position change invalidates parent cache
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,10 +182,10 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
if (y_limit > grid_h) y_limit = grid_h;
|
||||
|
||||
// #150 - Layers are now the sole source of grid rendering (base layer removed)
|
||||
// Render layers with z_index < 0 (below entities)
|
||||
// Render layers with z_index <= 0 (below entities)
|
||||
sortLayers();
|
||||
for (auto& layer : layers) {
|
||||
if (layer->z_index >= 0) break; // Stop at layers that go above entities
|
||||
if (layer->z_index > 0) break; // Stop at layers that go above entities (#257)
|
||||
layer->render(*activeTexture, left_spritepixels, top_spritepixels,
|
||||
left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height);
|
||||
}
|
||||
|
|
@ -222,9 +222,9 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
Resources::game->metrics.totalEntities += totalEntities;
|
||||
}
|
||||
|
||||
// #147 - Render dynamic layers with z_index >= 0 (above entities)
|
||||
// #147 - Render dynamic layers with z_index > 0 (above entities)
|
||||
for (auto& layer : layers) {
|
||||
if (layer->z_index < 0) continue; // Skip layers below entities
|
||||
if (layer->z_index <= 0) continue; // Skip layers at or below entities (#257)
|
||||
layer->render(*activeTexture, left_spritepixels, top_spritepixels,
|
||||
left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height);
|
||||
}
|
||||
|
|
@ -1074,7 +1074,8 @@ int UIGrid::set_size(PyUIGridObject* self, PyObject* value, void* closure) {
|
|||
tex_height = std::min(tex_height, 4096u);
|
||||
|
||||
self->data->renderTexture.create(tex_width, tex_height);
|
||||
|
||||
self->data->markDirty(); // #291: size change
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1091,6 +1092,7 @@ int UIGrid::set_center(PyUIGridObject* self, PyObject* value, void* closure) {
|
|||
}
|
||||
self->data->center_x = x;
|
||||
self->data->center_y = y;
|
||||
self->data->markDirty(); // #291: camera position change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1186,6 +1188,14 @@ int UIGrid::set_float_member(PyUIGridObject* self, PyObject* value, void* closur
|
|||
else if (member_ptr == 7) self->view->camera_rotation = val;
|
||||
self->view->position = self->view->box.getPosition();
|
||||
}
|
||||
|
||||
// #291: Dirty flag propagation for visual property changes
|
||||
if (member_ptr == 0 || member_ptr == 1) {
|
||||
self->data->markCompositeDirty(); // position change
|
||||
} else {
|
||||
self->data->markDirty(); // content/size change
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
// TODO (7DRL Day 2, item 5.) return Texture object
|
||||
|
|
@ -1310,6 +1320,7 @@ int UIGrid::set_fill_color(PyUIGridObject* self, PyObject* value, void* closure)
|
|||
|
||||
PyColorObject* color = (PyColorObject*)value;
|
||||
self->data->fill_color = color->data;
|
||||
self->data->markDirty(); // #291: color change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1342,6 +1353,7 @@ int UIGrid::set_perspective(PyUIGridObject* self, PyObject* value, void* closure
|
|||
if (value == Py_None) {
|
||||
// Clear perspective but keep perspective_enabled unchanged
|
||||
self->data->perspective_entity.reset();
|
||||
self->data->markDirty(); // #291: FOV rendering change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1354,6 +1366,7 @@ int UIGrid::set_perspective(PyUIGridObject* self, PyObject* value, void* closure
|
|||
PyUIEntityObject* entity_obj = (PyUIEntityObject*)value;
|
||||
self->data->perspective_entity = entity_obj->data;
|
||||
self->data->perspective_enabled = true; // Enable perspective when entity assigned
|
||||
self->data->markDirty(); // #291: FOV rendering change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1369,6 +1382,7 @@ int UIGrid::set_perspective_enabled(PyUIGridObject* self, PyObject* value, void*
|
|||
return -1; // Error occurred
|
||||
}
|
||||
self->data->perspective_enabled = enabled;
|
||||
self->data->markDirty(); // #291: FOV rendering toggle
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1401,6 +1415,7 @@ int UIGrid::set_fov(PyUIGridObject* self, PyObject* value, void* closure)
|
|||
return -1;
|
||||
}
|
||||
self->data->fov_algorithm = algo;
|
||||
self->data->markDirty(); // #291: FOV algorithm change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1425,6 +1440,7 @@ int UIGrid::set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure)
|
|||
return -1;
|
||||
}
|
||||
self->data->fov_radius = (int)radius;
|
||||
self->data->markDirty(); // #291: FOV radius change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -167,10 +167,10 @@ void UIGridView::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
int y_limit = top_edge + height_sq + 2;
|
||||
if (y_limit > grid_data->grid_h) y_limit = grid_data->grid_h;
|
||||
|
||||
// Render layers below entities (z_index < 0)
|
||||
// Render layers below entities (z_index <= 0)
|
||||
grid_data->sortLayers();
|
||||
for (auto& layer : grid_data->layers) {
|
||||
if (layer->z_index >= 0) break;
|
||||
if (layer->z_index > 0) break; // #257: z_index=0 is ground level (below entities)
|
||||
layer->render(*activeTexture, left_spritepixels, top_spritepixels,
|
||||
left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height);
|
||||
}
|
||||
|
|
@ -191,9 +191,9 @@ void UIGridView::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
}
|
||||
}
|
||||
|
||||
// Render layers above entities (z_index >= 0)
|
||||
// Render layers above entities (z_index > 0)
|
||||
for (auto& layer : grid_data->layers) {
|
||||
if (layer->z_index < 0) continue;
|
||||
if (layer->z_index <= 0) continue; // #257: skip ground-level and below
|
||||
layer->render(*activeTexture, left_spritepixels, top_spritepixels,
|
||||
left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -423,6 +423,7 @@ int UILine::set_start(PyUILineObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setStart(vec->data);
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -443,6 +444,7 @@ int UILine::set_end(PyUILineObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setEnd(vec->data);
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -462,6 +464,7 @@ int UILine::set_color(PyUILineObject* self, PyObject* value, void* closure) {
|
|||
return -1;
|
||||
}
|
||||
self->data->setColor(color->data);
|
||||
self->data->markDirty(); // #291: color change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -486,6 +489,7 @@ int UILine::set_thickness(PyUILineObject* self, PyObject* value, void* closure)
|
|||
}
|
||||
|
||||
self->data->setThickness(thickness);
|
||||
self->data->markDirty(); // #291: visual change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -285,16 +285,26 @@ int UISprite::set_float_member(PyUISpriteObject* self, PyObject* value, void* cl
|
|||
PyErr_SetString(PyExc_TypeError, "Value must be a number (int or float)");
|
||||
return -1;
|
||||
}
|
||||
if (member_ptr == 0) //x
|
||||
if (member_ptr == 0) { //x
|
||||
self->data->setPosition(sf::Vector2f(val, self->data->getPosition().y));
|
||||
else if (member_ptr == 1) //y
|
||||
self->data->markCompositeDirty(); // #291: position change
|
||||
}
|
||||
else if (member_ptr == 1) { //y
|
||||
self->data->setPosition(sf::Vector2f(self->data->getPosition().x, val));
|
||||
else if (member_ptr == 2) // scale (uniform)
|
||||
self->data->markCompositeDirty(); // #291: position change
|
||||
}
|
||||
else if (member_ptr == 2) { // scale (uniform)
|
||||
self->data->setScale(sf::Vector2f(val, val));
|
||||
else if (member_ptr == 3) // scale_x
|
||||
self->data->markDirty(); // #291: visual change
|
||||
}
|
||||
else if (member_ptr == 3) { // scale_x
|
||||
self->data->setScale(sf::Vector2f(val, self->data->getScale().y));
|
||||
else if (member_ptr == 4) // scale_y
|
||||
self->data->markDirty(); // #291: visual change
|
||||
}
|
||||
else if (member_ptr == 4) { // scale_y
|
||||
self->data->setScale(sf::Vector2f(self->data->getScale().x, val));
|
||||
self->data->markDirty(); // #291: visual change
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -339,6 +349,7 @@ int UISprite::set_int_member(PyUISpriteObject* self, PyObject* value, void* clos
|
|||
}
|
||||
|
||||
self->data->setSpriteIndex(val);
|
||||
self->data->markDirty(); // #291: sprite content change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -364,7 +375,8 @@ int UISprite::set_texture(PyUISpriteObject* self, PyObject* value, void* closure
|
|||
|
||||
// Update the sprite's texture
|
||||
self->data->setTexture(pytexture->data);
|
||||
|
||||
self->data->markDirty(); // #291: texture change
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -387,6 +399,7 @@ int UISprite::set_pos(PyUISpriteObject* self, PyObject* value, void* closure)
|
|||
return -1;
|
||||
}
|
||||
self->data->setPosition(vec->data);
|
||||
self->data->markCompositeDirty(); // #291: position change
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
239
src/scripts/cos/scenes/menu.py
Normal file
239
src/scripts/cos/scenes/menu.py
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
"""Main menu scene for Crypt of Sokoban.
|
||||
|
||||
Displays a title screen with a live demo grid, play button,
|
||||
and settings buttons. Replaces the 7DRL's MainMenu class
|
||||
that duplicated entity spawning code from the Crypt class.
|
||||
"""
|
||||
|
||||
import random
|
||||
import mcrfpy
|
||||
from cos import Resources
|
||||
from cos.constants import (
|
||||
GRID_ZOOM, ENEMY_PRESETS, COLOR_TEXT,
|
||||
)
|
||||
from cos.level.generator import Level
|
||||
from cos.entities.player import PlayerEntity
|
||||
from cos.entities.enemies import EnemyEntity
|
||||
from cos.entities.objects import BoulderEntity, ExitEntity
|
||||
from cos.ui.widgets import SweetButton
|
||||
|
||||
|
||||
class MenuScene:
|
||||
"""Main menu with animated demo and navigation buttons.
|
||||
|
||||
The demo grid shows a small generated level with entities
|
||||
that wander randomly, giving the menu visual interest.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.scene = mcrfpy.Scene("menu")
|
||||
self.ui = self.scene.children
|
||||
self.play_scene = None
|
||||
|
||||
res = Resources()
|
||||
|
||||
# -- Demo grid (background) ---------------------------------------
|
||||
self.entities = [] # entity registry for demo animation
|
||||
self._demo_level = Level(20, 20)
|
||||
self.grid = self._demo_level.grid
|
||||
self.grid.zoom = 1.75
|
||||
gw = int(self.grid.grid_size.x)
|
||||
gh = int(self.grid.grid_size.y)
|
||||
self.grid.center_camera((gw / 2.0, gh / 2.0))
|
||||
|
||||
demo_plan = [
|
||||
("boulder", "boulder", "rat", "cyclops", "boulder"),
|
||||
("spawn",),
|
||||
("rat", "big rat"),
|
||||
("button", "boulder", "exit"),
|
||||
]
|
||||
coords = self._demo_level.generate(demo_plan)
|
||||
self._spawn_demo_entities(coords)
|
||||
|
||||
# Wire up engine entities for rendering
|
||||
for entity in self.entities:
|
||||
entity.entity.grid = self.grid
|
||||
|
||||
self.demo_timer = mcrfpy.Timer("demo_motion", self._demo_tick, 100)
|
||||
|
||||
# -- Title text ---------------------------------------------------
|
||||
drop_shadow = mcrfpy.Caption(
|
||||
text="Crypt Of Sokoban", pos=(150, 10), font=res.font,
|
||||
fill_color=(96, 96, 96), outline_color=(192, 0, 0),
|
||||
)
|
||||
drop_shadow.outline = 3
|
||||
drop_shadow.font_size = 64
|
||||
|
||||
title = mcrfpy.Caption(
|
||||
text="Crypt Of Sokoban", pos=(158, 18), font=res.font,
|
||||
fill_color=COLOR_TEXT,
|
||||
)
|
||||
title.font_size = 64
|
||||
|
||||
# -- Toast notification -------------------------------------------
|
||||
self.toast = mcrfpy.Caption(
|
||||
text="", pos=(150, 400), font=res.font,
|
||||
fill_color=(0, 0, 0),
|
||||
)
|
||||
self.toast.font_size = 28
|
||||
self.toast.outline = 2
|
||||
self.toast.outline_color = (255, 255, 255)
|
||||
self._toast_remaining = None
|
||||
self._toast_timer = None
|
||||
|
||||
# -- Buttons ------------------------------------------------------
|
||||
play_btn = SweetButton(
|
||||
(20, 248), "PLAY",
|
||||
box_width=200, box_height=110,
|
||||
icon=1, icon_scale=2.0, click=self._on_play,
|
||||
)
|
||||
|
||||
config_btn = SweetButton(
|
||||
(10, 678), "Settings", icon=2, click=self._on_config,
|
||||
)
|
||||
|
||||
scale_btn = SweetButton(
|
||||
(266, 678), "Scale up\nto 1080p", icon=15, click=self._on_scale,
|
||||
)
|
||||
self._scaled = False
|
||||
|
||||
music_btn = SweetButton(
|
||||
(522, 678), "Music\nON", icon=12, click=self._on_music_toggle,
|
||||
)
|
||||
|
||||
sfx_btn = SweetButton(
|
||||
(778, 678), "SFX\nON", icon=0, click=self._on_sfx_toggle,
|
||||
)
|
||||
|
||||
# -- Assemble scene -----------------------------------------------
|
||||
for element in (
|
||||
self.grid, drop_shadow, title, self.toast,
|
||||
play_btn.base_frame, config_btn.base_frame,
|
||||
scale_btn.base_frame, music_btn.base_frame,
|
||||
sfx_btn.base_frame,
|
||||
):
|
||||
self.ui.append(element)
|
||||
|
||||
# -- Entity registry (minimal, for demo only) -------------------------
|
||||
|
||||
def register_entity(self, entity):
|
||||
self.entities.append(entity)
|
||||
|
||||
def unregister_entity(self, entity):
|
||||
if entity in self.entities:
|
||||
self.entities.remove(entity)
|
||||
|
||||
# -- Demo animation ---------------------------------------------------
|
||||
|
||||
def _spawn_demo_entities(self, coords):
|
||||
"""Spawn entities for the demo grid. Reuses the same entity
|
||||
creation as PlayScene to avoid the 7DRL's duplicated spawning."""
|
||||
buttons = []
|
||||
for name, pos in sorted(coords, key=lambda c: c[0]):
|
||||
if name == "spawn":
|
||||
self.player = PlayerEntity(game=self)
|
||||
self.player.draw_pos = pos
|
||||
elif name == "boulder":
|
||||
BoulderEntity(pos[0], pos[1], game=self)
|
||||
elif name == "button":
|
||||
buttons.append(pos)
|
||||
elif name == "exit":
|
||||
btn = buttons.pop(0)
|
||||
ExitEntity(pos[0], pos[1], btn[0], btn[1], game=self)
|
||||
elif name in ENEMY_PRESETS:
|
||||
EnemyEntity.from_preset(name, pos[0], pos[1], game=self)
|
||||
|
||||
def _demo_tick(self, timer, runtime):
|
||||
"""Timer callback: animate demo entities randomly."""
|
||||
try:
|
||||
dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))
|
||||
self.player.try_move(*random.choice(dirs))
|
||||
for entity in self.entities:
|
||||
entity.act()
|
||||
except Exception:
|
||||
pass # demo animation is cosmetic; don't crash the menu
|
||||
|
||||
# -- Navigation -------------------------------------------------------
|
||||
|
||||
def activate(self):
|
||||
self.scene.activate()
|
||||
|
||||
def _on_play(self, btn, args):
|
||||
if args[3] == mcrfpy.InputState.RELEASED:
|
||||
return
|
||||
self.demo_timer.stop()
|
||||
from cos.scenes.play import PlayScene
|
||||
self.play_scene = PlayScene()
|
||||
self.play_scene.activate()
|
||||
|
||||
# -- Settings buttons -------------------------------------------------
|
||||
|
||||
def _on_config(self, btn, args):
|
||||
if args[3] == mcrfpy.InputState.RELEASED:
|
||||
return
|
||||
self._show_toast("Settings will go here.")
|
||||
|
||||
def _on_scale(self, btn, args):
|
||||
if args[3] == mcrfpy.InputState.RELEASED:
|
||||
return
|
||||
self._scaled = not self._scaled
|
||||
btn.unpress()
|
||||
if self._scaled:
|
||||
self._show_toast("Windowed mode only.\nCheck Settings for fine-tuned controls.")
|
||||
mcrfpy.set_scale(1.3)
|
||||
btn.text = "Scale down\nto 1.0x"
|
||||
else:
|
||||
mcrfpy.set_scale(1.0)
|
||||
btn.text = "Scale up\nto 1080p"
|
||||
|
||||
def _on_music_toggle(self, btn, args):
|
||||
if args[3] == mcrfpy.InputState.RELEASED:
|
||||
return
|
||||
res = Resources()
|
||||
res.music_enabled = not res.music_enabled
|
||||
if res.music_enabled:
|
||||
res.set_music_volume(res.music_volume)
|
||||
btn.text = "Music\nON"
|
||||
btn.sprite_number = 12
|
||||
else:
|
||||
self._show_toast("Use your volume keys or\nlook in Settings for a volume meter.")
|
||||
res.set_music_volume(0)
|
||||
btn.text = "Music\nOFF"
|
||||
btn.sprite_number = 17
|
||||
|
||||
def _on_sfx_toggle(self, btn, args):
|
||||
if args[3] == mcrfpy.InputState.RELEASED:
|
||||
return
|
||||
res = Resources()
|
||||
res.sfx_enabled = not res.sfx_enabled
|
||||
if res.sfx_enabled:
|
||||
res.set_sfx_volume(res.sfx_volume)
|
||||
btn.text = "SFX\nON"
|
||||
btn.sprite_number = 0
|
||||
else:
|
||||
self._show_toast("Use your volume keys or\nlook in Settings for a volume meter.")
|
||||
res.set_sfx_volume(0)
|
||||
btn.text = "SFX\nOFF"
|
||||
btn.sprite_number = 17
|
||||
|
||||
# -- Toast notification -----------------------------------------------
|
||||
|
||||
def _show_toast(self, text):
|
||||
self.toast.text = text
|
||||
self._toast_remaining = 350
|
||||
self.toast.fill_color = (255, 255, 255, 255)
|
||||
self.toast.outline_color = (0, 0, 0, 255)
|
||||
if self._toast_timer:
|
||||
self._toast_timer.stop()
|
||||
self._toast_timer = mcrfpy.Timer("toast_timer", self._toast_tick, 100)
|
||||
|
||||
def _toast_tick(self, timer, runtime):
|
||||
self._toast_remaining -= 5
|
||||
if self._toast_remaining < 0:
|
||||
self._toast_timer.stop()
|
||||
self._toast_timer = None
|
||||
self.toast.text = ""
|
||||
return
|
||||
alpha = min(self._toast_remaining, 255)
|
||||
self.toast.fill_color = (255, 255, 255, alpha)
|
||||
self.toast.outline_color = (0, 0, 0, alpha)
|
||||
|
|
@ -729,10 +729,10 @@ class MainMenu:
|
|||
sweet_btn.unpress()
|
||||
if self.scaled:
|
||||
self.toast_say("Windowed mode only, sorry!\nCheck Settings for for fine-tuned controls.")
|
||||
mcrfpy.setScale(window_scale)
|
||||
mcrfpy.set_scale(window_scale)
|
||||
sweet_btn.text = "Scale down\n to 1.0x"
|
||||
else:
|
||||
mcrfpy.setScale(1.0)
|
||||
mcrfpy.set_scale(1.0)
|
||||
sweet_btn.text = "Scale up\nto 1080p"
|
||||
|
||||
def music_toggle(self, sweet_btn, args):
|
||||
|
|
|
|||
803
src/scripts_demo/game.py
Normal file
803
src/scripts_demo/game.py
Normal file
|
|
@ -0,0 +1,803 @@
|
|||
"""McRogueFace Web Demo - A roguelike dungeon crawler showcasing engine features.
|
||||
|
||||
Features demonstrated:
|
||||
- BSP dungeon generation with corridors
|
||||
- Wang tile autotiling for pretty dungeons
|
||||
- Entity system with player and enemies
|
||||
- Field of view with fog of war
|
||||
- Turn-based bump combat
|
||||
- UI overlays (health bar, messages, depth counter)
|
||||
- Animations and timers
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# -- Assets ------------------------------------------------------------------
|
||||
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
|
||||
font = mcrfpy.Font("assets/JetbrainsMono.ttf")
|
||||
|
||||
# Try to load Wang tileset for pretty autotiling
|
||||
try:
|
||||
_tileset = mcrfpy.TileSetFile("assets/kenney_TD_MR_IP.tsx")
|
||||
_wang_set = _tileset.wang_set("dungeon")
|
||||
_Terrain = _wang_set.terrain_enum()
|
||||
HAS_WANG = True
|
||||
except Exception:
|
||||
HAS_WANG = False
|
||||
|
||||
# -- Sprite indices from kenney_TD_MR_IP (12 cols x 55 rows, 16x16) ----------
|
||||
# Rows are 0-indexed; sprite_index = row * 12 + col
|
||||
FLOOR_TILE = 145 # open stone floor
|
||||
WALL_TILE = 251 # solid wall
|
||||
PLAYER_SPRITE = 84 # hero character
|
||||
RAT_SPRITE = 123 # small rat enemy
|
||||
CYCLOPS_SPRITE = 109 # big enemy
|
||||
SKELETON_SPRITE = 110 # skeleton enemy
|
||||
HEART_FULL = 210
|
||||
HEART_HALF = 209
|
||||
HEART_EMPTY = 208
|
||||
POTION_SPRITE = 115 # red potion
|
||||
STAIRS_SPRITE = 91 # stairs down
|
||||
SKULL_SPRITE = 135 # death indicator
|
||||
TREASURE_SPRITE = 127 # treasure chest
|
||||
|
||||
# -- Configuration ------------------------------------------------------------
|
||||
MAP_W, MAP_H = 40, 30
|
||||
ZOOM = 2.0
|
||||
GRID_PX_W = 1024
|
||||
GRID_PX_H = 700
|
||||
FOV_RADIUS = 10
|
||||
MAX_HP = 10
|
||||
ENEMY_SIGHT = 8
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Wang tile autotiling
|
||||
# =============================================================================
|
||||
def paint_tiles(grid, w, h):
|
||||
"""Apply Wang tile autotiling or fallback to simple tiles."""
|
||||
if HAS_WANG:
|
||||
dm = mcrfpy.DiscreteMap((w, h))
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
if grid.at((x, y)).walkable:
|
||||
dm.set(x, y, int(_Terrain.GROUND))
|
||||
else:
|
||||
dm.set(x, y, int(_Terrain.WALL))
|
||||
tiles = _wang_set.resolve(dm)
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
tid = tiles[y * w + x]
|
||||
if tid >= 0:
|
||||
grid.at((x, y)).tilesprite = tid
|
||||
else:
|
||||
grid.at((x, y)).tilesprite = (
|
||||
FLOOR_TILE if grid.at((x, y)).walkable else WALL_TILE
|
||||
)
|
||||
else:
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
grid.at((x, y)).tilesprite = (
|
||||
FLOOR_TILE if grid.at((x, y)).walkable else WALL_TILE
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon generator (BSP)
|
||||
# =============================================================================
|
||||
class Dungeon:
|
||||
def __init__(self, grid, w, h):
|
||||
self.grid = grid
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.rooms = [] # list of (cx, cy) room centers
|
||||
self.walkable = set() # walkable cell coords
|
||||
|
||||
def generate(self):
|
||||
# Reset all cells to walls
|
||||
for x in range(self.w):
|
||||
for y in range(self.h):
|
||||
self.grid.at((x, y)).walkable = False
|
||||
self.grid.at((x, y)).transparent = False
|
||||
|
||||
# BSP split
|
||||
bsp = mcrfpy.BSP(pos=(1, 1), size=(self.w - 2, self.h - 2))
|
||||
bsp.split_recursive(depth=5, min_size=(4, 4), max_ratio=1.5)
|
||||
leaves = list(bsp.leaves())
|
||||
|
||||
# Carve rooms (1-cell margin from leaf edges)
|
||||
for leaf in leaves:
|
||||
lx, ly = int(leaf.pos[0]), int(leaf.pos[1])
|
||||
lw, lh = int(leaf.size[0]), int(leaf.size[1])
|
||||
cx = lx + lw // 2
|
||||
cy = ly + lh // 2
|
||||
self.rooms.append((cx, cy))
|
||||
for rx in range(lx + 1, lx + lw - 1):
|
||||
for ry in range(ly + 1, ly + lh - 1):
|
||||
if 0 <= rx < self.w and 0 <= ry < self.h:
|
||||
self.grid.at((rx, ry)).walkable = True
|
||||
self.grid.at((rx, ry)).transparent = True
|
||||
self.walkable.add((rx, ry))
|
||||
|
||||
# Carve corridors using BSP adjacency
|
||||
adj = bsp.adjacency
|
||||
connected = set()
|
||||
for i in range(len(adj)):
|
||||
for j in adj[i]:
|
||||
edge = (min(i, j), max(i, j))
|
||||
if edge in connected:
|
||||
continue
|
||||
connected.add(edge)
|
||||
self._dig_corridor(self.rooms[i], self.rooms[j])
|
||||
|
||||
# Apply tile graphics
|
||||
paint_tiles(self.grid, self.w, self.h)
|
||||
|
||||
def _dig_corridor(self, start, end):
|
||||
x1, x2 = min(start[0], end[0]), max(start[0], end[0])
|
||||
y1, y2 = min(start[1], end[1]), max(start[1], end[1])
|
||||
# L-shaped corridor
|
||||
if random.random() < 0.5:
|
||||
tx, ty = x1, y2
|
||||
else:
|
||||
tx, ty = x2, y1
|
||||
for x in range(x1, x2 + 1):
|
||||
if 0 <= x < self.w and 0 <= ty < self.h:
|
||||
self.grid.at((x, ty)).walkable = True
|
||||
self.grid.at((x, ty)).transparent = True
|
||||
self.walkable.add((x, ty))
|
||||
for y in range(y1, y2 + 1):
|
||||
if 0 <= tx < self.w and 0 <= y < self.h:
|
||||
self.grid.at((tx, y)).walkable = True
|
||||
self.grid.at((tx, y)).transparent = True
|
||||
self.walkable.add((tx, y))
|
||||
|
||||
def random_floor(self, exclude=None):
|
||||
"""Find a random walkable cell not in the exclude set."""
|
||||
exclude = exclude or set()
|
||||
candidates = list(self.walkable - exclude)
|
||||
if not candidates:
|
||||
return None
|
||||
return random.choice(candidates)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Game state
|
||||
# =============================================================================
|
||||
class Game:
|
||||
def __init__(self):
|
||||
self.scene = mcrfpy.Scene("game")
|
||||
self.ui = self.scene.children
|
||||
self.depth = 1
|
||||
self.player_hp = MAX_HP
|
||||
self.player_max_hp = MAX_HP
|
||||
self.player_atk = 2
|
||||
self.player_def = 0
|
||||
self.score = 0
|
||||
self.game_over = False
|
||||
self.enemies = [] # list of dicts: {entity, hp, atk, def, sprite, name}
|
||||
self.items = [] # list of dicts: {entity, kind}
|
||||
self.dungeon = None
|
||||
self.fog_layer = None
|
||||
self.grid = None
|
||||
self.player = None
|
||||
self.message_timer = None
|
||||
self.occupied = set() # cells occupied by entities
|
||||
|
||||
self._build_ui()
|
||||
self._new_level()
|
||||
|
||||
self.scene.on_key = self.on_key
|
||||
self.scene.activate()
|
||||
|
||||
# -- UI -------------------------------------------------------------------
|
||||
def _build_ui(self):
|
||||
# Main grid
|
||||
self.grid = mcrfpy.Grid(
|
||||
grid_size=(MAP_W, MAP_H),
|
||||
texture=texture,
|
||||
pos=(0, 0),
|
||||
size=(GRID_PX_W, GRID_PX_H),
|
||||
)
|
||||
self.grid.zoom = ZOOM
|
||||
self.grid.center = (MAP_W / 2.0 * 16, MAP_H / 2.0 * 16)
|
||||
self.ui.append(self.grid)
|
||||
|
||||
# HUD bar at bottom
|
||||
self.hud = mcrfpy.Frame(
|
||||
pos=(0, GRID_PX_H), size=(1024, 68),
|
||||
fill_color=(20, 16, 28, 240)
|
||||
)
|
||||
self.ui.append(self.hud)
|
||||
|
||||
# Health display
|
||||
self.health_label = mcrfpy.Caption(
|
||||
text="HP: 10/10", pos=(12, 6), font=font,
|
||||
fill_color=(220, 50, 50)
|
||||
)
|
||||
self.health_label.font_size = 20
|
||||
self.health_label.outline = 2
|
||||
self.health_label.outline_color = (0, 0, 0)
|
||||
self.hud.children.append(self.health_label)
|
||||
|
||||
# Heart sprites
|
||||
self.hearts = []
|
||||
for i in range(5):
|
||||
h = mcrfpy.Sprite(
|
||||
x=12 + i * 36, y=32,
|
||||
texture=texture, sprite_index=HEART_FULL, scale=2.0
|
||||
)
|
||||
self.hearts.append(h)
|
||||
self.hud.children.append(h)
|
||||
|
||||
# Depth label
|
||||
self.depth_label = mcrfpy.Caption(
|
||||
text="Depth: 1", pos=(220, 6), font=font,
|
||||
fill_color=(180, 180, 220)
|
||||
)
|
||||
self.depth_label.font_size = 20
|
||||
self.depth_label.outline = 2
|
||||
self.depth_label.outline_color = (0, 0, 0)
|
||||
self.hud.children.append(self.depth_label)
|
||||
|
||||
# Score label
|
||||
self.score_label = mcrfpy.Caption(
|
||||
text="Score: 0", pos=(220, 32), font=font,
|
||||
fill_color=(220, 200, 80)
|
||||
)
|
||||
self.score_label.font_size = 18
|
||||
self.score_label.outline = 2
|
||||
self.score_label.outline_color = (0, 0, 0)
|
||||
self.hud.children.append(self.score_label)
|
||||
|
||||
# Message area
|
||||
self.msg_label = mcrfpy.Caption(
|
||||
text="Arrow keys to move. Bump enemies to attack.",
|
||||
pos=(450, 6), font=font,
|
||||
fill_color=(160, 200, 160)
|
||||
)
|
||||
self.msg_label.font_size = 16
|
||||
self.msg_label.outline = 1
|
||||
self.msg_label.outline_color = (0, 0, 0)
|
||||
self.hud.children.append(self.msg_label)
|
||||
|
||||
self.msg_label2 = mcrfpy.Caption(
|
||||
text="Find the stairs to descend deeper!",
|
||||
pos=(450, 30), font=font,
|
||||
fill_color=(140, 160, 180)
|
||||
)
|
||||
self.msg_label2.font_size = 14
|
||||
self.msg_label2.outline = 1
|
||||
self.msg_label2.outline_color = (0, 0, 0)
|
||||
self.hud.children.append(self.msg_label2)
|
||||
|
||||
# -- Level generation -----------------------------------------------------
|
||||
def _new_level(self):
|
||||
# Clear old entities
|
||||
while len(self.grid.entities) > 0:
|
||||
self.grid.entities.pop(0)
|
||||
self.enemies.clear()
|
||||
self.items.clear()
|
||||
self.occupied.clear()
|
||||
|
||||
# Generate dungeon
|
||||
self.dungeon = Dungeon(self.grid, MAP_W, MAP_H)
|
||||
self.dungeon.generate()
|
||||
|
||||
# Place player in first room
|
||||
px, py = self.dungeon.rooms[0]
|
||||
if self.player is None:
|
||||
self.player = mcrfpy.Entity(
|
||||
grid_pos=(px, py), texture=texture,
|
||||
sprite_index=PLAYER_SPRITE
|
||||
)
|
||||
else:
|
||||
self.player.grid_pos = (px, py)
|
||||
self.grid.entities.append(self.player)
|
||||
self.occupied.add((px, py))
|
||||
|
||||
# Place stairs in last room
|
||||
sx, sy = self.dungeon.rooms[-1]
|
||||
stairs = mcrfpy.Entity(
|
||||
grid_pos=(sx, sy), texture=texture,
|
||||
sprite_index=STAIRS_SPRITE
|
||||
)
|
||||
self.grid.entities.append(stairs)
|
||||
self.stairs_pos = (sx, sy)
|
||||
|
||||
# Place enemies (more enemies on deeper levels)
|
||||
num_enemies = min(3 + self.depth * 2, 15)
|
||||
enemy_types = self._enemy_table()
|
||||
for _ in range(num_enemies):
|
||||
pos = self.dungeon.random_floor(exclude=self.occupied)
|
||||
if pos is None:
|
||||
break
|
||||
etype = random.choice(enemy_types)
|
||||
e = mcrfpy.Entity(
|
||||
grid_pos=pos, texture=texture,
|
||||
sprite_index=etype["sprite"]
|
||||
)
|
||||
self.grid.entities.append(e)
|
||||
self.enemies.append({
|
||||
"entity": e,
|
||||
"hp": etype["hp"],
|
||||
"max_hp": etype["hp"],
|
||||
"atk": etype["atk"],
|
||||
"def": etype["def"],
|
||||
"name": etype["name"],
|
||||
"sprite": etype["sprite"],
|
||||
})
|
||||
self.occupied.add(pos)
|
||||
|
||||
# Place health potions
|
||||
num_potions = random.randint(1, 3)
|
||||
for _ in range(num_potions):
|
||||
pos = self.dungeon.random_floor(exclude=self.occupied)
|
||||
if pos is None:
|
||||
break
|
||||
item = mcrfpy.Entity(
|
||||
grid_pos=pos, texture=texture,
|
||||
sprite_index=POTION_SPRITE
|
||||
)
|
||||
self.grid.entities.append(item)
|
||||
self.items.append({"entity": item, "kind": "potion", "pos": pos})
|
||||
self.occupied.add(pos)
|
||||
|
||||
# Place treasure
|
||||
num_treasure = random.randint(1, 2 + self.depth)
|
||||
for _ in range(num_treasure):
|
||||
pos = self.dungeon.random_floor(exclude=self.occupied)
|
||||
if pos is None:
|
||||
break
|
||||
item = mcrfpy.Entity(
|
||||
grid_pos=pos, texture=texture,
|
||||
sprite_index=TREASURE_SPRITE
|
||||
)
|
||||
self.grid.entities.append(item)
|
||||
self.items.append({"entity": item, "kind": "treasure", "pos": pos})
|
||||
self.occupied.add(pos)
|
||||
|
||||
# Set up fog of war
|
||||
self.fog_layer = mcrfpy.ColorLayer(name="fog", z_index=10)
|
||||
self.grid.add_layer(self.fog_layer)
|
||||
self.fog_layer.fill(mcrfpy.Color(0, 0, 0, 255))
|
||||
self.discovered = set()
|
||||
|
||||
# Center camera on player
|
||||
self._center_camera()
|
||||
self._update_fov()
|
||||
self._update_hud()
|
||||
|
||||
# Depth label
|
||||
self.depth_label.text = f"Depth: {self.depth}"
|
||||
|
||||
def _enemy_table(self):
|
||||
"""Return available enemy types scaled by depth."""
|
||||
table = [
|
||||
{"name": "Rat", "sprite": RAT_SPRITE,
|
||||
"hp": 2 + self.depth // 3, "atk": 1, "def": 0},
|
||||
]
|
||||
if self.depth >= 2:
|
||||
table.append(
|
||||
{"name": "Skeleton", "sprite": SKELETON_SPRITE,
|
||||
"hp": 3 + self.depth // 2, "atk": 2, "def": 1}
|
||||
)
|
||||
if self.depth >= 4:
|
||||
table.append(
|
||||
{"name": "Cyclops", "sprite": CYCLOPS_SPRITE,
|
||||
"hp": 6 + self.depth, "atk": 3, "def": 2}
|
||||
)
|
||||
return table
|
||||
|
||||
# -- Camera ---------------------------------------------------------------
|
||||
def _center_camera(self):
|
||||
px = int(self.player.grid_pos.x)
|
||||
py = int(self.player.grid_pos.y)
|
||||
self.grid.center = (px * 16 + 8, py * 16 + 8)
|
||||
|
||||
# -- FOV ------------------------------------------------------------------
|
||||
def _update_fov(self):
|
||||
if self.fog_layer is None:
|
||||
return
|
||||
px = int(self.player.grid_pos.x)
|
||||
py = int(self.player.grid_pos.y)
|
||||
self.grid.compute_fov((px, py), radius=FOV_RADIUS)
|
||||
|
||||
for x in range(MAP_W):
|
||||
for y in range(MAP_H):
|
||||
if self.grid.is_in_fov((x, y)):
|
||||
self.discovered.add((x, y))
|
||||
self.fog_layer.set((x, y), mcrfpy.Color(0, 0, 0, 0))
|
||||
elif (x, y) in self.discovered:
|
||||
self.fog_layer.set((x, y), mcrfpy.Color(0, 0, 0, 140))
|
||||
# else: stays at 255 alpha (fully hidden)
|
||||
|
||||
# -- HUD ------------------------------------------------------------------
|
||||
def _update_hud(self):
|
||||
self.health_label.text = f"HP: {self.player_hp}/{self.player_max_hp}"
|
||||
self.score_label.text = f"Score: {self.score}"
|
||||
# Update heart sprites
|
||||
for i, h in enumerate(self.hearts):
|
||||
full = self.player_hp - i * 2
|
||||
cap = self.player_max_hp - i * 2
|
||||
if cap < 1:
|
||||
h.sprite_index = 659 # invisible/blank
|
||||
elif full >= 2:
|
||||
h.sprite_index = HEART_FULL
|
||||
elif full == 1:
|
||||
h.sprite_index = HEART_HALF
|
||||
else:
|
||||
h.sprite_index = HEART_EMPTY
|
||||
|
||||
def _show_message(self, line1, line2=""):
|
||||
self.msg_label.text = line1
|
||||
self.msg_label2.text = line2
|
||||
|
||||
# -- Combat ---------------------------------------------------------------
|
||||
def _attack_enemy(self, enemy_data):
|
||||
dmg = max(1, self.player_atk - enemy_data["def"])
|
||||
enemy_data["hp"] -= dmg
|
||||
name = enemy_data["name"]
|
||||
if enemy_data["hp"] <= 0:
|
||||
self._show_message(
|
||||
f"You slay the {name}! (+{10 * self.depth} pts)",
|
||||
f"Hit for {dmg} damage - lethal!"
|
||||
)
|
||||
self.score += 10 * self.depth
|
||||
# Remove from grid
|
||||
for i in range(len(self.grid.entities)):
|
||||
if self.grid.entities[i] is enemy_data["entity"]:
|
||||
self.grid.entities.pop(i)
|
||||
break
|
||||
ex = int(enemy_data["entity"].grid_pos.x)
|
||||
ey = int(enemy_data["entity"].grid_pos.y)
|
||||
self.occupied.discard((ex, ey))
|
||||
self.enemies.remove(enemy_data)
|
||||
else:
|
||||
self._show_message(
|
||||
f"You hit the {name} for {dmg}!",
|
||||
f"{name} HP: {enemy_data['hp']}/{enemy_data['max_hp']}"
|
||||
)
|
||||
|
||||
def _enemy_attacks_player(self, enemy_data):
|
||||
dmg = max(1, enemy_data["atk"] - self.player_def)
|
||||
self.player_hp -= dmg
|
||||
name = enemy_data["name"]
|
||||
if self.player_hp <= 0:
|
||||
self.player_hp = 0
|
||||
self._update_hud()
|
||||
self._game_over()
|
||||
return
|
||||
self._show_message(
|
||||
f"The {name} hits you for {dmg}!",
|
||||
f"HP: {self.player_hp}/{self.player_max_hp}"
|
||||
)
|
||||
|
||||
def _game_over(self):
|
||||
self.game_over = True
|
||||
# Darken screen
|
||||
overlay = mcrfpy.Frame(
|
||||
pos=(0, 0), size=(1024, 768),
|
||||
fill_color=(0, 0, 0, 180)
|
||||
)
|
||||
self.ui.append(overlay)
|
||||
|
||||
title = mcrfpy.Caption(
|
||||
text="YOU DIED", pos=(340, 250), font=font,
|
||||
fill_color=(200, 30, 30)
|
||||
)
|
||||
title.font_size = 60
|
||||
title.outline = 4
|
||||
title.outline_color = (0, 0, 0)
|
||||
overlay.children.append(title)
|
||||
|
||||
info = mcrfpy.Caption(
|
||||
text=f"Reached depth {self.depth} with {self.score} points",
|
||||
pos=(280, 340), font=font,
|
||||
fill_color=(200, 200, 200)
|
||||
)
|
||||
info.font_size = 22
|
||||
info.outline = 2
|
||||
info.outline_color = (0, 0, 0)
|
||||
overlay.children.append(info)
|
||||
|
||||
restart = mcrfpy.Caption(
|
||||
text="Press R to restart",
|
||||
pos=(370, 400), font=font,
|
||||
fill_color=(160, 200, 160)
|
||||
)
|
||||
restart.font_size = 20
|
||||
restart.outline = 2
|
||||
restart.outline_color = (0, 0, 0)
|
||||
overlay.children.append(restart)
|
||||
self.game_over_overlay = overlay
|
||||
|
||||
def _restart(self):
|
||||
self.game_over = False
|
||||
self.player_hp = MAX_HP
|
||||
self.player_max_hp = MAX_HP
|
||||
self.player_atk = 2
|
||||
self.player_def = 0
|
||||
self.depth = 1
|
||||
self.score = 0
|
||||
self.player = None
|
||||
# Remove overlay
|
||||
if hasattr(self, 'game_over_overlay'):
|
||||
# Rebuild UI from scratch
|
||||
while len(self.ui) > 0:
|
||||
self.ui.pop(0)
|
||||
self._build_ui()
|
||||
self._new_level()
|
||||
self.scene.on_key = self.on_key
|
||||
|
||||
# -- Items ----------------------------------------------------------------
|
||||
def _check_items(self, px, py):
|
||||
for item in self.items[:]:
|
||||
ix = int(item["entity"].grid_pos.x)
|
||||
iy = int(item["entity"].grid_pos.y)
|
||||
if ix == px and iy == py:
|
||||
if item["kind"] == "potion":
|
||||
heal = random.randint(2, 4)
|
||||
self.player_hp = min(self.player_max_hp, self.player_hp + heal)
|
||||
self._show_message(
|
||||
f"Healed {heal} HP!",
|
||||
f"HP: {self.player_hp}/{self.player_max_hp}"
|
||||
)
|
||||
elif item["kind"] == "treasure":
|
||||
pts = random.randint(5, 15) * self.depth
|
||||
self.score += pts
|
||||
self._show_message(
|
||||
f"Found treasure! (+{pts} pts)",
|
||||
f"Total score: {self.score}"
|
||||
)
|
||||
# Remove item entity
|
||||
for i in range(len(self.grid.entities)):
|
||||
if self.grid.entities[i] is item["entity"]:
|
||||
self.grid.entities.pop(i)
|
||||
break
|
||||
self.occupied.discard((ix, iy))
|
||||
self.items.remove(item)
|
||||
|
||||
# -- Enemy AI (simple: move toward player if visible) ---------------------
|
||||
def _enemy_turn(self):
|
||||
px = int(self.player.grid_pos.x)
|
||||
py = int(self.player.grid_pos.y)
|
||||
|
||||
for edata in self.enemies[:]:
|
||||
ex = int(edata["entity"].grid_pos.x)
|
||||
ey = int(edata["entity"].grid_pos.y)
|
||||
|
||||
# Only act if in FOV (player can see them)
|
||||
if not self.grid.is_in_fov((ex, ey)):
|
||||
continue
|
||||
|
||||
# Manhattan distance
|
||||
dist = abs(ex - px) + abs(ey - py)
|
||||
if dist > ENEMY_SIGHT:
|
||||
continue
|
||||
|
||||
# Adjacent? Attack!
|
||||
if dist == 1:
|
||||
self._enemy_attacks_player(edata)
|
||||
if self.game_over:
|
||||
return
|
||||
continue
|
||||
|
||||
# Move toward player (simple greedy)
|
||||
dx = 0
|
||||
dy = 0
|
||||
if abs(ex - px) > abs(ey - py):
|
||||
dx = 1 if px > ex else -1
|
||||
else:
|
||||
dy = 1 if py > ey else -1
|
||||
|
||||
nx, ny = ex + dx, ey + dy
|
||||
|
||||
# Check bounds and walkability
|
||||
if (0 <= nx < MAP_W and 0 <= ny < MAP_H
|
||||
and self.grid.at((nx, ny)).walkable
|
||||
and (nx, ny) not in self.occupied):
|
||||
self.occupied.discard((ex, ey))
|
||||
edata["entity"].grid_pos = (nx, ny)
|
||||
self.occupied.add((nx, ny))
|
||||
|
||||
# -- Input ----------------------------------------------------------------
|
||||
def on_key(self, key, state):
|
||||
if state != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
if self.game_over:
|
||||
if key == mcrfpy.Key.R:
|
||||
self._restart()
|
||||
return
|
||||
|
||||
dx, dy = 0, 0
|
||||
if key == mcrfpy.Key.UP or key == mcrfpy.Key.W:
|
||||
dy = -1
|
||||
elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S:
|
||||
dy = 1
|
||||
elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A:
|
||||
dx = -1
|
||||
elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D:
|
||||
dx = 1
|
||||
elif key == mcrfpy.Key.PERIOD:
|
||||
# Wait a turn
|
||||
self._enemy_turn()
|
||||
self._update_fov()
|
||||
self._update_hud()
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
if dx == 0 and dy == 0:
|
||||
return
|
||||
|
||||
px = int(self.player.grid_pos.x)
|
||||
py = int(self.player.grid_pos.y)
|
||||
nx, ny = px + dx, py + dy
|
||||
|
||||
# Bounds check
|
||||
if nx < 0 or nx >= MAP_W or ny < 0 or ny >= MAP_H:
|
||||
return
|
||||
|
||||
# Check for enemy at target
|
||||
for edata in self.enemies:
|
||||
ex = int(edata["entity"].grid_pos.x)
|
||||
ey = int(edata["entity"].grid_pos.y)
|
||||
if ex == nx and ey == ny:
|
||||
self._attack_enemy(edata)
|
||||
self._enemy_turn()
|
||||
self._update_fov()
|
||||
self._center_camera()
|
||||
self._update_hud()
|
||||
return
|
||||
|
||||
# Check walkability
|
||||
if not self.grid.at((nx, ny)).walkable:
|
||||
return
|
||||
|
||||
# Move player
|
||||
self.occupied.discard((px, py))
|
||||
self.player.grid_pos = (nx, ny)
|
||||
self.occupied.add((nx, ny))
|
||||
|
||||
# Check stairs
|
||||
if (nx, ny) == self.stairs_pos:
|
||||
self.depth += 1
|
||||
self._show_message(
|
||||
f"Descending to depth {self.depth}...",
|
||||
"The dungeon grows more dangerous."
|
||||
)
|
||||
self._new_level()
|
||||
self._enemy_turn()
|
||||
self._update_fov()
|
||||
self._center_camera()
|
||||
self._update_hud()
|
||||
return
|
||||
|
||||
# Check items
|
||||
self._check_items(nx, ny)
|
||||
|
||||
# Enemy turn
|
||||
self._enemy_turn()
|
||||
if not self.game_over:
|
||||
self._update_fov()
|
||||
self._center_camera()
|
||||
self._update_hud()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Title screen
|
||||
# =============================================================================
|
||||
class TitleScreen:
|
||||
def __init__(self):
|
||||
self.scene = mcrfpy.Scene("title")
|
||||
ui = self.scene.children
|
||||
|
||||
# Dark background
|
||||
bg = mcrfpy.Frame(
|
||||
pos=(0, 0), size=(1024, 768),
|
||||
fill_color=(12, 10, 20)
|
||||
)
|
||||
ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(
|
||||
text="McRogueFace", pos=(240, 140), font=font,
|
||||
fill_color=(220, 60, 60)
|
||||
)
|
||||
title.font_size = 72
|
||||
title.outline = 4
|
||||
title.outline_color = (0, 0, 0)
|
||||
bg.children.append(title)
|
||||
|
||||
# Subtitle
|
||||
sub = mcrfpy.Caption(
|
||||
text="A Python-Powered Roguelike Engine",
|
||||
pos=(270, 240), font=font,
|
||||
fill_color=(160, 160, 200)
|
||||
)
|
||||
sub.font_size = 22
|
||||
sub.outline = 2
|
||||
sub.outline_color = (0, 0, 0)
|
||||
bg.children.append(sub)
|
||||
|
||||
# Features list
|
||||
features = [
|
||||
"BSP dungeon generation",
|
||||
"Wang tile autotiling",
|
||||
"Field of view & fog of war",
|
||||
"Turn-based combat",
|
||||
"Entity system with Python scripting",
|
||||
]
|
||||
for i, feat in enumerate(features):
|
||||
dot = mcrfpy.Caption(
|
||||
text=f" {feat}",
|
||||
pos=(320, 320 + i * 32), font=font,
|
||||
fill_color=(140, 180, 140)
|
||||
)
|
||||
dot.font_size = 16
|
||||
dot.outline = 1
|
||||
dot.outline_color = (0, 0, 0)
|
||||
bg.children.append(dot)
|
||||
|
||||
# Start prompt
|
||||
start = mcrfpy.Caption(
|
||||
text="Press ENTER or SPACE to begin",
|
||||
pos=(310, 540), font=font,
|
||||
fill_color=(200, 200, 100)
|
||||
)
|
||||
start.font_size = 20
|
||||
start.outline = 2
|
||||
start.outline_color = (0, 0, 0)
|
||||
bg.children.append(start)
|
||||
|
||||
# Animate the start prompt
|
||||
self._blink_visible = True
|
||||
self._start_caption = start
|
||||
|
||||
# Controls hint
|
||||
controls = mcrfpy.Caption(
|
||||
text="Controls: Arrow keys / WASD to move, . to wait, R to restart",
|
||||
pos=(200, 600), font=font,
|
||||
fill_color=(100, 100, 130)
|
||||
)
|
||||
controls.font_size = 14
|
||||
controls.outline = 1
|
||||
controls.outline_color = (0, 0, 0)
|
||||
bg.children.append(controls)
|
||||
|
||||
# Version info
|
||||
ver = mcrfpy.Caption(
|
||||
text="Built with McRogueFace - C++ engine, Python gameplay",
|
||||
pos=(250, 700), font=font,
|
||||
fill_color=(60, 60, 80)
|
||||
)
|
||||
ver.font_size = 12
|
||||
bg.children.append(ver)
|
||||
|
||||
self.scene.on_key = self.on_key
|
||||
self.scene.activate()
|
||||
|
||||
# Blink timer for "Press ENTER"
|
||||
self.blink_timer = mcrfpy.Timer("blink", self._blink, 600)
|
||||
|
||||
def _blink(self, timer, runtime):
|
||||
self._blink_visible = not self._blink_visible
|
||||
if self._blink_visible:
|
||||
self._start_caption.fill_color = mcrfpy.Color(200, 200, 100)
|
||||
else:
|
||||
self._start_caption.fill_color = mcrfpy.Color(200, 200, 100, 60)
|
||||
|
||||
def on_key(self, key, state):
|
||||
if state != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
if key in (mcrfpy.Key.ENTER, mcrfpy.Key.SPACE):
|
||||
self.blink_timer.stop()
|
||||
Game()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Entry point
|
||||
# =============================================================================
|
||||
title = TitleScreen()
|
||||
|
|
@ -1121,22 +1121,10 @@ 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."""
|
||||
...
|
||||
|
||||
def step(dt: float) -> None:
|
||||
"""Advance the game loop by dt seconds (headless mode only)."""
|
||||
...
|
||||
|
|
|
|||
|
|
@ -1298,22 +1298,10 @@ 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."""
|
||||
...
|
||||
|
||||
def step(dt: float) -> None:
|
||||
"""Advance the game loop by dt seconds (headless mode only)."""
|
||||
...
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ def collect_metrics(timer, runtime):
|
|||
return
|
||||
|
||||
# Collect sample
|
||||
m = mcrfpy.getMetrics()
|
||||
m = mcrfpy.get_metrics()
|
||||
metrics_samples.append({
|
||||
'frame_time': m['frame_time'],
|
||||
'avg_frame_time': m['avg_frame_time'],
|
||||
|
|
|
|||
275
tests/regression/issue_288_291_dirty_flags_test.py
Normal file
275
tests/regression/issue_288_291_dirty_flags_test.py
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
"""Test render cache dirty flag propagation for issues #288-#291.
|
||||
|
||||
#288: UICollection mutations don't invalidate parent Frame's render cache
|
||||
#289: Caption Python property setters don't call markDirty()
|
||||
#290: UIDrawable base x/y/pos setters don't propagate dirty flags to parent
|
||||
#291: Audit all Python property setters for missing markDirty() calls
|
||||
|
||||
These tests exercise all property setters that were missing dirty flag calls,
|
||||
inside a clip_children=True Frame (which uses render caching). The test verifies
|
||||
that no crashes occur and properties are correctly set after modification.
|
||||
Visual correctness requires a non-headless render test.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
test_pass = True
|
||||
test_count = 0
|
||||
fail_count = 0
|
||||
|
||||
def check(condition, msg):
|
||||
global test_pass, test_count, fail_count
|
||||
test_count += 1
|
||||
if not condition:
|
||||
print(f" FAIL: {msg}")
|
||||
test_pass = False
|
||||
fail_count += 1
|
||||
|
||||
# Create a scene with a clipped parent frame (uses render caching)
|
||||
scene = mcrfpy.Scene("test_dirty_flags")
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
parent = mcrfpy.Frame(pos=(10, 10), size=(800, 600),
|
||||
fill_color=mcrfpy.Color(40, 40, 40),
|
||||
clip_children=True)
|
||||
scene.children.append(parent)
|
||||
|
||||
# ============================================================
|
||||
# Test #290: UIDrawable base x/y/pos setters (all drawable types)
|
||||
# ============================================================
|
||||
print("Testing #290: UIDrawable position setters...")
|
||||
|
||||
frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100),
|
||||
fill_color=mcrfpy.Color(255, 0, 0))
|
||||
parent.children.append(frame)
|
||||
|
||||
# Test x setter
|
||||
frame.x = 50.0
|
||||
check(frame.x == 50.0, "frame.x setter")
|
||||
|
||||
# Test y setter
|
||||
frame.y = 60.0
|
||||
check(frame.y == 60.0, "frame.y setter")
|
||||
|
||||
# Test pos setter (tuple)
|
||||
frame.pos = (70.0, 80.0)
|
||||
check(frame.x == 70.0 and frame.y == 80.0, "frame.pos setter (tuple)")
|
||||
|
||||
# Test w/h setters
|
||||
frame.w = 200.0
|
||||
check(frame.w == 200.0, "frame.w setter")
|
||||
frame.h = 150.0
|
||||
check(frame.h == 150.0, "frame.h setter")
|
||||
|
||||
# ============================================================
|
||||
# Test #289: Caption property setters
|
||||
# ============================================================
|
||||
print("Testing #289: Caption property setters...")
|
||||
|
||||
cap = mcrfpy.Caption(text="Hello", pos=(100, 100))
|
||||
parent.children.append(cap)
|
||||
|
||||
# Text setter
|
||||
cap.text = "World"
|
||||
check(cap.text == "World", "caption.text setter")
|
||||
|
||||
# Fill color setter
|
||||
cap.fill_color = mcrfpy.Color(255, 0, 0)
|
||||
c = cap.fill_color
|
||||
check(c.r == 255 and c.g == 0 and c.b == 0, "caption.fill_color setter")
|
||||
|
||||
# Outline color setter
|
||||
cap.outline_color = mcrfpy.Color(0, 255, 0)
|
||||
c = cap.outline_color
|
||||
check(c.r == 0 and c.g == 255 and c.b == 0, "caption.outline_color setter")
|
||||
|
||||
# Outline thickness setter
|
||||
cap.outline = 2.0
|
||||
check(cap.outline == 2.0, "caption.outline setter")
|
||||
|
||||
# Font size setter
|
||||
cap.font_size = 24
|
||||
check(cap.font_size == 24, "caption.font_size setter")
|
||||
|
||||
# ============================================================
|
||||
# Test #288: UICollection mutations
|
||||
# ============================================================
|
||||
print("Testing #288: UICollection mutations...")
|
||||
|
||||
# append (already tested above, but test with clip_children parent)
|
||||
child1 = mcrfpy.Frame(pos=(0, 0), size=(20, 20),
|
||||
fill_color=mcrfpy.Color(0, 0, 255))
|
||||
initial_count = len(parent.children)
|
||||
parent.children.append(child1)
|
||||
check(len(parent.children) == initial_count + 1, "collection append")
|
||||
|
||||
# insert
|
||||
child2 = mcrfpy.Frame(pos=(30, 0), size=(20, 20),
|
||||
fill_color=mcrfpy.Color(0, 255, 0))
|
||||
parent.children.insert(0, child2)
|
||||
check(len(parent.children) == initial_count + 2, "collection insert")
|
||||
|
||||
# setitem (replace)
|
||||
child3 = mcrfpy.Frame(pos=(60, 0), size=(20, 20),
|
||||
fill_color=mcrfpy.Color(255, 255, 0))
|
||||
parent.children[0] = child3
|
||||
check(len(parent.children) == initial_count + 2, "collection setitem")
|
||||
|
||||
# remove
|
||||
parent.children.remove(child1)
|
||||
check(len(parent.children) == initial_count + 1, "collection remove")
|
||||
|
||||
# pop
|
||||
popped = parent.children.pop()
|
||||
check(len(parent.children) == initial_count, "collection pop")
|
||||
|
||||
# extend
|
||||
extras = [
|
||||
mcrfpy.Frame(pos=(0, 200), size=(20, 20), fill_color=mcrfpy.Color(128, 128, 128)),
|
||||
mcrfpy.Frame(pos=(30, 200), size=(20, 20), fill_color=mcrfpy.Color(64, 64, 64))
|
||||
]
|
||||
parent.children.extend(extras)
|
||||
check(len(parent.children) == initial_count + 2, "collection extend")
|
||||
|
||||
# slice deletion
|
||||
del parent.children[initial_count:]
|
||||
check(len(parent.children) == initial_count, "collection slice delete")
|
||||
|
||||
# ============================================================
|
||||
# Test #291: UISprite property setters
|
||||
# ============================================================
|
||||
print("Testing #291: UISprite property setters...")
|
||||
|
||||
# Need a texture for sprite tests - use a test texture if available
|
||||
try:
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
sprite = mcrfpy.Sprite(pos=(200, 200), texture=tex, sprite_index=0)
|
||||
parent.children.append(sprite)
|
||||
|
||||
sprite.scale = 2.0
|
||||
check(sprite.scale == 2.0, "sprite.scale setter")
|
||||
|
||||
sprite.sprite_index = 1
|
||||
check(sprite.sprite_index == 1, "sprite.sprite_index setter")
|
||||
|
||||
# Texture setter
|
||||
sprite.texture = tex
|
||||
check(True, "sprite.texture setter (no crash)")
|
||||
|
||||
# Pos setter
|
||||
sprite.pos = (210, 210)
|
||||
check(True, "sprite.pos setter (no crash)")
|
||||
except Exception as e:
|
||||
print(f" (Sprite tests skipped - no test texture: {e})")
|
||||
|
||||
# ============================================================
|
||||
# Test #291: UICircle property setters
|
||||
# ============================================================
|
||||
print("Testing #291: UICircle property setters...")
|
||||
|
||||
circle = mcrfpy.Circle(radius=25.0, center=(300, 300),
|
||||
fill_color=mcrfpy.Color(255, 128, 0))
|
||||
parent.children.append(circle)
|
||||
|
||||
circle.radius = 30.0
|
||||
check(circle.radius == 30.0, "circle.radius setter")
|
||||
|
||||
circle.fill_color = mcrfpy.Color(0, 128, 255)
|
||||
c = circle.fill_color
|
||||
check(c.r == 0 and c.g == 128 and c.b == 255, "circle.fill_color setter")
|
||||
|
||||
circle.outline_color = mcrfpy.Color(255, 255, 255)
|
||||
c = circle.outline_color
|
||||
check(c.r == 255, "circle.outline_color setter")
|
||||
|
||||
circle.outline = 3.0
|
||||
check(circle.outline == 3.0, "circle.outline setter")
|
||||
|
||||
# ============================================================
|
||||
# Test #291: UILine property setters
|
||||
# ============================================================
|
||||
print("Testing #291: UILine property setters...")
|
||||
|
||||
line = mcrfpy.Line(start=(10, 400), end=(200, 400),
|
||||
thickness=2.0, color=mcrfpy.Color(255, 0, 255))
|
||||
parent.children.append(line)
|
||||
|
||||
line.start = (20, 410)
|
||||
check(True, "line.start setter (no crash)")
|
||||
|
||||
line.end = (210, 410)
|
||||
check(True, "line.end setter (no crash)")
|
||||
|
||||
line.color = mcrfpy.Color(0, 255, 255)
|
||||
c = line.color
|
||||
check(c.r == 0 and c.g == 255 and c.b == 255, "line.color setter")
|
||||
|
||||
line.thickness = 4.0
|
||||
check(line.thickness == 4.0, "line.thickness setter")
|
||||
|
||||
# ============================================================
|
||||
# Test #291: UIArc property setters
|
||||
# ============================================================
|
||||
print("Testing #291: UIArc property setters...")
|
||||
|
||||
arc = mcrfpy.Arc(center=(400, 300), radius=40.0, start_angle=0.0,
|
||||
end_angle=180.0, color=mcrfpy.Color(128, 0, 255),
|
||||
thickness=3.0)
|
||||
parent.children.append(arc)
|
||||
|
||||
arc.radius = 50.0
|
||||
check(arc.radius == 50.0, "arc.radius setter")
|
||||
|
||||
arc.start_angle = 45.0
|
||||
check(arc.start_angle == 45.0, "arc.start_angle setter")
|
||||
|
||||
arc.end_angle = 270.0
|
||||
check(arc.end_angle == 270.0, "arc.end_angle setter")
|
||||
|
||||
arc.color = mcrfpy.Color(255, 128, 128)
|
||||
c = arc.color
|
||||
check(c.r == 255 and c.g == 128 and c.b == 128, "arc.color setter")
|
||||
|
||||
arc.thickness = 5.0
|
||||
check(arc.thickness == 5.0, "arc.thickness setter")
|
||||
|
||||
# ============================================================
|
||||
# Test #291: UIGrid property setters
|
||||
# ============================================================
|
||||
print("Testing #291: UIGrid property setters...")
|
||||
|
||||
try:
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(500, 100), size=(200, 200))
|
||||
parent.children.append(grid)
|
||||
|
||||
grid.center_x = 5.0
|
||||
check(True, "grid.center_x setter (no crash)")
|
||||
|
||||
grid.center_y = 5.0
|
||||
check(True, "grid.center_y setter (no crash)")
|
||||
|
||||
grid.zoom = 2.0
|
||||
check(grid.zoom == 2.0, "grid.zoom setter")
|
||||
|
||||
grid.fill_color = mcrfpy.Color(20, 20, 40)
|
||||
check(True, "grid.fill_color setter (no crash)")
|
||||
except Exception as e:
|
||||
print(f" (Grid tests skipped: {e})")
|
||||
|
||||
# ============================================================
|
||||
# Trigger a render cycle to exercise dirty flag code paths
|
||||
# ============================================================
|
||||
print("Triggering render cycle...")
|
||||
mcrfpy.step(0.016) # ~1 frame at 60fps
|
||||
|
||||
# ============================================================
|
||||
# Summary
|
||||
# ============================================================
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Results: {test_count - fail_count}/{test_count} passed")
|
||||
if test_pass:
|
||||
print("PASS: All dirty flag propagation tests passed")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"FAIL: {fail_count} test(s) failed")
|
||||
sys.exit(1)
|
||||
|
|
@ -14,7 +14,7 @@ def test_metrics(timer, runtime):
|
|||
print("\nRunning metrics test...")
|
||||
|
||||
# Get metrics
|
||||
metrics = mcrfpy.getMetrics()
|
||||
metrics = mcrfpy.get_metrics()
|
||||
|
||||
print("\nPerformance Metrics:")
|
||||
print(f" Frame Time: {metrics['frame_time']:.2f} ms")
|
||||
|
|
@ -81,7 +81,7 @@ def test_metrics(timer, runtime):
|
|||
# Schedule another check after 100ms
|
||||
def check_later(timer2, runtime2):
|
||||
global success
|
||||
metrics2 = mcrfpy.getMetrics()
|
||||
metrics2 = mcrfpy.get_metrics()
|
||||
|
||||
print(f"\nMetrics after 100ms:")
|
||||
print(f" Frame Time: {metrics2['frame_time']:.2f} ms")
|
||||
|
|
|
|||
428
web/index.html
Normal file
428
web/index.html
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>McRogueFace - Web Demo</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap');
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--bg-dark: #0c0a14;
|
||||
--bg-card: #16132a;
|
||||
--bg-surface: #1e1a36;
|
||||
--accent: #e94560;
|
||||
--accent-glow: #e9456040;
|
||||
--text: #e8e6f0;
|
||||
--text-dim: #8886a0;
|
||||
--text-muted: #5a587a;
|
||||
--gold: #f0c040;
|
||||
--green: #40d080;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: var(--bg-dark);
|
||||
color: var(--text);
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 40px 20px 20px;
|
||||
background: linear-gradient(180deg, #14102a 0%, var(--bg-dark) 100%);
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
text-shadow: 0 0 40px var(--accent-glow);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.hero .subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.hero .tagline {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
max-width: 600px;
|
||||
margin: 0 auto 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.canvas-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 20px 30px;
|
||||
}
|
||||
|
||||
.canvas-frame {
|
||||
position: relative;
|
||||
background: #000;
|
||||
border: 2px solid #2a2648;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6), 0 0 60px var(--accent-glow);
|
||||
/* fixed aspect ratio container for 1024x768 */
|
||||
width: min(1024px, calc(100vw - 40px));
|
||||
aspect-ratio: 1024 / 768;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
-ms-interpolation-mode: nearest-neighbor;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(12, 10, 20, 0.95);
|
||||
z-index: 10;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.loading-overlay.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #2a2648;
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
#status {
|
||||
margin-top: 16px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.info-section {
|
||||
max-width: 1024px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px 50px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.info-grid { grid-template-columns: 1fr; }
|
||||
.hero h1 { font-size: 2rem; }
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid #2a2648;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
color: var(--gold);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-card p, .info-card ul {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-dim);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-card ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.info-card ul li::before {
|
||||
content: "> ";
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.links {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.links a {
|
||||
display: inline-block;
|
||||
margin: 0 12px;
|
||||
padding: 10px 24px;
|
||||
background: var(--bg-surface);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
border: 1px solid #2a2648;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.links a:hover {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 20px var(--accent-glow);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
border-top: 1px solid #1a1830;
|
||||
}
|
||||
|
||||
.click-to-focus {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(12, 10, 20, 0.7);
|
||||
z-index: 5;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.click-to-focus.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.click-to-focus span {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text);
|
||||
padding: 12px 24px;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hero">
|
||||
<h1>McRogueFace</h1>
|
||||
<p class="subtitle">A Python-Powered Roguelike Engine</p>
|
||||
<p class="tagline">
|
||||
C++ engine with Python scripting, compiled to WebAssembly.
|
||||
BSP dungeons, Wang tile autotiling, field of view, and turn-based combat
|
||||
— all running in your browser.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="canvas-wrapper">
|
||||
<div class="canvas-frame">
|
||||
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
|
||||
<div class="loading-overlay" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p id="status">Loading...</p>
|
||||
</div>
|
||||
<div class="click-to-focus hidden" id="focus-prompt">
|
||||
<span>Click to play</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<div class="info-grid">
|
||||
<div class="info-card">
|
||||
<h3>Controls</h3>
|
||||
<ul>
|
||||
<li>Arrow keys or WASD to move</li>
|
||||
<li>Bump into enemies to attack</li>
|
||||
<li>Walk over potions to heal</li>
|
||||
<li>Find stairs to descend deeper</li>
|
||||
<li>Period (.) to wait a turn</li>
|
||||
<li>R to restart after death</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>Engine Features</h3>
|
||||
<ul>
|
||||
<li>BSP dungeon generation (libtcod)</li>
|
||||
<li>Wang tile autotiling (Tiled .tsx)</li>
|
||||
<li>Field of view & fog of war</li>
|
||||
<li>Entity/Grid system with layers</li>
|
||||
<li>Python 3.14 scripting (full interpreter)</li>
|
||||
<li>SDL2 + OpenGL ES 2 rendering</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>About This Demo</h3>
|
||||
<p>
|
||||
This entire game is written in Python, running on the McRogueFace
|
||||
C++ engine compiled to WebAssembly via Emscripten. The game logic,
|
||||
dungeon generation, AI, and UI are all Python — the engine
|
||||
handles rendering, input, and the tile/entity system.
|
||||
</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>Tech Stack</h3>
|
||||
<ul>
|
||||
<li>C++17 game engine core</li>
|
||||
<li>Python 3.14 (cross-compiled to WASM)</li>
|
||||
<li>SDL2 + OpenGL ES 2 (via Emscripten)</li>
|
||||
<li>libtcod (BSP, FOV, pathfinding)</li>
|
||||
<li>Kenney Tiny Dungeon tileset</li>
|
||||
<li>~16 MB total download</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a href="https://github.com/jmccardle/McRogueFace">GitHub</a>
|
||||
<a href="https://gamedev.ffwf.net/gitea/john/McRogueFace">Gitea</a>
|
||||
<a href="https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki">Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
McRogueFace — Created for 7DRL 2023, actively developed.
|
||||
Engine by John McCardle. Tiles by Kenney.nl.
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var canvasElement = document.getElementById('canvas');
|
||||
var loadingElement = document.getElementById('loading');
|
||||
var statusElement = document.getElementById('status');
|
||||
var focusPrompt = document.getElementById('focus-prompt');
|
||||
var runtimeReady = false;
|
||||
var hasFocus = false;
|
||||
|
||||
function updateCanvasSize() {
|
||||
// Match canvas backing buffer to its display size
|
||||
var rect = canvasElement.getBoundingClientRect();
|
||||
var w = Math.floor(rect.width);
|
||||
var h = Math.floor(rect.height);
|
||||
if (runtimeReady) {
|
||||
Module.ccall('notify_canvas_resize', null, ['number', 'number'], [w, h]);
|
||||
} else {
|
||||
canvasElement.width = w;
|
||||
canvasElement.height = h;
|
||||
}
|
||||
}
|
||||
updateCanvasSize();
|
||||
window.addEventListener('resize', updateCanvasSize);
|
||||
|
||||
// Focus management
|
||||
canvasElement.addEventListener('click', function() {
|
||||
canvasElement.focus();
|
||||
hasFocus = true;
|
||||
focusPrompt.classList.add('hidden');
|
||||
});
|
||||
canvasElement.addEventListener('mousedown', function() {
|
||||
if (document.activeElement !== canvasElement) {
|
||||
canvasElement.focus();
|
||||
hasFocus = true;
|
||||
focusPrompt.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
canvasElement.addEventListener('blur', function() {
|
||||
if (runtimeReady) {
|
||||
hasFocus = false;
|
||||
focusPrompt.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
focusPrompt.addEventListener('click', function() {
|
||||
canvasElement.focus();
|
||||
hasFocus = true;
|
||||
focusPrompt.classList.add('hidden');
|
||||
});
|
||||
|
||||
var Module = {
|
||||
preRun: [function() {
|
||||
FS.mkdir('/save');
|
||||
FS.mount(IDBFS, {}, '/save');
|
||||
Module.addRunDependency('idbfs-restore');
|
||||
FS.syncfs(true, function(err) {
|
||||
if (err) console.error('Failed to restore /save/:', err);
|
||||
Module.removeRunDependency('idbfs-restore');
|
||||
});
|
||||
}],
|
||||
print: function(text) {
|
||||
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
||||
console.log(text);
|
||||
},
|
||||
printErr: function(text) {
|
||||
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
||||
console.error(text);
|
||||
},
|
||||
canvas: canvasElement,
|
||||
setStatus: function(text) {
|
||||
if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
|
||||
if (text === Module.setStatus.last.text) return;
|
||||
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
|
||||
var now = Date.now();
|
||||
if (m && now - Module.setStatus.last.time < 30) return;
|
||||
Module.setStatus.last.time = now;
|
||||
Module.setStatus.last.text = text;
|
||||
if (m) text = m[1];
|
||||
statusElement.textContent = text;
|
||||
},
|
||||
totalDependencies: 0,
|
||||
monitorRunDependencies: function(left) {
|
||||
this.totalDependencies = Math.max(this.totalDependencies, left);
|
||||
Module.setStatus(left
|
||||
? 'Preparing... (' + (this.totalDependencies - left) + '/' + this.totalDependencies + ')'
|
||||
: 'All downloads complete.');
|
||||
},
|
||||
onRuntimeInitialized: function() {
|
||||
runtimeReady = true;
|
||||
loadingElement.classList.add('hidden');
|
||||
canvasElement.focus();
|
||||
hasFocus = true;
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
};
|
||||
|
||||
Module.setStatus('Downloading...');
|
||||
|
||||
window.onerror = function(event) {
|
||||
Module.setStatus('Error! See browser console.');
|
||||
};
|
||||
|
||||
// Emscripten symbol resolver shim
|
||||
if (typeof resolveGlobalSymbol === 'undefined') {
|
||||
window.resolveGlobalSymbol = function(name, direct) {
|
||||
return {
|
||||
sym: Module['_' + name] || Module[name],
|
||||
type: 'function'
|
||||
};
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<script async src="mcrogueface.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue