Add cookbook and tutorial showcase demos
tests/demo/: - cookbook_showcase.py: Interactive demo of cookbook recipes - tutorial_showcase.py: Visual walkthrough of tutorial content - tutorial_screenshots.py: Automated screenshot generation - new_features_showcase.py: Demo of modern API features - procgen_showcase.py: Procedural generation examples - simple_showcase.py: Minimal working examples Created during docs modernization to verify cookbook examples work. 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
23afae69ad
commit
a1b692bb1f
6 changed files with 1734 additions and 0 deletions
495
tests/demo/cookbook_showcase.py
Normal file
495
tests/demo/cookbook_showcase.py
Normal file
|
|
@ -0,0 +1,495 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Cookbook Screenshot Showcase - Visual examples for cookbook recipes!
|
||||||
|
|
||||||
|
Generates beautiful screenshots for cookbook pages.
|
||||||
|
Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/cookbook_showcase.py
|
||||||
|
|
||||||
|
In headless mode, automation.screenshot() is SYNCHRONOUS - no timer dance needed!
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
from mcrfpy import automation
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Output directory - in the docs site images folder
|
||||||
|
OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/cookbook"
|
||||||
|
|
||||||
|
# Tile sprites from the labeled tileset
|
||||||
|
TILES = {
|
||||||
|
'player_knight': 84,
|
||||||
|
'player_mage': 85,
|
||||||
|
'player_rogue': 86,
|
||||||
|
'player_warrior': 87,
|
||||||
|
'enemy_slime': 108,
|
||||||
|
'enemy_orc': 120,
|
||||||
|
'enemy_skeleton': 123,
|
||||||
|
'floor_stone': 42,
|
||||||
|
'wall_stone': 30,
|
||||||
|
'wall_brick': 14,
|
||||||
|
'torch': 72,
|
||||||
|
'chest_closed': 89,
|
||||||
|
'item_potion': 113,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def screenshot_health_bar():
|
||||||
|
"""Create a health bar showcase."""
|
||||||
|
scene = mcrfpy.Scene("health_bar")
|
||||||
|
|
||||||
|
# Dark background
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600))
|
||||||
|
bg.fill_color = mcrfpy.Color(20, 20, 30)
|
||||||
|
scene.children.append(bg)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption(text="Health Bar Recipe", pos=(50, 30))
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 28
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
subtitle = mcrfpy.Caption(text="Nested frames for dynamic UI elements", pos=(50, 60))
|
||||||
|
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
subtitle.font_size = 16
|
||||||
|
scene.children.append(subtitle)
|
||||||
|
|
||||||
|
# Example health bars at different levels
|
||||||
|
y_start = 120
|
||||||
|
bar_configs = [
|
||||||
|
("Player - Full Health", 100, 100, mcrfpy.Color(50, 200, 50)),
|
||||||
|
("Player - Damaged", 65, 100, mcrfpy.Color(200, 200, 50)),
|
||||||
|
("Player - Critical", 20, 100, mcrfpy.Color(200, 50, 50)),
|
||||||
|
("Boss - 3/4 Health", 750, 1000, mcrfpy.Color(150, 50, 150)),
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, (label, current, maximum, color) in enumerate(bar_configs):
|
||||||
|
y = y_start + i * 100
|
||||||
|
|
||||||
|
# Label
|
||||||
|
lbl = mcrfpy.Caption(text=label, pos=(50, y))
|
||||||
|
lbl.fill_color = mcrfpy.Color(220, 220, 220)
|
||||||
|
lbl.font_size = 18
|
||||||
|
scene.children.append(lbl)
|
||||||
|
|
||||||
|
# Background bar
|
||||||
|
bar_bg = mcrfpy.Frame(pos=(50, y + 30), size=(400, 30))
|
||||||
|
bar_bg.fill_color = mcrfpy.Color(40, 40, 50)
|
||||||
|
bar_bg.outline = 2
|
||||||
|
bar_bg.outline_color = mcrfpy.Color(80, 80, 100)
|
||||||
|
scene.children.append(bar_bg)
|
||||||
|
|
||||||
|
# Fill bar (scaled to current/maximum)
|
||||||
|
fill_width = int(400 * (current / maximum))
|
||||||
|
bar_fill = mcrfpy.Frame(pos=(50, y + 30), size=(fill_width, 30))
|
||||||
|
bar_fill.fill_color = color
|
||||||
|
scene.children.append(bar_fill)
|
||||||
|
|
||||||
|
# Text overlay
|
||||||
|
hp_text = mcrfpy.Caption(text=f"{current}/{maximum}", pos=(60, y + 35))
|
||||||
|
hp_text.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
hp_text.font_size = 16
|
||||||
|
scene.children.append(hp_text)
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
output_path = os.path.join(OUTPUT_DIR, "ui_health_bar.png")
|
||||||
|
automation.screenshot(output_path)
|
||||||
|
print(f" -> {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def screenshot_fog_of_war():
|
||||||
|
"""Create a fog of war showcase."""
|
||||||
|
scene = mcrfpy.Scene("fog_of_war")
|
||||||
|
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 80),
|
||||||
|
size=(700, 480),
|
||||||
|
grid_size=(16, 12),
|
||||||
|
texture=texture,
|
||||||
|
zoom=2.8
|
||||||
|
)
|
||||||
|
grid.fill_color = mcrfpy.Color(0, 0, 0) # Black for unknown areas
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption(text="Fog of War Recipe", pos=(50, 20))
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 28
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
subtitle = mcrfpy.Caption(text="Visible, discovered, and unknown areas", pos=(50, 50))
|
||||||
|
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
subtitle.font_size = 16
|
||||||
|
scene.children.append(subtitle)
|
||||||
|
|
||||||
|
# Fill floor
|
||||||
|
for y in range(12):
|
||||||
|
for x in range(16):
|
||||||
|
grid.at(x, y).tilesprite = TILES['floor_stone']
|
||||||
|
|
||||||
|
# Add walls
|
||||||
|
for x in range(16):
|
||||||
|
grid.at(x, 0).tilesprite = TILES['wall_stone']
|
||||||
|
grid.at(x, 11).tilesprite = TILES['wall_stone']
|
||||||
|
for y in range(12):
|
||||||
|
grid.at(0, y).tilesprite = TILES['wall_stone']
|
||||||
|
grid.at(15, y).tilesprite = TILES['wall_stone']
|
||||||
|
|
||||||
|
# Interior walls (to break LOS)
|
||||||
|
for y in range(3, 8):
|
||||||
|
grid.at(8, y).tilesprite = TILES['wall_brick']
|
||||||
|
|
||||||
|
# Player (mage with light)
|
||||||
|
player = mcrfpy.Entity(grid_pos=(4, 6), texture=texture, sprite_index=TILES['player_mage'])
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Hidden enemies on the other side
|
||||||
|
enemy1 = mcrfpy.Entity(grid_pos=(12, 4), texture=texture, sprite_index=TILES['enemy_orc'])
|
||||||
|
grid.entities.append(enemy1)
|
||||||
|
enemy2 = mcrfpy.Entity(grid_pos=(13, 8), texture=texture, sprite_index=TILES['enemy_skeleton'])
|
||||||
|
grid.entities.append(enemy2)
|
||||||
|
|
||||||
|
# Torch in visible area
|
||||||
|
torch = mcrfpy.Entity(grid_pos=(2, 3), texture=texture, sprite_index=TILES['torch'])
|
||||||
|
grid.entities.append(torch)
|
||||||
|
|
||||||
|
grid.center = (4 * 16 + 8, 6 * 16 + 8)
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
output_path = os.path.join(OUTPUT_DIR, "grid_fog_of_war.png")
|
||||||
|
automation.screenshot(output_path)
|
||||||
|
print(f" -> {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def screenshot_combat_melee():
|
||||||
|
"""Create a melee combat showcase."""
|
||||||
|
scene = mcrfpy.Scene("combat_melee")
|
||||||
|
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 80),
|
||||||
|
size=(700, 480),
|
||||||
|
grid_size=(12, 9),
|
||||||
|
texture=texture,
|
||||||
|
zoom=3.5
|
||||||
|
)
|
||||||
|
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption(text="Melee Combat Recipe", pos=(50, 20))
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 28
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
subtitle = mcrfpy.Caption(text="Bump-to-attack mechanics with damage calculation", pos=(50, 50))
|
||||||
|
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
subtitle.font_size = 16
|
||||||
|
scene.children.append(subtitle)
|
||||||
|
|
||||||
|
# Fill with dirt floor (battle arena feel)
|
||||||
|
for y in range(9):
|
||||||
|
for x in range(12):
|
||||||
|
grid.at(x, y).tilesprite = 50 # dirt
|
||||||
|
|
||||||
|
# Brick walls
|
||||||
|
for x in range(12):
|
||||||
|
grid.at(x, 0).tilesprite = TILES['wall_brick']
|
||||||
|
grid.at(x, 8).tilesprite = TILES['wall_brick']
|
||||||
|
for y in range(9):
|
||||||
|
grid.at(0, y).tilesprite = TILES['wall_brick']
|
||||||
|
grid.at(11, y).tilesprite = TILES['wall_brick']
|
||||||
|
|
||||||
|
# Player knight engaging orc!
|
||||||
|
player = mcrfpy.Entity(grid_pos=(4, 4), texture=texture, sprite_index=TILES['player_knight'])
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
enemy = mcrfpy.Entity(grid_pos=(6, 4), texture=texture, sprite_index=TILES['enemy_orc'])
|
||||||
|
grid.entities.append(enemy)
|
||||||
|
|
||||||
|
# Fallen enemy (bones)
|
||||||
|
bones = mcrfpy.Entity(grid_pos=(8, 6), texture=texture, sprite_index=75) # bones
|
||||||
|
grid.entities.append(bones)
|
||||||
|
|
||||||
|
# Potion for healing
|
||||||
|
potion = mcrfpy.Entity(grid_pos=(3, 2), texture=texture, sprite_index=TILES['item_potion'])
|
||||||
|
grid.entities.append(potion)
|
||||||
|
|
||||||
|
grid.center = (5 * 16 + 8, 4 * 16 + 8)
|
||||||
|
|
||||||
|
# Combat log UI
|
||||||
|
log_frame = mcrfpy.Frame(pos=(50, 520), size=(700, 60))
|
||||||
|
log_frame.fill_color = mcrfpy.Color(30, 30, 40, 220)
|
||||||
|
log_frame.outline = 1
|
||||||
|
log_frame.outline_color = mcrfpy.Color(60, 60, 80)
|
||||||
|
scene.children.append(log_frame)
|
||||||
|
|
||||||
|
msg1 = mcrfpy.Caption(text="You hit the Orc for 8 damage!", pos=(10, 10))
|
||||||
|
msg1.fill_color = mcrfpy.Color(255, 200, 100)
|
||||||
|
msg1.font_size = 14
|
||||||
|
log_frame.children.append(msg1)
|
||||||
|
|
||||||
|
msg2 = mcrfpy.Caption(text="The Orc hits you for 4 damage!", pos=(10, 30))
|
||||||
|
msg2.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
|
msg2.font_size = 14
|
||||||
|
log_frame.children.append(msg2)
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
output_path = os.path.join(OUTPUT_DIR, "combat_melee.png")
|
||||||
|
automation.screenshot(output_path)
|
||||||
|
print(f" -> {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def screenshot_dungeon_generator():
|
||||||
|
"""Create a dungeon generator showcase."""
|
||||||
|
scene = mcrfpy.Scene("dungeon_gen")
|
||||||
|
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 80),
|
||||||
|
size=(700, 480),
|
||||||
|
grid_size=(24, 16),
|
||||||
|
texture=texture,
|
||||||
|
zoom=2.0
|
||||||
|
)
|
||||||
|
grid.fill_color = mcrfpy.Color(10, 10, 15)
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption(text="Dungeon Generator Recipe", pos=(50, 20))
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 28
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
subtitle = mcrfpy.Caption(text="Procedural rooms connected by corridors", pos=(50, 50))
|
||||||
|
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
subtitle.font_size = 16
|
||||||
|
scene.children.append(subtitle)
|
||||||
|
|
||||||
|
# Fill with walls
|
||||||
|
for y in range(16):
|
||||||
|
for x in range(24):
|
||||||
|
grid.at(x, y).tilesprite = TILES['wall_stone']
|
||||||
|
|
||||||
|
# Carve rooms
|
||||||
|
rooms = [
|
||||||
|
(2, 2, 6, 5), # Room 1
|
||||||
|
(10, 2, 7, 5), # Room 2
|
||||||
|
(18, 3, 5, 4), # Room 3
|
||||||
|
(2, 9, 5, 5), # Room 4
|
||||||
|
(10, 10, 6, 5), # Room 5
|
||||||
|
(18, 9, 5, 6), # Room 6
|
||||||
|
]
|
||||||
|
|
||||||
|
for rx, ry, rw, rh in rooms:
|
||||||
|
for y in range(ry, ry + rh):
|
||||||
|
for x in range(rx, rx + rw):
|
||||||
|
if x < 24 and y < 16:
|
||||||
|
grid.at(x, y).tilesprite = TILES['floor_stone']
|
||||||
|
|
||||||
|
# Carve corridors (horizontal and vertical)
|
||||||
|
# Room 1 to Room 2
|
||||||
|
for x in range(7, 11):
|
||||||
|
grid.at(x, 4).tilesprite = 50 # dirt corridor
|
||||||
|
# Room 2 to Room 3
|
||||||
|
for x in range(16, 19):
|
||||||
|
grid.at(x, 4).tilesprite = 50
|
||||||
|
# Room 1 to Room 4
|
||||||
|
for y in range(6, 10):
|
||||||
|
grid.at(4, y).tilesprite = 50
|
||||||
|
# Room 2 to Room 5
|
||||||
|
for y in range(6, 11):
|
||||||
|
grid.at(13, y).tilesprite = 50
|
||||||
|
# Room 3 to Room 6
|
||||||
|
for y in range(6, 10):
|
||||||
|
grid.at(20, y).tilesprite = 50
|
||||||
|
# Room 5 to Room 6
|
||||||
|
for x in range(15, 19):
|
||||||
|
grid.at(x, 12).tilesprite = 50
|
||||||
|
|
||||||
|
# Add player in first room
|
||||||
|
player = mcrfpy.Entity(grid_pos=(4, 4), texture=texture, sprite_index=TILES['player_knight'])
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Add decorations
|
||||||
|
grid.entities.append(mcrfpy.Entity(grid_pos=(3, 3), texture=texture, sprite_index=TILES['torch']))
|
||||||
|
grid.entities.append(mcrfpy.Entity(grid_pos=(12, 4), texture=texture, sprite_index=TILES['torch']))
|
||||||
|
grid.entities.append(mcrfpy.Entity(grid_pos=(19, 11), texture=texture, sprite_index=TILES['chest_closed']))
|
||||||
|
grid.entities.append(mcrfpy.Entity(grid_pos=(13, 12), texture=texture, sprite_index=TILES['enemy_slime']))
|
||||||
|
grid.entities.append(mcrfpy.Entity(grid_pos=(20, 5), texture=texture, sprite_index=TILES['enemy_skeleton']))
|
||||||
|
|
||||||
|
grid.center = (12 * 16, 8 * 16)
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
output_path = os.path.join(OUTPUT_DIR, "grid_dungeon_generator.png")
|
||||||
|
automation.screenshot(output_path)
|
||||||
|
print(f" -> {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def screenshot_floating_text():
|
||||||
|
"""Create a floating text/damage numbers showcase."""
|
||||||
|
scene = mcrfpy.Scene("floating_text")
|
||||||
|
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 100),
|
||||||
|
size=(700, 420),
|
||||||
|
grid_size=(12, 8),
|
||||||
|
texture=texture,
|
||||||
|
zoom=3.5
|
||||||
|
)
|
||||||
|
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption(text="Floating Text Recipe", pos=(50, 20))
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 28
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
subtitle = mcrfpy.Caption(text="Animated damage numbers and status messages", pos=(50, 50))
|
||||||
|
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
subtitle.font_size = 16
|
||||||
|
scene.children.append(subtitle)
|
||||||
|
|
||||||
|
# Fill floor
|
||||||
|
for y in range(8):
|
||||||
|
for x in range(12):
|
||||||
|
grid.at(x, y).tilesprite = TILES['floor_stone']
|
||||||
|
|
||||||
|
# Walls
|
||||||
|
for x in range(12):
|
||||||
|
grid.at(x, 0).tilesprite = TILES['wall_stone']
|
||||||
|
grid.at(x, 7).tilesprite = TILES['wall_stone']
|
||||||
|
for y in range(8):
|
||||||
|
grid.at(0, y).tilesprite = TILES['wall_stone']
|
||||||
|
grid.at(11, y).tilesprite = TILES['wall_stone']
|
||||||
|
|
||||||
|
# Player and enemy in combat
|
||||||
|
player = mcrfpy.Entity(grid_pos=(4, 4), texture=texture, sprite_index=TILES['player_warrior'])
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
enemy = mcrfpy.Entity(grid_pos=(7, 4), texture=texture, sprite_index=TILES['enemy_orc'])
|
||||||
|
grid.entities.append(enemy)
|
||||||
|
|
||||||
|
grid.center = (5.5 * 16, 4 * 16)
|
||||||
|
|
||||||
|
# Floating damage numbers (as captions positioned over entities)
|
||||||
|
# These would normally animate upward
|
||||||
|
dmg1 = mcrfpy.Caption(text="-12", pos=(330, 240))
|
||||||
|
dmg1.fill_color = mcrfpy.Color(255, 80, 80)
|
||||||
|
dmg1.font_size = 24
|
||||||
|
scene.children.append(dmg1)
|
||||||
|
|
||||||
|
dmg2 = mcrfpy.Caption(text="-5", pos=(500, 260))
|
||||||
|
dmg2.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
|
dmg2.font_size = 20
|
||||||
|
scene.children.append(dmg2)
|
||||||
|
|
||||||
|
crit = mcrfpy.Caption(text="CRITICAL!", pos=(280, 200))
|
||||||
|
crit.fill_color = mcrfpy.Color(255, 200, 50)
|
||||||
|
crit.font_size = 18
|
||||||
|
scene.children.append(crit)
|
||||||
|
|
||||||
|
heal = mcrfpy.Caption(text="+8", pos=(320, 280))
|
||||||
|
heal.fill_color = mcrfpy.Color(100, 255, 100)
|
||||||
|
heal.font_size = 20
|
||||||
|
scene.children.append(heal)
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
output_path = os.path.join(OUTPUT_DIR, "effects_floating_text.png")
|
||||||
|
automation.screenshot(output_path)
|
||||||
|
print(f" -> {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def screenshot_message_log():
|
||||||
|
"""Create a message log showcase."""
|
||||||
|
scene = mcrfpy.Scene("message_log")
|
||||||
|
|
||||||
|
# Dark background
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600))
|
||||||
|
bg.fill_color = mcrfpy.Color(20, 20, 30)
|
||||||
|
scene.children.append(bg)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption(text="Message Log Recipe", pos=(50, 30))
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 28
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
subtitle = mcrfpy.Caption(text="Scrollable combat and event messages", pos=(50, 60))
|
||||||
|
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
subtitle.font_size = 16
|
||||||
|
scene.children.append(subtitle)
|
||||||
|
|
||||||
|
# Message log frame
|
||||||
|
log_frame = mcrfpy.Frame(pos=(50, 100), size=(700, 400))
|
||||||
|
log_frame.fill_color = mcrfpy.Color(30, 30, 40)
|
||||||
|
log_frame.outline = 2
|
||||||
|
log_frame.outline_color = mcrfpy.Color(60, 60, 80)
|
||||||
|
scene.children.append(log_frame)
|
||||||
|
|
||||||
|
# Sample messages with colors
|
||||||
|
messages = [
|
||||||
|
("Welcome to the dungeon!", mcrfpy.Color(200, 200, 255)),
|
||||||
|
("You see a dark corridor ahead.", mcrfpy.Color(180, 180, 180)),
|
||||||
|
("A goblin appears!", mcrfpy.Color(255, 200, 100)),
|
||||||
|
("You hit the Goblin for 8 damage!", mcrfpy.Color(255, 255, 150)),
|
||||||
|
("The Goblin hits you for 3 damage!", mcrfpy.Color(255, 100, 100)),
|
||||||
|
("You hit the Goblin for 12 damage! Critical hit!", mcrfpy.Color(255, 200, 50)),
|
||||||
|
("The Goblin dies!", mcrfpy.Color(150, 255, 150)),
|
||||||
|
("You found a Healing Potion.", mcrfpy.Color(100, 200, 255)),
|
||||||
|
("An Orc blocks your path!", mcrfpy.Color(255, 150, 100)),
|
||||||
|
("You drink the Healing Potion. +15 HP", mcrfpy.Color(100, 255, 100)),
|
||||||
|
("You hit the Orc for 6 damage!", mcrfpy.Color(255, 255, 150)),
|
||||||
|
("The Orc hits you for 8 damage!", mcrfpy.Color(255, 100, 100)),
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, (msg, color) in enumerate(messages):
|
||||||
|
caption = mcrfpy.Caption(text=msg, pos=(15, 15 + i * 30))
|
||||||
|
caption.fill_color = color
|
||||||
|
caption.font_size = 16
|
||||||
|
log_frame.children.append(caption)
|
||||||
|
|
||||||
|
# Scroll indicator
|
||||||
|
scroll = mcrfpy.Caption(text="▼ More messages below", pos=(580, 370))
|
||||||
|
scroll.fill_color = mcrfpy.Color(100, 100, 120)
|
||||||
|
scroll.font_size = 12
|
||||||
|
log_frame.children.append(scroll)
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
output_path = os.path.join(OUTPUT_DIR, "ui_message_log.png")
|
||||||
|
automation.screenshot(output_path)
|
||||||
|
print(f" -> {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Generate all cookbook screenshots!"""
|
||||||
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
print("=== Cookbook Screenshot Showcase ===")
|
||||||
|
print(f"Output: {OUTPUT_DIR}\n")
|
||||||
|
|
||||||
|
showcases = [
|
||||||
|
('Health Bar UI', screenshot_health_bar),
|
||||||
|
('Fog of War', screenshot_fog_of_war),
|
||||||
|
('Melee Combat', screenshot_combat_melee),
|
||||||
|
('Dungeon Generator', screenshot_dungeon_generator),
|
||||||
|
('Floating Text', screenshot_floating_text),
|
||||||
|
('Message Log', screenshot_message_log),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, func in showcases:
|
||||||
|
print(f"Generating {name}...")
|
||||||
|
try:
|
||||||
|
func()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ERROR: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
print("\n=== All cookbook screenshots generated! ===")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
255
tests/demo/new_features_showcase.py
Normal file
255
tests/demo/new_features_showcase.py
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
New Features Screenshot Showcase - Alignment + Dijkstra-to-HeightMap
|
||||||
|
|
||||||
|
Generates screenshots for the new API cookbook recipes.
|
||||||
|
Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/new_features_showcase.py
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
from mcrfpy import automation
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/cookbook"
|
||||||
|
|
||||||
|
|
||||||
|
def screenshot_alignment():
|
||||||
|
"""Create an alignment system showcase."""
|
||||||
|
scene = mcrfpy.Scene("alignment")
|
||||||
|
|
||||||
|
# Dark background
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600))
|
||||||
|
bg.fill_color = mcrfpy.Color(20, 20, 30)
|
||||||
|
scene.children.append(bg)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption(text="UI Alignment System", pos=(50, 20))
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 28
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
subtitle = mcrfpy.Caption(text="Auto-positioning with reactive resize", pos=(50, 50))
|
||||||
|
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
subtitle.font_size = 16
|
||||||
|
scene.children.append(subtitle)
|
||||||
|
|
||||||
|
# Demo container
|
||||||
|
container = mcrfpy.Frame(pos=(100, 100), size=(600, 400))
|
||||||
|
container.fill_color = mcrfpy.Color(40, 40, 50)
|
||||||
|
container.outline = 2
|
||||||
|
container.outline_color = mcrfpy.Color(80, 80, 100)
|
||||||
|
scene.children.append(container)
|
||||||
|
|
||||||
|
# Container label
|
||||||
|
container_label = mcrfpy.Caption(text="Parent Container (600x400)", pos=(10, 10))
|
||||||
|
container_label.fill_color = mcrfpy.Color(100, 100, 120)
|
||||||
|
container_label.font_size = 12
|
||||||
|
container.children.append(container_label)
|
||||||
|
|
||||||
|
# 9 alignment positions demo
|
||||||
|
alignments = [
|
||||||
|
(mcrfpy.Alignment.TOP_LEFT, "TL", mcrfpy.Color(200, 80, 80)),
|
||||||
|
(mcrfpy.Alignment.TOP_CENTER, "TC", mcrfpy.Color(200, 150, 80)),
|
||||||
|
(mcrfpy.Alignment.TOP_RIGHT, "TR", mcrfpy.Color(200, 200, 80)),
|
||||||
|
(mcrfpy.Alignment.CENTER_LEFT, "CL", mcrfpy.Color(80, 200, 80)),
|
||||||
|
(mcrfpy.Alignment.CENTER, "C", mcrfpy.Color(80, 200, 200)),
|
||||||
|
(mcrfpy.Alignment.CENTER_RIGHT, "CR", mcrfpy.Color(80, 80, 200)),
|
||||||
|
(mcrfpy.Alignment.BOTTOM_LEFT, "BL", mcrfpy.Color(150, 80, 200)),
|
||||||
|
(mcrfpy.Alignment.BOTTOM_CENTER, "BC", mcrfpy.Color(200, 80, 200)),
|
||||||
|
(mcrfpy.Alignment.BOTTOM_RIGHT, "BR", mcrfpy.Color(200, 80, 150)),
|
||||||
|
]
|
||||||
|
|
||||||
|
for align, label, color in alignments:
|
||||||
|
box = mcrfpy.Frame(pos=(0, 0), size=(60, 40))
|
||||||
|
box.fill_color = color
|
||||||
|
box.outline = 1
|
||||||
|
box.outline_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
box.align = align
|
||||||
|
if align != mcrfpy.Alignment.CENTER:
|
||||||
|
box.margin = 15.0
|
||||||
|
|
||||||
|
# Label inside box
|
||||||
|
text = mcrfpy.Caption(text=label, pos=(0, 0))
|
||||||
|
text.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
text.font_size = 16
|
||||||
|
text.align = mcrfpy.Alignment.CENTER
|
||||||
|
box.children.append(text)
|
||||||
|
|
||||||
|
container.children.append(box)
|
||||||
|
|
||||||
|
# Legend
|
||||||
|
legend = mcrfpy.Caption(text="TL=TOP_LEFT TC=TOP_CENTER TR=TOP_RIGHT etc.", pos=(100, 520))
|
||||||
|
legend.fill_color = mcrfpy.Color(150, 150, 170)
|
||||||
|
legend.font_size = 14
|
||||||
|
scene.children.append(legend)
|
||||||
|
|
||||||
|
legend2 = mcrfpy.Caption(text="All boxes have margin=15 except CENTER", pos=(100, 545))
|
||||||
|
legend2.fill_color = mcrfpy.Color(150, 150, 170)
|
||||||
|
legend2.font_size = 14
|
||||||
|
scene.children.append(legend2)
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
output_path = os.path.join(OUTPUT_DIR, "ui_alignment.png")
|
||||||
|
automation.screenshot(output_path)
|
||||||
|
print(f" -> {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def screenshot_dijkstra_heightmap():
|
||||||
|
"""Create a dijkstra-to-heightmap showcase."""
|
||||||
|
scene = mcrfpy.Scene("dijkstra_hmap")
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption(text="Dijkstra to HeightMap", pos=(50, 20))
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 28
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
subtitle = mcrfpy.Caption(text="Distance-based gradients for fog, difficulty, and visualization", pos=(50, 50))
|
||||||
|
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
subtitle.font_size = 16
|
||||||
|
scene.children.append(subtitle)
|
||||||
|
|
||||||
|
# Create grid for dijkstra visualization
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 90),
|
||||||
|
size=(350, 350),
|
||||||
|
grid_size=(16, 16),
|
||||||
|
texture=texture,
|
||||||
|
zoom=1.3
|
||||||
|
)
|
||||||
|
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# Initialize grid
|
||||||
|
for y in range(16):
|
||||||
|
for x in range(16):
|
||||||
|
grid.at((x, y)).walkable = True
|
||||||
|
grid.at((x, y)).tilesprite = 42 # floor
|
||||||
|
|
||||||
|
# Add some walls
|
||||||
|
for i in range(5, 11):
|
||||||
|
grid.at((i, 5)).walkable = False
|
||||||
|
grid.at((i, 5)).tilesprite = 30 # wall
|
||||||
|
grid.at((5, i)).walkable = False
|
||||||
|
grid.at((5, i)).tilesprite = 30
|
||||||
|
|
||||||
|
# Player at center
|
||||||
|
player = mcrfpy.Entity(grid_pos=(8, 8), texture=texture, sprite_index=84)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Get dijkstra and create color visualization
|
||||||
|
dijkstra = grid.get_dijkstra_map((8, 8))
|
||||||
|
hmap = dijkstra.to_heightmap(unreachable=-1.0)
|
||||||
|
|
||||||
|
# Find max for normalization
|
||||||
|
max_dist = 0
|
||||||
|
for y in range(16):
|
||||||
|
for x in range(16):
|
||||||
|
d = hmap[(x, y)]
|
||||||
|
if d > max_dist and d >= 0:
|
||||||
|
max_dist = d
|
||||||
|
|
||||||
|
# Second visualization panel - color gradient
|
||||||
|
viz_frame = mcrfpy.Frame(pos=(420, 90), size=(350, 350))
|
||||||
|
viz_frame.fill_color = mcrfpy.Color(30, 30, 40)
|
||||||
|
viz_frame.outline = 2
|
||||||
|
viz_frame.outline_color = mcrfpy.Color(60, 60, 80)
|
||||||
|
scene.children.append(viz_frame)
|
||||||
|
|
||||||
|
viz_label = mcrfpy.Caption(text="Distance Visualization", pos=(80, 10))
|
||||||
|
viz_label.fill_color = mcrfpy.Color(200, 200, 220)
|
||||||
|
viz_label.font_size = 16
|
||||||
|
viz_frame.children.append(viz_label)
|
||||||
|
|
||||||
|
# Draw colored squares for each cell
|
||||||
|
cell_size = 20
|
||||||
|
offset_x = 15
|
||||||
|
offset_y = 35
|
||||||
|
|
||||||
|
for y in range(16):
|
||||||
|
for x in range(16):
|
||||||
|
dist = hmap[(x, y)]
|
||||||
|
|
||||||
|
if dist < 0:
|
||||||
|
# Unreachable - dark red
|
||||||
|
color = mcrfpy.Color(60, 0, 0)
|
||||||
|
elif dist == 0:
|
||||||
|
# Source - bright yellow
|
||||||
|
color = mcrfpy.Color(255, 255, 0)
|
||||||
|
else:
|
||||||
|
# Gradient: green (near) to blue (far)
|
||||||
|
t = min(1.0, dist / max_dist)
|
||||||
|
r = 0
|
||||||
|
g = int(200 * (1 - t))
|
||||||
|
b = int(200 * t)
|
||||||
|
color = mcrfpy.Color(r, g, b)
|
||||||
|
|
||||||
|
cell = mcrfpy.Frame(
|
||||||
|
pos=(offset_x + x * cell_size, offset_y + y * cell_size),
|
||||||
|
size=(cell_size - 1, cell_size - 1)
|
||||||
|
)
|
||||||
|
cell.fill_color = color
|
||||||
|
viz_frame.children.append(cell)
|
||||||
|
|
||||||
|
# Legend
|
||||||
|
legend_frame = mcrfpy.Frame(pos=(50, 460), size=(720, 100))
|
||||||
|
legend_frame.fill_color = mcrfpy.Color(30, 30, 40)
|
||||||
|
legend_frame.outline = 1
|
||||||
|
legend_frame.outline_color = mcrfpy.Color(60, 60, 80)
|
||||||
|
scene.children.append(legend_frame)
|
||||||
|
|
||||||
|
leg1 = mcrfpy.Caption(text="Use Cases:", pos=(15, 10))
|
||||||
|
leg1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
leg1.font_size = 16
|
||||||
|
legend_frame.children.append(leg1)
|
||||||
|
|
||||||
|
uses = [
|
||||||
|
"Distance-based enemy difficulty",
|
||||||
|
"Fog intensity gradients",
|
||||||
|
"Pathfinding visualization",
|
||||||
|
"Influence maps for AI",
|
||||||
|
]
|
||||||
|
for i, use in enumerate(uses):
|
||||||
|
txt = mcrfpy.Caption(text=f"- {use}", pos=(15 + (i // 2) * 350, 35 + (i % 2) * 25))
|
||||||
|
txt.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
txt.font_size = 14
|
||||||
|
legend_frame.children.append(txt)
|
||||||
|
|
||||||
|
# Color key
|
||||||
|
key_label = mcrfpy.Caption(text="Yellow=Source Green=Near Blue=Far Red=Blocked", pos=(420, 450))
|
||||||
|
key_label.fill_color = mcrfpy.Color(150, 150, 170)
|
||||||
|
key_label.font_size = 12
|
||||||
|
scene.children.append(key_label)
|
||||||
|
|
||||||
|
scene.activate()
|
||||||
|
output_path = os.path.join(OUTPUT_DIR, "grid_dijkstra_heightmap.png")
|
||||||
|
automation.screenshot(output_path)
|
||||||
|
print(f" -> {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
print("=== New Features Screenshot Showcase ===")
|
||||||
|
print(f"Output: {OUTPUT_DIR}\n")
|
||||||
|
|
||||||
|
showcases = [
|
||||||
|
('Alignment System', screenshot_alignment),
|
||||||
|
('Dijkstra to HeightMap', screenshot_dijkstra_heightmap),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, func in showcases:
|
||||||
|
print(f"Generating {name}...")
|
||||||
|
try:
|
||||||
|
func()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ERROR: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
print("\n=== New feature screenshots generated! ===")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
286
tests/demo/procgen_showcase.py
Normal file
286
tests/demo/procgen_showcase.py
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate screenshots for procgen cookbook recipes.
|
||||||
|
|
||||||
|
Uses Frame-based visualization since Grid cell colors use ColorLayer API.
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
from mcrfpy import automation
|
||||||
|
import sys
|
||||||
|
|
||||||
|
OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/cookbook"
|
||||||
|
|
||||||
|
# Simple PRNG
|
||||||
|
_seed = 42
|
||||||
|
|
||||||
|
def random():
|
||||||
|
global _seed
|
||||||
|
_seed = (_seed * 1103515245 + 12345) & 0x7fffffff
|
||||||
|
return (_seed >> 16) / 32768.0
|
||||||
|
|
||||||
|
def seed(n):
|
||||||
|
global _seed
|
||||||
|
_seed = n
|
||||||
|
|
||||||
|
def choice(lst):
|
||||||
|
return lst[int(random() * len(lst))]
|
||||||
|
|
||||||
|
|
||||||
|
def screenshot_cellular_caves():
|
||||||
|
"""Generate cellular automata caves visualization."""
|
||||||
|
print("Generating cellular automata caves...")
|
||||||
|
|
||||||
|
scene = mcrfpy.Scene("caves")
|
||||||
|
scene.activate()
|
||||||
|
mcrfpy.step(0.1)
|
||||||
|
|
||||||
|
# Background
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(640, 500))
|
||||||
|
bg.fill_color = mcrfpy.Color(15, 15, 25)
|
||||||
|
scene.children.append(bg)
|
||||||
|
|
||||||
|
width, height = 50, 35
|
||||||
|
cell_size = 12
|
||||||
|
seed(42)
|
||||||
|
|
||||||
|
# Store cell data
|
||||||
|
cells = [[False for _ in range(width)] for _ in range(height)]
|
||||||
|
|
||||||
|
# Step 1: Random noise (45% walls)
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
if x == 0 or x == width-1 or y == 0 or y == height-1:
|
||||||
|
cells[y][x] = True # Border walls
|
||||||
|
else:
|
||||||
|
cells[y][x] = random() < 0.45
|
||||||
|
|
||||||
|
# Step 2: Smooth with cellular automata (5 iterations)
|
||||||
|
for _ in range(5):
|
||||||
|
new_cells = [[cells[y][x] for x in range(width)] for y in range(height)]
|
||||||
|
for y in range(1, height - 1):
|
||||||
|
for x in range(1, width - 1):
|
||||||
|
wall_count = sum(
|
||||||
|
1 for dy in [-1, 0, 1] for dx in [-1, 0, 1]
|
||||||
|
if not (dx == 0 and dy == 0) and cells[y + dy][x + dx]
|
||||||
|
)
|
||||||
|
if wall_count >= 5:
|
||||||
|
new_cells[y][x] = True
|
||||||
|
elif wall_count <= 3:
|
||||||
|
new_cells[y][x] = False
|
||||||
|
cells = new_cells
|
||||||
|
|
||||||
|
# Find largest connected region
|
||||||
|
visited = set()
|
||||||
|
regions = []
|
||||||
|
|
||||||
|
def flood_fill(start_x, start_y):
|
||||||
|
result = []
|
||||||
|
stack = [(start_x, start_y)]
|
||||||
|
while stack:
|
||||||
|
x, y = stack.pop()
|
||||||
|
if (x, y) in visited or x < 0 or x >= width or y < 0 or y >= height:
|
||||||
|
continue
|
||||||
|
if cells[y][x]: # Wall
|
||||||
|
continue
|
||||||
|
visited.add((x, y))
|
||||||
|
result.append((x, y))
|
||||||
|
stack.extend([(x+1, y), (x-1, y), (x, y+1), (x, y-1)])
|
||||||
|
return result
|
||||||
|
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
if (x, y) not in visited and not cells[y][x]:
|
||||||
|
region = flood_fill(x, y)
|
||||||
|
if region:
|
||||||
|
regions.append(region)
|
||||||
|
|
||||||
|
largest = max(regions, key=len) if regions else []
|
||||||
|
largest_set = set(largest)
|
||||||
|
|
||||||
|
# Draw cells as colored frames
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
px = 20 + x * cell_size
|
||||||
|
py = 20 + y * cell_size
|
||||||
|
cell = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1))
|
||||||
|
|
||||||
|
if cells[y][x]:
|
||||||
|
cell.fill_color = mcrfpy.Color(60, 40, 30) # Wall
|
||||||
|
elif (x, y) in largest_set:
|
||||||
|
cell.fill_color = mcrfpy.Color(50, 90, 100) # Main cave
|
||||||
|
else:
|
||||||
|
cell.fill_color = mcrfpy.Color(45, 35, 30) # Filled region
|
||||||
|
|
||||||
|
scene.children.append(cell)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption(text="Cellular Automata Caves", pos=(20, 445))
|
||||||
|
title.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
title.font_size = 18
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
subtitle = mcrfpy.Caption(text="45% fill, 5 iterations, largest region preserved", pos=(20, 468))
|
||||||
|
subtitle.fill_color = mcrfpy.Color(130, 130, 140)
|
||||||
|
subtitle.font_size = 12
|
||||||
|
scene.children.append(subtitle)
|
||||||
|
|
||||||
|
mcrfpy.step(0.1)
|
||||||
|
automation.screenshot(OUTPUT_DIR + "/procgen_cellular_caves.png")
|
||||||
|
print("Saved: procgen_cellular_caves.png")
|
||||||
|
|
||||||
|
|
||||||
|
def screenshot_wfc():
|
||||||
|
"""Generate WFC pattern visualization."""
|
||||||
|
print("Generating WFC patterns...")
|
||||||
|
|
||||||
|
scene = mcrfpy.Scene("wfc")
|
||||||
|
scene.activate()
|
||||||
|
mcrfpy.step(0.1)
|
||||||
|
|
||||||
|
# Background
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(640, 500))
|
||||||
|
bg.fill_color = mcrfpy.Color(15, 20, 15)
|
||||||
|
scene.children.append(bg)
|
||||||
|
|
||||||
|
width, height = 40, 28
|
||||||
|
cell_size = 15
|
||||||
|
seed(123)
|
||||||
|
|
||||||
|
GRASS, DIRT, WATER, SAND = 0, 1, 2, 3
|
||||||
|
colors = {
|
||||||
|
GRASS: mcrfpy.Color(60, 120, 50),
|
||||||
|
DIRT: mcrfpy.Color(100, 70, 40),
|
||||||
|
WATER: mcrfpy.Color(40, 80, 140),
|
||||||
|
SAND: mcrfpy.Color(180, 160, 90)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = {
|
||||||
|
GRASS: {'N': [GRASS, DIRT, SAND], 'S': [GRASS, DIRT, SAND],
|
||||||
|
'E': [GRASS, DIRT, SAND], 'W': [GRASS, DIRT, SAND]},
|
||||||
|
DIRT: {'N': [GRASS, DIRT], 'S': [GRASS, DIRT],
|
||||||
|
'E': [GRASS, DIRT], 'W': [GRASS, DIRT]},
|
||||||
|
WATER: {'N': [WATER, SAND], 'S': [WATER, SAND],
|
||||||
|
'E': [WATER, SAND], 'W': [WATER, SAND]},
|
||||||
|
SAND: {'N': [GRASS, WATER, SAND], 'S': [GRASS, WATER, SAND],
|
||||||
|
'E': [GRASS, WATER, SAND], 'W': [GRASS, WATER, SAND]}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles = set(rules.keys())
|
||||||
|
possibilities = {(x, y): set(tiles) for y in range(height) for x in range(width)}
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# Seed water lake
|
||||||
|
for x in range(22, 32):
|
||||||
|
for y in range(8, 18):
|
||||||
|
possibilities[(x, y)] = {WATER}
|
||||||
|
result[(x, y)] = WATER
|
||||||
|
|
||||||
|
# Seed dirt path
|
||||||
|
for y in range(10, 18):
|
||||||
|
possibilities[(3, y)] = {DIRT}
|
||||||
|
result[(3, y)] = DIRT
|
||||||
|
|
||||||
|
directions = {'N': (0, -1), 'S': (0, 1), 'E': (1, 0), 'W': (-1, 0)}
|
||||||
|
|
||||||
|
def propagate(sx, sy):
|
||||||
|
stack = [(sx, sy)]
|
||||||
|
while stack:
|
||||||
|
x, y = stack.pop()
|
||||||
|
current = possibilities[(x, y)]
|
||||||
|
for dir_name, (dx, dy) in directions.items():
|
||||||
|
nx, ny = x + dx, y + dy
|
||||||
|
if not (0 <= nx < width and 0 <= ny < height):
|
||||||
|
continue
|
||||||
|
neighbor = possibilities[(nx, ny)]
|
||||||
|
if len(neighbor) == 1:
|
||||||
|
continue
|
||||||
|
allowed = set()
|
||||||
|
for tile in current:
|
||||||
|
if dir_name in rules[tile]:
|
||||||
|
allowed.update(rules[tile][dir_name])
|
||||||
|
new_opts = neighbor & allowed
|
||||||
|
if new_opts and new_opts != neighbor:
|
||||||
|
possibilities[(nx, ny)] = new_opts
|
||||||
|
stack.append((nx, ny))
|
||||||
|
|
||||||
|
# Propagate from seeds
|
||||||
|
for x in range(22, 32):
|
||||||
|
for y in range(8, 18):
|
||||||
|
propagate(x, y)
|
||||||
|
for y in range(10, 18):
|
||||||
|
propagate(3, y)
|
||||||
|
|
||||||
|
# Collapse
|
||||||
|
for _ in range(width * height):
|
||||||
|
best, best_e = None, 1000.0
|
||||||
|
for pos, opts in possibilities.items():
|
||||||
|
if len(opts) > 1:
|
||||||
|
e = len(opts) + random() * 0.1
|
||||||
|
if e < best_e:
|
||||||
|
best_e, best = e, pos
|
||||||
|
|
||||||
|
if best is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
x, y = best
|
||||||
|
opts = list(possibilities[(x, y)])
|
||||||
|
if not opts:
|
||||||
|
break
|
||||||
|
|
||||||
|
weights = {GRASS: 5, DIRT: 2, WATER: 1, SAND: 2}
|
||||||
|
weighted = []
|
||||||
|
for t in opts:
|
||||||
|
weighted.extend([t] * weights.get(t, 1))
|
||||||
|
chosen = choice(weighted) if weighted else GRASS
|
||||||
|
|
||||||
|
possibilities[(x, y)] = {chosen}
|
||||||
|
result[(x, y)] = chosen
|
||||||
|
propagate(x, y)
|
||||||
|
|
||||||
|
# Fill remaining
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
if (x, y) not in result:
|
||||||
|
opts = list(possibilities[(x, y)])
|
||||||
|
result[(x, y)] = choice(opts) if opts else GRASS
|
||||||
|
|
||||||
|
# Draw
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
px = 20 + x * cell_size
|
||||||
|
py = 20 + y * cell_size
|
||||||
|
cell = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1))
|
||||||
|
cell.fill_color = colors[result[(x, y)]]
|
||||||
|
scene.children.append(cell)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption(text="Wave Function Collapse", pos=(20, 445))
|
||||||
|
title.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
title.font_size = 18
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
subtitle = mcrfpy.Caption(text="Constraint-based terrain (seeded lake + path)", pos=(20, 468))
|
||||||
|
subtitle.fill_color = mcrfpy.Color(130, 140, 130)
|
||||||
|
subtitle.font_size = 12
|
||||||
|
scene.children.append(subtitle)
|
||||||
|
|
||||||
|
# Legend
|
||||||
|
for i, (name, tid) in enumerate([("Grass", GRASS), ("Dirt", DIRT), ("Sand", SAND), ("Water", WATER)]):
|
||||||
|
lx, ly = 480, 445 + i * 14
|
||||||
|
swatch = mcrfpy.Frame(pos=(lx, ly), size=(12, 12))
|
||||||
|
swatch.fill_color = colors[tid]
|
||||||
|
scene.children.append(swatch)
|
||||||
|
label = mcrfpy.Caption(text=name, pos=(lx + 16, ly))
|
||||||
|
label.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
|
label.font_size = 11
|
||||||
|
scene.children.append(label)
|
||||||
|
|
||||||
|
mcrfpy.step(0.1)
|
||||||
|
automation.screenshot(OUTPUT_DIR + "/procgen_wfc.png")
|
||||||
|
print("Saved: procgen_wfc.png")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
screenshot_cellular_caves()
|
||||||
|
screenshot_wfc()
|
||||||
|
print("\nDone!")
|
||||||
|
sys.exit(0)
|
||||||
103
tests/demo/simple_showcase.py
Normal file
103
tests/demo/simple_showcase.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple Tutorial Screenshot Generator
|
||||||
|
|
||||||
|
This creates ONE screenshot - the part01 tutorial showcase.
|
||||||
|
Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/simple_showcase.py
|
||||||
|
|
||||||
|
NOTE: In headless mode, automation.screenshot() is SYNCHRONOUS - it renders
|
||||||
|
and captures immediately. No timer dance needed!
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
from mcrfpy import automation
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Output
|
||||||
|
OUTPUT_PATH = "/opt/goblincorps/repos/mcrogueface.github.io/images/tutorials/part_01_grid_movement.png"
|
||||||
|
|
||||||
|
# Tile sprites from the labeled tileset
|
||||||
|
PLAYER_KNIGHT = 84
|
||||||
|
FLOOR_STONE = 42
|
||||||
|
WALL_STONE = 30
|
||||||
|
TORCH = 72
|
||||||
|
BARREL = 73
|
||||||
|
SKULL = 74
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Create the part01 showcase screenshot."""
|
||||||
|
# Ensure output dir exists
|
||||||
|
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
|
||||||
|
|
||||||
|
# Create scene
|
||||||
|
scene = mcrfpy.Scene("showcase")
|
||||||
|
|
||||||
|
# Load texture
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
|
||||||
|
# Create grid - bigger zoom for visibility
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 80),
|
||||||
|
size=(700, 480),
|
||||||
|
grid_size=(12, 9),
|
||||||
|
texture=texture,
|
||||||
|
zoom=3.5
|
||||||
|
)
|
||||||
|
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||||
|
scene.children.append(grid)
|
||||||
|
|
||||||
|
# Fill with floor
|
||||||
|
for y in range(9):
|
||||||
|
for x in range(12):
|
||||||
|
grid.at(x, y).tilesprite = FLOOR_STONE
|
||||||
|
|
||||||
|
# Add wall border
|
||||||
|
for x in range(12):
|
||||||
|
grid.at(x, 0).tilesprite = WALL_STONE
|
||||||
|
grid.at(x, 0).walkable = False
|
||||||
|
grid.at(x, 8).tilesprite = WALL_STONE
|
||||||
|
grid.at(x, 8).walkable = False
|
||||||
|
for y in range(9):
|
||||||
|
grid.at(0, y).tilesprite = WALL_STONE
|
||||||
|
grid.at(0, y).walkable = False
|
||||||
|
grid.at(11, y).tilesprite = WALL_STONE
|
||||||
|
grid.at(11, y).walkable = False
|
||||||
|
|
||||||
|
# Add player entity - a knight!
|
||||||
|
player = mcrfpy.Entity(
|
||||||
|
grid_pos=(6, 4),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=PLAYER_KNIGHT
|
||||||
|
)
|
||||||
|
grid.entities.append(player)
|
||||||
|
|
||||||
|
# Add decorations
|
||||||
|
for pos, sprite in [((2, 2), TORCH), ((9, 2), TORCH), ((2, 6), BARREL), ((9, 6), SKULL)]:
|
||||||
|
entity = mcrfpy.Entity(grid_pos=pos, texture=texture, sprite_index=sprite)
|
||||||
|
grid.entities.append(entity)
|
||||||
|
|
||||||
|
# Center camera on player
|
||||||
|
grid.center = (6 * 16 + 8, 4 * 16 + 8)
|
||||||
|
|
||||||
|
# Add title
|
||||||
|
title = mcrfpy.Caption(text="Part 1: The '@' and the Dungeon Grid", pos=(50, 20))
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 28
|
||||||
|
scene.children.append(title)
|
||||||
|
|
||||||
|
subtitle = mcrfpy.Caption(text="Creating a grid, placing entities, handling input", pos=(50, 50))
|
||||||
|
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
subtitle.font_size = 16
|
||||||
|
scene.children.append(subtitle)
|
||||||
|
|
||||||
|
# Activate scene
|
||||||
|
scene.activate()
|
||||||
|
|
||||||
|
# In headless mode, screenshot() is synchronous - renders then captures!
|
||||||
|
result = automation.screenshot(OUTPUT_PATH)
|
||||||
|
print(f"Screenshot saved: {OUTPUT_PATH} (result: {result})")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
# Run it
|
||||||
|
main()
|
||||||
169
tests/demo/tutorial_screenshots.py
Normal file
169
tests/demo/tutorial_screenshots.py
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Tutorial Screenshot Generator
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
./mcrogueface --headless --exec tests/demo/tutorial_screenshots.py
|
||||||
|
|
||||||
|
Extracts code from tutorial markdown files and generates screenshots.
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
from mcrfpy import automation
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Paths
|
||||||
|
DOCS_REPO = "/opt/goblincorps/repos/mcrogueface.github.io"
|
||||||
|
TUTORIAL_DIR = os.path.join(DOCS_REPO, "tutorial")
|
||||||
|
OUTPUT_DIR = os.path.join(DOCS_REPO, "images", "tutorials")
|
||||||
|
|
||||||
|
# Tutorials to process (in order)
|
||||||
|
TUTORIALS = [
|
||||||
|
"part_01_grid_movement.md",
|
||||||
|
"part_02_tiles_collision.md",
|
||||||
|
"part_03_dungeon_generation.md",
|
||||||
|
"part_04_fov.md",
|
||||||
|
"part_05_enemies.md",
|
||||||
|
"part_06_combat.md",
|
||||||
|
"part_07_ui.md",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_code_from_markdown(filepath):
|
||||||
|
"""Extract the main Python code block from a tutorial markdown file."""
|
||||||
|
with open(filepath, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Find code blocks after "## The Complete Code" header
|
||||||
|
# Look for the first python code block after that header
|
||||||
|
complete_code_match = re.search(
|
||||||
|
r'##\s+The Complete Code.*?```python\s*\n(.*?)```',
|
||||||
|
content,
|
||||||
|
re.DOTALL | re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
if complete_code_match:
|
||||||
|
return complete_code_match.group(1)
|
||||||
|
|
||||||
|
# Fallback: just get the first large python code block
|
||||||
|
code_blocks = re.findall(r'```python\s*\n(.*?)```', content, re.DOTALL)
|
||||||
|
if code_blocks:
|
||||||
|
# Return the largest code block (likely the main example)
|
||||||
|
return max(code_blocks, key=len)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def add_screenshot_hook(code, screenshot_path):
|
||||||
|
"""Add screenshot capture code to the end of the script."""
|
||||||
|
# Add code to take screenshot after a brief delay
|
||||||
|
hook_code = f'''
|
||||||
|
|
||||||
|
# === Screenshot capture hook (added by tutorial_screenshots.py) ===
|
||||||
|
import mcrfpy
|
||||||
|
from mcrfpy import automation
|
||||||
|
import sys
|
||||||
|
|
||||||
|
_screenshot_taken = [False]
|
||||||
|
|
||||||
|
def _take_screenshot(timer, runtime):
|
||||||
|
if not _screenshot_taken[0]:
|
||||||
|
_screenshot_taken[0] = True
|
||||||
|
automation.screenshot("{screenshot_path}")
|
||||||
|
print(f"Screenshot saved: {screenshot_path}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Wait a moment for scene to render, then capture
|
||||||
|
mcrfpy.Timer("_screenshot_hook", _take_screenshot, 200)
|
||||||
|
'''
|
||||||
|
return code + hook_code
|
||||||
|
|
||||||
|
|
||||||
|
class TutorialScreenshotter:
|
||||||
|
"""Manages tutorial screenshot generation."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.tutorials = []
|
||||||
|
self.current_index = 0
|
||||||
|
|
||||||
|
def load_tutorials(self):
|
||||||
|
"""Load and parse all tutorial files."""
|
||||||
|
for filename in TUTORIALS:
|
||||||
|
filepath = os.path.join(TUTORIAL_DIR, filename)
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
print(f"Warning: {filepath} not found, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
code = extract_code_from_markdown(filepath)
|
||||||
|
if code:
|
||||||
|
# Generate output filename
|
||||||
|
base = os.path.splitext(filename)[0]
|
||||||
|
screenshot_name = f"{base}.png"
|
||||||
|
self.tutorials.append({
|
||||||
|
'name': filename,
|
||||||
|
'code': code,
|
||||||
|
'screenshot': screenshot_name,
|
||||||
|
'filepath': filepath,
|
||||||
|
})
|
||||||
|
print(f"Loaded: {filename}")
|
||||||
|
else:
|
||||||
|
print(f"Warning: No code found in {filename}")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Generate all screenshots."""
|
||||||
|
# Ensure output directory exists
|
||||||
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
print(f"\nGenerating {len(self.tutorials)} tutorial screenshots...")
|
||||||
|
print(f"Output directory: {OUTPUT_DIR}\n")
|
||||||
|
|
||||||
|
self.process_next()
|
||||||
|
|
||||||
|
def process_next(self):
|
||||||
|
"""Process the next tutorial."""
|
||||||
|
if self.current_index >= len(self.tutorials):
|
||||||
|
print("\nAll screenshots generated!")
|
||||||
|
sys.exit(0)
|
||||||
|
return
|
||||||
|
|
||||||
|
tutorial = self.tutorials[self.current_index]
|
||||||
|
print(f"[{self.current_index + 1}/{len(self.tutorials)}] Processing {tutorial['name']}...")
|
||||||
|
|
||||||
|
# Add screenshot hook to the code
|
||||||
|
screenshot_path = os.path.join(OUTPUT_DIR, tutorial['screenshot'])
|
||||||
|
modified_code = add_screenshot_hook(tutorial['code'], screenshot_path)
|
||||||
|
|
||||||
|
# Write to temp file and execute
|
||||||
|
temp_path = f"/tmp/tutorial_screenshot_{self.current_index}.py"
|
||||||
|
with open(temp_path, 'w') as f:
|
||||||
|
f.write(modified_code)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Execute the code
|
||||||
|
exec(compile(modified_code, temp_path, 'exec'), {'__name__': '__main__'})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing {tutorial['name']}: {e}")
|
||||||
|
self.current_index += 1
|
||||||
|
self.process_next()
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.unlink(temp_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
screenshotter = TutorialScreenshotter()
|
||||||
|
screenshotter.load_tutorials()
|
||||||
|
|
||||||
|
if not screenshotter.tutorials:
|
||||||
|
print("No tutorials found to process!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
screenshotter.run()
|
||||||
|
|
||||||
|
|
||||||
|
# Run when executed
|
||||||
|
main()
|
||||||
426
tests/demo/tutorial_showcase.py
Normal file
426
tests/demo/tutorial_showcase.py
Normal file
|
|
@ -0,0 +1,426 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Tutorial Screenshot Showcase - ALL THE SCREENSHOTS!
|
||||||
|
|
||||||
|
Generates beautiful screenshots for all tutorial parts.
|
||||||
|
Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/tutorial_showcase.py
|
||||||
|
|
||||||
|
In headless mode, automation.screenshot() is SYNCHRONOUS - no timer dance needed!
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
from mcrfpy import automation
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Output directory
|
||||||
|
OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/tutorials"
|
||||||
|
|
||||||
|
# Tile meanings from the labeled tileset - the FUN sprites!
|
||||||
|
TILES = {
|
||||||
|
# Players - knights and heroes!
|
||||||
|
'player_knight': 84,
|
||||||
|
'player_mage': 85,
|
||||||
|
'player_rogue': 86,
|
||||||
|
'player_warrior': 87,
|
||||||
|
'player_archer': 88,
|
||||||
|
'player_alt1': 96,
|
||||||
|
'player_alt2': 97,
|
||||||
|
'player_alt3': 98,
|
||||||
|
|
||||||
|
# Enemies - scary!
|
||||||
|
'enemy_slime': 108,
|
||||||
|
'enemy_bat': 109,
|
||||||
|
'enemy_spider': 110,
|
||||||
|
'enemy_rat': 111,
|
||||||
|
'enemy_orc': 120,
|
||||||
|
'enemy_troll': 121,
|
||||||
|
'enemy_ghost': 122,
|
||||||
|
'enemy_skeleton': 123,
|
||||||
|
'enemy_demon': 124,
|
||||||
|
'enemy_boss': 92,
|
||||||
|
|
||||||
|
# Terrain
|
||||||
|
'floor_stone': 42,
|
||||||
|
'floor_wood': 49,
|
||||||
|
'floor_grass': 48,
|
||||||
|
'floor_dirt': 50,
|
||||||
|
'wall_stone': 30,
|
||||||
|
'wall_brick': 14,
|
||||||
|
'wall_mossy': 28,
|
||||||
|
|
||||||
|
# Items
|
||||||
|
'item_potion': 113,
|
||||||
|
'item_scroll': 114,
|
||||||
|
'item_key': 115,
|
||||||
|
'item_coin': 116,
|
||||||
|
|
||||||
|
# Equipment
|
||||||
|
'equip_sword': 101,
|
||||||
|
'equip_shield': 102,
|
||||||
|
'equip_helm': 103,
|
||||||
|
'equip_armor': 104,
|
||||||
|
|
||||||
|
# Chests and doors
|
||||||
|
'chest_closed': 89,
|
||||||
|
'chest_open': 90,
|
||||||
|
'door_closed': 33,
|
||||||
|
'door_open': 35,
|
||||||
|
|
||||||
|
# Decorations
|
||||||
|
'torch': 72,
|
||||||
|
'barrel': 73,
|
||||||
|
'skull': 74,
|
||||||
|
'bones': 75,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TutorialShowcase:
|
||||||
|
"""Creates beautiful showcase screenshots for tutorials."""
|
||||||
|
|
||||||
|
def __init__(self, scene_name, output_name):
|
||||||
|
self.scene = mcrfpy.Scene(scene_name)
|
||||||
|
self.output_path = os.path.join(OUTPUT_DIR, output_name)
|
||||||
|
self.grid = None
|
||||||
|
|
||||||
|
def setup_grid(self, width, height, zoom=3.0):
|
||||||
|
"""Create a grid with nice defaults."""
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||||
|
self.grid = mcrfpy.Grid(
|
||||||
|
pos=(50, 80),
|
||||||
|
size=(700, 500),
|
||||||
|
grid_size=(width, height),
|
||||||
|
texture=texture,
|
||||||
|
zoom=zoom
|
||||||
|
)
|
||||||
|
self.grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||||
|
self.scene.children.append(self.grid)
|
||||||
|
self.texture = texture
|
||||||
|
return self.grid
|
||||||
|
|
||||||
|
def add_title(self, text, subtitle=None):
|
||||||
|
"""Add a title to the scene."""
|
||||||
|
title = mcrfpy.Caption(text=text, pos=(50, 20))
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
title.font_size = 28
|
||||||
|
self.scene.children.append(title)
|
||||||
|
|
||||||
|
if subtitle:
|
||||||
|
sub = mcrfpy.Caption(text=subtitle, pos=(50, 50))
|
||||||
|
sub.fill_color = mcrfpy.Color(180, 180, 200)
|
||||||
|
sub.font_size = 16
|
||||||
|
self.scene.children.append(sub)
|
||||||
|
|
||||||
|
def fill_floor(self, tile=None):
|
||||||
|
"""Fill grid with floor tiles."""
|
||||||
|
if tile is None:
|
||||||
|
tile = TILES['floor_stone']
|
||||||
|
w, h = int(self.grid.grid_size[0]), int(self.grid.grid_size[1])
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
self.grid.at(x, y).tilesprite = tile
|
||||||
|
|
||||||
|
def add_walls(self, tile=None):
|
||||||
|
"""Add wall border."""
|
||||||
|
if tile is None:
|
||||||
|
tile = TILES['wall_stone']
|
||||||
|
w, h = int(self.grid.grid_size[0]), int(self.grid.grid_size[1])
|
||||||
|
for x in range(w):
|
||||||
|
self.grid.at(x, 0).tilesprite = tile
|
||||||
|
self.grid.at(x, 0).walkable = False
|
||||||
|
self.grid.at(x, h-1).tilesprite = tile
|
||||||
|
self.grid.at(x, h-1).walkable = False
|
||||||
|
for y in range(h):
|
||||||
|
self.grid.at(0, y).tilesprite = tile
|
||||||
|
self.grid.at(0, y).walkable = False
|
||||||
|
self.grid.at(w-1, y).tilesprite = tile
|
||||||
|
self.grid.at(w-1, y).walkable = False
|
||||||
|
|
||||||
|
def add_entity(self, x, y, sprite):
|
||||||
|
"""Add an entity to the grid."""
|
||||||
|
entity = mcrfpy.Entity(
|
||||||
|
grid_pos=(x, y),
|
||||||
|
texture=self.texture,
|
||||||
|
sprite_index=sprite
|
||||||
|
)
|
||||||
|
self.grid.entities.append(entity)
|
||||||
|
return entity
|
||||||
|
|
||||||
|
def center_on(self, x, y):
|
||||||
|
"""Center camera on a position."""
|
||||||
|
self.grid.center = (x * 16 + 8, y * 16 + 8)
|
||||||
|
|
||||||
|
def screenshot(self):
|
||||||
|
"""Take the screenshot - synchronous in headless mode!"""
|
||||||
|
self.scene.activate()
|
||||||
|
result = automation.screenshot(self.output_path)
|
||||||
|
print(f" -> {self.output_path} (result: {result})")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def part01_grid_movement():
|
||||||
|
"""Part 1: Grid Movement - Knight in a dungeon room."""
|
||||||
|
showcase = TutorialShowcase("part01", "part_01_grid_movement.png")
|
||||||
|
showcase.setup_grid(12, 9, zoom=3.5)
|
||||||
|
showcase.add_title("Part 1: The '@' and the Dungeon Grid",
|
||||||
|
"Creating a grid, placing entities, handling input")
|
||||||
|
|
||||||
|
showcase.fill_floor(TILES['floor_stone'])
|
||||||
|
showcase.add_walls(TILES['wall_stone'])
|
||||||
|
|
||||||
|
# Add the player (a cool knight, not boring @)
|
||||||
|
showcase.add_entity(6, 4, TILES['player_knight'])
|
||||||
|
|
||||||
|
# Add some decorations to make it interesting
|
||||||
|
showcase.add_entity(2, 2, TILES['torch'])
|
||||||
|
showcase.add_entity(9, 2, TILES['torch'])
|
||||||
|
showcase.add_entity(2, 6, TILES['barrel'])
|
||||||
|
showcase.add_entity(9, 6, TILES['skull'])
|
||||||
|
|
||||||
|
showcase.center_on(6, 4)
|
||||||
|
showcase.screenshot()
|
||||||
|
|
||||||
|
|
||||||
|
def part02_tiles_collision():
|
||||||
|
"""Part 2: Tiles and Collision - Walls and walkability."""
|
||||||
|
showcase = TutorialShowcase("part02", "part_02_tiles_collision.png")
|
||||||
|
showcase.setup_grid(14, 10, zoom=3.0)
|
||||||
|
showcase.add_title("Part 2: Tiles, Collision, and Walkability",
|
||||||
|
"Different tile types and blocking movement")
|
||||||
|
|
||||||
|
showcase.fill_floor(TILES['floor_stone'])
|
||||||
|
showcase.add_walls(TILES['wall_brick'])
|
||||||
|
|
||||||
|
# Create some interior walls to show collision
|
||||||
|
for y in range(2, 5):
|
||||||
|
showcase.grid.at(5, y).tilesprite = TILES['wall_stone']
|
||||||
|
showcase.grid.at(5, y).walkable = False
|
||||||
|
for y in range(5, 8):
|
||||||
|
showcase.grid.at(9, y).tilesprite = TILES['wall_stone']
|
||||||
|
showcase.grid.at(9, y).walkable = False
|
||||||
|
|
||||||
|
# Add a door
|
||||||
|
showcase.grid.at(5, 5).tilesprite = TILES['door_closed']
|
||||||
|
showcase.grid.at(5, 5).walkable = False
|
||||||
|
|
||||||
|
# Player navigating the maze
|
||||||
|
showcase.add_entity(3, 4, TILES['player_warrior'])
|
||||||
|
|
||||||
|
# Chest as goal
|
||||||
|
showcase.add_entity(11, 5, TILES['chest_closed'])
|
||||||
|
|
||||||
|
showcase.center_on(7, 5)
|
||||||
|
showcase.screenshot()
|
||||||
|
|
||||||
|
|
||||||
|
def part03_dungeon_generation():
|
||||||
|
"""Part 3: Dungeon Generation - Procedural rooms and corridors."""
|
||||||
|
showcase = TutorialShowcase("part03", "part_03_dungeon_generation.png")
|
||||||
|
showcase.setup_grid(20, 14, zoom=2.5)
|
||||||
|
showcase.add_title("Part 3: Procedural Dungeon Generation",
|
||||||
|
"Random rooms connected by corridors")
|
||||||
|
|
||||||
|
# Fill with walls first
|
||||||
|
for y in range(14):
|
||||||
|
for x in range(20):
|
||||||
|
showcase.grid.at(x, y).tilesprite = TILES['wall_stone']
|
||||||
|
showcase.grid.at(x, y).walkable = False
|
||||||
|
|
||||||
|
# Carve out two rooms
|
||||||
|
# Room 1 (left)
|
||||||
|
for y in range(3, 8):
|
||||||
|
for x in range(2, 8):
|
||||||
|
showcase.grid.at(x, y).tilesprite = TILES['floor_stone']
|
||||||
|
showcase.grid.at(x, y).walkable = True
|
||||||
|
|
||||||
|
# Room 2 (right)
|
||||||
|
for y in range(6, 12):
|
||||||
|
for x in range(12, 18):
|
||||||
|
showcase.grid.at(x, y).tilesprite = TILES['floor_stone']
|
||||||
|
showcase.grid.at(x, y).walkable = True
|
||||||
|
|
||||||
|
# Corridor connecting them
|
||||||
|
for x in range(7, 13):
|
||||||
|
showcase.grid.at(x, 6).tilesprite = TILES['floor_dirt']
|
||||||
|
showcase.grid.at(x, 6).walkable = True
|
||||||
|
for y in range(6, 9):
|
||||||
|
showcase.grid.at(12, y).tilesprite = TILES['floor_dirt']
|
||||||
|
showcase.grid.at(12, y).walkable = True
|
||||||
|
|
||||||
|
# Player in first room
|
||||||
|
showcase.add_entity(4, 5, TILES['player_knight'])
|
||||||
|
|
||||||
|
# Some loot in second room
|
||||||
|
showcase.add_entity(14, 9, TILES['chest_closed'])
|
||||||
|
showcase.add_entity(16, 8, TILES['item_potion'])
|
||||||
|
|
||||||
|
# Torches
|
||||||
|
showcase.add_entity(3, 3, TILES['torch'])
|
||||||
|
showcase.add_entity(6, 3, TILES['torch'])
|
||||||
|
showcase.add_entity(13, 7, TILES['torch'])
|
||||||
|
|
||||||
|
showcase.center_on(10, 7)
|
||||||
|
showcase.screenshot()
|
||||||
|
|
||||||
|
|
||||||
|
def part04_fov():
|
||||||
|
"""Part 4: Field of View - Showing explored vs visible areas."""
|
||||||
|
showcase = TutorialShowcase("part04", "part_04_fov.png")
|
||||||
|
showcase.setup_grid(16, 12, zoom=2.8)
|
||||||
|
showcase.add_title("Part 4: Field of View and Fog of War",
|
||||||
|
"What the player can see vs. the unknown")
|
||||||
|
|
||||||
|
showcase.fill_floor(TILES['floor_stone'])
|
||||||
|
showcase.add_walls(TILES['wall_brick'])
|
||||||
|
|
||||||
|
# Some interior pillars to block sight
|
||||||
|
for pos in [(5, 4), (5, 7), (10, 5), (10, 8)]:
|
||||||
|
showcase.grid.at(pos[0], pos[1]).tilesprite = TILES['wall_mossy']
|
||||||
|
showcase.grid.at(pos[0], pos[1]).walkable = False
|
||||||
|
|
||||||
|
# Player with "light"
|
||||||
|
showcase.add_entity(8, 6, TILES['player_mage'])
|
||||||
|
|
||||||
|
# Hidden enemy (player wouldn't see this!)
|
||||||
|
showcase.add_entity(12, 3, TILES['enemy_ghost'])
|
||||||
|
|
||||||
|
# Visible enemies
|
||||||
|
showcase.add_entity(3, 5, TILES['enemy_bat'])
|
||||||
|
showcase.add_entity(6, 8, TILES['enemy_spider'])
|
||||||
|
|
||||||
|
showcase.center_on(8, 6)
|
||||||
|
showcase.screenshot()
|
||||||
|
|
||||||
|
|
||||||
|
def part05_enemies():
|
||||||
|
"""Part 5: Enemies - A dungeon full of monsters."""
|
||||||
|
showcase = TutorialShowcase("part05", "part_05_enemies.png")
|
||||||
|
showcase.setup_grid(18, 12, zoom=2.5)
|
||||||
|
showcase.add_title("Part 5: Adding Enemies",
|
||||||
|
"Different monster types with AI behavior")
|
||||||
|
|
||||||
|
showcase.fill_floor(TILES['floor_stone'])
|
||||||
|
showcase.add_walls(TILES['wall_stone'])
|
||||||
|
|
||||||
|
# The hero
|
||||||
|
showcase.add_entity(3, 5, TILES['player_warrior'])
|
||||||
|
|
||||||
|
# A variety of enemies
|
||||||
|
showcase.add_entity(7, 3, TILES['enemy_slime'])
|
||||||
|
showcase.add_entity(10, 6, TILES['enemy_bat'])
|
||||||
|
showcase.add_entity(8, 8, TILES['enemy_spider'])
|
||||||
|
showcase.add_entity(14, 4, TILES['enemy_orc'])
|
||||||
|
showcase.add_entity(15, 8, TILES['enemy_skeleton'])
|
||||||
|
showcase.add_entity(12, 5, TILES['enemy_rat'])
|
||||||
|
|
||||||
|
# Boss at the end
|
||||||
|
showcase.add_entity(15, 6, TILES['enemy_boss'])
|
||||||
|
|
||||||
|
# Some decorations
|
||||||
|
showcase.add_entity(5, 2, TILES['bones'])
|
||||||
|
showcase.add_entity(13, 9, TILES['skull'])
|
||||||
|
showcase.add_entity(2, 8, TILES['torch'])
|
||||||
|
showcase.add_entity(16, 2, TILES['torch'])
|
||||||
|
|
||||||
|
showcase.center_on(9, 5)
|
||||||
|
showcase.screenshot()
|
||||||
|
|
||||||
|
|
||||||
|
def part06_combat():
|
||||||
|
"""Part 6: Combat - Battle in progress!"""
|
||||||
|
showcase = TutorialShowcase("part06", "part_06_combat.png")
|
||||||
|
showcase.setup_grid(14, 10, zoom=3.0)
|
||||||
|
showcase.add_title("Part 6: Combat System",
|
||||||
|
"HP, attack, defense, and turn-based fighting")
|
||||||
|
|
||||||
|
showcase.fill_floor(TILES['floor_dirt'])
|
||||||
|
showcase.add_walls(TILES['wall_brick'])
|
||||||
|
|
||||||
|
# Battle scene - player vs enemy
|
||||||
|
showcase.add_entity(5, 5, TILES['player_knight'])
|
||||||
|
showcase.add_entity(8, 5, TILES['enemy_orc'])
|
||||||
|
|
||||||
|
# Fallen enemies (show combat has happened)
|
||||||
|
showcase.add_entity(4, 3, TILES['bones'])
|
||||||
|
showcase.add_entity(9, 7, TILES['skull'])
|
||||||
|
|
||||||
|
# Equipment the player has
|
||||||
|
showcase.add_entity(3, 6, TILES['equip_shield'])
|
||||||
|
showcase.add_entity(10, 4, TILES['item_potion'])
|
||||||
|
|
||||||
|
showcase.center_on(6, 5)
|
||||||
|
showcase.screenshot()
|
||||||
|
|
||||||
|
|
||||||
|
def part07_ui():
|
||||||
|
"""Part 7: User Interface - Health bars and menus."""
|
||||||
|
showcase = TutorialShowcase("part07", "part_07_ui.png")
|
||||||
|
showcase.setup_grid(12, 8, zoom=3.0)
|
||||||
|
showcase.add_title("Part 7: User Interface",
|
||||||
|
"Health bars, message logs, and menus")
|
||||||
|
|
||||||
|
showcase.fill_floor(TILES['floor_wood'])
|
||||||
|
showcase.add_walls(TILES['wall_brick'])
|
||||||
|
|
||||||
|
# Player
|
||||||
|
showcase.add_entity(6, 4, TILES['player_rogue'])
|
||||||
|
|
||||||
|
# Some items to interact with
|
||||||
|
showcase.add_entity(4, 3, TILES['chest_open'])
|
||||||
|
showcase.add_entity(8, 5, TILES['item_coin'])
|
||||||
|
|
||||||
|
# Add UI overlay example - health bar frame
|
||||||
|
ui_frame = mcrfpy.Frame(pos=(50, 520), size=(200, 40))
|
||||||
|
ui_frame.fill_color = mcrfpy.Color(40, 40, 50, 200)
|
||||||
|
ui_frame.outline = 2
|
||||||
|
ui_frame.outline_color = mcrfpy.Color(80, 80, 100)
|
||||||
|
showcase.scene.children.append(ui_frame)
|
||||||
|
|
||||||
|
# Health label
|
||||||
|
hp_label = mcrfpy.Caption(text="HP: 45/50", pos=(10, 10))
|
||||||
|
hp_label.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
|
hp_label.font_size = 18
|
||||||
|
ui_frame.children.append(hp_label)
|
||||||
|
|
||||||
|
# Health bar background
|
||||||
|
hp_bg = mcrfpy.Frame(pos=(90, 12), size=(100, 16))
|
||||||
|
hp_bg.fill_color = mcrfpy.Color(60, 20, 20)
|
||||||
|
ui_frame.children.append(hp_bg)
|
||||||
|
|
||||||
|
# Health bar fill
|
||||||
|
hp_fill = mcrfpy.Frame(pos=(90, 12), size=(90, 16)) # 90% health
|
||||||
|
hp_fill.fill_color = mcrfpy.Color(200, 50, 50)
|
||||||
|
ui_frame.children.append(hp_fill)
|
||||||
|
|
||||||
|
showcase.center_on(6, 4)
|
||||||
|
showcase.screenshot()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Generate all showcase screenshots!"""
|
||||||
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
print("=== Tutorial Screenshot Showcase ===")
|
||||||
|
print(f"Output: {OUTPUT_DIR}\n")
|
||||||
|
|
||||||
|
showcases = [
|
||||||
|
('Part 1: Grid Movement', part01_grid_movement),
|
||||||
|
('Part 2: Tiles & Collision', part02_tiles_collision),
|
||||||
|
('Part 3: Dungeon Generation', part03_dungeon_generation),
|
||||||
|
('Part 4: Field of View', part04_fov),
|
||||||
|
('Part 5: Enemies', part05_enemies),
|
||||||
|
('Part 6: Combat', part06_combat),
|
||||||
|
('Part 7: UI', part07_ui),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, func in showcases:
|
||||||
|
print(f"Generating {name}...")
|
||||||
|
try:
|
||||||
|
func()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ERROR: {e}")
|
||||||
|
|
||||||
|
print("\n=== All screenshots generated! ===")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue