McRogueFace/shade_sprite/demo.py

1590 lines
56 KiB
Python
Raw Normal View History

"""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
2026-02-27 22:12:17 -05:00
8 - Entity Animation: engine-native Entity.animate() with loop - all formats
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 (
2026-02-27 22:12:17 -05:00
AnimatedSprite, Direction, PUNY_24, PUNY_29, SLIME, CREATURE_RPGMAKER,
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(
2026-02-27 22:12:17 -05:00
text="[1-8] 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",
2026-02-27 22:12:17 -05:00
mcrfpy.Key.NUM_8: "entity_anim",
}
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(
2026-02-27 22:12:17 -05:00
text="[Q/E] Sheet [A/D] Animation [W/S] Direction [1-8] 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(
2026-02-27 22:12:17 -05:00
text="[Left/Right] Hue +/-30 [Up/Down] Sat +/-0.1 [Z/X] Lit +/-0.1 [Q/E] Sheet [1-8] 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(
2026-02-27 22:12:17 -05:00
text="[W/S] Direction [A/D] Animation [1-8] 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(
2026-02-27 22:12:17 -05:00
text="[Space] Re-roll factions [1-8] 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(
2026-02-27 22:12:17 -05:00
text="[Q/E] Overlay sheet [Left/Right] Overlay hue +/-30 [1-8] 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(
2026-02-27 22:12:17 -05:00
text="[Tab] Slot [Q/E] Sheet [Left/Right] Hue [Up/Down] Sat [Z/X] Lit [T] Toggle [R] Randomize [1-8] 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)
2026-02-27 22:12:17 -05:00
controls = mcrfpy.Caption(text="[1-8] 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(
2026-02-27 22:12:17 -05:00
text="[W/S] Category [A/D] Subcategory [PgUp/PgDn] Scroll files [1-8] 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
2026-02-27 22:12:17 -05:00
# ---------------------------------------------------------------------------
# Scene 8: Entity Animation (engine-native, all formats)
# ---------------------------------------------------------------------------
def _format_frame_list(fmt, anim_name, direction):
"""Convert animation def to flat sprite index list for Entity.animate()."""
anim = fmt.animations[anim_name]
return [fmt.sprite_index(f.col, direction) for f in anim.frames]
def _format_duration(fmt, anim_name):
"""Total duration in seconds."""
anim = fmt.animations[anim_name]
return sum(f.duration for f in anim.frames) / 1000.0
def _build_scene_entity_anim():
scene = mcrfpy.Scene("entity_anim")
sheets = _available_sheets()
if not sheets:
return _no_assets_fallback(scene, "Entity Animation")
ui = scene.children
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=BG)
ui.append(bg)
title = mcrfpy.Caption(text="[8] Entity Animation (engine-native loop)",
pos=(20, 10), fill_color=TITLE_COLOR)
ui.append(title)
explain = mcrfpy.Caption(
text="Entity.animate('sprite_index', [frames], duration, loop=True) - no Python timer needed",
pos=(20, 40), fill_color=LABEL_COLOR)
ui.append(explain)
# Collect all format sections
# Each section: format, texture path, available animations, grid + entities
sections = [] # (fmt, name, tex, grid, entities, anim_names)
state = {"anim_idx": 0, "dir_idx": 0}
section_y = 80
grid_w, grid_h = 200, 160
# --- PUNY_24 ---
puny24_lbl = mcrfpy.Caption(text="PUNY_24 (8-dir, free)",
pos=(20, section_y), fill_color=ACCENT_COLOR)
ui.append(puny24_lbl)
fmt24 = PUNY_24
tex24 = mcrfpy.Texture(sheets[0], fmt24.tile_w, fmt24.tile_h)
grid24 = mcrfpy.Grid(grid_size=(8, 1), texture=tex24,
pos=(20, section_y + 25), size=(grid_w * 2, grid_h))
grid24.zoom = 0.25
ui.append(grid24)
entities24 = []
anim_names24 = list(fmt24.animations.keys())
for i, d in enumerate(Direction):
e = mcrfpy.Entity(grid_pos=(i, 0), texture=tex24, sprite_index=0)
grid24.entities.append(e)
entities24.append(e)
sections.append((fmt24, "PUNY_24", tex24, grid24, entities24, anim_names24))
# Direction labels for compass
for i, d in enumerate(Direction):
lbl = mcrfpy.Caption(text=d.name, pos=(20 + i * 50, section_y + 25 + grid_h + 2),
fill_color=DIM_COLOR)
ui.append(lbl)
# --- PUNY_29 (if paid sheets exist with 29 cols) ---
# PUNY_29 uses 928px wide sheets; check if any available are that size
puny29_sheet = None
for s in sheets:
try:
# Try loading as PUNY_29 to check
t = mcrfpy.Texture(s, PUNY_29.tile_w, PUNY_29.tile_h)
# Check column count via sprite count (29 cols * 8 rows = 232)
puny29_sheet = s
break
except Exception:
pass
section_y2 = section_y + grid_h + 45
if puny29_sheet:
puny29_lbl = mcrfpy.Caption(text="PUNY_29 (8-dir, paid - extra anims)",
pos=(20, section_y2), fill_color=ACCENT_COLOR)
ui.append(puny29_lbl)
fmt29 = PUNY_29
tex29 = mcrfpy.Texture(puny29_sheet, fmt29.tile_w, fmt29.tile_h)
grid29 = mcrfpy.Grid(grid_size=(8, 1), texture=tex29,
pos=(20, section_y2 + 25), size=(grid_w * 2, grid_h))
grid29.zoom = 0.25
ui.append(grid29)
entities29 = []
anim_names29 = list(fmt29.animations.keys())
for i, d in enumerate(Direction):
e = mcrfpy.Entity(grid_pos=(i, 0), texture=tex29, sprite_index=0)
grid29.entities.append(e)
entities29.append(e)
sections.append((fmt29, "PUNY_29", tex29, grid29, entities29, anim_names29))
else:
puny29_lbl = mcrfpy.Caption(text="PUNY_29 (not available - need 928px wide sheet)",
pos=(20, section_y2), fill_color=DIM_COLOR)
ui.append(puny29_lbl)
# --- SLIME ---
section_y3 = section_y2 + grid_h + 45
slime_p = _slime_path()
if slime_p:
slime_lbl = mcrfpy.Caption(text="SLIME (1-dir, non-directional)",
pos=(20, section_y3), fill_color=ACCENT_COLOR)
ui.append(slime_lbl)
fmt_slime = SLIME
tex_slime = mcrfpy.Texture(slime_p, fmt_slime.tile_w, fmt_slime.tile_h)
grid_slime = mcrfpy.Grid(grid_size=(2, 1), texture=tex_slime,
pos=(20, section_y3 + 25), size=(120, grid_h))
grid_slime.zoom = 0.25
ui.append(grid_slime)
entities_slime = []
anim_names_slime = list(fmt_slime.animations.keys())
for i, aname in enumerate(anim_names_slime):
e = mcrfpy.Entity(grid_pos=(i, 0), texture=tex_slime, sprite_index=0)
grid_slime.entities.append(e)
entities_slime.append(e)
slime_note = mcrfpy.Caption(
text="idle / walk", pos=(20, section_y3 + 25 + grid_h + 2),
fill_color=DIM_COLOR)
ui.append(slime_note)
sections.append((fmt_slime, "SLIME", tex_slime, grid_slime,
entities_slime, anim_names_slime))
else:
slime_lbl = mcrfpy.Caption(text="SLIME (not available)",
pos=(20, section_y3), fill_color=DIM_COLOR)
ui.append(slime_lbl)
# --- Info panel (right side) ---
info_x = 500
anim_info = mcrfpy.Caption(text="Animation: idle", pos=(info_x, 80),
fill_color=HIGHLIGHT_COLOR)
ui.append(anim_info)
dir_info = mcrfpy.Caption(text="Direction: S (0)", pos=(info_x, 110),
fill_color=LABEL_COLOR)
ui.append(dir_info)
frame_info = mcrfpy.Caption(text="", pos=(info_x, 140),
fill_color=ACCENT_COLOR)
ui.append(frame_info)
# Code example
code_y = 200
code_lines = [
"# Engine-native sprite frame animation:",
"frames = [fmt.sprite_index(f.col, dir)",
" for f in fmt.animations['walk'].frames]",
"entity.animate('sprite_index', frames,",
" duration, loop=True)",
"",
"# No Python Timer or AnimatedSprite needed!",
"# The C++ AnimationManager handles the loop.",
]
for i, line in enumerate(code_lines):
c = mcrfpy.Caption(text=line, pos=(info_x, code_y + i * 25),
fill_color=mcrfpy.Color(150, 200, 150))
ui.append(c)
# Show all available animation names per format
names_y = code_y + len(code_lines) * 25 + 20
for fmt, name, _, _, _, anim_names in sections:
albl = mcrfpy.Caption(
text=f"{name}: {', '.join(anim_names)}",
pos=(info_x, names_y), fill_color=DIM_COLOR)
ui.append(albl)
names_y += 25
def _apply_anims():
"""Apply current animation to all entities in all sections."""
d = Direction(state["dir_idx"])
for fmt, name, tex, grid, entities, anim_names in sections:
idx = state["anim_idx"] % len(anim_names)
anim_name = anim_names[idx]
frames = _format_frame_list(fmt, anim_name, d)
dur = _format_duration(fmt, anim_name)
is_loop = fmt.animations[anim_name].loop
for e in entities:
e.animate("sprite_index", frames, dur, loop=is_loop)
# Use first section for info display
if sections:
fmt0, _, _, _, _, anames0 = sections[0]
idx0 = state["anim_idx"] % len(anames0)
aname = anames0[idx0]
adef = fmt0.animations[aname]
nf = len(adef.frames)
loop_str = "loop" if adef.loop else "one-shot"
chain_str = f" -> {adef.chain_to}" if adef.chain_to else ""
anim_info.text = f"Animation: {aname}"
frame_info.text = f"Frames: {nf} ({loop_str}{chain_str})"
dir_info.text = f"Direction: {d.name} ({d.value})"
_apply_anims()
controls = mcrfpy.Caption(
text="[A/D] Animation [W/S] Direction [1-8] 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.A:
state["anim_idx"] -= 1
_apply_anims()
elif key == mcrfpy.Key.D:
state["anim_idx"] += 1
_apply_anims()
elif key == mcrfpy.Key.W:
state["dir_idx"] = (state["dir_idx"] - 1) % 8
_apply_anims()
elif key == mcrfpy.Key.S:
state["dir_idx"] = (state["dir_idx"] + 1) % 8
_apply_anims()
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()
2026-02-27 22:12:17 -05:00
_build_scene_entity_anim()
# 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