entity animation version demo

This commit is contained in:
John McCardle 2026-02-27 22:12:17 -05:00
commit a52568cc8d

View file

@ -11,6 +11,7 @@ Scenes:
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
8 - Entity Animation: engine-native Entity.animate() with loop - all formats
Controls shown on-screen per scene.
"""
@ -90,7 +91,7 @@ if __name__ == "__main__":
sys.path.insert(0, parent_dir)
from shade_sprite import (
AnimatedSprite, Direction, PUNY_24, SLIME,
AnimatedSprite, Direction, PUNY_24, PUNY_29, SLIME, CREATURE_RPGMAKER,
CharacterAssembler,
AssetLibrary, FactionGenerator,
)
@ -130,7 +131,7 @@ def _no_assets_fallback(scene, scene_name):
pos=(20, 60), fill_color=WARN_COLOR)
ui.append(msg)
controls = mcrfpy.Caption(
text="[1-7] Switch scenes",
text="[1-8] Switch scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
@ -152,6 +153,7 @@ def _handle_scene_switch(key):
mcrfpy.Key.NUM_5: "layers",
mcrfpy.Key.NUM_6: "equip",
mcrfpy.Key.NUM_7: "inventory",
mcrfpy.Key.NUM_8: "entity_anim",
}
name = scene_map.get(key)
if name:
@ -267,7 +269,7 @@ def _build_scene_viewer():
ui.append(anim_ref)
controls = mcrfpy.Caption(
text="[Q/E] Sheet [A/D] Animation [W/S] Direction [1-7] Scenes",
text="[Q/E] Sheet [A/D] Animation [W/S] Direction [1-8] Scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
@ -415,7 +417,7 @@ def _build_scene_hsl():
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",
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)
@ -547,7 +549,7 @@ def _build_scene_gallery():
ui.append(dir_info)
controls = mcrfpy.Caption(
text="[W/S] Direction [A/D] Animation [1-7] Scenes",
text="[W/S] Direction [A/D] Animation [1-8] Scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
@ -659,7 +661,7 @@ def _build_scene_factions():
_populate()
controls = mcrfpy.Caption(
text="[Space] Re-roll factions [1-7] Scenes",
text="[Space] Re-roll factions [1-8] Scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
@ -840,7 +842,7 @@ def _build_scene_layers():
ui.append(code_lbl4)
controls = mcrfpy.Caption(
text="[Q/E] Overlay sheet [Left/Right] Overlay hue +/-30 [1-7] Scenes",
text="[Q/E] Overlay sheet [Left/Right] Overlay hue +/-30 [1-8] Scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
@ -1062,7 +1064,7 @@ def _build_scene_equip():
_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",
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)
@ -1148,7 +1150,7 @@ def _build_scene_inventory():
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",
controls = mcrfpy.Caption(text="[1-8] Switch scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
@ -1275,7 +1277,7 @@ def _build_scene_inventory():
_refresh()
controls = mcrfpy.Caption(
text="[W/S] Category [A/D] Subcategory [PgUp/PgDn] Scroll files [1-7] Scenes",
text="[W/S] Category [A/D] Subcategory [PgUp/PgDn] Scroll files [1-8] Scenes",
pos=(20, 740), fill_color=DIM_COLOR)
ui.append(controls)
@ -1321,6 +1323,240 @@ def _build_scene_inventory():
return scene
# ---------------------------------------------------------------------------
# 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
# ---------------------------------------------------------------------------
@ -1338,6 +1574,7 @@ def main():
_build_scene_layers()
_build_scene_equip()
_build_scene_inventory()
_build_scene_entity_anim()
# Start animation timer (20fps animation updates)
# Keep a reference so the Python cache lookup works and (timer, runtime) is passed