diff --git a/CMakeLists.txt b/CMakeLists.txt index dc2a63d..21f79d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -266,6 +266,11 @@ if(MCRF_SDL2) target_compile_definitions(mcrogueface PRIVATE MCRF_SDL2) endif() +# Asset/script directories for WASM preloading (game projects override these) +set(MCRF_ASSETS_DIR "${CMAKE_SOURCE_DIR}/assets" CACHE PATH "Assets directory for WASM preloading") +set(MCRF_SCRIPTS_DIR "${CMAKE_SOURCE_DIR}/src/scripts" CACHE PATH "Scripts directory for WASM preloading") +set(MCRF_SCRIPTS_PLAYGROUND_DIR "${CMAKE_SOURCE_DIR}/src/scripts_playground" CACHE PATH "Playground scripts for WASM") + # Emscripten-specific link options (use ports for zlib, bzip2, sqlite3) if(EMSCRIPTEN) # Base Emscripten options @@ -287,9 +292,9 @@ if(EMSCRIPTEN) # Preload Python stdlib into virtual filesystem at /lib/python3.14 --preload-file=${CMAKE_SOURCE_DIR}/wasm_stdlib/lib@/lib # Preload game scripts into /scripts (use playground scripts if MCRF_PLAYGROUND is set) - --preload-file=${CMAKE_SOURCE_DIR}/src/$,scripts_playground,scripts>@/scripts + --preload-file=$,${MCRF_SCRIPTS_PLAYGROUND_DIR},${MCRF_SCRIPTS_DIR}>@/scripts # Preload assets - --preload-file=${CMAKE_SOURCE_DIR}/assets@/assets + --preload-file=${MCRF_ASSETS_DIR}@/assets # Use custom HTML shell - game shell (fullscreen) or playground shell (REPL) --shell-file=${CMAKE_SOURCE_DIR}/src/$,shell_game.html,shell.html> # Pre-JS to fix browser zoom causing undefined values in events diff --git a/mcrf-init.sh b/mcrf-init.sh new file mode 100755 index 0000000..d33a7d8 --- /dev/null +++ b/mcrf-init.sh @@ -0,0 +1,422 @@ +#!/bin/bash +# +# mcrf-init.sh - Initialize a McRogueFace game project +# +# Creates a game project directory with symlinks to a pre-built engine, +# so game developers only write Python + assets, never rebuild the engine. +# +# Usage: +# mcrf-init # initialize current directory +# mcrf-init my-game # create and initialize my-game/ +# +# Setup (add to ~/.bashrc): +# alias mcrf-init='/path/to/McRogueFace/mcrf-init.sh' +# # or: export PATH="$PATH:/path/to/McRogueFace" +# +# After init: +# make run # run the game +# make dist # package for distribution + +set -e + +# --- Resolve engine root (where this script lives) --- +ENGINE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# --- Determine game directory --- +if [ -n "$1" ]; then + GAME_DIR="$(pwd)/$1" + mkdir -p "$GAME_DIR" +else + GAME_DIR="$(pwd)" +fi + +# --- Colors --- +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BOLD='\033[1m' +NC='\033[0m' + +info() { echo -e "${GREEN}[mcrf]${NC} $1"; } +warn() { echo -e "${YELLOW}[mcrf]${NC} $1"; } +error() { echo -e "${RED}[mcrf]${NC} $1"; } + +# --- Safety checks --- +if [ "$GAME_DIR" = "$ENGINE_ROOT" ]; then + error "Cannot init inside the engine directory itself." + exit 1 +fi + +if [ ! -f "$ENGINE_ROOT/build/mcrogueface" ]; then + error "Engine not built. Run 'make' in $ENGINE_ROOT first." + exit 1 +fi + +# Engine version for packaging +ENGINE_VERSION=$(grep 'MCRFPY_VERSION' "$ENGINE_ROOT/src/McRogueFaceVersion.h" \ + | sed 's/.*"\(.*\)"/\1/') + +info "Initializing McRogueFace game project" +info " Engine: $ENGINE_ROOT (v${ENGINE_VERSION})" +info " Game: $GAME_DIR" +echo + +# --- Create game directories --- +mkdir -p "$GAME_DIR/assets" +mkdir -p "$GAME_DIR/scripts" + +# --- Set up build/ (Linux) --- +info "Setting up build/ (Linux)..." +mkdir -p "$GAME_DIR/build" + +# Binary +ln -sfn "$ENGINE_ROOT/build/mcrogueface" "$GAME_DIR/build/mcrogueface" + +# Shared libraries (entire lib/ tree: .so files + Python stdlib + extensions) +ln -sfn "$ENGINE_ROOT/build/lib" "$GAME_DIR/build/lib" + +# Game content: symlink to project's own directories +ln -sfn ../assets "$GAME_DIR/build/assets" +ln -sfn ../scripts "$GAME_DIR/build/scripts" + +# --- Set up build-windows/ (if engine has it) --- +if [ -f "$ENGINE_ROOT/build-windows/mcrogueface.exe" ]; then + info "Setting up build-windows/..." + mkdir -p "$GAME_DIR/build-windows" + + # Executable + ln -sfn "$ENGINE_ROOT/build-windows/mcrogueface.exe" "$GAME_DIR/build-windows/mcrogueface.exe" + + # DLLs + for dll in "$ENGINE_ROOT/build-windows/"*.dll; do + [ -f "$dll" ] && ln -sfn "$dll" "$GAME_DIR/build-windows/$(basename "$dll")" + done + + # Python stdlib zip + [ -f "$ENGINE_ROOT/build-windows/python314.zip" ] && \ + ln -sfn "$ENGINE_ROOT/build-windows/python314.zip" "$GAME_DIR/build-windows/python314.zip" + + # Python lib directory + [ -d "$ENGINE_ROOT/build-windows/lib" ] && \ + ln -sfn "$ENGINE_ROOT/build-windows/lib" "$GAME_DIR/build-windows/lib" + + # Game content + ln -sfn ../assets "$GAME_DIR/build-windows/assets" + ln -sfn ../scripts "$GAME_DIR/build-windows/scripts" +else + warn "No Windows build found (skipping build-windows/)" +fi + +# --- WASM build info --- +info "WASM support: use 'make wasm' to build (requires emsdk)" + +# --- Starter game.py --- +if [ ! -f "$GAME_DIR/scripts/game.py" ]; then + info "Creating starter scripts/game.py..." + cat > "$GAME_DIR/scripts/game.py" << 'PYTHON' +"""McRogueFace Game - created by mcrf-init""" +import mcrfpy + +scene = mcrfpy.Scene("title") +ui = scene.children + +ui.append(mcrfpy.Caption( + text="My McRogueFace Game", + pos=(512, 300), + font=mcrfpy.Font("assets/JetbrainsMono.ttf"), + fill_color=(255, 255, 255), + font_size=48 +)) + +ui.append(mcrfpy.Caption( + text="Press ESC to quit", + pos=(512, 400), + font=mcrfpy.Font("assets/JetbrainsMono.ttf"), + fill_color=(180, 180, 180), + font_size=24 +)) + +def on_key(key, state): + if key == mcrfpy.Key.ESCAPE and state == mcrfpy.InputState.PRESSED: + mcrfpy.exit() + +scene.on_key = on_key +mcrfpy.current_scene = scene +PYTHON +fi + +# --- Copy a default font if game assets/ is empty --- +if [ -z "$(ls -A "$GAME_DIR/assets/" 2>/dev/null)" ]; then + if [ -f "$ENGINE_ROOT/assets/JetbrainsMono.ttf" ]; then + info "Copying default font to assets/..." + cp "$ENGINE_ROOT/assets/JetbrainsMono.ttf" "$GAME_DIR/assets/" + fi +fi + +# --- .gitignore --- +if [ ! -f "$GAME_DIR/.gitignore" ]; then + info "Creating .gitignore..." + cat > "$GAME_DIR/.gitignore" << 'GITIGNORE' +# McRogueFace game project +build/ +build-windows/ +build-wasm/ +dist/ +__pycache__/ +*.pyc +*.png.bak +GITIGNORE +fi + +# --- pyrightconfig.json (for Vim/LSP autocomplete) --- +if [ ! -f "$GAME_DIR/pyrightconfig.json" ]; then + info "Creating pyrightconfig.json (IDE autocomplete)..." + cat > "$GAME_DIR/pyrightconfig.json" << PYRIGHT +{ + "include": ["scripts"], + "extraPaths": ["${ENGINE_ROOT}/stubs"], + "pythonVersion": "3.14", + "pythonPlatform": "Linux", + "typeCheckingMode": "basic", + "reportMissingModuleSource": false +} +PYRIGHT +fi + +# --- Makefile --- +info "Creating Makefile..." +cat > "$GAME_DIR/Makefile" << MAKEFILE +# McRogueFace Game Project Makefile +# Generated by mcrf-init +# +# Engine: ${ENGINE_ROOT} + +ENGINE_ROOT := ${ENGINE_ROOT} +GAME_NAME := $(basename "$GAME_DIR") +GAME_DIR := \$(shell pwd) + +# Game version - edit this or create a VERSION file +VERSION := \$(shell cat VERSION 2>/dev/null || echo "0.1.0") + +# Engine version (from the linked build) +ENGINE_VERSION := \$(shell grep 'MCRFPY_VERSION' "\$(ENGINE_ROOT)/src/McRogueFaceVersion.h" \\ + | sed 's/.*"\\(.*\\)"/\\1/' 2>/dev/null || echo "unknown") + +.PHONY: run run-windows wasm serve-wasm dist dist-linux dist-windows dist-wasm clean-dist clean-wasm info + +# ============================================================ +# Run targets +# ============================================================ + +run: + @cd build && ./mcrogueface + +run-headless: + @cd build && ./mcrogueface --headless --exec ../scripts/game.py + +run-windows: + @echo "Launch build-windows/mcrogueface.exe on a Windows machine or via Wine" + +serve-wasm: + @if [ ! -f build-wasm/mcrogueface.html ]; then \\ + echo "No WASM build found. Run 'make wasm' first."; \\ + exit 1; \\ + fi + @echo "Serving WASM build at http://localhost:8080" + @cd build-wasm && python3 -m http.server 8080 + +# ============================================================ +# WASM build (requires emsdk) +# ============================================================ + +wasm: + @if ! command -v emcmake >/dev/null 2>&1; then \\ + echo "Error: emcmake not found. Activate emsdk first:"; \\ + echo " source ~/emsdk/emsdk_env.sh"; \\ + exit 1; \\ + fi + @echo "Building WASM with game assets..." + @emcmake cmake -S "\$(ENGINE_ROOT)" -B build-wasm \\ + -DMCRF_SDL2=ON \\ + -DMCRF_GAME_SHELL=ON \\ + -DMCRF_ASSETS_DIR="\$(GAME_DIR)/assets" \\ + -DMCRF_SCRIPTS_DIR="\$(GAME_DIR)/scripts" + @emmake make -C build-wasm -j\$\$(nproc) + @echo "" + @echo "WASM build complete: build-wasm/" + @echo " make serve-wasm - test in browser" + @echo " make dist-wasm - package for distribution" + +clean-wasm: + @rm -rf build-wasm + @echo "Cleaned build-wasm/" + +# ============================================================ +# Distribution packaging +# ============================================================ + +dist: dist-linux dist-windows dist-wasm + @echo "" + @echo "=== Packages ===" + @ls -lh dist/ 2>/dev/null + +dist-linux: + @if [ ! -f build/mcrogueface ]; then \\ + echo "Error: build/mcrogueface not found. Run mcrf-init first."; \\ + exit 1; \\ + fi + @echo "Packaging \$(GAME_NAME) for Linux..." + @mkdir -p dist + \$(eval PKG := dist/\$(GAME_NAME)-\$(VERSION)-Linux) + @rm -rf \$(PKG) + @mkdir -p \$(PKG)/lib + @# Binary + @cp "\$(ENGINE_ROOT)/build/mcrogueface" \$(PKG)/ + @# Shared libraries + @for lib in libpython3.14.so.1.0 \\ + libsfml-graphics.so.2.6.1 libsfml-window.so.2.6.1 \\ + libsfml-system.so.2.6.1 libsfml-audio.so.2.6.1 \\ + libtcod.so; do \\ + [ -f "\$(ENGINE_ROOT)/__lib/\$\$lib" ] && \\ + cp "\$(ENGINE_ROOT)/__lib/\$\$lib" \$(PKG)/lib/; \\ + done + @# Library symlinks + @cd \$(PKG)/lib && \\ + ln -sf libpython3.14.so.1.0 libpython3.14.so && \\ + ln -sf libsfml-graphics.so.2.6.1 libsfml-graphics.so.2.6 && \\ + ln -sf libsfml-graphics.so.2.6.1 libsfml-graphics.so && \\ + ln -sf libsfml-window.so.2.6.1 libsfml-window.so.2.6 && \\ + ln -sf libsfml-window.so.2.6.1 libsfml-window.so && \\ + ln -sf libsfml-system.so.2.6.1 libsfml-system.so.2.6 && \\ + ln -sf libsfml-system.so.2.6.1 libsfml-system.so && \\ + ln -sf libsfml-audio.so.2.6.1 libsfml-audio.so.2.6 && \\ + ln -sf libsfml-audio.so.2.6.1 libsfml-audio.so && \\ + ln -sf libtcod.so libtcod.so.2 + @# Python extension modules + @if [ -d "\$(ENGINE_ROOT)/__lib/Python/lib.linux-x86_64-3.14" ]; then \\ + mkdir -p \$(PKG)/lib/Python/lib.linux-x86_64-3.14; \\ + for so in "\$(ENGINE_ROOT)/__lib/Python/lib.linux-x86_64-3.14"/*.so; do \\ + case "\$\$(basename \$\$so)" in \\ + *test*|xxlimited*|_ctypes_test*|_xxtestfuzz*) continue ;; \\ + *) cp "\$\$so" \$(PKG)/lib/Python/lib.linux-x86_64-3.14/ ;; \\ + esac; \\ + done; \\ + fi + @# Python stdlib + @if [ -d "\$(ENGINE_ROOT)/__lib/Python/Lib" ]; then \\ + mkdir -p \$(PKG)/lib/Python; \\ + cp -r "\$(ENGINE_ROOT)/__lib/Python/Lib" \$(PKG)/lib/Python/; \\ + rm -rf \$(PKG)/lib/Python/Lib/test \$(PKG)/lib/Python/Lib/tests \\ + \$(PKG)/lib/Python/Lib/idlelib \$(PKG)/lib/Python/Lib/tkinter \\ + \$(PKG)/lib/Python/Lib/turtledemo \$(PKG)/lib/Python/Lib/pydoc_data \\ + \$(PKG)/lib/Python/Lib/lib2to3 \$(PKG)/lib/Python/Lib/ensurepip \\ + \$(PKG)/lib/Python/Lib/_pyrepl; \\ + find \$(PKG)/lib/Python/Lib -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true; \\ + find \$(PKG)/lib/Python/Lib -name "*.pyc" -delete 2>/dev/null || true; \\ + find \$(PKG)/lib/Python/Lib -name "test_*.py" -delete 2>/dev/null || true; \\ + fi + @# Game content (real copies, not symlinks) + @cp -r assets \$(PKG)/assets + @cp -r scripts \$(PKG)/scripts + @# Run script + @printf '#!/bin/bash\\nDIR="\$\$(cd "\$\$(dirname "\$\$0")" && pwd)"\\nexport LD_LIBRARY_PATH="\$\$DIR/lib:\$\$LD_LIBRARY_PATH"\\nexec "\$\$DIR/mcrogueface" "\$\$@"\\n' > \$(PKG)/run.sh + @chmod +x \$(PKG)/run.sh + @# Archive + @cd dist && tar -czf "\$(GAME_NAME)-\$(VERSION)-Linux.tar.gz" "\$(GAME_NAME)-\$(VERSION)-Linux" + @rm -rf \$(PKG) + @echo "Created: dist/\$(GAME_NAME)-\$(VERSION)-Linux.tar.gz" + +dist-windows: + @if [ ! -f "\$(ENGINE_ROOT)/build-windows/mcrogueface.exe" ]; then \\ + echo "No Windows build available. Skipping."; \\ + exit 0; \\ + fi + @echo "Packaging \$(GAME_NAME) for Windows..." + @mkdir -p dist + \$(eval PKG := dist/\$(GAME_NAME)-\$(VERSION)-Windows) + @rm -rf \$(PKG) + @mkdir -p \$(PKG) + @# Executable and DLLs + @cp "\$(ENGINE_ROOT)/build-windows/mcrogueface.exe" \$(PKG)/ + @for dll in "\$(ENGINE_ROOT)/build-windows/"*.dll; do \\ + [ -f "\$\$dll" ] && cp "\$\$dll" \$(PKG)/; \\ + done + @# Python stdlib zip + @[ -f "\$(ENGINE_ROOT)/build-windows/python314.zip" ] && \\ + cp "\$(ENGINE_ROOT)/build-windows/python314.zip" \$(PKG)/ || true + @# Python lib directory + @if [ -d "\$(ENGINE_ROOT)/build-windows/lib" ]; then \\ + cp -r "\$(ENGINE_ROOT)/build-windows/lib" \$(PKG)/lib; \\ + fi + @# Game content (real copies) + @cp -r assets \$(PKG)/assets + @cp -r scripts \$(PKG)/scripts + @# Archive + @cd dist && zip -qr "\$(GAME_NAME)-\$(VERSION)-Windows.zip" "\$(GAME_NAME)-\$(VERSION)-Windows" + @rm -rf \$(PKG) + @echo "Created: dist/\$(GAME_NAME)-\$(VERSION)-Windows.zip" + +dist-wasm: + @if ! command -v emcmake >/dev/null 2>&1; then \\ + echo "WASM: emsdk not activated. Skipping WASM package."; \\ + echo " To include WASM: source ~/emsdk/emsdk_env.sh && make dist-wasm"; \\ + exit 0; \\ + fi; \\ + if [ ! -f build-wasm/mcrogueface.html ]; then \\ + echo "Building WASM first..."; \\ + \$(MAKE) wasm || exit 1; \\ + fi; \\ + echo "Packaging \$(GAME_NAME) for Web..."; \\ + mkdir -p dist; \\ + PKG="dist/\$(GAME_NAME)-\$(VERSION)-Web"; \\ + rm -rf "\$\$PKG"; \\ + mkdir -p "\$\$PKG"; \\ + cp build-wasm/mcrogueface.html "\$\$PKG/index.html"; \\ + cp build-wasm/mcrogueface.js "\$\$PKG/"; \\ + cp build-wasm/mcrogueface.wasm "\$\$PKG/"; \\ + cp build-wasm/mcrogueface.data "\$\$PKG/"; \\ + cd dist && zip -qr "\$(GAME_NAME)-\$(VERSION)-Web.zip" "\$(GAME_NAME)-\$(VERSION)-Web"; \\ + rm -rf "\$\$PKG"; \\ + echo "Created: dist/\$(GAME_NAME)-\$(VERSION)-Web.zip" + +clean-dist: + @rm -rf dist + @echo "Cleaned dist/" + +# ============================================================ +# Info +# ============================================================ + +info: + @echo "Game: \$(GAME_NAME) v\$(VERSION)" + @echo "Engine: McRogueFace v\$(ENGINE_VERSION)" + @echo "Root: \$(ENGINE_ROOT)" + @echo "" + @echo "Targets:" + @echo " make run Run the game" + @echo " make run-headless Run headless (for testing)" + @echo " make wasm Build for web (requires emsdk)" + @echo " make serve-wasm Test WASM build in browser" + @echo " make dist Package for all platforms" + @echo " make dist-linux Package for Linux only" + @echo " make dist-windows Package for Windows only" + @echo " make dist-wasm Package for web only" + @echo " make clean-dist Remove dist/" + @echo " make clean-wasm Remove build-wasm/" +MAKEFILE + +# --- VERSION file --- +if [ ! -f "$GAME_DIR/VERSION" ]; then + echo "0.1.0" > "$GAME_DIR/VERSION" +fi + +# --- Done --- +echo +info "Project ready!" +echo +echo -e " ${BOLD}make run${NC} - play the game" +echo -e " ${BOLD}make dist${NC} - package for distribution" +echo -e " ${BOLD}make info${NC} - show project info" +echo +echo -e " Edit ${BOLD}scripts/game.py${NC} and ${BOLD}assets/${NC} to build your game." +echo -e " Edit ${BOLD}VERSION${NC} to set your game version." diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..adfcbf4 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,8 @@ +{ + "include": ["src/scripts", "shade_sprite", "tests"], + "extraPaths": ["stubs"], + "pythonVersion": "3.14", + "pythonPlatform": "Linux", + "typeCheckingMode": "basic", + "reportMissingModuleSource": false +}