Shade (merchant-shade.itch.io) entity animation tests

This commit is contained in:
John McCardle 2026-02-16 20:19:39 -05:00
commit 6fdf7279ce
10 changed files with 1813 additions and 3 deletions

661
shade_sprite/demo.py Normal file
View file

@ -0,0 +1,661 @@
"""shade_sprite interactive demo.
Run from the build directory:
./mcrogueface --exec ../shade_sprite/demo.py
Or copy the shade_sprite directory into build/scripts/ and run:
./mcrogueface --exec scripts/shade_sprite/demo.py
Scenes:
1 - Animation Viewer: cycle animations and directions
2 - HSL Recolor: live hue/saturation/lightness shifting
3 - Creature Gallery: grid of animated characters
4 - Faction Generator: random faction color schemes
Controls shown on-screen per scene.
"""
import mcrfpy
import sys
import os
import random
# ---------------------------------------------------------------------------
# Asset discovery
# ---------------------------------------------------------------------------
# Search paths for Puny Character sprites
_SEARCH_PATHS = [
"assets/Puny-Characters",
"../assets/Puny-Characters",
# 7DRL dev location
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 sheets available in the free CC0 pack
_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
# Import shade_sprite (handle being run from different locations)
if __name__ == "__main__":
# Add parent dir to path so shade_sprite can be imported
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, PUNY_29, SLIME,
detect_format, CharacterAssembler,
)
# ---------------------------------------------------------------------------
# Globals
# ---------------------------------------------------------------------------
_animated_sprites = [] # all AnimatedSprite instances to tick
_active_scene = None
def _tick_all(timer, runtime):
"""Global animation tick callback."""
for a in _animated_sprites:
a.tick(timer.interval)
# ---------------------------------------------------------------------------
# Scene 1: Animation Viewer
# ---------------------------------------------------------------------------
def _build_scene_viewer():
scene = mcrfpy.Scene("viewer")
ui = scene.children
# Background
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
fill_color=mcrfpy.Color(30, 30, 40))
ui.append(bg)
# Title
title = mcrfpy.Caption(text="shade_sprite - Animation Viewer",
pos=(20, 10),
fill_color=mcrfpy.Color(220, 220, 255))
ui.append(title)
sheets = _available_sheets()
if not sheets:
msg = mcrfpy.Caption(
text="No sprite assets found. Place Puny-Characters PNGs in assets/Puny-Characters/",
pos=(20, 60),
fill_color=mcrfpy.Color(255, 100, 100))
ui.append(msg)
return scene
# State
state = {
"sheet_idx": 0,
"anim_idx": 0,
"dir_idx": 0,
}
# Determine format
fmt = PUNY_24 # Free pack is 768x256
anim_names = list(fmt.animations.keys())
dir_names = [d.name for d in Direction]
# Load first sheet
tex = mcrfpy.Texture(sheets[0], fmt.tile_w, fmt.tile_h)
# Main sprite display (scaled up 4x)
sprite = mcrfpy.Sprite(texture=tex, pos=(200, 200), 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=mcrfpy.Color(180, 180, 200))
ui.append(sheet_label)
anim_label = mcrfpy.Caption(
text=f"Animation: idle",
pos=(20, 80),
fill_color=mcrfpy.Color(180, 180, 200))
ui.append(anim_label)
dir_label = mcrfpy.Caption(
text=f"Direction: S",
pos=(20, 110),
fill_color=mcrfpy.Color(180, 180, 200))
ui.append(dir_label)
controls = mcrfpy.Caption(
text="[Q/E] Sheet [A/D] Animation [W/S] Direction [2] HSL [3] Gallery [4] Factions",
pos=(20, 740),
fill_color=mcrfpy.Color(120, 120, 140))
ui.append(controls)
# Also show all 8 directions as small sprites
dir_sprites = []
dir_anims = []
for i, d in enumerate(Direction):
dx = 500 + (i % 4) * 80
dy = 200 + (i // 4) * 100
s = mcrfpy.Sprite(texture=tex, pos=(dx, dy), scale=3.0)
ui.append(s)
a = AnimatedSprite(s, fmt, d)
a.play("idle")
_animated_sprites.append(a)
dir_sprites.append(s)
dir_anims.append(a)
# Direction label
lbl = mcrfpy.Caption(text=d.name, pos=(dx + 10, dy - 18),
fill_color=mcrfpy.Color(150, 150, 170))
ui.append(lbl)
def on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.Q:
# Previous sheet
state["sheet_idx"] = (state["sheet_idx"] - 1) % len(sheets)
_reload_sheet()
elif key == mcrfpy.Key.E:
# Next sheet
state["sheet_idx"] = (state["sheet_idx"] + 1) % len(sheets)
_reload_sheet()
elif key == mcrfpy.Key.A:
# Previous animation
state["anim_idx"] = (state["anim_idx"] - 1) % len(anim_names)
_update_anim()
elif key == mcrfpy.Key.D:
# Next animation
state["anim_idx"] = (state["anim_idx"] + 1) % len(anim_names)
_update_anim()
elif key == mcrfpy.Key.W:
# Previous direction
state["dir_idx"] = (state["dir_idx"] - 1) % 8
_update_dir()
elif key == mcrfpy.Key.S:
# Next direction
state["dir_idx"] = (state["dir_idx"] + 1) % 8
_update_dir()
elif key == mcrfpy.Key.Num2:
mcrfpy.Scene("hsl").activate()
elif key == mcrfpy.Key.Num3:
mcrfpy.Scene("gallery").activate()
elif key == mcrfpy.Key.Num4:
mcrfpy.Scene("factions").activate()
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}"
def _update_dir():
d = Direction(state["dir_idx"])
anim.direction = d
dir_label.text = f"Direction: {d.name}"
scene.on_key = on_key
return scene
# ---------------------------------------------------------------------------
# Scene 2: HSL Recolor Demo
# ---------------------------------------------------------------------------
def _build_scene_hsl():
scene = mcrfpy.Scene("hsl")
ui = scene.children
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
fill_color=mcrfpy.Color(30, 30, 40))
ui.append(bg)
title = mcrfpy.Caption(text="shade_sprite - HSL Recoloring",
pos=(20, 10),
fill_color=mcrfpy.Color(220, 220, 255))
ui.append(title)
sheets = _available_sheets()
if not sheets:
msg = mcrfpy.Caption(
text="No sprite assets found.",
pos=(20, 60),
fill_color=mcrfpy.Color(255, 100, 100))
ui.append(msg)
def on_key(key, action):
if action == mcrfpy.InputState.PRESSED and key == mcrfpy.Key.Num1:
mcrfpy.Scene("viewer").activate()
scene.on_key = on_key
return scene
fmt = PUNY_24
state = {
"hue": 0.0,
"sat": 0.0,
"lit": 0.0,
"sheet_idx": 0,
}
# Original sprite (left)
orig_tex = mcrfpy.Texture(sheets[0], fmt.tile_w, fmt.tile_h)
orig_sprite = mcrfpy.Sprite(texture=orig_tex, pos=(150, 250), 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=(170, 220),
fill_color=mcrfpy.Color(180, 180, 200))
ui.append(orig_label)
# Shifted sprite (right)
shifted_sprite = mcrfpy.Sprite(texture=orig_tex, pos=(550, 250), 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="Shifted", pos=(570, 220),
fill_color=mcrfpy.Color(180, 180, 200))
ui.append(shifted_label)
# HSL value displays
hue_label = mcrfpy.Caption(text="Hue: 0.0", pos=(20, 500),
fill_color=mcrfpy.Color(255, 180, 180))
ui.append(hue_label)
sat_label = mcrfpy.Caption(text="Sat: 0.0", pos=(20, 530),
fill_color=mcrfpy.Color(180, 255, 180))
ui.append(sat_label)
lit_label = mcrfpy.Caption(text="Lit: 0.0", pos=(20, 560),
fill_color=mcrfpy.Color(180, 180, 255))
ui.append(lit_label)
controls = mcrfpy.Caption(
text="[Left/Right] Hue +/-30 [Up/Down] Sat +/-0.1 [Z/X] Lit +/-0.1 [Q/E] Sheet [1] Viewer",
pos=(20, 740),
fill_color=mcrfpy.Color(120, 120, 140))
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 on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
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()
elif key == mcrfpy.Key.Num1:
mcrfpy.Scene("viewer").activate()
return
elif key == mcrfpy.Key.Num3:
mcrfpy.Scene("gallery").activate()
return
elif key == mcrfpy.Key.Num4:
mcrfpy.Scene("factions").activate()
return
if changed:
_rebuild_shifted()
def _reload():
path = sheets[state["sheet_idx"]]
new_tex = mcrfpy.Texture(path, fmt.tile_w, fmt.tile_h)
orig_sprite.texture = new_tex
_rebuild_shifted()
scene.on_key = on_key
_rebuild_shifted()
return scene
# ---------------------------------------------------------------------------
# Scene 3: Creature Gallery
# ---------------------------------------------------------------------------
def _build_scene_gallery():
scene = mcrfpy.Scene("gallery")
ui = scene.children
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
fill_color=mcrfpy.Color(30, 30, 40))
ui.append(bg)
title = mcrfpy.Caption(text="shade_sprite - Character Gallery",
pos=(20, 10),
fill_color=mcrfpy.Color(220, 220, 255))
ui.append(title)
sheets = _available_sheets()
if not sheets:
msg = mcrfpy.Caption(
text="No sprite assets found.",
pos=(20, 60),
fill_color=mcrfpy.Color(255, 100, 100))
ui.append(msg)
def on_key(key, action):
if action == mcrfpy.InputState.PRESSED and key == mcrfpy.Key.Num1:
mcrfpy.Scene("viewer").activate()
scene.on_key = on_key
return scene
fmt = PUNY_24
directions = [Direction.S, Direction.SW, Direction.W, Direction.NW,
Direction.N, Direction.NE, Direction.E, Direction.SE]
# Layout: grid of characters, 4 columns
cols = 4
x_start, y_start = 40, 60
x_spacing, y_spacing = 240, 130
scale = 3.0
gallery_anims = []
count = min(len(sheets), 16) # max 4x4 grid
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 + 20, y + 20),
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),
fill_color=mcrfpy.Color(150, 150, 170))
ui.append(lbl)
state = {"dir_idx": 0, "anim_idx": 1} # start with walk
anim_names = list(fmt.animations.keys())
controls = mcrfpy.Caption(
text="[W/S] Direction [A/D] Animation [1] Viewer [2] HSL [4] Factions",
pos=(20, 740),
fill_color=mcrfpy.Color(120, 120, 140))
ui.append(controls)
def on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
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
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
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)
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)
elif key == mcrfpy.Key.Num1:
mcrfpy.Scene("viewer").activate()
elif key == mcrfpy.Key.Num2:
mcrfpy.Scene("hsl").activate()
elif key == mcrfpy.Key.Num4:
mcrfpy.Scene("factions").activate()
scene.on_key = on_key
return scene
# ---------------------------------------------------------------------------
# Scene 4: Faction Generator
# ---------------------------------------------------------------------------
def _build_scene_factions():
scene = mcrfpy.Scene("factions")
ui = scene.children
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
fill_color=mcrfpy.Color(30, 30, 40))
ui.append(bg)
title = mcrfpy.Caption(text="shade_sprite - Faction Generator",
pos=(20, 10),
fill_color=mcrfpy.Color(220, 220, 255))
ui.append(title)
sheets = _available_sheets()
if not sheets:
msg = mcrfpy.Caption(
text="No sprite assets found.",
pos=(20, 60),
fill_color=mcrfpy.Color(255, 100, 100))
ui.append(msg)
def on_key(key, action):
if action == mcrfpy.InputState.PRESSED and key == mcrfpy.Key.Num1:
mcrfpy.Scene("viewer").activate()
scene.on_key = on_key
return scene
fmt = PUNY_24
scale = 3.0
# State for generated factions
faction_anims = [] # store references for animation ticking
faction_sprites = [] # sprites to update on re-roll
faction_labels = []
# Faction colors (HSL hue values)
faction_hues = [0, 60, 120, 180, 240, 300]
faction_names_pool = [
"Iron Guard", "Shadow Pact", "Dawn Order", "Ember Clan",
"Frost Legion", "Vine Court", "Storm Band", "Ash Wardens",
"Gold Company", "Crimson Oath", "Azure Fleet", "Jade Circle",
]
def _generate_factions():
# Clear old faction animations from global list
for a in faction_anims:
if a in _animated_sprites:
_animated_sprites.remove(a)
faction_anims.clear()
# Pick 4 factions with random hues and characters
hues = random.sample(faction_hues, min(4, len(faction_hues)))
names = random.sample(faction_names_pool, 4)
# We'll create sprites dynamically
# Clear old sprites (rebuild scene content below bg/title/controls)
while len(ui) > 3: # keep bg, title, controls
# Can't easily remove from UICollection, so we rebuild the scene
pass
# Actually, just position everything and update textures
return hues, names
def _build_faction_display():
for a in faction_anims:
if a in _animated_sprites:
_animated_sprites.remove(a)
faction_anims.clear()
faction_sprites.clear()
faction_labels.clear()
hues = [random.uniform(0, 360) for _ in range(4)]
names = random.sample(faction_names_pool, 4)
y_start = 80
for fi in range(4):
y = y_start + fi * 160
hue = hues[fi]
# Faction name
lbl = mcrfpy.Caption(
text=f"{names[fi]} (hue {hue:.0f})",
pos=(20, y),
fill_color=mcrfpy.Color(200, 200, 220))
ui.append(lbl)
faction_labels.append(lbl)
# Pick 4 random character sheets for this faction
chosen = random.sample(sheets, min(4, len(sheets)))
for ci, path in enumerate(chosen):
x = 40 + ci * 200
# Apply faction hue shift
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)
faction_sprites.append(s)
a = AnimatedSprite(s, fmt, Direction.S)
a.play("walk")
_animated_sprites.append(a)
faction_anims.append(a)
controls = mcrfpy.Caption(
text="[Space] Re-roll factions [1] Viewer [2] HSL [3] Gallery",
pos=(20, 740),
fill_color=mcrfpy.Color(120, 120, 140))
ui.append(controls)
_build_faction_display()
def on_key(key, action):
if action != mcrfpy.InputState.PRESSED:
return
if key == mcrfpy.Key.SPACE:
# Rebuild scene
_rebuild_factions_scene()
elif key == mcrfpy.Key.Num1:
mcrfpy.Scene("viewer").activate()
elif key == mcrfpy.Key.Num2:
mcrfpy.Scene("hsl").activate()
elif key == mcrfpy.Key.Num3:
mcrfpy.Scene("gallery").activate()
def _rebuild_factions_scene():
# Easiest: rebuild the whole scene
new_scene = _build_scene_factions()
new_scene.activate()
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 all scenes
_build_scene_viewer()
_build_scene_hsl()
_build_scene_gallery()
_build_scene_factions()
# Start animation timer (20fps animation updates)
mcrfpy.Timer("shade_anim", _tick_all, 50)
# Activate first scene
mcrfpy.Scene("viewer").activate()
if __name__ == "__main__":
main()
sys.exit(0)