McRogueFace/shade_sprite/demo.py
John McCardle 80e14163f9 Shade sprite module: faction generation, asset scanning, TextureCache
Extends the shade_sprite module (for merchant-shade.itch.io character
sprite sheets) with procedural faction generation and asset management:

- FactionGenerator: seed-based faction recipes with Biome, Element,
  Aesthetic, and RoleType enums for thematic variety
- AssetLibrary: filesystem scanner that discovers and categorizes
  layer PNGs by type (skins, clothes, hair, etc.)
- TextureCache: avoids redundant disk I/O when building many variants
- CharacterAssembler: HSL shift documentation, method improvements
- Demo expanded to 6 interactive scenes (animation viewer, HSL recolor,
  character gallery, faction generator, layer compositing, equipment)
- EVALUATION.md: 7DRL readiness assessment of the full module
- 329-line faction generation test suite

Assets themselves are not included -- sprite sheets are external
dependencies, some under commercial license.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 23:17:24 -05:00

1353 lines
47 KiB
Python

"""shade_sprite interactive demo.
Run from the build directory:
./mcrogueface ../shade_sprite/demo.py
Scenes:
1 - Animation Viewer: cycle through all animations and 8 facing directions
2 - HSL Recolor: live hue/saturation/lightness shifting side-by-side
3 - Character Gallery: 4x4 grid of all available character sheets
4 - Faction Generator: random faction color schemes applied to squads
5 - Layer Compositing: demonstrates CharacterAssembler layered texture building
6 - Equipment Customizer: procedural + user-driven layer coloring for gear
7 - Asset Inventory: browse discovered layer categories and files
Controls shown on-screen per scene.
"""
import mcrfpy
import sys
import os
import random
# ---------------------------------------------------------------------------
# Asset discovery
# ---------------------------------------------------------------------------
_SEARCH_PATHS = [
"assets/Puny-Characters",
"../assets/Puny-Characters",
os.path.expanduser(
"~/Development/7DRL2026_Liber_Noster_jmccardle/"
"assets_sources/Puny-Characters"
),
]
def _find_asset_dir():
for p in _SEARCH_PATHS:
if os.path.isdir(p):
return p
return None
ASSET_DIR = _find_asset_dir()
_CHARACTER_FILES = [
"Warrior-Red.png", "Warrior-Blue.png",
"Soldier-Red.png", "Soldier-Blue.png", "Soldier-Yellow.png",
"Archer-Green.png", "Archer-Purple.png",
"Mage-Red.png", "Mage-Cyan.png",
"Human-Soldier-Red.png", "Human-Soldier-Cyan.png",
"Human-Worker-Red.png", "Human-Worker-Cyan.png",
"Orc-Grunt.png", "Orc-Peon-Red.png", "Orc-Peon-Cyan.png",
"Orc-Soldier-Red.png", "Orc-Soldier-Cyan.png",
"Character-Base.png",
]
def _available_sheets():
"""Return list of full paths to available character sheets."""
if not ASSET_DIR:
return []
sheets = []
for f in _CHARACTER_FILES:
p = os.path.join(ASSET_DIR, f)
if os.path.isfile(p):
sheets.append(p)
return sheets
def _slime_path():
"""Return path to Slime.png if available."""
if not ASSET_DIR:
return None
p = os.path.join(ASSET_DIR, "Slime.png")
return p if os.path.isfile(p) else None
def _base_path():
"""Return path to Character-Base.png if available."""
if not ASSET_DIR:
return None
p = os.path.join(ASSET_DIR, "Character-Base.png")
return p if os.path.isfile(p) else None
# Import shade_sprite
if __name__ == "__main__":
script_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(script_dir)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
from shade_sprite import (
AnimatedSprite, Direction, PUNY_24, SLIME,
CharacterAssembler,
AssetLibrary, FactionGenerator,
)
# ---------------------------------------------------------------------------
# Colors
# ---------------------------------------------------------------------------
BG = mcrfpy.Color(30, 30, 40)
TITLE_COLOR = mcrfpy.Color(220, 220, 255)
LABEL_COLOR = mcrfpy.Color(180, 180, 200)
DIM_COLOR = mcrfpy.Color(120, 120, 140)
WARN_COLOR = mcrfpy.Color(255, 100, 100)
ACCENT_COLOR = mcrfpy.Color(100, 200, 255)
HIGHLIGHT_COLOR = mcrfpy.Color(255, 220, 100)
# ---------------------------------------------------------------------------
# Global animation state
# ---------------------------------------------------------------------------
_animated_sprites = []
def _tick_all(timer, runtime):
for a in _animated_sprites:
a.tick(timer.interval)
def _no_assets_fallback(scene, scene_name):
"""Add 'no assets' message and basic navigation to a scene."""
ui = scene.children
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=BG)
ui.append(bg)
title = mcrfpy.Caption(text=f"shade_sprite - {scene_name}",
pos=(20, 10), fill_color=TITLE_COLOR)
ui.append(title)
msg = mcrfpy.Caption(
text="No sprite assets found. Place Puny-Characters PNGs in assets/Puny-Characters/",
pos=(20, 60), fill_color=WARN_COLOR)
ui.append(msg)
controls = mcrfpy.Caption(
text="[1-7] Switch scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
def on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return
_handle_scene_switch(key)
scene.on_key = on_key
return scene
def _handle_scene_switch(key):
"""Common scene switching for number keys."""
scene_map = {
mcrfpy.Key.NUM_1: "viewer",
mcrfpy.Key.NUM_2: "hsl",
mcrfpy.Key.NUM_3: "gallery",
mcrfpy.Key.NUM_4: "factions",
mcrfpy.Key.NUM_5: "layers",
mcrfpy.Key.NUM_6: "equip",
mcrfpy.Key.NUM_7: "inventory",
}
name = scene_map.get(key)
if name:
mcrfpy.Scene(name).activate()
return True
return False
# ---------------------------------------------------------------------------
# Scene 1: Animation Viewer
# ---------------------------------------------------------------------------
def _build_scene_viewer():
scene = mcrfpy.Scene("viewer")
sheets = _available_sheets()
if not sheets:
return _no_assets_fallback(scene, "Animation Viewer")
ui = scene.children
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=BG)
ui.append(bg)
title = mcrfpy.Caption(text="[1] Animation Viewer",
pos=(20, 10), fill_color=TITLE_COLOR)
ui.append(title)
fmt = PUNY_24
anim_names = list(fmt.animations.keys())
state = {"sheet_idx": 0, "anim_idx": 0, "dir_idx": 0}
# Load first sheet
tex = mcrfpy.Texture(sheets[0], fmt.tile_w, fmt.tile_h)
# Main sprite (large)
sprite = mcrfpy.Sprite(texture=tex, pos=(80, 180), scale=6.0)
ui.append(sprite)
anim = AnimatedSprite(sprite, fmt, Direction.S)
anim.play("idle")
_animated_sprites.append(anim)
# Info labels
sheet_label = mcrfpy.Caption(
text=f"Sheet: {os.path.basename(sheets[0])}",
pos=(20, 50), fill_color=LABEL_COLOR)
ui.append(sheet_label)
anim_label = mcrfpy.Caption(
text="Animation: idle", pos=(20, 80), fill_color=LABEL_COLOR)
ui.append(anim_label)
dir_label = mcrfpy.Caption(
text="Direction: S (0)", pos=(20, 110), fill_color=LABEL_COLOR)
ui.append(dir_label)
frame_info = mcrfpy.Caption(
text="", pos=(20, 140), fill_color=ACCENT_COLOR)
ui.append(frame_info)
# 8 directional previews in a compass layout
compass_cx, compass_cy = 620, 350
compass_offsets = {
Direction.N: (0, -120),
Direction.NE: (100, -85),
Direction.E: (140, 0),
Direction.SE: (100, 85),
Direction.S: (0, 120),
Direction.SW: (-100, 85),
Direction.W: (-140, 0),
Direction.NW: (-100, -85),
}
dir_sprites = []
dir_anims = []
dir_labels = []
for d in Direction:
ox, oy = compass_offsets[d]
x = compass_cx + ox - 16 # center 32px * 2 scale
y = compass_cy + oy - 16
s = mcrfpy.Sprite(texture=tex, pos=(x, y), scale=2.0)
ui.append(s)
a = AnimatedSprite(s, fmt, d)
a.play("idle")
_animated_sprites.append(a)
dir_sprites.append(s)
dir_anims.append(a)
lbl = mcrfpy.Caption(text=d.name, pos=(x + 5, y - 18),
fill_color=DIM_COLOR)
ui.append(lbl)
dir_labels.append(lbl)
# Compass center label
compass_title = mcrfpy.Caption(text="8-Dir Compass",
pos=(compass_cx - 50, compass_cy - 10),
fill_color=DIM_COLOR)
ui.append(compass_title)
# Slime demo (different format)
slime_path = _slime_path()
slime_anim = None
if slime_path:
slime_lbl = mcrfpy.Caption(text="Slime (1-dir, SLIME format):",
pos=(80, 520), fill_color=LABEL_COLOR)
ui.append(slime_lbl)
slime_tex = mcrfpy.Texture(slime_path, SLIME.tile_w, SLIME.tile_h)
slime_spr = mcrfpy.Sprite(texture=slime_tex, pos=(80, 550), scale=4.0)
ui.append(slime_spr)
slime_anim = AnimatedSprite(slime_spr, SLIME, Direction.S)
slime_anim.play("walk")
_animated_sprites.append(slime_anim)
# Animation list reference
anim_ref_y = 520 if not slime_path else 640
anim_ref = mcrfpy.Caption(
text="Animations: " + ", ".join(anim_names),
pos=(20, anim_ref_y), fill_color=DIM_COLOR)
ui.append(anim_ref)
controls = mcrfpy.Caption(
text="[Q/E] Sheet [A/D] Animation [W/S] Direction [1-7] Scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
def _update_frame_info():
a = fmt.animations[anim_names[state["anim_idx"]]]
nf = len(a.frames)
loop_str = "loop" if a.loop else "one-shot"
chain_str = f" -> {a.chain_to}" if a.chain_to else ""
frame_info.text = f"Frames: {nf} ({loop_str}{chain_str})"
_update_frame_info()
def _reload_sheet():
path = sheets[state["sheet_idx"]]
new_tex = mcrfpy.Texture(path, fmt.tile_w, fmt.tile_h)
sprite.texture = new_tex
for s in dir_sprites:
s.texture = new_tex
sheet_label.text = f"Sheet: {os.path.basename(path)}"
_update_anim()
def _update_anim():
name = anim_names[state["anim_idx"]]
anim.play(name)
for a in dir_anims:
a.play(name)
anim_label.text = f"Animation: {name}"
_update_frame_info()
def _update_dir():
d = Direction(state["dir_idx"])
anim.direction = d
dir_label.text = f"Direction: {d.name} ({d.value})"
def on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return
if _handle_scene_switch(key):
return
if key == mcrfpy.Key.Q:
state["sheet_idx"] = (state["sheet_idx"] - 1) % len(sheets)
_reload_sheet()
elif key == mcrfpy.Key.E:
state["sheet_idx"] = (state["sheet_idx"] + 1) % len(sheets)
_reload_sheet()
elif key == mcrfpy.Key.A:
state["anim_idx"] = (state["anim_idx"] - 1) % len(anim_names)
_update_anim()
elif key == mcrfpy.Key.D:
state["anim_idx"] = (state["anim_idx"] + 1) % len(anim_names)
_update_anim()
elif key == mcrfpy.Key.W:
state["dir_idx"] = (state["dir_idx"] - 1) % 8
_update_dir()
elif key == mcrfpy.Key.S:
state["dir_idx"] = (state["dir_idx"] + 1) % 8
_update_dir()
scene.on_key = on_key
return scene
# ---------------------------------------------------------------------------
# Scene 2: HSL Recolor Demo
# ---------------------------------------------------------------------------
def _build_scene_hsl():
scene = mcrfpy.Scene("hsl")
sheets = _available_sheets()
if not sheets:
return _no_assets_fallback(scene, "HSL Recoloring")
ui = scene.children
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=BG)
ui.append(bg)
title = mcrfpy.Caption(text="[2] HSL Recoloring",
pos=(20, 10), fill_color=TITLE_COLOR)
ui.append(title)
fmt = PUNY_24
state = {"hue": 0.0, "sat": 0.0, "lit": 0.0, "sheet_idx": 0}
# Original (left)
orig_tex = mcrfpy.Texture(sheets[0], fmt.tile_w, fmt.tile_h)
orig_sprite = mcrfpy.Sprite(texture=orig_tex, pos=(120, 200), scale=6.0)
ui.append(orig_sprite)
orig_anim = AnimatedSprite(orig_sprite, fmt, Direction.S)
orig_anim.play("walk")
_animated_sprites.append(orig_anim)
orig_label = mcrfpy.Caption(text="Original", pos=(145, 170),
fill_color=LABEL_COLOR)
ui.append(orig_label)
# Shifted (center)
shifted_sprite = mcrfpy.Sprite(texture=orig_tex, pos=(420, 200), scale=6.0)
ui.append(shifted_sprite)
shifted_anim = AnimatedSprite(shifted_sprite, fmt, Direction.S)
shifted_anim.play("walk")
_animated_sprites.append(shifted_anim)
shifted_label = mcrfpy.Caption(text="HSL Shifted", pos=(430, 170),
fill_color=LABEL_COLOR)
ui.append(shifted_label)
# Hue wheel preview: show 6 hue rotations at once (right side)
wheel_label = mcrfpy.Caption(text="Hue Wheel (60-degree steps):",
pos=(700, 80), fill_color=LABEL_COLOR)
ui.append(wheel_label)
wheel_sprites = []
wheel_anims = []
for i in range(6):
hue = i * 60.0
y = 110 + i * 90
shifted_tex = orig_tex.hsl_shift(hue)
s = mcrfpy.Sprite(texture=shifted_tex, pos=(730, y), scale=2.5)
ui.append(s)
a = AnimatedSprite(s, fmt, Direction.S)
a.play("walk")
_animated_sprites.append(a)
wheel_sprites.append(s)
wheel_anims.append(a)
lbl = mcrfpy.Caption(text=f"{hue:.0f} deg", pos=(810, y + 20),
fill_color=DIM_COLOR)
ui.append(lbl)
# HSL value displays
hue_label = mcrfpy.Caption(text="Hue: 0", pos=(120, 440),
fill_color=mcrfpy.Color(255, 180, 180))
ui.append(hue_label)
sat_label = mcrfpy.Caption(text="Sat: 0.0", pos=(120, 470),
fill_color=mcrfpy.Color(180, 255, 180))
ui.append(sat_label)
lit_label = mcrfpy.Caption(text="Lit: 0.0", pos=(120, 500),
fill_color=mcrfpy.Color(180, 180, 255))
ui.append(lit_label)
# Explanation
explain = mcrfpy.Caption(
text="Hue rotates color wheel. Sat adjusts vibrancy. Lit adjusts brightness.",
pos=(120, 540), fill_color=DIM_COLOR)
ui.append(explain)
explain2 = mcrfpy.Caption(
text="tex.hsl_shift(hue, sat, lit) returns a NEW texture (original unchanged)",
pos=(120, 565), fill_color=DIM_COLOR)
ui.append(explain2)
controls = mcrfpy.Caption(
text="[Left/Right] Hue +/-30 [Up/Down] Sat +/-0.1 [Z/X] Lit +/-0.1 [Q/E] Sheet [1-7] Scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
def _rebuild_shifted():
path = sheets[state["sheet_idx"]]
base = mcrfpy.Texture(path, fmt.tile_w, fmt.tile_h)
shifted = base.hsl_shift(state["hue"], state["sat"], state["lit"])
shifted_sprite.texture = shifted
hue_label.text = f"Hue: {state['hue']:.0f}"
sat_label.text = f"Sat: {state['sat']:.1f}"
lit_label.text = f"Lit: {state['lit']:.1f}"
def _reload():
path = sheets[state["sheet_idx"]]
new_tex = mcrfpy.Texture(path, fmt.tile_w, fmt.tile_h)
orig_sprite.texture = new_tex
# Update hue wheel with new base
for i, s in enumerate(wheel_sprites):
hue = i * 60.0
s.texture = new_tex.hsl_shift(hue)
_rebuild_shifted()
def on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return
if _handle_scene_switch(key):
return
changed = False
if key == mcrfpy.Key.LEFT:
state["hue"] = (state["hue"] - 30.0) % 360.0
changed = True
elif key == mcrfpy.Key.RIGHT:
state["hue"] = (state["hue"] + 30.0) % 360.0
changed = True
elif key == mcrfpy.Key.UP:
state["sat"] = min(1.0, state["sat"] + 0.1)
changed = True
elif key == mcrfpy.Key.DOWN:
state["sat"] = max(-1.0, state["sat"] - 0.1)
changed = True
elif key == mcrfpy.Key.Z:
state["lit"] = max(-1.0, state["lit"] - 0.1)
changed = True
elif key == mcrfpy.Key.X:
state["lit"] = min(1.0, state["lit"] + 0.1)
changed = True
elif key == mcrfpy.Key.Q:
state["sheet_idx"] = (state["sheet_idx"] - 1) % len(sheets)
_reload()
elif key == mcrfpy.Key.E:
state["sheet_idx"] = (state["sheet_idx"] + 1) % len(sheets)
_reload()
if changed:
_rebuild_shifted()
scene.on_key = on_key
return scene
# ---------------------------------------------------------------------------
# Scene 3: Creature Gallery
# ---------------------------------------------------------------------------
def _build_scene_gallery():
scene = mcrfpy.Scene("gallery")
sheets = _available_sheets()
if not sheets:
return _no_assets_fallback(scene, "Character Gallery")
ui = scene.children
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=BG)
ui.append(bg)
title = mcrfpy.Caption(text="[3] Character Gallery",
pos=(20, 10), fill_color=TITLE_COLOR)
ui.append(title)
fmt = PUNY_24
anim_names = list(fmt.animations.keys())
state = {"dir_idx": 0, "anim_idx": 1} # start with walk
# 5-column grid
cols = 5
x_start, y_start = 30, 60
x_spacing, y_spacing = 195, 130
scale = 2.5
gallery_anims = []
count = min(len(sheets), 20)
for i in range(count):
col = i % cols
row = i // cols
x = x_start + col * x_spacing
y = y_start + row * y_spacing
tex = mcrfpy.Texture(sheets[i], fmt.tile_w, fmt.tile_h)
sprite = mcrfpy.Sprite(texture=tex, pos=(x + 30, y + 25), scale=scale)
ui.append(sprite)
a = AnimatedSprite(sprite, fmt, Direction.S)
a.play("walk")
_animated_sprites.append(a)
gallery_anims.append(a)
name = os.path.basename(sheets[i]).replace(".png", "")
lbl = mcrfpy.Caption(text=name, pos=(x, y + 5),
fill_color=DIM_COLOR)
ui.append(lbl)
# Slime in gallery too
slime_p = _slime_path()
slime_anim_ref = None
if slime_p:
row = count // cols
col = count % cols
x = x_start + col * x_spacing
y = y_start + row * y_spacing
stex = mcrfpy.Texture(slime_p, SLIME.tile_w, SLIME.tile_h)
sspr = mcrfpy.Sprite(texture=stex, pos=(x + 30, y + 25), scale=scale)
ui.append(sspr)
slime_anim_ref = AnimatedSprite(sspr, SLIME, Direction.S)
slime_anim_ref.play("walk")
_animated_sprites.append(slime_anim_ref)
lbl = mcrfpy.Caption(text="Slime", pos=(x, y + 5), fill_color=DIM_COLOR)
ui.append(lbl)
dir_info = mcrfpy.Caption(text="Direction: S Animation: walk",
pos=(20, 700), fill_color=LABEL_COLOR)
ui.append(dir_info)
controls = mcrfpy.Caption(
text="[W/S] Direction [A/D] Animation [1-7] Scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
def on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return
if _handle_scene_switch(key):
return
if key == mcrfpy.Key.W:
state["dir_idx"] = (state["dir_idx"] - 1) % 8
d = Direction(state["dir_idx"])
for a in gallery_anims:
a.direction = d
dir_info.text = f"Direction: {d.name} Animation: {anim_names[state['anim_idx']]}"
elif key == mcrfpy.Key.S:
state["dir_idx"] = (state["dir_idx"] + 1) % 8
d = Direction(state["dir_idx"])
for a in gallery_anims:
a.direction = d
dir_info.text = f"Direction: {d.name} Animation: {anim_names[state['anim_idx']]}"
elif key == mcrfpy.Key.A:
state["anim_idx"] = (state["anim_idx"] - 1) % len(anim_names)
name = anim_names[state["anim_idx"]]
for a in gallery_anims:
a.play(name)
dir_info.text = f"Direction: {Direction(state['dir_idx']).name} Animation: {name}"
elif key == mcrfpy.Key.D:
state["anim_idx"] = (state["anim_idx"] + 1) % len(anim_names)
name = anim_names[state["anim_idx"]]
for a in gallery_anims:
a.play(name)
dir_info.text = f"Direction: {Direction(state['dir_idx']).name} Animation: {name}"
scene.on_key = on_key
return scene
# ---------------------------------------------------------------------------
# Scene 4: Faction Generator
# ---------------------------------------------------------------------------
_FACTION_NAMES = [
"Iron Guard", "Shadow Pact", "Dawn Order", "Ember Clan",
"Frost Legion", "Vine Court", "Storm Band", "Ash Wardens",
"Gold Company", "Crimson Oath", "Azure Fleet", "Jade Circle",
"Silver Hand", "Night Watch", "Sun Speakers", "Bone Reavers",
]
def _build_scene_factions():
scene = mcrfpy.Scene("factions")
sheets = _available_sheets()
if not sheets:
return _no_assets_fallback(scene, "Faction Generator")
ui = scene.children
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=BG)
ui.append(bg)
title = mcrfpy.Caption(text="[4] Faction Generator",
pos=(20, 10), fill_color=TITLE_COLOR)
ui.append(title)
fmt = PUNY_24
scale = 3.0
faction_anims = []
def _populate():
"""Generate 4 random factions with hue-shifted squads."""
# Remove old faction anims from global list
for a in faction_anims:
if a in _animated_sprites:
_animated_sprites.remove(a)
faction_anims.clear()
hues = [random.uniform(0, 360) for _ in range(4)]
names = random.sample(_FACTION_NAMES, 4)
y_start = 70
for fi in range(4):
y = y_start + fi * 165
hue = hues[fi]
# Faction header with colored indicator
lbl = mcrfpy.Caption(
text=f"{names[fi]} (hue {hue:.0f})",
pos=(20, y), fill_color=HIGHLIGHT_COLOR)
ui.append(lbl)
# Pick 5 random characters for this faction
chosen = random.sample(sheets, min(5, len(sheets)))
for ci, path in enumerate(chosen):
x = 30 + ci * 180
base_tex = mcrfpy.Texture(path, fmt.tile_w, fmt.tile_h)
shifted_tex = base_tex.hsl_shift(hue)
s = mcrfpy.Sprite(texture=shifted_tex, pos=(x, y + 30),
scale=scale)
ui.append(s)
a = AnimatedSprite(s, fmt, Direction.S)
a.play("walk")
_animated_sprites.append(a)
faction_anims.append(a)
# Character name below
cname = os.path.basename(path).replace(".png", "")
nlbl = mcrfpy.Caption(text=cname, pos=(x, y + 130),
fill_color=DIM_COLOR)
ui.append(nlbl)
_populate()
controls = mcrfpy.Caption(
text="[Space] Re-roll factions [1-7] Scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
def on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return
if _handle_scene_switch(key):
return
if key == mcrfpy.Key.SPACE:
# Rebuild scene from scratch
new_scene = _build_scene_factions()
new_scene.activate()
scene.on_key = on_key
return scene
# ---------------------------------------------------------------------------
# Scene 5: Layer Compositing
# ---------------------------------------------------------------------------
def _build_scene_layers():
scene = mcrfpy.Scene("layers")
sheets = _available_sheets()
base_p = _base_path()
if not sheets or not base_p:
return _no_assets_fallback(scene, "Layer Compositing")
ui = scene.children
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=BG)
ui.append(bg)
title = mcrfpy.Caption(text="[5] Layer Compositing (CharacterAssembler)",
pos=(20, 10), fill_color=TITLE_COLOR)
ui.append(title)
fmt = PUNY_24
scale = 5.0
# Explanation
explain = mcrfpy.Caption(
text="CharacterAssembler composites multiple texture layers with HSL shifts.",
pos=(20, 45), fill_color=LABEL_COLOR)
ui.append(explain)
explain2 = mcrfpy.Caption(
text="Base layer (skin) + overlay (equipment) with color variation = unique characters.",
pos=(20, 70), fill_color=LABEL_COLOR)
ui.append(explain2)
# Find sheets that aren't Character-Base for overlay
overlay_sheets = [s for s in sheets
if "Character-Base" not in os.path.basename(s)]
# --- Column 1: Show the base layer alone ---
col1_x = 30
base_lbl = mcrfpy.Caption(text="Base Layer", pos=(col1_x, 110),
fill_color=ACCENT_COLOR)
ui.append(base_lbl)
base_tex = mcrfpy.Texture(base_p, fmt.tile_w, fmt.tile_h)
base_spr = mcrfpy.Sprite(texture=base_tex, pos=(col1_x + 10, 140), scale=scale)
ui.append(base_spr)
base_anim = AnimatedSprite(base_spr, fmt, Direction.S)
base_anim.play("walk")
_animated_sprites.append(base_anim)
base_note = mcrfpy.Caption(text="Character-Base.png", pos=(col1_x, 310),
fill_color=DIM_COLOR)
ui.append(base_note)
# --- Column 2: Show an overlay alone ---
col2_x = 250
overlay_lbl = mcrfpy.Caption(text="Overlay Layer", pos=(col2_x, 110),
fill_color=ACCENT_COLOR)
ui.append(overlay_lbl)
state = {"overlay_idx": 0, "hue": 0.0}
overlay_tex = mcrfpy.Texture(overlay_sheets[0], fmt.tile_w, fmt.tile_h)
overlay_spr = mcrfpy.Sprite(texture=overlay_tex, pos=(col2_x + 10, 140),
scale=scale)
ui.append(overlay_spr)
overlay_anim = AnimatedSprite(overlay_spr, fmt, Direction.S)
overlay_anim.play("walk")
_animated_sprites.append(overlay_anim)
overlay_name_lbl = mcrfpy.Caption(
text=os.path.basename(overlay_sheets[0]),
pos=(col2_x, 310), fill_color=DIM_COLOR)
ui.append(overlay_name_lbl)
# --- Column 3: Composite result ---
col3_x = 470
comp_lbl = mcrfpy.Caption(text="Composite Result", pos=(col3_x, 110),
fill_color=ACCENT_COLOR)
ui.append(comp_lbl)
# Build initial composite
assembler = CharacterAssembler(fmt)
assembler.add_layer(base_p)
assembler.add_layer(overlay_sheets[0])
comp_tex = assembler.build("demo_composite")
comp_spr = mcrfpy.Sprite(texture=comp_tex, pos=(col3_x + 10, 140),
scale=scale)
ui.append(comp_spr)
comp_anim = AnimatedSprite(comp_spr, fmt, Direction.S)
comp_anim.play("walk")
_animated_sprites.append(comp_anim)
comp_note = mcrfpy.Caption(text="Base + Overlay composited",
pos=(col3_x, 310), fill_color=DIM_COLOR)
ui.append(comp_note)
# --- Column 4: Composite with hue shift ---
col4_x = 690
shifted_lbl = mcrfpy.Caption(text="Shifted Composite", pos=(col4_x, 110),
fill_color=ACCENT_COLOR)
ui.append(shifted_lbl)
assembler2 = CharacterAssembler(fmt)
assembler2.add_layer(base_p)
assembler2.add_layer(overlay_sheets[0], hue_shift=120.0)
shifted_comp_tex = assembler2.build("demo_shifted")
shifted_comp_spr = mcrfpy.Sprite(texture=shifted_comp_tex,
pos=(col4_x + 10, 140), scale=scale)
ui.append(shifted_comp_spr)
shifted_comp_anim = AnimatedSprite(shifted_comp_spr, fmt, Direction.S)
shifted_comp_anim.play("walk")
_animated_sprites.append(shifted_comp_anim)
hue_note = mcrfpy.Caption(text=f"Overlay hue: {state['hue']:.0f}",
pos=(col4_x, 310), fill_color=DIM_COLOR)
ui.append(hue_note)
# --- Row 2: Show multiple hue-shifted composites ---
row2_y = 370
row2_lbl = mcrfpy.Caption(
text="Same base + overlay, 6 hue rotations (60-degree increments):",
pos=(30, row2_y), fill_color=LABEL_COLOR)
ui.append(row2_lbl)
row2_anims = []
for i in range(6):
hue = i * 60.0
x = 30 + i * 160
y = row2_y + 30
asm = CharacterAssembler(fmt)
asm.add_layer(base_p)
asm.add_layer(overlay_sheets[0], hue_shift=hue)
tex = asm.build(f"row2_{i}")
s = mcrfpy.Sprite(texture=tex, pos=(x + 20, y), scale=3.0)
ui.append(s)
a = AnimatedSprite(s, fmt, Direction.S)
a.play("walk")
_animated_sprites.append(a)
row2_anims.append((s, a))
lbl = mcrfpy.Caption(text=f"hue={hue:.0f}", pos=(x + 10, y + 100),
fill_color=DIM_COLOR)
ui.append(lbl)
# Code example
code_lbl = mcrfpy.Caption(
text='asm = CharacterAssembler(PUNY_24)',
pos=(30, 600), fill_color=mcrfpy.Color(150, 200, 150))
ui.append(code_lbl)
code_lbl2 = mcrfpy.Caption(
text='asm.add_layer("Character-Base.png")',
pos=(30, 625), fill_color=mcrfpy.Color(150, 200, 150))
ui.append(code_lbl2)
code_lbl3 = mcrfpy.Caption(
text='asm.add_layer("Warrior-Red.png", hue_shift=120.0)',
pos=(30, 650), fill_color=mcrfpy.Color(150, 200, 150))
ui.append(code_lbl3)
code_lbl4 = mcrfpy.Caption(
text='texture = asm.build("my_character")',
pos=(30, 675), fill_color=mcrfpy.Color(150, 200, 150))
ui.append(code_lbl4)
controls = mcrfpy.Caption(
text="[Q/E] Overlay sheet [Left/Right] Overlay hue +/-30 [1-7] Scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
def _rebuild():
path = overlay_sheets[state["overlay_idx"]]
hue = state["hue"]
# Update overlay preview
new_overlay_tex = mcrfpy.Texture(path, fmt.tile_w, fmt.tile_h)
overlay_spr.texture = new_overlay_tex
overlay_name_lbl.text = os.path.basename(path)
# Rebuild unshifted composite
asm = CharacterAssembler(fmt)
asm.add_layer(base_p)
asm.add_layer(path)
comp_spr.texture = asm.build("demo_composite")
# Rebuild shifted composite
asm2 = CharacterAssembler(fmt)
asm2.add_layer(base_p)
asm2.add_layer(path, hue_shift=hue)
shifted_comp_spr.texture = asm2.build("demo_shifted")
hue_note.text = f"Overlay hue: {hue:.0f}"
# Rebuild row2
for i, (s, a) in enumerate(row2_anims):
h = i * 60.0
asm3 = CharacterAssembler(fmt)
asm3.add_layer(base_p)
asm3.add_layer(path, hue_shift=h)
s.texture = asm3.build(f"row2_{i}")
def on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return
if _handle_scene_switch(key):
return
if key == mcrfpy.Key.Q:
state["overlay_idx"] = (state["overlay_idx"] - 1) % len(overlay_sheets)
_rebuild()
elif key == mcrfpy.Key.E:
state["overlay_idx"] = (state["overlay_idx"] + 1) % len(overlay_sheets)
_rebuild()
elif key == mcrfpy.Key.LEFT:
state["hue"] = (state["hue"] - 30.0) % 360.0
_rebuild()
elif key == mcrfpy.Key.RIGHT:
state["hue"] = (state["hue"] + 30.0) % 360.0
_rebuild()
scene.on_key = on_key
return scene
# ---------------------------------------------------------------------------
# Scene 6: Equipment Customizer
# ---------------------------------------------------------------------------
def _build_scene_equip():
scene = mcrfpy.Scene("equip")
sheets = _available_sheets()
base_p = _base_path()
if not sheets or not base_p:
return _no_assets_fallback(scene, "Equipment Customizer")
ui = scene.children
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=BG)
ui.append(bg)
title = mcrfpy.Caption(text="[6] Equipment Customizer",
pos=(20, 10), fill_color=TITLE_COLOR)
ui.append(title)
fmt = PUNY_24
overlay_sheets = [s for s in sheets
if "Character-Base" not in os.path.basename(s)]
# Three independent equipment "slots" - each selects overlay + hue
# Simulates: Body armor, Weapon style, Trim/accent
slot_names = ["Body Armor", "Weapon Style", "Accent Layer"]
slot_defaults = [
{"sheet_idx": 0, "hue": 0.0, "sat": 0.0, "lit": 0.0, "enabled": True},
{"sheet_idx": min(2, len(overlay_sheets) - 1), "hue": 120.0,
"sat": 0.0, "lit": 0.0, "enabled": True},
{"sheet_idx": min(4, len(overlay_sheets) - 1), "hue": 240.0,
"sat": 0.0, "lit": -0.3, "enabled": False},
]
slots = [dict(d) for d in slot_defaults]
state = {"active_slot": 0, "dir_idx": 0}
# Main character preview (large)
preview_spr = mcrfpy.Sprite(pos=(400, 150), scale=8.0)
ui.append(preview_spr)
preview_anim = AnimatedSprite(preview_spr, fmt, Direction.S)
preview_anim.play("walk")
_animated_sprites.append(preview_anim)
# Direction label
dir_lbl = mcrfpy.Caption(text="Direction: S", pos=(400, 420),
fill_color=LABEL_COLOR)
ui.append(dir_lbl)
# Slot panels (left side)
slot_labels = []
slot_info_labels = []
slot_indicators = []
for i, sname in enumerate(slot_names):
y = 80 + i * 180
# Slot header
indicator = mcrfpy.Caption(
text=f">>> {sname} <<<" if i == 0 else f" {sname}",
pos=(20, y),
fill_color=HIGHLIGHT_COLOR if i == 0 else LABEL_COLOR)
ui.append(indicator)
slot_indicators.append(indicator)
# Status
slot = slots[i]
enabled_str = "ON" if slot["enabled"] else "OFF"
sheet_name = os.path.basename(overlay_sheets[slot["sheet_idx"]]).replace(".png", "")
info = mcrfpy.Caption(
text=f"[{enabled_str}] {sheet_name} H:{slot['hue']:.0f} S:{slot['sat']:.1f} L:{slot['lit']:.1f}",
pos=(20, y + 30),
fill_color=ACCENT_COLOR if slot["enabled"] else mcrfpy.Color(80, 80, 100))
ui.append(info)
slot_info_labels.append(info)
# Small preview for this slot
slot_tex = mcrfpy.Texture(overlay_sheets[slot["sheet_idx"]],
fmt.tile_w, fmt.tile_h)
if slot["hue"] != 0.0 or slot["sat"] != 0.0 or slot["lit"] != 0.0:
slot_tex = slot_tex.hsl_shift(slot["hue"], slot["sat"], slot["lit"])
slot_spr = mcrfpy.Sprite(texture=slot_tex, pos=(20, y + 55), scale=3.0)
ui.append(slot_spr)
slot_labels.append(slot_spr)
# Row of procedurally generated variants at bottom
row_y = 550
row_lbl = mcrfpy.Caption(
text="Procedural Variants (randomized per slot):",
pos=(20, row_y), fill_color=LABEL_COLOR)
ui.append(row_lbl)
variant_sprites = []
variant_anims = []
for i in range(6):
x = 30 + i * 155
s = mcrfpy.Sprite(pos=(x + 20, row_y + 30), scale=3.0)
ui.append(s)
a = AnimatedSprite(s, fmt, Direction.S)
a.play("walk")
_animated_sprites.append(a)
variant_sprites.append(s)
variant_anims.append(a)
def _build_composite():
"""Build composite texture from current slot settings."""
asm = CharacterAssembler(fmt)
asm.add_layer(base_p)
for slot in slots:
if slot["enabled"]:
path = overlay_sheets[slot["sheet_idx"]]
asm.add_layer(path, hue_shift=slot["hue"],
sat_shift=slot["sat"], lit_shift=slot["lit"])
return asm.build("equip_preview")
def _update_preview():
"""Rebuild main preview and slot info."""
tex = _build_composite()
preview_spr.texture = tex
for i, slot in enumerate(slots):
enabled_str = "ON" if slot["enabled"] else "OFF"
sheet_name = os.path.basename(
overlay_sheets[slot["sheet_idx"]]).replace(".png", "")
slot_info_labels[i].text = (
f"[{enabled_str}] {sheet_name} "
f"H:{slot['hue']:.0f} S:{slot['sat']:.1f} L:{slot['lit']:.1f}")
if slot["enabled"]:
slot_info_labels[i].fill_color = ACCENT_COLOR
else:
slot_info_labels[i].fill_color = mcrfpy.Color(80, 80, 100)
# Update slot preview sprite
stex = mcrfpy.Texture(overlay_sheets[slot["sheet_idx"]],
fmt.tile_w, fmt.tile_h)
if slot["hue"] != 0.0 or slot["sat"] != 0.0 or slot["lit"] != 0.0:
stex = stex.hsl_shift(slot["hue"], slot["sat"], slot["lit"])
slot_labels[i].texture = stex
# Update slot indicators
for i, ind in enumerate(slot_indicators):
sname = slot_names[i]
if i == state["active_slot"]:
ind.text = f">>> {sname} <<<"
ind.fill_color = HIGHLIGHT_COLOR
else:
ind.text = f" {sname}"
ind.fill_color = LABEL_COLOR
def _generate_variants():
"""Create 6 random procedural variants."""
for i in range(6):
asm = CharacterAssembler(fmt)
asm.add_layer(base_p)
# Each variant gets 1-2 random layers with random hues
n_layers = random.randint(1, 2)
for _ in range(n_layers):
path = random.choice(overlay_sheets)
hue = random.uniform(0, 360)
sat = random.uniform(-0.3, 0.3)
lit = random.uniform(-0.2, 0.1)
asm.add_layer(path, hue_shift=hue, sat_shift=sat, lit_shift=lit)
variant_sprites[i].texture = asm.build(f"variant_{i}")
_update_preview()
_generate_variants()
controls = mcrfpy.Caption(
text="[Tab] Slot [Q/E] Sheet [Left/Right] Hue [Up/Down] Sat [Z/X] Lit [T] Toggle [R] Randomize [1-7] Scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
def on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return
if _handle_scene_switch(key):
return
slot = slots[state["active_slot"]]
if key == mcrfpy.Key.TAB:
state["active_slot"] = (state["active_slot"] + 1) % len(slots)
_update_preview()
elif key == mcrfpy.Key.T:
slot["enabled"] = not slot["enabled"]
_update_preview()
elif key == mcrfpy.Key.Q:
slot["sheet_idx"] = (slot["sheet_idx"] - 1) % len(overlay_sheets)
_update_preview()
elif key == mcrfpy.Key.E:
slot["sheet_idx"] = (slot["sheet_idx"] + 1) % len(overlay_sheets)
_update_preview()
elif key == mcrfpy.Key.LEFT:
slot["hue"] = (slot["hue"] - 30.0) % 360.0
_update_preview()
elif key == mcrfpy.Key.RIGHT:
slot["hue"] = (slot["hue"] + 30.0) % 360.0
_update_preview()
elif key == mcrfpy.Key.UP:
slot["sat"] = min(1.0, slot["sat"] + 0.1)
_update_preview()
elif key == mcrfpy.Key.DOWN:
slot["sat"] = max(-1.0, slot["sat"] - 0.1)
_update_preview()
elif key == mcrfpy.Key.Z:
slot["lit"] = max(-1.0, slot["lit"] - 0.1)
_update_preview()
elif key == mcrfpy.Key.X:
slot["lit"] = min(1.0, slot["lit"] + 0.1)
_update_preview()
elif key == mcrfpy.Key.R:
_generate_variants()
elif key == mcrfpy.Key.W:
state["dir_idx"] = (state["dir_idx"] - 1) % 8
d = Direction(state["dir_idx"])
preview_anim.direction = d
for a in variant_anims:
a.direction = d
dir_lbl.text = f"Direction: {d.name}"
elif key == mcrfpy.Key.S:
state["dir_idx"] = (state["dir_idx"] + 1) % 8
d = Direction(state["dir_idx"])
preview_anim.direction = d
for a in variant_anims:
a.direction = d
dir_lbl.text = f"Direction: {d.name}"
scene.on_key = on_key
return scene
# ---------------------------------------------------------------------------
# Scene 7: Asset Inventory Browser
# ---------------------------------------------------------------------------
def _build_scene_inventory():
scene = mcrfpy.Scene("inventory")
ui = scene.children
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=BG)
ui.append(bg)
title = mcrfpy.Caption(text="[7] Asset Inventory",
pos=(20, 10), fill_color=TITLE_COLOR)
ui.append(title)
lib = AssetLibrary()
if not lib.available:
msg = mcrfpy.Caption(
text="No paid Puny Characters v2.1 pack found.",
pos=(20, 60), fill_color=WARN_COLOR)
ui.append(msg)
msg2 = mcrfpy.Caption(
text="The AssetLibrary scans the 'Individual Spritesheets' directory.",
pos=(20, 90), fill_color=DIM_COLOR)
ui.append(msg2)
controls = mcrfpy.Caption(text="[1-7] Switch scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
def on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return
_handle_scene_switch(key)
scene.on_key = on_key
return scene
# Build category data
categories = lib.categories
cat_data = [] # list of (key, label, count, subcats_with_counts)
for cat_key in categories:
files = lib.layers(cat_key)
subcats = lib.subcategories(cat_key)
sub_info = []
for sc in subcats:
sc_files = lib.layers_in(cat_key, sc)
label = sc if sc else "(root)"
sub_info.append((label, len(sc_files), [f.name for f in sc_files]))
display_name = cat_key.replace("_", " ").title()
cat_data.append((cat_key, display_name, len(files), sub_info))
state = {"cat_idx": 0, "sub_idx": 0, "scroll": 0}
MAX_VISIBLE_FILES = 18
# Summary header
summary = lib.summary()
total = sum(summary.values())
species_list = ", ".join(lib.species)
summary_lbl = mcrfpy.Caption(
text=f"Found {total} layer files in {len(categories)} categories. Species: {species_list}",
pos=(20, 45), fill_color=LABEL_COLOR)
ui.append(summary_lbl)
# Left panel: category list
left_x = 20
cat_labels = []
for i, (key, display, count, _) in enumerate(cat_data):
y = 85 + i * 28
prefix = ">>>" if i == 0 else " "
lbl = mcrfpy.Caption(
text=f"{prefix} {display} ({count})",
pos=(left_x, y),
fill_color=HIGHLIGHT_COLOR if i == 0 else LABEL_COLOR)
ui.append(lbl)
cat_labels.append(lbl)
# Center panel: subcategory list
center_x = 280
sub_header = mcrfpy.Caption(text="Subcategories:",
pos=(center_x, 85), fill_color=ACCENT_COLOR)
ui.append(sub_header)
# We'll dynamically create labels for subcategories
sub_labels = []
sub_label_pool = [] # pre-allocated caption objects
for i in range(12):
lbl = mcrfpy.Caption(text="", pos=(center_x, 110 + i * 25),
fill_color=LABEL_COLOR)
ui.append(lbl)
sub_label_pool.append(lbl)
# Right panel: file list
right_x = 560
file_header = mcrfpy.Caption(text="Files:",
pos=(right_x, 85), fill_color=ACCENT_COLOR)
ui.append(file_header)
file_label_pool = []
for i in range(MAX_VISIBLE_FILES):
lbl = mcrfpy.Caption(text="", pos=(right_x, 110 + i * 25),
fill_color=DIM_COLOR)
ui.append(lbl)
file_label_pool.append(lbl)
scroll_info = mcrfpy.Caption(text="", pos=(right_x, 110 + MAX_VISIBLE_FILES * 25),
fill_color=DIM_COLOR)
ui.append(scroll_info)
def _refresh():
cat_key, display, count, sub_info = cat_data[state["cat_idx"]]
# Update category highlights
for i, lbl in enumerate(cat_labels):
key, disp, cnt, _ = cat_data[i]
if i == state["cat_idx"]:
lbl.text = f">>> {disp} ({cnt})"
lbl.fill_color = HIGHLIGHT_COLOR
else:
lbl.text = f" {disp} ({cnt})"
lbl.fill_color = LABEL_COLOR
# Update subcategory list
for i, lbl in enumerate(sub_label_pool):
if i < len(sub_info):
sc_label, sc_count, _ = sub_info[i]
prefix = ">" if i == state["sub_idx"] else " "
lbl.text = f"{prefix} {sc_label} ({sc_count})"
lbl.fill_color = ACCENT_COLOR if i == state["sub_idx"] else LABEL_COLOR
else:
lbl.text = ""
# Update file list for selected subcategory
if state["sub_idx"] < len(sub_info):
_, _, file_names = sub_info[state["sub_idx"]]
else:
file_names = []
scroll = state["scroll"]
visible = file_names[scroll:scroll + MAX_VISIBLE_FILES]
for i, lbl in enumerate(file_label_pool):
if i < len(visible):
lbl.text = visible[i]
else:
lbl.text = ""
if len(file_names) > MAX_VISIBLE_FILES:
scroll_info.text = f"({scroll + 1}-{min(scroll + MAX_VISIBLE_FILES, len(file_names))} of {len(file_names)}, PgUp/PgDn)"
else:
scroll_info.text = ""
_refresh()
controls = mcrfpy.Caption(
text="[W/S] Category [A/D] Subcategory [PgUp/PgDn] Scroll files [1-7] Scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
def on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return
if _handle_scene_switch(key):
return
_, _, _, sub_info = cat_data[state["cat_idx"]]
if key == mcrfpy.Key.W:
state["cat_idx"] = (state["cat_idx"] - 1) % len(cat_data)
state["sub_idx"] = 0
state["scroll"] = 0
_refresh()
elif key == mcrfpy.Key.S:
state["cat_idx"] = (state["cat_idx"] + 1) % len(cat_data)
state["sub_idx"] = 0
state["scroll"] = 0
_refresh()
elif key == mcrfpy.Key.A:
if sub_info:
state["sub_idx"] = (state["sub_idx"] - 1) % len(sub_info)
state["scroll"] = 0
_refresh()
elif key == mcrfpy.Key.D:
if sub_info:
state["sub_idx"] = (state["sub_idx"] + 1) % len(sub_info)
state["scroll"] = 0
_refresh()
elif key == mcrfpy.Key.PAGEDOWN:
if state["sub_idx"] < len(sub_info):
_, _, file_names = sub_info[state["sub_idx"]]
max_scroll = max(0, len(file_names) - MAX_VISIBLE_FILES)
state["scroll"] = min(state["scroll"] + MAX_VISIBLE_FILES, max_scroll)
_refresh()
elif key == mcrfpy.Key.PAGEUP:
state["scroll"] = max(0, state["scroll"] - MAX_VISIBLE_FILES)
_refresh()
scene.on_key = on_key
return scene
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
if not ASSET_DIR:
print("WARNING: No Puny-Characters asset directory found.")
print("Searched:", _SEARCH_PATHS)
print("The demo will show placeholder messages.")
print()
_build_scene_viewer()
_build_scene_hsl()
_build_scene_gallery()
_build_scene_factions()
_build_scene_layers()
_build_scene_equip()
_build_scene_inventory()
# Start animation timer (20fps animation updates)
# Keep a reference so the Python cache lookup works and (timer, runtime) is passed
global _anim_timer
_anim_timer = mcrfpy.Timer("shade_anim", _tick_all, 50)
# Activate first scene
mcrfpy.Scene("viewer").activate()
if __name__ == "__main__":
main()
# Do NOT call sys.exit(0) here - let the game loop run