Shade (merchant-shade.itch.io) entity animation tests
This commit is contained in:
parent
2681cbd957
commit
6fdf7279ce
10 changed files with 1813 additions and 3 deletions
66
shade_sprite/__init__.py
Normal file
66
shade_sprite/__init__.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""shade_sprite - Sprite animation and compositing for Merchant Shade sprite packs.
|
||||
|
||||
A standalone module for McRogueFace that loads, composites, and animates
|
||||
layered pixel art character sheets. Supports multiple sprite sheet formats
|
||||
including the Puny Characters pack (paid & free), RPG Maker creatures,
|
||||
and slime sheets.
|
||||
|
||||
Quick start:
|
||||
from shade_sprite import AnimatedSprite, Direction, PUNY_29
|
||||
import mcrfpy
|
||||
|
||||
tex = mcrfpy.Texture("Warrior-Red.png", 32, 32)
|
||||
sprite = mcrfpy.Sprite(texture=tex, pos=(100, 100), scale=2.0)
|
||||
scene.children.append(sprite)
|
||||
|
||||
anim = AnimatedSprite(sprite, PUNY_29)
|
||||
anim.play("walk")
|
||||
anim.direction = Direction.E
|
||||
|
||||
def tick_anims(timer, runtime):
|
||||
anim.tick(timer.interval)
|
||||
mcrfpy.Timer("anim", tick_anims, 50)
|
||||
|
||||
For layered characters:
|
||||
from shade_sprite import CharacterAssembler, PUNY_29
|
||||
|
||||
assembler = CharacterAssembler(PUNY_29)
|
||||
assembler.add_layer("skins/Human1.png")
|
||||
assembler.add_layer("clothes/BasicBlue-Body.png", hue_shift=120.0)
|
||||
assembler.add_layer("hair/M-Hairstyle1-Black.png")
|
||||
texture = assembler.build("my_character")
|
||||
"""
|
||||
|
||||
from .formats import (
|
||||
Direction,
|
||||
AnimFrame,
|
||||
AnimDef,
|
||||
SheetFormat,
|
||||
PUNY_29,
|
||||
PUNY_24,
|
||||
CREATURE_RPGMAKER,
|
||||
SLIME,
|
||||
ALL_FORMATS,
|
||||
detect_format,
|
||||
)
|
||||
from .animation import AnimatedSprite
|
||||
from .assembler import CharacterAssembler
|
||||
|
||||
__all__ = [
|
||||
# Core classes
|
||||
"AnimatedSprite",
|
||||
"CharacterAssembler",
|
||||
# Format definitions
|
||||
"Direction",
|
||||
"AnimFrame",
|
||||
"AnimDef",
|
||||
"SheetFormat",
|
||||
# Predefined formats
|
||||
"PUNY_29",
|
||||
"PUNY_24",
|
||||
"CREATURE_RPGMAKER",
|
||||
"SLIME",
|
||||
"ALL_FORMATS",
|
||||
# Utilities
|
||||
"detect_format",
|
||||
]
|
||||
127
shade_sprite/animation.py
Normal file
127
shade_sprite/animation.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
"""AnimatedSprite - animation state machine for sprite sheet playback.
|
||||
|
||||
Wraps an mcrfpy.Sprite with frame timing and directional animation.
|
||||
Call tick() each frame (or from a timer) to advance the animation.
|
||||
"""
|
||||
from .formats import Direction, SheetFormat, AnimDef
|
||||
|
||||
|
||||
class AnimatedSprite:
|
||||
"""Animates an mcrfpy.Sprite using a SheetFormat definition.
|
||||
|
||||
The sprite's sprite_index is updated automatically based on the
|
||||
current animation, direction, and elapsed time.
|
||||
|
||||
Args:
|
||||
sprite: An mcrfpy.Sprite object to animate
|
||||
fmt: SheetFormat describing the sheet layout
|
||||
direction: Initial facing direction (default: Direction.S)
|
||||
"""
|
||||
|
||||
def __init__(self, sprite, fmt, direction=Direction.S):
|
||||
self.sprite = sprite
|
||||
self.fmt = fmt
|
||||
self._direction = direction
|
||||
self._anim_name = None
|
||||
self._anim = None
|
||||
self._frame_idx = 0
|
||||
self._elapsed = 0.0
|
||||
self._finished = False
|
||||
|
||||
# Start with idle if available
|
||||
if "idle" in fmt.animations:
|
||||
self.play("idle")
|
||||
|
||||
@property
|
||||
def direction(self):
|
||||
return self._direction
|
||||
|
||||
@direction.setter
|
||||
def direction(self, d):
|
||||
if not isinstance(d, Direction):
|
||||
d = Direction(d)
|
||||
if d != self._direction:
|
||||
self._direction = d
|
||||
self._update_tile()
|
||||
|
||||
@property
|
||||
def animation_name(self):
|
||||
return self._anim_name
|
||||
|
||||
@property
|
||||
def frame_index(self):
|
||||
return self._frame_idx
|
||||
|
||||
@property
|
||||
def finished(self):
|
||||
return self._finished
|
||||
|
||||
def set_direction(self, d):
|
||||
"""Set facing direction. Updates tile immediately."""
|
||||
self.direction = d
|
||||
|
||||
def play(self, anim_name):
|
||||
"""Start playing a named animation.
|
||||
|
||||
Args:
|
||||
anim_name: Animation name (must exist in the format's animations dict)
|
||||
|
||||
Raises:
|
||||
KeyError: If animation name not found in format
|
||||
"""
|
||||
if anim_name not in self.fmt.animations:
|
||||
raise KeyError(
|
||||
f"Animation '{anim_name}' not found in format '{self.fmt.name}'. "
|
||||
f"Available: {list(self.fmt.animations.keys())}"
|
||||
)
|
||||
self._anim_name = anim_name
|
||||
self._anim = self.fmt.animations[anim_name]
|
||||
self._frame_idx = 0
|
||||
self._elapsed = 0.0
|
||||
self._finished = False
|
||||
self._update_tile()
|
||||
|
||||
def tick(self, dt_ms):
|
||||
"""Advance animation clock by dt_ms milliseconds.
|
||||
|
||||
Call this from a timer callback or game loop. Updates the
|
||||
sprite's sprite_index when frames change.
|
||||
|
||||
Args:
|
||||
dt_ms: Time elapsed in milliseconds since last tick
|
||||
"""
|
||||
if self._anim is None or self._finished:
|
||||
return
|
||||
|
||||
self._elapsed += dt_ms
|
||||
frames = self._anim.frames
|
||||
|
||||
# Advance frames while we have accumulated enough time
|
||||
while self._elapsed >= frames[self._frame_idx].duration:
|
||||
self._elapsed -= frames[self._frame_idx].duration
|
||||
self._frame_idx += 1
|
||||
|
||||
if self._frame_idx >= len(frames):
|
||||
if self._anim.loop:
|
||||
self._frame_idx = 0
|
||||
else:
|
||||
# One-shot finished
|
||||
if self._anim.chain_to and self._anim.chain_to in self.fmt.animations:
|
||||
self.play(self._anim.chain_to)
|
||||
return
|
||||
else:
|
||||
# Stay on last frame
|
||||
self._frame_idx = len(frames) - 1
|
||||
self._finished = True
|
||||
self._elapsed = 0.0
|
||||
break
|
||||
|
||||
self._update_tile()
|
||||
|
||||
def _update_tile(self):
|
||||
"""Set sprite.sprite_index based on current animation frame and direction."""
|
||||
if self._anim is None:
|
||||
return
|
||||
frame = self._anim.frames[self._frame_idx]
|
||||
idx = self.fmt.sprite_index(frame.col, self._direction)
|
||||
self.sprite.sprite_index = idx
|
||||
75
shade_sprite/assembler.py
Normal file
75
shade_sprite/assembler.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
"""CharacterAssembler - composite layered sprite sheets with HSL recoloring.
|
||||
|
||||
Uses the engine's Texture.composite() and texture.hsl_shift() methods to
|
||||
build composite character textures from multiple layer PNG files, without
|
||||
requiring PIL or any external Python packages.
|
||||
"""
|
||||
import mcrfpy
|
||||
from .formats import PUNY_29, SheetFormat
|
||||
|
||||
|
||||
class CharacterAssembler:
|
||||
"""Build composite character sheets from layer files.
|
||||
|
||||
Layers are added bottom-to-top (skin first, then clothes, hair, etc).
|
||||
Each layer can be HSL-shifted for recoloring before compositing.
|
||||
|
||||
Args:
|
||||
fmt: SheetFormat describing the sprite dimensions (default: PUNY_29)
|
||||
"""
|
||||
|
||||
def __init__(self, fmt=None):
|
||||
if fmt is None:
|
||||
fmt = PUNY_29
|
||||
self.fmt = fmt
|
||||
self.layers = []
|
||||
|
||||
def add_layer(self, path, hue_shift=0.0, sat_shift=0.0, lit_shift=0.0):
|
||||
"""Queue a layer PNG with optional HSL recoloring.
|
||||
|
||||
Args:
|
||||
path: File path to the layer PNG
|
||||
hue_shift: Hue rotation in degrees [0, 360)
|
||||
sat_shift: Saturation adjustment [-1.0, 1.0]
|
||||
lit_shift: Lightness adjustment [-1.0, 1.0]
|
||||
"""
|
||||
self.layers.append((path, hue_shift, sat_shift, lit_shift))
|
||||
return self # allow chaining
|
||||
|
||||
def clear(self):
|
||||
"""Remove all queued layers."""
|
||||
self.layers.clear()
|
||||
return self
|
||||
|
||||
def build(self, name="<composed>"):
|
||||
"""Composite all queued layers into a single Texture.
|
||||
|
||||
Loads each layer file, applies HSL shifts if any, then composites
|
||||
all layers bottom-to-top using alpha blending.
|
||||
|
||||
Args:
|
||||
name: Optional name for the resulting texture
|
||||
|
||||
Returns:
|
||||
mcrfpy.Texture: The composited texture
|
||||
|
||||
Raises:
|
||||
ValueError: If no layers have been added
|
||||
IOError: If a layer file cannot be loaded
|
||||
"""
|
||||
if not self.layers:
|
||||
raise ValueError("No layers added. Call add_layer() first.")
|
||||
|
||||
textures = []
|
||||
for path, h, s, l in self.layers:
|
||||
tex = mcrfpy.Texture(path, self.fmt.tile_w, self.fmt.tile_h)
|
||||
if h != 0.0 or s != 0.0 or l != 0.0:
|
||||
tex = tex.hsl_shift(h, s, l)
|
||||
textures.append(tex)
|
||||
|
||||
if len(textures) == 1:
|
||||
return textures[0]
|
||||
|
||||
return mcrfpy.Texture.composite(
|
||||
textures, self.fmt.tile_w, self.fmt.tile_h, name
|
||||
)
|
||||
661
shade_sprite/demo.py
Normal file
661
shade_sprite/demo.py
Normal 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)
|
||||
261
shade_sprite/formats.py
Normal file
261
shade_sprite/formats.py
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
"""Sheet layout definitions for Merchant Shade sprite packs.
|
||||
|
||||
Data-driven animation format descriptions. Each SheetFormat defines the
|
||||
tile dimensions, direction layout, and animation frame sequences for a
|
||||
specific sprite sheet layout.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class Direction(IntEnum):
|
||||
"""8-directional facing. Row index matches Puny Character sheet layout."""
|
||||
S = 0
|
||||
SW = 1
|
||||
W = 2
|
||||
NW = 3
|
||||
N = 4
|
||||
NE = 5
|
||||
E = 6
|
||||
SE = 7
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnimFrame:
|
||||
"""A single frame in an animation sequence."""
|
||||
col: int # Column index in the sheet
|
||||
duration: int # Duration in milliseconds
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnimDef:
|
||||
"""Definition of a single animation type."""
|
||||
name: str
|
||||
frames: list # list of AnimFrame
|
||||
loop: bool = True
|
||||
chain_to: str = None # animation to play after one-shot finishes
|
||||
|
||||
|
||||
@dataclass
|
||||
class SheetFormat:
|
||||
"""Complete definition of a sprite sheet layout."""
|
||||
name: str
|
||||
tile_w: int # Pixel width of each frame
|
||||
tile_h: int # Pixel height of each frame
|
||||
columns: int # Total columns in the sheet
|
||||
rows: int # Total rows (directions)
|
||||
directions: int # Number of directional rows (4 or 8)
|
||||
animations: dict = field(default_factory=dict) # name -> AnimDef
|
||||
grid_cell: tuple = (16, 16) # Target grid cell size
|
||||
render_offset: tuple = (0, 0) # Pixel offset for rendering on grid
|
||||
|
||||
def direction_row(self, d):
|
||||
"""Get row index for a direction, wrapping for 4-dir and 1-dir sheets."""
|
||||
if self.directions == 1:
|
||||
return 0
|
||||
if self.directions == 8:
|
||||
return int(d)
|
||||
# 4-dir: S=0, W=1, E=2, N=3
|
||||
mapping = {
|
||||
Direction.S: 0, Direction.SW: 0,
|
||||
Direction.W: 1, Direction.NW: 1,
|
||||
Direction.N: 3, Direction.NE: 3,
|
||||
Direction.E: 2, Direction.SE: 2,
|
||||
}
|
||||
return mapping.get(d, 0)
|
||||
|
||||
def sprite_index(self, col, direction):
|
||||
"""Get the flat sprite index for a column and direction."""
|
||||
row = self.direction_row(direction)
|
||||
return row * self.columns + col
|
||||
|
||||
|
||||
def _make_anim(name, start_col, count, ms_per_frame, loop=True, chain_to="idle"):
|
||||
"""Helper to create a simple sequential animation."""
|
||||
frames = [AnimFrame(col=start_col + i, duration=ms_per_frame)
|
||||
for i in range(count)]
|
||||
if loop:
|
||||
return AnimDef(name=name, frames=frames, loop=True)
|
||||
return AnimDef(name=name, frames=frames, loop=False, chain_to=chain_to)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 29-column paid Puny Character format (928x256 @ 32x32)
|
||||
# =============================================================================
|
||||
_puny29_anims = {}
|
||||
|
||||
# idle: cols 0-1, 300ms each, loop
|
||||
_puny29_anims["idle"] = AnimDef("idle", [
|
||||
AnimFrame(0, 300), AnimFrame(1, 300),
|
||||
], loop=True)
|
||||
|
||||
# walk: cols 1-4, 200ms each, loop
|
||||
_puny29_anims["walk"] = AnimDef("walk", [
|
||||
AnimFrame(1, 200), AnimFrame(2, 200), AnimFrame(3, 200), AnimFrame(4, 200),
|
||||
], loop=True)
|
||||
|
||||
# slash: cols 5-8, 100ms each, one-shot
|
||||
_puny29_anims["slash"] = _make_anim("slash", 5, 4, 100, loop=False)
|
||||
|
||||
# bow: cols 9-12, 100ms each, one-shot
|
||||
_puny29_anims["bow"] = _make_anim("bow", 9, 4, 100, loop=False)
|
||||
|
||||
# thrust: cols 13-15 + repeat last, 100ms each, one-shot
|
||||
_puny29_anims["thrust"] = AnimDef("thrust", [
|
||||
AnimFrame(13, 100), AnimFrame(14, 100), AnimFrame(15, 100), AnimFrame(15, 100),
|
||||
], loop=False, chain_to="idle")
|
||||
|
||||
# spellcast: cols 16-18 + repeat last, 100ms each, one-shot
|
||||
_puny29_anims["spellcast"] = AnimDef("spellcast", [
|
||||
AnimFrame(16, 100), AnimFrame(17, 100), AnimFrame(18, 100), AnimFrame(18, 100),
|
||||
], loop=False, chain_to="idle")
|
||||
|
||||
# hurt: cols 19-21, 100ms each, one-shot
|
||||
_puny29_anims["hurt"] = _make_anim("hurt", 19, 3, 100, loop=False)
|
||||
|
||||
# death: cols 22-24, 100ms + 800ms hold, one-shot (no chain)
|
||||
_puny29_anims["death"] = AnimDef("death", [
|
||||
AnimFrame(22, 100), AnimFrame(23, 100), AnimFrame(24, 800),
|
||||
], loop=False, chain_to=None)
|
||||
|
||||
# dodge: bounce pattern 25,26,25,27, 200ms each, one-shot
|
||||
_puny29_anims["dodge"] = AnimDef("dodge", [
|
||||
AnimFrame(25, 200), AnimFrame(26, 200), AnimFrame(25, 200), AnimFrame(27, 200),
|
||||
], loop=False, chain_to="idle")
|
||||
|
||||
# item_use: col 28, mixed timing, one-shot
|
||||
_puny29_anims["item_use"] = AnimDef("item_use", [
|
||||
AnimFrame(28, 300),
|
||||
], loop=False, chain_to="idle")
|
||||
|
||||
PUNY_29 = SheetFormat(
|
||||
name="puny_29",
|
||||
tile_w=32, tile_h=32,
|
||||
columns=29, rows=8,
|
||||
directions=8,
|
||||
animations=_puny29_anims,
|
||||
grid_cell=(16, 16),
|
||||
render_offset=(-8, -16),
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 24-column free Puny Character format (768x256 @ 32x32)
|
||||
# =============================================================================
|
||||
_puny24_anims = {}
|
||||
|
||||
# Same layout but without dodge, item_use, and death has fewer frames
|
||||
_puny24_anims["idle"] = AnimDef("idle", [
|
||||
AnimFrame(0, 300), AnimFrame(1, 300),
|
||||
], loop=True)
|
||||
|
||||
_puny24_anims["walk"] = AnimDef("walk", [
|
||||
AnimFrame(1, 200), AnimFrame(2, 200), AnimFrame(3, 200), AnimFrame(4, 200),
|
||||
], loop=True)
|
||||
|
||||
_puny24_anims["slash"] = _make_anim("slash", 5, 4, 100, loop=False)
|
||||
_puny24_anims["bow"] = _make_anim("bow", 9, 4, 100, loop=False)
|
||||
|
||||
_puny24_anims["thrust"] = AnimDef("thrust", [
|
||||
AnimFrame(13, 100), AnimFrame(14, 100), AnimFrame(15, 100), AnimFrame(15, 100),
|
||||
], loop=False, chain_to="idle")
|
||||
|
||||
_puny24_anims["spellcast"] = AnimDef("spellcast", [
|
||||
AnimFrame(16, 100), AnimFrame(17, 100), AnimFrame(18, 100), AnimFrame(18, 100),
|
||||
], loop=False, chain_to="idle")
|
||||
|
||||
_puny24_anims["hurt"] = _make_anim("hurt", 19, 3, 100, loop=False)
|
||||
|
||||
_puny24_anims["death"] = AnimDef("death", [
|
||||
AnimFrame(22, 100), AnimFrame(23, 100),
|
||||
], loop=False, chain_to=None)
|
||||
|
||||
PUNY_24 = SheetFormat(
|
||||
name="puny_24",
|
||||
tile_w=32, tile_h=32,
|
||||
columns=24, rows=8,
|
||||
directions=8,
|
||||
animations=_puny24_anims,
|
||||
grid_cell=(16, 16),
|
||||
render_offset=(-8, -16),
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# RPG Maker creature format (288x192, 3x4 per character, 4 chars per sheet)
|
||||
# =============================================================================
|
||||
_creature_rpg_anims = {}
|
||||
|
||||
# Walk: 3 columns (left-step, stand, right-step), 200ms each, loop
|
||||
_creature_rpg_anims["idle"] = AnimDef("idle", [
|
||||
AnimFrame(1, 400),
|
||||
], loop=True)
|
||||
|
||||
_creature_rpg_anims["walk"] = AnimDef("walk", [
|
||||
AnimFrame(0, 200), AnimFrame(1, 200), AnimFrame(2, 200), AnimFrame(1, 200),
|
||||
], loop=True)
|
||||
|
||||
CREATURE_RPGMAKER = SheetFormat(
|
||||
name="creature_rpgmaker",
|
||||
tile_w=24, tile_h=24,
|
||||
columns=3, rows=4,
|
||||
directions=4,
|
||||
animations=_creature_rpg_anims,
|
||||
grid_cell=(16, 16),
|
||||
render_offset=(-4, -8),
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Slime format (480x32, 15x1, non-directional)
|
||||
# =============================================================================
|
||||
_slime_anims = {}
|
||||
|
||||
_slime_anims["idle"] = AnimDef("idle", [
|
||||
AnimFrame(0, 300), AnimFrame(1, 300),
|
||||
], loop=True)
|
||||
|
||||
_slime_anims["walk"] = AnimDef("walk", [
|
||||
AnimFrame(0, 200), AnimFrame(1, 200), AnimFrame(2, 200), AnimFrame(3, 200),
|
||||
], loop=True)
|
||||
|
||||
SLIME = SheetFormat(
|
||||
name="slime",
|
||||
tile_w=32, tile_h=32,
|
||||
columns=15, rows=1,
|
||||
directions=1,
|
||||
animations=_slime_anims,
|
||||
grid_cell=(16, 16),
|
||||
render_offset=(-8, -16),
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Format auto-detection
|
||||
# =============================================================================
|
||||
_FORMAT_TABLE = {
|
||||
(928, 256): PUNY_29,
|
||||
(768, 256): PUNY_24,
|
||||
(480, 32): SLIME,
|
||||
# RPG Maker sheets: 288x192 is 4 characters, each 72x192 / 3x4
|
||||
# Individual characters extracted: 72x96 (3 cols x 4 rows of 24x24)
|
||||
(72, 96): CREATURE_RPGMAKER,
|
||||
(288, 192): CREATURE_RPGMAKER, # Full sheet (need sub-region extraction)
|
||||
}
|
||||
|
||||
|
||||
def detect_format(width, height):
|
||||
"""Auto-detect sheet format from pixel dimensions.
|
||||
|
||||
Args:
|
||||
width: Image width in pixels
|
||||
height: Image height in pixels
|
||||
|
||||
Returns:
|
||||
SheetFormat or None if no match found
|
||||
"""
|
||||
return _FORMAT_TABLE.get((width, height))
|
||||
|
||||
|
||||
# All predefined formats for iteration
|
||||
ALL_FORMATS = [PUNY_29, PUNY_24, CREATURE_RPGMAKER, SLIME]
|
||||
Loading…
Add table
Add a link
Reference in a new issue