Compare commits

..

85 Commits

Author SHA1 Message Date
John McCardle 3325e4895e Refactor: Python 3.12, build libtcod & SFML from source. Cmake build. Directory cleanup
directories needed:
* build - for cmake output
* deps - stuff needed to compile McRogueface (headers)
	libtcod -> ../modules/libtcod/src/libtcod
	sfml -> ../modules/SFML/include/SFML
	python -> ../modules/cpython/Include
* lib - stuff needed to link McRogueFace (shared objects); also required at runtime
	libtcod -> `../modules/libtcod/buildsys/autotools/.libs/libtcod.so.1.0.24`
	sfml -> `../modules/SFML/build/lib/*`
	python -> `../modules/cpython/libpython3.12.so`; standard lib at ../modules/cpython/build/lib.linux-x86_64-3.12 & ../modules/cpython/Lib

You can get dependencies by:
 - Build from source (i.e. all submodules)
 - Go download them from each project's website
 - install packages from your distro and symlink them to deps/lib directories
2024-02-24 20:40:40 -05:00
John McCardle 705943abba initial cmake config (builds, python standard library not available) 2024-02-23 21:55:16 -05:00
John McCardle 47b485ca7d Submodules: Add dependencies
This is the prerequisite to:
* complete from-source build
* removing compilation libraries from the git repo
* rebasing the project for "alpha 0.1" release
2024-02-23 08:37:36 -05:00
John McCardle f2eaee95ec (minor) sprite object enabled in collection 2023-10-20 22:23:20 -04:00
John McCardle 494658e5c3 Sprite fixes. Changing sprite_number now changes the visible texture. Repr fixed. 2023-09-15 22:09:29 -04:00
John McCardle 6e820af8c4 mcrfpy.Sprite / PyUISpriteType compiles; too excited to fully test it (todo: modify the macro to get instances into and out of collections) 2023-09-13 23:23:08 -04:00
John McCardle cf485ef327 Refactoring UISprite to play well with Python API and match existing code styles. Default constructor will have to go, which complicates the Python class slightly for mcrfpy.Sprite 2023-09-11 20:30:10 -04:00
John McCardle 3a1432212f UICollection.remove 2023-09-09 10:14:11 -04:00
John McCardle 38b6a3cade UICollection.append, tests are good for Caption and Frame objects created by Python to be drawn by the UITestScene 2023-09-09 08:49:02 -04:00
John McCardle 1bbb0aa5b8 Caption object seems to be instantiable with a Font object now. Can't test actual rendering without a way to add objects to a collection. 2023-09-07 23:10:21 -04:00
John McCardle bec2b3294d Added PyFont/mcrfpy.Font object 2023-09-07 22:25:19 -04:00
John McCardle 9486104377 Converted py_instance to a macro (don't ask me why it doesn't work as a function) and first pass at UICaption functionality. UISprite C++ tests. 2023-09-03 20:40:52 -04:00
John McCardle 5267287b05 checkpoint: found that py_instance causes segfaults when called separately, but the same exact code inside of the _getitem method works fine. I can't explain that. I guess I'll turn it into a macro so the actions are inline and I can move on to finishing the other UI classes. 2023-09-03 12:46:23 -04:00
John McCardle b8af8bc870 UIDrawable to Python object (untested, but compiling) 2023-09-02 20:02:07 -04:00
John McCardle 0ef0a5d506 Switched UIFrame and Scene to store their UIDrawables in shared_ptr to vector, instead of directly to vector. Every object that can be exposed to Python has to be safely shareable so it doesn't become a segfault, and that includes the UIDrawable collections AND the UIDrawable members. So we get the terrifying type for collections of child elements: 'std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>>'. May I be forgiven for my sins 2023-09-02 14:00:48 -04:00
John McCardle a41d3d4a54 Iterator class fleshed out. Several implementations left to do, but it compiles, and I think every function definition for the PyUICollectionType and PyUICollectionIterType is now in place 2023-09-02 09:22:34 -04:00
John McCardle 6d4bc2989c UICollection work: fixed compilation, still quite a bit of stubs 2023-09-02 04:40:05 -04:00
John McCardle d5a7cbca85 In progress: UICollection. mcrfpydef::PyUICollection_sqmethods needs to be made static or moved somewhere it won't be multiply defined 2023-09-01 23:31:31 -04:00
John McCardle 5d8510747c Color and Frame classes pretty well integrated from the Python perspective now 2023-09-01 18:28:59 -04:00
John McCardle 06052c81c9 PyUIFrame looking not half bad. There seems to be a glitch with the color values. 2023-09-01 12:14:24 -04:00
John McCardle 6fe7b842ef Successful use of a copy-modify-paste template of Python type (UIFrame -> PyUIFrameObject) 2023-08-31 22:32:58 -04:00
John McCardle 50d926fe37 Aug 30/31 updates. Tinkering with shared_ptr. Working towards exposing UI objects to Python UI. Color object updated for shared_ptr structure that will be repeated for the rest of the UI objects. Still a lot of open questions, but committing here to get back on track after a few hours wasted trying to solve this problem too generally via templates. 2023-08-31 13:51:56 -04:00
John McCardle 795701c986 Cleanup: less curly braces in destructor 2023-08-30 14:50:27 -04:00
John McCardle 884a49a63a Switching UIFrame to sf::Color* for outline and fill members. Haven't tested with Python integration, but I wrote the methods to take a crack at it 2023-08-30 14:38:49 -04:00
John McCardle c4d5a497d4 Color container type for Python working. I still need to implement UIFrame using it. 2023-08-28 05:44:26 -04:00
John McCardle ba97aebf3e Showing FPS on title bar (GameEngine) 2023-08-27 19:58:15 -04:00
John McCardle ac0ec4bb71 In-work: Python segfaults when adding new objects to module 2023-08-25 21:57:42 -04:00
John McCardle a455c44b34 Debugging & build with debug symbols scripts 2023-08-25 21:56:27 -04:00
John McCardle 96e78e6150 Prepwork: marked the spot for adding more types to the Python module 2023-08-21 20:27:47 -04:00
John McCardle 0dd86056a8 Cleanup: remove python embedding test file 2023-08-12 20:06:16 -04:00
John McCardle 26cb410b8e Cleanup: Remove UITestScene. I believe test functionality will be better expressed as Python scripts 2023-08-12 19:48:29 -04:00
John McCardle d09fc87499 Cleanup: remove Item class/component. This may be added back later, but it's not in the EngJam 2023 plan and is being removed as a noisy, underdeveloped concept. 2023-08-12 15:37:46 -04:00
John McCardle b022dfa6e8 Cleanup: remove VectorShape class (it'll return, someday...) 2023-08-12 15:07:48 -04:00
John McCardle 232ce34d54 Cleanup: remove references to DrawSprite API method (debug method to draw on SFML window for a single frame) 2023-08-12 10:57:53 -04:00
John McCardle c1c17bab69 Basic, buggy movement purely from Python API 2023-07-17 22:08:06 -04:00
John McCardle e85861cbb2 I've worked keybinding functionality into Python, but there are some workarounds and notes (See the Jankfile) 2023-07-17 16:15:35 -04:00
John McCardle d6446e18ea Tinkering with input
I want to move keyboard input defs to the Python API. I laid the groundwork for it today.

From the JANKFILE:

- working on API endpoint `_registerInputAction`.

it will add "_py" as a suffix to the action string and register it along with other scene actions.

- Adding public Scene methods. These are on the base class with default of return `false`.

`bool Scene::registerActionInjected(int code, std::string name)` and `unregisterActionInjected`

the PythonScene (and other scenes that support injected user input) can override this method, check existing registrations, and return `true` when succeeding.

Also, upgraded to C++20 (g++ `c++2a`), mostly because I want to use map::contains.
2023-07-13 23:01:09 -04:00
John McCardle d3826804a0 (Minor) It's CO*M*P4300 2023-07-08 20:00:30 -04:00
John McCardle b4c49c4619 Giving myself credit for LGJ2023 tech demo features 2023-07-08 19:47:06 -04:00
John McCardle 76ac236be3 Linux Game Jam 2023 mini-contribution 2023-07-08 19:42:47 -04:00
John McCardle 97793fb26b Clean up console output for 7DRL submission 2023-03-12 00:32:27 -05:00
John McCardle b3134f0890 Basic hallways 2023-03-11 23:34:34 -05:00
John McCardle dfcc39dd43 toggleable camera following that allows for pan action 2023-03-11 21:54:54 -05:00
John McCardle 29ac89b489 bugfix: use float instead of int for modMenu/listMenus API calls, as this was corrupting the coordinates of sprites on uimenus 2023-03-11 17:15:06 -05:00
John McCardle b4daac6e0c Camera following functionality, first pass 2023-03-11 16:11:10 -05:00
John McCardle 3fd60d76ea Making empty space transparent to fix FOV, more generous FOV algorithm 2023-03-10 21:42:50 -05:00
John McCardle 99fa92f8ba Field of view, discovered tiles, opaque fog of war rendering 2023-03-10 19:39:44 -05:00
John McCardle 9441f357df 'entity only' grid update, saves lots of throughput on larger grids 2023-03-10 12:50:49 -05:00
John McCardle 34feb226e4 collision 2023-03-10 11:35:46 -05:00
John McCardle 486a1cd17c Keyboard control 2023-03-10 09:21:56 -05:00
John McCardle 8d9148b88d Music tester & animation work 2023-03-09 20:40:47 -05:00
John McCardle f1798189f0 Animation testing w/ Miniworld sprites 2023-03-09 14:29:37 -05:00
John McCardle 5b168737ce License update and bug list. Final Github release during 7DRL 2023. 2023-03-09 08:50:11 -05:00
John McCardle 6875cb5fe1 Spawning & drawing entities from Python API 2023-03-09 08:44:04 -05:00
John McCardle 87483cc8ad Sound APIs for Python 2023-03-08 12:34:03 -05:00
John McCardle 620def19f1 Fixed animations, jank-noted some issues & workarounds with the Animation python API 2023-03-08 09:20:57 -05:00
John McCardle c9b97b9b35 TestScene script for animation testing 2023-03-07 20:11:11 -05:00
John McCardle 8e59152a8f Windows fixes 2023-03-07 17:09:54 -08:00
John McCardle fedfcd46a3 Test animation now moves the entire UIMenu object (and children) 2023-03-07 20:03:09 -05:00
John McCardle c551c721ce Animation work: removing pointers from the entire class in favor of std::function/lambdas to write values. This actually works with SFML classes, because I can wrap the setter class 2023-03-07 07:39:41 -05:00
John McCardle d74635ee4e Check in... animations are roughly half built 2023-03-06 20:54:23 -05:00
John McCardle 47e823d5b9 Animation class (not tested) 2023-03-05 22:44:39 -05:00
John McCardle 6dbf8a5119 Pan/Zoom grids, Python basic generation template provided 2023-03-05 19:58:20 -05:00
John McCardle a53ae29467 Python object models 2023-03-04 23:18:21 -05:00
John McCardle 257aa3c3d2 Fully python-driven scene. Lots of interaction needs testing but the broad strokes are there for mouse pan/zoom on multiple grids and any number of UIs 2023-03-04 23:04:16 -05:00
John McCardle a4b6c2c428 Pan, zoom, and mouse-to-gridsquare translation that I can be proud of. Cleanup required, though 2023-03-04 19:04:05 -05:00
John McCardle b0ef1d2303 Tweak point conversion to prevent off-grid (negative) points from registering as on the grid 2023-03-04 00:18:19 -05:00
John McCardle b3f946ecb2 API endpoints: Create and retrieve grid/gridpoint objects 2023-03-03 23:57:42 -05:00
John McCardle 6a4150ec05 Screen to Grid is working pretty reliably, even if switching to float coordinates did make zoom at high values a bit wobbly. 2023-03-03 22:26:38 -05:00
John McCardle e295bfb742 python callbacks, working on grid 2023-03-03 22:16:47 -05:00
John McCardle 2ec97dfb1c Full modification of UI items seems to be working 2023-03-02 22:07:23 -05:00
John McCardle f89896176c Modify UI objects by calling mcrfpy.modMenu(m) 2023-03-02 20:41:43 -05:00
John McCardle de753713d5 UI from Python now working fairly comprehensively 2023-03-02 18:57:09 -05:00
John McCardle c8124e84dc Updated UIMenu to a map on the C++ side, Python gets the title property so changes can be properly, jankily, looked up 2023-03-02 06:35:13 -05:00
John McCardle a1e9129923 Send Menu color to Python 2023-03-02 05:53:17 -05:00
John McCardle f23dfbe4ba Another checkpoint. Compiling/building/running - Python API tests from in-engine REPL are not passing, though 2023-03-01 21:37:42 -05:00
John McCardle 1e9fd77a13 JANK MODE: Messy / broken commit - in progress
Needed to make a checkpoint, gods forgive me for committing known broken code straight to master. The jam has in a sense already begun.

I tested a smaller solution in the xplat_concept repo earlier today.
In short, I'm going to build a janky method to add new + report existing UI elements. After that's done, the UI building should be done from python modules, hastening the UI design.

This is ugly and bad, I am truly sorry. We just need to get through 7DRL, so I can't make it pretty today.
2023-02-28 23:19:43 -05:00
John McCardle 2c1946c29b Grid - widget for holding multi-layer map data 2023-02-27 07:02:34 -05:00
John McCardle 6d05f8bc63 Return NONE properly from test API point 2023-02-27 07:01:46 -05:00
John McCardle a4d0efe334 To-do list progress updated 2023-02-26 10:53:58 -05:00
John McCardle 6a47bc1e28 Windows/msvc lockstep changes 2023-02-26 07:51:03 -08:00
John McCardle 6a2c3c6c36 McRogueFace Python API (McRFPy_API) demo class 2023-02-26 10:23:44 -05:00
John McCardle a6f59085eb Update MSVC project for Windows build 2023-02-25 03:17:05 -08:00
John McCardle d2499a67f8 Porting in old gamejam code. Removed SOME cruft, more likely remains. Sound + sprite test. 2023-02-24 23:46:34 -05:00
John McCardle 1784489dfb Windows / MSVC commit. Bring your own Python PCBuild directory, I don't want to upload it to git. 2023-02-23 19:38:41 -08:00
269 changed files with 5872 additions and 49294 deletions

21
.gitignore vendored
View File

@ -9,25 +9,4 @@ obj
build
lib
obj
__pycache__
.cache/
7DRL2025 Release/
CMakeFiles/
Makefile
*.md
*.zip
__lib/
_oldscripts/
assets/
cellular_automata_fire/
*.txt
deps/
fetch_issues_txt.py
forest_fire_CA.py
mcrogueface.github.io
scripts/
test_*
tcod_reference
.archive

View File

@ -17,11 +17,20 @@ include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
# TODO: Move this into the WIN32 if block below (as 'else')
#include_directories(${CMAKE_SOURCE_DIR}/platform/linux)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
# Collect all the source files
file(GLOB_RECURSE SOURCES "src/*.cpp")
# Create a list of libraries to link against
set(LINK_LIBS
m
dl
util
pthread
python3.12
sfml-graphics
sfml-window
sfml-system
@ -30,33 +39,19 @@ set(LINK_LIBS
# On Windows, add any additional libs and include directories
if(WIN32)
# Windows-specific Python library name (no dots)
list(APPEND LINK_LIBS python312)
# Add the necessary Windows-specific libraries and include directories
# include_directories(path_to_additional_includes)
# link_directories(path_to_additional_libs)
# list(APPEND LINK_LIBS additional_windows_libs)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
else()
# Unix/Linux specific libraries
list(APPEND LINK_LIBS python3.12 m dl util pthread)
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
endif()
# Add the directory where the linker should look for the libraries
#link_directories(${CMAKE_SOURCE_DIR}/deps_linux)
link_directories(${CMAKE_SOURCE_DIR}/__lib)
link_directories(${CMAKE_SOURCE_DIR}/lib)
# Define the executable target before linking libraries
add_executable(mcrogueface ${SOURCES})
# On Windows, set subsystem to WINDOWS to hide console
if(WIN32)
set_target_properties(mcrogueface PROPERTIES
WIN32_EXECUTABLE TRUE
LINK_FLAGS "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup")
endif()
# Now the linker will find the libraries in the specified directory
target_link_libraries(mcrogueface ${LINK_LIBS})
@ -73,28 +68,9 @@ add_custom_command(TARGET mcrogueface POST_BUILD
# Copy Python standard library to build directory
add_custom_command(TARGET mcrogueface POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib)
${CMAKE_SOURCE_DIR}/lib $<TARGET_FILE_DIR:mcrogueface>/lib)
# On Windows, copy DLLs to executable directory
if(WIN32)
# Copy all DLL files from lib to the executable directory
add_custom_command(TARGET mcrogueface POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>
COMMAND ${CMAKE_COMMAND} -E echo "Copied DLLs to executable directory")
# Alternative: Copy specific DLLs if you want more control
# file(GLOB DLLS "${CMAKE_SOURCE_DIR}/__lib/*.dll")
# foreach(DLL ${DLLS})
# add_custom_command(TARGET mcrogueface POST_BUILD
# COMMAND ${CMAKE_COMMAND} -E copy_if_different
# ${DLL} $<TARGET_FILE_DIR:mcrogueface>)
# endforeach()
endif()
# rpath for including shared libraries (Linux/Unix only)
if(NOT WIN32)
set_target_properties(mcrogueface PROPERTIES
INSTALL_RPATH "$ORIGIN/./lib")
endif()
# rpath for including shared libraries
set_target_properties(mcrogueface PROPERTIES
INSTALL_RPATH "./lib")

View File

@ -1,54 +0,0 @@
# Convenience Makefile wrapper for McRogueFace
# This delegates to CMake build in the build directory
.PHONY: all build clean run test dist help
# Default target
all: build
# Build the project
build:
@./build.sh
# Clean build artifacts
clean:
@./clean.sh
# Run the game
run: build
@cd build && ./mcrogueface
# Run in Python mode
python: build
@cd build && ./mcrogueface -i
# Test basic functionality
test: build
@echo "Testing McRogueFace..."
@cd build && ./mcrogueface -V
@cd build && ./mcrogueface -c "print('Test passed')"
@cd build && ./mcrogueface --headless -c "import mcrfpy; print('mcrfpy imported successfully')"
# Create distribution archive
dist: build
@echo "Creating distribution archive..."
@cd build && zip -r ../McRogueFace-$$(date +%Y%m%d).zip . -x "*.o" "CMakeFiles/*" "Makefile" "*.cmake"
@echo "Distribution archive created: McRogueFace-$$(date +%Y%m%d).zip"
# Show help
help:
@echo "McRogueFace Build System"
@echo "======================="
@echo ""
@echo "Available targets:"
@echo " make - Build the project (default)"
@echo " make build - Build the project"
@echo " make clean - Remove all build artifacts"
@echo " make run - Build and run the game"
@echo " make python - Build and run in Python interactive mode"
@echo " make test - Run basic tests"
@echo " make dist - Create distribution archive"
@echo " make help - Show this help message"
@echo ""
@echo "Build output goes to: ./build/"
@echo "Distribution archives are created in project root"

143
README.md
View File

@ -1,127 +1,30 @@
# McRogueFace
# McRogueFace - 2D Game Engine
An experimental prototype game engine built for my own use in 7DRL 2023.
*Blame my wife for the name*
A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML.
## Tenets:
* Core roguelike logic from libtcod: field of view, pathfinding
* Animate sprites with multiple frames. Smooth transitions for positions, sizes, zoom, and camera
* Simple GUI element system allows keyboard and mouse input, composition
* No compilation or installation necessary. The runtime is a full Python environment; "Zip And Ship"
* C++ first, Python close behind.
* Entity-Component system based on David Churchill's Memorial University COMP4300 course lectures available on Youtube.
* Graphics, particles and shaders provided by SFML.
* Pathfinding, noise generation, and other Roguelike goodness provided by TCOD.
![ Image ]()
## Why?
**Pre-Alpha Release Demo**: my 7DRL 2025 entry *"Crypt of Sokoban"* - a prototype with buttons, boulders, enemies, and items.
I did the r/RoguelikeDev TCOD tutorial in Python. I loved it, but I did not want to be limited to ASCII. I want to be able to draw pixels on top of my tiles (like lines or circles) and eventually incorporate even more polish.
## Quick Start
## To-do
**Download**:
- The entire McRogueFace visual framework:
- **Sprite**: an image file or one sprite from a shared sprite sheet
- **Caption**: load a font, display text
- **Frame**: A rectangle; put other things on it to move or manage GUIs as modules
- **Grid**: A 2D array of tiles with zoom + position control
- **Entity**: Lives on a Grid, displays a sprite, and can have a perspective or move along a path
- **Animation**: Change any property on any of the above over time
```bash
# Clone and build
git clone <wherever you found this repo>
cd McRogueFace
make
# Run the example game
cd build
./mcrogueface
```
## Example: Creating a Simple Scene
```python
import mcrfpy
# Create a new scene
mcrfpy.createScene("intro")
# Add a text caption
caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!")
caption.size = 48
caption.fill_color = (255, 255, 255)
# Add to scene
mcrfpy.sceneUI("intro").append(caption)
# Switch to the scene
mcrfpy.setScene("intro")
```
## Documentation
### 📚 Full Documentation Site
For comprehensive documentation, tutorials, and API reference, visit:
**[https://mcrogueface.github.io](https://mcrogueface.github.io)**
The documentation site includes:
- **[Quickstart Guide](https://mcrogueface.github.io/quickstart)** - Get running in 5 minutes
- **[McRogueFace Does The Entire Roguelike Tutorial](https://mcrogueface.github.io/tutorials)** - Step-by-step game building
- **[Complete API Reference](https://mcrogueface.github.io/api)** - Every function documented
- **[Cookbook](https://mcrogueface.github.io/cookbook)** - Ready-to-use code recipes
- **[C++ Extension Guide](https://mcrogueface.github.io/extending-cpp)** - For C++ developers: Add engine features
## Build Requirements
- C++17 compiler (GCC 7+ or Clang 5+)
- CMake 3.14+
- Python 3.12+
- SFML 2.6
- Linux or Windows (macOS untested)
## Project Structure
```
McRogueFace/
├── assets/ # Sprites, fonts, audio
├── build/ # Build output directory: zip + ship
│ ├─ (*)assets/ # (copied location of assets)
│ ├─ (*)scripts/ # (copied location of src/scripts)
│ └─ lib/ # SFML, TCOD libraries, Python + standard library / modules
├── deps/ # Python, SFML, and libtcod imports can be tossed in here to build
│ └─ platform/ # windows, linux subdirectories for OS-specific cpython config
├── docs/ # generated HTML, markdown docs
│ └─ stubs/ # .pyi files for editor integration
├── modules/ # git submodules, to build all of McRogueFace's dependencies from source
├── src/ # C++ engine source
│ └─ scripts/ # Python game scripts (copied during build)
└── tests/ # Automated test suite
└── tools/ # For the McRogueFace ecosystem: docs generation
```
If you are building McRogueFace to implement game logic or scene configuration in C++, you'll have to compile the project.
If you are writing a game in Python using McRogueFace, you only need to rename and zip/distribute the `build` directory.
## Philosophy
- **C++ every frame, Python every tick**: All rendering data is handled in C++. Structure your UI and program animations in Python, and they are rendered without Python. All game logic can be written in Python.
- **No Compiling Required; Zip And Ship**: Implement your game objects with Python, zip up McRogueFace with your "game.py" to ship
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod
- **Hands-Off Testing**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration
- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter
## Contributing
PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request.
The project has a private roadmap and issue list. Reach out via email or social media if you have bugs or feature requests.
## License
This project is licensed under the MIT License - see LICENSE file for details.
## Acknowledgments
- Developed for 7-Day Roguelike 2023, 2024, 2025 - here's to many more
- Built with [SFML](https://www.sfml-dev.org/), [libtcod](https://github.com/libtcod/libtcod), and Python
- Inspired by David Churchill's COMP4300 game engine lectures
* ✅ Initial Commit
* ✅ Integrate scene, action, entity, component system from COMP4300 engine
* ✅ Windows / Visual Studio project
* ✅ Draw Sprites
* ✅ Play Sounds
* ✅ Draw UI, spawn entity from Python code
* ❌ Python AI for entities (NPCs on set paths, enemies towards player)
* ✅ Walking / Collision
* ❌ "Boards" (stairs / doors / walk off edge of screen)
* ❌ Cutscenes - interrupt normal controls, text scroll, character portraits
* ❌ Mouse integration - tooltips, zoom, click to select targets, cursors

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

View File

@ -1,54 +0,0 @@
#!/bin/bash
# Build script for McRogueFace - compiles everything into ./build directory
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}McRogueFace Build Script${NC}"
echo "========================="
# Create build directory if it doesn't exist
if [ ! -d "build" ]; then
echo -e "${YELLOW}Creating build directory...${NC}"
mkdir build
fi
# Change to build directory
cd build
# Run CMake to generate build files
echo -e "${YELLOW}Running CMake...${NC}"
cmake .. -DCMAKE_BUILD_TYPE=Release
# Check if CMake succeeded
if [ $? -ne 0 ]; then
echo -e "${RED}CMake configuration failed!${NC}"
exit 1
fi
# Run make with parallel jobs
echo -e "${YELLOW}Building with make...${NC}"
make -j$(nproc)
# Check if make succeeded
if [ $? -ne 0 ]; then
echo -e "${RED}Build failed!${NC}"
exit 1
fi
echo -e "${GREEN}Build completed successfully!${NC}"
echo ""
echo "The build directory contains:"
ls -la
echo ""
echo -e "${GREEN}To run McRogueFace:${NC}"
echo " cd build"
echo " ./mcrogueface"
echo ""
echo -e "${GREEN}To create a distribution archive:${NC}"
echo " cd build"
echo " zip -r ../McRogueFace-$(date +%Y%m%d).zip ."

View File

@ -1,36 +0,0 @@
@echo off
REM Windows build script for McRogueFace
REM Run this over SSH without Visual Studio GUI
echo Building McRogueFace for Windows...
REM Clean previous build
if exist build_win rmdir /s /q build_win
mkdir build_win
cd build_win
REM Generate Visual Studio project files with CMake
REM Use -G to specify generator, -A for architecture
REM Visual Studio 2022 = "Visual Studio 17 2022"
REM Visual Studio 2019 = "Visual Studio 16 2019"
cmake -G "Visual Studio 17 2022" -A x64 ..
if errorlevel 1 (
echo CMake configuration failed!
exit /b 1
)
REM Build using MSBuild (comes with Visual Studio)
REM You can also use cmake --build . --config Release
msbuild McRogueFace.sln /p:Configuration=Release /p:Platform=x64 /m
if errorlevel 1 (
echo Build failed!
exit /b 1
)
echo Build completed successfully!
echo Executable location: build_win\Release\mcrogueface.exe
REM Alternative: Using cmake to build (works with any generator)
REM cmake --build . --config Release --parallel
cd ..

View File

@ -1,42 +0,0 @@
@echo off
REM Windows build script using cmake --build (generator-agnostic)
REM This version works with any CMake generator
echo Building McRogueFace for Windows using CMake...
REM Set build directory
set BUILD_DIR=build_win
set CONFIG=Release
REM Clean previous build
if exist %BUILD_DIR% rmdir /s /q %BUILD_DIR%
mkdir %BUILD_DIR%
cd %BUILD_DIR%
REM Configure with CMake
REM You can change the generator here if needed:
REM -G "Visual Studio 17 2022" (VS 2022)
REM -G "Visual Studio 16 2019" (VS 2019)
REM -G "MinGW Makefiles" (MinGW)
REM -G "Ninja" (Ninja build system)
cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=%CONFIG% ..
if errorlevel 1 (
echo CMake configuration failed!
cd ..
exit /b 1
)
REM Build using cmake (works with any generator)
cmake --build . --config %CONFIG% --parallel
if errorlevel 1 (
echo Build failed!
cd ..
exit /b 1
)
echo.
echo Build completed successfully!
echo Executable: %BUILD_DIR%\%CONFIG%\mcrogueface.exe
echo.
cd ..

View File

@ -1,112 +0,0 @@
[
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/GameEngine.cpp.o -c /home/john/Development/McRogueFace/src/GameEngine.cpp",
"file": "/home/john/Development/McRogueFace/src/GameEngine.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/IndexTexture.cpp.o -c /home/john/Development/McRogueFace/src/IndexTexture.cpp",
"file": "/home/john/Development/McRogueFace/src/IndexTexture.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/McRFPy_API.cpp.o -c /home/john/Development/McRogueFace/src/McRFPy_API.cpp",
"file": "/home/john/Development/McRogueFace/src/McRFPy_API.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyCallable.cpp.o -c /home/john/Development/McRogueFace/src/PyCallable.cpp",
"file": "/home/john/Development/McRogueFace/src/PyCallable.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyColor.cpp.o -c /home/john/Development/McRogueFace/src/PyColor.cpp",
"file": "/home/john/Development/McRogueFace/src/PyColor.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyFont.cpp.o -c /home/john/Development/McRogueFace/src/PyFont.cpp",
"file": "/home/john/Development/McRogueFace/src/PyFont.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyScene.cpp.o -c /home/john/Development/McRogueFace/src/PyScene.cpp",
"file": "/home/john/Development/McRogueFace/src/PyScene.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyTexture.cpp.o -c /home/john/Development/McRogueFace/src/PyTexture.cpp",
"file": "/home/john/Development/McRogueFace/src/PyTexture.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyVector.cpp.o -c /home/john/Development/McRogueFace/src/PyVector.cpp",
"file": "/home/john/Development/McRogueFace/src/PyVector.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Resources.cpp.o -c /home/john/Development/McRogueFace/src/Resources.cpp",
"file": "/home/john/Development/McRogueFace/src/Resources.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Scene.cpp.o -c /home/john/Development/McRogueFace/src/Scene.cpp",
"file": "/home/john/Development/McRogueFace/src/Scene.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Timer.cpp.o -c /home/john/Development/McRogueFace/src/Timer.cpp",
"file": "/home/john/Development/McRogueFace/src/Timer.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICaption.cpp.o -c /home/john/Development/McRogueFace/src/UICaption.cpp",
"file": "/home/john/Development/McRogueFace/src/UICaption.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICollection.cpp.o -c /home/john/Development/McRogueFace/src/UICollection.cpp",
"file": "/home/john/Development/McRogueFace/src/UICollection.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIDrawable.cpp.o -c /home/john/Development/McRogueFace/src/UIDrawable.cpp",
"file": "/home/john/Development/McRogueFace/src/UIDrawable.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIEntity.cpp.o -c /home/john/Development/McRogueFace/src/UIEntity.cpp",
"file": "/home/john/Development/McRogueFace/src/UIEntity.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIFrame.cpp.o -c /home/john/Development/McRogueFace/src/UIFrame.cpp",
"file": "/home/john/Development/McRogueFace/src/UIFrame.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGrid.cpp.o -c /home/john/Development/McRogueFace/src/UIGrid.cpp",
"file": "/home/john/Development/McRogueFace/src/UIGrid.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGridPoint.cpp.o -c /home/john/Development/McRogueFace/src/UIGridPoint.cpp",
"file": "/home/john/Development/McRogueFace/src/UIGridPoint.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UISprite.cpp.o -c /home/john/Development/McRogueFace/src/UISprite.cpp",
"file": "/home/john/Development/McRogueFace/src/UISprite.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UITestScene.cpp.o -c /home/john/Development/McRogueFace/src/UITestScene.cpp",
"file": "/home/john/Development/McRogueFace/src/UITestScene.cpp"
},
{
"directory": "/home/john/Development/McRogueFace/build",
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/main.cpp.o -c /home/john/Development/McRogueFace/src/main.cpp",
"file": "/home/john/Development/McRogueFace/src/main.cpp"
}
]

View File

@ -1,40 +0,0 @@
#ifndef __PLATFORM
#define __PLATFORM
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
std::wstring executable_path()
{
/*
wchar_t buffer[MAX_PATH];
GetModuleFileName(NULL, buffer, MAX_PATH);
std::wstring exec_path = buffer;
*/
auto exec_path = std::filesystem::canonical("/proc/self/exe").parent_path();
return exec_path.wstring();
//size_t path_index = exec_path.find_last_of('/');
//return exec_path.substr(0, path_index);
}
std::wstring executable_filename()
{
auto exec_path = std::filesystem::canonical("/proc/self/exe");
return exec_path.wstring();
}
std::wstring working_path()
{
auto cwd = std::filesystem::current_path();
return cwd.wstring();
}
std::string narrow_string(std::wstring convertme)
{
//setup converter
using convert_type = std::codecvt_utf8<wchar_t>;
std::wstring_convert<convert_type, wchar_t> converter;
//use converter (.to_bytes: wstr->str, .from_bytes: str->wstr)
return converter.to_bytes(convertme);
}
#endif

View File

@ -1,39 +0,0 @@
#ifndef __PLATFORM
#define __PLATFORM
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 0
#include <Windows.h>
std::wstring executable_path()
{
wchar_t buffer[MAX_PATH];
GetModuleFileName(NULL, buffer, MAX_PATH);
std::wstring exec_path = buffer;
size_t path_index = exec_path.find_last_of(L"\\/");
return exec_path.substr(0, path_index);
}
std::wstring executable_filename()
{
wchar_t buffer[MAX_PATH];
GetModuleFileName(NULL, buffer, MAX_PATH);
std::wstring exec_path = buffer;
return exec_path;
}
std::wstring working_path()
{
auto cwd = std::filesystem::current_path();
return cwd.wstring();
}
std::string narrow_string(std::wstring convertme)
{
//setup converter
using convert_type = std::codecvt_utf8<wchar_t>;
std::wstring_convert<convert_type, wchar_t> converter;
//use converter (.to_bytes: wstr->str, .from_bytes: str->wstr)
return converter.to_bytes(convertme);
}
#endif

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,532 +0,0 @@
"""Type stubs for McRogueFace Python API.
Core game engine interface for creating roguelike games with Python.
"""
from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
# Type aliases
UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid']
Transition = Union[str, None]
# Classes
class Color:
"""SFML Color Object for RGBA colors."""
r: int
g: int
b: int
a: int
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ...
def from_hex(self, hex_string: str) -> 'Color':
"""Create color from hex string (e.g., '#FF0000' or 'FF0000')."""
...
def to_hex(self) -> str:
"""Convert color to hex string format."""
...
def lerp(self, other: 'Color', t: float) -> 'Color':
"""Linear interpolation between two colors."""
...
class Vector:
"""SFML Vector Object for 2D coordinates."""
x: float
y: float
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float, y: float) -> None: ...
def add(self, other: 'Vector') -> 'Vector': ...
def subtract(self, other: 'Vector') -> 'Vector': ...
def multiply(self, scalar: float) -> 'Vector': ...
def divide(self, scalar: float) -> 'Vector': ...
def distance(self, other: 'Vector') -> float: ...
def normalize(self) -> 'Vector': ...
def dot(self, other: 'Vector') -> float: ...
class Texture:
"""SFML Texture Object for images."""
def __init__(self, filename: str) -> None: ...
filename: str
width: int
height: int
sprite_count: int
class Font:
"""SFML Font Object for text rendering."""
def __init__(self, filename: str) -> None: ...
filename: str
family: str
class Drawable:
"""Base class for all drawable UI elements."""
x: float
y: float
visible: bool
z_index: int
name: str
pos: Vector
def get_bounds(self) -> Tuple[float, float, float, float]:
"""Get bounding box as (x, y, width, height)."""
...
def move(self, dx: float, dy: float) -> None:
"""Move by relative offset (dx, dy)."""
...
def resize(self, width: float, height: float) -> None:
"""Resize to new dimensions (width, height)."""
...
class Frame(Drawable):
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)
A rectangular frame UI element that can contain other drawable elements.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0,
fill_color: Optional[Color] = None, outline_color: Optional[Color] = None,
outline: float = 0, click: Optional[Callable] = None,
children: Optional[List[UIElement]] = None) -> None: ...
w: float
h: float
fill_color: Color
outline_color: Color
outline: float
click: Optional[Callable[[float, float, int], None]]
children: 'UICollection'
clip_children: bool
class Caption(Drawable):
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)
A text display UI element with customizable font and styling.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, text: str = '', x: float = 0, y: float = 0,
font: Optional[Font] = None, fill_color: Optional[Color] = None,
outline_color: Optional[Color] = None, outline: float = 0,
click: Optional[Callable] = None) -> None: ...
text: str
font: Font
fill_color: Color
outline_color: Color
outline: float
click: Optional[Callable[[float, float, int], None]]
w: float # Read-only, computed from text
h: float # Read-only, computed from text
class Sprite(Drawable):
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)
A sprite UI element that displays a texture or portion of a texture atlas.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None,
sprite_index: int = 0, scale: float = 1.0,
click: Optional[Callable] = None) -> None: ...
texture: Texture
sprite_index: int
scale: float
click: Optional[Callable[[float, float, int], None]]
w: float # Read-only, computed from texture
h: float # Read-only, computed from texture
class Grid(Drawable):
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)
A grid-based tilemap UI element for rendering tile-based levels and game worlds.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20),
texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16,
scale: float = 1.0, click: Optional[Callable] = None) -> None: ...
grid_size: Tuple[int, int]
tile_width: int
tile_height: int
texture: Texture
scale: float
points: List[List['GridPoint']]
entities: 'EntityCollection'
background_color: Color
click: Optional[Callable[[int, int, int], None]]
def at(self, x: int, y: int) -> 'GridPoint':
"""Get grid point at tile coordinates."""
...
class GridPoint:
"""Grid point representing a single tile."""
texture_index: int
solid: bool
color: Color
class GridPointState:
"""State information for a grid point."""
texture_index: int
color: Color
class Entity(Drawable):
"""Entity(grid_x=0, grid_y=0, texture=None, sprite_index=0, name='')
Game entity that lives within a Grid.
"""
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, grid_x: float = 0, grid_y: float = 0, texture: Optional[Texture] = None,
sprite_index: int = 0, name: str = '') -> None: ...
grid_x: float
grid_y: float
texture: Texture
sprite_index: int
grid: Optional[Grid]
def at(self, grid_x: float, grid_y: float) -> None:
"""Move entity to grid position."""
...
def die(self) -> None:
"""Remove entity from its grid."""
...
def index(self) -> int:
"""Get index in parent grid's entity collection."""
...
class UICollection:
"""Collection of UI drawable elements (Frame, Caption, Sprite, Grid)."""
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> UIElement: ...
def __setitem__(self, index: int, value: UIElement) -> None: ...
def __delitem__(self, index: int) -> None: ...
def __contains__(self, item: UIElement) -> bool: ...
def __iter__(self) -> Any: ...
def __add__(self, other: 'UICollection') -> 'UICollection': ...
def __iadd__(self, other: 'UICollection') -> 'UICollection': ...
def append(self, item: UIElement) -> None: ...
def extend(self, items: List[UIElement]) -> None: ...
def remove(self, item: UIElement) -> None: ...
def index(self, item: UIElement) -> int: ...
def count(self, item: UIElement) -> int: ...
class EntityCollection:
"""Collection of Entity objects."""
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> Entity: ...
def __setitem__(self, index: int, value: Entity) -> None: ...
def __delitem__(self, index: int) -> None: ...
def __contains__(self, item: Entity) -> bool: ...
def __iter__(self) -> Any: ...
def __add__(self, other: 'EntityCollection') -> 'EntityCollection': ...
def __iadd__(self, other: 'EntityCollection') -> 'EntityCollection': ...
def append(self, item: Entity) -> None: ...
def extend(self, items: List[Entity]) -> None: ...
def remove(self, item: Entity) -> None: ...
def index(self, item: Entity) -> int: ...
def count(self, item: Entity) -> int: ...
class Scene:
"""Base class for object-oriented scenes."""
name: str
def __init__(self, name: str) -> None: ...
def activate(self) -> None:
"""Called when scene becomes active."""
...
def deactivate(self) -> None:
"""Called when scene becomes inactive."""
...
def get_ui(self) -> UICollection:
"""Get UI elements collection."""
...
def on_keypress(self, key: str, pressed: bool) -> None:
"""Handle keyboard events."""
...
def on_click(self, x: float, y: float, button: int) -> None:
"""Handle mouse clicks."""
...
def on_enter(self) -> None:
"""Called when entering the scene."""
...
def on_exit(self) -> None:
"""Called when leaving the scene."""
...
def on_resize(self, width: int, height: int) -> None:
"""Handle window resize events."""
...
def update(self, dt: float) -> None:
"""Update scene logic."""
...
class Timer:
"""Timer object for scheduled callbacks."""
name: str
interval: int
active: bool
def __init__(self, name: str, callback: Callable[[float], None], interval: int) -> None: ...
def pause(self) -> None:
"""Pause the timer."""
...
def resume(self) -> None:
"""Resume the timer."""
...
def cancel(self) -> None:
"""Cancel and remove the timer."""
...
class Window:
"""Window singleton for managing the game window."""
resolution: Tuple[int, int]
fullscreen: bool
vsync: bool
title: str
fps_limit: int
game_resolution: Tuple[int, int]
scaling_mode: str
@staticmethod
def get() -> 'Window':
"""Get the window singleton instance."""
...
class Animation:
"""Animation object for animating UI properties."""
target: Any
property: str
duration: float
easing: str
loop: bool
on_complete: Optional[Callable]
def __init__(self, target: Any, property: str, start_value: Any, end_value: Any,
duration: float, easing: str = 'linear', loop: bool = False,
on_complete: Optional[Callable] = None) -> None: ...
def start(self) -> None:
"""Start the animation."""
...
def update(self, dt: float) -> bool:
"""Update animation, returns True if still running."""
...
def get_current_value(self) -> Any:
"""Get the current interpolated value."""
...
# Module functions
def createSoundBuffer(filename: str) -> int:
"""Load a sound effect from a file and return its buffer ID."""
...
def loadMusic(filename: str) -> None:
"""Load and immediately play background music from a file."""
...
def setMusicVolume(volume: int) -> None:
"""Set the global music volume (0-100)."""
...
def setSoundVolume(volume: int) -> None:
"""Set the global sound effects volume (0-100)."""
...
def playSound(buffer_id: int) -> None:
"""Play a sound effect using a previously loaded buffer."""
...
def getMusicVolume() -> int:
"""Get the current music volume level (0-100)."""
...
def getSoundVolume() -> int:
"""Get the current sound effects volume level (0-100)."""
...
def sceneUI(scene: Optional[str] = None) -> UICollection:
"""Get all UI elements for a scene."""
...
def currentScene() -> str:
"""Get the name of the currently active scene."""
...
def setScene(scene: str, transition: Optional[str] = None, duration: float = 0.0) -> None:
"""Switch to a different scene with optional transition effect."""
...
def createScene(name: str) -> None:
"""Create a new empty scene."""
...
def keypressScene(handler: Callable[[str, bool], None]) -> None:
"""Set the keyboard event handler for the current scene."""
...
def setTimer(name: str, handler: Callable[[float], None], interval: int) -> None:
"""Create or update a recurring timer."""
...
def delTimer(name: str) -> None:
"""Stop and remove a timer."""
...
def exit() -> None:
"""Cleanly shut down the game engine and exit the application."""
...
def setScale(multiplier: float) -> None:
"""Scale the game window size (deprecated - use Window.resolution)."""
...
def find(name: str, scene: Optional[str] = None) -> Optional[UIElement]:
"""Find the first UI element with the specified name."""
...
def findAll(pattern: str, scene: Optional[str] = None) -> List[UIElement]:
"""Find all UI elements matching a name pattern (supports * wildcards)."""
...
def getMetrics() -> Dict[str, Union[int, float]]:
"""Get current performance metrics."""
...
# Submodule
class automation:
"""Automation API for testing and scripting."""
@staticmethod
def screenshot(filename: str) -> bool:
"""Save a screenshot to the specified file."""
...
@staticmethod
def position() -> Tuple[int, int]:
"""Get current mouse position as (x, y) tuple."""
...
@staticmethod
def size() -> Tuple[int, int]:
"""Get screen size as (width, height) tuple."""
...
@staticmethod
def onScreen(x: int, y: int) -> bool:
"""Check if coordinates are within screen bounds."""
...
@staticmethod
def moveTo(x: int, y: int, duration: float = 0.0) -> None:
"""Move mouse to absolute position."""
...
@staticmethod
def moveRel(xOffset: int, yOffset: int, duration: float = 0.0) -> None:
"""Move mouse relative to current position."""
...
@staticmethod
def dragTo(x: int, y: int, duration: float = 0.0, button: str = 'left') -> None:
"""Drag mouse to position."""
...
@staticmethod
def dragRel(xOffset: int, yOffset: int, duration: float = 0.0, button: str = 'left') -> None:
"""Drag mouse relative to current position."""
...
@staticmethod
def click(x: Optional[int] = None, y: Optional[int] = None, clicks: int = 1,
interval: float = 0.0, button: str = 'left') -> None:
"""Click mouse at position."""
...
@staticmethod
def mouseDown(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
"""Press mouse button down."""
...
@staticmethod
def mouseUp(x: Optional[int] = None, y: Optional[int] = None, button: str = 'left') -> None:
"""Release mouse button."""
...
@staticmethod
def keyDown(key: str) -> None:
"""Press key down."""
...
@staticmethod
def keyUp(key: str) -> None:
"""Release key."""
...
@staticmethod
def press(key: str) -> None:
"""Press and release a key."""
...
@staticmethod
def typewrite(text: str, interval: float = 0.0) -> None:
"""Type text with optional interval between characters."""
...

View File

@ -1,209 +0,0 @@
"""Type stubs for McRogueFace Python API.
Auto-generated - do not edit directly.
"""
from typing import Any, List, Dict, Tuple, Optional, Callable, Union
# Module documentation
# McRogueFace Python API\n\nCore game engine interface for creating roguelike games with Python.\n\nThis module provides:\n- Scene management (createScene, setScene, currentScene)\n- UI components (Frame, Caption, Sprite, Grid)\n- Entity system for game objects\n- Audio playback (sound effects and music)\n- Timer system for scheduled events\n- Input handling\n- Performance metrics\n\nExample:\n import mcrfpy\n \n # Create a new scene\n mcrfpy.createScene('game')\n mcrfpy.setScene('game')\n \n # Add UI elements\n frame = mcrfpy.Frame(10, 10, 200, 100)\n caption = mcrfpy.Caption('Hello World', 50, 50)\n mcrfpy.sceneUI().extend([frame, caption])\n
# Classes
class Animation:
"""Animation object for animating UI properties"""
def __init__(selftype(self)) -> None: ...
def get_current_value(self, *args, **kwargs) -> Any: ...
def start(self, *args, **kwargs) -> Any: ...
def update(selfreturns True if still running) -> Any: ...
class Caption:
"""Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Color:
"""SFML Color Object"""
def __init__(selftype(self)) -> None: ...
def from_hex(selfe.g., '#FF0000' or 'FF0000') -> Any: ...
def lerp(self, *args, **kwargs) -> Any: ...
def to_hex(self, *args, **kwargs) -> Any: ...
class Drawable:
"""Base class for all drawable UI elements"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Entity:
"""UIEntity objects"""
def __init__(selftype(self)) -> None: ...
def at(self, *args, **kwargs) -> Any: ...
def die(self, *args, **kwargs) -> Any: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def index(self, *args, **kwargs) -> Any: ...
def move(selfdx, dy) -> Any: ...
def path_to(selfx: int, y: int) -> bool: ...
def resize(selfwidth, height) -> Any: ...
def update_visibility(self) -> None: ...
class EntityCollection:
"""Iterable, indexable collection of Entities"""
def __init__(selftype(self)) -> None: ...
def append(self, *args, **kwargs) -> Any: ...
def count(self, *args, **kwargs) -> Any: ...
def extend(self, *args, **kwargs) -> Any: ...
def index(self, *args, **kwargs) -> Any: ...
def remove(self, *args, **kwargs) -> Any: ...
class Font:
"""SFML Font Object"""
def __init__(selftype(self)) -> None: ...
class Frame:
"""Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Grid:
"""Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)"""
def __init__(selftype(self)) -> None: ...
def at(self, *args, **kwargs) -> Any: ...
def compute_astar_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
def compute_dijkstra(selfroot_x: int, root_y: int, diagonal_cost: float = 1.41) -> None: ...
def compute_fov(selfx: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None: ...
def find_path(selfx1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def get_dijkstra_distance(selfx: int, y: int) -> Optional[float]: ...
def get_dijkstra_path(selfx: int, y: int) -> List[Tuple[int, int]]: ...
def is_in_fov(selfx: int, y: int) -> bool: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class GridPoint:
"""UIGridPoint object"""
def __init__(selftype(self)) -> None: ...
class GridPointState:
"""UIGridPointState object"""
def __init__(selftype(self)) -> None: ...
class Scene:
"""Base class for object-oriented scenes"""
def __init__(selftype(self)) -> None: ...
def activate(self, *args, **kwargs) -> Any: ...
def get_ui(self, *args, **kwargs) -> Any: ...
def register_keyboard(selfalternative to overriding on_keypress) -> Any: ...
class Sprite:
"""Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)"""
def __init__(selftype(self)) -> None: ...
def get_bounds(selfx, y, width, height) -> Any: ...
def move(selfdx, dy) -> Any: ...
def resize(selfwidth, height) -> Any: ...
class Texture:
"""SFML Texture Object"""
def __init__(selftype(self)) -> None: ...
class Timer:
"""Timer object for scheduled callbacks"""
def __init__(selftype(self)) -> None: ...
def cancel(self, *args, **kwargs) -> Any: ...
def pause(self, *args, **kwargs) -> Any: ...
def restart(self, *args, **kwargs) -> Any: ...
def resume(self, *args, **kwargs) -> Any: ...
class UICollection:
"""Iterable, indexable collection of UI objects"""
def __init__(selftype(self)) -> None: ...
def append(self, *args, **kwargs) -> Any: ...
def count(self, *args, **kwargs) -> Any: ...
def extend(self, *args, **kwargs) -> Any: ...
def index(self, *args, **kwargs) -> Any: ...
def remove(self, *args, **kwargs) -> Any: ...
class UICollectionIter:
"""Iterator for a collection of UI objects"""
def __init__(selftype(self)) -> None: ...
class UIEntityCollectionIter:
"""Iterator for a collection of UI objects"""
def __init__(selftype(self)) -> None: ...
class Vector:
"""SFML Vector Object"""
def __init__(selftype(self)) -> None: ...
def angle(self, *args, **kwargs) -> Any: ...
def copy(self, *args, **kwargs) -> Any: ...
def distance_to(self, *args, **kwargs) -> Any: ...
def dot(self, *args, **kwargs) -> Any: ...
def magnitude(self, *args, **kwargs) -> Any: ...
def magnitude_squared(self, *args, **kwargs) -> Any: ...
def normalize(self, *args, **kwargs) -> Any: ...
class Window:
"""Window singleton for accessing and modifying the game window properties"""
def __init__(selftype(self)) -> None: ...
def center(self, *args, **kwargs) -> Any: ...
def get(self, *args, **kwargs) -> Any: ...
def screenshot(self, *args, **kwargs) -> Any: ...
# Functions
def createScene(name: str) -> None: ...
def createSoundBuffer(filename: str) -> int: ...
def currentScene() -> str: ...
def delTimer(name: str) -> None: ...
def exit() -> None: ...
def find(name: str, scene: str = None) -> UIDrawable | None: ...
def findAll(pattern: str, scene: str = None) -> list: ...
def getMetrics() -> dict: ...
def getMusicVolume() -> int: ...
def getSoundVolume() -> int: ...
def keypressScene(handler: callable) -> None: ...
def loadMusic(filename: str) -> None: ...
def playSound(buffer_id: int) -> None: ...
def sceneUI(scene: str = None) -> list: ...
def setMusicVolume(volume: int) -> None: ...
def setScale(multiplier: float) -> None: ...
def setScene(scene: str, transition: str = None, duration: float = 0.0) -> None: ...
def setSoundVolume(volume: int) -> None: ...
def setTimer(name: str, handler: callable, interval: int) -> None: ...
# Constants
FOV_BASIC: int
FOV_DIAMOND: int
FOV_PERMISSIVE_0: int
FOV_PERMISSIVE_1: int
FOV_PERMISSIVE_2: int
FOV_PERMISSIVE_3: int
FOV_PERMISSIVE_4: int
FOV_PERMISSIVE_5: int
FOV_PERMISSIVE_6: int
FOV_PERMISSIVE_7: int
FOV_PERMISSIVE_8: int
FOV_RESTRICTIVE: int
FOV_SHADOW: int
default_font: Any
default_texture: Any

View File

@ -1,24 +0,0 @@
"""Type stubs for McRogueFace automation API."""
from typing import Optional, Tuple
def click(x=None, y=None, clicks=1, interval=0.0, button='left') -> Any: ...
def doubleClick(x=None, y=None) -> Any: ...
def dragRel(xOffset, yOffset, duration=0.0, button='left') -> Any: ...
def dragTo(x, y, duration=0.0, button='left') -> Any: ...
def hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c')) -> Any: ...
def keyDown(key) -> Any: ...
def keyUp(key) -> Any: ...
def middleClick(x=None, y=None) -> Any: ...
def mouseDown(x=None, y=None, button='left') -> Any: ...
def mouseUp(x=None, y=None, button='left') -> Any: ...
def moveRel(xOffset, yOffset, duration=0.0) -> Any: ...
def moveTo(x, y, duration=0.0) -> Any: ...
def onScreen(x, y) -> Any: ...
def position() - Get current mouse position as (x, y) -> Any: ...
def rightClick(x=None, y=None) -> Any: ...
def screenshot(filename) -> Any: ...
def scroll(clicks, x=None, y=None) -> Any: ...
def size() - Get screen size as (width, height) -> Any: ...
def tripleClick(x=None, y=None) -> Any: ...
def typewrite(message, interval=0.0) -> Any: ...

View File

View File

@ -1,253 +0,0 @@
# Part 0 - Setting Up McRogueFace
Welcome to the McRogueFace Roguelike Tutorial! This tutorial will teach you how to create a complete roguelike game using the McRogueFace game engine. Unlike traditional Python libraries, McRogueFace is a complete, portable game engine that includes everything you need to make and distribute games.
## What is McRogueFace?
McRogueFace is a high-performance game engine with Python scripting support. Think of it like Unity or Godot, but specifically designed for roguelikes and 2D games. It includes:
- A complete Python 3.12 runtime (no installation needed!)
- High-performance C++ rendering and entity management
- Built-in UI components and scene management
- Integrated audio system
- Professional sprite-based graphics
- Easy distribution - your players don't need Python installed!
## Prerequisites
Before starting this tutorial, you should:
- Have basic Python knowledge (variables, functions, classes)
- Be comfortable editing text files
- Have a text editor (VS Code, Sublime Text, Notepad++, etc.)
That's it! Unlike other roguelike tutorials, you don't need Python installed - McRogueFace includes everything.
## Getting McRogueFace
### Step 1: Download the Engine
1. Visit the McRogueFace releases page
2. Download the version for your operating system:
- `McRogueFace-Windows.zip` for Windows
- `McRogueFace-MacOS.zip` for macOS
- `McRogueFace-Linux.zip` for Linux
### Step 2: Extract the Archive
Extract the downloaded archive to a folder where you want to develop your game. You should see this structure:
```
McRogueFace/
├── mcrogueface (or mcrogueface.exe on Windows)
├── scripts/
│ └── game.py
├── assets/
│ ├── sprites/
│ ├── fonts/
│ └── audio/
└── lib/
```
### Step 3: Run the Engine
Run the McRogueFace executable:
- **Windows**: Double-click `mcrogueface.exe`
- **Mac/Linux**: Open a terminal in the folder and run `./mcrogueface`
You should see a window open with the default McRogueFace demo. This shows the engine is working correctly!
## Your First McRogueFace Script
Let's modify the engine to display "Hello Roguelike!" instead of the default demo.
### Step 1: Open game.py
Open `scripts/game.py` in your text editor. You'll see the default demo code. Replace it entirely with:
```python
import mcrfpy
# Create a new scene called "hello"
mcrfpy.createScene("hello")
# Switch to our new scene
mcrfpy.setScene("hello")
# Get the UI container for our scene
ui = mcrfpy.sceneUI("hello")
# Create a text caption
caption = mcrfpy.Caption("Hello Roguelike!", 400, 300)
caption.font_size = 32
caption.fill_color = mcrfpy.Color(255, 255, 255) # White text
# Add the caption to our scene
ui.append(caption)
# Create a smaller instruction caption
instruction = mcrfpy.Caption("Press ESC to exit", 400, 350)
instruction.font_size = 16
instruction.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
ui.append(instruction)
# Set up a simple key handler
def handle_keys(key, state):
if state == "start" and key == "Escape":
mcrfpy.setScene(None) # This exits the game
mcrfpy.keypressScene(handle_keys)
print("Hello Roguelike is running!")
```
### Step 2: Save and Run
1. Save the file
2. If McRogueFace is still running, it will automatically reload!
3. If not, run the engine again
You should now see "Hello Roguelike!" displayed in the window.
### Step 3: Understanding the Code
Let's break down what we just wrote:
1. **Import mcrfpy**: This is McRogueFace's Python API
2. **Create a scene**: Scenes are like game states (menu, gameplay, inventory, etc.)
3. **UI elements**: We create Caption objects for text display
4. **Colors**: McRogueFace uses RGB colors (0-255 for each component)
5. **Input handling**: We set up a callback for keyboard input
6. **Scene switching**: Setting the scene to None exits the game
## Key Differences from Pure Python Development
### The Game Loop
Unlike typical Python scripts, McRogueFace runs your code inside its game loop:
1. The engine starts and loads `scripts/game.py`
2. Your script sets up scenes, UI elements, and callbacks
3. The engine runs at 60 FPS, handling rendering and input
4. Your callbacks are triggered by game events
### Hot Reloading
McRogueFace can reload your scripts while running! Just save your changes and the engine will reload automatically. This makes development incredibly fast.
### Asset Pipeline
McRogueFace includes a complete asset system:
- **Sprites**: Place images in `assets/sprites/`
- **Fonts**: TrueType fonts in `assets/fonts/`
- **Audio**: Sound effects and music in `assets/audio/`
We'll explore these in later lessons.
## Testing Your Setup
Let's create a more interactive test to ensure everything is working properly:
```python
import mcrfpy
# Create our test scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
ui = mcrfpy.sceneUI("test")
# Create a background frame
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(20, 20, 30) # Dark blue-gray
ui.append(background)
# Title text
title = mcrfpy.Caption("McRogueFace Setup Test", 512, 100)
title.font_size = 36
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
ui.append(title)
# Status text that will update
status_text = mcrfpy.Caption("Press any key to test input...", 512, 300)
status_text.font_size = 20
status_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status_text)
# Instructions
instructions = [
"Arrow Keys: Test movement input",
"Space: Test action input",
"Mouse Click: Test mouse input",
"ESC: Exit"
]
y_offset = 400
for instruction in instructions:
inst_caption = mcrfpy.Caption(instruction, 512, y_offset)
inst_caption.font_size = 16
inst_caption.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(inst_caption)
y_offset += 30
# Input handler
def handle_input(key, state):
if state != "start":
return
if key == "Escape":
mcrfpy.setScene(None)
else:
status_text.text = f"You pressed: {key}"
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green
# Set up input handling
mcrfpy.keypressScene(handle_input)
print("Setup test is running! Try pressing different keys.")
```
## Troubleshooting
### Engine Won't Start
- **Windows**: Make sure you extracted all files, not just the .exe
- **Mac**: You may need to right-click and select "Open" the first time
- **Linux**: Make sure the file is executable: `chmod +x mcrogueface`
### Scripts Not Loading
- Ensure your script is named exactly `game.py` in the `scripts/` folder
- Check the console output for Python errors
- Make sure you're using Python 3 syntax
### Performance Issues
- McRogueFace should run smoothly at 60 FPS
- If not, check if your graphics drivers are updated
- The engine shows FPS in the window title
## What's Next?
Congratulations! You now have McRogueFace set up and running. You've learned:
- How to download and run the McRogueFace engine
- The basic structure of a McRogueFace project
- How to create scenes and UI elements
- How to handle keyboard input
- The development workflow with hot reloading
In Part 1, we'll create our player character and implement movement. We'll explore McRogueFace's entity system and learn how to create a game world.
## Why McRogueFace?
Before we continue, let's highlight why McRogueFace is excellent for roguelike development:
1. **No Installation Hassles**: Your players just download and run - no Python needed!
2. **Professional Performance**: C++ engine core means smooth gameplay even with hundreds of entities
3. **Built-in Features**: UI, audio, scenes, and animations are already there
4. **Easy Distribution**: Just zip your game folder and share it
5. **Rapid Development**: Hot reloading and Python scripting for quick iteration
Ready to make a roguelike? Let's continue to Part 1!

View File

@ -1,33 +0,0 @@
import mcrfpy
# Create a new scene called "hello"
mcrfpy.createScene("hello")
# Switch to our new scene
mcrfpy.setScene("hello")
# Get the UI container for our scene
ui = mcrfpy.sceneUI("hello")
# Create a text caption
caption = mcrfpy.Caption("Hello Roguelike!", 400, 300)
caption.font_size = 32
caption.fill_color = mcrfpy.Color(255, 255, 255) # White text
# Add the caption to our scene
ui.append(caption)
# Create a smaller instruction caption
instruction = mcrfpy.Caption("Press ESC to exit", 400, 350)
instruction.font_size = 16
instruction.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
ui.append(instruction)
# Set up a simple key handler
def handle_keys(key, state):
if state == "start" and key == "Escape":
mcrfpy.setScene(None) # This exits the game
mcrfpy.keypressScene(handle_keys)
print("Hello Roguelike is running!")

View File

@ -1,55 +0,0 @@
import mcrfpy
# Create our test scene
mcrfpy.createScene("test")
mcrfpy.setScene("test")
ui = mcrfpy.sceneUI("test")
# Create a background frame
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(20, 20, 30) # Dark blue-gray
ui.append(background)
# Title text
title = mcrfpy.Caption("McRogueFace Setup Test", 512, 100)
title.font_size = 36
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
ui.append(title)
# Status text that will update
status_text = mcrfpy.Caption("Press any key to test input...", 512, 300)
status_text.font_size = 20
status_text.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status_text)
# Instructions
instructions = [
"Arrow Keys: Test movement input",
"Space: Test action input",
"Mouse Click: Test mouse input",
"ESC: Exit"
]
y_offset = 400
for instruction in instructions:
inst_caption = mcrfpy.Caption(instruction, 512, y_offset)
inst_caption.font_size = 16
inst_caption.fill_color = mcrfpy.Color(150, 150, 150)
ui.append(inst_caption)
y_offset += 30
# Input handler
def handle_input(key, state):
if state != "start":
return
if key == "Escape":
mcrfpy.setScene(None)
else:
status_text.text = f"You pressed: {key}"
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green
# Set up input handling
mcrfpy.keypressScene(handle_input)
print("Setup test is running! Try pressing different keys.")

View File

@ -1,457 +0,0 @@
# Part 1 - Drawing the '@' Symbol and Moving It Around
In Part 0, we set up McRogueFace and created a simple "Hello Roguelike" scene. Now it's time to create the foundation of our game: a player character that can move around the screen.
In traditional roguelikes, the player is represented by the '@' symbol. We'll honor that tradition while taking advantage of McRogueFace's powerful sprite-based rendering system.
## Understanding McRogueFace's Architecture
Before we dive into code, let's understand two key concepts in McRogueFace:
### Grid - The Game World
A `Grid` represents your game world. It's a 2D array of tiles where each tile can be:
- **Walkable or not** (for collision detection)
- **Transparent or not** (for field of view, which we'll cover later)
- **Have a visual appearance** (sprite index and color)
Think of the Grid as the dungeon floor, walls, and other static elements.
### Entity - Things That Move
An `Entity` represents anything that can move around on the Grid:
- The player character
- Monsters
- Items (if you want them to be thrown or moved)
- Projectiles
Entities exist "on top of" the Grid and automatically handle smooth movement animation between tiles.
## Creating Our Game World
Let's start by creating a simple room for our player to move around in. Create a new `game.py`:
```python
import mcrfpy
# Define some constants for our tile types
FLOOR_TILE = 0
WALL_TILE = 1
PLAYER_SPRITE = 2
# Window configuration
mcrfpy.createScene("game")
mcrfpy.setScene("game")
# Configure window properties
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 1"
# Get the UI container for our scene
ui = mcrfpy.sceneUI("game")
# Create a dark background
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
ui.append(background)
```
Now we need to set up our tileset. For this tutorial, we'll use ASCII-style sprites. McRogueFace comes with a built-in ASCII tileset:
```python
# Load the ASCII tileset
# This tileset has characters mapped to sprite indices
# For example: @ = 64, # = 35, . = 46
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
# Create the game grid
# 50x30 tiles is a good size for a roguelike
GRID_WIDTH = 50
GRID_HEIGHT = 30
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
grid.position = (100, 100) # Position on screen
grid.size = (800, 480) # Size in pixels
# Add the grid to our UI
ui.append(grid)
```
## Initializing the Game World
Now let's fill our grid with a simple room:
```python
def create_room():
"""Create a room with walls around the edges"""
# Fill everything with floor tiles first
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
cell.sprite_index = 46 # '.' character
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
# Create walls around the edges
for x in range(GRID_WIDTH):
# Top wall
cell = grid.at(x, 0)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
# Bottom wall
cell = grid.at(x, GRID_HEIGHT - 1)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
for y in range(GRID_HEIGHT):
# Left wall
cell = grid.at(0, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Right wall
cell = grid.at(GRID_WIDTH - 1, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Create the room
create_room()
```
## Creating the Player
Now let's add our player character:
```python
# Create the player entity
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
player.sprite_index = 64 # '@' character
player.color = mcrfpy.Color(255, 255, 255) # White
# The entity is automatically added to the grid when we pass grid= parameter
# This is equivalent to: grid.entities.append(player)
```
## Handling Input
McRogueFace uses a callback system for input. For a turn-based roguelike, we only care about key presses, not releases:
```python
def handle_input(key, state):
"""Handle keyboard input for player movement"""
# Only process key presses, not releases
if state != "start":
return
# Movement deltas
dx, dy = 0, 0
# Arrow keys
if key == "Up":
dy = -1
elif key == "Down":
dy = 1
elif key == "Left":
dx = -1
elif key == "Right":
dx = 1
# Numpad movement (for true roguelike feel!)
elif key == "Num7": # Northwest
dx, dy = -1, -1
elif key == "Num8": # North
dy = -1
elif key == "Num9": # Northeast
dx, dy = 1, -1
elif key == "Num4": # West
dx = -1
elif key == "Num6": # East
dx = 1
elif key == "Num1": # Southwest
dx, dy = -1, 1
elif key == "Num2": # South
dy = 1
elif key == "Num3": # Southeast
dx, dy = 1, 1
# Escape to quit
elif key == "Escape":
mcrfpy.setScene(None)
return
# If there's movement, try to move the player
if dx != 0 or dy != 0:
move_player(dx, dy)
# Register the input handler
mcrfpy.keypressScene(handle_input)
```
## Implementing Movement with Collision Detection
Now let's implement the movement function with proper collision detection:
```python
def move_player(dx, dy):
"""Move the player if the destination is walkable"""
# Calculate new position
new_x = player.x + dx
new_y = player.y + dy
# Check bounds
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
return
# Check if the destination is walkable
destination = grid.at(new_x, new_y)
if destination.walkable:
# Move the player
player.x = new_x
player.y = new_y
# The entity will automatically animate to the new position!
```
## Adding Visual Polish
Let's add some UI elements to make our game look more polished:
```python
# Add a title
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
ui.append(title)
# Add instructions
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
ui.append(instructions)
# Add a status line at the bottom
status = mcrfpy.Caption("@ You", 100, 600)
status.font_size = 18
status.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(status)
```
## Complete Code
Here's the complete `game.py` for Part 1:
```python
import mcrfpy
# Window configuration
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 1"
# Get the UI container for our scene
ui = mcrfpy.sceneUI("game")
# Create a dark background
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
ui.append(background)
# Load the ASCII tileset
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
# Create the game grid
GRID_WIDTH = 50
GRID_HEIGHT = 30
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
grid.position = (100, 100)
grid.size = (800, 480)
ui.append(grid)
def create_room():
"""Create a room with walls around the edges"""
# Fill everything with floor tiles first
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
cell.sprite_index = 46 # '.' character
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
# Create walls around the edges
for x in range(GRID_WIDTH):
# Top wall
cell = grid.at(x, 0)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
# Bottom wall
cell = grid.at(x, GRID_HEIGHT - 1)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
for y in range(GRID_HEIGHT):
# Left wall
cell = grid.at(0, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Right wall
cell = grid.at(GRID_WIDTH - 1, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Create the room
create_room()
# Create the player entity
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
player.sprite_index = 64 # '@' character
player.color = mcrfpy.Color(255, 255, 255) # White
def move_player(dx, dy):
"""Move the player if the destination is walkable"""
# Calculate new position
new_x = player.x + dx
new_y = player.y + dy
# Check bounds
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
return
# Check if the destination is walkable
destination = grid.at(new_x, new_y)
if destination.walkable:
# Move the player
player.x = new_x
player.y = new_y
def handle_input(key, state):
"""Handle keyboard input for player movement"""
# Only process key presses, not releases
if state != "start":
return
# Movement deltas
dx, dy = 0, 0
# Arrow keys
if key == "Up":
dy = -1
elif key == "Down":
dy = 1
elif key == "Left":
dx = -1
elif key == "Right":
dx = 1
# Numpad movement (for true roguelike feel!)
elif key == "Num7": # Northwest
dx, dy = -1, -1
elif key == "Num8": # North
dy = -1
elif key == "Num9": # Northeast
dx, dy = 1, -1
elif key == "Num4": # West
dx = -1
elif key == "Num6": # East
dx = 1
elif key == "Num1": # Southwest
dx, dy = -1, 1
elif key == "Num2": # South
dy = 1
elif key == "Num3": # Southeast
dx, dy = 1, 1
# Escape to quit
elif key == "Escape":
mcrfpy.setScene(None)
return
# If there's movement, try to move the player
if dx != 0 or dy != 0:
move_player(dx, dy)
# Register the input handler
mcrfpy.keypressScene(handle_input)
# Add UI elements
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(title)
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(instructions)
status = mcrfpy.Caption("@ You", 100, 600)
status.font_size = 18
status.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(status)
print("Part 1: The @ symbol moves!")
```
## Understanding What We've Built
Let's review the key concepts we've implemented:
1. **Grid-Entity Architecture**: The Grid represents our static world (floors and walls), while the Entity (player) moves on top of it.
2. **Collision Detection**: By checking the `walkable` property of grid cells, we prevent the player from walking through walls.
3. **Turn-Based Input**: By only responding to key presses (not releases), we've created true turn-based movement.
4. **Visual Feedback**: The Entity system automatically animates movement between tiles, giving smooth visual feedback.
## Exercises
Try these modifications to deepen your understanding:
1. **Add More Rooms**: Create multiple rooms connected by corridors
2. **Different Tile Types**: Add doors (walkable but different appearance)
3. **Sprint Movement**: Hold Shift to move multiple tiles at once
4. **Mouse Support**: Click a tile to pathfind to it (we'll cover pathfinding properly later)
## ASCII Sprite Reference
Here are some useful ASCII character indices for the default tileset:
- @ (player): 64
- # (wall): 35
- . (floor): 46
- + (door): 43
- ~ (water): 126
- % (item): 37
- ! (potion): 33
## What's Next?
In Part 2, we'll expand our world with:
- A proper Entity system for managing multiple objects
- NPCs that can also move around
- A more interesting map layout
- The beginning of our game architecture
The foundation is set - you have a player character that can move around a world with collision detection. This is the core of any roguelike game!

View File

@ -1,162 +0,0 @@
import mcrfpy
# Window configuration
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 1"
# Get the UI container for our scene
ui = mcrfpy.sceneUI("game")
# Create a dark background
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
ui.append(background)
# Load the ASCII tileset
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
# Create the game grid
GRID_WIDTH = 50
GRID_HEIGHT = 30
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
grid.position = (100, 100)
grid.size = (800, 480)
ui.append(grid)
def create_room():
"""Create a room with walls around the edges"""
# Fill everything with floor tiles first
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
cell = grid.at(x, y)
cell.walkable = True
cell.transparent = True
cell.sprite_index = 46 # '.' character
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
# Create walls around the edges
for x in range(GRID_WIDTH):
# Top wall
cell = grid.at(x, 0)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
# Bottom wall
cell = grid.at(x, GRID_HEIGHT - 1)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
for y in range(GRID_HEIGHT):
# Left wall
cell = grid.at(0, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Right wall
cell = grid.at(GRID_WIDTH - 1, y)
cell.walkable = False
cell.transparent = False
cell.sprite_index = 35 # '#' character
cell.color = mcrfpy.Color(100, 100, 100)
# Create the room
create_room()
# Create the player entity
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
player.sprite_index = 64 # '@' character
player.color = mcrfpy.Color(255, 255, 255) # White
def move_player(dx, dy):
"""Move the player if the destination is walkable"""
# Calculate new position
new_x = player.x + dx
new_y = player.y + dy
# Check bounds
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
return
# Check if the destination is walkable
destination = grid.at(new_x, new_y)
if destination.walkable:
# Move the player
player.x = new_x
player.y = new_y
def handle_input(key, state):
"""Handle keyboard input for player movement"""
# Only process key presses, not releases
if state != "start":
return
# Movement deltas
dx, dy = 0, 0
# Arrow keys
if key == "Up":
dy = -1
elif key == "Down":
dy = 1
elif key == "Left":
dx = -1
elif key == "Right":
dx = 1
# Numpad movement (for true roguelike feel!)
elif key == "Num7": # Northwest
dx, dy = -1, -1
elif key == "Num8": # North
dy = -1
elif key == "Num9": # Northeast
dx, dy = 1, -1
elif key == "Num4": # West
dx = -1
elif key == "Num6": # East
dx = 1
elif key == "Num1": # Southwest
dx, dy = -1, 1
elif key == "Num2": # South
dy = 1
elif key == "Num3": # Southeast
dx, dy = 1, 1
# Escape to quit
elif key == "Escape":
mcrfpy.setScene(None)
return
# If there's movement, try to move the player
if dx != 0 or dy != 0:
move_player(dx, dy)
# Register the input handler
mcrfpy.keypressScene(handle_input)
# Add UI elements
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
ui.append(title)
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(instructions)
status = mcrfpy.Caption("@ You", 100, 600)
status.font_size = 18
status.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(status)
print("Part 1: The @ symbol moves!")

View File

@ -1,562 +0,0 @@
# Part 2 - The Generic Entity, the Render Functions, and the Map
In Part 1, we created a player character that could move around a simple room. Now it's time to build a proper architecture for our roguelike. We'll create a flexible entity system, a proper map structure, and organize our code for future expansion.
## Understanding Game Architecture
Before diving into code, let's understand the architecture we're building:
1. **Entities**: Anything that can exist in the game world (player, monsters, items)
2. **Game Map**: The dungeon structure with tiles that can be walls or floors
3. **Game Engine**: Coordinates everything - entities, map, input, and rendering
In McRogueFace, we'll adapt these concepts to work with the engine's scene-based architecture.
## Creating a Flexible Entity System
While McRogueFace provides a built-in `Entity` class, we'll create a wrapper to add game-specific functionality:
```python
class GameObject:
"""Base class for all game objects (player, monsters, items)"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks # Does this entity block movement?
self._entity = None # The McRogueFace entity
self.grid = None # Reference to the grid
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = self.color
def move(self, dx, dy):
"""Move by the given amount if possible"""
if not self.grid:
return
new_x = self.x + dx
new_y = self.y + dy
# Update our position
self.x = new_x
self.y = new_y
# Update the visual entity
if self._entity:
self._entity.x = new_x
self._entity.y = new_y
def destroy(self):
"""Remove this entity from the game"""
if self._entity and self.grid:
# Find and remove from grid's entity list
for i, entity in enumerate(self.grid.entities):
if entity == self._entity:
del self.grid.entities[i]
break
self._entity = None
```
## Building the Game Map
Let's create a proper map class that manages our dungeon:
```python
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = [] # List of GameObjects
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Initialize all tiles as walls
self.fill_with_walls()
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, color=(100, 100, 100))
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*color)
def create_room(self, x1, y1, x2, y2):
"""Carve out a room in the map"""
# Make sure coordinates are in the right order
x1, x2 = min(x1, x2), max(x1, x2)
y1, y2 = min(y1, y2), max(y1, y2)
# Carve out floor tiles
for y in range(y1, y2 + 1):
for x in range(x1, x2 + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_h(self, x1, x2, y):
"""Create a horizontal tunnel"""
for x in range(min(x1, x2), max(x1, x2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_v(self, y1, y2, x):
"""Create a vertical tunnel"""
for y in range(min(y1, y2), max(y1, y2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
# Check map boundaries
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
# Check if tile is walkable
if not self.grid.at(x, y).walkable:
return True
# Check if any blocking entity is at this position
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
```
## Creating the Game Engine
Now let's build our game engine to tie everything together:
```python
class Engine:
"""Main game engine that manages game state"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
# Create the game scene
mcrfpy.createScene("game")
mcrfpy.setScene("game")
# Configure window
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 2"
# Get UI container
self.ui = mcrfpy.sceneUI("game")
# Add background
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
# Load tileset
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
# Create the game world
self.setup_game()
# Setup input handling
self.setup_input()
# Add UI elements
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
# Create the map
self.game_map = GameMap(50, 30)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create some rooms
self.game_map.create_room(10, 10, 20, 20)
self.game_map.create_room(30, 15, 40, 25)
self.game_map.create_room(15, 22, 25, 28)
# Connect rooms with tunnels
self.game_map.create_tunnel_h(20, 30, 15)
self.game_map.create_tunnel_v(20, 22, 20)
# Create player
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
self.game_map.add_entity(self.player)
# Create an NPC
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
self.game_map.add_entity(npc)
self.entities.append(npc)
# Create some items (non-blocking)
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
self.game_map.add_entity(potion)
self.entities.append(potion)
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
# Check if movement is blocked
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
else:
# Check if we bumped into an entity
target = self.game_map.get_blocking_entity_at(new_x, new_y)
if target:
print(f"You bump into the {target.name}!")
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
# Movement keys
movement = {
"Up": (0, -1),
"Down": (0, 1),
"Left": (-1, 0),
"Right": (1, 0),
"Num7": (-1, -1),
"Num8": (0, -1),
"Num9": (1, -1),
"Num4": (-1, 0),
"Num6": (1, 0),
"Num1": (-1, 1),
"Num2": (0, 1),
"Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
# Title
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
# Instructions
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
```
## Putting It All Together
Here's the complete `game.py` file:
```python
import mcrfpy
class GameObject:
"""Base class for all game objects (player, monsters, items)"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount if possible"""
if not self.grid:
return
new_x = self.x + dx
new_y = self.y + dy
self.x = new_x
self.y = new_y
if self._entity:
self._entity.x = new_x
self._entity.y = new_y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
self.fill_with_walls()
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, color=(100, 100, 100))
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*color)
def create_room(self, x1, y1, x2, y2):
"""Carve out a room in the map"""
x1, x2 = min(x1, x2), max(x1, x2)
y1, y2 = min(y1, y2), max(y1, y2)
for y in range(y1, y2 + 1):
for x in range(x1, x2 + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_h(self, x1, x2, y):
"""Create a horizontal tunnel"""
for x in range(min(x1, x2), max(x1, x2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_v(self, y1, y2, x):
"""Create a vertical tunnel"""
for y in range(min(y1, y2), max(y1, y2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
class Engine:
"""Main game engine that manages game state"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 2"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(50, 30)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
self.game_map.create_room(10, 10, 20, 20)
self.game_map.create_room(30, 15, 40, 25)
self.game_map.create_room(15, 22, 25, 28)
self.game_map.create_tunnel_h(20, 30, 15)
self.game_map.create_tunnel_v(20, 22, 20)
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
self.game_map.add_entity(self.player)
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
self.game_map.add_entity(npc)
self.entities.append(npc)
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
self.game_map.add_entity(potion)
self.entities.append(potion)
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
else:
target = self.game_map.get_blocking_entity_at(new_x, new_y)
if target:
print(f"You bump into the {target.name}!")
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Create and run the game
engine = Engine()
print("Part 2: Entities and Maps!")
```
## Understanding the Architecture
### GameObject Class
Our `GameObject` class wraps McRogueFace's `Entity` and adds:
- Game logic properties (name, blocking)
- Position tracking independent of the visual entity
- Easy attachment/detachment from grids
### GameMap Class
The `GameMap` manages:
- The McRogueFace `Grid` for visual representation
- A list of all entities in the map
- Collision detection including entity blocking
- Map generation utilities (rooms, tunnels)
### Engine Class
The `Engine` coordinates everything:
- Scene and UI setup
- Game state management
- Input handling
- Entity-map interactions
## Key Improvements from Part 1
1. **Proper Entity Management**: Multiple entities can exist and interact
2. **Blocking Entities**: Some entities block movement, others don't
3. **Map Generation**: Tools for creating rooms and tunnels
4. **Collision System**: Checks both tiles and entities
5. **Organized Code**: Clear separation of concerns
## Exercises
1. **Add More Entity Types**: Create different sprites for monsters, items, and NPCs
2. **Entity Interactions**: Make items disappear when walked over
3. **Random Map Generation**: Place rooms and tunnels randomly
4. **Entity Properties**: Add health, damage, or other attributes to GameObjects
## What's Next?
In Part 3, we'll implement proper dungeon generation with:
- Procedurally generated rooms
- Smart tunnel routing
- Entity spawning
- The beginning of a real roguelike dungeon!
We now have a solid foundation with proper entity management and map structure. This architecture will serve us well as we add more complex features to our roguelike!

View File

@ -1,217 +0,0 @@
import mcrfpy
class GameObject:
"""Base class for all game objects (player, monsters, items)"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount if possible"""
if not self.grid:
return
new_x = self.x + dx
new_y = self.y + dy
self.x = new_x
self.y = new_y
if self._entity:
self._entity.x = new_x
self._entity.y = new_y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
self.fill_with_walls()
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, color=(100, 100, 100))
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*color)
def create_room(self, x1, y1, x2, y2):
"""Carve out a room in the map"""
x1, x2 = min(x1, x2), max(x1, x2)
y1, y2 = min(y1, y2), max(y1, y2)
for y in range(y1, y2 + 1):
for x in range(x1, x2 + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_h(self, x1, x2, y):
"""Create a horizontal tunnel"""
for x in range(min(x1, x2), max(x1, x2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def create_tunnel_v(self, y1, y2, x):
"""Create a vertical tunnel"""
for y in range(min(y1, y2), max(y1, y2) + 1):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
class Engine:
"""Main game engine that manages game state"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 2"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(50, 30)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
self.game_map.create_room(10, 10, 20, 20)
self.game_map.create_room(30, 15, 40, 25)
self.game_map.create_room(15, 22, 25, 28)
self.game_map.create_tunnel_h(20, 30, 15)
self.game_map.create_tunnel_v(20, 22, 20)
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
self.game_map.add_entity(self.player)
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
self.game_map.add_entity(npc)
self.entities.append(npc)
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
self.game_map.add_entity(potion)
self.entities.append(potion)
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
else:
target = self.game_map.get_blocking_entity_at(new_x, new_y)
if target:
print(f"You bump into the {target.name}!")
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Create and run the game
engine = Engine()
print("Part 2: Entities and Maps!")

View File

@ -1,548 +0,0 @@
# Part 3 - Generating a Dungeon
In Parts 1 and 2, we created a player that could move around and interact with a hand-crafted dungeon. Now it's time to generate dungeons procedurally - a core feature of any roguelike game!
## The Plan
We'll create a dungeon generator that:
1. Places rectangular rooms randomly
2. Ensures rooms don't overlap
3. Connects rooms with tunnels
4. Places the player in the first room
This is a classic approach used by many roguelikes, and it creates interesting, playable dungeons.
## Creating a Room Class
First, let's create a class to represent rectangular rooms:
```python
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
"""Return the center coordinates of the room"""
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
"""Return the inner area of the room as a tuple of slices
This property returns the area inside the walls.
We'll add 1 to min coordinates and subtract 1 from max coordinates.
"""
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
"""Return True if this room overlaps with another RectangularRoom"""
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
```
## Implementing Tunnel Generation
Since McRogueFace doesn't include line-drawing algorithms, let's implement simple L-shaped tunnels:
```python
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
# Randomly decide whether to go horizontal first or vertical first
if random.random() < 0.5:
# Horizontal, then vertical
corner_x = x2
corner_y = y1
else:
# Vertical, then horizontal
corner_x = x1
corner_y = y2
# Generate the coordinates
# First line: from start to corner
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
# Second line: from corner to end
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
```
## The Dungeon Generator
Now let's update our GameMap class to generate dungeons:
```python
import random
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = [] # Keep track of rooms for game logic
def generate_dungeon(
self,
max_rooms,
room_min_size,
room_max_size,
player
):
"""Generate a new dungeon map"""
# Start with everything as walls
self.fill_with_walls()
for r in range(max_rooms):
# Random width and height
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
# Random position without going out of bounds
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
# Create the room
new_room = RectangularRoom(x, y, room_width, room_height)
# Check if it intersects with any existing room
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue # This room intersects, so go to the next attempt
# If we get here, it's a valid room
# Carve out this room
self.carve_room(new_room)
# Place the player in the center of the first room
if len(self.rooms) == 0:
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
# All rooms after the first:
# Tunnel between this room and the previous one
self.carve_tunnel(self.rooms[-1].center, new_room.center)
# Finally, append the new room to the list
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(30, 30, 40)) # Slightly different color for tunnels
```
## Complete Code
Here's the complete `game.py` with procedural dungeon generation:
```python
import mcrfpy
import random
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
"""Return the center coordinates of the room"""
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
"""Return the inner area of the room"""
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
"""Return True if this room overlaps with another"""
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
# Generate the coordinates
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, color=(100, 100, 100))
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*color)
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
self.carve_tunnel(self.rooms[-1].center, new_room.center)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(30, 30, 40))
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 3"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player (before dungeon generation)
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Add player to map
self.game_map.add_entity(self.player)
# Add some monsters in random rooms
for i in range(5):
if i < len(self.game_map.rooms) - 1: # Don't spawn in first room
room = self.game_map.rooms[i + 1]
x, y = room.center
# Create an orc
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
elif key == "Space":
# Regenerate the dungeon
self.regenerate_dungeon()
mcrfpy.keypressScene(handle_keys)
def regenerate_dungeon(self):
"""Generate a new dungeon"""
# Clear existing entities
self.game_map.entities.clear()
self.game_map.rooms.clear()
self.entities.clear()
# Clear the entity list in the grid
if self.game_map.grid:
self.game_map.grid.entities.clear()
# Regenerate
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Re-add player
self.game_map.add_entity(self.player)
# Add new monsters
for i in range(5):
if i < len(self.game_map.rooms) - 1:
room = self.game_map.rooms[i + 1]
x, y = room.center
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Procedural Dungeon Generation", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move, SPACE to regenerate, ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Create and run the game
engine = Engine()
print("Part 3: Procedural Dungeon Generation!")
print("Press SPACE to generate a new dungeon")
```
## Understanding the Algorithm
Our dungeon generation algorithm is simple but effective:
1. **Start with solid walls** - The entire map begins filled with wall tiles
2. **Try to place rooms** - Generate random rooms and check for overlaps
3. **Connect with tunnels** - Each new room connects to the previous one
4. **Place entities** - The player starts in the first room, monsters in others
### Room Placement
The algorithm attempts to place `max_rooms` rooms, but may place fewer if many attempts result in overlapping rooms. This is called "rejection sampling" - we generate random rooms and reject ones that don't fit.
### Tunnel Design
Our L-shaped tunnels are simple but effective. They either go:
- Horizontal first, then vertical
- Vertical first, then horizontal
This creates variety while ensuring all rooms are connected.
## Experimenting with Parameters
Try adjusting these parameters to create different dungeon styles:
```python
# Sparse dungeon with large rooms
self.game_map.generate_dungeon(
max_rooms=10,
room_min_size=10,
room_max_size=15,
player=self.player
)
# Dense dungeon with small rooms
self.game_map.generate_dungeon(
max_rooms=50,
room_min_size=4,
room_max_size=6,
player=self.player
)
```
## Visual Enhancements
Notice how we gave tunnels a slightly different color:
- Rooms: `color=(50, 50, 50)` - Medium gray
- Tunnels: `color=(30, 30, 40)` - Darker with blue tint
This subtle difference helps players understand the dungeon layout.
## Exercises
1. **Different Room Shapes**: Create circular or cross-shaped rooms
2. **Better Tunnel Routing**: Implement A* pathfinding for more natural tunnels
3. **Room Types**: Create special rooms (treasure rooms, trap rooms)
4. **Dungeon Themes**: Use different tile sets and colors for different dungeon levels
## What's Next?
In Part 4, we'll implement Field of View (FOV) so the player can only see parts of the dungeon they've explored. This will add mystery and atmosphere to our procedurally generated dungeons!
Our dungeon generator is now creating unique, playable levels every time. The foundation of a true roguelike is taking shape!

View File

@ -1,312 +0,0 @@
import mcrfpy
import random
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
"""Return the center coordinates of the room"""
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
"""Return the inner area of the room"""
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
"""Return True if this room overlaps with another"""
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
# Generate the coordinates
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, color=(100, 100, 100))
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*color)
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
self.carve_tunnel(self.rooms[-1].center, new_room.center)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(50, 50, 50))
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, color=(30, 30, 40))
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 3"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player (before dungeon generation)
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Add player to map
self.game_map.add_entity(self.player)
# Add some monsters in random rooms
for i in range(5):
if i < len(self.game_map.rooms) - 1: # Don't spawn in first room
room = self.game_map.rooms[i + 1]
x, y = room.center
# Create an orc
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
elif key == "Space":
# Regenerate the dungeon
self.regenerate_dungeon()
mcrfpy.keypressScene(handle_keys)
def regenerate_dungeon(self):
"""Generate a new dungeon"""
# Clear existing entities
self.game_map.entities.clear()
self.game_map.rooms.clear()
self.entities.clear()
# Clear the entity list in the grid
if self.game_map.grid:
self.game_map.grid.entities.clear()
# Regenerate
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Re-add player
self.game_map.add_entity(self.player)
# Add new monsters
for i in range(5):
if i < len(self.game_map.rooms) - 1:
room = self.game_map.rooms[i + 1]
x, y = room.center
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Procedural Dungeon Generation", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move, SPACE to regenerate, ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Create and run the game
engine = Engine()
print("Part 3: Procedural Dungeon Generation!")
print("Press SPACE to generate a new dungeon")

View File

@ -1,520 +0,0 @@
# Part 4 - Field of View
One of the defining features of roguelikes is exploration and discovery. In Part 3, we could see the entire dungeon at once. Now we'll implement Field of View (FOV) so players can only see what their character can actually see, adding mystery and tactical depth to our game.
## Understanding Field of View
Field of View creates three distinct visibility states for each tile:
1. **Visible**: Currently in the player's line of sight
2. **Explored**: Previously seen but not currently visible
3. **Unexplored**: Never seen (completely hidden)
This creates the classic "fog of war" effect where you remember the layout of areas you've explored, but can't see current enemy positions unless they're in your view.
## McRogueFace's FOV System
Good news! McRogueFace includes built-in FOV support through its C++ engine. We just need to enable and configure it. The engine uses an efficient shadowcasting algorithm that provides smooth, realistic line-of-sight calculations.
Let's update our code to use FOV:
```python
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
```
## Configuring Visibility Rendering
McRogueFace automatically handles the rendering of visible/explored/unexplored tiles. We need to set up our grid to use perspective-based rendering:
```python
class GameMap:
"""Manages the game world"""
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering (0 = first entity = player)
self.grid.perspective = 0
return self.grid
```
## Visual Appearance Configuration
Let's define how our tiles look in different visibility states:
```python
# Color configurations for visibility states
COLORS_VISIBLE = {
'wall': (100, 100, 100), # Light gray
'floor': (50, 50, 50), # Dark gray
'tunnel': (30, 30, 40), # Dark blue-gray
}
COLORS_EXPLORED = {
'wall': (50, 50, 70), # Darker, bluish
'floor': (20, 20, 30), # Very dark
'tunnel': (15, 15, 25), # Almost black
}
# Update the tile-setting methods to store the tile type
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
# Store both visible and explored colors
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
# The engine will automatically darken explored tiles
```
## Complete Implementation
Here's the complete updated `game.py` with FOV:
```python
import mcrfpy
import random
# Color configurations for visibility
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering (0 = first entity = player)
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
self.carve_tunnel(self.rooms[-1].center, new_room.center)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
self.fov_radius = 8
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 4"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Add player to map
self.game_map.add_entity(self.player)
# Add monsters in random rooms
for i in range(10):
if i < len(self.game_map.rooms) - 1:
room = self.game_map.rooms[i + 1]
x, y = room.center
# Randomly offset from center
x += random.randint(-2, 2)
y += random.randint(-2, 2)
# Make sure position is walkable
if self.game_map.grid.at(x, y).walkable:
if i % 2 == 0:
# Create an orc
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
else:
# Create a troll
troll = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
self.game_map.add_entity(troll)
self.entities.append(troll)
# Initial FOV calculation
self.player.update_fov()
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
elif key == "v":
# Toggle FOV on/off
if self.game_map.grid.perspective == 0:
self.game_map.grid.perspective = -1 # Omniscient
print("FOV disabled - omniscient view")
else:
self.game_map.grid.perspective = 0 # Player perspective
print("FOV enabled - player perspective")
elif key == "Plus" or key == "Equals":
# Increase FOV radius
self.fov_radius = min(self.fov_radius + 1, 20)
self.player._entity.update_fov(radius=self.fov_radius)
print(f"FOV radius: {self.fov_radius}")
elif key == "Minus":
# Decrease FOV radius
self.fov_radius = max(self.fov_radius - 1, 3)
self.player._entity.update_fov(radius=self.fov_radius)
print(f"FOV radius: {self.fov_radius}")
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Field of View", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move | V to toggle FOV | +/- to adjust radius | ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# FOV indicator
self.fov_text = mcrfpy.Caption(f"FOV Radius: {self.fov_radius}", 900, 100)
self.fov_text.font_size = 14
self.fov_text.fill_color = mcrfpy.Color(150, 200, 255)
self.ui.append(self.fov_text)
# Create and run the game
engine = Engine()
print("Part 4: Field of View!")
print("Press V to toggle FOV on/off")
print("Press +/- to adjust FOV radius")
```
## How FOV Works
McRogueFace's built-in FOV system uses a shadowcasting algorithm that:
1. **Casts rays** from the player's position to tiles within the radius
2. **Checks transparency** along each ray path
3. **Marks tiles as visible** if the ray reaches them unobstructed
4. **Remembers explored tiles** automatically
The engine handles all the complex calculations in C++ for optimal performance.
## Visibility States in Detail
### Visible Tiles
- Currently in the player's line of sight
- Rendered at full brightness
- Show current entity positions
### Explored Tiles
- Previously seen but not currently visible
- Rendered darker/muted
- Show remembered terrain but not entities
### Unexplored Tiles
- Never been in the player's FOV
- Rendered as black/invisible
- Complete mystery to the player
## FOV Parameters
You can customize FOV behavior:
```python
# Basic FOV update
entity.update_fov(radius=8)
# The grid's perspective property controls rendering:
grid.perspective = 0 # Use first entity's FOV (player)
grid.perspective = 1 # Use second entity's FOV
grid.perspective = -1 # Omniscient (no FOV, see everything)
```
## Performance Considerations
McRogueFace's C++ FOV implementation is highly optimized:
- Uses efficient shadowcasting algorithm
- Only recalculates when needed
- Handles large maps smoothly
- Automatically culls entities outside FOV
## Visual Polish
The engine automatically handles visual transitions:
- Smooth color changes between visibility states
- Entities fade in/out of view
- Explored areas remain visible but dimmed
## Exercises
1. **Variable Vision**: Give different entities different FOV radii
2. **Light Sources**: Create torches that expand local FOV
3. **Blind Spots**: Add pillars that create interesting shadows
4. **X-Ray Vision**: Temporary power-up to see through walls
## What's Next?
In Part 5, we'll place enemies throughout the dungeon and implement basic interactions. With FOV in place, enemies will appear and disappear as you explore, creating tension and surprise!
Field of View transforms our dungeon from a tactical puzzle into a mysterious world to explore. The fog of war adds atmosphere and gameplay depth that's essential to the roguelike experience.

View File

@ -1,334 +0,0 @@
import mcrfpy
import random
# Color configurations for visibility
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering (0 = first entity = player)
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
self.carve_tunnel(self.rooms[-1].center, new_room.center)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
self.fov_radius = 8
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 4"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player
)
# Add player to map
self.game_map.add_entity(self.player)
# Add monsters in random rooms
for i in range(10):
if i < len(self.game_map.rooms) - 1:
room = self.game_map.rooms[i + 1]
x, y = room.center
# Randomly offset from center
x += random.randint(-2, 2)
y += random.randint(-2, 2)
# Make sure position is walkable
if self.game_map.grid.at(x, y).walkable:
if i % 2 == 0:
# Create an orc
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
self.game_map.add_entity(orc)
self.entities.append(orc)
else:
# Create a troll
troll = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
self.game_map.add_entity(troll)
self.entities.append(troll)
# Initial FOV calculation
self.player.update_fov()
def handle_movement(self, dx, dy):
"""Handle player movement"""
new_x = self.player.x + dx
new_y = self.player.y + dy
if not self.game_map.is_blocked(new_x, new_y):
self.player.move(dx, dy)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
self.handle_movement(dx, dy)
elif key == "Escape":
mcrfpy.setScene(None)
elif key == "v":
# Toggle FOV on/off
if self.game_map.grid.perspective == 0:
self.game_map.grid.perspective = -1 # Omniscient
print("FOV disabled - omniscient view")
else:
self.game_map.grid.perspective = 0 # Player perspective
print("FOV enabled - player perspective")
elif key == "Plus" or key == "Equals":
# Increase FOV radius
self.fov_radius = min(self.fov_radius + 1, 20)
self.player._entity.update_fov(radius=self.fov_radius)
print(f"FOV radius: {self.fov_radius}")
elif key == "Minus":
# Decrease FOV radius
self.fov_radius = max(self.fov_radius - 1, 3)
self.player._entity.update_fov(radius=self.fov_radius)
print(f"FOV radius: {self.fov_radius}")
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Field of View", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move | V to toggle FOV | +/- to adjust radius | ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# FOV indicator
self.fov_text = mcrfpy.Caption(f"FOV Radius: {self.fov_radius}", 900, 100)
self.fov_text.font_size = 14
self.fov_text.fill_color = mcrfpy.Color(150, 200, 255)
self.ui.append(self.fov_text)
# Create and run the game
engine = Engine()
print("Part 4: Field of View!")
print("Press V to toggle FOV on/off")
print("Press +/- to adjust FOV radius")

View File

@ -1,570 +0,0 @@
# Part 5 - Placing Enemies and Kicking Them (Harmlessly)
Now that we have Field of View working, it's time to populate our dungeon with enemies! In this part, we'll:
- Place enemies randomly in rooms
- Implement entity-to-entity collision detection
- Create basic interactions (bumping into enemies)
- Set the stage for combat in Part 6
## Enemy Spawning System
First, let's create a system to spawn enemies in our dungeon rooms. We'll avoid placing them in the first room (where the player starts) to give players a safe starting area.
```python
def spawn_enemies_in_room(room, game_map, max_enemies=2):
"""Spawn between 0 and max_enemies in a room"""
import random
number_of_enemies = random.randint(0, max_enemies)
for i in range(number_of_enemies):
# Try to find a valid position
attempts = 10
while attempts > 0:
# Random position within room bounds
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
# Check if position is valid
if not game_map.is_blocked(x, y):
# 80% chance for orc, 20% for troll
if random.random() < 0.8:
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
else:
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
game_map.add_entity(enemy)
break
attempts -= 1
```
## Enhanced Collision Detection
We need to improve our collision detection to check for entities, not just walls:
```python
class GameMap:
"""Manages the game world"""
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
# Check boundaries
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
# Check walls
if not self.grid.at(x, y).walkable:
return True
# Check entities
if self.get_blocking_entity_at(x, y):
return True
return False
```
## Action System Introduction
Let's create a simple action system to handle different types of interactions:
```python
class Action:
"""Base class for all actions"""
pass
class MovementAction(Action):
"""Action for moving an entity"""
def __init__(self, dx, dy):
self.dx = dx
self.dy = dy
class BumpAction(Action):
"""Action for bumping into something"""
def __init__(self, dx, dy, target=None):
self.dx = dx
self.dy = dy
self.target = target
class WaitAction(Action):
"""Action for waiting/skipping turn"""
pass
```
## Handling Player Actions
Now let's update our movement handling to support bumping into enemies:
```python
def handle_player_turn(self, action):
"""Process the player's action"""
if isinstance(action, MovementAction):
dest_x = self.player.x + action.dx
dest_y = self.player.y + action.dy
# Check what's at the destination
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
if target:
# We bumped into something!
print(f"You kick the {target.name} in the shins, much to its annoyance!")
elif not self.game_map.is_blocked(dest_x, dest_y):
# Move the player
self.player.move(action.dx, action.dy)
# Update message
self.status_text.text = "Exploring the dungeon..."
else:
# Bumped into a wall
self.status_text.text = "Ouch! You bump into a wall."
elif isinstance(action, WaitAction):
self.status_text.text = "You wait..."
```
## Complete Updated Code
Here's the complete `game.py` with enemy placement and interactions:
```python
import mcrfpy
import random
# Color configurations
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
# Actions
class Action:
"""Base class for all actions"""
pass
class MovementAction(Action):
"""Action for moving an entity"""
def __init__(self, dx, dy):
self.dx = dx
self.dy = dy
class WaitAction(Action):
"""Action for waiting/skipping turn"""
pass
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
def spawn_enemies_in_room(room, game_map, max_enemies=2):
"""Spawn between 0 and max_enemies in a room"""
number_of_enemies = random.randint(0, max_enemies)
enemies_spawned = []
for i in range(number_of_enemies):
# Try to find a valid position
attempts = 10
while attempts > 0:
# Random position within room bounds
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
# Check if position is valid
if not game_map.is_blocked(x, y):
# 80% chance for orc, 20% for troll
if random.random() < 0.8:
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
else:
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
game_map.add_entity(enemy)
enemies_spawned.append(enemy)
break
attempts -= 1
return enemies_spawned
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
# First room - place player
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
# All other rooms - add tunnel and enemies
self.carve_tunnel(self.rooms[-1].center, new_room.center)
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
if self.get_blocking_entity_at(x, y):
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 5"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player,
max_enemies_per_room=2
)
# Add player to map
self.game_map.add_entity(self.player)
# Store reference to all entities
self.entities = [e for e in self.game_map.entities if e != self.player]
# Initial FOV calculation
self.player.update_fov()
def handle_player_turn(self, action):
"""Process the player's action"""
if isinstance(action, MovementAction):
dest_x = self.player.x + action.dx
dest_y = self.player.y + action.dy
# Check what's at the destination
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
if target:
# We bumped into something!
print(f"You kick the {target.name} in the shins, much to its annoyance!")
self.status_text.text = f"You kick the {target.name}!"
elif not self.game_map.is_blocked(dest_x, dest_y):
# Move the player
self.player.move(action.dx, action.dy)
self.status_text.text = ""
else:
# Bumped into a wall
self.status_text.text = "Blocked!"
elif isinstance(action, WaitAction):
self.status_text.text = "You wait..."
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
action = None
# Movement keys
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
if dx == 0 and dy == 0:
action = WaitAction()
else:
action = MovementAction(dx, dy)
elif key == "Period":
action = WaitAction()
elif key == "Escape":
mcrfpy.setScene(None)
return
# Process the action
if action:
self.handle_player_turn(action)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Placing Enemies", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move | . to wait | Bump into enemies! | ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Status text
self.status_text = mcrfpy.Caption("", 512, 600)
self.status_text.font_size = 18
self.status_text.fill_color = mcrfpy.Color(255, 200, 200)
self.ui.append(self.status_text)
# Entity count
entity_count = len(self.entities)
count_text = mcrfpy.Caption(f"Enemies: {entity_count}", 900, 100)
count_text.font_size = 14
count_text.fill_color = mcrfpy.Color(150, 150, 255)
self.ui.append(count_text)
# Create and run the game
engine = Engine()
print("Part 5: Placing Enemies!")
print("Try bumping into enemies - combat coming in Part 6!")
```
## Understanding Entity Interactions
### Collision Detection
Our system now checks three things when the player tries to move:
1. **Map boundaries** - Can't move outside the map
2. **Wall tiles** - Can't walk through walls
3. **Blocking entities** - Can't walk through enemies
### The Action System
We've introduced a simple action system that will grow in Part 6:
- `Action` - Base class for all actions
- `MovementAction` - Represents attempted movement
- `WaitAction` - Skip a turn (important for turn-based games)
### Entity Spawning
Enemies are placed randomly in rooms with these rules:
- Never in the first room (player's starting room)
- Random number between 0 and max per room
- 80% orcs, 20% trolls
- Must be placed on walkable, unoccupied tiles
## Visual Feedback
With FOV enabled, enemies will appear and disappear as you explore:
- Enemies in sight are fully visible
- Enemies in explored but dark areas are hidden
- Creates tension and surprise encounters
## Exercises
1. **More Enemy Types**: Add different sprites and names (goblins, skeletons)
2. **Enemy Density**: Adjust spawn rates based on dungeon depth
3. **Special Rooms**: Create rooms with guaranteed enemies or treasures
4. **Better Feedback**: Add sound effects or visual effects for bumping
## What's Next?
In Part 6, we'll transform those harmless kicks into a real combat system! We'll add:
- Health points for all entities
- Damage calculations
- Death and corpses
- Combat messages
- The beginning of a real roguelike!
Right now our enemies are just obstacles. Soon they'll fight back!

View File

@ -1,388 +0,0 @@
import mcrfpy
import random
# Color configurations
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
# Actions
class Action:
"""Base class for all actions"""
pass
class MovementAction(Action):
"""Action for moving an entity"""
def __init__(self, dx, dy):
self.dx = dx
self.dy = dy
class WaitAction(Action):
"""Action for waiting/skipping turn"""
pass
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name, blocks=False):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
def spawn_enemies_in_room(room, game_map, max_enemies=2):
"""Spawn between 0 and max_enemies in a room"""
number_of_enemies = random.randint(0, max_enemies)
enemies_spawned = []
for i in range(number_of_enemies):
# Try to find a valid position
attempts = 10
while attempts > 0:
# Random position within room bounds
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
# Check if position is valid
if not game_map.is_blocked(x, y):
# 80% chance for orc, 20% for troll
if random.random() < 0.8:
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
else:
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
game_map.add_entity(enemy)
enemies_spawned.append(enemy)
break
attempts -= 1
return enemies_spawned
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
# First room - place player
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
# All other rooms - add tunnel and enemies
self.carve_tunnel(self.rooms[-1].center, new_room.center)
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
if self.get_blocking_entity_at(x, y):
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 5"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player,
max_enemies_per_room=2
)
# Add player to map
self.game_map.add_entity(self.player)
# Store reference to all entities
self.entities = [e for e in self.game_map.entities if e != self.player]
# Initial FOV calculation
self.player.update_fov()
def handle_player_turn(self, action):
"""Process the player's action"""
if isinstance(action, MovementAction):
dest_x = self.player.x + action.dx
dest_y = self.player.y + action.dy
# Check what's at the destination
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
if target:
# We bumped into something!
print(f"You kick the {target.name} in the shins, much to its annoyance!")
self.status_text.text = f"You kick the {target.name}!"
elif not self.game_map.is_blocked(dest_x, dest_y):
# Move the player
self.player.move(action.dx, action.dy)
self.status_text.text = ""
else:
# Bumped into a wall
self.status_text.text = "Blocked!"
elif isinstance(action, WaitAction):
self.status_text.text = "You wait..."
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
action = None
# Movement keys
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
if dx == 0 and dy == 0:
action = WaitAction()
else:
action = MovementAction(dx, dy)
elif key == "Period":
action = WaitAction()
elif key == "Escape":
mcrfpy.setScene(None)
return
# Process the action
if action:
self.handle_player_turn(action)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Placing Enemies", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Arrow keys to move | . to wait | Bump into enemies! | ESC to quit", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Status text
self.status_text = mcrfpy.Caption("", 512, 600)
self.status_text.font_size = 18
self.status_text.fill_color = mcrfpy.Color(255, 200, 200)
self.ui.append(self.status_text)
# Entity count
entity_count = len(self.entities)
count_text = mcrfpy.Caption(f"Enemies: {entity_count}", 900, 100)
count_text.font_size = 14
count_text.fill_color = mcrfpy.Color(150, 150, 255)
self.ui.append(count_text)
# Create and run the game
engine = Engine()
print("Part 5: Placing Enemies!")
print("Try bumping into enemies - combat coming in Part 6!")

View File

@ -1,743 +0,0 @@
# Part 6 - Doing (and Taking) Some Damage
It's time to turn our harmless kicks into real combat! In this part, we'll implement:
- Health points for all entities
- A damage calculation system
- Death and corpse mechanics
- Combat feedback messages
- The foundation of tactical roguelike combat
## Adding Combat Stats
First, let's enhance our GameObject class with combat capabilities:
```python
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name,
blocks=False, hp=0, defense=0, power=0):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
# Combat stats
self.max_hp = hp
self.hp = hp
self.defense = defense
self.power = power
@property
def is_alive(self):
"""Returns True if this entity can act"""
return self.hp > 0
def take_damage(self, amount):
"""Apply damage to this entity"""
damage = amount - self.defense
if damage > 0:
self.hp -= damage
# Check for death
if self.hp <= 0 and self.hp + damage > 0:
self.die()
return damage
def die(self):
"""Handle entity death"""
if self.name == "Player":
# Player death is special - we'll handle it differently
self.sprite_index = 64 # Stay as @ but change color
self.color = (127, 0, 0) # Dark red
if self._entity:
self._entity.color = mcrfpy.Color(127, 0, 0)
print("You have died!")
else:
# Enemy death
self.sprite_index = 37 # % character for corpse
self.color = (127, 0, 0) # Dark red
self.blocks = False # Corpses don't block
self.name = f"remains of {self.name}"
if self._entity:
self._entity.sprite_index = 37
self._entity.color = mcrfpy.Color(127, 0, 0)
```
## The Combat System
Now let's implement actual combat when entities bump into each other:
```python
class MeleeAction(Action):
"""Action for melee attacks"""
def __init__(self, attacker, target):
self.attacker = attacker
self.target = target
def perform(self):
"""Execute the attack"""
if not self.target.is_alive:
return # Can't attack the dead
damage = self.attacker.power - self.target.defense
if damage > 0:
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
self.target.take_damage(damage)
else:
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
return attack_desc
```
## Entity Factories
Let's create factory functions for consistent entity creation:
```python
def create_player(x, y):
"""Create the player entity"""
return GameObject(
x=x, y=y,
sprite_index=64, # @
color=(255, 255, 255),
name="Player",
blocks=True,
hp=30,
defense=2,
power=5
)
def create_orc(x, y):
"""Create an orc enemy"""
return GameObject(
x=x, y=y,
sprite_index=111, # o
color=(63, 127, 63),
name="Orc",
blocks=True,
hp=10,
defense=0,
power=3
)
def create_troll(x, y):
"""Create a troll enemy"""
return GameObject(
x=x, y=y,
sprite_index=84, # T
color=(0, 127, 0),
name="Troll",
blocks=True,
hp=16,
defense=1,
power=4
)
```
## The Message Log
Combat needs feedback! Let's create a simple message log:
```python
class MessageLog:
"""Manages game messages"""
def __init__(self, max_messages=5):
self.messages = []
self.max_messages = max_messages
def add_message(self, text, color=(255, 255, 255)):
"""Add a message to the log"""
self.messages.append((text, color))
# Keep only recent messages
if len(self.messages) > self.max_messages:
self.messages.pop(0)
def render(self, ui, x, y, line_height=20):
"""Render messages to the UI"""
for i, (text, color) in enumerate(self.messages):
caption = mcrfpy.Caption(text, x, y + i * line_height)
caption.font_size = 14
caption.fill_color = mcrfpy.Color(*color)
ui.append(caption)
```
## Complete Implementation
Here's the complete `game.py` with combat:
```python
import mcrfpy
import random
# Color configurations
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
# Message colors
COLOR_PLAYER_ATK = (230, 230, 230)
COLOR_ENEMY_ATK = (255, 200, 200)
COLOR_PLAYER_DIE = (255, 100, 100)
COLOR_ENEMY_DIE = (255, 165, 0)
# Actions
class Action:
"""Base class for all actions"""
pass
class MovementAction(Action):
"""Action for moving an entity"""
def __init__(self, dx, dy):
self.dx = dx
self.dy = dy
class MeleeAction(Action):
"""Action for melee attacks"""
def __init__(self, attacker, target):
self.attacker = attacker
self.target = target
def perform(self):
"""Execute the attack"""
if not self.target.is_alive:
return None
damage = self.attacker.power - self.target.defense
if damage > 0:
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
self.target.take_damage(damage)
# Choose color based on attacker
if self.attacker.name == "Player":
color = COLOR_PLAYER_ATK
else:
color = COLOR_ENEMY_ATK
return attack_desc, color
else:
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
return attack_desc, (150, 150, 150)
class WaitAction(Action):
"""Action for waiting/skipping turn"""
pass
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name,
blocks=False, hp=0, defense=0, power=0):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
# Combat stats
self.max_hp = hp
self.hp = hp
self.defense = defense
self.power = power
@property
def is_alive(self):
"""Returns True if this entity can act"""
return self.hp > 0
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
def take_damage(self, amount):
"""Apply damage to this entity"""
self.hp -= amount
# Check for death
if self.hp <= 0:
self.die()
def die(self):
"""Handle entity death"""
if self.name == "Player":
# Player death
self.sprite_index = 64 # Stay as @
self.color = (127, 0, 0) # Dark red
if self._entity:
self._entity.color = mcrfpy.Color(127, 0, 0)
else:
# Enemy death
self.sprite_index = 37 # % character for corpse
self.color = (127, 0, 0) # Dark red
self.blocks = False # Corpses don't block
self.name = f"remains of {self.name}"
if self._entity:
self._entity.sprite_index = 37
self._entity.color = mcrfpy.Color(127, 0, 0)
# Entity factories
def create_player(x, y):
"""Create the player entity"""
return GameObject(
x=x, y=y,
sprite_index=64, # @
color=(255, 255, 255),
name="Player",
blocks=True,
hp=30,
defense=2,
power=5
)
def create_orc(x, y):
"""Create an orc enemy"""
return GameObject(
x=x, y=y,
sprite_index=111, # o
color=(63, 127, 63),
name="Orc",
blocks=True,
hp=10,
defense=0,
power=3
)
def create_troll(x, y):
"""Create a troll enemy"""
return GameObject(
x=x, y=y,
sprite_index=84, # T
color=(0, 127, 0),
name="Troll",
blocks=True,
hp=16,
defense=1,
power=4
)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
def spawn_enemies_in_room(room, game_map, max_enemies=2):
"""Spawn between 0 and max_enemies in a room"""
number_of_enemies = random.randint(0, max_enemies)
enemies_spawned = []
for i in range(number_of_enemies):
attempts = 10
while attempts > 0:
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
if not game_map.is_blocked(x, y):
# 80% chance for orc, 20% for troll
if random.random() < 0.8:
enemy = create_orc(x, y)
else:
enemy = create_troll(x, y)
game_map.add_entity(enemy)
enemies_spawned.append(enemy)
break
attempts -= 1
return enemies_spawned
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
# First room - place player
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
# All other rooms - add tunnel and enemies
self.carve_tunnel(self.rooms[-1].center, new_room.center)
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
if self.get_blocking_entity_at(x, y):
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
self.messages = [] # Simple message log
self.max_messages = 5
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 6"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def add_message(self, text, color=(255, 255, 255)):
"""Add a message to the log"""
self.messages.append((text, color))
if len(self.messages) > self.max_messages:
self.messages.pop(0)
self.update_message_display()
def update_message_display(self):
"""Update the message display"""
# Clear old messages
for caption in self.message_captions:
# Remove from UI (McRogueFace doesn't have remove, so we hide it)
caption.text = ""
# Display current messages
for i, (text, color) in enumerate(self.messages):
if i < len(self.message_captions):
self.message_captions[i].text = text
self.message_captions[i].fill_color = mcrfpy.Color(*color)
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = create_player(0, 0)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player,
max_enemies_per_room=2
)
# Add player to map
self.game_map.add_entity(self.player)
# Store reference to all entities
self.entities = [e for e in self.game_map.entities if e != self.player]
# Initial FOV calculation
self.player.update_fov()
# Welcome message
self.add_message("Welcome to the dungeon!", (100, 100, 255))
def handle_player_turn(self, action):
"""Process the player's action"""
if not self.player.is_alive:
return
if isinstance(action, MovementAction):
dest_x = self.player.x + action.dx
dest_y = self.player.y + action.dy
# Check what's at the destination
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
if target:
# Attack!
attack = MeleeAction(self.player, target)
result = attack.perform()
if result:
text, color = result
self.add_message(text, color)
# Check if target died
if not target.is_alive:
death_msg = f"The {target.name.replace('remains of ', '')} is dead!"
self.add_message(death_msg, COLOR_ENEMY_DIE)
elif not self.game_map.is_blocked(dest_x, dest_y):
# Move the player
self.player.move(action.dx, action.dy)
elif isinstance(action, WaitAction):
pass # Do nothing
# Enemy turns
self.handle_enemy_turns()
def handle_enemy_turns(self):
"""Let all enemies take their turn"""
for entity in self.entities:
if entity.is_alive:
# Simple AI: if player is adjacent, attack. Otherwise, do nothing.
dx = entity.x - self.player.x
dy = entity.y - self.player.y
distance = abs(dx) + abs(dy)
if distance == 1: # Adjacent to player
attack = MeleeAction(entity, self.player)
result = attack.perform()
if result:
text, color = result
self.add_message(text, color)
# Check if player died
if not self.player.is_alive:
self.add_message("You have died!", COLOR_PLAYER_DIE)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
action = None
# Movement keys
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
if dx == 0 and dy == 0:
action = WaitAction()
else:
action = MovementAction(dx, dy)
elif key == "Period":
action = WaitAction()
elif key == "Escape":
mcrfpy.setScene(None)
return
# Process the action
if action:
self.handle_player_turn(action)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Combat System", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Attack enemies by bumping into them!", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Player stats
self.hp_text = mcrfpy.Caption(f"HP: {self.player.hp}/{self.player.max_hp}", 50, 100)
self.hp_text.font_size = 18
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
self.ui.append(self.hp_text)
# Message log
self.message_captions = []
for i in range(self.max_messages):
caption = mcrfpy.Caption("", 50, 620 + i * 20)
caption.font_size = 14
caption.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(caption)
self.message_captions.append(caption)
# Timer to update HP display
def update_stats(dt):
self.hp_text.text = f"HP: {self.player.hp}/{self.player.max_hp}"
if self.player.hp <= 0:
self.hp_text.fill_color = mcrfpy.Color(127, 0, 0)
elif self.player.hp < self.player.max_hp // 3:
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
else:
self.hp_text.fill_color = mcrfpy.Color(0, 255, 0)
mcrfpy.setTimer("update_stats", update_stats, 100)
# Create and run the game
engine = Engine()
print("Part 6: Combat System!")
print("Attack enemies to defeat them, but watch your HP!")

View File

@ -1,568 +0,0 @@
import mcrfpy
import random
# Color configurations
COLORS_VISIBLE = {
'wall': (100, 100, 100),
'floor': (50, 50, 50),
'tunnel': (30, 30, 40),
}
# Message colors
COLOR_PLAYER_ATK = (230, 230, 230)
COLOR_ENEMY_ATK = (255, 200, 200)
COLOR_PLAYER_DIE = (255, 100, 100)
COLOR_ENEMY_DIE = (255, 165, 0)
# Actions
class Action:
"""Base class for all actions"""
pass
class MovementAction(Action):
"""Action for moving an entity"""
def __init__(self, dx, dy):
self.dx = dx
self.dy = dy
class MeleeAction(Action):
"""Action for melee attacks"""
def __init__(self, attacker, target):
self.attacker = attacker
self.target = target
def perform(self):
"""Execute the attack"""
if not self.target.is_alive:
return None
damage = self.attacker.power - self.target.defense
if damage > 0:
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
self.target.take_damage(damage)
# Choose color based on attacker
if self.attacker.name == "Player":
color = COLOR_PLAYER_ATK
else:
color = COLOR_ENEMY_ATK
return attack_desc, color
else:
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
return attack_desc, (150, 150, 150)
class WaitAction(Action):
"""Action for waiting/skipping turn"""
pass
class GameObject:
"""Base class for all game objects"""
def __init__(self, x, y, sprite_index, color, name,
blocks=False, hp=0, defense=0, power=0):
self.x = x
self.y = y
self.sprite_index = sprite_index
self.color = color
self.name = name
self.blocks = blocks
self._entity = None
self.grid = None
# Combat stats
self.max_hp = hp
self.hp = hp
self.defense = defense
self.power = power
@property
def is_alive(self):
"""Returns True if this entity can act"""
return self.hp > 0
def attach_to_grid(self, grid):
"""Attach this game object to a McRogueFace grid"""
self.grid = grid
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
self._entity.sprite_index = self.sprite_index
self._entity.color = mcrfpy.Color(*self.color)
def move(self, dx, dy):
"""Move by the given amount"""
if not self.grid:
return
self.x += dx
self.y += dy
if self._entity:
self._entity.x = self.x
self._entity.y = self.y
# Update FOV when player moves
if self.name == "Player":
self.update_fov()
def update_fov(self):
"""Update field of view from this entity's position"""
if self._entity and self.grid:
self._entity.update_fov(radius=8)
def take_damage(self, amount):
"""Apply damage to this entity"""
self.hp -= amount
# Check for death
if self.hp <= 0:
self.die()
def die(self):
"""Handle entity death"""
if self.name == "Player":
# Player death
self.sprite_index = 64 # Stay as @
self.color = (127, 0, 0) # Dark red
if self._entity:
self._entity.color = mcrfpy.Color(127, 0, 0)
else:
# Enemy death
self.sprite_index = 37 # % character for corpse
self.color = (127, 0, 0) # Dark red
self.blocks = False # Corpses don't block
self.name = f"remains of {self.name}"
if self._entity:
self._entity.sprite_index = 37
self._entity.color = mcrfpy.Color(127, 0, 0)
# Entity factories
def create_player(x, y):
"""Create the player entity"""
return GameObject(
x=x, y=y,
sprite_index=64, # @
color=(255, 255, 255),
name="Player",
blocks=True,
hp=30,
defense=2,
power=5
)
def create_orc(x, y):
"""Create an orc enemy"""
return GameObject(
x=x, y=y,
sprite_index=111, # o
color=(63, 127, 63),
name="Orc",
blocks=True,
hp=10,
defense=0,
power=3
)
def create_troll(x, y):
"""Create a troll enemy"""
return GameObject(
x=x, y=y,
sprite_index=84, # T
color=(0, 127, 0),
name="Troll",
blocks=True,
hp=16,
defense=1,
power=4
)
class RectangularRoom:
"""A rectangular room with its position and size"""
def __init__(self, x, y, width, height):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self):
center_x = (self.x1 + self.x2) // 2
center_y = (self.y1 + self.y2) // 2
return center_x, center_y
@property
def inner(self):
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
def intersects(self, other):
return (
self.x1 <= other.x2
and self.x2 >= other.x1
and self.y1 <= other.y2
and self.y2 >= other.y1
)
def tunnel_between(start, end):
"""Return an L-shaped tunnel between two points"""
x1, y1 = start
x2, y2 = end
if random.random() < 0.5:
corner_x = x2
corner_y = y1
else:
corner_x = x1
corner_y = y2
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
yield x, y1
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
yield corner_x, y
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
yield x, corner_y
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
yield x2, y
def spawn_enemies_in_room(room, game_map, max_enemies=2):
"""Spawn between 0 and max_enemies in a room"""
number_of_enemies = random.randint(0, max_enemies)
enemies_spawned = []
for i in range(number_of_enemies):
attempts = 10
while attempts > 0:
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
if not game_map.is_blocked(x, y):
# 80% chance for orc, 20% for troll
if random.random() < 0.8:
enemy = create_orc(x, y)
else:
enemy = create_troll(x, y)
game_map.add_entity(enemy)
enemies_spawned.append(enemy)
break
attempts -= 1
return enemies_spawned
class GameMap:
"""Manages the game world"""
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = None
self.entities = []
self.rooms = []
def create_grid(self, tileset):
"""Create the McRogueFace grid"""
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
self.grid.position = (100, 100)
self.grid.size = (800, 480)
# Enable perspective rendering
self.grid.perspective = 0
return self.grid
def fill_with_walls(self):
"""Fill the entire map with wall tiles"""
for y in range(self.height):
for x in range(self.width):
self.set_tile(x, y, walkable=False, transparent=False,
sprite_index=35, tile_type='wall')
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
"""Set properties for a specific tile"""
if 0 <= x < self.width and 0 <= y < self.height:
cell = self.grid.at(x, y)
cell.walkable = walkable
cell.transparent = transparent
cell.sprite_index = sprite_index
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
"""Generate a new dungeon map"""
self.fill_with_walls()
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, self.width - room_width - 1)
y = random.randint(0, self.height - room_height - 1)
new_room = RectangularRoom(x, y, room_width, room_height)
if any(new_room.intersects(other_room) for other_room in self.rooms):
continue
self.carve_room(new_room)
if len(self.rooms) == 0:
# First room - place player
player.x, player.y = new_room.center
if player._entity:
player._entity.x, player._entity.y = new_room.center
else:
# All other rooms - add tunnel and enemies
self.carve_tunnel(self.rooms[-1].center, new_room.center)
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
self.rooms.append(new_room)
def carve_room(self, room):
"""Carve out a room"""
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
for y in range(inner_y1, inner_y2):
for x in range(inner_x1, inner_x2):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='floor')
def carve_tunnel(self, start, end):
"""Carve a tunnel between two points"""
for x, y in tunnel_between(start, end):
self.set_tile(x, y, walkable=True, transparent=True,
sprite_index=46, tile_type='tunnel')
def get_blocking_entity_at(self, x, y):
"""Return any blocking entity at the given position"""
for entity in self.entities:
if entity.blocks and entity.x == x and entity.y == y:
return entity
return None
def is_blocked(self, x, y):
"""Check if a tile blocks movement"""
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return True
if not self.grid.at(x, y).walkable:
return True
if self.get_blocking_entity_at(x, y):
return True
return False
def add_entity(self, entity):
"""Add a GameObject to the map"""
self.entities.append(entity)
entity.attach_to_grid(self.grid)
class Engine:
"""Main game engine"""
def __init__(self):
self.game_map = None
self.player = None
self.entities = []
self.messages = [] # Simple message log
self.max_messages = 5
mcrfpy.createScene("game")
mcrfpy.setScene("game")
window = mcrfpy.Window.get()
window.title = "McRogueFace Roguelike - Part 6"
self.ui = mcrfpy.sceneUI("game")
background = mcrfpy.Frame(0, 0, 1024, 768)
background.fill_color = mcrfpy.Color(0, 0, 0)
self.ui.append(background)
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
self.setup_game()
self.setup_input()
self.setup_ui()
def add_message(self, text, color=(255, 255, 255)):
"""Add a message to the log"""
self.messages.append((text, color))
if len(self.messages) > self.max_messages:
self.messages.pop(0)
self.update_message_display()
def update_message_display(self):
"""Update the message display"""
# Clear old messages
for caption in self.message_captions:
# Remove from UI (McRogueFace doesn't have remove, so we hide it)
caption.text = ""
# Display current messages
for i, (text, color) in enumerate(self.messages):
if i < len(self.message_captions):
self.message_captions[i].text = text
self.message_captions[i].fill_color = mcrfpy.Color(*color)
def setup_game(self):
"""Initialize the game world"""
self.game_map = GameMap(80, 45)
grid = self.game_map.create_grid(self.tileset)
self.ui.append(grid)
# Create player
self.player = create_player(0, 0)
# Generate the dungeon
self.game_map.generate_dungeon(
max_rooms=30,
room_min_size=6,
room_max_size=10,
player=self.player,
max_enemies_per_room=2
)
# Add player to map
self.game_map.add_entity(self.player)
# Store reference to all entities
self.entities = [e for e in self.game_map.entities if e != self.player]
# Initial FOV calculation
self.player.update_fov()
# Welcome message
self.add_message("Welcome to the dungeon!", (100, 100, 255))
def handle_player_turn(self, action):
"""Process the player's action"""
if not self.player.is_alive:
return
if isinstance(action, MovementAction):
dest_x = self.player.x + action.dx
dest_y = self.player.y + action.dy
# Check what's at the destination
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
if target:
# Attack!
attack = MeleeAction(self.player, target)
result = attack.perform()
if result:
text, color = result
self.add_message(text, color)
# Check if target died
if not target.is_alive:
death_msg = f"The {target.name.replace('remains of ', '')} is dead!"
self.add_message(death_msg, COLOR_ENEMY_DIE)
elif not self.game_map.is_blocked(dest_x, dest_y):
# Move the player
self.player.move(action.dx, action.dy)
elif isinstance(action, WaitAction):
pass # Do nothing
# Enemy turns
self.handle_enemy_turns()
def handle_enemy_turns(self):
"""Let all enemies take their turn"""
for entity in self.entities:
if entity.is_alive:
# Simple AI: if player is adjacent, attack. Otherwise, do nothing.
dx = entity.x - self.player.x
dy = entity.y - self.player.y
distance = abs(dx) + abs(dy)
if distance == 1: # Adjacent to player
attack = MeleeAction(entity, self.player)
result = attack.perform()
if result:
text, color = result
self.add_message(text, color)
# Check if player died
if not self.player.is_alive:
self.add_message("You have died!", COLOR_PLAYER_DIE)
def setup_input(self):
"""Setup keyboard input handling"""
def handle_keys(key, state):
if state != "start":
return
action = None
# Movement keys
movement = {
"Up": (0, -1), "Down": (0, 1),
"Left": (-1, 0), "Right": (1, 0),
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
}
if key in movement:
dx, dy = movement[key]
if dx == 0 and dy == 0:
action = WaitAction()
else:
action = MovementAction(dx, dy)
elif key == "Period":
action = WaitAction()
elif key == "Escape":
mcrfpy.setScene(None)
return
# Process the action
if action:
self.handle_player_turn(action)
mcrfpy.keypressScene(handle_keys)
def setup_ui(self):
"""Setup UI elements"""
title = mcrfpy.Caption("Combat System", 512, 30)
title.font_size = 24
title.fill_color = mcrfpy.Color(255, 255, 100)
self.ui.append(title)
instructions = mcrfpy.Caption("Attack enemies by bumping into them!", 512, 60)
instructions.font_size = 16
instructions.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(instructions)
# Player stats
self.hp_text = mcrfpy.Caption(f"HP: {self.player.hp}/{self.player.max_hp}", 50, 100)
self.hp_text.font_size = 18
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
self.ui.append(self.hp_text)
# Message log
self.message_captions = []
for i in range(self.max_messages):
caption = mcrfpy.Caption("", 50, 620 + i * 20)
caption.font_size = 14
caption.fill_color = mcrfpy.Color(200, 200, 200)
self.ui.append(caption)
self.message_captions.append(caption)
# Timer to update HP display
def update_stats(dt):
self.hp_text.text = f"HP: {self.player.hp}/{self.player.max_hp}"
if self.player.hp <= 0:
self.hp_text.fill_color = mcrfpy.Color(127, 0, 0)
elif self.player.hp < self.player.max_hp // 3:
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
else:
self.hp_text.fill_color = mcrfpy.Color(0, 255, 0)
mcrfpy.setTimer("update_stats", update_stats, 100)
# Create and run the game
engine = Engine()
print("Part 6: Combat System!")
print("Attack enemies to defeat them, but watch your HP!")

View File

@ -1,80 +0,0 @@
"""
McRogueFace Tutorial - Part 0: Introduction to Scene, Texture, and Grid
This tutorial introduces the basic building blocks:
- Scene: A container for UI elements and game state
- Texture: Loading image assets for use in the game
- Grid: A tilemap component for rendering tile-based worlds
"""
import mcrfpy
import random
# Create and activate a new scene
mcrfpy.createScene("tutorial")
mcrfpy.setScene("tutorial")
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
# Create a grid of tiles
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
grid_width, grid_height = 25, 20 # width, height in number of tiles
# calculating the size in pixels to fit the entire grid on-screen
zoom = 2.0
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
grid = mcrfpy.Grid(
pos=grid_position,
grid_size=(grid_width, grid_height),
texture=texture,
size=grid_size, # height and width on screen
)
grid.zoom = zoom
grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile
# Define tile types
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
WALL_TILES = [3, 7, 11]
# Fill the grid with a simple pattern
for y in range(grid_height):
for x in range(grid_width):
# Create walls around the edges
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
tile_index = random.choice(WALL_TILES)
else:
# Fill interior with floor tiles
tile_index = random.choice(FLOOR_TILES)
# Set the tile at this position
point = grid.at(x, y)
if point:
point.tilesprite = tile_index
# Add the grid to the scene
mcrfpy.sceneUI("tutorial").append(grid)
# Add a title caption
title = mcrfpy.Caption((320, 10),
text="McRogueFace Tutorial - Part 0",
)
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
mcrfpy.sceneUI("tutorial").append(title)
# Add instructions
instructions = mcrfpy.Caption((280, 750),
text="Scene + Texture + Grid = Tilemap!",
)
instructions.font_size=18
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
mcrfpy.sceneUI("tutorial").append(instructions)
print("Tutorial Part 0 loaded!")
print(f"Created a {grid.grid_size[0]}x{grid.grid_size[1]} grid")
print(f"Grid positioned at ({grid.x}, {grid.y})")

View File

@ -1,116 +0,0 @@
"""
McRogueFace Tutorial - Part 1: Entities and Keyboard Input
This tutorial builds on Part 0 by adding:
- Entity: A game object that can be placed in a grid
- Keyboard handling: Responding to key presses to move the entity
"""
import mcrfpy
import random
# Create and activate a new scene
mcrfpy.createScene("tutorial")
mcrfpy.setScene("tutorial")
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
# Load the hero sprite texture (32x32 sprite sheet)
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
# Create a grid of tiles
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
grid_width, grid_height = 25, 20 # width, height in number of tiles
# calculating the size in pixels to fit the entire grid on-screen
zoom = 2.0
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
grid = mcrfpy.Grid(
pos=grid_position,
grid_size=(grid_width, grid_height),
texture=texture,
size=grid_size, # height and width on screen
)
grid.zoom = zoom
grid.center = (grid_width/2.0)*16, (grid_height/2.0)*16 # center on the middle of the central tile
# Define tile types
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
WALL_TILES = [3, 7, 11]
# Fill the grid with a simple pattern
for y in range(grid_height):
for x in range(grid_width):
# Create walls around the edges
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
tile_index = random.choice(WALL_TILES)
else:
# Fill interior with floor tiles
tile_index = random.choice(FLOOR_TILES)
# Set the tile at this position
point = grid.at(x, y)
if point:
point.tilesprite = tile_index
# Add the grid to the scene
mcrfpy.sceneUI("tutorial").append(grid)
# Create a player entity at position (4, 4)
player = mcrfpy.Entity(
(4, 4), # Entity positions are tile coordinates
texture=hero_texture,
sprite_index=0 # Use the first sprite in the texture
)
# Add the player entity to the grid
grid.entities.append(player)
# Define keyboard handler
def handle_keys(key, state):
"""Handle keyboard input to move the player"""
if state == "start": # Only respond to key press, not release
# Get current player position in grid coordinates
px, py = player.x, player.y
# Calculate new position based on key press
if key == "W" or key == "Up":
py -= 1
elif key == "S" or key == "Down":
py += 1
elif key == "A" or key == "Left":
px -= 1
elif key == "D" or key == "Right":
px += 1
# Update player position (no collision checking yet)
player.x = px
player.y = py
# Register the keyboard handler
mcrfpy.keypressScene(handle_keys)
# Add a title caption
title = mcrfpy.Caption((320, 10),
text="McRogueFace Tutorial - Part 1",
)
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
mcrfpy.sceneUI("tutorial").append(title)
# Add instructions
instructions = mcrfpy.Caption((200, 750),
text="Use WASD or Arrow Keys to move the hero!",
)
instructions.font_size=18
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
mcrfpy.sceneUI("tutorial").append(instructions)
print("Tutorial Part 1 loaded!")
print(f"Player entity created at grid position (4, 4)")
print("Use WASD or Arrow keys to move!")

View File

@ -1,117 +0,0 @@
"""
McRogueFace Tutorial - Part 1: Entities and Keyboard Input
This tutorial builds on Part 0 by adding:
- Entity: A game object that can be placed in a grid
- Keyboard handling: Responding to key presses to move the entity
"""
import mcrfpy
import random
# Create and activate a new scene
mcrfpy.createScene("tutorial")
mcrfpy.setScene("tutorial")
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
# Load the hero sprite texture (32x32 sprite sheet)
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
# Create a grid of tiles
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
grid_width, grid_height = 25, 20 # width, height in number of tiles
# calculating the size in pixels to fit the entire grid on-screen
zoom = 2.0
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
grid = mcrfpy.Grid(
pos=grid_position,
grid_size=(grid_width, grid_height),
texture=texture,
size=grid_size, # height and width on screen
)
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
# Define tile types
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
WALL_TILES = [3, 7, 11]
# Fill the grid with a simple pattern
for y in range(grid_height):
for x in range(grid_width):
# Create walls around the edges
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
tile_index = random.choice(WALL_TILES)
else:
# Fill interior with floor tiles
tile_index = random.choice(FLOOR_TILES)
# Set the tile at this position
point = grid.at(x, y)
if point:
point.tilesprite = tile_index
# Add the grid to the scene
mcrfpy.sceneUI("tutorial").append(grid)
# Create a player entity at position (4, 4)
player = mcrfpy.Entity(
(4, 4), # Entity positions are tile coordinates
texture=hero_texture,
sprite_index=0 # Use the first sprite in the texture
)
# Add the player entity to the grid
grid.entities.append(player)
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
# Define keyboard handler
def handle_keys(key, state):
"""Handle keyboard input to move the player"""
if state == "start": # Only respond to key press, not release
# Get current player position in grid coordinates
px, py = player.x, player.y
# Calculate new position based on key press
if key == "W" or key == "Up":
py -= 1
elif key == "S" or key == "Down":
py += 1
elif key == "A" or key == "Left":
px -= 1
elif key == "D" or key == "Right":
px += 1
# Update player position (no collision checking yet)
player.x = px
player.y = py
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
# Register the keyboard handler
mcrfpy.keypressScene(handle_keys)
# Add a title caption
title = mcrfpy.Caption((320, 10),
text="McRogueFace Tutorial - Part 1",
)
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
mcrfpy.sceneUI("tutorial").append(title)
# Add instructions
instructions = mcrfpy.Caption((200, 750),
text="Use WASD or Arrow Keys to move the hero!",
)
instructions.font_size=18
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
mcrfpy.sceneUI("tutorial").append(instructions)
print("Tutorial Part 1 loaded!")
print(f"Player entity created at grid position (4, 4)")
print("Use WASD or Arrow keys to move!")

View File

@ -1,149 +0,0 @@
"""
McRogueFace Tutorial - Part 2: Animated Movement
This tutorial builds on Part 1 by adding:
- Animation system for smooth movement
- Movement that takes 0.5 seconds per tile
- Input blocking during movement animation
"""
import mcrfpy
import random
# Create and activate a new scene
mcrfpy.createScene("tutorial")
mcrfpy.setScene("tutorial")
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
# Load the hero sprite texture (32x32 sprite sheet)
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
# Create a grid of tiles
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
grid_width, grid_height = 25, 20 # width, height in number of tiles
# calculating the size in pixels to fit the entire grid on-screen
zoom = 2.0
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
grid = mcrfpy.Grid(
pos=grid_position,
grid_size=(grid_width, grid_height),
texture=texture,
size=grid_size, # height and width on screen
)
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
# Define tile types
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
WALL_TILES = [3, 7, 11]
# Fill the grid with a simple pattern
for y in range(grid_height):
for x in range(grid_width):
# Create walls around the edges
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
tile_index = random.choice(WALL_TILES)
else:
# Fill interior with floor tiles
tile_index = random.choice(FLOOR_TILES)
# Set the tile at this position
point = grid.at(x, y)
if point:
point.tilesprite = tile_index
# Add the grid to the scene
mcrfpy.sceneUI("tutorial").append(grid)
# Create a player entity at position (4, 4)
player = mcrfpy.Entity(
(4, 4), # Entity positions are tile coordinates
texture=hero_texture,
sprite_index=0 # Use the first sprite in the texture
)
# Add the player entity to the grid
grid.entities.append(player)
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
# Movement state tracking
is_moving = False
move_animations = [] # Track active animations
# Animation completion callback
def movement_complete(runtime):
"""Called when movement animation completes"""
global is_moving
is_moving = False
# Ensure grid is centered on final position
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
motion_speed = 0.30 # seconds per tile
# Define keyboard handler
def handle_keys(key, state):
"""Handle keyboard input to move the player"""
global is_moving, move_animations
if state == "start" and not is_moving: # Only respond to key press when not moving
# Get current player position in grid coordinates
px, py = player.x, player.y
new_x, new_y = px, py
# Calculate new position based on key press
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
# If position changed, start movement animation
if new_x != px or new_y != py:
is_moving = True
# Create animations for player position
anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad")
anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
anim_x.start(player)
anim_y.start(player)
# Animate grid center to follow player
center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
center_x.start(grid)
center_y.start(grid)
# Set a timer to mark movement as complete
mcrfpy.setTimer("move_complete", movement_complete, 500)
# Register the keyboard handler
mcrfpy.keypressScene(handle_keys)
# Add a title caption
title = mcrfpy.Caption((320, 10),
text="McRogueFace Tutorial - Part 2",
)
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
mcrfpy.sceneUI("tutorial").append(title)
# Add instructions
instructions = mcrfpy.Caption((150, 750),
text="Smooth movement! Each step takes 0.5 seconds.",
)
instructions.font_size=18
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
mcrfpy.sceneUI("tutorial").append(instructions)
print("Tutorial Part 2 loaded!")
print(f"Player entity created at grid position (4, 4)")
print("Movement is now animated over 0.5 seconds per tile!")
print("Use WASD or Arrow keys to move!")

View File

@ -1,241 +0,0 @@
"""
McRogueFace Tutorial - Part 2: Enhanced with Single Move Queue
This tutorial builds on Part 2 by adding:
- Single queued move system for responsive input
- Debug display showing position and queue status
- Smooth continuous movement when keys are held
- Animation callbacks to prevent race conditions
"""
import mcrfpy
import random
# Create and activate a new scene
mcrfpy.createScene("tutorial")
mcrfpy.setScene("tutorial")
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
# Load the hero sprite texture (32x32 sprite sheet)
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
# Create a grid of tiles
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
grid_width, grid_height = 25, 20 # width, height in number of tiles
# calculating the size in pixels to fit the entire grid on-screen
zoom = 2.0
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
grid = mcrfpy.Grid(
pos=grid_position,
grid_size=(grid_width, grid_height),
texture=texture,
size=grid_size, # height and width on screen
)
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
# Define tile types
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
WALL_TILES = [3, 7, 11]
# Fill the grid with a simple pattern
for y in range(grid_height):
for x in range(grid_width):
# Create walls around the edges
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
tile_index = random.choice(WALL_TILES)
else:
# Fill interior with floor tiles
tile_index = random.choice(FLOOR_TILES)
# Set the tile at this position
point = grid.at(x, y)
if point:
point.tilesprite = tile_index
# Add the grid to the scene
mcrfpy.sceneUI("tutorial").append(grid)
# Create a player entity at position (4, 4)
player = mcrfpy.Entity(
(4, 4), # Entity positions are tile coordinates
texture=hero_texture,
sprite_index=0 # Use the first sprite in the texture
)
# Add the player entity to the grid
grid.entities.append(player)
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
# Movement state tracking
is_moving = False
move_queue = [] # List to store queued moves (max 1 item)
#last_position = (4, 4) # Track last position
current_destination = None # Track where we're currently moving to
current_move = None # Track current move direction
# Store animation references
player_anim_x = None
player_anim_y = None
grid_anim_x = None
grid_anim_y = None
# Debug display caption
debug_caption = mcrfpy.Caption((10, 40),
text="Last: (4, 4) | Queue: 0 | Dest: None",
)
debug_caption.font_size = 16
debug_caption.fill_color = mcrfpy.Color(255, 255, 0, 255)
mcrfpy.sceneUI("tutorial").append(debug_caption)
# Additional debug caption for movement state
move_debug_caption = mcrfpy.Caption((10, 60),
text="Moving: False | Current: None | Queued: None",
)
move_debug_caption.font_size = 16
move_debug_caption.fill_color = mcrfpy.Color(255, 200, 0, 255)
mcrfpy.sceneUI("tutorial").append(move_debug_caption)
def key_to_direction(key):
"""Convert key to direction string"""
if key == "W" or key == "Up":
return "Up"
elif key == "S" or key == "Down":
return "Down"
elif key == "A" or key == "Left":
return "Left"
elif key == "D" or key == "Right":
return "Right"
return None
def update_debug_display():
"""Update the debug caption with current state"""
queue_count = len(move_queue)
dest_text = f"({current_destination[0]}, {current_destination[1]})" if current_destination else "None"
debug_caption.text = f"Last: ({player.x}, {player.y}) | Queue: {queue_count} | Dest: {dest_text}"
# Update movement state debug
current_dir = key_to_direction(current_move) if current_move else "None"
queued_dir = key_to_direction(move_queue[0]) if move_queue else "None"
move_debug_caption.text = f"Moving: {is_moving} | Current: {current_dir} | Queued: {queued_dir}"
# Animation completion callback
def movement_complete(anim, target):
"""Called when movement animation completes"""
global is_moving, move_queue, current_destination, current_move
global player_anim_x, player_anim_y
print(f"In callback for animation: {anim=} {target=}")
# Clear movement state
is_moving = False
current_move = None
current_destination = None
# Clear animation references
player_anim_x = None
player_anim_y = None
# Update last position to where we actually are now
#last_position = (int(player.x), int(player.y))
# Ensure grid is centered on final position
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
# Check if there's a queued move
if move_queue:
# Pop the next move from the queue
next_move = move_queue.pop(0)
print(f"Processing queued move: {next_move}")
# Process it like a fresh input
process_move(next_move)
update_debug_display()
motion_speed = 0.30 # seconds per tile
def process_move(key):
"""Process a move based on the key"""
global is_moving, current_move, current_destination, move_queue
global player_anim_x, player_anim_y, grid_anim_x, grid_anim_y
# If already moving, just update the queue
if is_moving:
print(f"process_move processing {key=} as a queued move (is_moving = True)")
# Clear queue and add new move (only keep 1 queued move)
move_queue.clear()
move_queue.append(key)
update_debug_display()
return
print(f"process_move processing {key=} as a new, immediate animation (is_moving = False)")
# Calculate new position from current position
px, py = int(player.x), int(player.y)
new_x, new_y = px, py
# Calculate new position based on key press (only one tile movement)
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
# Start the move if position changed
if new_x != px or new_y != py:
is_moving = True
current_move = key
current_destination = (new_x, new_y)
# only animate a single axis, same callback from either
if new_x != px:
player_anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad", callback=movement_complete)
player_anim_x.start(player)
elif new_y != py:
player_anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad", callback=movement_complete)
player_anim_y.start(player)
# Animate grid center to follow player
grid_anim_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
grid_anim_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
grid_anim_x.start(grid)
grid_anim_y.start(grid)
update_debug_display()
# Define keyboard handler
def handle_keys(key, state):
"""Handle keyboard input to move the player"""
if state == "start":
# Only process movement keys
if key in ["W", "Up", "S", "Down", "A", "Left", "D", "Right"]:
print(f"handle_keys producing actual input: {key=}")
process_move(key)
# Register the keyboard handler
mcrfpy.keypressScene(handle_keys)
# Add a title caption
title = mcrfpy.Caption((320, 10),
text="McRogueFace Tutorial - Part 2 Enhanced",
)
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
mcrfpy.sceneUI("tutorial").append(title)
# Add instructions
instructions = mcrfpy.Caption((150, 750),
text="One-move queue system with animation callbacks!",
)
instructions.font_size=18
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
mcrfpy.sceneUI("tutorial").append(instructions)
print("Tutorial Part 2 Enhanced loaded!")
print(f"Player entity created at grid position (4, 4)")
print("Movement now uses animation callbacks to prevent race conditions!")
print("Use WASD or Arrow keys to move!")

View File

@ -1,149 +0,0 @@
"""
McRogueFace Tutorial - Part 2: Animated Movement
This tutorial builds on Part 1 by adding:
- Animation system for smooth movement
- Movement that takes 0.5 seconds per tile
- Input blocking during movement animation
"""
import mcrfpy
import random
# Create and activate a new scene
mcrfpy.createScene("tutorial")
mcrfpy.setScene("tutorial")
# Load the texture (4x3 tiles, 64x48 pixels total, 16x16 per tile)
texture = mcrfpy.Texture("assets/tutorial2.png", 16, 16)
# Load the hero sprite texture (32x32 sprite sheet)
hero_texture = mcrfpy.Texture("assets/custom_player.png", 16, 16)
# Create a grid of tiles
# Each tile is 16x16 pixels, so with 3x zoom: 16*3 = 48 pixels per tile
grid_width, grid_height = 25, 20 # width, height in number of tiles
# calculating the size in pixels to fit the entire grid on-screen
zoom = 2.0
grid_size = grid_width * zoom * 16, grid_height * zoom * 16
# calculating the position to center the grid on the screen - assuming default 1024x768 resolution
grid_position = (1024 - grid_size[0]) / 2, (768 - grid_size[1]) / 2
grid = mcrfpy.Grid(
pos=grid_position,
grid_size=(grid_width, grid_height),
texture=texture,
size=grid_size, # height and width on screen
)
grid.zoom = 3.0 # we're not using the zoom variable! It's going to be really big!
# Define tile types
FLOOR_TILES = [0, 1, 2, 4, 5, 6, 8, 9, 10]
WALL_TILES = [3, 7, 11]
# Fill the grid with a simple pattern
for y in range(grid_height):
for x in range(grid_width):
# Create walls around the edges
if x == 0 or x == grid_width-1 or y == 0 or y == grid_height-1:
tile_index = random.choice(WALL_TILES)
else:
# Fill interior with floor tiles
tile_index = random.choice(FLOOR_TILES)
# Set the tile at this position
point = grid.at(x, y)
if point:
point.tilesprite = tile_index
# Add the grid to the scene
mcrfpy.sceneUI("tutorial").append(grid)
# Create a player entity at position (4, 4)
player = mcrfpy.Entity(
(4, 4), # Entity positions are tile coordinates
texture=hero_texture,
sprite_index=0 # Use the first sprite in the texture
)
# Add the player entity to the grid
grid.entities.append(player)
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16 # grid center is in texture/pixel coordinates
# Movement state tracking
is_moving = False
move_animations = [] # Track active animations
# Animation completion callback
def movement_complete(runtime):
"""Called when movement animation completes"""
global is_moving
is_moving = False
# Ensure grid is centered on final position
grid.center = (player.x + 0.5) * 16, (player.y + 0.5) * 16
motion_speed = 0.30 # seconds per tile
# Define keyboard handler
def handle_keys(key, state):
"""Handle keyboard input to move the player"""
global is_moving, move_animations
if state == "start" and not is_moving: # Only respond to key press when not moving
# Get current player position in grid coordinates
px, py = player.x, player.y
new_x, new_y = px, py
# Calculate new position based on key press
if key == "W" or key == "Up":
new_y -= 1
elif key == "S" or key == "Down":
new_y += 1
elif key == "A" or key == "Left":
new_x -= 1
elif key == "D" or key == "Right":
new_x += 1
# If position changed, start movement animation
if new_x != px or new_y != py:
is_moving = True
# Create animations for player position
anim_x = mcrfpy.Animation("x", float(new_x), motion_speed, "easeInOutQuad")
anim_y = mcrfpy.Animation("y", float(new_y), motion_speed, "easeInOutQuad")
anim_x.start(player)
anim_y.start(player)
# Animate grid center to follow player
center_x = mcrfpy.Animation("center_x", (new_x + 0.5) * 16, motion_speed, "linear")
center_y = mcrfpy.Animation("center_y", (new_y + 0.5) * 16, motion_speed, "linear")
center_x.start(grid)
center_y.start(grid)
# Set a timer to mark movement as complete
mcrfpy.setTimer("move_complete", movement_complete, 500)
# Register the keyboard handler
mcrfpy.keypressScene(handle_keys)
# Add a title caption
title = mcrfpy.Caption((320, 10),
text="McRogueFace Tutorial - Part 2",
)
title.fill_color = mcrfpy.Color(255, 255, 255, 255)
mcrfpy.sceneUI("tutorial").append(title)
# Add instructions
instructions = mcrfpy.Caption((150, 750),
"Smooth movement! Each step takes 0.5 seconds.",
)
instructions.font_size=18
instructions.fill_color = mcrfpy.Color(200, 200, 200, 255)
mcrfpy.sceneUI("tutorial").append(instructions)
print("Tutorial Part 2 loaded!")
print(f"Player entity created at grid position (4, 4)")
print("Movement is now animated over 0.5 seconds per tile!")
print("Use WASD or Arrow keys to move!")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

50
scripts/Grid.py Normal file
View File

@ -0,0 +1,50 @@
class GridPoint:
def __init__(self, color, walkable, tilesprite, transparent, visible, discovered, color_overlay, tile_overlay, uisprite):
self.color = color
self.walkable = walkable
self.tilesprite = tilesprite
self.transparent = transparent
self.visible = visible
self.discovered = discovered
self.color_overlay = color_overlay
self.tile_overlay = tile_overlay
self.uisprite = uisprite
def __repr__(self):
return f"<GridPoint {self.color}, {self.tilesprite}/{self.uisprite} {'W' if self.walkable else '-'}{'T' if self.transparent else '-'}{'V' if self.visible else '-'}{'D' if self.discovered else '-'} {self.color_overlay}/{self.tile_overlay}>"
class Grid:
def __init__(self, title, gx, gy, gs, x, y, w, h, visible=False):
self.title = title
self.grid_x = gx
self.grid_y = gy
self.grid_size = gs
self.x = x
self.y = y
self.w = w
self.h = h
self.visible = visible
self.points = []
self.entities = []
def at(self, x, y):
if not (x > 0 and y > 0 and x < self.grid_x and y < self.grid_y): return None
return self.points[y * self.grid_y + x]
def __repr__(self):
return f"<Grid {self.grid_x}x{self.grid_y}, grid_size={self.grid_size}, (({self.x},{self.y}), ({self.w}, {self.h})), visible={self.visible}>"
# CGrid(Grid* _g, int _ti, int _si, int _x, int _y, bool _v)
class Entity:
def __init__(self, parent, tex_index, sprite_index, x, y, visible=True):
self.parent = parent
self.tex_index = tex_index
self.sprite_index = sprite_index
self.x = x
self.y = y
self.visible = visible
def __repr__(self):
return f"<Entity on grid {repr(self.parent)}@({self.x},{self.y}), TI={self.tex_index}, SI={self.sprite_index}, visible={self.visible}>"

79
scripts/MusicScene.py Normal file
View File

@ -0,0 +1,79 @@
import mcrfpy
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED, GREEN, BLUE = (255, 0, 0), (0, 255, 0), (0, 0, 255)
DARKRED, DARKGREEN, DARKBLUE = (192, 0, 0), (0, 192, 0), (0, 0, 192)
class MusicScene:
def __init__(self, ui_name = "demobox1", grid_name = "demogrid"):
# Texture & Sound Loading
print("Load textures")
mcrfpy.createTexture("./assets/test_portraits.png", 32, 8, 8) #0 - portraits
mcrfpy.createTexture("./assets/alives_other.png", 16, 64, 64) #1 - TinyWorld NPCs
mcrfpy.createTexture("./assets/alives_other.png", 32, 32, 32) #2 - TinyWorld NPCs - 2x2 sprite
# {"createSoundBuffer", McRFPy_API::_createSoundBuffer, METH_VARARGS, "(filename)"},
#{"loadMusic", McRFPy_API::_loadMusic, METH_VARARGS, "(filename)"},
#{"setMusicVolume", McRFPy_API::_setMusicVolume, METH_VARARGS, "(int)"},
#{"setSoundVolume", McRFPy_API::_setSoundVolume, METH_VARARGS, "(int)"},
#{"playSound", McRFPy_API::_playSound, METH_VARARGS, "(int)"},
#{"getMusicVolume", McRFPy_API::_getMusicVolume, METH_VARARGS, ""},
#{"getSoundVolume", McRFPy_API::_getSoundVolume, METH_VARARGS, ""},
mcrfpy.loadMusic("./assets/ultima.ogg")
mcrfpy.createSoundBuffer("./assets/boom.wav")
self.ui_name = ui_name
self.grid_name = grid_name
print("Create UI")
# Create dialog UI
mcrfpy.createMenu(ui_name, 20, 540, 500, 200)
mcrfpy.createCaption(ui_name, f"Music Volume: {mcrfpy.getMusicVolume()}", 24, RED)
mcrfpy.createCaption(ui_name, f"SFX Volume: {mcrfpy.getSoundVolume()}", 24, RED)
#mcrfpy.createButton(ui_name, 250, 20, 100, 50, DARKBLUE, (0, 0, 0), "clicky", "testaction")
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKRED, (0, 0, 0), "Music+", "mvol+")
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKGREEN, (0, 0, 0), "Music-", "mvol-")
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKBLUE, GREEN, "SFX+", "svol+")
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKBLUE, RED, "SFX-", "svol-")
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKRED, (0, 0, 0), "REPL", "startrepl")
mcrfpy.createSprite(ui_name, 1, 0, 20, 40, 3.0)
print("Create UI 2")
entitymenu = "entitytestmenu"
mcrfpy.createMenu(entitymenu, 840, 20, 20, 500)
mcrfpy.createButton(entitymenu, 0, 10, 150, 40, DARKBLUE, BLACK, "PlayM", "playm")
mcrfpy.createButton(entitymenu, 0, 60, 150, 40, DARKBLUE, BLACK, "StopM", "stopm")
mcrfpy.createButton(entitymenu, 0, 110, 150, 40, DARKBLUE, BLACK, "SFX", "boom")
print("Make UIs visible")
self.menus = mcrfpy.listMenus()
self.menus[0].visible = True
self.menus[1].w = 170
self.menus[1].visible = True
mcrfpy.modMenu(self.menus[0])
mcrfpy.modMenu(self.menus[1])
self.mvol = mcrfpy.getMusicVolume()
self.svol = mcrfpy.getSoundVolume()
mcrfpy.registerPyAction("mvol+", lambda: self.setmvol(self.mvol+10))
mcrfpy.registerPyAction("mvol-", lambda: self.setmvol(self.mvol-10))
mcrfpy.registerPyAction("svol+", lambda: self.setsvol(self.svol+10))
mcrfpy.registerPyAction("svol-", lambda: self.setsvol(self.svol-10))
mcrfpy.registerPyAction("playm", lambda: None)
mcrfpy.registerPyAction("stopm", lambda: None)
mcrfpy.registerPyAction("boom", lambda: mcrfpy.playSound(0))
def setmvol(self, v):
mcrfpy.setMusicVolume(int(v))
self.menus[0].captions[0].text = f"Music Volume: {mcrfpy.getMusicVolume():.1f}"
mcrfpy.modMenu(self.menus[0])
self.mvol = mcrfpy.getMusicVolume()
def setsvol(self, v):
mcrfpy.setSoundVolume(int(v))
self.menus[0].captions[1].text = f"Sound Volume: {mcrfpy.getSoundVolume():.1f}"
mcrfpy.modMenu(self.menus[0])
self.svol = mcrfpy.getSoundVolume()
scene = None
def start():
global scene
scene = MusicScene()

575
scripts/TestScene.py Normal file
View File

@ -0,0 +1,575 @@
import UIMenu
import Grid
import mcrfpy
from random import randint, choice
from pprint import pprint
#print("TestScene imported")
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED, GREEN, BLUE = (255, 0, 0), (0, 255, 0), (0, 0, 255)
DARKRED, DARKGREEN, DARKBLUE = (192, 0, 0), (0, 192, 0), (0, 0, 192)
animations_in_progress = 0
# don't load grid over and over, use the global scene
scene = None
class TestEntity:
def __init__(self, grid, label, tex_index, basesprite, x, y, texture_width=64, walk_frames=5, attack_frames=6, do_fov=False):
self.grid = grid
self.basesprite = basesprite
self.texture_width = texture_width
self.walk_frames = walk_frames
self.attack_frames = attack_frames
self.x = x
self.y = y
self.facing_direction = 0
self.do_fov = do_fov
self.label = label
self.inventory = []
#print(f"Calling C++ with: {repr((self.grid, label, tex_index, self.basesprite, x, y, self))}")
grids = mcrfpy.listGrids()
for g in grids:
if g.title == self.grid:
self.entity_index = len(g.entities)
mcrfpy.createEntity(self.grid, label, tex_index, self.basesprite, x, y, self)
def ai_act(self):
return # no AI motion
#if self.label == "player": return
self.move(randint(-1, 1), randint(-1, 1))
scene.actors += 1
def player_act(self):
#print("I'M INTERVENING")
mcrfpy.unlockPlayerInput()
scene.updatehints()
def die(self):
#self.x = -2
#self.y = -2
self.move(-1000,-1000)
self.animate(0,animove=(-1000,-1000))
self.x = -1000
self.y = -1000
self.label = "dead"
def interact(self, initiator, callback):
print(f"Interacted with {self.label}. ", end='')
if self.label == 'item':
print("'taking' item.")
callback()
self.die()
else:
print("blocking movement.")
def move(self, dx, dy, force=False):
# select animation direction
# prefer left or right for diagonals.
#grids = mcrfpy.listGrids()
for g in scene.grids:
if g.title == self.grid:
if not force and g.at(self.x + dx, self.y + dy) is None or not g.at(self.x + dx, self.y + dy).walkable:
#print("Blocked at target location.")
return
if not force: # entities can be stepped on when force=True (like collecting items!)
for entity in scene.tes:
if (entity.x, entity.y) == (self.x + dx, self.y + dy):
print(f"Blocked by entity {entity.label} at ({entity.x}, {entity.y})")
return entity.interact(self, lambda: self.move(dx, dy, force=True))
if self.label == "player":
mcrfpy.lockPlayerInput()
scene.updatehints()
if (dx == 0 and dy == 0):
direction = self.facing_direction # TODO, jump straight to computer turn
elif (dx):
direction = 2 if dx == +1 else 3
else:
direction = 0 if dy == +1 else 1
self.animate(direction, move=True, animove=(self.x + dx, self.y+dy))
self.facing_direction = direction
if (self.do_fov): mcrfpy.refreshFov()
def animate(self, direction, attacking=False, move=False, animove=None):
start_sprite = self.basesprite + (self.texture_width * (direction + (4 if attacking else 0)))
animation_frames = [start_sprite + i for i in range((self.attack_frames if attacking else self.walk_frames))]
mcrfpy.createAnimation(
0.15, # duration, seconds
self.grid, # parent: a UIMenu or Grid key
"entity", # target type: 'menu', 'grid', 'caption', 'button', 'sprite', or 'entity'
self.entity_index, # target id: integer index for menu or grid objs; None for grid/menu
"sprite", # field: 'position', 'size', 'bgcolor', 'textcolor', or 'sprite'
self.animation_done, #callback: callable once animation is complete
False, #loop: repeat indefinitely
animation_frames # values: iterable of frames for 'sprite', lerp target for others
)
#global animations_in_progress
#animations_in_progress += 1
if move:
pos = [self.x, self.y]
if (direction == 0): pos[1] += 1
elif (direction == 1): pos[1] -= 1
elif (direction == 2): pos[0] += 1
elif (direction == 3): pos[0] -= 1
if not animove:
self.x, self.y = pos
animove = pos
else:
pos = animove
self.x, self.y = animove
#scene.move_entity(self.grid, self.entity_index, pos)
#for g in mcrfpy.listGrids():
for g in scene.grids:
if g.title == self.grid:
g.entities[self.entity_index].x = pos[0]
g.entities[self.entity_index].y = pos[1]
mcrfpy.modGrid(g, True)
if animove:
mcrfpy.createAnimation(
0.25, # duration, seconds
self.grid, # parent: a UIMenu or Grid key
"entity", # target type: 'menu', 'grid', 'caption', 'button', 'sprite', or 'entity'
self.entity_index, # target id: integer index for menu or grid objs; None for grid/menu
"position", # field: 'position', 'size', 'bgcolor', 'textcolor', or 'sprite'
self.animation_done, #callback: callable once animation is complete
False, #loop: repeat indefinitely
animove # values: iterable of frames for 'sprite', lerp target for others
)
#animations_in_progress += 1
def animation_done(self):
#global animations_in_progress
#animations_in_progress -= 1
scene.actors -= 1
#print(f"{self} done animating - {scene.actors} remaining")
if scene.actors <= 0:
scene.actors = 0
mcrfpy.unlockPlayerInput()
scene.updatehints()
class TestItemEntity(TestEntity):
def __init__(self, grid, label, tex_index, basesprite, x, y, texture_width=64, walk_frames=5, attack_frames=6, do_fov=False, item="Nothing"):
super().__init__(grid, label, tex_index, basesprite, x, y, texture_width, walk_frames, attack_frames, do_fov)
self.item = item
def interact(self, initiator, callback):
if self.label == 'dead': return super().interact(initiator, callback)
print(f"Interacted with {self.label}, an item. Adding {self.item} to {initiator.label}'s inventory")
initiator.inventory.append(self.item)
callback()
scene.itemguis()
self.die()
class TestDoorEntity(TestEntity):
def __init__(self, grid, label, tex_index, basesprite, x, y, texture_width=64, walk_frames=5, attack_frames=6, do_fov=False, key="Nothing"):
super().__init__(grid, label, tex_index, basesprite, x, y, texture_width, walk_frames, attack_frames, do_fov)
self.key = key
def interact(self, initiator, callback):
if self.label == 'dead': return super().interact(initiator, callback)
print(f"Interacted with {self.label}, a Door. ", end='')
if self.key in initiator.inventory:
initiator.inventory.remove(self.key)
print("Taking key & passing.")
callback()
scene.itemguis()
self.die()
else:
print("The door is locked.")
class TestScene:
def __init__(self, ui_name = "demobox1", grid_name = "demogrid"):
# Texture & Sound Loading
self.actors = 0
#print("Load textures")
mcrfpy.createTexture("./assets/test_portraits.png", 32, 8, 8) #0 - portraits
mcrfpy.createTexture("./assets/alives_other.png", 16, 64, 64) #1 - TinyWorld NPCs
mcrfpy.createTexture("./assets/alives_other.png", 32, 32, 32) #2 - TinyWorld NPCs - 2x2 sprite
mcrfpy.createTexture("./assets/custom_player.png", 16, 5, 13) #3 - player
mcrfpy.createTexture("./assets/Sprite-0001.png", 80, 10, 10) #4 - LGJ2023 keycard + other icons
mcrfpy.createTexture("./assets/tiny_keycards.png", 16, 8, 8) #5 - tiny keycards (ground items)
self.ui_name = ui_name
self.grid_name = grid_name
# Menu index = 0
#print("Create UI")
# Create dialog UI
mcrfpy.createMenu(ui_name, 20, 540, 500, 200)
#mcrfpy.createCaption(ui_name, "Hello There", 18, BLACK)
mcrfpy.createCaption(ui_name, "", 24, RED)
#mcrfpy.createButton(ui_name, 250, 20, 100, 50, DARKBLUE, (0, 0, 0), "clicky", "testaction")
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKRED, (0, 0, 0), "REPL", "startrepl")
##mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKGREEN, (0, 0, 0), "map gen", "gridgen")
#mcrfpy.createButton(ui_name, 250, 20, 100, 50, DARKGREEN, (0, 0, 0), "mapL", "gridgen2")
#mcrfpy.createButton(ui_name, 250, 20, 100, 50, DARKBLUE, (192, 0, 0), "a_menu", "animtest")
#mcrfpy.createButton(ui_name, 250, 20, 100, 50, DARKRED, GREEN, "a_spr", "animtest2")
#mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKBLUE, GREEN, "Next sp", "nextsp")
#mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKBLUE, RED, "Prev sp", "prevsp")
#mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKBLUE, DARKGREEN, "+16 sp", "skipsp")
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKGREEN, (0, 0, 0), "Next", "nextsp")
mcrfpy.createButton(ui_name, 250, 0, 130, 40, DARKBLUE, (0, 0, 0), "Prev", "prevsp")
mcrfpy.createSprite(ui_name, 4, 1, 10, 10, 2.0)
# Menu index = 1
#print("Create UI 2")
entitymenu = "entitytestmenu"
mcrfpy.createMenu(entitymenu, 840, 20, 20, 500)
mcrfpy.createButton(entitymenu, 0, 10, 150, 40, DARKBLUE, BLACK, "Up", "test_up")
mcrfpy.createButton(entitymenu, 0, 60, 150, 40, DARKBLUE, BLACK, "Down", "test_down")
mcrfpy.createButton(entitymenu, 0, 110, 150, 40, DARKBLUE, BLACK, "Left", "test_left")
mcrfpy.createButton(entitymenu, 0, 160, 150, 40, DARKBLUE, BLACK, "Right", "test_right")
mcrfpy.createButton(entitymenu, 0, 210, 150, 40, DARKBLUE, BLACK, "Attack", "test_attack")
mcrfpy.createButton(entitymenu, 0, 210, 150, 40, DARKBLUE, RED, "TE", "testent")
# Menu index = 2
mcrfpy.createMenu( "gridtitlemenu", 0, -10, 0, 0)
mcrfpy.createCaption("gridtitlemenu", "<grid name>", 18, WHITE)
#mcrfpy.createCaption("gridtitlemenu", "<camstate>", 16, WHITE)
# Menu index = 3
mcrfpy.createMenu( "hintsmenu", 0, 505, 0, 0)
mcrfpy.createCaption("hintsmenu", "<hintline>", 16, WHITE)
# Menu index = 4
# menu names must be created in alphabetical order (?!) - thanks, C++ hash map
mcrfpy.createMenu( "i", 600, 20, 0, 0)
#mcrfpy.createMenu( "camstatemenu", 600, 20, 0, 0)
mcrfpy.createCaption("i", "<camstate>", 16, WHITE)
# Menu index = 5
mcrfpy.createMenu( "j", 600, 500, 0, 0)
mcrfpy.createButton( "j", 0, 0, 80, 40, DARKBLUE, WHITE, "Recenter", "activatecamfollow")
# Menu index = 6, 7, 8, 9, 10: keycard sprites
mcrfpy.createMenu("k", 540, 540, 80, 80) #6
mcrfpy.createSprite("k", 4, 0, 10, 10, 1.0)
mcrfpy.createMenu("l", 540 + (80 * 1), 540, 80, 80) #7
mcrfpy.createSprite("l", 4, 1, 10, 10, 1.0)
mcrfpy.createMenu("m", 540 + (80 * 2), 540, 80, 80) #8
mcrfpy.createSprite("m", 4, 2, 10, 10, 1.0)
mcrfpy.createMenu("n", 540 + (80 * 3), 540, 80, 80) #9
mcrfpy.createSprite("n", 4, 3, 10, 10, 1.0)
mcrfpy.createMenu("o", 540 + (80 * 4), 540, 80, 80) #10
mcrfpy.createSprite("o", 4, 4, 10, 10, 1.0)
mcrfpy.createMenu("p", 20, 20, 40, 40) #11
#mcrfpy.createButton("p", 0, 0, 130, 40, DARKGREEN, (0, 0, 0), "Register", "keyregistration")
mcrfpy.createButton("p", 0, 0, 130, 40, DARKGREEN, (0, 0, 0), "Register", "startrepl")
mcrfpy.registerPyAction("keyregistration", keyregistration)
#print("Make UIs visible")
self.menus = mcrfpy.listMenus()
self.menus[0].visible = True
self.menus[1].w = 170
self.menus[1].visible = True
self.menus[2].visible = True
for mn in range(2, 6):
self.menus[mn].bgcolor = BLACK
self.menus[mn].visible = True
mcrfpy.modMenu(self.menus[mn])
for mn in range(6, 11):
self.menus[mn].bgcolor = BLACK
self.menus[mn].visible = False
mcrfpy.modMenu(self.menus[mn])
self.menus[11].visible=True
mcrfpy.modMenu(self.menus[11])
#self.menus[2].bgcolor = BLACK
#self.menus[3].visible = True
#self.menus[3].bgcolor = BLACK
#self.menus[4].visible = True
#self.menus[4].bgcolor = BLACK
#self.menus[5].visible = True
#mcrfpy.modMenu(self.menus[0])
#mcrfpy.modMenu(self.menus[1])
#mcrfpy.modMenu(self.menus[2])
#mcrfpy.modMenu(self.menus[3])
#mcrfpy.modMenu(self.menus[4])
#mcrfpy.modMenu(self.menus[5])
#pprint(mcrfpy.listMenus())
#print(f"UI 1 gave back this sprite: {self.menus[0].sprites}")
#print("Create grid")
# create grid (title, gx, gy, gs, x, y, w, h)
mcrfpy.createGrid(grid_name, 100, 100, 16, 20, 20, 800, 500)
self.grids = mcrfpy.listGrids()
#print(self.grids)
#print("Create entities")
#mcrfpy.createEntity("demogrid", "dragon", 2, 545, 10, 10, lambda: None)
#mcrfpy.createEntity("demogrid", "tinyenemy", 1, 1538, 3, 6, lambda: None)
#print("Create fancy entity")
self.player = TestEntity("demogrid", "player", 3, 20, 17, 3, 5, walk_frames=4, attack_frames=5, do_fov=True)
self.tes = [
TestEntity("demogrid", "classtest", 1, 1538, 5, 7, 64, walk_frames=5, attack_frames=6),
TestEntity("demogrid", "classtest", 1, 1545, 7, 9, 64, walk_frames=5, attack_frames=6),
TestEntity("demogrid", "classtest", 1, 1552, 9, 11, 64, walk_frames=5, attack_frames=6),
TestEntity("demogrid", "classtest", 1, 1566, 11, 13, 64, walk_frames=4, attack_frames=6),
#TestEntity("demogrid", "item", 1, 1573, 13, 15, 64, walk_frames=4, attack_frames=6),
TestEntity("demogrid", "classtest", 1, 1583, 15, 17, 64, walk_frames=4, attack_frames=6),
TestEntity("demogrid", "classtest", 1, 130, 9, 7, 64, walk_frames=5, attack_frames=6),
TestEntity("demogrid", "classtest", 1, 136, 11, 9, 64, walk_frames=5, attack_frames=6),
TestEntity("demogrid", "classtest", 1, 143, 13, 11, 64, walk_frames=5, attack_frames=6),
TestEntity("demogrid", "classtest", 1, 158, 15, 13, 64, walk_frames=5, attack_frames=6),
TestEntity("demogrid", "classtest", 1, 165, 17, 15, 64, walk_frames=5, attack_frames=6),
self.player,
TestItemEntity("demogrid", "GreenKeyCard", 5, 0, 19, 3, texture_width=64,
walk_frames=5, attack_frames=6, do_fov=False, item="Green Keycard"),
TestItemEntity("demogrid", "BlueKeyCard", 5, 1, 21, 3, texture_width=64,
walk_frames=5, attack_frames=6, do_fov=False, item="Blue Keycard"),
TestItemEntity("demogrid", "RedKeyCard", 5, 2, 23, 3, texture_width=64,
walk_frames=5, attack_frames=6, do_fov=False, item="Red Keycard"),
TestItemEntity("demogrid", "OrangeKeyCard", 5, 3, 25, 3, texture_width=64,
walk_frames=5, attack_frames=6, do_fov=False, item="Orange Keycard"),
TestItemEntity("demogrid", "DevilKeyCard", 5, 4, 27, 3, texture_width=64,
walk_frames=5, attack_frames=6, do_fov=False, item="Devil Keycard"),
TestDoorEntity("demogrid", "GreenKeyDoor", 5, 8, 19, 5, texture_width=64,
walk_frames=5, attack_frames=6, do_fov=False, key="Green Keycard"),
TestDoorEntity("demogrid", "BlueKeyDoor", 5, 9, 21, 5, texture_width=64,
walk_frames=5, attack_frames=6, do_fov=False, key="Blue Keycard"),
TestDoorEntity("demogrid", "RedKeyDoor", 5, 10, 23, 5, texture_width=64,
walk_frames=5, attack_frames=6, do_fov=False, key="Red Keycard"),
TestDoorEntity("demogrid", "OrangeKeyDoor", 5, 11, 25, 5, texture_width=64,
walk_frames=5, attack_frames=6, do_fov=False, key="Orange Keycard"),
TestDoorEntity("demogrid", "DevilKeyDoor", 5, 12, 27, 5, texture_width=64,
walk_frames=5, attack_frames=6, do_fov=False, key="Devil Keycard")
]
self.grids = mcrfpy.listGrids()
self.entity_direction = 0
mcrfpy.registerPyAction("test_down", lambda: [te.animate(0, False, True) for te in self.tes])
mcrfpy.registerPyAction("test_up", lambda: [te.animate(1, False, True) for te in self.tes])
mcrfpy.registerPyAction("test_right", lambda: [te.animate(2, False, True) for te in self.tes])
mcrfpy.registerPyAction("test_left", lambda: [te.animate(3, False, True) for te in self.tes])
mcrfpy.registerPyAction("test_attack", lambda: [te.animate(0, True) for te in self.tes])
mcrfpy.registerPyAction("testent", lambda: [te.animate(2, True) for te in self.tes])
mcrfpy.registerPyAction("activatecamfollow", lambda: mcrfpy.camFollow(True))
# Button behavior
self.clicks = 0
self.sprite_index = 0
#mcrfpy.registerPyAction("testaction", self.click)
mcrfpy.registerPyAction("gridgen", self.gridgen)
#mcrfpy.registerPyAction("gridgen2", lambda: self.gridgen())
#mcrfpy.registerPyAction("animtest", lambda: self.createAnimation())
#mcrfpy.registerPyAction("animtest2", lambda: self.createAnimation2())
mcrfpy.registerPyAction("nextsp", lambda: self.changesprite(1))
mcrfpy.registerPyAction("prevsp", lambda: self.changesprite(-1))
mcrfpy.registerPyAction("skipsp", lambda: self.changesprite(16))
mcrfpy.unlockPlayerInput()
mcrfpy.setActiveGrid("demogrid")
self.gridgen()
self.updatehints()
mcrfpy.camFollow(True)
def itemguis(self):
print(self.player.inventory)
items = ["Green Keycard", "Blue Keycard", "Red Keycard", "Orange Keycard", "Devil Keycard"]
for mn in range(6, 11):
self.menus[mn].visible = items[mn-6] in self.player.inventory
mcrfpy.modMenu(self.menus[mn])
def animate_entity(self, direction, attacking=False):
if direction is None:
direction = self.entity_direction
else:
self.entity_direction = direction
dragon_sprite = 545 + (32 * (direction + (4 if attacking else 0)))
dragon_animation = [dragon_sprite + i for i in range((5 if attacking else 4))]
mcrfpy.createAnimation(
1.0, # duration, seconds
"demogrid", # parent: a UIMenu or Grid key
"entity", # target type: 'menu', 'grid', 'caption', 'button', 'sprite', or 'entity'
0, # target id: integer index for menu or grid objs; None for grid/menu
"sprite", # field: 'position', 'size', 'bgcolor', 'textcolor', or 'sprite'
lambda: self.animation_done("demobox1"), #callback: callable once animation is complete
False, #loop: repeat indefinitely
dragon_animation # values: iterable of frames for 'sprite', lerp target for others
)
orc_sprite = 1538 + (64 * (direction + (4 if attacking else 0)))
orc_animation = [orc_sprite + i for i in range((5 if attacking else 4))]
mcrfpy.createAnimation(
1.0, # duration, seconds
"demogrid", # parent: a UIMenu or Grid key
"entity", # target type: 'menu', 'grid', 'caption', 'button', 'sprite', or 'entity'
1, # target id: integer index for menu or grid objs; None for grid/menu
"sprite", # field: 'position', 'size', 'bgcolor', 'textcolor', or 'sprite'
lambda: self.animation_done("demobox1"), #callback: callable once animation is complete
False, #loop: repeat indefinitely
orc_animation # values: iterable of frames for 'sprite', lerp target for others
)
#def move_entity(self, targetgrid, entity_index, position):
# for i, g in enumerate(self.grids):
# if g.title == targetgrid:
# g.entities[entity_index].x = position[0]
# g.entities[entity_index].y = position[1]
# #pts = g.points
# g.visible = True
# mcrfpy.modGrid(g)
# self.grids = mcrfpy.listGrids()
# #self.grids[i].points = pts
# return
def changesprite(self, n):
self.sprite_index += n
self.menus[0].captions[0].text = f"Sprite #{self.sprite_index}"
self.menus[0].sprites[0].sprite_index = self.sprite_index
mcrfpy.modMenu(self.menus[0])
def click(self):
self.clicks += 1
self.menus[0].captions[0].text = f"Clicks: {self.clicks}"
self.menus[0].sprites[0].sprite_index = randint(0, 3)
mcrfpy.modMenu(self.menus[0])
def updatehints(self):
self.menus[2].captions[0].text=mcrfpy.activeGrid()
mcrfpy.modMenu(self.menus[2])
self.menus[3].captions[0].text=mcrfpy.inputMode()
mcrfpy.modMenu(self.menus[3])
#self.menus[4].captions[0].text=f"follow: {mcrfpy.camFollow()}"
self.menus[4].captions[0].text="following" if mcrfpy.camFollow() else "free"
mcrfpy.modMenu(self.menus[4])
self.menus[5].visible = not mcrfpy.camFollow()
mcrfpy.modMenu(self.menus[5])
def gridgen(self):
#print(f"[Python] modifying {len(self.grids[0].points)} grid points")
for p in self.grids[0].points:
#p.color = (randint(0, 255), randint(64, 192), 0)
p.color = (0,0,0)
p.walkable = False
p.transparent = False
room_centers = [(randint(0, self.grids[0].grid_x-1), randint(0, self.grids[0].grid_y-1)) for i in range(20)] + [(17, 3), (20,10) + (20,5)]
#room_centers.append((3, 5))
for r in room_centers:
# random hallway
target = choice(room_centers)
length1 = abs(target[0] - r[0])
xbase = min(target[0], r[0])
for x in range(length1):
gpoint = self.grids[0].at(x, r[1])
if gpoint is None: continue
gpoint.walkable = True
gpoint.transparent = True
gpoint.color = (192, 192, 192)
length2 = abs(target[1] - r[1])
ybase = min(target[1], r[1])
for y in range(length2):
gpoint = self.grids[0].at(r[0], y)
if gpoint is None: continue
gpoint.walkable = True
gpoint.transparent = True
gpoint.color = (192, 192, 192)
for r in room_centers:
#print(r)
room_color = (randint(16, 24)*8, randint(16, 24)*8, randint(16, 24)*8)
#self.grids[0].at(r[0], r[1]).walkable = True
#self.grids[0].at(r[0], r[1]).color = room_color
halfx, halfy = randint(2, 11), randint(2,11)
for p_x in range(r[0] - halfx, r[0] + halfx):
for p_y in range(r[1] - halfy, r[1] + halfy):
gpoint = self.grids[0].at(p_x, p_y)
if gpoint is None: continue
gpoint.walkable = True
gpoint.transparent = True
gpoint.color = room_color
#print()
#print("[Python] Modifying:")
self.grids[0].at(10, 10).color = (255, 255, 255)
self.grids[0].at(10, 10).walkable = False
self.grids[0].visible = True
mcrfpy.modGrid(self.grids[0])
#self.grids = mcrfpy.listGrids()
#print(f"Sent grid: {repr(self.grids[0])}")
#print(f"Received grid: {repr(mcrfpy.listGrids()[0])}")
def animation_done(self, s):
print(f"The `{s}` animation completed.")
#self.menus = mcrfpy.listMenus()
# if (!PyArg_ParseTuple(args, "fsssiOOO", &duration, &parent, &target_type, &target_id, &field, &callback, &loop_obj, &values_obj)) return NULL;
def createAnimation(self):
print(self.menus)
self.menus = mcrfpy.listMenus()
self.menus[0].w = 500
self.menus[0].h = 200
print(self.menus)
mcrfpy.modMenu(self.menus[0])
print(self.menus)
mcrfpy.createAnimation(
3.0, # duration, seconds
"demobox1", # parent: a UIMenu or Grid key
"menu", # target type: 'menu', 'grid', 'caption', 'button', 'sprite', or 'entity'
0, # target id: integer index for menu or grid objs; None for grid/menu
"size", # field: 'position', 'size', 'bgcolor', 'textcolor', or 'sprite'
lambda: self.animation_done("demobox1"), #callback: callable once animation is complete
False, #loop: repeat indefinitely
[150, 100] # values: iterable of frames for 'sprite', lerp target for others
)
def createAnimation2(self):
mcrfpy.createAnimation(
5,
"demobox1",
"sprite",
0,
"sprite",
lambda: self.animation_done("sprite change"),
False,
[0, 1, 2, 1, 2, 0]
)
def keytest():
print("Key tested.")
def keyregistration():
print("Registering 'keytest'")
mcrfpy.registerPyAction("keytest", keytest)
print("Registering input")
print(mcrfpy.registerInputAction(15, "keytest")) # 15 = P
mcrfpy.registerPyAction("player_move_up", lambda: scene.player.move(0, -1))
mcrfpy.registerPyAction("player_move_left", lambda: scene.player.move(-1, 0))
mcrfpy.registerPyAction("player_move_down", lambda: scene.player.move(0, 1))
mcrfpy.registerPyAction("player_move_right", lambda: scene.player.move(1, 0))
mcrfpy.registerInputAction(ord('w') - ord('a'), "player_move_up")
mcrfpy.registerInputAction(ord('a') - ord('a'), "player_move_left")
mcrfpy.registerInputAction(ord('s') - ord('a'), "player_move_down")
mcrfpy.registerInputAction(ord('d') - ord('a'), "player_move_right")
def start():
global scene
#print("TestScene.start called")
scene = TestScene()
mcrfpy.refreshFov()
scene.updatehints()

48
scripts/UIMenu.py Normal file
View File

@ -0,0 +1,48 @@
class Caption:
def __init__(self, text, textsize, color):
self.text = text
self.textsize = textsize
self.color = color
def __repr__(self):
return f"<Caption text={self.text}, textsize={self.textsize}, color={self.color}>"
class Button:
def __init__(self, x, y, w, h, bgcolor, textcolor, text, actioncode):
self.x = x
self.y = y
self.w = w
self.h = h
self.bgcolor = bgcolor
self.textcolor = textcolor
self.text = text
self.actioncode = actioncode
def __repr__(self):
return f"<Button ({self.x}, {self.y}, {self.w}, {self.h}), bgcolor={self.bgcolor}, textcolor={self.textcolor}, actioncode={self.actioncode}>"
class Sprite:
def __init__(self, tex_index, sprite_index, x, y):
self.tex_index = tex_index
self.sprite_index = sprite_index
self.x = x
self.y = y
def __repr__(self):
return f"<Sprite tex_index={self.tex_index}, self.sprite_index={self.sprite_index}, x={self.x}, y={self.y}>"
class UIMenu:
def __init__(self, title, x, y, w, h, bgcolor, visible=False):
self.title = title
self.x = x
self.y = y
self.w = w
self.h = h
self.bgcolor = bgcolor
self.visible = visible
self.captions = []
self.buttons = []
self.sprites = []
def __repr__(self):
return f"<UIMenu title={repr(self.title)}, x={self.x}, y={self.y}, w={self.w}, h={self.h}, bgcolor={self.bgcolor}, visible={self.visible}, {len(self.captions)} captions, {len(self.buttons)} buttons, {len(self.sprites)} sprites>"

10
scripts/engine_user.py Normal file
View File

@ -0,0 +1,10 @@
print("[Python] Attempting import")
import scriptable
print(f"[Python] calling fibonacci(8): {scriptable.fibonacci(8)}")
print(f"[Python] calling fibonacci(15): {scriptable.fibonacci(15)}")
import venv
print("[Python] Importing library installed with pip")
import numpy

37
scripts/test_ui.py Normal file
View File

@ -0,0 +1,37 @@
import mcrfpy
mcrfpy.createTexture("./assets/test_portraits.png", 32, 8, 8)
from random import choice, randint
box_colors = [
(0, 0, 192),
(0, 192, 0),
(192, 0, 0),
(192, 192, 0),
(0, 192, 192),
(192, 0, 192)
]
text_colors = [
(0, 0, 255),
(0, 255, 0),
(255, 0, 0),
(255, 255, 0),
(0, 255, 255),
(255, 0, 255)
]
test_x = 500
test_y = 10
for i in range(40):
ui_name = f"test{i}"
mcrfpy.createMenu(ui_name, test_x, test_y, 400, 200)
mcrfpy.createCaption(ui_name, "Hello There", 18, choice(text_colors))
mcrfpy.createButton(ui_name, 250, 20, 100, 50, choice(box_colors), (0, 0, 0), "asdf", "testaction")
mcrfpy.createSprite(ui_name, 0, randint(0, 3), 650, 60, 5.0)
test_x -= 50
test_y += 50
if (test_x <= 50):
test_x = 500
#print(test_x)

View File

@ -11,10 +11,10 @@ public:
const static int WHEEL_NUM = 4;
const static int WHEEL_NEG = 2;
const static int WHEEL_DEL = 1;
static int keycode(const sf::Keyboard::Key& k) { return KEY + (int)k; }
static int keycode(const sf::Mouse::Button& b) { return MOUSEBUTTON + (int)b; }
static int keycode(sf::Keyboard::Key& k) { return KEY + (int)k; }
static int keycode(sf::Mouse::Button& b) { return MOUSEBUTTON + (int)b; }
//static int keycode(sf::Mouse::Wheel& w, float d) { return MOUSEWHEEL + (((int)w)<<12) + int(d*16) + 512; }
static int keycode(const sf::Mouse::Wheel& w, float d) {
static int keycode(sf::Mouse::Wheel& w, float d) {
int neg = 0;
if (d < 0) { neg = 1; }
return MOUSEWHEEL + (w * WHEEL_NUM) + (neg * WHEEL_NEG) + 1;
@ -31,116 +31,4 @@ public:
if (a & WHEEL_NEG) factor = -1;
return (a & WHEEL_DEL) * factor;
}
static std::string key_str(const sf::Keyboard::Key& keycode)
{
switch(keycode)
{
case sf::Keyboard::Key::Unknown: return "Unknown"; break;
case sf::Keyboard::Key::A: return "A"; break;
case sf::Keyboard::Key::B: return "B"; break;
case sf::Keyboard::Key::C: return "C"; break;
case sf::Keyboard::Key::D: return "D"; break;
case sf::Keyboard::Key::E: return "E"; break;
case sf::Keyboard::Key::F: return "F"; break;
case sf::Keyboard::Key::G: return "G"; break;
case sf::Keyboard::Key::H: return "H"; break;
case sf::Keyboard::Key::I: return "I"; break;
case sf::Keyboard::Key::J: return "J"; break;
case sf::Keyboard::Key::K: return "K"; break;
case sf::Keyboard::Key::L: return "L"; break;
case sf::Keyboard::Key::M: return "M"; break;
case sf::Keyboard::Key::N: return "N"; break;
case sf::Keyboard::Key::O: return "O"; break;
case sf::Keyboard::Key::P: return "P"; break;
case sf::Keyboard::Key::Q: return "Q"; break;
case sf::Keyboard::Key::R: return "R"; break;
case sf::Keyboard::Key::S: return "S"; break;
case sf::Keyboard::Key::T: return "T"; break;
case sf::Keyboard::Key::U: return "U"; break;
case sf::Keyboard::Key::V: return "V"; break;
case sf::Keyboard::Key::W: return "W"; break;
case sf::Keyboard::Key::X: return "X"; break;
case sf::Keyboard::Key::Y: return "Y"; break;
case sf::Keyboard::Key::Z: return "Z"; break;
case sf::Keyboard::Key::Num0: return "Num0"; break;
case sf::Keyboard::Key::Num1: return "Num1"; break;
case sf::Keyboard::Key::Num2: return "Num2"; break;
case sf::Keyboard::Key::Num3: return "Num3"; break;
case sf::Keyboard::Key::Num4: return "Num4"; break;
case sf::Keyboard::Key::Num5: return "Num5"; break;
case sf::Keyboard::Key::Num6: return "Num6"; break;
case sf::Keyboard::Key::Num7: return "Num7"; break;
case sf::Keyboard::Key::Num8: return "Num8"; break;
case sf::Keyboard::Key::Num9: return "Num9"; break;
case sf::Keyboard::Key::Escape: return "Escape"; break;
case sf::Keyboard::Key::LControl: return "LControl"; break;
case sf::Keyboard::Key::LShift: return "LShift"; break;
case sf::Keyboard::Key::LAlt: return "LAlt"; break;
case sf::Keyboard::Key::LSystem: return "LSystem"; break;
case sf::Keyboard::Key::RControl: return "RControl"; break;
case sf::Keyboard::Key::RShift: return "RShift"; break;
case sf::Keyboard::Key::RAlt: return "RAlt"; break;
case sf::Keyboard::Key::RSystem: return "RSystem"; break;
case sf::Keyboard::Key::Menu: return "Menu"; break;
case sf::Keyboard::Key::LBracket: return "LBracket"; break;
case sf::Keyboard::Key::RBracket: return "RBracket"; break;
case sf::Keyboard::Key::Semicolon: return "Semicolon"; break;
case sf::Keyboard::Key::Comma: return "Comma"; break;
case sf::Keyboard::Key::Period: return "Period"; break;
case sf::Keyboard::Key::Apostrophe: return "Apostrophe"; break;
case sf::Keyboard::Key::Slash: return "Slash"; break;
case sf::Keyboard::Key::Backslash: return "Backslash"; break;
case sf::Keyboard::Key::Grave: return "Grave"; break;
case sf::Keyboard::Key::Equal: return "Equal"; break;
case sf::Keyboard::Key::Hyphen: return "Hyphen"; break;
case sf::Keyboard::Key::Space: return "Space"; break;
case sf::Keyboard::Key::Enter: return "Enter"; break;
case sf::Keyboard::Key::Backspace: return "Backspace"; break;
case sf::Keyboard::Key::Tab: return "Tab"; break;
case sf::Keyboard::Key::PageUp: return "PageUp"; break;
case sf::Keyboard::Key::PageDown: return "PageDown"; break;
case sf::Keyboard::Key::End: return "End"; break;
case sf::Keyboard::Key::Home: return "Home"; break;
case sf::Keyboard::Key::Insert: return "Insert"; break;
case sf::Keyboard::Key::Delete: return "Delete"; break;
case sf::Keyboard::Key::Add: return "Add"; break;
case sf::Keyboard::Key::Subtract: return "Subtract"; break;
case sf::Keyboard::Key::Multiply: return "Multiply"; break;
case sf::Keyboard::Key::Divide: return "Divide"; break;
case sf::Keyboard::Key::Left: return "Left"; break;
case sf::Keyboard::Key::Right: return "Right"; break;
case sf::Keyboard::Key::Up: return "Up"; break;
case sf::Keyboard::Key::Down: return "Down"; break;
case sf::Keyboard::Key::Numpad0: return "Numpad0"; break;
case sf::Keyboard::Key::Numpad1: return "Numpad1"; break;
case sf::Keyboard::Key::Numpad2: return "Numpad2"; break;
case sf::Keyboard::Key::Numpad3: return "Numpad3"; break;
case sf::Keyboard::Key::Numpad4: return "Numpad4"; break;
case sf::Keyboard::Key::Numpad5: return "Numpad5"; break;
case sf::Keyboard::Key::Numpad6: return "Numpad6"; break;
case sf::Keyboard::Key::Numpad7: return "Numpad7"; break;
case sf::Keyboard::Key::Numpad8: return "Numpad8"; break;
case sf::Keyboard::Key::Numpad9: return "Numpad9"; break;
case sf::Keyboard::Key::F1: return "F1"; break;
case sf::Keyboard::Key::F2: return "F2"; break;
case sf::Keyboard::Key::F3: return "F3"; break;
case sf::Keyboard::Key::F4: return "F4"; break;
case sf::Keyboard::Key::F5: return "F5"; break;
case sf::Keyboard::Key::F6: return "F6"; break;
case sf::Keyboard::Key::F7: return "F7"; break;
case sf::Keyboard::Key::F8: return "F8"; break;
case sf::Keyboard::Key::F9: return "F9"; break;
case sf::Keyboard::Key::F10: return "F10"; break;
case sf::Keyboard::Key::F11: return "F11"; break;
case sf::Keyboard::Key::F12: return "F12"; break;
case sf::Keyboard::Key::F13: return "F13"; break;
case sf::Keyboard::Key::F14: return "F14"; break;
case sf::Keyboard::Key::F15: return "F15"; break;
case sf::Keyboard::Key::Pause: return "Pause"; break;
default:
return "Any";
break;
}
}
};

View File

@ -1,675 +1,124 @@
#include "Animation.h"
#include "UIDrawable.h"
#include "UIEntity.h"
#include "PyAnimation.h"
#include "McRFPy_API.h"
#include "PythonObjectCache.h"
#include <cmath>
#include <algorithm>
#include <unordered_map>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
Animation::Animation(float _d, std::function<void()> _cb, bool _l)
:duration(_d), callback(_cb), loop(_l), elapsed(0.0f) {}
// Forward declaration of PyAnimation type
namespace mcrfpydef {
extern PyTypeObject PyAnimationType;
}
// Animation implementation
Animation::Animation(const std::string& targetProperty,
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc,
bool delta,
PyObject* callback)
: targetProperty(targetProperty)
, targetValue(targetValue)
, duration(duration)
, easingFunc(easingFunc)
, delta(delta)
, pythonCallback(callback)
{
// Increase reference count for Python callback
if (pythonCallback) {
Py_INCREF(pythonCallback);
}
// linear interpolation constructor
template<typename T>
LerpAnimation<T>::LerpAnimation(float _d, T _ev, T _sv, std::function<void()> _cb, std::function<void(T)> _w, bool _l)
:Animation(_d, _cb, _l), //duration(_d), target(_t), callback(_cb), loop(_l),elapsed(0.0f),
startvalue(_sv), endvalue(_ev), write(_w) {}
// discrete values constructor
template<typename T>
DiscreteAnimation<T>::DiscreteAnimation(float _d, std::vector<T> _v, std::function<void()> _cb, std::function<void(T)> _w, bool _l)
:Animation(_d, _cb, _l), //duration(_d), target(_t), callback(_cb), loop(_l), elapsed(0.0f),
index(0), nonelapsed(0.0f), values(_v), write(_w) {
timestep = _d / _v.size();
}
/* // don't call virtual functions (like cancel()) from base class destructor
* // child classes destructors' are called first anyway
Animation::~Animation() {
// Decrease reference count for Python callback if we still own it
PyObject* callback = pythonCallback;
if (callback) {
pythonCallback = nullptr;
PyGILState_STATE gstate = PyGILState_Ensure();
Py_DECREF(callback);
PyGILState_Release(gstate);
}
// Clean up cache entry
if (serial_number != 0) {
PythonObjectCache::getInstance().remove(serial_number);
}
// deconstructor sets target to desired end state (no partial values)
cancel();
}
*/
template<>
void LerpAnimation<std::string>::lerp() {
//*(std::string*)target = ;
write(endvalue.substr(0, endvalue.length() * (elapsed / duration)));
}
void Animation::start(std::shared_ptr<UIDrawable> target) {
if (!target) return;
targetWeak = target;
elapsed = 0.0f;
callbackTriggered = false; // Reset callback state
// Capture start value from target
std::visit([this, &target](const auto& targetVal) {
using T = std::decay_t<decltype(targetVal)>;
if constexpr (std::is_same_v<T, float>) {
float value;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, int>) {
int value;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// For sprite animation, get current sprite index
int value;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, sf::Color>) {
sf::Color value;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
sf::Vector2f value;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, std::string>) {
std::string value;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
}, targetValue);
template<>
void LerpAnimation<int>::lerp() {
int delta = endvalue - startvalue;
//*(int*)target = ;
write(startvalue + (elapsed / duration * delta));
}
void Animation::startEntity(std::shared_ptr<UIEntity> target) {
if (!target) return;
entityTargetWeak = target;
elapsed = 0.0f;
callbackTriggered = false; // Reset callback state
// Capture the starting value from the entity
std::visit([this, target](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
float value = 0.0f;
if (target->getProperty(targetProperty, value)) {
startValue = value;
}
}
else if constexpr (std::is_same_v<T, int>) {
// For entities, we might need to handle sprite_index differently
if (targetProperty == "sprite_index" || targetProperty == "sprite_number") {
startValue = target->sprite.getSpriteIndex();
}
}
// Entities don't support other types yet
}, targetValue);
template<>
void LerpAnimation<float>::lerp() {
int delta = endvalue - startvalue;
//*(float*)target = ;
write(startvalue + (elapsed / duration * delta));
}
bool Animation::hasValidTarget() const {
return !targetWeak.expired() || !entityTargetWeak.expired();
template<>
void LerpAnimation<sf::Vector2f>::lerp() {
//std::cout << "sf::Vector2f implementation of lerp." << std::endl;
int delta_x = endvalue.x - startvalue.x;
int delta_y = endvalue.y - startvalue.y;
//std::cout << "Start: " << startvalue.x << ", " << startvalue.y << "; End: " << endvalue.x << ", " << endvalue.y << std::endl;
//std::cout << "Delta: " << delta_x << ", " << delta_y << std::endl;
//((sf::Vector2f*)target)->x = startvalue.x + (elapsed / duration * delta_x);
//((sf::Vector2f*)target)->y = startvalue.y + (elapsed / duration * delta_y);
write(sf::Vector2f(startvalue.x + (elapsed / duration * delta_x),
startvalue.y + (elapsed / duration * delta_y)));
}
void Animation::clearCallback() {
// Safely clear the callback when PyAnimation is being destroyed
PyObject* callback = pythonCallback;
if (callback) {
pythonCallback = nullptr;
callbackTriggered = true; // Prevent future triggering
PyGILState_STATE gstate = PyGILState_Ensure();
Py_DECREF(callback);
PyGILState_Release(gstate);
}
template<>
void LerpAnimation<sf::Vector2i>::lerp() {
int delta_x = endvalue.x - startvalue.y;
int delta_y = endvalue.y - startvalue.y;
//((sf::Vector2i*)target)->x = startvalue.x + (elapsed / duration * delta_x);
//((sf::Vector2i*)target)->y = startvalue.y + (elapsed / duration * delta_y);
write(sf::Vector2i(startvalue.x + (elapsed / duration * delta_x),
startvalue.y + (elapsed / duration * delta_y)));
}
void Animation::complete() {
// Jump to end of animation
elapsed = duration;
// Apply final value
if (auto target = targetWeak.lock()) {
AnimationValue finalValue = interpolate(1.0f);
applyValue(target.get(), finalValue);
}
else if (auto entity = entityTargetWeak.lock()) {
AnimationValue finalValue = interpolate(1.0f);
applyValue(entity.get(), finalValue);
}
template<typename T>
void LerpAnimation<T>::step(float delta) {
if (complete) return;
elapsed += delta;
//std::cout << "LerpAnimation step function. Elapsed: " << elapsed <<std::endl;
lerp();
if (isDone()) { callback(); complete = true; cancel(); }; //use the exact value, not my math
}
bool Animation::update(float deltaTime) {
// Try to lock weak_ptr to get shared_ptr
std::shared_ptr<UIDrawable> target = targetWeak.lock();
std::shared_ptr<UIEntity> entity = entityTargetWeak.lock();
// If both are null, target was destroyed
if (!target && !entity) {
return false; // Remove this animation
}
if (isComplete()) {
return false;
}
elapsed += deltaTime;
elapsed = std::min(elapsed, duration);
// Calculate easing value (0.0 to 1.0)
float t = duration > 0 ? elapsed / duration : 1.0f;
float easedT = easingFunc(t);
// Get interpolated value
AnimationValue currentValue = interpolate(easedT);
// Apply to whichever target is valid
if (target) {
applyValue(target.get(), currentValue);
} else if (entity) {
applyValue(entity.get(), currentValue);
}
// Trigger callback when animation completes
// Check pythonCallback again in case it was cleared during update
if (isComplete() && !callbackTriggered && pythonCallback) {
triggerCallback();
}
return !isComplete();
template<typename T>
void DiscreteAnimation<T>::step(float delta)
{
if (complete) return;
nonelapsed += delta;
//std::cout << "Nonelapsed: " << nonelapsed << " elapsed (pre-add): " << elapsed << " timestep: " << timestep << " duration: " << duration << " index: " << index << std::endl;
if (nonelapsed < timestep) return;
//std::cout << "values size: " << values.size() << " isDone(): " << isDone() << std::endl;
if (elapsed > duration && !complete) {callback(); complete = true; return; }
elapsed += nonelapsed; // or should it be += timestep?
if (index == values.size() - 1) return;
nonelapsed = 0; // or should it -= timestep?
index++;
//*(T*)target = values[index];
write(values[index]);
}
AnimationValue Animation::getCurrentValue() const {
float t = duration > 0 ? elapsed / duration : 1.0f;
float easedT = easingFunc(t);
return interpolate(easedT);
template<typename T>
void LerpAnimation<T>::cancel() {
//*(T*)target = endvalue;
write(endvalue);
}
AnimationValue Animation::interpolate(float t) const {
// Visit the variant to perform type-specific interpolation
return std::visit([this, t](const auto& target) -> AnimationValue {
using T = std::decay_t<decltype(target)>;
if constexpr (std::is_same_v<T, float>) {
// Interpolate float
const float* start = std::get_if<float>(&startValue);
if (!start) return target; // Type mismatch
if (delta) {
return *start + target * t;
} else {
return *start + (target - *start) * t;
}
}
else if constexpr (std::is_same_v<T, int>) {
// Interpolate integer
const int* start = std::get_if<int>(&startValue);
if (!start) return target;
float result;
if (delta) {
result = *start + target * t;
} else {
result = *start + (target - *start) * t;
}
return static_cast<int>(std::round(result));
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// For sprite animation, interpolate through the list
if (target.empty()) return target;
// Map t to an index in the vector
size_t index = static_cast<size_t>(t * (target.size() - 1));
index = std::min(index, target.size() - 1);
return static_cast<int>(target[index]);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
// Interpolate color
const sf::Color* start = std::get_if<sf::Color>(&startValue);
if (!start) return target;
sf::Color result;
if (delta) {
result.r = std::clamp(start->r + target.r * t, 0.0f, 255.0f);
result.g = std::clamp(start->g + target.g * t, 0.0f, 255.0f);
result.b = std::clamp(start->b + target.b * t, 0.0f, 255.0f);
result.a = std::clamp(start->a + target.a * t, 0.0f, 255.0f);
} else {
result.r = start->r + (target.r - start->r) * t;
result.g = start->g + (target.g - start->g) * t;
result.b = start->b + (target.b - start->b) * t;
result.a = start->a + (target.a - start->a) * t;
}
return result;
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
// Interpolate vector
const sf::Vector2f* start = std::get_if<sf::Vector2f>(&startValue);
if (!start) return target;
if (delta) {
return sf::Vector2f(start->x + target.x * t,
start->y + target.y * t);
} else {
return sf::Vector2f(start->x + (target.x - start->x) * t,
start->y + (target.y - start->y) * t);
}
}
else if constexpr (std::is_same_v<T, std::string>) {
// For text, show characters based on t
const std::string* start = std::get_if<std::string>(&startValue);
if (!start) return target;
// If delta mode, append characters from target
if (delta) {
size_t chars = static_cast<size_t>(target.length() * t);
return *start + target.substr(0, chars);
} else {
// Transition from start text to target text
if (t < 0.5f) {
// First half: remove characters from start
size_t chars = static_cast<size_t>(start->length() * (1.0f - t * 2.0f));
return start->substr(0, chars);
} else {
// Second half: add characters to target
size_t chars = static_cast<size_t>(target.length() * ((t - 0.5f) * 2.0f));
return target.substr(0, chars);
}
}
}
return target; // Fallback
}, targetValue);
template<typename T>
void DiscreteAnimation<T>::cancel() {
//*(T*)target = values[values.size() - 1];
write(values[values.size() - 1]);
}
void Animation::applyValue(UIDrawable* target, const AnimationValue& value) {
if (!target) return;
std::visit([this, target](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, int>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
target->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, std::string>) {
target->setProperty(targetProperty, val);
}
}, value);
bool Animation::isDone() {
return elapsed + Animation::EPSILON >= duration;
}
void Animation::applyValue(UIEntity* entity, const AnimationValue& value) {
if (!entity) return;
std::visit([this, entity](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
entity->setProperty(targetProperty, val);
}
else if constexpr (std::is_same_v<T, int>) {
entity->setProperty(targetProperty, val);
}
// Entities don't support other types yet
}, value);
namespace animation_template_implementations {
// instantiate to compile concrete templates
//LerpAnimation<sf::Vector2f> implement_vector2f;
auto implement_v2f_const = LerpAnimation<sf::Vector2<float>>(4.0, sf::Vector2<float>(), sf::Vector2f(1,1), [](){}, [](sf::Vector2f v){}, false);
auto implement_disc_i = DiscreteAnimation<int>(3.0, std::vector<int>{0},[](){},[](int){},false);
//LerpAnimation<sf::Vector2i> implement_vector2i;
//LerpAnimation<int> implment_int;
//LerpAnimation<std::string> implment_string;
//LerpAnimation<float> implement_float;
//DiscreteAnimation<int> implement_int_discrete;
}
void Animation::triggerCallback() {
if (!pythonCallback) return;
// Ensure we only trigger once
if (callbackTriggered) return;
callbackTriggered = true;
PyGILState_STATE gstate = PyGILState_Ensure();
// TODO: In future, create PyAnimation wrapper for this animation
// For now, pass None for both parameters
PyObject* args = PyTuple_New(2);
Py_INCREF(Py_None);
Py_INCREF(Py_None);
PyTuple_SetItem(args, 0, Py_None); // animation parameter
PyTuple_SetItem(args, 1, Py_None); // target parameter
PyObject* result = PyObject_CallObject(pythonCallback, args);
Py_DECREF(args);
if (!result) {
// Print error but don't crash
PyErr_Print();
PyErr_Clear(); // Clear the error state
} else {
Py_DECREF(result);
}
PyGILState_Release(gstate);
}
// Easing functions implementation
namespace EasingFunctions {
float linear(float t) {
return t;
}
float easeIn(float t) {
return t * t;
}
float easeOut(float t) {
return t * (2.0f - t);
}
float easeInOut(float t) {
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
}
// Quadratic
float easeInQuad(float t) {
return t * t;
}
float easeOutQuad(float t) {
return t * (2.0f - t);
}
float easeInOutQuad(float t) {
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
}
// Cubic
float easeInCubic(float t) {
return t * t * t;
}
float easeOutCubic(float t) {
float t1 = t - 1.0f;
return t1 * t1 * t1 + 1.0f;
}
float easeInOutCubic(float t) {
return t < 0.5f ? 4.0f * t * t * t : (t - 1.0f) * (2.0f * t - 2.0f) * (2.0f * t - 2.0f) + 1.0f;
}
// Quartic
float easeInQuart(float t) {
return t * t * t * t;
}
float easeOutQuart(float t) {
float t1 = t - 1.0f;
return 1.0f - t1 * t1 * t1 * t1;
}
float easeInOutQuart(float t) {
return t < 0.5f ? 8.0f * t * t * t * t : 1.0f - 8.0f * (t - 1.0f) * (t - 1.0f) * (t - 1.0f) * (t - 1.0f);
}
// Sine
float easeInSine(float t) {
return 1.0f - std::cos(t * M_PI / 2.0f);
}
float easeOutSine(float t) {
return std::sin(t * M_PI / 2.0f);
}
float easeInOutSine(float t) {
return 0.5f * (1.0f - std::cos(M_PI * t));
}
// Exponential
float easeInExpo(float t) {
return t == 0.0f ? 0.0f : std::pow(2.0f, 10.0f * (t - 1.0f));
}
float easeOutExpo(float t) {
return t == 1.0f ? 1.0f : 1.0f - std::pow(2.0f, -10.0f * t);
}
float easeInOutExpo(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
if (t < 0.5f) {
return 0.5f * std::pow(2.0f, 20.0f * t - 10.0f);
} else {
return 1.0f - 0.5f * std::pow(2.0f, -20.0f * t + 10.0f);
}
}
// Circular
float easeInCirc(float t) {
return 1.0f - std::sqrt(1.0f - t * t);
}
float easeOutCirc(float t) {
float t1 = t - 1.0f;
return std::sqrt(1.0f - t1 * t1);
}
float easeInOutCirc(float t) {
if (t < 0.5f) {
return 0.5f * (1.0f - std::sqrt(1.0f - 4.0f * t * t));
} else {
return 0.5f * (std::sqrt(1.0f - (2.0f * t - 2.0f) * (2.0f * t - 2.0f)) + 1.0f);
}
}
// Elastic
float easeInElastic(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
float p = 0.3f;
float a = 1.0f;
float s = p / 4.0f;
float t1 = t - 1.0f;
return -(a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p));
}
float easeOutElastic(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
float p = 0.3f;
float a = 1.0f;
float s = p / 4.0f;
return a * std::pow(2.0f, -10.0f * t) * std::sin((t - s) * (2.0f * M_PI) / p) + 1.0f;
}
float easeInOutElastic(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
float p = 0.45f;
float a = 1.0f;
float s = p / 4.0f;
if (t < 0.5f) {
float t1 = 2.0f * t - 1.0f;
return -0.5f * (a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p));
} else {
float t1 = 2.0f * t - 1.0f;
return a * std::pow(2.0f, -10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p) * 0.5f + 1.0f;
}
}
// Back (overshooting)
float easeInBack(float t) {
const float s = 1.70158f;
return t * t * ((s + 1.0f) * t - s);
}
float easeOutBack(float t) {
const float s = 1.70158f;
float t1 = t - 1.0f;
return t1 * t1 * ((s + 1.0f) * t1 + s) + 1.0f;
}
float easeInOutBack(float t) {
const float s = 1.70158f * 1.525f;
if (t < 0.5f) {
return 0.5f * (4.0f * t * t * ((s + 1.0f) * 2.0f * t - s));
} else {
float t1 = 2.0f * t - 2.0f;
return 0.5f * (t1 * t1 * ((s + 1.0f) * t1 + s) + 2.0f);
}
}
// Bounce
float easeOutBounce(float t) {
if (t < 1.0f / 2.75f) {
return 7.5625f * t * t;
} else if (t < 2.0f / 2.75f) {
float t1 = t - 1.5f / 2.75f;
return 7.5625f * t1 * t1 + 0.75f;
} else if (t < 2.5f / 2.75f) {
float t1 = t - 2.25f / 2.75f;
return 7.5625f * t1 * t1 + 0.9375f;
} else {
float t1 = t - 2.625f / 2.75f;
return 7.5625f * t1 * t1 + 0.984375f;
}
}
float easeInBounce(float t) {
return 1.0f - easeOutBounce(1.0f - t);
}
float easeInOutBounce(float t) {
if (t < 0.5f) {
return 0.5f * easeInBounce(2.0f * t);
} else {
return 0.5f * easeOutBounce(2.0f * t - 1.0f) + 0.5f;
}
}
// Get easing function by name
EasingFunction getByName(const std::string& name) {
static std::unordered_map<std::string, EasingFunction> easingMap = {
{"linear", linear},
{"easeIn", easeIn},
{"easeOut", easeOut},
{"easeInOut", easeInOut},
{"easeInQuad", easeInQuad},
{"easeOutQuad", easeOutQuad},
{"easeInOutQuad", easeInOutQuad},
{"easeInCubic", easeInCubic},
{"easeOutCubic", easeOutCubic},
{"easeInOutCubic", easeInOutCubic},
{"easeInQuart", easeInQuart},
{"easeOutQuart", easeOutQuart},
{"easeInOutQuart", easeInOutQuart},
{"easeInSine", easeInSine},
{"easeOutSine", easeOutSine},
{"easeInOutSine", easeInOutSine},
{"easeInExpo", easeInExpo},
{"easeOutExpo", easeOutExpo},
{"easeInOutExpo", easeInOutExpo},
{"easeInCirc", easeInCirc},
{"easeOutCirc", easeOutCirc},
{"easeInOutCirc", easeInOutCirc},
{"easeInElastic", easeInElastic},
{"easeOutElastic", easeOutElastic},
{"easeInOutElastic", easeInOutElastic},
{"easeInBack", easeInBack},
{"easeOutBack", easeOutBack},
{"easeInOutBack", easeInOutBack},
{"easeInBounce", easeInBounce},
{"easeOutBounce", easeOutBounce},
{"easeInOutBounce", easeInOutBounce}
};
auto it = easingMap.find(name);
if (it != easingMap.end()) {
return it->second;
}
return linear; // Default to linear
}
} // namespace EasingFunctions
// AnimationManager implementation
AnimationManager& AnimationManager::getInstance() {
static AnimationManager instance;
return instance;
}
void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) {
if (animation && animation->hasValidTarget()) {
if (isUpdating) {
// Defer adding during update to avoid iterator invalidation
pendingAnimations.push_back(animation);
} else {
activeAnimations.push_back(animation);
}
}
}
void AnimationManager::update(float deltaTime) {
// Set flag to defer new animations
isUpdating = true;
// Remove completed or invalid animations
activeAnimations.erase(
std::remove_if(activeAnimations.begin(), activeAnimations.end(),
[deltaTime](std::shared_ptr<Animation>& anim) {
return !anim || !anim->update(deltaTime);
}),
activeAnimations.end()
);
// Clear update flag
isUpdating = false;
// Add any animations that were created during update
if (!pendingAnimations.empty()) {
activeAnimations.insert(activeAnimations.end(),
pendingAnimations.begin(),
pendingAnimations.end());
pendingAnimations.clear();
}
}
void AnimationManager::clear(bool completeAnimations) {
if (completeAnimations) {
// Complete all animations before clearing
for (auto& anim : activeAnimations) {
if (anim) {
anim->complete();
}
}
}
activeAnimations.clear();
}

View File

@ -1,175 +1,50 @@
#pragma once
#include <string>
#include "Common.h"
#include <functional>
#include <memory>
#include <variant>
#include <vector>
#include <SFML/Graphics.hpp>
#include "Python.h"
// Forward declarations
class UIDrawable;
class UIEntity;
// Forward declare namespace
namespace EasingFunctions {
float linear(float t);
}
// Easing function type
typedef std::function<float(float)> EasingFunction;
// Animation target value can be various types
typedef std::variant<
float, // Single float value
int, // Single integer value
std::vector<int>, // List of integers (for sprite animation)
sf::Color, // Color animation
sf::Vector2f, // Vector animation
std::string // String animation (for text)
> AnimationValue;
class Animation {
class Animation
{
protected:
static constexpr float EPSILON = 0.05;
float duration, elapsed;
std::function<void()> callback;
bool loop;
bool complete=false;
public:
// Constructor
Animation(const std::string& targetProperty,
const AnimationValue& targetValue,
float duration,
EasingFunction easingFunc = EasingFunctions::linear,
bool delta = false,
PyObject* callback = nullptr);
// Destructor - cleanup Python callback reference
~Animation();
// Apply this animation to a drawable
void start(std::shared_ptr<UIDrawable> target);
// Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable)
void startEntity(std::shared_ptr<UIEntity> target);
// Complete the animation immediately (jump to final value)
void complete();
// Update animation (called each frame)
// Returns true if animation is still running, false if complete
bool update(float deltaTime);
// Get current interpolated value
AnimationValue getCurrentValue() const;
// Check if animation has valid target
bool hasValidTarget() const;
// Clear the callback (called when PyAnimation is deallocated)
void clearCallback();
// Animation properties
std::string getTargetProperty() const { return targetProperty; }
float getDuration() const { return duration; }
float getElapsed() const { return elapsed; }
bool isComplete() const { return elapsed >= duration; }
bool isDelta() const { return delta; }
private:
std::string targetProperty; // Property name to animate (e.g., "x", "color.r", "sprite_number")
AnimationValue startValue; // Starting value (captured when animation starts)
AnimationValue targetValue; // Target value to animate to
float duration; // Animation duration in seconds
float elapsed = 0.0f; // Elapsed time
EasingFunction easingFunc; // Easing function to use
bool delta; // If true, targetValue is relative to start
// RAII: Use weak_ptr for safe target tracking
std::weak_ptr<UIDrawable> targetWeak;
std::weak_ptr<UIEntity> entityTargetWeak;
// Callback support
PyObject* pythonCallback = nullptr; // Python callback function (we own a reference)
bool callbackTriggered = false; // Ensure callback only fires once
PyObject* pyAnimationWrapper = nullptr; // Weak reference to PyAnimation if created from Python
// Python object cache support
uint64_t serial_number = 0;
// Helper to interpolate between values
AnimationValue interpolate(float t) const;
// Helper to apply value to target
void applyValue(UIDrawable* target, const AnimationValue& value);
void applyValue(UIEntity* entity, const AnimationValue& value);
// Trigger callback when animation completes
void triggerCallback();
//Animation(float, T, T*, std::function<void()>, bool); // lerp
//Animation(float, std::vector<T>, T*, std::function<void()>, bool); // discrete
Animation(float, std::function<void()>, bool);
//Animation() {};
virtual void step(float) = 0;
virtual void cancel() = 0;
bool isDone();
};
// Easing functions library
namespace EasingFunctions {
// Basic easing functions
float linear(float t);
float easeIn(float t);
float easeOut(float t);
float easeInOut(float t);
// Advanced easing functions
float easeInQuad(float t);
float easeOutQuad(float t);
float easeInOutQuad(float t);
float easeInCubic(float t);
float easeOutCubic(float t);
float easeInOutCubic(float t);
float easeInQuart(float t);
float easeOutQuart(float t);
float easeInOutQuart(float t);
float easeInSine(float t);
float easeOutSine(float t);
float easeInOutSine(float t);
float easeInExpo(float t);
float easeOutExpo(float t);
float easeInOutExpo(float t);
float easeInCirc(float t);
float easeOutCirc(float t);
float easeInOutCirc(float t);
float easeInElastic(float t);
float easeOutElastic(float t);
float easeInOutElastic(float t);
float easeInBack(float t);
float easeOutBack(float t);
float easeInOutBack(float t);
float easeInBounce(float t);
float easeOutBounce(float t);
float easeInOutBounce(float t);
// Get easing function by name
EasingFunction getByName(const std::string& name);
}
// Animation manager to handle active animations
class AnimationManager {
template<typename T>
class LerpAnimation: public Animation
{
T startvalue, endvalue;
std::function<void(T)> write;
void lerp();
public:
static AnimationManager& getInstance();
// Add an animation to be managed
void addAnimation(std::shared_ptr<Animation> animation);
// Update all animations
void update(float deltaTime);
// Clear all animations (optionally completing them first)
void clear(bool completeAnimations = false);
private:
AnimationManager() = default;
std::vector<std::shared_ptr<Animation>> activeAnimations;
std::vector<std::shared_ptr<Animation>> pendingAnimations; // Animations to add after update
bool isUpdating = false; // Flag to track if we're in update loop
};
~LerpAnimation() { cancel(); }
LerpAnimation(float, T, T, std::function<void()>, std::function<void(T)>, bool);
//LerpAnimation() {};
void step(float) override final;
void cancel() override final;
};
template<typename T>
class DiscreteAnimation: public Animation
{
std::vector<T> values;
std::function<void(T)> write;
float nonelapsed, timestep;
int index;
public:
DiscreteAnimation(float, std::vector<T>, std::function<void()>, std::function<void(T)>, bool);
DiscreteAnimation() {};
~DiscreteAnimation() { cancel(); }
void step(float) override final;
void cancel() override final;
};

24
src/Button.cpp Normal file
View File

@ -0,0 +1,24 @@
#include "Button.h"
void Button::render(sf::RenderWindow & window)
{
window.draw(rect);
window.draw(caption);
}
Button::Button(int x, int y, int w, int h,
sf::Color _background, sf::Color _textcolor,
const char * _caption, sf::Font & font,
const char * _action)
{
rect.setPosition(sf::Vector2f(x, y));
rect.setSize(sf::Vector2f(w, h));
rect.setFillColor(_background);
caption.setFillColor(_textcolor);
caption.setPosition(sf::Vector2f(x, y));
caption.setString(_caption);
caption.setFont(font);
action = _action;
}

35
src/Button.h Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#include "Common.h"
class Button
{
protected:
public:
// TODO / JankMode: setter & getter for these three fields
// were protected, but directly changing them should be...fine?
sf::RectangleShape rect;
sf::Text caption;
std::string action;
Button() {};
Button(int x, int y, int w, int h,
sf::Color _background, sf::Color _textcolor,
const char * _caption, sf::Font & font,
const char * _action);
void setPosition(sf::Vector2f v) { rect.setPosition(v); caption.setPosition(v); }
void setSize(sf::Vector2f & v) { rect.setSize(v); }
void setBackground(sf::Color c) { rect.setFillColor(c); }
void setCaption(std::string & s) { caption.setString(s); }
void setTextColor(sf::Color c) { caption.setFillColor(c); }
void render(sf::RenderWindow & window);
auto contains(sf::Vector2i p) { return rect.getGlobalBounds().contains(p.x, p.y); }
auto contains(sf::Vector2f rel, sf::Vector2i p) {
return rect.getGlobalBounds().contains(p.x - rel.x, p.y - rel.y);
}
auto getAction() { return action; }
private:
};

View File

@ -1,172 +0,0 @@
#include "CommandLineParser.h"
#include <iostream>
#include <filesystem>
#include <algorithm>
CommandLineParser::CommandLineParser(int argc, char* argv[])
: argc(argc), argv(argv) {}
CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& config) {
ParseResult result;
current_arg = 1; // Reset for each parse
// Detect if running as Python interpreter
std::filesystem::path exec_name = std::filesystem::path(argv[0]).filename();
if (exec_name.string().find("python") == 0) {
config.headless = true;
config.python_mode = true;
}
while (current_arg < argc) {
std::string arg = argv[current_arg];
// Handle Python-style single-letter flags
if (arg == "-h" || arg == "--help") {
print_help();
result.should_exit = true;
result.exit_code = 0;
return result;
}
if (arg == "-V" || arg == "--version") {
print_version();
result.should_exit = true;
result.exit_code = 0;
return result;
}
// Python execution modes
if (arg == "-c") {
config.python_mode = true;
current_arg++;
if (current_arg >= argc) {
std::cerr << "Argument expected for the -c option" << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
config.python_command = argv[current_arg];
current_arg++;
continue;
}
if (arg == "-m") {
config.python_mode = true;
current_arg++;
if (current_arg >= argc) {
std::cerr << "Argument expected for the -m option" << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
config.python_module = argv[current_arg];
current_arg++;
// Collect remaining args as module args
while (current_arg < argc) {
config.script_args.push_back(argv[current_arg]);
current_arg++;
}
continue;
}
if (arg == "-i") {
config.interactive_mode = true;
config.python_mode = true;
current_arg++;
continue;
}
// McRogueFace specific flags
if (arg == "--headless") {
config.headless = true;
config.audio_enabled = false;
current_arg++;
continue;
}
if (arg == "--audio-off") {
config.audio_enabled = false;
current_arg++;
continue;
}
if (arg == "--audio-on") {
config.audio_enabled = true;
current_arg++;
continue;
}
if (arg == "--screenshot") {
config.take_screenshot = true;
current_arg++;
if (current_arg < argc && argv[current_arg][0] != '-') {
config.screenshot_path = argv[current_arg];
current_arg++;
} else {
config.screenshot_path = "screenshot.png";
}
continue;
}
if (arg == "--exec") {
current_arg++;
if (current_arg >= argc) {
std::cerr << "Argument expected for the --exec option" << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
config.exec_scripts.push_back(argv[current_arg]);
config.python_mode = true;
current_arg++;
continue;
}
// If no flags matched, treat as positional argument (script name)
if (arg[0] != '-') {
config.script_path = arg;
config.python_mode = true;
current_arg++;
// Remaining args are script args
while (current_arg < argc) {
config.script_args.push_back(argv[current_arg]);
current_arg++;
}
break;
}
// Unknown flag
std::cerr << "Unknown option: " << arg << std::endl;
result.should_exit = true;
result.exit_code = 1;
return result;
}
return result;
}
void CommandLineParser::print_help() {
std::cout << "usage: mcrogueface [option] ... [-c cmd | -m mod | file | -] [arg] ...\n"
<< "Options:\n"
<< " -c cmd : program passed in as string (terminates option list)\n"
<< " -h : print this help message and exit (also --help)\n"
<< " -i : inspect interactively after running script\n"
<< " -m mod : run library module as a script (terminates option list)\n"
<< " -V : print the Python version number and exit (also --version)\n"
<< "\n"
<< "McRogueFace specific options:\n"
<< " --exec file : execute script before main program (can be used multiple times)\n"
<< " --headless : run without creating a window (implies --audio-off)\n"
<< " --audio-off : disable audio\n"
<< " --audio-on : enable audio (even in headless mode)\n"
<< " --screenshot [path] : take a screenshot in headless mode\n"
<< "\n"
<< "Arguments:\n"
<< " file : program read from script file\n"
<< " - : program read from stdin\n"
<< " arg ...: arguments passed to program in sys.argv[1:]\n";
}
void CommandLineParser::print_version() {
std::cout << "Python 3.12.0 (McRogueFace embedded)\n";
}

View File

@ -1,30 +0,0 @@
#ifndef COMMAND_LINE_PARSER_H
#define COMMAND_LINE_PARSER_H
#include <string>
#include <vector>
#include "McRogueFaceConfig.h"
class CommandLineParser {
public:
struct ParseResult {
bool should_exit = false;
int exit_code = 0;
};
CommandLineParser(int argc, char* argv[]);
ParseResult parse(McRogueFaceConfig& config);
private:
int argc;
char** argv;
int current_arg = 1; // Skip program name
bool has_flag(const std::string& short_flag, const std::string& long_flag = "");
std::string get_next_arg(const std::string& flag_name);
void parse_positional_args(McRogueFaceConfig& config);
void print_help();
void print_version();
};
#endif // COMMAND_LINE_PARSER_H

138
src/Components.h Normal file
View File

@ -0,0 +1,138 @@
#pragma once
#include "Common.h"
#include "IndexSprite.h"
#include "Grid.h"
//#include "Item.h"
#include "Python.h"
#include <list>
class CGrid
{
public:
bool visible;
int x, y;
IndexSprite indexsprite;
Grid* grid;
CGrid(Grid* _g, int _ti, int _si, int _x, int _y, bool _v)
: visible(_v), x(_x), y(_y), grid(_g), indexsprite(_ti, _si, _x, _y, 1.0) {}
};
class CInventory
{
public:
//std::list<std::shared_ptr<Item>>;
int x;
};
class CBehavior
{
public:
PyObject* object;
CBehavior(PyObject* p): object(p) {}
};
/*
class CCombatant
{
public:
int hp;
int maxhp;
}
class CCaster
{
public:
int mp;
int maxmp;
}
class CLevel
{
int constitution; // +HP, resist effects
int strength; // +damage, block/parry
int dexterity; // +speed, dodge
int intelligence; // +MP, spell resist
int wisdom; // +damage, deflect
int luck; // crit, loot
}
*/
/*
class CTransform
{
public:
Vec2 pos = { 0.0, 0.0 };
Vec2 velocity = { 0.0, 0.0 };
float angle = 0;
CTransform(const Vec2 & p, const Vec2 & v, float a)
: pos(p), velocity(v), angle(a) {}
};
*/
/*
class CShape
{
public:
sf::CircleShape circle;
CShape(float radius, int points, const sf::Color & fill, const sf::Color & outline, float thickness)
: circle(radius, points)
{
circle.setFillColor(fill);
circle.setOutlineColor(outline);
circle.setOutlineThickness(thickness);
circle.setOrigin(radius, radius);
}
};
class CCollision
{
public:
float radius = 0;
CCollision(float r)
: radius(r) {}
};
class CScore
{
public:
int score = 0;
CScore(int s)
: score(s) {}
};
class CLifespan
{
public:
int remaining = 0;
int total = 0;
CLifespan(int t)
: remaining(t), total(t) {}
};
class CInput
{
public:
bool up = false;
bool left = false;
bool right = false;
bool down = false;
bool fire = false;
CInput() {}
};
class CSteer
{
public:
sf::Vector2f position;
sf::Vector2f velocity;
float v_max;
float dv_max;
float theta_max;
float dtheta_max;
};
*/

25
src/Entity.cpp Normal file
View File

@ -0,0 +1,25 @@
#include "Entity.h"
Entity::Entity(const size_t i, const std::string & t)
: m_id(i), m_tag(t) {}
bool Entity::isActive() const
{
return m_active;
}
const std::string & Entity::tag() const
{
return m_tag;
}
const size_t Entity::id() const
{
return m_id;
}
void Entity::destroy()
{
m_active = false;
}

35
src/Entity.h Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#include "Common.h"
#include "Components.h"
class Entity
{
friend class EntityManager;
bool m_active = true;
size_t m_id = 0;
std::string m_tag = "default";
//constructor and destructor
Entity(const size_t id, const std::string & t);
public:
// component pointers
//std::shared_ptr<CTransform> cTransform;
//std::shared_ptr<CShape> cShape;
//std::shared_ptr<CCollision> cCollision;
//std::shared_ptr<CInput> cInput;
//std::shared_ptr<CScore> cScore;
//std::shared_ptr<CLifespan> cLifespan;
std::shared_ptr<CGrid> cGrid;
std::shared_ptr<CInventory> cInventory;
std::shared_ptr<CBehavior> cBehavior;
//private member access functions
bool isActive() const;
const std::string & tag() const;
const size_t id() const;
void destroy();
};

73
src/EntityManager.cpp Normal file
View File

@ -0,0 +1,73 @@
#include "EntityManager.h"
EntityManager::EntityManager()
:m_totalEntities(0) {}
void EntityManager::update()
{
//TODO: add entities from m_entitiesToAdd to all vector / tag map
removeDeadEntities(m_entities);
// C++17 way of iterating!
for (auto& [tag, entityVec] : m_entityMap)
{
removeDeadEntities(entityVec);
}
for (auto& e : m_entitiesToAdd)
{
m_entities.push_back(e);
m_entityMap[e->tag()].push_back(e);
}
//if (m_entitiesToAdd.size())
// m_entitiesToAdd.erase(m_entitiesToAdd.begin(), m_entitiesToAdd.end());
m_entitiesToAdd = EntityVec();
}
void EntityManager::removeDeadEntities(EntityVec & vec)
{
EntityVec survivors; // New vector
for (auto& e : m_entities){
if (e->isActive()) survivors.push_back(e); // populate new vector
else if (e->cGrid) { // erase vector from grid
for( auto it = e->cGrid->grid->entities.begin(); it != e->cGrid->grid->entities.end(); it++){
if( *it == e ){
e->cGrid->grid->entities.erase( it );
break;
}
}
}
}
//std::cout << "All entities: " << m_entities.size() << " Survivors: " << survivors.size() << std::endl;
m_entities = survivors; // point to new vector
for (auto& [tag, entityVec] : m_entityMap)
{
EntityVec tag_survivors; // New vector
for (auto& e : m_entityMap[tag])
{
if (e->isActive()) tag_survivors.push_back(e); // populate new vector
}
m_entityMap[tag] = tag_survivors; // point to new vector
//std::cout << tag << " entities: " << m_entityMap[tag].size() << " Survivors: " << tag_survivors.size() << std::endl;
}
}
std::shared_ptr<Entity> EntityManager::addEntity(const std::string & tag)
{
// create the entity shared pointer
auto entity = std::shared_ptr<Entity>(new Entity(m_totalEntities++, tag));
m_entitiesToAdd.push_back(entity);
return entity;
}
const EntityVec & EntityManager::getEntities()
{
return m_entities;
}
const EntityVec & EntityManager::getEntities(const std::string & tag)
{
return m_entityMap[tag];
}

25
src/EntityManager.h Normal file
View File

@ -0,0 +1,25 @@
#pragma once
#include "Common.h"
#include "Entity.h"
typedef std::vector<std::shared_ptr<Entity>> EntityVec;
typedef std::map<std::string, EntityVec> EntityMap;
class EntityManager
{
EntityVec m_entities;
EntityVec m_entitiesToAdd;
EntityMap m_entityMap;
size_t m_totalEntities;
void removeDeadEntities(EntityVec & vec);
public:
EntityManager();
void update();
std::shared_ptr<Entity> addEntity(const std::string & tag);
const EntityVec & getEntities();
const EntityVec & getEntities(const std::string & tag);
};

View File

@ -1,381 +1,123 @@
#include "GameEngine.h"
#include "MenuScene.h"
//#include "UITestScene.h"
#include "ActionCode.h"
#include "McRFPy_API.h"
#include "PyScene.h"
#include "PythonScene.h"
#include "UITestScene.h"
#include "Resources.h"
#include "Animation.h"
#include "Timer.h"
#include <cmath>
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
{
}
GameEngine::GameEngine(const McRogueFaceConfig& cfg)
: config(cfg), headless(cfg.headless)
GameEngine::GameEngine()
{
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
Resources::game = this;
window_title = "McRogueFace Engine";
// Initialize rendering based on headless mode
if (headless) {
headless_renderer = std::make_unique<HeadlessRenderer>();
if (!headless_renderer->init(1024, 768)) {
throw std::runtime_error("Failed to initialize headless renderer");
}
render_target = &headless_renderer->getRenderTarget();
} else {
window = std::make_unique<sf::RenderWindow>();
window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close | sf::Style::Resize);
window->setFramerateLimit(60);
render_target = window.get();
}
visible = render_target->getDefaultView();
// Initialize the game view
gameView.setSize(static_cast<float>(gameResolution.x), static_cast<float>(gameResolution.y));
// Use integer center coordinates for pixel-perfect rendering
gameView.setCenter(std::floor(gameResolution.x / 2.0f), std::floor(gameResolution.y / 2.0f));
updateViewport();
window_title = "McRogueFace - r/RoguelikeDev Tutorial Run";
window.create(sf::VideoMode(1024, 768), window_title);
visible = window.getDefaultView();
window.setFramerateLimit(30);
scene = "uitest";
//std::cout << "Constructing MenuScene" << std::endl;
scenes["menu"] = new MenuScene(this);
scenes["uitest"] = new UITestScene(this);
//std::cout << "Constructed MenuScene" <<std::endl;
//scenes["play"] = new UITestScene(this);
//api = new McRFPy_API(this);
McRFPy_API::game = this;
// Only load game.py if no custom script/command/module/exec is specified
bool should_load_game = config.script_path.empty() &&
config.python_command.empty() &&
config.python_module.empty() &&
config.exec_scripts.empty() &&
!config.interactive_mode &&
!config.python_mode;
if (should_load_game) {
if (!Py_IsInitialized()) {
McRFPy_API::api_init();
}
McRFPy_API::executePyString("import mcrfpy");
McRFPy_API::executeScript("scripts/game.py");
}
// Execute any --exec scripts in order
if (!config.exec_scripts.empty()) {
if (!Py_IsInitialized()) {
McRFPy_API::api_init();
}
McRFPy_API::executePyString("import mcrfpy");
for (const auto& exec_script : config.exec_scripts) {
std::cout << "Executing script: " << exec_script << std::endl;
McRFPy_API::executeScript(exec_script.string());
}
std::cout << "All --exec scripts completed" << std::endl;
}
McRFPy_API::api_init();
McRFPy_API::executePyString("import mcrfpy");
//McRFPy_API::executePyString("from UIMenu import *");
//McRFPy_API::executePyString("from Grid import *");
//scenes["py"] = new PythonScene(this, "TestScene");
IndexSprite::game = this;
clock.restart();
runtime.restart();
}
GameEngine::~GameEngine()
{
cleanup();
for (auto& [name, scene] : scenes) {
delete scene;
}
}
void GameEngine::cleanup()
{
if (cleaned_up) return;
cleaned_up = true;
// Clear all animations first (RAII handles invalidation)
AnimationManager::getInstance().clear();
// Clear Python references before destroying C++ objects
// Clear all timers (they hold Python callables)
timers.clear();
// Clear McRFPy_API's reference to this game engine
if (McRFPy_API::game == this) {
McRFPy_API::game = nullptr;
}
// Force close the window if it's still open
if (window && window->isOpen()) {
window->close();
}
}
Scene* GameEngine::currentScene() { return scenes[scene]; }
void GameEngine::changeScene(std::string s)
{
changeScene(s, TransitionType::None, 0.0f);
}
void GameEngine::changeScene(std::string sceneName, TransitionType transitionType, float duration)
{
if (scenes.find(sceneName) == scenes.end())
{
std::cout << "Attempted to change to a scene that doesn't exist (`" << sceneName << "`)" << std::endl;
return;
}
if (transitionType == TransitionType::None || duration <= 0.0f)
{
// Immediate scene change
std::string old_scene = scene;
scene = sceneName;
// Trigger Python scene lifecycle events
McRFPy_API::triggerSceneChange(old_scene, sceneName);
}
else
{
// Start transition
transition.start(transitionType, scene, sceneName, duration);
// Render current scene to texture
sf::RenderTarget* original_target = render_target;
render_target = transition.oldSceneTexture.get();
transition.oldSceneTexture->clear();
currentScene()->render();
transition.oldSceneTexture->display();
// Change to new scene
std::string old_scene = scene;
scene = sceneName;
// Render new scene to texture
render_target = transition.newSceneTexture.get();
transition.newSceneTexture->clear();
currentScene()->render();
transition.newSceneTexture->display();
// Restore original render target and scene
render_target = original_target;
scene = old_scene;
}
}
void GameEngine::changeScene(std::string s) { std::cout << "Current scene is now '" << s << "'\n"; scene = s; }
void GameEngine::quit() { running = false; }
void GameEngine::setPause(bool p) { paused = p; }
sf::Font & GameEngine::getFont() { /*return font; */ return Resources::font; }
sf::RenderWindow & GameEngine::getWindow() {
if (!window) {
throw std::runtime_error("Window not available in headless mode");
}
return *window;
}
sf::RenderTarget & GameEngine::getRenderTarget() {
return *render_target;
}
void GameEngine::createScene(std::string s) { scenes[s] = new PyScene(this); }
void GameEngine::setWindowScale(float multiplier)
{
if (!headless && window) {
window->setSize(sf::Vector2u(gameResolution.x * multiplier, gameResolution.y * multiplier));
updateViewport();
}
}
sf::RenderWindow & GameEngine::getWindow() { return window; }
void GameEngine::run()
{
//std::cout << "GameEngine::run() starting main loop..." << std::endl;
float fps = 0.0;
frameTime = 0.016f; // Initialize to ~60 FPS
clock.restart();
while (running)
{
// Reset per-frame metrics
metrics.resetPerFrame();
currentScene()->update();
testTimers();
// Update Python scenes
McRFPy_API::updatePythonScenes(frameTime);
// Update animations (only if frameTime is valid)
if (frameTime > 0.0f && frameTime < 1.0f) {
AnimationManager::getInstance().update(frameTime);
}
if (!headless) {
sUserInput();
}
sUserInput();
if (!paused)
{
}
// Handle scene transitions
if (transition.type != TransitionType::None)
{
transition.update(frameTime);
if (transition.isComplete())
{
// Transition complete - finalize scene change
scene = transition.toScene;
transition.type = TransitionType::None;
// Trigger Python scene lifecycle events
McRFPy_API::triggerSceneChange(transition.fromScene, transition.toScene);
}
else
{
// Render transition
render_target->clear();
transition.render(*render_target);
}
}
else
{
// Normal scene rendering
currentScene()->render();
}
// Display the frame
if (headless) {
headless_renderer->display();
// Take screenshot if requested
if (config.take_screenshot) {
headless_renderer->saveScreenshot(config.screenshot_path.empty() ? "screenshot.png" : config.screenshot_path);
config.take_screenshot = false; // Only take one screenshot
}
} else {
window->display();
}
currentScene()->sRender();
currentFrame++;
frameTime = clock.restart().asSeconds();
fps = 1 / frameTime;
// Update profiling metrics
metrics.updateFrameTime(frameTime * 1000.0f); // Convert to milliseconds
int whole_fps = metrics.fps;
int tenth_fps = (metrics.fps * 10) % 10;
if (!headless && window) {
window->setTitle(window_title);
}
// In windowed mode, check if window was closed
if (!headless && window && !window->isOpen()) {
running = false;
}
}
// Clean up before exiting the run loop
cleanup();
}
std::shared_ptr<Timer> GameEngine::getTimer(const std::string& name)
{
auto it = timers.find(name);
if (it != timers.end()) {
return it->second;
}
return nullptr;
}
void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
{
auto it = timers.find(name);
if (it != timers.end()) // overwrite existing
{
if (target == NULL || target == Py_None)
{
// Delete: Overwrite existing timer with one that calls None. This will be deleted in the next timer check
// see gitea issue #4: this allows for a timer to be deleted during its own call to itself
timers[name] = std::make_shared<Timer>(Py_None, 1000, runtime.getElapsedTime().asMilliseconds());
return;
}
}
if (target == NULL || target == Py_None)
{
std::cout << "Refusing to initialize timer to None. It's not an error, it's just pointless." << std::endl;
return;
}
timers[name] = std::make_shared<Timer>(target, interval, runtime.getElapsedTime().asMilliseconds());
}
void GameEngine::testTimers()
{
int now = runtime.getElapsedTime().asMilliseconds();
auto it = timers.begin();
while (it != timers.end())
{
it->second->test(now);
// Remove timers that have been cancelled or are one-shot and fired
if (!it->second->getCallback() || it->second->getCallback() == Py_None)
{
it = timers.erase(it);
}
else
it++;
}
}
void GameEngine::processEvent(const sf::Event& event)
{
std::string actionType;
int actionCode = 0;
if (event.type == sf::Event::Closed) { running = false; return; }
// Handle window resize events
else if (event.type == sf::Event::Resized) {
// Update the viewport to handle the new window size
updateViewport();
// Notify Python scenes about the resize
McRFPy_API::triggerResize(event.size.width, event.size.height);
}
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start";
else if (event.type == sf::Event::KeyReleased || event.type == sf::Event::MouseButtonReleased) actionType = "end";
if (event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseButtonReleased)
actionCode = ActionCode::keycode(event.mouseButton.button);
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)
actionCode = ActionCode::keycode(event.key.code);
else if (event.type == sf::Event::MouseWheelScrolled)
{
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
{
int delta = 1;
if (event.mouseWheelScroll.delta < 0) delta = -1;
actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta );
}
}
else
return;
if (currentScene()->hasAction(actionCode))
{
std::string name = currentScene()->action(actionCode);
currentScene()->doAction(name, actionType);
}
else if (currentScene()->key_callable &&
(event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased))
{
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
window.setTitle(window_title + " " + std::to_string(fps) + " FPS");
}
}
void GameEngine::sUserInput()
{
sf::Event event;
while (window && window->pollEvent(event))
while (window.pollEvent(event))
{
processEvent(event);
std::string actionType;
int actionCode = 0;
if (event.type == sf::Event::Closed) { running = false; continue; }
else if (event.type == sf::Event::Resized) {
sf::FloatRect area(0.f, 0.f, event.size.width, event.size.height);
visible = sf::View(area);
window.setView(visible);
//std::cout << "Visible area set to (0, 0, " << event.size.width << ", " << event.size.height <<")"<<std::endl;
actionType = "resize";
}
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start";
else if (event.type == sf::Event::KeyReleased || event.type == sf::Event::MouseButtonReleased) actionType = "end";
if (event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseButtonReleased)
actionCode = ActionCode::keycode(event.mouseButton.button);
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)
actionCode = ActionCode::keycode(event.key.code);
else if (event.type == sf::Event::MouseWheelScrolled)
{
// //sf::Mouse::Wheel w = event.MouseWheelScrollEvent.wheel;
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
{
int delta = 1;
if (event.mouseWheelScroll.delta < 0) delta = -1;
actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta );
/*
std::cout << "[GameEngine] Generated MouseWheel code w(" << (int)event.mouseWheelScroll.wheel << ") d(" << event.mouseWheelScroll.delta << ") D(" << delta << ") = " << actionCode << std::endl;
std::cout << " test decode: isMouseWheel=" << ActionCode::isMouseWheel(actionCode) << ", wheel=" << ActionCode::wheel(actionCode) << ", delta=" << ActionCode::delta(actionCode) << std::endl;
std::cout << " math test: actionCode && WHEEL_NEG -> " << (actionCode && ActionCode::WHEEL_NEG) << "; actionCode && WHEEL_DEL -> " << (actionCode && ActionCode::WHEEL_DEL) << ";" << std::endl;
*/
}
// float d = event.MouseWheelScrollEvent.delta;
// actionCode = ActionCode::keycode(0, d);
}
else
continue;
//std::cout << "Event produced action code " << actionCode << ": " << actionType << std::endl;
if (currentScene()->hasAction(actionCode))
{
std::string name = currentScene()->action(actionCode);
currentScene()->doAction(name, actionType);
}
else
{
//std::cout << "[GameEngine] Action not registered for input: " << actionCode << ": " << actionType << std::endl;
}
}
}
@ -396,123 +138,3 @@ std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> GameEngine::scene_ui(s
if (scenes.count(target) == 0) return NULL;
return scenes[target]->ui_elements;
}
void GameEngine::setWindowTitle(const std::string& title)
{
window_title = title;
if (!headless && window) {
window->setTitle(title);
}
}
void GameEngine::setVSync(bool enabled)
{
vsync_enabled = enabled;
if (!headless && window) {
window->setVerticalSyncEnabled(enabled);
}
}
void GameEngine::setFramerateLimit(unsigned int limit)
{
framerate_limit = limit;
if (!headless && window) {
window->setFramerateLimit(limit);
}
}
void GameEngine::setGameResolution(unsigned int width, unsigned int height) {
gameResolution = sf::Vector2u(width, height);
gameView.setSize(static_cast<float>(width), static_cast<float>(height));
// Use integer center coordinates for pixel-perfect rendering
gameView.setCenter(std::floor(width / 2.0f), std::floor(height / 2.0f));
updateViewport();
}
void GameEngine::setViewportMode(ViewportMode mode) {
viewportMode = mode;
updateViewport();
}
std::string GameEngine::getViewportModeString() const {
switch (viewportMode) {
case ViewportMode::Center: return "center";
case ViewportMode::Stretch: return "stretch";
case ViewportMode::Fit: return "fit";
}
return "unknown";
}
void GameEngine::updateViewport() {
if (!render_target) return;
auto windowSize = render_target->getSize();
switch (viewportMode) {
case ViewportMode::Center: {
// 1:1 pixels, centered in window
float viewportWidth = std::min(static_cast<float>(gameResolution.x), static_cast<float>(windowSize.x));
float viewportHeight = std::min(static_cast<float>(gameResolution.y), static_cast<float>(windowSize.y));
// Floor offsets to ensure integer pixel alignment
float offsetX = std::floor((windowSize.x - viewportWidth) / 2.0f);
float offsetY = std::floor((windowSize.y - viewportHeight) / 2.0f);
gameView.setViewport(sf::FloatRect(
offsetX / windowSize.x,
offsetY / windowSize.y,
viewportWidth / windowSize.x,
viewportHeight / windowSize.y
));
break;
}
case ViewportMode::Stretch: {
// Fill entire window, ignore aspect ratio
gameView.setViewport(sf::FloatRect(0, 0, 1, 1));
break;
}
case ViewportMode::Fit: {
// Maintain aspect ratio with black bars
float windowAspect = static_cast<float>(windowSize.x) / windowSize.y;
float gameAspect = static_cast<float>(gameResolution.x) / gameResolution.y;
float viewportWidth, viewportHeight;
float offsetX = 0, offsetY = 0;
if (windowAspect > gameAspect) {
// Window is wider - black bars on sides
// Calculate viewport size in pixels and floor for pixel-perfect scaling
float pixelHeight = static_cast<float>(windowSize.y);
float pixelWidth = std::floor(pixelHeight * gameAspect);
viewportHeight = 1.0f;
viewportWidth = pixelWidth / windowSize.x;
offsetX = (1.0f - viewportWidth) / 2.0f;
} else {
// Window is taller - black bars on top/bottom
// Calculate viewport size in pixels and floor for pixel-perfect scaling
float pixelWidth = static_cast<float>(windowSize.x);
float pixelHeight = std::floor(pixelWidth / gameAspect);
viewportWidth = 1.0f;
viewportHeight = pixelHeight / windowSize.y;
offsetY = (1.0f - viewportHeight) / 2.0f;
}
gameView.setViewport(sf::FloatRect(offsetX, offsetY, viewportWidth, viewportHeight));
break;
}
}
// Apply the view
render_target->setView(gameView);
}
sf::Vector2f GameEngine::windowToGameCoords(const sf::Vector2f& windowPos) const {
if (!render_target) return windowPos;
// Convert window coordinates to game coordinates using the view
return render_target->mapPixelToCoords(sf::Vector2i(windowPos), gameView);
}

View File

@ -1,32 +1,17 @@
#pragma once
#include "Common.h"
#include "Entity.h"
#include "EntityManager.h"
#include "Scene.h"
#include "McRFPy_API.h"
#include "IndexTexture.h"
#include "Timer.h"
#include "PyCallable.h"
#include "McRogueFaceConfig.h"
#include "HeadlessRenderer.h"
#include "SceneTransition.h"
#include <memory>
class GameEngine
{
public:
// Viewport modes (moved here so private section can use it)
enum class ViewportMode {
Center, // 1:1 pixels, viewport centered in window
Stretch, // viewport size = window size, doesn't respect aspect ratio
Fit // maintains original aspect ratio, leaves black bars
};
private:
std::unique_ptr<sf::RenderWindow> window;
std::unique_ptr<HeadlessRenderer> headless_renderer;
sf::RenderTarget* render_target;
sf::RenderWindow window;
sf::Font font;
std::string scene;
std::map<std::string, Scene*> scenes;
bool running = true;
bool paused = false;
@ -35,106 +20,20 @@ private:
sf::Clock clock;
float frameTime;
std::string window_title;
bool headless = false;
McRogueFaceConfig config;
bool cleaned_up = false;
// Window state tracking
bool vsync_enabled = false;
unsigned int framerate_limit = 60;
// Scene transition state
SceneTransition transition;
// Viewport system
sf::Vector2u gameResolution{1024, 768}; // Fixed game resolution
sf::View gameView; // View for the game content
ViewportMode viewportMode = ViewportMode::Fit;
void updateViewport();
void testTimers();
public:
sf::Clock runtime;
std::map<std::string, std::shared_ptr<Timer>> timers;
std::string scene;
// Profiling metrics
struct ProfilingMetrics {
float frameTime = 0.0f; // Current frame time in milliseconds
float avgFrameTime = 0.0f; // Average frame time over last N frames
int fps = 0; // Frames per second
int drawCalls = 0; // Draw calls per frame
int uiElements = 0; // Number of UI elements rendered
int visibleElements = 0; // Number of visible elements
// Frame time history for averaging
static constexpr int HISTORY_SIZE = 60;
float frameTimeHistory[HISTORY_SIZE] = {0};
int historyIndex = 0;
void updateFrameTime(float deltaMs) {
frameTime = deltaMs;
frameTimeHistory[historyIndex] = deltaMs;
historyIndex = (historyIndex + 1) % HISTORY_SIZE;
// Calculate average
float sum = 0.0f;
for (int i = 0; i < HISTORY_SIZE; ++i) {
sum += frameTimeHistory[i];
}
avgFrameTime = sum / HISTORY_SIZE;
fps = avgFrameTime > 0 ? static_cast<int>(1000.0f / avgFrameTime) : 0;
}
void resetPerFrame() {
drawCalls = 0;
uiElements = 0;
visibleElements = 0;
}
} metrics;
GameEngine();
GameEngine(const McRogueFaceConfig& cfg);
~GameEngine();
Scene* currentScene();
void changeScene(std::string);
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
void createScene(std::string);
void quit();
void setPause(bool);
sf::Font & getFont();
sf::RenderWindow & getWindow();
sf::RenderTarget & getRenderTarget();
sf::RenderTarget* getRenderTargetPtr() { return render_target; }
void run();
void sUserInput();
void cleanup(); // Clean up Python references before destruction
int getFrame() { return currentFrame; }
float getFrameTime() { return frameTime; }
sf::View getView() { return visible; }
void manageTimer(std::string, PyObject*, int);
std::shared_ptr<Timer> getTimer(const std::string& name);
void setWindowScale(float);
bool isHeadless() const { return headless; }
void processEvent(const sf::Event& event);
// Window property accessors
const std::string& getWindowTitle() const { return window_title; }
void setWindowTitle(const std::string& title);
bool getVSync() const { return vsync_enabled; }
void setVSync(bool enabled);
unsigned int getFramerateLimit() const { return framerate_limit; }
void setFramerateLimit(unsigned int limit);
// Viewport system
void setGameResolution(unsigned int width, unsigned int height);
sf::Vector2u getGameResolution() const { return gameResolution; }
void setViewportMode(ViewportMode mode);
ViewportMode getViewportMode() const { return viewportMode; }
std::string getViewportModeString() const;
sf::Vector2f windowToGameCoords(const sf::Vector2f& windowPos) const;
// global textures for scripts to access
std::vector<IndexTexture> textures;

340
src/Grid.cpp Normal file
View File

@ -0,0 +1,340 @@
#include "Grid.h"
#include <cmath>
#include "Entity.h"
GridPoint::GridPoint():
color(0, 0, 0, 0), walkable(false), tilesprite(-1), transparent(false), visible(false), discovered(false), color_overlay(0,0,0,255), tile_overlay(-1), uisprite(-1)
{};
void Grid::setSprite(int ti)
{
int tx = ti % texture_width, ty = ti / texture_width;
sprite.setTextureRect(sf::IntRect(tx * grid_size, ty * grid_size, grid_size, grid_size));
}
Grid::Grid(int gx, int gy, int gs, int _x, int _y, int _w, int _h):
grid_size(gs),
grid_x(gx), grid_y(gy),
zoom(1.0f), center_x((gx/2) * gs), center_y((gy/2) * gs),
texture_width(12), texture_height(11), visible(false)
{
//grid_size = gs;
//zoom = 1.0f;
//grid_x = gx;
//grid_y = gy;
tcodmap = new TCODMap(gx, gy);
points.resize(gx*gy);
box.setSize(sf::Vector2f(_w, _h));
box.setPosition(sf::Vector2f(_x, _y));
box.setFillColor(sf::Color(0,0,0,0));
renderTexture.create(_w, _h);
texture.loadFromFile("./assets/kenney_tinydungeon.png");
texture.setSmooth(false);
sprite.setTexture(texture);
//output.setSize(box.getSize());
output.setTextureRect(
sf::IntRect(0, 0,
box.getSize().x, box.getSize().y));
output.setPosition(box.getPosition());
// textures are upside-down inside renderTexture
output.setTexture(renderTexture.getTexture());
// Show one texture at a time
sprite.setTexture(texture);
}
void Grid::refreshTCODmap() {
//int total = 0, walkable = 0, transparent = 0;
for (int x = 0; x < grid_x; x++) {
for (int y = 0; y < grid_y; y++) {
auto p = at(x, y);
//total++; if (p.walkable) walkable++; if (p.transparent) transparent++;
tcodmap->setProperties(x, y, p.transparent, p.walkable);
}
}
//std::cout << "Map refreshed: " << total << " squares, " << walkable << "walkable, " << transparent << " transparent" << std::endl;
}
void Grid::refreshTCODsight(int x, int y) {
tcodmap->computeFov(x,y, 0, true, FOV_PERMISSIVE_8);
for (int x = 0; x < grid_x; x++) {
for (int y = 0; y < grid_y; y++) {
auto& p = at(x, y);
if (p.visible && !tcodmap->isInFov(x, y)) {
p.discovered = true;
p.visible = false;
} else if (!p.visible && tcodmap->isInFov(x,y)) {
p.discovered = true;
p.visible = true;
}
}
}
}
bool Grid::inBounds(int x, int y) {
return (x >= 0 && y >= 0 && x < grid_x && y < grid_y);
}
void Grid::screenToGrid(int sx, int sy, int& gx, int& gy) {
float center_x_sq = center_x / grid_size;
float center_y_sq = center_y / grid_size;
float width_sq = box.getSize().x / (grid_size * zoom);
float height_sq = box.getSize().y / (grid_size * zoom);
float left_edge = center_x_sq - (width_sq / 2.0);
float right_edge = center_x_sq + (width_sq / 2.0);
float top_edge = center_y_sq - (height_sq / 2.0);
float bottom_edge = center_y_sq + (height_sq / 2.0);
float grid_px = zoom * grid_size;
//std::cout << "##############################" <<
// "\nscreen coord: (" << sx << ", " << sy << ")" << std::endl;
sx -= box.getPosition().x;
sy -= box.getPosition().y;
//std::cout << "box coord: (" << sx << ", " << sy << ")" << std::endl;
float mouse_x_sq = sx / grid_px;
float mouse_y_sq = sy / grid_px;
float ans_x = mouse_x_sq + left_edge;
float ans_y = mouse_y_sq + top_edge;
// compare integer method with this (mostly working) one
//int diff_realpixel_x = box.getSize().x / 2.0 - sx;
//int diff_realpixel_y = box.getSize().y / 2.0 - sy;
int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom);
int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom);
std::cout << "Float method got ans (" << ans_x << ", " << ans_y << ")"
<< std::endl << "Int method px (" << left_spritepixels + (sx/zoom) << ", " <<
top_spritepixels + (sy/zoom) << ")" << std::endl <<
"Int grid (" << (left_spritepixels + (sx/zoom) ) / grid_size <<
", " << (top_spritepixels + (sy/zoom)) / grid_size << ")" <<
std::endl;
// casting float turns -0.5 to 0; I want any negative coord to be OOB
if (ans_x < 0) ans_x = -1;
if (ans_y < 0) ans_y = -1;
gx = ans_x;
gy = ans_y;
/*
std::cout <<
"C: (" << center_x << ", " << center_y << ")" << std::endl <<
"W: " << width_sq << " H: " << height_sq << std::endl <<
"L: " << left_edge << " T: " << top_edge << std::endl <<
"R: " << right_edge << " B: " << bottom_edge << std::endl <<
"Grid Px: " << grid_px << "( zoom: " << zoom << ")" << std::endl <<
"answer: G(" << ans_x << ", " << ans_y << ")" << std::endl <<
"##############################" <<
std::endl;
*/
}
void Grid::renderPxToGrid(int sx, int sy, int& gx, int& gy) {
// just like screen px coversion, but no offset by grid's position
float center_x_sq = center_x / grid_size;
float center_y_sq = center_y / grid_size;
float width_sq = box.getSize().x / (grid_size * zoom);
float height_sq = box.getSize().y / (grid_size * zoom);
int width_px = box.getSize().x / 2.0;
int height_px = box.getSize().y / 2.0;
float left_edge = center_x_sq - (width_sq / 2.0);
float top_edge = center_y_sq - (height_sq / 2.0);
float grid_px = zoom * grid_size;
float sx_sq = sx / grid_px;
float sy_sq = sy / grid_px;
float ans_x = sx_sq + left_edge;
float ans_y = sy_sq + top_edge;
if (ans_x < 0) ans_x = -1;
if (ans_y < 0) ans_y = -1;
gx = ans_x;
gy = ans_y;
}
void Grid::integerGrid(float fx, float fy, int& gx, int& gy) {
if (fx < 0) fx -= 0.5;
if (fy < 0) fy -= 0.5;
gx = fx;
gy = fy;
}
void Grid::gridToRenderPx(int gx, int gy, int& sx, int& sy) {
// integer grid square (gx, gy) - what pixel (on rendertexture)
// should it's top left corner be at (the sprite's position)
// eff_gridsize = grid_size * zoom
// if center_x = 161, and grid_size is 16, that's 10 + 1/16 sprites
// center_x - (box.getSize().x / 2 / zoom) = left edge (in px)
// gx * eff_gridsize = pixel position without panning
// pixel_gx - left_edge = grid's render position
sx = (gx * grid_size * zoom) - (center_x - (box.getSize().x / 2.0 / zoom));
sy = (gy * grid_size * zoom) - (center_y - (box.getSize().y / 2.0 / zoom));
}
void Grid::render(sf::RenderWindow & window)
{
renderTexture.clear();
//renderTexture.draw(box);
// sprites that are visible according to zoom, center_x, center_y, and box width
float center_x_sq = center_x / grid_size;
float center_y_sq = center_y / grid_size;
float width_sq = box.getSize().x / (grid_size * zoom);
float height_sq = box.getSize().y / (grid_size * zoom);
float left_edge = center_x_sq - (width_sq / 2.0);
//float right_edge = center_x_sq + (width_sq / 2.0);
float top_edge = center_y_sq - (height_sq / 2.0);
//float bottom_edge = center_y_sq + (height_sq / 2.0);
int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom);
int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom);
sprite.setScale(sf::Vector2f(zoom, zoom));
sf::RectangleShape r; // for colors and overlays
r.setSize(sf::Vector2f(grid_size * zoom, grid_size * zoom));
r.setOutlineThickness(0);
int x_limit = left_edge + width_sq + 2;
if (x_limit > grid_x) x_limit = grid_x;
int y_limit = top_edge + height_sq + 2;
if (y_limit > grid_y) y_limit = grid_y;
//for (float x = (left_edge >= 0 ? left_edge : 0);
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
x < x_limit; //x < view_width;
x+=1)
{
//for (float y = (top_edge >= 0 ? top_edge : 0);
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
y < y_limit; //y < view_height;
y+=1)
{
// Converting everything to integer pixels to avoid jitter
//auto pixel_pos = sf::Vector2f(
// (x - left_edge) * (zoom * grid_size),
// (y - top_edge) * (zoom * grid_size));
// This failed horribly:
//int gx, gy; integerGrid(x, y, gx, gy);
//int px_x, px_y; gridToRenderPx(gx, gy, px_x, px_y);
//auto pixel_pos = sf::Vector2f(px_x, px_y);
// this draws coherently, but the coordinates
// don't match up with the mouse cursor function
// jitter not eliminated
auto pixel_pos = sf::Vector2f(
(x*grid_size - left_spritepixels) * zoom,
(y*grid_size - top_spritepixels) * zoom );
auto gridpoint = at(std::floor(x), std::floor(y));
sprite.setPosition(pixel_pos);
r.setPosition(pixel_pos);
r.setFillColor(gridpoint.color);
renderTexture.draw(r);
// tilesprite
// if discovered but not visible, set opacity to 90%
// if not discovered... just don't draw it?
if (gridpoint.tilesprite != -1) {
setSprite(gridpoint.tilesprite);
renderTexture.draw(sprite);
}
}
}
for (auto e : entities) {
auto drawent = e->cGrid->indexsprite.drawable();
drawent.setScale(zoom, zoom);
auto pixel_pos = sf::Vector2f(
(drawent.getPosition().x*grid_size - left_spritepixels) * zoom,
(drawent.getPosition().y*grid_size - top_spritepixels) * zoom );
drawent.setPosition(pixel_pos);
renderTexture.draw(drawent);
}
// loop again and draw on top of entities
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
x < x_limit; //x < view_width;
x+=1)
{
//for (float y = (top_edge >= 0 ? top_edge : 0);
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
y < y_limit; //y < view_height;
y+=1)
{
auto pixel_pos = sf::Vector2f(
(x*grid_size - left_spritepixels) * zoom,
(y*grid_size - top_spritepixels) * zoom );
auto gridpoint = at(std::floor(x), std::floor(y));
sprite.setPosition(pixel_pos);
r.setPosition(pixel_pos);
// visible & discovered layers for testing purposes
if (!gridpoint.discovered) {
r.setFillColor(sf::Color(16, 16, 20, 192)); // 255 opacity for actual blackout
renderTexture.draw(r);
} else if (!gridpoint.visible) {
r.setFillColor(sf::Color(32, 32, 40, 128));
renderTexture.draw(r);
}
// overlay
// uisprite
}
}
// grid lines for testing & validation
/*
sf::Vertex line[] =
{
sf::Vertex(sf::Vector2f(0, 0), sf::Color::Red),
sf::Vertex(box.getSize(), sf::Color::Red),
};
renderTexture.draw(line, 2, sf::Lines);
sf::Vertex lineb[] =
{
sf::Vertex(sf::Vector2f(0, box.getSize().y), sf::Color::Blue),
sf::Vertex(sf::Vector2f(box.getSize().x, 0), sf::Color::Blue),
};
renderTexture.draw(lineb, 2, sf::Lines);
*/
// render to window
renderTexture.display();
window.draw(output);
}
GridPoint& Grid::at(int x, int y)
{
return points[y * grid_x + x];
}

56
src/Grid.h Normal file
View File

@ -0,0 +1,56 @@
#pragma once
#include "Common.h"
#include "libtcod.h"
//#include "Entity.h"
class Entity; // forward declare
class GridPoint
{
public:
// Layers: color, walkable, tilesprite, transparent, visible, discovered, overlay, uisprite
sf::Color color;
bool walkable;
int tilesprite;
bool transparent, visible, discovered;
sf::Color color_overlay;
int tile_overlay, uisprite;
GridPoint();
};
class Grid
{
private:
public:
Grid();
sf::RectangleShape box; // view on window
bool visible;
sf::Texture texture;
sf::Sprite sprite, output;
sf::RenderTexture renderTexture;
TCODMap* tcodmap;
void setSprite(int);
const int texture_width, texture_height;
auto contains(sf::Vector2i p) { return box.getGlobalBounds().contains(p.x, p.y); }
Grid(int gx, int gy, int gs, int _x, int _y, int _w, int _h);
int grid_x, grid_y; // rectangle map size (integer - sprites)
int grid_size; // pixel size of 1 sprite
float zoom;
int center_x, center_y; // center in 1.0x Pixels
std::vector<GridPoint> points; // grid visible contents
std::vector<std::shared_ptr<Entity>> entities;
void render(sf::RenderWindow&); // draw to screen
GridPoint& at(int, int);
bool inBounds(int, int);
void screenToGrid(int, int, int&, int&);
void renderPxToGrid(int, int, int&, int&);
void gridToRenderPx(int, int, int&, int&);
void integerGrid(float, float, int&, int&);
void refreshTCODmap();
void refreshTCODsight(int, int);
TCODDijkstra *dijkstra; //= new TCODDijkstra(myMap);
};

View File

@ -1,27 +0,0 @@
#include "HeadlessRenderer.h"
#include <iostream>
bool HeadlessRenderer::init(int width, int height) {
if (!render_texture.create(width, height)) {
std::cerr << "Failed to create headless render texture" << std::endl;
return false;
}
return true;
}
sf::RenderTarget& HeadlessRenderer::getRenderTarget() {
return render_texture;
}
void HeadlessRenderer::saveScreenshot(const std::string& path) {
sf::Image screenshot = render_texture.getTexture().copyToImage();
if (!screenshot.saveToFile(path)) {
std::cerr << "Failed to save screenshot to: " << path << std::endl;
} else {
std::cout << "Screenshot saved to: " << path << std::endl;
}
}
void HeadlessRenderer::display() {
render_texture.display();
}

View File

@ -1,20 +0,0 @@
#ifndef HEADLESS_RENDERER_H
#define HEADLESS_RENDERER_H
#include <SFML/Graphics.hpp>
#include <memory>
#include <string>
class HeadlessRenderer {
private:
sf::RenderTexture render_texture;
public:
bool init(int width = 1024, int height = 768);
sf::RenderTarget& getRenderTarget();
void saveScreenshot(const std::string& path);
void display(); // Finalize the current frame
bool isOpen() const { return true; } // Always "open" in headless mode
};
#endif // HEADLESS_RENDERER_H

24
src/IndexSprite.cpp Normal file
View File

@ -0,0 +1,24 @@
#include "IndexSprite.h"
#include "GameEngine.h"
//int texture_index, sprite_index, x, y;
GameEngine* IndexSprite::game;
sf::Sprite IndexSprite::drawable()
{
sf::Sprite s;
auto& tex = IndexSprite::game->textures[texture_index];
s.setTexture(tex.texture);
s.setScale(sf::Vector2f(scale, scale));
s.setPosition(sf::Vector2f(x, y));
//std::cout << "Drawable position: " << x << ", " << y << " -> " << s.getPosition().x << ", " << s.getPosition().y << std::endl;
s.setTextureRect(tex.spriteCoordinates(sprite_index));
return s;
}
IndexSprite::IndexSprite(int _ti, int _si, float _x, float _y, float _s):
texture_index(_ti), sprite_index(_si), x(_x), y(_y), scale(_s) {
//std::cout << "IndexSprite constructed with x, y " << _x << ", " << _y << std::endl;
//std::cout << " * Stored x, y " << x << ", " << y << std::endl;
}

13
src/IndexSprite.h Normal file
View File

@ -0,0 +1,13 @@
#pragma once
#include "Common.h"
class GameEngine; // forward declare
class IndexSprite {
public:
int texture_index, sprite_index;
float x, y;
float scale;
static GameEngine* game;
sf::Sprite drawable();
IndexSprite(int, int, float, float, float);
};

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,19 @@
#pragma once
#include "Common.h"
#include "Entity.h"
//#include "EntityManager.h"
//#include "Scene.h"
//#include "GameEngine.h" // can't - need forward declaration
//#include "ActionCode.h"
#include "Python.h"
#include "UIMenu.h"
#include "Grid.h"
#include "IndexSprite.h"
#include "EntityManager.h"
#include <list>
#include "PyFont.h"
#include "PyTexture.h"
#include "McRogueFaceConfig.h"
// implementation required to link templates
#include "Animation.h"
class GameEngine; // forward declared (circular members)
@ -16,30 +24,65 @@ private:
texture_width = 12, texture_height = 11, // w & h sprite/frame count
texture_sprite_count = 11 * 12; // t_width * t_height, minus blanks?
// TODO: this is wrong, load resources @ GameEngineSprite sprite;
// sf::Texture texture;
//std::vector<PyMethodDef> mcrfpyMethodsVector;
//static PyObject* PyInit_mcrfpy();
McRFPy_API();
public:
static PyObject* mcrf_module;
static std::shared_ptr<PyFont> default_font;
static std::shared_ptr<PyTexture> default_texture;
//inline static sf::Sprite sprite;
//inline static sf::Texture texture;
//static void setSpriteTexture(int);
inline static sf::Sprite sprite;
inline static sf::Texture texture;
static void setSpriteTexture(int);
inline static GameEngine* game;
static void api_init();
static void api_init(const McRogueFaceConfig& config, int argc, char** argv);
static PyStatus init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv);
static void api_shutdown();
// Python API functionality - use mcrfpy.* in scripts
//static PyObject* _drawSprite(PyObject*, PyObject*);
static PyObject* _drawSprite(PyObject*, PyObject*);
static void REPL_device(FILE * fp, const char *filename);
static void REPL();
static std::vector<sf::SoundBuffer>* soundbuffers;
static sf::Music* music;
static sf::Sound* sfx;
// Jank mode engage: let the API hold data for Python to hack on
static std::map<std::string, UIMenu*> menus;
static EntityManager entities; // this is also kinda good, entities not on the current grid can still act (like monsters following you through doors??)
static std::map<std::string, Grid*> grids;
static std::list<Animation*> animations;
static std::vector<sf::SoundBuffer> soundbuffers;
static sf::Music music;
static sf::Sound sfx;
static std::shared_ptr<Entity> player;
static std::map<std::string, PyObject*> callbacks;
// Jank Python Method Exposures
static PyObject* _createMenu(PyObject*, PyObject*); // creates a new menu object in McRFPy_API::menus
static PyObject* _listMenus(PyObject*, PyObject*);
static PyObject* _modMenu(PyObject*, PyObject*);
static PyObject* _createCaption(PyObject*, PyObject*); // calls menu.add_caption
static PyObject* _createButton(PyObject*, PyObject*);
static PyObject* _createTexture(PyObject*, PyObject*);
static PyObject* _listTextures(PyObject*, PyObject*);
static PyObject* _createSprite(PyObject*, PyObject*);
// use _listMenus, probably will not implement
//static PyObject* _listCaptions(PyObject*, PyObject*);
//static PyObject* _listButtons(PyObject*, PyObject*);
static PyObject* _createEntity(PyObject*, PyObject*);
//static PyObject* _listEntities(PyObject*, PyObject*);
static PyObject* _createGrid(PyObject*, PyObject*);
static PyObject* _listGrids(PyObject*, PyObject*);
static PyObject* _modGrid(PyObject*, PyObject*);
static PyObject* _createAnimation(PyObject*, PyObject*);
static PyObject* _registerPyAction(PyObject*, PyObject*);
static PyObject* _registerInputAction(PyObject*, PyObject*);
static PyObject* _createSoundBuffer(PyObject*, PyObject*);
static PyObject* _loadMusic(PyObject*, PyObject*);
@ -49,40 +92,71 @@ public:
static PyObject* _getMusicVolume(PyObject*, PyObject*);
static PyObject* _getSoundVolume(PyObject*, PyObject*);
// allow all player actions (items, menus, movement, combat)
static PyObject* _unlockPlayerInput(PyObject*, PyObject*);
// disallow player actions (animating enemy actions)
static PyObject* _lockPlayerInput(PyObject*, PyObject*);
// prompt C++/Grid Objects to callback with a target Entity or grid space
static PyObject* _requestGridTarget(PyObject*, PyObject*);
// string for labeling the map
static std::string active_grid;
static PyObject* _activeGrid(PyObject*, PyObject*);
static PyObject* _setActiveGrid(PyObject*, PyObject*);
// string for prompting input
static std::string input_mode;
static PyObject* _inputMode(PyObject*, PyObject*);
// turn cycle
static int turn_number;
static PyObject* _turnNumber(PyObject*, PyObject*);
static PyObject* _refreshFov(PyObject*, PyObject*);
static bool do_camfollow;
static void camFollow();
static PyObject* _camFollow(PyObject*, PyObject*);
static PyObject* _sceneUI(PyObject*, PyObject*);
// scene control
static PyObject* _setScene(PyObject*, PyObject*);
static PyObject* _currentScene(PyObject*, PyObject*);
static PyObject* _createScene(PyObject*, PyObject*);
static PyObject* _keypressScene(PyObject*, PyObject*);
// timer control
static PyObject* _setTimer(PyObject*, PyObject*);
static PyObject* _delTimer(PyObject*, PyObject*);
static PyObject* _exit(PyObject*, PyObject*);
static PyObject* _setScale(PyObject*, PyObject*);
// accept keyboard input from scene
static sf::Vector2i cursor_position;
static void player_input(int, int);
static void computerTurn();
static void playerTurn();
// Jank Functionality
static UIMenu* createMenu(int posx, int posy, int sizex, int sizey);
static void createCaption(std::string menukey, std::string text, int fontsize, sf::Color textcolor);
static void createButton(std::string menukey, int x, int y, int w, int h, sf::Color bgcolor, sf::Color textcolor, std::string caption, std::string action);
static void createSprite(std::string menukey, int ti, int si, float x, float y, float scale);
static int createTexture(std::string filename, int grid_size, int grid_width, int grid_height);
//static void playSound(const char * filename);
//static void playMusic(const char * filename);
static void doAction(std::string);
// McRFPy_API(GameEngine*);
// API functionality - use from C++ directly
//void spawnEntity(int tex_index, int grid_x, int grid_y, PyObject* script);
static void executeScript(std::string);
static void executePyString(std::string);
// Helper to mark scenes as needing z_index resort
static void markSceneNeedsSort();
// Name-based finding methods
static PyObject* _find(PyObject*, PyObject*);
static PyObject* _findAll(PyObject*, PyObject*);
// Profiling/metrics
static PyObject* _getMetrics(PyObject*, PyObject*);
// Scene lifecycle management for Python Scene objects
static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene);
static void updatePythonScenes(float dt);
static void triggerResize(int width, int height);
};
/*
static PyMethodDef mcrfpyMethods[] = {
{"drawSprite", McRFPy_API::_drawSprite, METH_VARARGS,
"Draw a sprite (index, x, y)"},
{NULL, NULL, 0, NULL}
};
static PyModuleDef mcrfpyModule = {
PyModuleDef_HEAD_INIT, "mcrfpy", NULL, -1, mcrfpyMethods,
NULL, NULL, NULL, NULL
};
// Module initializer fn, passed to PyImport_AppendInittab
PyObject* PyInit_mcrfpy()
{
return PyModule_Create(&mcrfpyModule);
}
*/

View File

@ -1,817 +0,0 @@
#include "McRFPy_Automation.h"
#include "McRFPy_API.h"
#include "GameEngine.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include <unordered_map>
// Helper function to get game engine
GameEngine* McRFPy_Automation::getGameEngine() {
return McRFPy_API::game;
}
// Sleep helper
void McRFPy_Automation::sleep_ms(int milliseconds) {
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
}
// Convert string to SFML key code
sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) {
static const std::unordered_map<std::string, sf::Keyboard::Key> keyMap = {
// Letters
{"a", sf::Keyboard::A}, {"b", sf::Keyboard::B}, {"c", sf::Keyboard::C},
{"d", sf::Keyboard::D}, {"e", sf::Keyboard::E}, {"f", sf::Keyboard::F},
{"g", sf::Keyboard::G}, {"h", sf::Keyboard::H}, {"i", sf::Keyboard::I},
{"j", sf::Keyboard::J}, {"k", sf::Keyboard::K}, {"l", sf::Keyboard::L},
{"m", sf::Keyboard::M}, {"n", sf::Keyboard::N}, {"o", sf::Keyboard::O},
{"p", sf::Keyboard::P}, {"q", sf::Keyboard::Q}, {"r", sf::Keyboard::R},
{"s", sf::Keyboard::S}, {"t", sf::Keyboard::T}, {"u", sf::Keyboard::U},
{"v", sf::Keyboard::V}, {"w", sf::Keyboard::W}, {"x", sf::Keyboard::X},
{"y", sf::Keyboard::Y}, {"z", sf::Keyboard::Z},
// Numbers
{"0", sf::Keyboard::Num0}, {"1", sf::Keyboard::Num1}, {"2", sf::Keyboard::Num2},
{"3", sf::Keyboard::Num3}, {"4", sf::Keyboard::Num4}, {"5", sf::Keyboard::Num5},
{"6", sf::Keyboard::Num6}, {"7", sf::Keyboard::Num7}, {"8", sf::Keyboard::Num8},
{"9", sf::Keyboard::Num9},
// Function keys
{"f1", sf::Keyboard::F1}, {"f2", sf::Keyboard::F2}, {"f3", sf::Keyboard::F3},
{"f4", sf::Keyboard::F4}, {"f5", sf::Keyboard::F5}, {"f6", sf::Keyboard::F6},
{"f7", sf::Keyboard::F7}, {"f8", sf::Keyboard::F8}, {"f9", sf::Keyboard::F9},
{"f10", sf::Keyboard::F10}, {"f11", sf::Keyboard::F11}, {"f12", sf::Keyboard::F12},
{"f13", sf::Keyboard::F13}, {"f14", sf::Keyboard::F14}, {"f15", sf::Keyboard::F15},
// Special keys
{"escape", sf::Keyboard::Escape}, {"esc", sf::Keyboard::Escape},
{"enter", sf::Keyboard::Enter}, {"return", sf::Keyboard::Enter},
{"space", sf::Keyboard::Space}, {" ", sf::Keyboard::Space},
{"tab", sf::Keyboard::Tab}, {"\t", sf::Keyboard::Tab},
{"backspace", sf::Keyboard::BackSpace},
{"delete", sf::Keyboard::Delete}, {"del", sf::Keyboard::Delete},
{"insert", sf::Keyboard::Insert},
{"home", sf::Keyboard::Home},
{"end", sf::Keyboard::End},
{"pageup", sf::Keyboard::PageUp}, {"pgup", sf::Keyboard::PageUp},
{"pagedown", sf::Keyboard::PageDown}, {"pgdn", sf::Keyboard::PageDown},
// Arrow keys
{"left", sf::Keyboard::Left},
{"right", sf::Keyboard::Right},
{"up", sf::Keyboard::Up},
{"down", sf::Keyboard::Down},
// Modifiers
{"ctrl", sf::Keyboard::LControl}, {"ctrlleft", sf::Keyboard::LControl},
{"ctrlright", sf::Keyboard::RControl},
{"alt", sf::Keyboard::LAlt}, {"altleft", sf::Keyboard::LAlt},
{"altright", sf::Keyboard::RAlt},
{"shift", sf::Keyboard::LShift}, {"shiftleft", sf::Keyboard::LShift},
{"shiftright", sf::Keyboard::RShift},
{"win", sf::Keyboard::LSystem}, {"winleft", sf::Keyboard::LSystem},
{"winright", sf::Keyboard::RSystem}, {"command", sf::Keyboard::LSystem},
// Punctuation
{",", sf::Keyboard::Comma}, {".", sf::Keyboard::Period},
{"/", sf::Keyboard::Slash}, {"\\", sf::Keyboard::BackSlash},
{";", sf::Keyboard::SemiColon}, {"'", sf::Keyboard::Quote},
{"[", sf::Keyboard::LBracket}, {"]", sf::Keyboard::RBracket},
{"-", sf::Keyboard::Dash}, {"=", sf::Keyboard::Equal},
// Numpad
{"num0", sf::Keyboard::Numpad0}, {"num1", sf::Keyboard::Numpad1},
{"num2", sf::Keyboard::Numpad2}, {"num3", sf::Keyboard::Numpad3},
{"num4", sf::Keyboard::Numpad4}, {"num5", sf::Keyboard::Numpad5},
{"num6", sf::Keyboard::Numpad6}, {"num7", sf::Keyboard::Numpad7},
{"num8", sf::Keyboard::Numpad8}, {"num9", sf::Keyboard::Numpad9},
{"add", sf::Keyboard::Add}, {"subtract", sf::Keyboard::Subtract},
{"multiply", sf::Keyboard::Multiply}, {"divide", sf::Keyboard::Divide},
// Other
{"pause", sf::Keyboard::Pause},
{"capslock", sf::Keyboard::LControl}, // Note: SFML doesn't have CapsLock
{"numlock", sf::Keyboard::LControl}, // Note: SFML doesn't have NumLock
{"scrolllock", sf::Keyboard::LControl}, // Note: SFML doesn't have ScrollLock
};
auto it = keyMap.find(keyName);
if (it != keyMap.end()) {
return it->second;
}
return sf::Keyboard::Unknown;
}
// Inject mouse event into the game engine
void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button) {
auto engine = getGameEngine();
if (!engine) return;
sf::Event event;
event.type = type;
switch (type) {
case sf::Event::MouseMoved:
event.mouseMove.x = x;
event.mouseMove.y = y;
break;
case sf::Event::MouseButtonPressed:
case sf::Event::MouseButtonReleased:
event.mouseButton.button = button;
event.mouseButton.x = x;
event.mouseButton.y = y;
break;
case sf::Event::MouseWheelScrolled:
event.mouseWheelScroll.wheel = sf::Mouse::VerticalWheel;
event.mouseWheelScroll.delta = static_cast<float>(x); // x is used for scroll amount
event.mouseWheelScroll.x = x;
event.mouseWheelScroll.y = y;
break;
default:
break;
}
engine->processEvent(event);
}
// Inject keyboard event into the game engine
void McRFPy_Automation::injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key) {
auto engine = getGameEngine();
if (!engine) return;
sf::Event event;
event.type = type;
if (type == sf::Event::KeyPressed || type == sf::Event::KeyReleased) {
event.key.code = key;
event.key.alt = sf::Keyboard::isKeyPressed(sf::Keyboard::LAlt) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::RAlt);
event.key.control = sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::RControl);
event.key.shift = sf::Keyboard::isKeyPressed(sf::Keyboard::LShift) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::RShift);
event.key.system = sf::Keyboard::isKeyPressed(sf::Keyboard::LSystem) ||
sf::Keyboard::isKeyPressed(sf::Keyboard::RSystem);
}
engine->processEvent(event);
}
// Inject text event for typing
void McRFPy_Automation::injectTextEvent(sf::Uint32 unicode) {
auto engine = getGameEngine();
if (!engine) return;
sf::Event event;
event.type = sf::Event::TextEntered;
event.text.unicode = unicode;
engine->processEvent(event);
}
// Screenshot implementation
PyObject* McRFPy_Automation::_screenshot(PyObject* self, PyObject* args) {
const char* filename;
if (!PyArg_ParseTuple(args, "s", &filename)) {
return NULL;
}
auto engine = getGameEngine();
if (!engine) {
PyErr_SetString(PyExc_RuntimeError, "Game engine not initialized");
return NULL;
}
// Get the render target
sf::RenderTarget* target = engine->getRenderTargetPtr();
if (!target) {
PyErr_SetString(PyExc_RuntimeError, "No render target available");
return NULL;
}
// For RenderWindow, we can get a screenshot directly
if (auto* window = dynamic_cast<sf::RenderWindow*>(target)) {
sf::Vector2u windowSize = window->getSize();
sf::Texture texture;
texture.create(windowSize.x, windowSize.y);
texture.update(*window);
if (texture.copyToImage().saveToFile(filename)) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
}
// For RenderTexture (headless mode)
else if (auto* renderTexture = dynamic_cast<sf::RenderTexture*>(target)) {
if (renderTexture->getTexture().copyToImage().saveToFile(filename)) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
}
PyErr_SetString(PyExc_RuntimeError, "Unknown render target type");
return NULL;
}
// Get current mouse position
PyObject* McRFPy_Automation::_position(PyObject* self, PyObject* args) {
auto engine = getGameEngine();
if (!engine || !engine->getRenderTargetPtr()) {
return Py_BuildValue("(ii)", 0, 0);
}
// In headless mode, we'd need to track the simulated mouse position
// For now, return the actual mouse position relative to window if available
if (auto* window = dynamic_cast<sf::RenderWindow*>(engine->getRenderTargetPtr())) {
sf::Vector2i pos = sf::Mouse::getPosition(*window);
return Py_BuildValue("(ii)", pos.x, pos.y);
}
// In headless mode, return simulated position (TODO: track this)
return Py_BuildValue("(ii)", 0, 0);
}
// Get screen size
PyObject* McRFPy_Automation::_size(PyObject* self, PyObject* args) {
auto engine = getGameEngine();
if (!engine || !engine->getRenderTargetPtr()) {
return Py_BuildValue("(ii)", 1024, 768); // Default size
}
sf::Vector2u size = engine->getRenderTarget().getSize();
return Py_BuildValue("(ii)", size.x, size.y);
}
// Check if coordinates are on screen
PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args) {
int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
return NULL;
}
auto engine = getGameEngine();
if (!engine || !engine->getRenderTargetPtr()) {
Py_RETURN_FALSE;
}
sf::Vector2u size = engine->getRenderTarget().getSize();
if (x >= 0 && x < (int)size.x && y >= 0 && y < (int)size.y) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
}
// Move mouse to position
PyObject* McRFPy_Automation::_moveTo(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "duration", NULL};
int x, y;
float duration = 0.0f;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast<char**>(kwlist),
&x, &y, &duration)) {
return NULL;
}
// TODO: Implement smooth movement with duration
injectMouseEvent(sf::Event::MouseMoved, x, y);
if (duration > 0) {
sleep_ms(static_cast<int>(duration * 1000));
}
Py_RETURN_NONE;
}
// Move mouse relative
PyObject* McRFPy_Automation::_moveRel(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"xOffset", "yOffset", "duration", NULL};
int xOffset, yOffset;
float duration = 0.0f;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast<char**>(kwlist),
&xOffset, &yOffset, &duration)) {
return NULL;
}
// Get current position
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
int currentX, currentY;
if (!PyArg_ParseTuple(pos, "ii", &currentX, &currentY)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
// Move to new position
injectMouseEvent(sf::Event::MouseMoved, currentX + xOffset, currentY + yOffset);
if (duration > 0) {
sleep_ms(static_cast<int>(duration * 1000));
}
Py_RETURN_NONE;
}
// Click implementation
PyObject* McRFPy_Automation::_click(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "clicks", "interval", "button", NULL};
int x = -1, y = -1;
int clicks = 1;
float interval = 0.0f;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iiifs", const_cast<char**>(kwlist),
&x, &y, &clicks, &interval, &button)) {
return NULL;
}
// If no position specified, use current position
if (x == -1 || y == -1) {
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
}
// Determine button
sf::Mouse::Button sfButton = sf::Mouse::Left;
if (strcmp(button, "right") == 0) {
sfButton = sf::Mouse::Right;
} else if (strcmp(button, "middle") == 0) {
sfButton = sf::Mouse::Middle;
}
// Move to position first
injectMouseEvent(sf::Event::MouseMoved, x, y);
// Perform clicks
for (int i = 0; i < clicks; i++) {
if (i > 0 && interval > 0) {
sleep_ms(static_cast<int>(interval * 1000));
}
injectMouseEvent(sf::Event::MouseButtonPressed, x, y, sfButton);
sleep_ms(10); // Small delay between press and release
injectMouseEvent(sf::Event::MouseButtonReleased, x, y, sfButton);
}
Py_RETURN_NONE;
}
// Right click
PyObject* McRFPy_Automation::_rightClick(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", NULL};
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
return NULL;
}
// Build new args with button="right"
PyObject* newKwargs = PyDict_New();
PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("right"));
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
Py_DECREF(newKwargs);
return result;
}
// Double click
PyObject* McRFPy_Automation::_doubleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", NULL};
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
return NULL;
}
PyObject* newKwargs = PyDict_New();
PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(2));
PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1));
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
Py_DECREF(newKwargs);
return result;
}
// Type text
PyObject* McRFPy_Automation::_typewrite(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"message", "interval", NULL};
const char* message;
float interval = 0.0f;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|f", const_cast<char**>(kwlist),
&message, &interval)) {
return NULL;
}
// Type each character
for (size_t i = 0; message[i] != '\0'; i++) {
if (i > 0 && interval > 0) {
sleep_ms(static_cast<int>(interval * 1000));
}
char c = message[i];
// Handle special characters
if (c == '\n') {
injectKeyEvent(sf::Event::KeyPressed, sf::Keyboard::Enter);
injectKeyEvent(sf::Event::KeyReleased, sf::Keyboard::Enter);
} else if (c == '\t') {
injectKeyEvent(sf::Event::KeyPressed, sf::Keyboard::Tab);
injectKeyEvent(sf::Event::KeyReleased, sf::Keyboard::Tab);
} else {
// For regular characters, send text event
injectTextEvent(static_cast<sf::Uint32>(c));
}
}
Py_RETURN_NONE;
}
// Press and hold key
PyObject* McRFPy_Automation::_keyDown(PyObject* self, PyObject* args) {
const char* keyName;
if (!PyArg_ParseTuple(args, "s", &keyName)) {
return NULL;
}
sf::Keyboard::Key key = stringToKey(keyName);
if (key == sf::Keyboard::Unknown) {
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
return NULL;
}
injectKeyEvent(sf::Event::KeyPressed, key);
Py_RETURN_NONE;
}
// Release key
PyObject* McRFPy_Automation::_keyUp(PyObject* self, PyObject* args) {
const char* keyName;
if (!PyArg_ParseTuple(args, "s", &keyName)) {
return NULL;
}
sf::Keyboard::Key key = stringToKey(keyName);
if (key == sf::Keyboard::Unknown) {
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
return NULL;
}
injectKeyEvent(sf::Event::KeyReleased, key);
Py_RETURN_NONE;
}
// Hotkey combination
PyObject* McRFPy_Automation::_hotkey(PyObject* self, PyObject* args) {
// Get all keys as separate arguments
Py_ssize_t numKeys = PyTuple_Size(args);
if (numKeys == 0) {
PyErr_SetString(PyExc_ValueError, "hotkey() requires at least one key");
return NULL;
}
// Press all keys
for (Py_ssize_t i = 0; i < numKeys; i++) {
PyObject* keyObj = PyTuple_GetItem(args, i);
const char* keyName = PyUnicode_AsUTF8(keyObj);
if (!keyName) {
return NULL;
}
sf::Keyboard::Key key = stringToKey(keyName);
if (key == sf::Keyboard::Unknown) {
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
return NULL;
}
injectKeyEvent(sf::Event::KeyPressed, key);
sleep_ms(10); // Small delay between key presses
}
// Release all keys in reverse order
for (Py_ssize_t i = numKeys - 1; i >= 0; i--) {
PyObject* keyObj = PyTuple_GetItem(args, i);
const char* keyName = PyUnicode_AsUTF8(keyObj);
sf::Keyboard::Key key = stringToKey(keyName);
injectKeyEvent(sf::Event::KeyReleased, key);
sleep_ms(10);
}
Py_RETURN_NONE;
}
// Scroll wheel
PyObject* McRFPy_Automation::_scroll(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"clicks", "x", "y", NULL};
int clicks;
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|ii", const_cast<char**>(kwlist),
&clicks, &x, &y)) {
return NULL;
}
// If no position specified, use current position
if (x == -1 || y == -1) {
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
}
// Inject scroll event
injectMouseEvent(sf::Event::MouseWheelScrolled, clicks, y);
Py_RETURN_NONE;
}
// Other click types using the main click function
PyObject* McRFPy_Automation::_middleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", NULL};
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
return NULL;
}
PyObject* newKwargs = PyDict_New();
PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("middle"));
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
Py_DECREF(newKwargs);
return result;
}
PyObject* McRFPy_Automation::_tripleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", NULL};
int x = -1, y = -1;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
return NULL;
}
PyObject* newKwargs = PyDict_New();
PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(3));
PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1));
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
Py_DECREF(newKwargs);
return result;
}
// Mouse button press/release
PyObject* McRFPy_Automation::_mouseDown(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "button", NULL};
int x = -1, y = -1;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast<char**>(kwlist),
&x, &y, &button)) {
return NULL;
}
// If no position specified, use current position
if (x == -1 || y == -1) {
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
}
sf::Mouse::Button sfButton = sf::Mouse::Left;
if (strcmp(button, "right") == 0) {
sfButton = sf::Mouse::Right;
} else if (strcmp(button, "middle") == 0) {
sfButton = sf::Mouse::Middle;
}
injectMouseEvent(sf::Event::MouseButtonPressed, x, y, sfButton);
Py_RETURN_NONE;
}
PyObject* McRFPy_Automation::_mouseUp(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "button", NULL};
int x = -1, y = -1;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast<char**>(kwlist),
&x, &y, &button)) {
return NULL;
}
// If no position specified, use current position
if (x == -1 || y == -1) {
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
}
sf::Mouse::Button sfButton = sf::Mouse::Left;
if (strcmp(button, "right") == 0) {
sfButton = sf::Mouse::Right;
} else if (strcmp(button, "middle") == 0) {
sfButton = sf::Mouse::Middle;
}
injectMouseEvent(sf::Event::MouseButtonReleased, x, y, sfButton);
Py_RETURN_NONE;
}
// Drag operations
PyObject* McRFPy_Automation::_dragTo(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"x", "y", "duration", "button", NULL};
int x, y;
float duration = 0.0f;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast<char**>(kwlist),
&x, &y, &duration, &button)) {
return NULL;
}
// Get current position
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
int startX, startY;
if (!PyArg_ParseTuple(pos, "ii", &startX, &startY)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
// Mouse down at current position
PyObject* downArgs = Py_BuildValue("(ii)", startX, startY);
PyObject* downKwargs = PyDict_New();
PyDict_SetItemString(downKwargs, "button", PyUnicode_FromString(button));
PyObject* downResult = _mouseDown(self, downArgs, downKwargs);
Py_DECREF(downArgs);
Py_DECREF(downKwargs);
if (!downResult) return NULL;
Py_DECREF(downResult);
// Move to target position
if (duration > 0) {
// Smooth movement
int steps = static_cast<int>(duration * 60); // 60 FPS
for (int i = 1; i <= steps; i++) {
int currentX = startX + (x - startX) * i / steps;
int currentY = startY + (y - startY) * i / steps;
injectMouseEvent(sf::Event::MouseMoved, currentX, currentY);
sleep_ms(1000 / 60); // 60 FPS
}
} else {
injectMouseEvent(sf::Event::MouseMoved, x, y);
}
// Mouse up at target position
PyObject* upArgs = Py_BuildValue("(ii)", x, y);
PyObject* upKwargs = PyDict_New();
PyDict_SetItemString(upKwargs, "button", PyUnicode_FromString(button));
PyObject* upResult = _mouseUp(self, upArgs, upKwargs);
Py_DECREF(upArgs);
Py_DECREF(upKwargs);
if (!upResult) return NULL;
Py_DECREF(upResult);
Py_RETURN_NONE;
}
PyObject* McRFPy_Automation::_dragRel(PyObject* self, PyObject* args, PyObject* kwargs) {
static const char* kwlist[] = {"xOffset", "yOffset", "duration", "button", NULL};
int xOffset, yOffset;
float duration = 0.0f;
const char* button = "left";
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast<char**>(kwlist),
&xOffset, &yOffset, &duration, &button)) {
return NULL;
}
// Get current position
PyObject* pos = _position(self, NULL);
if (!pos) return NULL;
int currentX, currentY;
if (!PyArg_ParseTuple(pos, "ii", &currentX, &currentY)) {
Py_DECREF(pos);
return NULL;
}
Py_DECREF(pos);
// Call dragTo with absolute position
PyObject* dragArgs = Py_BuildValue("(ii)", currentX + xOffset, currentY + yOffset);
PyObject* dragKwargs = PyDict_New();
PyDict_SetItemString(dragKwargs, "duration", PyFloat_FromDouble(duration));
PyDict_SetItemString(dragKwargs, "button", PyUnicode_FromString(button));
PyObject* result = _dragTo(self, dragArgs, dragKwargs);
Py_DECREF(dragArgs);
Py_DECREF(dragKwargs);
return result;
}
// Method definitions for the automation module
static PyMethodDef automationMethods[] = {
{"screenshot", McRFPy_Automation::_screenshot, METH_VARARGS,
"screenshot(filename) - Save a screenshot to the specified file"},
{"position", McRFPy_Automation::_position, METH_NOARGS,
"position() - Get current mouse position as (x, y) tuple"},
{"size", McRFPy_Automation::_size, METH_NOARGS,
"size() - Get screen size as (width, height) tuple"},
{"onScreen", McRFPy_Automation::_onScreen, METH_VARARGS,
"onScreen(x, y) - Check if coordinates are within screen bounds"},
{"moveTo", (PyCFunction)McRFPy_Automation::_moveTo, METH_VARARGS | METH_KEYWORDS,
"moveTo(x, y, duration=0.0) - Move mouse to absolute position"},
{"moveRel", (PyCFunction)McRFPy_Automation::_moveRel, METH_VARARGS | METH_KEYWORDS,
"moveRel(xOffset, yOffset, duration=0.0) - Move mouse relative to current position"},
{"dragTo", (PyCFunction)McRFPy_Automation::_dragTo, METH_VARARGS | METH_KEYWORDS,
"dragTo(x, y, duration=0.0, button='left') - Drag mouse to position"},
{"dragRel", (PyCFunction)McRFPy_Automation::_dragRel, METH_VARARGS | METH_KEYWORDS,
"dragRel(xOffset, yOffset, duration=0.0, button='left') - Drag mouse relative to current position"},
{"click", (PyCFunction)McRFPy_Automation::_click, METH_VARARGS | METH_KEYWORDS,
"click(x=None, y=None, clicks=1, interval=0.0, button='left') - Click at position"},
{"rightClick", (PyCFunction)McRFPy_Automation::_rightClick, METH_VARARGS | METH_KEYWORDS,
"rightClick(x=None, y=None) - Right click at position"},
{"middleClick", (PyCFunction)McRFPy_Automation::_middleClick, METH_VARARGS | METH_KEYWORDS,
"middleClick(x=None, y=None) - Middle click at position"},
{"doubleClick", (PyCFunction)McRFPy_Automation::_doubleClick, METH_VARARGS | METH_KEYWORDS,
"doubleClick(x=None, y=None) - Double click at position"},
{"tripleClick", (PyCFunction)McRFPy_Automation::_tripleClick, METH_VARARGS | METH_KEYWORDS,
"tripleClick(x=None, y=None) - Triple click at position"},
{"scroll", (PyCFunction)McRFPy_Automation::_scroll, METH_VARARGS | METH_KEYWORDS,
"scroll(clicks, x=None, y=None) - Scroll wheel at position"},
{"mouseDown", (PyCFunction)McRFPy_Automation::_mouseDown, METH_VARARGS | METH_KEYWORDS,
"mouseDown(x=None, y=None, button='left') - Press mouse button"},
{"mouseUp", (PyCFunction)McRFPy_Automation::_mouseUp, METH_VARARGS | METH_KEYWORDS,
"mouseUp(x=None, y=None, button='left') - Release mouse button"},
{"typewrite", (PyCFunction)McRFPy_Automation::_typewrite, METH_VARARGS | METH_KEYWORDS,
"typewrite(message, interval=0.0) - Type text with optional interval between keystrokes"},
{"hotkey", McRFPy_Automation::_hotkey, METH_VARARGS,
"hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c'))"},
{"keyDown", McRFPy_Automation::_keyDown, METH_VARARGS,
"keyDown(key) - Press and hold a key"},
{"keyUp", McRFPy_Automation::_keyUp, METH_VARARGS,
"keyUp(key) - Release a key"},
{NULL, NULL, 0, NULL}
};
// Module definition for mcrfpy.automation
static PyModuleDef automationModule = {
PyModuleDef_HEAD_INIT,
"mcrfpy.automation",
"Automation API for McRogueFace - PyAutoGUI-compatible interface",
-1,
automationMethods
};
// Initialize automation submodule
PyObject* McRFPy_Automation::init_automation_module() {
PyObject* module = PyModule_Create(&automationModule);
if (module == NULL) {
return NULL;
}
return module;
}

View File

@ -1,56 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <SFML/Graphics.hpp>
#include <SFML/Window.hpp>
#include <string>
#include <chrono>
#include <thread>
class GameEngine;
class McRFPy_Automation {
public:
// Initialize the automation submodule
static PyObject* init_automation_module();
// Screenshot functionality
static PyObject* _screenshot(PyObject* self, PyObject* args);
// Mouse position and screen info
static PyObject* _position(PyObject* self, PyObject* args);
static PyObject* _size(PyObject* self, PyObject* args);
static PyObject* _onScreen(PyObject* self, PyObject* args);
// Mouse movement
static PyObject* _moveTo(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _moveRel(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _dragTo(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _dragRel(PyObject* self, PyObject* args, PyObject* kwargs);
// Mouse clicks
static PyObject* _click(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _rightClick(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _middleClick(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _doubleClick(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _tripleClick(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _scroll(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _mouseDown(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _mouseUp(PyObject* self, PyObject* args, PyObject* kwargs);
// Keyboard
static PyObject* _typewrite(PyObject* self, PyObject* args, PyObject* kwargs);
static PyObject* _hotkey(PyObject* self, PyObject* args);
static PyObject* _keyDown(PyObject* self, PyObject* args);
static PyObject* _keyUp(PyObject* self, PyObject* args);
// Helper functions
static void injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button = sf::Mouse::Left);
static void injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key);
static void injectTextEvent(sf::Uint32 unicode);
static sf::Keyboard::Key stringToKey(const std::string& keyName);
static void sleep_ms(int milliseconds);
private:
static GameEngine* getGameEngine();
};

View File

@ -1,324 +0,0 @@
#include "McRFPy_Libtcod.h"
#include "McRFPy_API.h"
#include "UIGrid.h"
#include <vector>
// Helper function to get UIGrid from Python object
static UIGrid* get_grid_from_pyobject(PyObject* obj) {
auto grid_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
if (!grid_type) {
PyErr_SetString(PyExc_RuntimeError, "Could not find Grid type");
return nullptr;
}
if (!PyObject_IsInstance(obj, (PyObject*)grid_type)) {
Py_DECREF(grid_type);
PyErr_SetString(PyExc_TypeError, "First argument must be a Grid object");
return nullptr;
}
Py_DECREF(grid_type);
PyUIGridObject* pygrid = (PyUIGridObject*)obj;
return pygrid->data.get();
}
// Field of View computation
static PyObject* McRFPy_Libtcod::compute_fov(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x, y, radius;
int light_walls = 1;
int algorithm = FOV_BASIC;
if (!PyArg_ParseTuple(args, "Oiii|ii", &grid_obj, &x, &y, &radius,
&light_walls, &algorithm)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
// Compute FOV using grid's method
grid->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
// Return list of visible cells
PyObject* visible_list = PyList_New(0);
for (int gy = 0; gy < grid->grid_y; gy++) {
for (int gx = 0; gx < grid->grid_x; gx++) {
if (grid->isInFOV(gx, gy)) {
PyObject* pos = Py_BuildValue("(ii)", gx, gy);
PyList_Append(visible_list, pos);
Py_DECREF(pos);
}
}
}
return visible_list;
}
// A* Pathfinding
static PyObject* McRFPy_Libtcod::find_path(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x1, y1, x2, y2;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTuple(args, "Oiiii|f", &grid_obj, &x1, &y1, &x2, &y2, &diagonal_cost)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
// Get path from grid
std::vector<std::pair<int, int>> path = grid->findPath(x1, y1, x2, y2, diagonal_cost);
// Convert to Python list
PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // steals reference
}
return path_list;
}
// Line drawing algorithm
static PyObject* McRFPy_Libtcod::line(PyObject* self, PyObject* args) {
int x1, y1, x2, y2;
if (!PyArg_ParseTuple(args, "iiii", &x1, &y1, &x2, &y2)) {
return NULL;
}
// Use TCOD's line algorithm
TCODLine::init(x1, y1, x2, y2);
PyObject* line_list = PyList_New(0);
int x, y;
// Step through line
while (!TCODLine::step(&x, &y)) {
PyObject* pos = Py_BuildValue("(ii)", x, y);
PyList_Append(line_list, pos);
Py_DECREF(pos);
}
return line_list;
}
// Line iterator (generator-like function)
static PyObject* McRFPy_Libtcod::line_iter(PyObject* self, PyObject* args) {
// For simplicity, just call line() for now
// A proper implementation would create an iterator object
return line(self, args);
}
// Dijkstra pathfinding
static PyObject* McRFPy_Libtcod::dijkstra_new(PyObject* self, PyObject* args) {
PyObject* grid_obj;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTuple(args, "O|f", &grid_obj, &diagonal_cost)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
// For now, just return the grid object since Dijkstra is part of the grid
Py_INCREF(grid_obj);
return grid_obj;
}
static PyObject* McRFPy_Libtcod::dijkstra_compute(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int root_x, root_y;
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &root_x, &root_y)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
grid->computeDijkstra(root_x, root_y);
Py_RETURN_NONE;
}
static PyObject* McRFPy_Libtcod::dijkstra_get_distance(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x, y;
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &x, &y)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
float distance = grid->getDijkstraDistance(x, y);
if (distance < 0) {
Py_RETURN_NONE;
}
return PyFloat_FromDouble(distance);
}
static PyObject* McRFPy_Libtcod::dijkstra_path_to(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x, y;
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &x, &y)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
std::vector<std::pair<int, int>> path = grid->getDijkstraPath(x, y);
PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // steals reference
}
return path_list;
}
// Add FOV algorithm constants to module
static PyObject* McRFPy_Libtcod::add_fov_constants(PyObject* module) {
// FOV algorithms
PyModule_AddIntConstant(module, "FOV_BASIC", FOV_BASIC);
PyModule_AddIntConstant(module, "FOV_DIAMOND", FOV_DIAMOND);
PyModule_AddIntConstant(module, "FOV_SHADOW", FOV_SHADOW);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_0", FOV_PERMISSIVE_0);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_1", FOV_PERMISSIVE_1);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_2", FOV_PERMISSIVE_2);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_3", FOV_PERMISSIVE_3);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_4", FOV_PERMISSIVE_4);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_5", FOV_PERMISSIVE_5);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_6", FOV_PERMISSIVE_6);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_7", FOV_PERMISSIVE_7);
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_8", FOV_PERMISSIVE_8);
PyModule_AddIntConstant(module, "FOV_RESTRICTIVE", FOV_RESTRICTIVE);
PyModule_AddIntConstant(module, "FOV_SYMMETRIC_SHADOWCAST", FOV_SYMMETRIC_SHADOWCAST);
return module;
}
// Method definitions
static PyMethodDef libtcodMethods[] = {
{"compute_fov", McRFPy_Libtcod::compute_fov, METH_VARARGS,
"compute_fov(grid, x, y, radius, light_walls=True, algorithm=FOV_BASIC)\n\n"
"Compute field of view from a position.\n\n"
"Args:\n"
" grid: Grid object to compute FOV on\n"
" x, y: Origin position\n"
" radius: Maximum sight radius\n"
" light_walls: Whether walls are lit when in FOV\n"
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_SHADOW, etc.)\n\n"
"Returns:\n"
" List of (x, y) tuples for visible cells"},
{"find_path", McRFPy_Libtcod::find_path, METH_VARARGS,
"find_path(grid, x1, y1, x2, y2, diagonal_cost=1.41)\n\n"
"Find shortest path between two points using A*.\n\n"
"Args:\n"
" grid: Grid object to pathfind on\n"
" x1, y1: Starting position\n"
" x2, y2: Target position\n"
" diagonal_cost: Cost of diagonal movement\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path, or empty list if no path exists"},
{"line", McRFPy_Libtcod::line, METH_VARARGS,
"line(x1, y1, x2, y2)\n\n"
"Get cells along a line using Bresenham's algorithm.\n\n"
"Args:\n"
" x1, y1: Starting position\n"
" x2, y2: Ending position\n\n"
"Returns:\n"
" List of (x, y) tuples along the line"},
{"line_iter", McRFPy_Libtcod::line_iter, METH_VARARGS,
"line_iter(x1, y1, x2, y2)\n\n"
"Iterate over cells along a line.\n\n"
"Args:\n"
" x1, y1: Starting position\n"
" x2, y2: Ending position\n\n"
"Returns:\n"
" Iterator of (x, y) tuples along the line"},
{"dijkstra_new", McRFPy_Libtcod::dijkstra_new, METH_VARARGS,
"dijkstra_new(grid, diagonal_cost=1.41)\n\n"
"Create a Dijkstra pathfinding context for a grid.\n\n"
"Args:\n"
" grid: Grid object to use for pathfinding\n"
" diagonal_cost: Cost of diagonal movement\n\n"
"Returns:\n"
" Grid object configured for Dijkstra pathfinding"},
{"dijkstra_compute", McRFPy_Libtcod::dijkstra_compute, METH_VARARGS,
"dijkstra_compute(grid, root_x, root_y)\n\n"
"Compute Dijkstra distance map from root position.\n\n"
"Args:\n"
" grid: Grid object with Dijkstra context\n"
" root_x, root_y: Root position to compute distances from"},
{"dijkstra_get_distance", McRFPy_Libtcod::dijkstra_get_distance, METH_VARARGS,
"dijkstra_get_distance(grid, x, y)\n\n"
"Get distance from root to a position.\n\n"
"Args:\n"
" grid: Grid object with computed Dijkstra map\n"
" x, y: Position to get distance for\n\n"
"Returns:\n"
" Float distance or None if position is invalid/unreachable"},
{"dijkstra_path_to", McRFPy_Libtcod::dijkstra_path_to, METH_VARARGS,
"dijkstra_path_to(grid, x, y)\n\n"
"Get shortest path from position to Dijkstra root.\n\n"
"Args:\n"
" grid: Grid object with computed Dijkstra map\n"
" x, y: Starting position\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path to root"},
{NULL, NULL, 0, NULL}
};
// Module definition
static PyModuleDef libtcodModule = {
PyModuleDef_HEAD_INIT,
"mcrfpy.libtcod",
"TCOD-compatible algorithms for field of view, pathfinding, and line drawing.\n\n"
"This module provides access to TCOD's algorithms integrated with McRogueFace grids.\n"
"Unlike the original TCOD, these functions work directly with Grid objects.\n\n"
"FOV Algorithms:\n"
" FOV_BASIC - Basic circular FOV\n"
" FOV_SHADOW - Shadow casting (recommended)\n"
" FOV_DIAMOND - Diamond-shaped FOV\n"
" FOV_PERMISSIVE_0 through FOV_PERMISSIVE_8 - Permissive variants\n"
" FOV_RESTRICTIVE - Most restrictive FOV\n"
" FOV_SYMMETRIC_SHADOWCAST - Symmetric shadow casting\n\n"
"Example:\n"
" import mcrfpy\n"
" from mcrfpy import libtcod\n\n"
" grid = mcrfpy.Grid(50, 50)\n"
" visible = libtcod.compute_fov(grid, 25, 25, 10)\n"
" path = libtcod.find_path(grid, 0, 0, 49, 49)",
-1,
libtcodMethods
};
// Module initialization
PyObject* McRFPy_Libtcod::init_libtcod_module() {
PyObject* m = PyModule_Create(&libtcodModule);
if (m == NULL) {
return NULL;
}
// Add FOV algorithm constants
add_fov_constants(m);
return m;
}

View File

@ -1,27 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <libtcod.h>
namespace McRFPy_Libtcod
{
// Field of View algorithms
static PyObject* compute_fov(PyObject* self, PyObject* args);
// Pathfinding
static PyObject* find_path(PyObject* self, PyObject* args);
static PyObject* dijkstra_new(PyObject* self, PyObject* args);
static PyObject* dijkstra_compute(PyObject* self, PyObject* args);
static PyObject* dijkstra_get_distance(PyObject* self, PyObject* args);
static PyObject* dijkstra_path_to(PyObject* self, PyObject* args);
// Line algorithms
static PyObject* line(PyObject* self, PyObject* args);
static PyObject* line_iter(PyObject* self, PyObject* args);
// FOV algorithm constants
static PyObject* add_fov_constants(PyObject* module);
// Module initialization
PyObject* init_libtcod_module();
}

View File

@ -1,33 +0,0 @@
#ifndef MCROGUEFACE_CONFIG_H
#define MCROGUEFACE_CONFIG_H
#include <string>
#include <vector>
#include <filesystem>
struct McRogueFaceConfig {
// McRogueFace specific
bool headless = false;
bool audio_enabled = true;
// Python interpreter emulation
bool python_mode = false;
std::string python_command; // -c command
std::string python_module; // -m module
bool interactive_mode = false; // -i flag
bool show_version = false; // -V flag
bool show_help = false; // -h flag
// Script execution
std::filesystem::path script_path;
std::vector<std::string> script_args;
// Scripts to execute before main script (--exec flag)
std::vector<std::filesystem::path> exec_scripts;
// Screenshot functionality for headless mode
std::string screenshot_path;
bool take_screenshot = false;
};
#endif // MCROGUEFACE_CONFIG_H

53
src/MenuScene.cpp Normal file
View File

@ -0,0 +1,53 @@
#include "MenuScene.h"
#include "ActionCode.h"
MenuScene::MenuScene(GameEngine* g) : Scene(g)
{
text.setFont(game->getFont());
text.setString("McRogueFace Engine - r/RoguelikeDev Tutorial 2023");
text.setCharacterSize(24);
//std::cout << "MenuScene Initialized. " << game << std::endl;
//std::cout << "Font: " << game->getFont().getInfo().family << std::endl;
text2.setFont(game->getFont());
text2.setString("Press 'Spacebar' to run demo");
text2.setCharacterSize(16);
text2.setPosition(0.0f, 50.0f);
text3.setFont(game->getFont());
text3.setString("use 'W' 'A' 'S' 'D' to move (even when blank; it's a bug)");
text3.setCharacterSize(16);
text3.setPosition(0.0f, 80.0f);
registerAction(ActionCode::KEY + sf::Keyboard::Space, "start_game");
registerAction(ActionCode::KEY + sf::Keyboard::Up, "up");
registerAction(ActionCode::KEY + sf::Keyboard::Down, "down");
}
void MenuScene::update()
{
//std::cout << "MenuScene update" << std::endl;
}
void MenuScene::doAction(std::string name, std::string type)
{
//std::cout << "MenuScene doAction: " << name << ", " << type << std::endl;
//if (name.compare("start_game") == 0 and type.compare("start") == 0)
if(ACTION("start_game", "start"))
game->changeScene("py");
/*
else if(ACTIONONCE("up"))
game->getWindow().setSize(sf::Vector2u(1280, 800));
else if(ACTIONONCE("down"))
game->getWindow().setSize(sf::Vector2u(1024, 768));
*/
}
void MenuScene::sRender()
{
game->getWindow().clear();
game->getWindow().draw(text);
game->getWindow().draw(text2);
game->getWindow().draw(text3);
game->getWindow().display();
}

18
src/MenuScene.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include "Common.h"
#include "Scene.h"
#include "GameEngine.h"
class MenuScene: public Scene
{
sf::Text text;
sf::Text text2;
sf::Text text3;
public:
MenuScene(GameEngine*);
void update() override final;
void doAction(std::string, std::string) override final;
void sRender() override final;
};

View File

@ -1,273 +0,0 @@
#include "PyAnimation.h"
#include "McRFPy_API.h"
#include "UIDrawable.h"
#include "UIFrame.h"
#include "UICaption.h"
#include "UISprite.h"
#include "UIGrid.h"
#include "UIEntity.h"
#include "UI.h" // For the PyTypeObject definitions
#include <cstring>
PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds) {
PyAnimationObject* self = (PyAnimationObject*)type->tp_alloc(type, 0);
if (self != NULL) {
// Will be initialized in init
}
return (PyObject*)self;
}
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr};
const char* property_name;
PyObject* target_value;
float duration;
const char* easing_name = "linear";
int delta = 0;
PyObject* callback = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast<char**>(keywords),
&property_name, &target_value, &duration, &easing_name, &delta, &callback)) {
return -1;
}
// Validate callback is callable if provided
if (callback && callback != Py_None && !PyCallable_Check(callback)) {
PyErr_SetString(PyExc_TypeError, "callback must be callable");
return -1;
}
// Convert None to nullptr for C++
if (callback == Py_None) {
callback = nullptr;
}
// Convert Python target value to AnimationValue
AnimationValue animValue;
if (PyFloat_Check(target_value)) {
animValue = static_cast<float>(PyFloat_AsDouble(target_value));
}
else if (PyLong_Check(target_value)) {
animValue = static_cast<int>(PyLong_AsLong(target_value));
}
else if (PyList_Check(target_value)) {
// List of integers for sprite animation
std::vector<int> indices;
Py_ssize_t size = PyList_Size(target_value);
for (Py_ssize_t i = 0; i < size; i++) {
PyObject* item = PyList_GetItem(target_value, i);
if (PyLong_Check(item)) {
indices.push_back(PyLong_AsLong(item));
} else {
PyErr_SetString(PyExc_TypeError, "Sprite animation list must contain only integers");
return -1;
}
}
animValue = indices;
}
else if (PyTuple_Check(target_value)) {
Py_ssize_t size = PyTuple_Size(target_value);
if (size == 2) {
// Vector2f
float x = PyFloat_AsDouble(PyTuple_GetItem(target_value, 0));
float y = PyFloat_AsDouble(PyTuple_GetItem(target_value, 1));
animValue = sf::Vector2f(x, y);
}
else if (size == 3 || size == 4) {
// Color (RGB or RGBA)
int r = PyLong_AsLong(PyTuple_GetItem(target_value, 0));
int g = PyLong_AsLong(PyTuple_GetItem(target_value, 1));
int b = PyLong_AsLong(PyTuple_GetItem(target_value, 2));
int a = size == 4 ? PyLong_AsLong(PyTuple_GetItem(target_value, 3)) : 255;
animValue = sf::Color(r, g, b, a);
}
else {
PyErr_SetString(PyExc_ValueError, "Tuple must have 2 elements (vector) or 3-4 elements (color)");
return -1;
}
}
else if (PyUnicode_Check(target_value)) {
// String for text animation
const char* str = PyUnicode_AsUTF8(target_value);
animValue = std::string(str);
}
else {
PyErr_SetString(PyExc_TypeError, "Target value must be float, int, list, tuple, or string");
return -1;
}
// Get easing function
EasingFunction easingFunc = EasingFunctions::getByName(easing_name);
// Create the Animation
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
return 0;
}
void PyAnimation::dealloc(PyAnimationObject* self) {
self->data.reset();
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyObject* PyAnimation::get_property(PyAnimationObject* self, void* closure) {
return PyUnicode_FromString(self->data->getTargetProperty().c_str());
}
PyObject* PyAnimation::get_duration(PyAnimationObject* self, void* closure) {
return PyFloat_FromDouble(self->data->getDuration());
}
PyObject* PyAnimation::get_elapsed(PyAnimationObject* self, void* closure) {
return PyFloat_FromDouble(self->data->getElapsed());
}
PyObject* PyAnimation::get_is_complete(PyAnimationObject* self, void* closure) {
return PyBool_FromLong(self->data->isComplete());
}
PyObject* PyAnimation::get_is_delta(PyAnimationObject* self, void* closure) {
return PyBool_FromLong(self->data->isDelta());
}
PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
PyObject* target_obj;
if (!PyArg_ParseTuple(args, "O", &target_obj)) {
return NULL;
}
// Check type by comparing type names
const char* type_name = Py_TYPE(target_obj)->tp_name;
if (strcmp(type_name, "mcrfpy.Frame") == 0) {
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj;
if (frame->data) {
self->data->start(frame->data);
AnimationManager::getInstance().addAnimation(self->data);
}
}
else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
if (caption->data) {
self->data->start(caption->data);
AnimationManager::getInstance().addAnimation(self->data);
}
}
else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
if (sprite->data) {
self->data->start(sprite->data);
AnimationManager::getInstance().addAnimation(self->data);
}
}
else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
if (grid->data) {
self->data->start(grid->data);
AnimationManager::getInstance().addAnimation(self->data);
}
}
else if (strcmp(type_name, "mcrfpy.Entity") == 0) {
// Special handling for Entity since it doesn't inherit from UIDrawable
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
if (entity->data) {
self->data->startEntity(entity->data);
AnimationManager::getInstance().addAnimation(self->data);
}
}
else {
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity");
return NULL;
}
Py_RETURN_NONE;
}
PyObject* PyAnimation::update(PyAnimationObject* self, PyObject* args) {
float deltaTime;
if (!PyArg_ParseTuple(args, "f", &deltaTime)) {
return NULL;
}
bool still_running = self->data->update(deltaTime);
return PyBool_FromLong(still_running);
}
PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args) {
AnimationValue value = self->data->getCurrentValue();
// Convert AnimationValue back to Python
return std::visit([](const auto& val) -> PyObject* {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) {
return PyFloat_FromDouble(val);
}
else if constexpr (std::is_same_v<T, int>) {
return PyLong_FromLong(val);
}
else if constexpr (std::is_same_v<T, std::vector<int>>) {
// This shouldn't happen as we interpolate to int
return PyLong_FromLong(0);
}
else if constexpr (std::is_same_v<T, sf::Color>) {
return Py_BuildValue("(iiii)", val.r, val.g, val.b, val.a);
}
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
return Py_BuildValue("(ff)", val.x, val.y);
}
else if constexpr (std::is_same_v<T, std::string>) {
return PyUnicode_FromString(val.c_str());
}
Py_RETURN_NONE;
}, value);
}
PyObject* PyAnimation::complete(PyAnimationObject* self, PyObject* args) {
if (self->data) {
self->data->complete();
}
Py_RETURN_NONE;
}
PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args) {
if (self->data && self->data->hasValidTarget()) {
Py_RETURN_TRUE;
}
Py_RETURN_FALSE;
}
PyGetSetDef PyAnimation::getsetters[] = {
{"property", (getter)get_property, NULL, "Target property name", NULL},
{"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL},
{"elapsed", (getter)get_elapsed, NULL, "Elapsed time in seconds", NULL},
{"is_complete", (getter)get_is_complete, NULL, "Whether animation is complete", NULL},
{"is_delta", (getter)get_is_delta, NULL, "Whether animation uses delta mode", NULL},
{NULL}
};
PyMethodDef PyAnimation::methods[] = {
{"start", (PyCFunction)start, METH_VARARGS,
"start(target) -> None\n\n"
"Start the animation on a target UI element.\n\n"
"Args:\n"
" target: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)\n\n"
"Note:\n"
" The animation will automatically stop if the target is destroyed."},
{"update", (PyCFunction)update, METH_VARARGS,
"Update the animation by deltaTime (returns True if still running)"},
{"get_current_value", (PyCFunction)get_current_value, METH_NOARGS,
"Get the current interpolated value"},
{"complete", (PyCFunction)complete, METH_NOARGS,
"complete() -> None\n\n"
"Complete the animation immediately by jumping to the final value."},
{"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS,
"hasValidTarget() -> bool\n\n"
"Check if the animation still has a valid target.\n\n"
"Returns:\n"
" True if the target still exists, False if it was destroyed."},
{NULL}
};

View File

@ -1,52 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "structmember.h"
#include "Animation.h"
#include <memory>
typedef struct {
PyObject_HEAD
std::shared_ptr<Animation> data;
} PyAnimationObject;
class PyAnimation {
public:
static PyObject* create(PyTypeObject* type, PyObject* args, PyObject* kwds);
static int init(PyAnimationObject* self, PyObject* args, PyObject* kwds);
static void dealloc(PyAnimationObject* self);
// Properties
static PyObject* get_property(PyAnimationObject* self, void* closure);
static PyObject* get_duration(PyAnimationObject* self, void* closure);
static PyObject* get_elapsed(PyAnimationObject* self, void* closure);
static PyObject* get_is_complete(PyAnimationObject* self, void* closure);
static PyObject* get_is_delta(PyAnimationObject* self, void* closure);
// Methods
static PyObject* start(PyAnimationObject* self, PyObject* args);
static PyObject* update(PyAnimationObject* self, PyObject* args);
static PyObject* get_current_value(PyAnimationObject* self, PyObject* args);
static PyObject* complete(PyAnimationObject* self, PyObject* args);
static PyObject* has_valid_target(PyAnimationObject* self, PyObject* args);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
};
namespace mcrfpydef {
static PyTypeObject PyAnimationType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Animation",
.tp_basicsize = sizeof(PyAnimationObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyAnimation::dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Animation object for animating UI properties"),
.tp_methods = PyAnimation::methods,
.tp_getset = PyAnimation::getsetters,
.tp_init = (initproc)PyAnimation::init,
.tp_new = PyAnimation::create,
};
}

View File

@ -1,91 +0,0 @@
#include "PyCallable.h"
PyCallable::PyCallable(PyObject* _target)
{
target = Py_XNewRef(_target);
}
PyCallable::PyCallable(const PyCallable& other)
{
target = Py_XNewRef(other.target);
}
PyCallable& PyCallable::operator=(const PyCallable& other)
{
if (this != &other) {
PyObject* old_target = target;
target = Py_XNewRef(other.target);
Py_XDECREF(old_target);
}
return *this;
}
PyCallable::~PyCallable()
{
if (target)
Py_DECREF(target);
}
PyObject* PyCallable::call(PyObject* args, PyObject* kwargs)
{
return PyObject_Call(target, args, kwargs);
}
bool PyCallable::isNone() const
{
return (target == Py_None || target == NULL);
}
PyClickCallable::PyClickCallable(PyObject* _target)
: PyCallable(_target)
{}
PyClickCallable::PyClickCallable()
: PyCallable(Py_None)
{}
void PyClickCallable::call(sf::Vector2f mousepos, std::string button, std::string action)
{
PyObject* args = Py_BuildValue("(iiss)", (int)mousepos.x, (int)mousepos.y, button.c_str(), action.c_str());
PyObject* retval = PyCallable::call(args, NULL);
if (!retval)
{
std::cout << "ClickCallable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else if (retval != Py_None)
{
std::cout << "ClickCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
std::cout << PyUnicode_AsUTF8(PyObject_Repr(retval)) << std::endl;
}
}
PyObject* PyClickCallable::borrow()
{
return target;
}
PyKeyCallable::PyKeyCallable(PyObject* _target)
: PyCallable(_target)
{}
PyKeyCallable::PyKeyCallable()
: PyCallable(Py_None)
{}
void PyKeyCallable::call(std::string key, std::string action)
{
if (target == Py_None || target == NULL) return;
PyObject* args = Py_BuildValue("(ss)", key.c_str(), action.c_str());
PyObject* retval = PyCallable::call(args, NULL);
if (!retval)
{
std::cout << "KeyCallable has raised an exception. It's going to STDERR and being dropped:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else if (retval != Py_None)
{
std::cout << "KeyCallable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
}
}

View File

@ -1,41 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
class PyCallable
{
protected:
PyObject* target;
public:
PyCallable(PyObject*);
PyCallable(const PyCallable& other);
PyCallable& operator=(const PyCallable& other);
~PyCallable();
PyObject* call(PyObject*, PyObject*);
bool isNone() const;
PyObject* borrow() const { return target; }
};
class PyClickCallable: public PyCallable
{
public:
void call(sf::Vector2f, std::string, std::string);
PyObject* borrow();
PyClickCallable(PyObject*);
PyClickCallable();
PyClickCallable(const PyClickCallable& other) : PyCallable(other) {}
PyClickCallable& operator=(const PyClickCallable& other) {
PyCallable::operator=(other);
return *this;
}
};
class PyKeyCallable: public PyCallable
{
public:
void call(std::string, std::string);
//PyObject* borrow(); // not yet implemented
PyKeyCallable(PyObject*);
PyKeyCallable();
};

View File

@ -1,330 +0,0 @@
#include "PyColor.h"
#include "McRFPy_API.h"
#include "PyObjectUtils.h"
#include "PyRAII.h"
#include <string>
#include <cstdio>
PyGetSetDef PyColor::getsetters[] = {
{"r", (getter)PyColor::get_member, (setter)PyColor::set_member, "Red component", (void*)0},
{"g", (getter)PyColor::get_member, (setter)PyColor::set_member, "Green component", (void*)1},
{"b", (getter)PyColor::get_member, (setter)PyColor::set_member, "Blue component", (void*)2},
{"a", (getter)PyColor::get_member, (setter)PyColor::set_member, "Alpha component", (void*)3},
{NULL}
};
PyMethodDef PyColor::methods[] = {
{"from_hex", (PyCFunction)PyColor::from_hex, METH_VARARGS | METH_CLASS, "Create Color from hex string (e.g., '#FF0000' or 'FF0000')"},
{"to_hex", (PyCFunction)PyColor::to_hex, METH_NOARGS, "Convert Color to hex string"},
{"lerp", (PyCFunction)PyColor::lerp, METH_VARARGS, "Linearly interpolate between this color and another"},
{NULL}
};
PyColor::PyColor(sf::Color target)
:data(target) {}
PyObject* PyColor::pyObject()
{
PyTypeObject* type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
if (!type) return nullptr;
PyColorObject* obj = (PyColorObject*)type->tp_alloc(type, 0);
Py_DECREF(type);
if (obj) {
obj->data = data;
}
return (PyObject*)obj;
}
sf::Color PyColor::fromPy(PyObject* obj)
{
PyColorObject* self = (PyColorObject*)obj;
return self->data;
}
sf::Color PyColor::fromPy(PyColorObject* self)
{
return self->data;
}
void PyColor::set(sf::Color color)
{
data = color;
}
sf::Color PyColor::get()
{
return data;
}
Py_hash_t PyColor::hash(PyObject* obj)
{
auto self = (PyColorObject*)obj;
Py_hash_t value = 0;
value += self->data.r;
value << 8; value += self->data.g;
value << 8; value += self->data.b;
value << 8; value += self->data.a;
return value;
}
PyObject* PyColor::repr(PyObject* obj)
{
PyColorObject* self = (PyColorObject*)obj;
std::ostringstream ss;
sf::Color c = self->data;
ss << "<Color (" << int(c.r) << ", " << int(c.g) << ", " << int(c.b) << ", " << int(c.a) << ")>";
std::string repr_str = ss.str();
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
}
int PyColor::init(PyColorObject* self, PyObject* args, PyObject* kwds) {
//using namespace mcrfpydef;
static const char* keywords[] = { "r", "g", "b", "a", nullptr };
PyObject* leader;
int r = -1, g = -1, b = -1, a = 255;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|iii", const_cast<char**>(keywords), &leader, &g, &b, &a)) {
PyErr_SetString(PyExc_TypeError, "mcrfpy.Color requires a 3-tuple, 4-tuple, color name, or integer values within 0-255 (r, g, b, optionally a)");
return -1;
}
//std::cout << "Arg parsing succeeded. Values: " << r << " " << g << " " << b << " " << a <<std::endl;
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(leader)) << std::endl;
// Tuple cases
if (PyTuple_Check(leader)) {
Py_ssize_t tupleSize = PyTuple_Size(leader);
if (tupleSize < 3 || tupleSize > 4) {
PyErr_SetString(PyExc_TypeError, "Invalid tuple length: mcrfpy.Color requires a 3-tuple, 4-tuple, color name, or integer values within 0-255 (r, g, b, optionally a)");
return -1;
}
r = PyLong_AsLong(PyTuple_GetItem(leader, 0));
g = PyLong_AsLong(PyTuple_GetItem(leader, 1));
b = PyLong_AsLong(PyTuple_GetItem(leader, 2));
if (tupleSize == 4) {
a = PyLong_AsLong(PyTuple_GetItem(leader, 3));
}
}
// Color name (not implemented yet)
else if (PyUnicode_Check(leader)) {
PyErr_SetString(PyExc_NotImplementedError, "Color names aren't ready yet");
return -1;
}
// Check if the leader is actually an integer for the r value
else if (PyLong_Check(leader)) {
r = PyLong_AsLong(leader);
// Additional validation not shown; g, b are required to be parsed
} else {
PyErr_SetString(PyExc_TypeError, "mcrfpy.Color requires a 3-tuple, 4-tuple, color name, or integer values within 0-255 (r, g, b, optionally a)");
return -1;
}
// Validate color values
if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255 || a < 0 || a > 255) {
PyErr_SetString(PyExc_ValueError, "Color values must be between 0 and 255.");
return -1;
}
self->data = sf::Color(r, g, b, a);
return 0;
}
PyObject* PyColor::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
{
auto obj = (PyObject*)type->tp_alloc(type, 0);
//Py_INCREF(obj);
return obj;
}
PyObject* PyColor::get_member(PyObject* obj, void* closure)
{
PyColorObject* self = (PyColorObject*)obj;
long member = (long)closure;
switch (member) {
case 0: // r
return PyLong_FromLong(self->data.r);
case 1: // g
return PyLong_FromLong(self->data.g);
case 2: // b
return PyLong_FromLong(self->data.b);
case 3: // a
return PyLong_FromLong(self->data.a);
default:
PyErr_SetString(PyExc_AttributeError, "Invalid color member");
return NULL;
}
}
int PyColor::set_member(PyObject* obj, PyObject* value, void* closure)
{
PyColorObject* self = (PyColorObject*)obj;
long member = (long)closure;
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "Color values must be integers");
return -1;
}
long val = PyLong_AsLong(value);
if (val < 0 || val > 255) {
PyErr_SetString(PyExc_ValueError, "Color values must be between 0 and 255");
return -1;
}
switch (member) {
case 0: // r
self->data.r = static_cast<sf::Uint8>(val);
break;
case 1: // g
self->data.g = static_cast<sf::Uint8>(val);
break;
case 2: // b
self->data.b = static_cast<sf::Uint8>(val);
break;
case 3: // a
self->data.a = static_cast<sf::Uint8>(val);
break;
default:
PyErr_SetString(PyExc_AttributeError, "Invalid color member");
return -1;
}
return 0;
}
PyColorObject* PyColor::from_arg(PyObject* args)
{
// Use RAII for type reference management
PyRAII::PyTypeRef type("Color", McRFPy_API::mcrf_module);
if (!type) {
return NULL;
}
// Check if args is already a Color instance
if (PyObject_IsInstance(args, (PyObject*)type.get())) {
return (PyColorObject*)args;
}
// Create new Color object using RAII
PyRAII::PyObjectRef obj(type->tp_alloc(type.get(), 0), true);
if (!obj) {
return NULL;
}
// Initialize the object
int err = init((PyColorObject*)obj.get(), args, NULL);
if (err) {
// obj will be automatically cleaned up when it goes out of scope
return NULL;
}
// Release ownership and return
return (PyColorObject*)obj.release();
}
// Color helper method implementations
PyObject* PyColor::from_hex(PyObject* cls, PyObject* args)
{
const char* hex_str;
if (!PyArg_ParseTuple(args, "s", &hex_str)) {
return NULL;
}
std::string hex(hex_str);
// Remove # if present
if (hex.length() > 0 && hex[0] == '#') {
hex = hex.substr(1);
}
// Validate hex string
if (hex.length() != 6 && hex.length() != 8) {
PyErr_SetString(PyExc_ValueError, "Hex string must be 6 or 8 characters (RGB or RGBA)");
return NULL;
}
// Parse hex values
try {
unsigned int r = std::stoul(hex.substr(0, 2), nullptr, 16);
unsigned int g = std::stoul(hex.substr(2, 2), nullptr, 16);
unsigned int b = std::stoul(hex.substr(4, 2), nullptr, 16);
unsigned int a = 255;
if (hex.length() == 8) {
a = std::stoul(hex.substr(6, 2), nullptr, 16);
}
// Create new Color object
PyTypeObject* type = (PyTypeObject*)cls;
PyColorObject* color = (PyColorObject*)type->tp_alloc(type, 0);
if (color) {
color->data = sf::Color(r, g, b, a);
}
return (PyObject*)color;
} catch (const std::exception& e) {
PyErr_SetString(PyExc_ValueError, "Invalid hex string");
return NULL;
}
}
PyObject* PyColor::to_hex(PyColorObject* self, PyObject* Py_UNUSED(ignored))
{
char hex[10]; // #RRGGBBAA + null terminator
// Include alpha only if not fully opaque
if (self->data.a < 255) {
snprintf(hex, sizeof(hex), "#%02X%02X%02X%02X",
self->data.r, self->data.g, self->data.b, self->data.a);
} else {
snprintf(hex, sizeof(hex), "#%02X%02X%02X",
self->data.r, self->data.g, self->data.b);
}
return PyUnicode_FromString(hex);
}
PyObject* PyColor::lerp(PyColorObject* self, PyObject* args)
{
PyObject* other_obj;
float t;
if (!PyArg_ParseTuple(args, "Of", &other_obj, &t)) {
return NULL;
}
// Validate other color
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color");
if (!PyObject_IsInstance(other_obj, (PyObject*)type)) {
Py_DECREF(type);
PyErr_SetString(PyExc_TypeError, "First argument must be a Color");
return NULL;
}
PyColorObject* other = (PyColorObject*)other_obj;
// Clamp t to [0, 1]
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
// Perform linear interpolation
sf::Uint8 r = static_cast<sf::Uint8>(self->data.r + (other->data.r - self->data.r) * t);
sf::Uint8 g = static_cast<sf::Uint8>(self->data.g + (other->data.g - self->data.g) * t);
sf::Uint8 b = static_cast<sf::Uint8>(self->data.b + (other->data.b - self->data.b) * t);
sf::Uint8 a = static_cast<sf::Uint8>(self->data.a + (other->data.a - self->data.a) * t);
// Create new Color object
PyColorObject* result = (PyColorObject*)type->tp_alloc(type, 0);
Py_DECREF(type);
if (result) {
result->data = sf::Color(r, g, b, a);
}
return (PyObject*)result;
}

View File

@ -1,56 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
class PyColor;
class UIDrawable; // forward declare for pointer
typedef struct {
PyObject_HEAD
sf::Color data;
} PyColorObject;
class PyColor
{
private:
public:
sf::Color data;
PyColor(sf::Color);
void set(sf::Color);
sf::Color get();
PyObject* pyObject();
static sf::Color fromPy(PyObject*);
static sf::Color fromPy(PyColorObject*);
static PyObject* repr(PyObject*);
static Py_hash_t hash(PyObject*);
static int init(PyColorObject*, PyObject*, PyObject*);
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
static PyObject* get_member(PyObject*, void*);
static int set_member(PyObject*, PyObject*, void*);
// Color helper methods
static PyObject* from_hex(PyObject* cls, PyObject* args);
static PyObject* to_hex(PyColorObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* lerp(PyColorObject* self, PyObject* args);
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
static PyColorObject* from_arg(PyObject*);
};
namespace mcrfpydef {
static PyTypeObject PyColorType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Color",
.tp_basicsize = sizeof(PyColorObject),
.tp_itemsize = 0,
.tp_repr = PyColor::repr,
.tp_hash = PyColor::hash,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Color Object"),
.tp_methods = PyColor::methods,
.tp_getset = PyColor::getsetters,
.tp_init = (initproc)PyColor::init,
.tp_new = PyColor::pynew,
};
}

View File

@ -1,179 +0,0 @@
#include "PyDrawable.h"
#include "McRFPy_API.h"
// Click property getter
static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure)
{
if (!self->data->click_callable)
Py_RETURN_NONE;
PyObject* ptr = self->data->click_callable->borrow();
if (ptr && ptr != Py_None)
return ptr;
else
Py_RETURN_NONE;
}
// Click property setter
static int PyDrawable_set_click(PyDrawableObject* self, PyObject* value, void* closure)
{
if (value == Py_None) {
self->data->click_unregister();
} else if (PyCallable_Check(value)) {
self->data->click_register(value);
} else {
PyErr_SetString(PyExc_TypeError, "click must be callable or None");
return -1;
}
return 0;
}
// Z-index property getter
static PyObject* PyDrawable_get_z_index(PyDrawableObject* self, void* closure)
{
return PyLong_FromLong(self->data->z_index);
}
// Z-index property setter
static int PyDrawable_set_z_index(PyDrawableObject* self, PyObject* value, void* closure)
{
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "z_index must be an integer");
return -1;
}
int val = PyLong_AsLong(value);
self->data->z_index = val;
// Mark scene as needing resort
self->data->notifyZIndexChanged();
return 0;
}
// Visible property getter (new for #87)
static PyObject* PyDrawable_get_visible(PyDrawableObject* self, void* closure)
{
return PyBool_FromLong(self->data->visible);
}
// Visible property setter (new for #87)
static int PyDrawable_set_visible(PyDrawableObject* self, PyObject* value, void* closure)
{
if (!PyBool_Check(value)) {
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
return -1;
}
self->data->visible = (value == Py_True);
return 0;
}
// Opacity property getter (new for #88)
static PyObject* PyDrawable_get_opacity(PyDrawableObject* self, void* closure)
{
return PyFloat_FromDouble(self->data->opacity);
}
// Opacity property setter (new for #88)
static int PyDrawable_set_opacity(PyDrawableObject* self, PyObject* value, void* closure)
{
float val;
if (PyFloat_Check(value)) {
val = PyFloat_AsDouble(value);
} else if (PyLong_Check(value)) {
val = PyLong_AsLong(value);
} else {
PyErr_SetString(PyExc_TypeError, "opacity must be a number");
return -1;
}
// Clamp to valid range
if (val < 0.0f) val = 0.0f;
if (val > 1.0f) val = 1.0f;
self->data->opacity = val;
return 0;
}
// GetSetDef array for properties
static PyGetSetDef PyDrawable_getsetters[] = {
{"click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click,
"Callable executed when object is clicked", NULL},
{"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index,
"Z-order for rendering (lower values rendered first)", NULL},
{"visible", (getter)PyDrawable_get_visible, (setter)PyDrawable_set_visible,
"Whether the object is visible", NULL},
{"opacity", (getter)PyDrawable_get_opacity, (setter)PyDrawable_set_opacity,
"Opacity level (0.0 = transparent, 1.0 = opaque)", NULL},
{NULL} // Sentinel
};
// get_bounds method implementation (#89)
static PyObject* PyDrawable_get_bounds(PyDrawableObject* self, PyObject* Py_UNUSED(args))
{
auto bounds = self->data->get_bounds();
return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height);
}
// move method implementation (#98)
static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args)
{
float dx, dy;
if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) {
return NULL;
}
self->data->move(dx, dy);
Py_RETURN_NONE;
}
// resize method implementation (#98)
static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args)
{
float w, h;
if (!PyArg_ParseTuple(args, "ff", &w, &h)) {
return NULL;
}
self->data->resize(w, h);
Py_RETURN_NONE;
}
// Method definitions
static PyMethodDef PyDrawable_methods[] = {
{"get_bounds", (PyCFunction)PyDrawable_get_bounds, METH_NOARGS,
"Get bounding box as (x, y, width, height)"},
{"move", (PyCFunction)PyDrawable_move, METH_VARARGS,
"Move by relative offset (dx, dy)"},
{"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS,
"Resize to new dimensions (width, height)"},
{NULL} // Sentinel
};
// Type initialization
static int PyDrawable_init(PyDrawableObject* self, PyObject* args, PyObject* kwds)
{
PyErr_SetString(PyExc_TypeError, "Drawable is an abstract base class and cannot be instantiated directly");
return -1;
}
namespace mcrfpydef {
PyTypeObject PyDrawableType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Drawable",
.tp_basicsize = sizeof(PyDrawableObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self) {
PyDrawableObject* obj = (PyDrawableObject*)self;
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = PyDoc_STR("Base class for all drawable UI elements"),
.tp_methods = PyDrawable_methods,
.tp_getset = PyDrawable_getsetters,
.tp_init = (initproc)PyDrawable_init,
.tp_new = PyType_GenericNew,
};
}

View File

@ -1,15 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "UIDrawable.h"
// Python object structure for UIDrawable base class
typedef struct {
PyObject_HEAD
std::shared_ptr<UIDrawable> data;
} PyDrawableObject;
// Declare the Python type for Drawable base class
namespace mcrfpydef {
extern PyTypeObject PyDrawableType;
}

View File

@ -1,79 +0,0 @@
#include "PyFont.h"
#include "McRFPy_API.h"
PyFont::PyFont(std::string filename)
: source(filename)
{
font = sf::Font();
font.loadFromFile(source);
}
PyObject* PyFont::pyObject()
{
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font");
//PyObject* obj = PyType_GenericAlloc(&mcrfpydef::PyFontType, 0);
PyObject* obj = PyFont::pynew(type, Py_None, Py_None);
try {
((PyFontObject*)obj)->data = shared_from_this();
}
catch (std::bad_weak_ptr& e)
{
std::cout << "Bad weak ptr: shared_from_this() failed in PyFont::pyObject(); did you create a PyFont outside of std::make_shared? enjoy your segfault, soon!" << std::endl;
}
// TODO - shared_from_this will raise an exception if the object does not have a shared pointer. Constructor should be made private; write a factory function
return obj;
}
PyObject* PyFont::repr(PyObject* obj)
{
PyFontObject* self = (PyFontObject*)obj;
std::ostringstream ss;
if (!self->data)
{
ss << "<Font [invalid internal object]>";
std::string repr_str = ss.str();
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
}
auto& pfont = *(self->data);
ss << "<Font (family=" << pfont.font.getInfo().family << ") source=`" << pfont.source << "`>";
std::string repr_str = ss.str();
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
}
Py_hash_t PyFont::hash(PyObject* obj)
{
auto self = (PyFontObject*)obj;
return reinterpret_cast<Py_hash_t>(self->data.get());
}
int PyFont::init(PyFontObject* self, PyObject* args, PyObject* kwds)
{
static const char* keywords[] = { "filename", nullptr };
char* filename;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char**>(keywords), &filename))
return -1;
self->data = std::make_shared<PyFont>(filename);
return 0;
}
PyObject* PyFont::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
{
return (PyObject*)type->tp_alloc(type, 0);
}
PyObject* PyFont::get_family(PyFontObject* self, void* closure)
{
return PyUnicode_FromString(self->data->font.getInfo().family.c_str());
}
PyObject* PyFont::get_source(PyFontObject* self, void* closure)
{
return PyUnicode_FromString(self->data->source.c_str());
}
PyGetSetDef PyFont::getsetters[] = {
{"family", (getter)PyFont::get_family, NULL, "Font family name", NULL},
{"source", (getter)PyFont::get_source, NULL, "Source filename of the font", NULL},
{NULL} // Sentinel
};

View File

@ -1,47 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
class PyFont;
typedef struct {
PyObject_HEAD
std::shared_ptr<PyFont> data;
} PyFontObject;
class PyFont : public std::enable_shared_from_this<PyFont>
{
private:
std::string source;
public:
PyFont(std::string filename);
sf::Font font;
PyObject* pyObject();
static PyObject* repr(PyObject*);
static Py_hash_t hash(PyObject*);
static int init(PyFontObject*, PyObject*, PyObject*);
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
// Getters for properties
static PyObject* get_family(PyFontObject* self, void* closure);
static PyObject* get_source(PyFontObject* self, void* closure);
static PyGetSetDef getsetters[];
};
namespace mcrfpydef {
static PyTypeObject PyFontType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Font",
.tp_basicsize = sizeof(PyFontObject),
.tp_itemsize = 0,
.tp_repr = PyFont::repr,
//.tp_hash = PyFont::hash,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Font Object"),
.tp_getset = PyFont::getsetters,
//.tp_base = &PyBaseObject_Type,
.tp_init = (initproc)PyFont::init,
.tp_new = PyType_GenericNew, //PyFont::pynew,
};
}

View File

@ -1,76 +0,0 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "McRFPy_API.h"
#include "PyRAII.h"
namespace PyObjectUtils {
// Template for getting Python type object from module
template<typename T>
PyTypeObject* getPythonType(const char* typeName) {
PyTypeObject* type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, typeName);
if (!type) {
PyErr_Format(PyExc_RuntimeError, "Could not find %s type in module", typeName);
}
return type;
}
// Generic function to create a Python object of given type
inline PyObject* createPyObjectGeneric(const char* typeName) {
PyTypeObject* type = getPythonType<void>(typeName);
if (!type) return nullptr;
PyObject* obj = type->tp_alloc(type, 0);
Py_DECREF(type);
return obj;
}
// Helper function to allocate and initialize a Python object with data
template<typename PyObjType, typename DataType>
PyObject* createPyObjectWithData(const char* typeName, DataType data) {
PyTypeObject* type = getPythonType<void>(typeName);
if (!type) return nullptr;
PyObjType* obj = (PyObjType*)type->tp_alloc(type, 0);
Py_DECREF(type);
if (obj) {
obj->data = data;
}
return (PyObject*)obj;
}
// Function to convert UIDrawable to appropriate Python object
// This is moved to UICollection.cpp to avoid circular dependencies
// RAII-based object creation example
inline PyObject* createPyObjectGenericRAII(const char* typeName) {
PyRAII::PyTypeRef type(typeName, McRFPy_API::mcrf_module);
if (!type) {
PyErr_Format(PyExc_RuntimeError, "Could not find %s type in module", typeName);
return nullptr;
}
PyObject* obj = type->tp_alloc(type.get(), 0);
// Return the new reference (caller owns it)
return obj;
}
// Example of using PyObjectRef for safer reference management
template<typename PyObjType, typename DataType>
PyObject* createPyObjectWithDataRAII(const char* typeName, DataType data) {
PyRAII::PyObjectRef obj = PyRAII::createObject<PyObjType>(typeName, McRFPy_API::mcrf_module);
if (!obj) {
PyErr_Format(PyExc_RuntimeError, "Could not create %s object", typeName);
return nullptr;
}
// Access the object through the RAII wrapper
((PyObjType*)obj.get())->data = data;
// Release ownership to return to Python
return obj.release();
}
}

View File

@ -1,164 +0,0 @@
#pragma once
#include "Python.h"
#include "PyVector.h"
#include "McRFPy_API.h"
// Helper class for standardized position argument parsing across UI classes
class PyPositionHelper {
public:
// Template structure for parsing results
struct ParseResult {
float x = 0.0f;
float y = 0.0f;
bool has_position = false;
};
struct ParseResultInt {
int x = 0;
int y = 0;
bool has_position = false;
};
// Parse position from multiple formats for UI class constructors
// Supports: (x, y), x=x, y=y, ((x,y)), (pos=(x,y)), (Vector), pos=Vector
static ParseResult parse_position(PyObject* args, PyObject* kwds,
int* arg_index = nullptr)
{
ParseResult result;
float x = 0.0f, y = 0.0f;
PyObject* pos_obj = nullptr;
int start_index = arg_index ? *arg_index : 0;
// Check for positional tuple (x, y) first
if (!kwds && PyTuple_Size(args) > start_index + 1) {
PyObject* first = PyTuple_GetItem(args, start_index);
PyObject* second = PyTuple_GetItem(args, start_index + 1);
// Check if both are numbers
if ((PyFloat_Check(first) || PyLong_Check(first)) &&
(PyFloat_Check(second) || PyLong_Check(second))) {
x = PyFloat_Check(first) ? PyFloat_AsDouble(first) : PyLong_AsLong(first);
y = PyFloat_Check(second) ? PyFloat_AsDouble(second) : PyLong_AsLong(second);
result.x = x;
result.y = y;
result.has_position = true;
if (arg_index) *arg_index += 2;
return result;
}
}
// Check for single positional argument that might be tuple or Vector
if (!kwds && PyTuple_Size(args) > start_index) {
PyObject* first = PyTuple_GetItem(args, start_index);
PyVectorObject* vec = PyVector::from_arg(first);
if (vec) {
result.x = vec->data.x;
result.y = vec->data.y;
result.has_position = true;
if (arg_index) *arg_index += 1;
return result;
}
}
// Try keyword arguments
if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_kw = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj) {
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
result.has_position = true;
return result;
}
}
if (pos_kw) {
PyVectorObject* vec = PyVector::from_arg(pos_kw);
if (vec) {
result.x = vec->data.x;
result.y = vec->data.y;
result.has_position = true;
return result;
}
}
}
return result;
}
// Parse integer position for Grid.at() and similar
static ParseResultInt parse_position_int(PyObject* args, PyObject* kwds)
{
ParseResultInt result;
// Check for positional tuple (x, y) first
if (!kwds && PyTuple_Size(args) >= 2) {
PyObject* first = PyTuple_GetItem(args, 0);
PyObject* second = PyTuple_GetItem(args, 1);
if (PyLong_Check(first) && PyLong_Check(second)) {
result.x = PyLong_AsLong(first);
result.y = PyLong_AsLong(second);
result.has_position = true;
return result;
}
}
// Check for single tuple argument
if (!kwds && PyTuple_Size(args) == 1) {
PyObject* first = PyTuple_GetItem(args, 0);
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
PyObject* x_obj = PyTuple_GetItem(first, 0);
PyObject* y_obj = PyTuple_GetItem(first, 1);
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
result.x = PyLong_AsLong(x_obj);
result.y = PyLong_AsLong(y_obj);
result.has_position = true;
return result;
}
}
}
// Try keyword arguments
if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj && PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
result.x = PyLong_AsLong(x_obj);
result.y = PyLong_AsLong(y_obj);
result.has_position = true;
return result;
}
if (pos_obj && PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
if (PyLong_Check(x_val) && PyLong_Check(y_val)) {
result.x = PyLong_AsLong(x_val);
result.y = PyLong_AsLong(y_val);
result.has_position = true;
return result;
}
}
}
return result;
}
// Error message helper
static void set_position_error() {
PyErr_SetString(PyExc_TypeError,
"Position can be specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), or pos=Vector");
}
static void set_position_int_error() {
PyErr_SetString(PyExc_TypeError,
"Position must be specified as: (x, y), x=x, y=y, ((x,y)), or pos=(x,y) with integer values");
}
};

View File

@ -1,138 +0,0 @@
#pragma once
#include "Python.h"
#include <utility>
namespace PyRAII {
// RAII wrapper for PyObject* that automatically manages reference counting
class PyObjectRef {
private:
PyObject* ptr;
public:
// Constructors
PyObjectRef() : ptr(nullptr) {}
explicit PyObjectRef(PyObject* p, bool steal_ref = false) : ptr(p) {
if (ptr && !steal_ref) {
Py_INCREF(ptr);
}
}
// Copy constructor
PyObjectRef(const PyObjectRef& other) : ptr(other.ptr) {
if (ptr) {
Py_INCREF(ptr);
}
}
// Move constructor
PyObjectRef(PyObjectRef&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
// Destructor
~PyObjectRef() {
Py_XDECREF(ptr);
}
// Copy assignment
PyObjectRef& operator=(const PyObjectRef& other) {
if (this != &other) {
Py_XDECREF(ptr);
ptr = other.ptr;
if (ptr) {
Py_INCREF(ptr);
}
}
return *this;
}
// Move assignment
PyObjectRef& operator=(PyObjectRef&& other) noexcept {
if (this != &other) {
Py_XDECREF(ptr);
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
// Access operators
PyObject* get() const { return ptr; }
PyObject* operator->() const { return ptr; }
PyObject& operator*() const { return *ptr; }
operator bool() const { return ptr != nullptr; }
// Release ownership (for returning to Python)
PyObject* release() {
PyObject* temp = ptr;
ptr = nullptr;
return temp;
}
// Reset with new pointer
void reset(PyObject* p = nullptr, bool steal_ref = false) {
if (p != ptr) {
Py_XDECREF(ptr);
ptr = p;
if (ptr && !steal_ref) {
Py_INCREF(ptr);
}
}
}
};
// Helper class for managing PyTypeObject* references from module lookups
class PyTypeRef {
private:
PyTypeObject* type;
public:
PyTypeRef() : type(nullptr) {}
explicit PyTypeRef(const char* typeName, PyObject* module) {
type = (PyTypeObject*)PyObject_GetAttrString(module, typeName);
// GetAttrString returns a new reference, so we own it
}
~PyTypeRef() {
Py_XDECREF((PyObject*)type);
}
// Delete copy operations to prevent accidental reference issues
PyTypeRef(const PyTypeRef&) = delete;
PyTypeRef& operator=(const PyTypeRef&) = delete;
// Allow move operations
PyTypeRef(PyTypeRef&& other) noexcept : type(other.type) {
other.type = nullptr;
}
PyTypeRef& operator=(PyTypeRef&& other) noexcept {
if (this != &other) {
Py_XDECREF((PyObject*)type);
type = other.type;
other.type = nullptr;
}
return *this;
}
PyTypeObject* get() const { return type; }
PyTypeObject* operator->() const { return type; }
operator bool() const { return type != nullptr; }
};
// Convenience function to create a new object with RAII
template<typename PyObjType>
PyObjectRef createObject(const char* typeName, PyObject* module) {
PyTypeRef type(typeName, module);
if (!type) {
return PyObjectRef();
}
PyObject* obj = type->tp_alloc(type.get(), 0);
// tp_alloc returns a new reference, so we steal it
return PyObjectRef(obj, true);
}
}

View File

@ -1,94 +0,0 @@
#include "PyScene.h"
#include "ActionCode.h"
#include "Resources.h"
#include "PyCallable.h"
#include <algorithm>
PyScene::PyScene(GameEngine* g) : Scene(g)
{
// mouse events
registerAction(ActionCode::MOUSEBUTTON + sf::Mouse::Left, "left");
registerAction(ActionCode::MOUSEBUTTON + sf::Mouse::Right, "right");
registerAction(ActionCode::MOUSEWHEEL + ActionCode::WHEEL_DEL, "wheel_up");
registerAction(ActionCode::MOUSEWHEEL + ActionCode::WHEEL_NEG + ActionCode::WHEEL_DEL, "wheel_down");
// console (` / ~ key) - don't hard code.
//registerAction(ActionCode::KEY + sf::Keyboard::Grave, "debug_menu");
}
void PyScene::update()
{
}
void PyScene::do_mouse_input(std::string button, std::string type)
{
// In headless mode, mouse input is not available
if (game->isHeadless()) {
return;
}
auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow());
// Convert window coordinates to game coordinates using the viewport
auto mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos));
// Only sort if z_index values have changed
if (ui_elements_need_sort) {
// Sort in ascending order (same as render)
std::sort(ui_elements->begin(), ui_elements->end(),
[](const auto& a, const auto& b) { return a->z_index < b->z_index; });
ui_elements_need_sort = false;
}
// Check elements in reverse z-order (highest z_index first, top to bottom)
// Use reverse iterators to go from end to beginning
for (auto it = ui_elements->rbegin(); it != ui_elements->rend(); ++it) {
const auto& element = *it;
if (!element->visible) continue;
if (auto target = element->click_at(sf::Vector2f(mousepos))) {
target->click_callable->call(mousepos, button, type);
return; // Stop after first handler
}
}
}
void PyScene::doAction(std::string name, std::string type)
{
if (name.compare("left") == 0 || name.compare("rclick") == 0 || name.compare("wheel_up") == 0 || name.compare("wheel_down") == 0) {
do_mouse_input(name, type);
}
else if ACTIONONCE("debug_menu") {
McRFPy_API::REPL();
}
}
void PyScene::render()
{
game->getRenderTarget().clear();
// Only sort if z_index values have changed
if (ui_elements_need_sort) {
std::sort(ui_elements->begin(), ui_elements->end(),
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
return a->z_index < b->z_index;
});
ui_elements_need_sort = false;
}
// Render in sorted order (no need to copy anymore)
for (auto e: *ui_elements)
{
if (e) {
// Track metrics
game->metrics.uiElements++;
if (e->visible) {
game->metrics.visibleElements++;
// Count this as a draw call (each visible element = 1+ draw calls)
game->metrics.drawCalls++;
}
e->render();
}
}
// Display is handled by GameEngine
}

Some files were not shown because too many files have changed in this diff Show More