Add web-playable WASM demo with BSP dungeon crawler
- Create self-contained demo game script (src/scripts_demo/game.py) showcasing: BSP dungeon generation, Wang tile autotiling, FOV with fog of war, turn-based bump combat, enemy AI, items/treasure, title screen - Add MCRF_DEMO CMake option for building with demo scripts - Add web/index.html landing page with dark theme, controls reference, feature list, and links to GitHub/Gitea - Build with: emcmake cmake -DMCRF_SDL2=ON -DMCRF_DEMO=ON -DMCRF_GAME_SHELL=ON Note: Makefile wasm-demo/serve-demo targets also added locally but Makefile is gitignored. Use CMake directly or force-add the Makefile to track it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c332772324
commit
d73a207535
3 changed files with 1237 additions and 2 deletions
|
|
@ -17,6 +17,9 @@ option(MCRF_SDL2 "Build with SDL2+OpenGL ES 2 backend instead of SFML" OFF)
|
|||
# Playground mode - minimal scripts for web playground (REPL-focused)
|
||||
option(MCRF_PLAYGROUND "Build with minimal playground scripts instead of full game" OFF)
|
||||
|
||||
# Demo mode - self-contained demo game for web showcase
|
||||
option(MCRF_DEMO "Build with demo scripts (web showcase)" OFF)
|
||||
|
||||
# Game shell mode - fullscreen canvas, no REPL chrome (for itch.io / standalone web games)
|
||||
option(MCRF_GAME_SHELL "Use minimal game-only HTML shell (no REPL)" OFF)
|
||||
|
||||
|
|
@ -343,6 +346,7 @@ endif()
|
|||
set(MCRF_ASSETS_DIR "${CMAKE_SOURCE_DIR}/assets" CACHE PATH "Assets directory for WASM preloading")
|
||||
set(MCRF_SCRIPTS_DIR "${CMAKE_SOURCE_DIR}/src/scripts" CACHE PATH "Scripts directory for WASM preloading")
|
||||
set(MCRF_SCRIPTS_PLAYGROUND_DIR "${CMAKE_SOURCE_DIR}/src/scripts_playground" CACHE PATH "Playground scripts for WASM")
|
||||
set(MCRF_SCRIPTS_DEMO_DIR "${CMAKE_SOURCE_DIR}/src/scripts_demo" CACHE PATH "Demo scripts for WASM showcase")
|
||||
|
||||
# Emscripten-specific link options (use ports for zlib, bzip2, sqlite3)
|
||||
if(EMSCRIPTEN)
|
||||
|
|
@ -365,8 +369,8 @@ if(EMSCRIPTEN)
|
|||
-sALLOW_UNIMPLEMENTED_SYSCALLS=1
|
||||
# Preload Python stdlib into virtual filesystem at /lib/python3.14
|
||||
--preload-file=${CMAKE_SOURCE_DIR}/wasm_stdlib/lib@/lib
|
||||
# Preload game scripts into /scripts (use playground scripts if MCRF_PLAYGROUND is set)
|
||||
--preload-file=$<IF:$<BOOL:${MCRF_PLAYGROUND}>,${MCRF_SCRIPTS_PLAYGROUND_DIR},${MCRF_SCRIPTS_DIR}>@/scripts
|
||||
# Preload game scripts into /scripts (playground, demo, or full game)
|
||||
--preload-file=$<IF:$<BOOL:${MCRF_PLAYGROUND}>,${MCRF_SCRIPTS_PLAYGROUND_DIR},$<IF:$<BOOL:${MCRF_DEMO}>,${MCRF_SCRIPTS_DEMO_DIR},${MCRF_SCRIPTS_DIR}>>@/scripts
|
||||
# Preload assets
|
||||
--preload-file=${MCRF_ASSETS_DIR}@/assets
|
||||
# Use custom HTML shell - game shell (fullscreen) or playground shell (REPL)
|
||||
|
|
|
|||
803
src/scripts_demo/game.py
Normal file
803
src/scripts_demo/game.py
Normal file
|
|
@ -0,0 +1,803 @@
|
|||
"""McRogueFace Web Demo - A roguelike dungeon crawler showcasing engine features.
|
||||
|
||||
Features demonstrated:
|
||||
- BSP dungeon generation with corridors
|
||||
- Wang tile autotiling for pretty dungeons
|
||||
- Entity system with player and enemies
|
||||
- Field of view with fog of war
|
||||
- Turn-based bump combat
|
||||
- UI overlays (health bar, messages, depth counter)
|
||||
- Animations and timers
|
||||
"""
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# -- Assets ------------------------------------------------------------------
|
||||
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
|
||||
font = mcrfpy.Font("assets/JetbrainsMono.ttf")
|
||||
|
||||
# Try to load Wang tileset for pretty autotiling
|
||||
try:
|
||||
_tileset = mcrfpy.TileSetFile("assets/kenney_TD_MR_IP.tsx")
|
||||
_wang_set = _tileset.wang_set("dungeon")
|
||||
_Terrain = _wang_set.terrain_enum()
|
||||
HAS_WANG = True
|
||||
except Exception:
|
||||
HAS_WANG = False
|
||||
|
||||
# -- Sprite indices from kenney_TD_MR_IP (12 cols x 55 rows, 16x16) ----------
|
||||
# Rows are 0-indexed; sprite_index = row * 12 + col
|
||||
FLOOR_TILE = 145 # open stone floor
|
||||
WALL_TILE = 251 # solid wall
|
||||
PLAYER_SPRITE = 84 # hero character
|
||||
RAT_SPRITE = 123 # small rat enemy
|
||||
CYCLOPS_SPRITE = 109 # big enemy
|
||||
SKELETON_SPRITE = 110 # skeleton enemy
|
||||
HEART_FULL = 210
|
||||
HEART_HALF = 209
|
||||
HEART_EMPTY = 208
|
||||
POTION_SPRITE = 115 # red potion
|
||||
STAIRS_SPRITE = 91 # stairs down
|
||||
SKULL_SPRITE = 135 # death indicator
|
||||
TREASURE_SPRITE = 127 # treasure chest
|
||||
|
||||
# -- Configuration ------------------------------------------------------------
|
||||
MAP_W, MAP_H = 40, 30
|
||||
ZOOM = 2.0
|
||||
GRID_PX_W = 1024
|
||||
GRID_PX_H = 700
|
||||
FOV_RADIUS = 10
|
||||
MAX_HP = 10
|
||||
ENEMY_SIGHT = 8
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Wang tile autotiling
|
||||
# =============================================================================
|
||||
def paint_tiles(grid, w, h):
|
||||
"""Apply Wang tile autotiling or fallback to simple tiles."""
|
||||
if HAS_WANG:
|
||||
dm = mcrfpy.DiscreteMap((w, h))
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
if grid.at((x, y)).walkable:
|
||||
dm.set(x, y, int(_Terrain.GROUND))
|
||||
else:
|
||||
dm.set(x, y, int(_Terrain.WALL))
|
||||
tiles = _wang_set.resolve(dm)
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
tid = tiles[y * w + x]
|
||||
if tid >= 0:
|
||||
grid.at((x, y)).tilesprite = tid
|
||||
else:
|
||||
grid.at((x, y)).tilesprite = (
|
||||
FLOOR_TILE if grid.at((x, y)).walkable else WALL_TILE
|
||||
)
|
||||
else:
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
grid.at((x, y)).tilesprite = (
|
||||
FLOOR_TILE if grid.at((x, y)).walkable else WALL_TILE
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Dungeon generator (BSP)
|
||||
# =============================================================================
|
||||
class Dungeon:
|
||||
def __init__(self, grid, w, h):
|
||||
self.grid = grid
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.rooms = [] # list of (cx, cy) room centers
|
||||
self.walkable = set() # walkable cell coords
|
||||
|
||||
def generate(self):
|
||||
# Reset all cells to walls
|
||||
for x in range(self.w):
|
||||
for y in range(self.h):
|
||||
self.grid.at((x, y)).walkable = False
|
||||
self.grid.at((x, y)).transparent = False
|
||||
|
||||
# BSP split
|
||||
bsp = mcrfpy.BSP(pos=(1, 1), size=(self.w - 2, self.h - 2))
|
||||
bsp.split_recursive(depth=5, min_size=(4, 4), max_ratio=1.5)
|
||||
leaves = list(bsp.leaves())
|
||||
|
||||
# Carve rooms (1-cell margin from leaf edges)
|
||||
for leaf in leaves:
|
||||
lx, ly = int(leaf.pos[0]), int(leaf.pos[1])
|
||||
lw, lh = int(leaf.size[0]), int(leaf.size[1])
|
||||
cx = lx + lw // 2
|
||||
cy = ly + lh // 2
|
||||
self.rooms.append((cx, cy))
|
||||
for rx in range(lx + 1, lx + lw - 1):
|
||||
for ry in range(ly + 1, ly + lh - 1):
|
||||
if 0 <= rx < self.w and 0 <= ry < self.h:
|
||||
self.grid.at((rx, ry)).walkable = True
|
||||
self.grid.at((rx, ry)).transparent = True
|
||||
self.walkable.add((rx, ry))
|
||||
|
||||
# Carve corridors using BSP adjacency
|
||||
adj = bsp.adjacency
|
||||
connected = set()
|
||||
for i in range(len(adj)):
|
||||
for j in adj[i]:
|
||||
edge = (min(i, j), max(i, j))
|
||||
if edge in connected:
|
||||
continue
|
||||
connected.add(edge)
|
||||
self._dig_corridor(self.rooms[i], self.rooms[j])
|
||||
|
||||
# Apply tile graphics
|
||||
paint_tiles(self.grid, self.w, self.h)
|
||||
|
||||
def _dig_corridor(self, start, end):
|
||||
x1, x2 = min(start[0], end[0]), max(start[0], end[0])
|
||||
y1, y2 = min(start[1], end[1]), max(start[1], end[1])
|
||||
# L-shaped corridor
|
||||
if random.random() < 0.5:
|
||||
tx, ty = x1, y2
|
||||
else:
|
||||
tx, ty = x2, y1
|
||||
for x in range(x1, x2 + 1):
|
||||
if 0 <= x < self.w and 0 <= ty < self.h:
|
||||
self.grid.at((x, ty)).walkable = True
|
||||
self.grid.at((x, ty)).transparent = True
|
||||
self.walkable.add((x, ty))
|
||||
for y in range(y1, y2 + 1):
|
||||
if 0 <= tx < self.w and 0 <= y < self.h:
|
||||
self.grid.at((tx, y)).walkable = True
|
||||
self.grid.at((tx, y)).transparent = True
|
||||
self.walkable.add((tx, y))
|
||||
|
||||
def random_floor(self, exclude=None):
|
||||
"""Find a random walkable cell not in the exclude set."""
|
||||
exclude = exclude or set()
|
||||
candidates = list(self.walkable - exclude)
|
||||
if not candidates:
|
||||
return None
|
||||
return random.choice(candidates)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Game state
|
||||
# =============================================================================
|
||||
class Game:
|
||||
def __init__(self):
|
||||
self.scene = mcrfpy.Scene("game")
|
||||
self.ui = self.scene.children
|
||||
self.depth = 1
|
||||
self.player_hp = MAX_HP
|
||||
self.player_max_hp = MAX_HP
|
||||
self.player_atk = 2
|
||||
self.player_def = 0
|
||||
self.score = 0
|
||||
self.game_over = False
|
||||
self.enemies = [] # list of dicts: {entity, hp, atk, def, sprite, name}
|
||||
self.items = [] # list of dicts: {entity, kind}
|
||||
self.dungeon = None
|
||||
self.fog_layer = None
|
||||
self.grid = None
|
||||
self.player = None
|
||||
self.message_timer = None
|
||||
self.occupied = set() # cells occupied by entities
|
||||
|
||||
self._build_ui()
|
||||
self._new_level()
|
||||
|
||||
self.scene.on_key = self.on_key
|
||||
self.scene.activate()
|
||||
|
||||
# -- UI -------------------------------------------------------------------
|
||||
def _build_ui(self):
|
||||
# Main grid
|
||||
self.grid = mcrfpy.Grid(
|
||||
grid_size=(MAP_W, MAP_H),
|
||||
texture=texture,
|
||||
pos=(0, 0),
|
||||
size=(GRID_PX_W, GRID_PX_H),
|
||||
)
|
||||
self.grid.zoom = ZOOM
|
||||
self.grid.center = (MAP_W / 2.0 * 16, MAP_H / 2.0 * 16)
|
||||
self.ui.append(self.grid)
|
||||
|
||||
# HUD bar at bottom
|
||||
self.hud = mcrfpy.Frame(
|
||||
pos=(0, GRID_PX_H), size=(1024, 68),
|
||||
fill_color=(20, 16, 28, 240)
|
||||
)
|
||||
self.ui.append(self.hud)
|
||||
|
||||
# Health display
|
||||
self.health_label = mcrfpy.Caption(
|
||||
text="HP: 10/10", pos=(12, 6), font=font,
|
||||
fill_color=(220, 50, 50)
|
||||
)
|
||||
self.health_label.font_size = 20
|
||||
self.health_label.outline = 2
|
||||
self.health_label.outline_color = (0, 0, 0)
|
||||
self.hud.children.append(self.health_label)
|
||||
|
||||
# Heart sprites
|
||||
self.hearts = []
|
||||
for i in range(5):
|
||||
h = mcrfpy.Sprite(
|
||||
x=12 + i * 36, y=32,
|
||||
texture=texture, sprite_index=HEART_FULL, scale=2.0
|
||||
)
|
||||
self.hearts.append(h)
|
||||
self.hud.children.append(h)
|
||||
|
||||
# Depth label
|
||||
self.depth_label = mcrfpy.Caption(
|
||||
text="Depth: 1", pos=(220, 6), font=font,
|
||||
fill_color=(180, 180, 220)
|
||||
)
|
||||
self.depth_label.font_size = 20
|
||||
self.depth_label.outline = 2
|
||||
self.depth_label.outline_color = (0, 0, 0)
|
||||
self.hud.children.append(self.depth_label)
|
||||
|
||||
# Score label
|
||||
self.score_label = mcrfpy.Caption(
|
||||
text="Score: 0", pos=(220, 32), font=font,
|
||||
fill_color=(220, 200, 80)
|
||||
)
|
||||
self.score_label.font_size = 18
|
||||
self.score_label.outline = 2
|
||||
self.score_label.outline_color = (0, 0, 0)
|
||||
self.hud.children.append(self.score_label)
|
||||
|
||||
# Message area
|
||||
self.msg_label = mcrfpy.Caption(
|
||||
text="Arrow keys to move. Bump enemies to attack.",
|
||||
pos=(450, 6), font=font,
|
||||
fill_color=(160, 200, 160)
|
||||
)
|
||||
self.msg_label.font_size = 16
|
||||
self.msg_label.outline = 1
|
||||
self.msg_label.outline_color = (0, 0, 0)
|
||||
self.hud.children.append(self.msg_label)
|
||||
|
||||
self.msg_label2 = mcrfpy.Caption(
|
||||
text="Find the stairs to descend deeper!",
|
||||
pos=(450, 30), font=font,
|
||||
fill_color=(140, 160, 180)
|
||||
)
|
||||
self.msg_label2.font_size = 14
|
||||
self.msg_label2.outline = 1
|
||||
self.msg_label2.outline_color = (0, 0, 0)
|
||||
self.hud.children.append(self.msg_label2)
|
||||
|
||||
# -- Level generation -----------------------------------------------------
|
||||
def _new_level(self):
|
||||
# Clear old entities
|
||||
while len(self.grid.entities) > 0:
|
||||
self.grid.entities.pop(0)
|
||||
self.enemies.clear()
|
||||
self.items.clear()
|
||||
self.occupied.clear()
|
||||
|
||||
# Generate dungeon
|
||||
self.dungeon = Dungeon(self.grid, MAP_W, MAP_H)
|
||||
self.dungeon.generate()
|
||||
|
||||
# Place player in first room
|
||||
px, py = self.dungeon.rooms[0]
|
||||
if self.player is None:
|
||||
self.player = mcrfpy.Entity(
|
||||
grid_pos=(px, py), texture=texture,
|
||||
sprite_index=PLAYER_SPRITE
|
||||
)
|
||||
else:
|
||||
self.player.grid_pos = (px, py)
|
||||
self.grid.entities.append(self.player)
|
||||
self.occupied.add((px, py))
|
||||
|
||||
# Place stairs in last room
|
||||
sx, sy = self.dungeon.rooms[-1]
|
||||
stairs = mcrfpy.Entity(
|
||||
grid_pos=(sx, sy), texture=texture,
|
||||
sprite_index=STAIRS_SPRITE
|
||||
)
|
||||
self.grid.entities.append(stairs)
|
||||
self.stairs_pos = (sx, sy)
|
||||
|
||||
# Place enemies (more enemies on deeper levels)
|
||||
num_enemies = min(3 + self.depth * 2, 15)
|
||||
enemy_types = self._enemy_table()
|
||||
for _ in range(num_enemies):
|
||||
pos = self.dungeon.random_floor(exclude=self.occupied)
|
||||
if pos is None:
|
||||
break
|
||||
etype = random.choice(enemy_types)
|
||||
e = mcrfpy.Entity(
|
||||
grid_pos=pos, texture=texture,
|
||||
sprite_index=etype["sprite"]
|
||||
)
|
||||
self.grid.entities.append(e)
|
||||
self.enemies.append({
|
||||
"entity": e,
|
||||
"hp": etype["hp"],
|
||||
"max_hp": etype["hp"],
|
||||
"atk": etype["atk"],
|
||||
"def": etype["def"],
|
||||
"name": etype["name"],
|
||||
"sprite": etype["sprite"],
|
||||
})
|
||||
self.occupied.add(pos)
|
||||
|
||||
# Place health potions
|
||||
num_potions = random.randint(1, 3)
|
||||
for _ in range(num_potions):
|
||||
pos = self.dungeon.random_floor(exclude=self.occupied)
|
||||
if pos is None:
|
||||
break
|
||||
item = mcrfpy.Entity(
|
||||
grid_pos=pos, texture=texture,
|
||||
sprite_index=POTION_SPRITE
|
||||
)
|
||||
self.grid.entities.append(item)
|
||||
self.items.append({"entity": item, "kind": "potion", "pos": pos})
|
||||
self.occupied.add(pos)
|
||||
|
||||
# Place treasure
|
||||
num_treasure = random.randint(1, 2 + self.depth)
|
||||
for _ in range(num_treasure):
|
||||
pos = self.dungeon.random_floor(exclude=self.occupied)
|
||||
if pos is None:
|
||||
break
|
||||
item = mcrfpy.Entity(
|
||||
grid_pos=pos, texture=texture,
|
||||
sprite_index=TREASURE_SPRITE
|
||||
)
|
||||
self.grid.entities.append(item)
|
||||
self.items.append({"entity": item, "kind": "treasure", "pos": pos})
|
||||
self.occupied.add(pos)
|
||||
|
||||
# Set up fog of war
|
||||
self.fog_layer = mcrfpy.ColorLayer(name="fog", z_index=10)
|
||||
self.grid.add_layer(self.fog_layer)
|
||||
self.fog_layer.fill(mcrfpy.Color(0, 0, 0, 255))
|
||||
self.discovered = set()
|
||||
|
||||
# Center camera on player
|
||||
self._center_camera()
|
||||
self._update_fov()
|
||||
self._update_hud()
|
||||
|
||||
# Depth label
|
||||
self.depth_label.text = f"Depth: {self.depth}"
|
||||
|
||||
def _enemy_table(self):
|
||||
"""Return available enemy types scaled by depth."""
|
||||
table = [
|
||||
{"name": "Rat", "sprite": RAT_SPRITE,
|
||||
"hp": 2 + self.depth // 3, "atk": 1, "def": 0},
|
||||
]
|
||||
if self.depth >= 2:
|
||||
table.append(
|
||||
{"name": "Skeleton", "sprite": SKELETON_SPRITE,
|
||||
"hp": 3 + self.depth // 2, "atk": 2, "def": 1}
|
||||
)
|
||||
if self.depth >= 4:
|
||||
table.append(
|
||||
{"name": "Cyclops", "sprite": CYCLOPS_SPRITE,
|
||||
"hp": 6 + self.depth, "atk": 3, "def": 2}
|
||||
)
|
||||
return table
|
||||
|
||||
# -- Camera ---------------------------------------------------------------
|
||||
def _center_camera(self):
|
||||
px = int(self.player.grid_pos.x)
|
||||
py = int(self.player.grid_pos.y)
|
||||
self.grid.center = (px * 16 + 8, py * 16 + 8)
|
||||
|
||||
# -- FOV ------------------------------------------------------------------
|
||||
def _update_fov(self):
|
||||
if self.fog_layer is None:
|
||||
return
|
||||
px = int(self.player.grid_pos.x)
|
||||
py = int(self.player.grid_pos.y)
|
||||
self.grid.compute_fov((px, py), radius=FOV_RADIUS)
|
||||
|
||||
for x in range(MAP_W):
|
||||
for y in range(MAP_H):
|
||||
if self.grid.is_in_fov((x, y)):
|
||||
self.discovered.add((x, y))
|
||||
self.fog_layer.set((x, y), mcrfpy.Color(0, 0, 0, 0))
|
||||
elif (x, y) in self.discovered:
|
||||
self.fog_layer.set((x, y), mcrfpy.Color(0, 0, 0, 140))
|
||||
# else: stays at 255 alpha (fully hidden)
|
||||
|
||||
# -- HUD ------------------------------------------------------------------
|
||||
def _update_hud(self):
|
||||
self.health_label.text = f"HP: {self.player_hp}/{self.player_max_hp}"
|
||||
self.score_label.text = f"Score: {self.score}"
|
||||
# Update heart sprites
|
||||
for i, h in enumerate(self.hearts):
|
||||
full = self.player_hp - i * 2
|
||||
cap = self.player_max_hp - i * 2
|
||||
if cap < 1:
|
||||
h.sprite_index = 659 # invisible/blank
|
||||
elif full >= 2:
|
||||
h.sprite_index = HEART_FULL
|
||||
elif full == 1:
|
||||
h.sprite_index = HEART_HALF
|
||||
else:
|
||||
h.sprite_index = HEART_EMPTY
|
||||
|
||||
def _show_message(self, line1, line2=""):
|
||||
self.msg_label.text = line1
|
||||
self.msg_label2.text = line2
|
||||
|
||||
# -- Combat ---------------------------------------------------------------
|
||||
def _attack_enemy(self, enemy_data):
|
||||
dmg = max(1, self.player_atk - enemy_data["def"])
|
||||
enemy_data["hp"] -= dmg
|
||||
name = enemy_data["name"]
|
||||
if enemy_data["hp"] <= 0:
|
||||
self._show_message(
|
||||
f"You slay the {name}! (+{10 * self.depth} pts)",
|
||||
f"Hit for {dmg} damage - lethal!"
|
||||
)
|
||||
self.score += 10 * self.depth
|
||||
# Remove from grid
|
||||
for i in range(len(self.grid.entities)):
|
||||
if self.grid.entities[i] is enemy_data["entity"]:
|
||||
self.grid.entities.pop(i)
|
||||
break
|
||||
ex = int(enemy_data["entity"].grid_pos.x)
|
||||
ey = int(enemy_data["entity"].grid_pos.y)
|
||||
self.occupied.discard((ex, ey))
|
||||
self.enemies.remove(enemy_data)
|
||||
else:
|
||||
self._show_message(
|
||||
f"You hit the {name} for {dmg}!",
|
||||
f"{name} HP: {enemy_data['hp']}/{enemy_data['max_hp']}"
|
||||
)
|
||||
|
||||
def _enemy_attacks_player(self, enemy_data):
|
||||
dmg = max(1, enemy_data["atk"] - self.player_def)
|
||||
self.player_hp -= dmg
|
||||
name = enemy_data["name"]
|
||||
if self.player_hp <= 0:
|
||||
self.player_hp = 0
|
||||
self._update_hud()
|
||||
self._game_over()
|
||||
return
|
||||
self._show_message(
|
||||
f"The {name} hits you for {dmg}!",
|
||||
f"HP: {self.player_hp}/{self.player_max_hp}"
|
||||
)
|
||||
|
||||
def _game_over(self):
|
||||
self.game_over = True
|
||||
# Darken screen
|
||||
overlay = mcrfpy.Frame(
|
||||
pos=(0, 0), size=(1024, 768),
|
||||
fill_color=(0, 0, 0, 180)
|
||||
)
|
||||
self.ui.append(overlay)
|
||||
|
||||
title = mcrfpy.Caption(
|
||||
text="YOU DIED", pos=(340, 250), font=font,
|
||||
fill_color=(200, 30, 30)
|
||||
)
|
||||
title.font_size = 60
|
||||
title.outline = 4
|
||||
title.outline_color = (0, 0, 0)
|
||||
overlay.children.append(title)
|
||||
|
||||
info = mcrfpy.Caption(
|
||||
text=f"Reached depth {self.depth} with {self.score} points",
|
||||
pos=(280, 340), font=font,
|
||||
fill_color=(200, 200, 200)
|
||||
)
|
||||
info.font_size = 22
|
||||
info.outline = 2
|
||||
info.outline_color = (0, 0, 0)
|
||||
overlay.children.append(info)
|
||||
|
||||
restart = mcrfpy.Caption(
|
||||
text="Press R to restart",
|
||||
pos=(370, 400), font=font,
|
||||
fill_color=(160, 200, 160)
|
||||
)
|
||||
restart.font_size = 20
|
||||
restart.outline = 2
|
||||
restart.outline_color = (0, 0, 0)
|
||||
overlay.children.append(restart)
|
||||
self.game_over_overlay = overlay
|
||||
|
||||
def _restart(self):
|
||||
self.game_over = False
|
||||
self.player_hp = MAX_HP
|
||||
self.player_max_hp = MAX_HP
|
||||
self.player_atk = 2
|
||||
self.player_def = 0
|
||||
self.depth = 1
|
||||
self.score = 0
|
||||
self.player = None
|
||||
# Remove overlay
|
||||
if hasattr(self, 'game_over_overlay'):
|
||||
# Rebuild UI from scratch
|
||||
while len(self.ui) > 0:
|
||||
self.ui.pop(0)
|
||||
self._build_ui()
|
||||
self._new_level()
|
||||
self.scene.on_key = self.on_key
|
||||
|
||||
# -- Items ----------------------------------------------------------------
|
||||
def _check_items(self, px, py):
|
||||
for item in self.items[:]:
|
||||
ix = int(item["entity"].grid_pos.x)
|
||||
iy = int(item["entity"].grid_pos.y)
|
||||
if ix == px and iy == py:
|
||||
if item["kind"] == "potion":
|
||||
heal = random.randint(2, 4)
|
||||
self.player_hp = min(self.player_max_hp, self.player_hp + heal)
|
||||
self._show_message(
|
||||
f"Healed {heal} HP!",
|
||||
f"HP: {self.player_hp}/{self.player_max_hp}"
|
||||
)
|
||||
elif item["kind"] == "treasure":
|
||||
pts = random.randint(5, 15) * self.depth
|
||||
self.score += pts
|
||||
self._show_message(
|
||||
f"Found treasure! (+{pts} pts)",
|
||||
f"Total score: {self.score}"
|
||||
)
|
||||
# Remove item entity
|
||||
for i in range(len(self.grid.entities)):
|
||||
if self.grid.entities[i] is item["entity"]:
|
||||
self.grid.entities.pop(i)
|
||||
break
|
||||
self.occupied.discard((ix, iy))
|
||||
self.items.remove(item)
|
||||
|
||||
# -- Enemy AI (simple: move toward player if visible) ---------------------
|
||||
def _enemy_turn(self):
|
||||
px = int(self.player.grid_pos.x)
|
||||
py = int(self.player.grid_pos.y)
|
||||
|
||||
for edata in self.enemies[:]:
|
||||
ex = int(edata["entity"].grid_pos.x)
|
||||
ey = int(edata["entity"].grid_pos.y)
|
||||
|
||||
# Only act if in FOV (player can see them)
|
||||
if not self.grid.is_in_fov((ex, ey)):
|
||||
continue
|
||||
|
||||
# Manhattan distance
|
||||
dist = abs(ex - px) + abs(ey - py)
|
||||
if dist > ENEMY_SIGHT:
|
||||
continue
|
||||
|
||||
# Adjacent? Attack!
|
||||
if dist == 1:
|
||||
self._enemy_attacks_player(edata)
|
||||
if self.game_over:
|
||||
return
|
||||
continue
|
||||
|
||||
# Move toward player (simple greedy)
|
||||
dx = 0
|
||||
dy = 0
|
||||
if abs(ex - px) > abs(ey - py):
|
||||
dx = 1 if px > ex else -1
|
||||
else:
|
||||
dy = 1 if py > ey else -1
|
||||
|
||||
nx, ny = ex + dx, ey + dy
|
||||
|
||||
# Check bounds and walkability
|
||||
if (0 <= nx < MAP_W and 0 <= ny < MAP_H
|
||||
and self.grid.at((nx, ny)).walkable
|
||||
and (nx, ny) not in self.occupied):
|
||||
self.occupied.discard((ex, ey))
|
||||
edata["entity"].grid_pos = (nx, ny)
|
||||
self.occupied.add((nx, ny))
|
||||
|
||||
# -- Input ----------------------------------------------------------------
|
||||
def on_key(self, key, state):
|
||||
if state != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
if self.game_over:
|
||||
if key == mcrfpy.Key.R:
|
||||
self._restart()
|
||||
return
|
||||
|
||||
dx, dy = 0, 0
|
||||
if key == mcrfpy.Key.UP or key == mcrfpy.Key.W:
|
||||
dy = -1
|
||||
elif key == mcrfpy.Key.DOWN or key == mcrfpy.Key.S:
|
||||
dy = 1
|
||||
elif key == mcrfpy.Key.LEFT or key == mcrfpy.Key.A:
|
||||
dx = -1
|
||||
elif key == mcrfpy.Key.RIGHT or key == mcrfpy.Key.D:
|
||||
dx = 1
|
||||
elif key == mcrfpy.Key.PERIOD:
|
||||
# Wait a turn
|
||||
self._enemy_turn()
|
||||
self._update_fov()
|
||||
self._update_hud()
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
if dx == 0 and dy == 0:
|
||||
return
|
||||
|
||||
px = int(self.player.grid_pos.x)
|
||||
py = int(self.player.grid_pos.y)
|
||||
nx, ny = px + dx, py + dy
|
||||
|
||||
# Bounds check
|
||||
if nx < 0 or nx >= MAP_W or ny < 0 or ny >= MAP_H:
|
||||
return
|
||||
|
||||
# Check for enemy at target
|
||||
for edata in self.enemies:
|
||||
ex = int(edata["entity"].grid_pos.x)
|
||||
ey = int(edata["entity"].grid_pos.y)
|
||||
if ex == nx and ey == ny:
|
||||
self._attack_enemy(edata)
|
||||
self._enemy_turn()
|
||||
self._update_fov()
|
||||
self._center_camera()
|
||||
self._update_hud()
|
||||
return
|
||||
|
||||
# Check walkability
|
||||
if not self.grid.at((nx, ny)).walkable:
|
||||
return
|
||||
|
||||
# Move player
|
||||
self.occupied.discard((px, py))
|
||||
self.player.grid_pos = (nx, ny)
|
||||
self.occupied.add((nx, ny))
|
||||
|
||||
# Check stairs
|
||||
if (nx, ny) == self.stairs_pos:
|
||||
self.depth += 1
|
||||
self._show_message(
|
||||
f"Descending to depth {self.depth}...",
|
||||
"The dungeon grows more dangerous."
|
||||
)
|
||||
self._new_level()
|
||||
self._enemy_turn()
|
||||
self._update_fov()
|
||||
self._center_camera()
|
||||
self._update_hud()
|
||||
return
|
||||
|
||||
# Check items
|
||||
self._check_items(nx, ny)
|
||||
|
||||
# Enemy turn
|
||||
self._enemy_turn()
|
||||
if not self.game_over:
|
||||
self._update_fov()
|
||||
self._center_camera()
|
||||
self._update_hud()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Title screen
|
||||
# =============================================================================
|
||||
class TitleScreen:
|
||||
def __init__(self):
|
||||
self.scene = mcrfpy.Scene("title")
|
||||
ui = self.scene.children
|
||||
|
||||
# Dark background
|
||||
bg = mcrfpy.Frame(
|
||||
pos=(0, 0), size=(1024, 768),
|
||||
fill_color=(12, 10, 20)
|
||||
)
|
||||
ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(
|
||||
text="McRogueFace", pos=(240, 140), font=font,
|
||||
fill_color=(220, 60, 60)
|
||||
)
|
||||
title.font_size = 72
|
||||
title.outline = 4
|
||||
title.outline_color = (0, 0, 0)
|
||||
bg.children.append(title)
|
||||
|
||||
# Subtitle
|
||||
sub = mcrfpy.Caption(
|
||||
text="A Python-Powered Roguelike Engine",
|
||||
pos=(270, 240), font=font,
|
||||
fill_color=(160, 160, 200)
|
||||
)
|
||||
sub.font_size = 22
|
||||
sub.outline = 2
|
||||
sub.outline_color = (0, 0, 0)
|
||||
bg.children.append(sub)
|
||||
|
||||
# Features list
|
||||
features = [
|
||||
"BSP dungeon generation",
|
||||
"Wang tile autotiling",
|
||||
"Field of view & fog of war",
|
||||
"Turn-based combat",
|
||||
"Entity system with Python scripting",
|
||||
]
|
||||
for i, feat in enumerate(features):
|
||||
dot = mcrfpy.Caption(
|
||||
text=f" {feat}",
|
||||
pos=(320, 320 + i * 32), font=font,
|
||||
fill_color=(140, 180, 140)
|
||||
)
|
||||
dot.font_size = 16
|
||||
dot.outline = 1
|
||||
dot.outline_color = (0, 0, 0)
|
||||
bg.children.append(dot)
|
||||
|
||||
# Start prompt
|
||||
start = mcrfpy.Caption(
|
||||
text="Press ENTER or SPACE to begin",
|
||||
pos=(310, 540), font=font,
|
||||
fill_color=(200, 200, 100)
|
||||
)
|
||||
start.font_size = 20
|
||||
start.outline = 2
|
||||
start.outline_color = (0, 0, 0)
|
||||
bg.children.append(start)
|
||||
|
||||
# Animate the start prompt
|
||||
self._blink_visible = True
|
||||
self._start_caption = start
|
||||
|
||||
# Controls hint
|
||||
controls = mcrfpy.Caption(
|
||||
text="Controls: Arrow keys / WASD to move, . to wait, R to restart",
|
||||
pos=(200, 600), font=font,
|
||||
fill_color=(100, 100, 130)
|
||||
)
|
||||
controls.font_size = 14
|
||||
controls.outline = 1
|
||||
controls.outline_color = (0, 0, 0)
|
||||
bg.children.append(controls)
|
||||
|
||||
# Version info
|
||||
ver = mcrfpy.Caption(
|
||||
text="Built with McRogueFace - C++ engine, Python gameplay",
|
||||
pos=(250, 700), font=font,
|
||||
fill_color=(60, 60, 80)
|
||||
)
|
||||
ver.font_size = 12
|
||||
bg.children.append(ver)
|
||||
|
||||
self.scene.on_key = self.on_key
|
||||
self.scene.activate()
|
||||
|
||||
# Blink timer for "Press ENTER"
|
||||
self.blink_timer = mcrfpy.Timer("blink", self._blink, 600)
|
||||
|
||||
def _blink(self, timer, runtime):
|
||||
self._blink_visible = not self._blink_visible
|
||||
if self._blink_visible:
|
||||
self._start_caption.fill_color = mcrfpy.Color(200, 200, 100)
|
||||
else:
|
||||
self._start_caption.fill_color = mcrfpy.Color(200, 200, 100, 60)
|
||||
|
||||
def on_key(self, key, state):
|
||||
if state != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
if key in (mcrfpy.Key.ENTER, mcrfpy.Key.SPACE):
|
||||
self.blink_timer.stop()
|
||||
Game()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Entry point
|
||||
# =============================================================================
|
||||
title = TitleScreen()
|
||||
428
web/index.html
Normal file
428
web/index.html
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>McRogueFace - Web Demo</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap');
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--bg-dark: #0c0a14;
|
||||
--bg-card: #16132a;
|
||||
--bg-surface: #1e1a36;
|
||||
--accent: #e94560;
|
||||
--accent-glow: #e9456040;
|
||||
--text: #e8e6f0;
|
||||
--text-dim: #8886a0;
|
||||
--text-muted: #5a587a;
|
||||
--gold: #f0c040;
|
||||
--green: #40d080;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: var(--bg-dark);
|
||||
color: var(--text);
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 40px 20px 20px;
|
||||
background: linear-gradient(180deg, #14102a 0%, var(--bg-dark) 100%);
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
text-shadow: 0 0 40px var(--accent-glow);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.hero .subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.hero .tagline {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
max-width: 600px;
|
||||
margin: 0 auto 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.canvas-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 20px 30px;
|
||||
}
|
||||
|
||||
.canvas-frame {
|
||||
position: relative;
|
||||
background: #000;
|
||||
border: 2px solid #2a2648;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6), 0 0 60px var(--accent-glow);
|
||||
/* fixed aspect ratio container for 1024x768 */
|
||||
width: min(1024px, calc(100vw - 40px));
|
||||
aspect-ratio: 1024 / 768;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
-ms-interpolation-mode: nearest-neighbor;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(12, 10, 20, 0.95);
|
||||
z-index: 10;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.loading-overlay.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #2a2648;
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
#status {
|
||||
margin-top: 16px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.info-section {
|
||||
max-width: 1024px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px 50px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.info-grid { grid-template-columns: 1fr; }
|
||||
.hero h1 { font-size: 2rem; }
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid #2a2648;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
color: var(--gold);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-card p, .info-card ul {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-dim);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-card ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.info-card ul li::before {
|
||||
content: "> ";
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.links {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.links a {
|
||||
display: inline-block;
|
||||
margin: 0 12px;
|
||||
padding: 10px 24px;
|
||||
background: var(--bg-surface);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
border: 1px solid #2a2648;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.links a:hover {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 20px var(--accent-glow);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
border-top: 1px solid #1a1830;
|
||||
}
|
||||
|
||||
.click-to-focus {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(12, 10, 20, 0.7);
|
||||
z-index: 5;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.click-to-focus.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.click-to-focus span {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text);
|
||||
padding: 12px 24px;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hero">
|
||||
<h1>McRogueFace</h1>
|
||||
<p class="subtitle">A Python-Powered Roguelike Engine</p>
|
||||
<p class="tagline">
|
||||
C++ engine with Python scripting, compiled to WebAssembly.
|
||||
BSP dungeons, Wang tile autotiling, field of view, and turn-based combat
|
||||
— all running in your browser.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="canvas-wrapper">
|
||||
<div class="canvas-frame">
|
||||
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
|
||||
<div class="loading-overlay" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p id="status">Loading...</p>
|
||||
</div>
|
||||
<div class="click-to-focus hidden" id="focus-prompt">
|
||||
<span>Click to play</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<div class="info-grid">
|
||||
<div class="info-card">
|
||||
<h3>Controls</h3>
|
||||
<ul>
|
||||
<li>Arrow keys or WASD to move</li>
|
||||
<li>Bump into enemies to attack</li>
|
||||
<li>Walk over potions to heal</li>
|
||||
<li>Find stairs to descend deeper</li>
|
||||
<li>Period (.) to wait a turn</li>
|
||||
<li>R to restart after death</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>Engine Features</h3>
|
||||
<ul>
|
||||
<li>BSP dungeon generation (libtcod)</li>
|
||||
<li>Wang tile autotiling (Tiled .tsx)</li>
|
||||
<li>Field of view & fog of war</li>
|
||||
<li>Entity/Grid system with layers</li>
|
||||
<li>Python 3.14 scripting (full interpreter)</li>
|
||||
<li>SDL2 + OpenGL ES 2 rendering</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>About This Demo</h3>
|
||||
<p>
|
||||
This entire game is written in Python, running on the McRogueFace
|
||||
C++ engine compiled to WebAssembly via Emscripten. The game logic,
|
||||
dungeon generation, AI, and UI are all Python — the engine
|
||||
handles rendering, input, and the tile/entity system.
|
||||
</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>Tech Stack</h3>
|
||||
<ul>
|
||||
<li>C++17 game engine core</li>
|
||||
<li>Python 3.14 (cross-compiled to WASM)</li>
|
||||
<li>SDL2 + OpenGL ES 2 (via Emscripten)</li>
|
||||
<li>libtcod (BSP, FOV, pathfinding)</li>
|
||||
<li>Kenney Tiny Dungeon tileset</li>
|
||||
<li>~16 MB total download</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a href="https://github.com/jmccardle/McRogueFace">GitHub</a>
|
||||
<a href="https://gamedev.ffwf.net/gitea/john/McRogueFace">Gitea</a>
|
||||
<a href="https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki">Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
McRogueFace — Created for 7DRL 2023, actively developed.
|
||||
Engine by John McCardle. Tiles by Kenney.nl.
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var canvasElement = document.getElementById('canvas');
|
||||
var loadingElement = document.getElementById('loading');
|
||||
var statusElement = document.getElementById('status');
|
||||
var focusPrompt = document.getElementById('focus-prompt');
|
||||
var runtimeReady = false;
|
||||
var hasFocus = false;
|
||||
|
||||
function updateCanvasSize() {
|
||||
// Match canvas backing buffer to its display size
|
||||
var rect = canvasElement.getBoundingClientRect();
|
||||
var w = Math.floor(rect.width);
|
||||
var h = Math.floor(rect.height);
|
||||
if (runtimeReady) {
|
||||
Module.ccall('notify_canvas_resize', null, ['number', 'number'], [w, h]);
|
||||
} else {
|
||||
canvasElement.width = w;
|
||||
canvasElement.height = h;
|
||||
}
|
||||
}
|
||||
updateCanvasSize();
|
||||
window.addEventListener('resize', updateCanvasSize);
|
||||
|
||||
// Focus management
|
||||
canvasElement.addEventListener('click', function() {
|
||||
canvasElement.focus();
|
||||
hasFocus = true;
|
||||
focusPrompt.classList.add('hidden');
|
||||
});
|
||||
canvasElement.addEventListener('mousedown', function() {
|
||||
if (document.activeElement !== canvasElement) {
|
||||
canvasElement.focus();
|
||||
hasFocus = true;
|
||||
focusPrompt.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
canvasElement.addEventListener('blur', function() {
|
||||
if (runtimeReady) {
|
||||
hasFocus = false;
|
||||
focusPrompt.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
focusPrompt.addEventListener('click', function() {
|
||||
canvasElement.focus();
|
||||
hasFocus = true;
|
||||
focusPrompt.classList.add('hidden');
|
||||
});
|
||||
|
||||
var Module = {
|
||||
preRun: [function() {
|
||||
FS.mkdir('/save');
|
||||
FS.mount(IDBFS, {}, '/save');
|
||||
Module.addRunDependency('idbfs-restore');
|
||||
FS.syncfs(true, function(err) {
|
||||
if (err) console.error('Failed to restore /save/:', err);
|
||||
Module.removeRunDependency('idbfs-restore');
|
||||
});
|
||||
}],
|
||||
print: function(text) {
|
||||
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
||||
console.log(text);
|
||||
},
|
||||
printErr: function(text) {
|
||||
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
||||
console.error(text);
|
||||
},
|
||||
canvas: canvasElement,
|
||||
setStatus: function(text) {
|
||||
if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
|
||||
if (text === Module.setStatus.last.text) return;
|
||||
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
|
||||
var now = Date.now();
|
||||
if (m && now - Module.setStatus.last.time < 30) return;
|
||||
Module.setStatus.last.time = now;
|
||||
Module.setStatus.last.text = text;
|
||||
if (m) text = m[1];
|
||||
statusElement.textContent = text;
|
||||
},
|
||||
totalDependencies: 0,
|
||||
monitorRunDependencies: function(left) {
|
||||
this.totalDependencies = Math.max(this.totalDependencies, left);
|
||||
Module.setStatus(left
|
||||
? 'Preparing... (' + (this.totalDependencies - left) + '/' + this.totalDependencies + ')'
|
||||
: 'All downloads complete.');
|
||||
},
|
||||
onRuntimeInitialized: function() {
|
||||
runtimeReady = true;
|
||||
loadingElement.classList.add('hidden');
|
||||
canvasElement.focus();
|
||||
hasFocus = true;
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
};
|
||||
|
||||
Module.setStatus('Downloading...');
|
||||
|
||||
window.onerror = function(event) {
|
||||
Module.setStatus('Error! See browser console.');
|
||||
};
|
||||
|
||||
// Emscripten symbol resolver shim
|
||||
if (typeof resolveGlobalSymbol === 'undefined') {
|
||||
window.resolveGlobalSymbol = function(name, direct) {
|
||||
return {
|
||||
sym: Module['_' + name] || Module[name],
|
||||
type: 'function'
|
||||
};
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<script async src="mcrogueface.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue