Compare commits
No commits in common. "a52568cc8dd3f0503c47d18f0ee2fdaee2deff0c" and "e2d3e5696889cf566210d3bb803f24231fa3104f" have entirely different histories.
a52568cc8d
...
e2d3e56968
14 changed files with 66 additions and 812 deletions
|
|
@ -639,10 +639,10 @@ build/
|
||||||
|
|
||||||
## Important Notes
|
## Important Notes
|
||||||
|
|
||||||
- The project uses SFML for graphics/audio (or SDL2 when building for wasm) and libtcod for roguelike utilities
|
- The project uses SFML for graphics/audio and libtcod for roguelike utilities
|
||||||
- Python scripts are loaded at runtime from the `scripts/` directory
|
- Python scripts are loaded at runtime from the `scripts/` directory
|
||||||
- Asset loading expects specific paths relative to the executable
|
- Asset loading expects specific paths relative to the executable
|
||||||
- The game was created for 7DRL 2023
|
- The game was created for 7DRL 2025 as "Crypt of Sokoban"
|
||||||
- Iterator implementations require careful handling of C++/Python boundaries
|
- Iterator implementations require careful handling of C++/Python boundaries
|
||||||
|
|
||||||
## Testing Guidelines
|
## Testing Guidelines
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ Scenes:
|
||||||
5 - Layer Compositing: demonstrates CharacterAssembler layered texture building
|
5 - Layer Compositing: demonstrates CharacterAssembler layered texture building
|
||||||
6 - Equipment Customizer: procedural + user-driven layer coloring for gear
|
6 - Equipment Customizer: procedural + user-driven layer coloring for gear
|
||||||
7 - Asset Inventory: browse discovered layer categories and files
|
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.
|
Controls shown on-screen per scene.
|
||||||
"""
|
"""
|
||||||
|
|
@ -91,7 +90,7 @@ if __name__ == "__main__":
|
||||||
sys.path.insert(0, parent_dir)
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
from shade_sprite import (
|
from shade_sprite import (
|
||||||
AnimatedSprite, Direction, PUNY_24, PUNY_29, SLIME, CREATURE_RPGMAKER,
|
AnimatedSprite, Direction, PUNY_24, SLIME,
|
||||||
CharacterAssembler,
|
CharacterAssembler,
|
||||||
AssetLibrary, FactionGenerator,
|
AssetLibrary, FactionGenerator,
|
||||||
)
|
)
|
||||||
|
|
@ -131,7 +130,7 @@ def _no_assets_fallback(scene, scene_name):
|
||||||
pos=(20, 60), fill_color=WARN_COLOR)
|
pos=(20, 60), fill_color=WARN_COLOR)
|
||||||
ui.append(msg)
|
ui.append(msg)
|
||||||
controls = mcrfpy.Caption(
|
controls = mcrfpy.Caption(
|
||||||
text="[1-8] Switch scenes",
|
text="[1-7] Switch scenes",
|
||||||
pos=(20, 740), fill_color=DIM_COLOR)
|
pos=(20, 740), fill_color=DIM_COLOR)
|
||||||
ui.append(controls)
|
ui.append(controls)
|
||||||
|
|
||||||
|
|
@ -153,7 +152,6 @@ def _handle_scene_switch(key):
|
||||||
mcrfpy.Key.NUM_5: "layers",
|
mcrfpy.Key.NUM_5: "layers",
|
||||||
mcrfpy.Key.NUM_6: "equip",
|
mcrfpy.Key.NUM_6: "equip",
|
||||||
mcrfpy.Key.NUM_7: "inventory",
|
mcrfpy.Key.NUM_7: "inventory",
|
||||||
mcrfpy.Key.NUM_8: "entity_anim",
|
|
||||||
}
|
}
|
||||||
name = scene_map.get(key)
|
name = scene_map.get(key)
|
||||||
if name:
|
if name:
|
||||||
|
|
@ -269,7 +267,7 @@ def _build_scene_viewer():
|
||||||
ui.append(anim_ref)
|
ui.append(anim_ref)
|
||||||
|
|
||||||
controls = mcrfpy.Caption(
|
controls = mcrfpy.Caption(
|
||||||
text="[Q/E] Sheet [A/D] Animation [W/S] Direction [1-8] Scenes",
|
text="[Q/E] Sheet [A/D] Animation [W/S] Direction [1-7] Scenes",
|
||||||
pos=(20, 740), fill_color=DIM_COLOR)
|
pos=(20, 740), fill_color=DIM_COLOR)
|
||||||
ui.append(controls)
|
ui.append(controls)
|
||||||
|
|
||||||
|
|
@ -417,7 +415,7 @@ def _build_scene_hsl():
|
||||||
ui.append(explain2)
|
ui.append(explain2)
|
||||||
|
|
||||||
controls = mcrfpy.Caption(
|
controls = mcrfpy.Caption(
|
||||||
text="[Left/Right] Hue +/-30 [Up/Down] Sat +/-0.1 [Z/X] Lit +/-0.1 [Q/E] Sheet [1-8] Scenes",
|
text="[Left/Right] Hue +/-30 [Up/Down] Sat +/-0.1 [Z/X] Lit +/-0.1 [Q/E] Sheet [1-7] Scenes",
|
||||||
pos=(20, 740), fill_color=DIM_COLOR)
|
pos=(20, 740), fill_color=DIM_COLOR)
|
||||||
ui.append(controls)
|
ui.append(controls)
|
||||||
|
|
||||||
|
|
@ -549,7 +547,7 @@ def _build_scene_gallery():
|
||||||
ui.append(dir_info)
|
ui.append(dir_info)
|
||||||
|
|
||||||
controls = mcrfpy.Caption(
|
controls = mcrfpy.Caption(
|
||||||
text="[W/S] Direction [A/D] Animation [1-8] Scenes",
|
text="[W/S] Direction [A/D] Animation [1-7] Scenes",
|
||||||
pos=(20, 740), fill_color=DIM_COLOR)
|
pos=(20, 740), fill_color=DIM_COLOR)
|
||||||
ui.append(controls)
|
ui.append(controls)
|
||||||
|
|
||||||
|
|
@ -661,7 +659,7 @@ def _build_scene_factions():
|
||||||
_populate()
|
_populate()
|
||||||
|
|
||||||
controls = mcrfpy.Caption(
|
controls = mcrfpy.Caption(
|
||||||
text="[Space] Re-roll factions [1-8] Scenes",
|
text="[Space] Re-roll factions [1-7] Scenes",
|
||||||
pos=(20, 740), fill_color=DIM_COLOR)
|
pos=(20, 740), fill_color=DIM_COLOR)
|
||||||
ui.append(controls)
|
ui.append(controls)
|
||||||
|
|
||||||
|
|
@ -842,7 +840,7 @@ def _build_scene_layers():
|
||||||
ui.append(code_lbl4)
|
ui.append(code_lbl4)
|
||||||
|
|
||||||
controls = mcrfpy.Caption(
|
controls = mcrfpy.Caption(
|
||||||
text="[Q/E] Overlay sheet [Left/Right] Overlay hue +/-30 [1-8] Scenes",
|
text="[Q/E] Overlay sheet [Left/Right] Overlay hue +/-30 [1-7] Scenes",
|
||||||
pos=(20, 740), fill_color=DIM_COLOR)
|
pos=(20, 740), fill_color=DIM_COLOR)
|
||||||
ui.append(controls)
|
ui.append(controls)
|
||||||
|
|
||||||
|
|
@ -1064,7 +1062,7 @@ def _build_scene_equip():
|
||||||
_generate_variants()
|
_generate_variants()
|
||||||
|
|
||||||
controls = mcrfpy.Caption(
|
controls = mcrfpy.Caption(
|
||||||
text="[Tab] Slot [Q/E] Sheet [Left/Right] Hue [Up/Down] Sat [Z/X] Lit [T] Toggle [R] Randomize [1-8] Scenes",
|
text="[Tab] Slot [Q/E] Sheet [Left/Right] Hue [Up/Down] Sat [Z/X] Lit [T] Toggle [R] Randomize [1-7] Scenes",
|
||||||
pos=(20, 740), fill_color=DIM_COLOR)
|
pos=(20, 740), fill_color=DIM_COLOR)
|
||||||
ui.append(controls)
|
ui.append(controls)
|
||||||
|
|
||||||
|
|
@ -1150,7 +1148,7 @@ def _build_scene_inventory():
|
||||||
text="The AssetLibrary scans the 'Individual Spritesheets' directory.",
|
text="The AssetLibrary scans the 'Individual Spritesheets' directory.",
|
||||||
pos=(20, 90), fill_color=DIM_COLOR)
|
pos=(20, 90), fill_color=DIM_COLOR)
|
||||||
ui.append(msg2)
|
ui.append(msg2)
|
||||||
controls = mcrfpy.Caption(text="[1-8] Switch scenes",
|
controls = mcrfpy.Caption(text="[1-7] Switch scenes",
|
||||||
pos=(20, 740), fill_color=DIM_COLOR)
|
pos=(20, 740), fill_color=DIM_COLOR)
|
||||||
ui.append(controls)
|
ui.append(controls)
|
||||||
|
|
||||||
|
|
@ -1277,7 +1275,7 @@ def _build_scene_inventory():
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
controls = mcrfpy.Caption(
|
controls = mcrfpy.Caption(
|
||||||
text="[W/S] Category [A/D] Subcategory [PgUp/PgDn] Scroll files [1-8] Scenes",
|
text="[W/S] Category [A/D] Subcategory [PgUp/PgDn] Scroll files [1-7] Scenes",
|
||||||
pos=(20, 740), fill_color=DIM_COLOR)
|
pos=(20, 740), fill_color=DIM_COLOR)
|
||||||
ui.append(controls)
|
ui.append(controls)
|
||||||
|
|
||||||
|
|
@ -1323,240 +1321,6 @@ def _build_scene_inventory():
|
||||||
return scene
|
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
|
# Main
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -1574,7 +1338,6 @@ def main():
|
||||||
_build_scene_layers()
|
_build_scene_layers()
|
||||||
_build_scene_equip()
|
_build_scene_equip()
|
||||||
_build_scene_inventory()
|
_build_scene_inventory()
|
||||||
_build_scene_entity_anim()
|
|
||||||
|
|
||||||
# Start animation timer (20fps animation updates)
|
# Start animation timer (20fps animation updates)
|
||||||
# Keep a reference so the Python cache lookup works and (timer, runtime) is passed
|
# Keep a reference so the Python cache lookup works and (timer, runtime) is passed
|
||||||
|
|
|
||||||
|
|
@ -1141,20 +1141,19 @@ PyObject* Entity3D::py_update_visibility(PyEntity3DObject* self, PyObject* args)
|
||||||
|
|
||||||
PyObject* Entity3D::py_animate(PyEntity3DObject* self, PyObject* args, PyObject* kwds)
|
PyObject* Entity3D::py_animate(PyEntity3DObject* self, PyObject* args, PyObject* kwds)
|
||||||
{
|
{
|
||||||
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "loop", "callback", "conflict_mode", nullptr};
|
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", "conflict_mode", nullptr};
|
||||||
|
|
||||||
const char* property_name;
|
const char* property_name;
|
||||||
PyObject* target_value;
|
PyObject* target_value;
|
||||||
float duration;
|
float duration;
|
||||||
PyObject* easing_arg = Py_None;
|
PyObject* easing_arg = Py_None;
|
||||||
int delta = 0;
|
int delta = 0;
|
||||||
int loop_val = 0;
|
|
||||||
PyObject* callback = nullptr;
|
PyObject* callback = nullptr;
|
||||||
const char* conflict_mode_str = nullptr;
|
const char* conflict_mode_str = nullptr;
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OppOs", const_cast<char**>(keywords),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpOs", const_cast<char**>(keywords),
|
||||||
&property_name, &target_value, &duration,
|
&property_name, &target_value, &duration,
|
||||||
&easing_arg, &delta, &loop_val, &callback, &conflict_mode_str)) {
|
&easing_arg, &delta, &callback, &conflict_mode_str)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1217,7 +1216,7 @@ PyObject* Entity3D::py_animate(PyEntity3DObject* self, PyObject* args, PyObject*
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the Animation
|
// Create the Animation
|
||||||
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, loop_val != 0, callback);
|
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
|
||||||
|
|
||||||
// Start on this entity (uses startEntity3D)
|
// Start on this entity (uses startEntity3D)
|
||||||
animation->startEntity3D(self->data);
|
animation->startEntity3D(self->data);
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,12 @@ Animation::Animation(const std::string& targetProperty,
|
||||||
float duration,
|
float duration,
|
||||||
EasingFunction easingFunc,
|
EasingFunction easingFunc,
|
||||||
bool delta,
|
bool delta,
|
||||||
bool loop,
|
|
||||||
PyObject* callback)
|
PyObject* callback)
|
||||||
: targetProperty(targetProperty)
|
: targetProperty(targetProperty)
|
||||||
, targetValue(targetValue)
|
, targetValue(targetValue)
|
||||||
, duration(duration)
|
, duration(duration)
|
||||||
, easingFunc(easingFunc)
|
, easingFunc(easingFunc)
|
||||||
, delta(delta)
|
, delta(delta)
|
||||||
, loop(loop)
|
|
||||||
, pythonCallback(callback)
|
, pythonCallback(callback)
|
||||||
{
|
{
|
||||||
// Increase reference count for Python callback
|
// Increase reference count for Python callback
|
||||||
|
|
@ -125,7 +123,7 @@ void Animation::start(std::shared_ptr<UIDrawable> target) {
|
||||||
|
|
||||||
// For zero-duration animations, apply final value immediately
|
// For zero-duration animations, apply final value immediately
|
||||||
if (duration <= 0.0f) {
|
if (duration <= 0.0f) {
|
||||||
AnimationValue finalValue = interpolate(easingFunc(1.0f));
|
AnimationValue finalValue = interpolate(1.0f);
|
||||||
applyValue(target.get(), finalValue);
|
applyValue(target.get(), finalValue);
|
||||||
if (pythonCallback && !callbackTriggered) {
|
if (pythonCallback && !callbackTriggered) {
|
||||||
triggerCallback();
|
triggerCallback();
|
||||||
|
|
@ -157,18 +155,12 @@ void Animation::startEntity(std::shared_ptr<UIEntity> target) {
|
||||||
startValue = target->sprite.getSpriteIndex();
|
startValue = target->sprite.getSpriteIndex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if constexpr (std::is_same_v<T, std::vector<int>>) {
|
|
||||||
// For sprite animation frame lists, get current sprite index
|
|
||||||
if (targetProperty == "sprite_index" || targetProperty == "sprite_number") {
|
|
||||||
startValue = target->sprite.getSpriteIndex();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Entities don't support other types yet
|
// Entities don't support other types yet
|
||||||
}, targetValue);
|
}, targetValue);
|
||||||
|
|
||||||
// For zero-duration animations, apply final value immediately
|
// For zero-duration animations, apply final value immediately
|
||||||
if (duration <= 0.0f) {
|
if (duration <= 0.0f) {
|
||||||
AnimationValue finalValue = interpolate(easingFunc(1.0f));
|
AnimationValue finalValue = interpolate(1.0f);
|
||||||
applyValue(target.get(), finalValue);
|
applyValue(target.get(), finalValue);
|
||||||
if (pythonCallback && !callbackTriggered) {
|
if (pythonCallback && !callbackTriggered) {
|
||||||
triggerCallback();
|
triggerCallback();
|
||||||
|
|
@ -206,7 +198,7 @@ void Animation::startEntity3D(std::shared_ptr<mcrf::Entity3D> target) {
|
||||||
|
|
||||||
// For zero-duration animations, apply final value immediately
|
// For zero-duration animations, apply final value immediately
|
||||||
if (duration <= 0.0f) {
|
if (duration <= 0.0f) {
|
||||||
AnimationValue finalValue = interpolate(easingFunc(1.0f));
|
AnimationValue finalValue = interpolate(1.0f);
|
||||||
applyValue(target.get(), finalValue);
|
applyValue(target.get(), finalValue);
|
||||||
if (pythonCallback && !callbackTriggered) {
|
if (pythonCallback && !callbackTriggered) {
|
||||||
triggerCallback();
|
triggerCallback();
|
||||||
|
|
@ -236,20 +228,17 @@ void Animation::complete() {
|
||||||
// Jump to end of animation
|
// Jump to end of animation
|
||||||
elapsed = duration;
|
elapsed = duration;
|
||||||
|
|
||||||
// Apply final value through easing function
|
// Apply final value
|
||||||
// For standard easings, easingFunc(1.0) = 1.0 (no change)
|
|
||||||
// For ping-pong easings, easingFunc(1.0) = 0.0 (returns to start value)
|
|
||||||
float finalT = easingFunc(1.0f);
|
|
||||||
if (auto target = targetWeak.lock()) {
|
if (auto target = targetWeak.lock()) {
|
||||||
AnimationValue finalValue = interpolate(finalT);
|
AnimationValue finalValue = interpolate(1.0f);
|
||||||
applyValue(target.get(), finalValue);
|
applyValue(target.get(), finalValue);
|
||||||
}
|
}
|
||||||
else if (auto entity = entityTargetWeak.lock()) {
|
else if (auto entity = entityTargetWeak.lock()) {
|
||||||
AnimationValue finalValue = interpolate(finalT);
|
AnimationValue finalValue = interpolate(1.0f);
|
||||||
applyValue(entity.get(), finalValue);
|
applyValue(entity.get(), finalValue);
|
||||||
}
|
}
|
||||||
else if (auto entity3d = entity3dTargetWeak.lock()) {
|
else if (auto entity3d = entity3dTargetWeak.lock()) {
|
||||||
AnimationValue finalValue = interpolate(finalT);
|
AnimationValue finalValue = interpolate(1.0f);
|
||||||
applyValue(entity3d.get(), finalValue);
|
applyValue(entity3d.get(), finalValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -280,9 +269,8 @@ bool Animation::update(float deltaTime) {
|
||||||
// Apply final value once before returning
|
// Apply final value once before returning
|
||||||
if (isComplete()) {
|
if (isComplete()) {
|
||||||
if (!callbackTriggered) {
|
if (!callbackTriggered) {
|
||||||
// Apply final value through easing function
|
// Apply final value for zero-duration animations
|
||||||
float finalT = easingFunc(1.0f);
|
AnimationValue finalValue = interpolate(1.0f);
|
||||||
AnimationValue finalValue = interpolate(finalT);
|
|
||||||
if (target) {
|
if (target) {
|
||||||
applyValue(target.get(), finalValue);
|
applyValue(target.get(), finalValue);
|
||||||
} else if (entity) {
|
} else if (entity) {
|
||||||
|
|
@ -300,11 +288,7 @@ bool Animation::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
elapsed += deltaTime;
|
elapsed += deltaTime;
|
||||||
if (loop && duration > 0.0f) {
|
|
||||||
while (elapsed >= duration) elapsed -= duration;
|
|
||||||
} else {
|
|
||||||
elapsed = std::min(elapsed, duration);
|
elapsed = std::min(elapsed, duration);
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate easing value (0.0 to 1.0)
|
// Calculate easing value (0.0 to 1.0)
|
||||||
float t = duration > 0 ? elapsed / duration : 1.0f;
|
float t = duration > 0 ? elapsed / duration : 1.0f;
|
||||||
|
|
@ -738,9 +722,8 @@ void Animation::triggerCallback() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final value (interpolated through easing function at t=1.0)
|
// Final value (interpolated at t=1.0)
|
||||||
// For ping-pong easings, this returns the start value (easingFunc(1.0) = 0.0)
|
PyObject* valueObj = animationValueToPython(interpolate(1.0f));
|
||||||
PyObject* valueObj = animationValueToPython(interpolate(easingFunc(1.0f)));
|
|
||||||
if (!valueObj) {
|
if (!valueObj) {
|
||||||
Py_DECREF(targetObj);
|
Py_DECREF(targetObj);
|
||||||
Py_DECREF(propertyObj);
|
Py_DECREF(propertyObj);
|
||||||
|
|
@ -973,38 +956,6 @@ float easeInOutBounce(float t) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping-pong easing functions (0 -> 1 -> 0)
|
|
||||||
// These are designed for looping animations where the value should
|
|
||||||
// smoothly return to the start position each cycle.
|
|
||||||
|
|
||||||
float pingPong(float t) {
|
|
||||||
// Linear triangle wave: 0 -> 1 -> 0
|
|
||||||
return 1.0f - std::fabs(2.0f * t - 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
float pingPongSmooth(float t) {
|
|
||||||
// Sine bell curve: smooth acceleration and deceleration
|
|
||||||
return std::sin(static_cast<float>(M_PI) * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
float pingPongEaseIn(float t) {
|
|
||||||
// Quadratic ease at rest positions (smooth departure/return, sharp peak)
|
|
||||||
float pp = 1.0f - std::fabs(2.0f * t - 1.0f);
|
|
||||||
return pp * pp;
|
|
||||||
}
|
|
||||||
|
|
||||||
float pingPongEaseOut(float t) {
|
|
||||||
// Ease-out at peak (sharp departure, smooth turnaround)
|
|
||||||
float pp = 1.0f - std::fabs(2.0f * t - 1.0f);
|
|
||||||
return pp * (2.0f - pp);
|
|
||||||
}
|
|
||||||
|
|
||||||
float pingPongEaseInOut(float t) {
|
|
||||||
// sin^2: smooth everywhere including at loop seam
|
|
||||||
float s = std::sin(static_cast<float>(M_PI) * t);
|
|
||||||
return s * s;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get easing function by name
|
// Get easing function by name
|
||||||
EasingFunction getByName(const std::string& name) {
|
EasingFunction getByName(const std::string& name) {
|
||||||
static std::unordered_map<std::string, EasingFunction> easingMap = {
|
static std::unordered_map<std::string, EasingFunction> easingMap = {
|
||||||
|
|
@ -1038,12 +989,7 @@ EasingFunction getByName(const std::string& name) {
|
||||||
{"easeInOutBack", easeInOutBack},
|
{"easeInOutBack", easeInOutBack},
|
||||||
{"easeInBounce", easeInBounce},
|
{"easeInBounce", easeInBounce},
|
||||||
{"easeOutBounce", easeOutBounce},
|
{"easeOutBounce", easeOutBounce},
|
||||||
{"easeInOutBounce", easeInOutBounce},
|
{"easeInOutBounce", easeInOutBounce}
|
||||||
{"pingPong", pingPong},
|
|
||||||
{"pingPongSmooth", pingPongSmooth},
|
|
||||||
{"pingPongEaseIn", pingPongEaseIn},
|
|
||||||
{"pingPongEaseOut", pingPongEaseOut},
|
|
||||||
{"pingPongEaseInOut", pingPongEaseInOut}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
auto it = easingMap.find(name);
|
auto it = easingMap.find(name);
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ public:
|
||||||
float duration,
|
float duration,
|
||||||
EasingFunction easingFunc = EasingFunctions::linear,
|
EasingFunction easingFunc = EasingFunctions::linear,
|
||||||
bool delta = false,
|
bool delta = false,
|
||||||
bool loop = false,
|
|
||||||
PyObject* callback = nullptr);
|
PyObject* callback = nullptr);
|
||||||
|
|
||||||
// Destructor - cleanup Python callback reference
|
// Destructor - cleanup Python callback reference
|
||||||
|
|
@ -87,10 +86,9 @@ public:
|
||||||
std::string getTargetProperty() const { return targetProperty; }
|
std::string getTargetProperty() const { return targetProperty; }
|
||||||
float getDuration() const { return duration; }
|
float getDuration() const { return duration; }
|
||||||
float getElapsed() const { return elapsed; }
|
float getElapsed() const { return elapsed; }
|
||||||
bool isComplete() const { return (!loop && elapsed >= duration) || stopped; }
|
bool isComplete() const { return elapsed >= duration || stopped; }
|
||||||
bool isStopped() const { return stopped; }
|
bool isStopped() const { return stopped; }
|
||||||
bool isDelta() const { return delta; }
|
bool isDelta() const { return delta; }
|
||||||
bool isLooping() const { return loop; }
|
|
||||||
|
|
||||||
// Get raw target pointer for property locking (#120)
|
// Get raw target pointer for property locking (#120)
|
||||||
void* getTargetPtr() const {
|
void* getTargetPtr() const {
|
||||||
|
|
@ -108,7 +106,6 @@ private:
|
||||||
float elapsed = 0.0f; // Elapsed time
|
float elapsed = 0.0f; // Elapsed time
|
||||||
EasingFunction easingFunc; // Easing function to use
|
EasingFunction easingFunc; // Easing function to use
|
||||||
bool delta; // If true, targetValue is relative to start
|
bool delta; // If true, targetValue is relative to start
|
||||||
bool loop; // If true, animation repeats from start when complete
|
|
||||||
bool stopped = false; // If true, animation was stopped without completing
|
bool stopped = false; // If true, animation was stopped without completing
|
||||||
|
|
||||||
// RAII: Use weak_ptr for safe target tracking
|
// RAII: Use weak_ptr for safe target tracking
|
||||||
|
|
@ -181,13 +178,6 @@ namespace EasingFunctions {
|
||||||
float easeOutBounce(float t);
|
float easeOutBounce(float t);
|
||||||
float easeInOutBounce(float t);
|
float easeInOutBounce(float t);
|
||||||
|
|
||||||
// Ping-pong easing functions (0 -> 1 -> 0, for looping animations)
|
|
||||||
float pingPong(float t);
|
|
||||||
float pingPongSmooth(float t);
|
|
||||||
float pingPongEaseIn(float t);
|
|
||||||
float pingPongEaseOut(float t);
|
|
||||||
float pingPongEaseInOut(float t);
|
|
||||||
|
|
||||||
// Get easing function by name
|
// Get easing function by name
|
||||||
EasingFunction getByName(const std::string& name);
|
EasingFunction getByName(const std::string& name);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,18 +20,17 @@ PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds
|
||||||
}
|
}
|
||||||
|
|
||||||
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
|
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
|
||||||
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "loop", "callback", nullptr};
|
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr};
|
||||||
|
|
||||||
const char* property_name;
|
const char* property_name;
|
||||||
PyObject* target_value;
|
PyObject* target_value;
|
||||||
float duration;
|
float duration;
|
||||||
PyObject* easing_arg = Py_None;
|
PyObject* easing_arg = Py_None;
|
||||||
int delta = 0;
|
int delta = 0;
|
||||||
int loop_val = 0;
|
|
||||||
PyObject* callback = nullptr;
|
PyObject* callback = nullptr;
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OppO", const_cast<char**>(keywords),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpO", const_cast<char**>(keywords),
|
||||||
&property_name, &target_value, &duration, &easing_arg, &delta, &loop_val, &callback)) {
|
&property_name, &target_value, &duration, &easing_arg, &delta, &callback)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,7 +107,7 @@ int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the Animation
|
// Create the Animation
|
||||||
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, loop_val != 0, callback);
|
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -180,10 +179,6 @@ PyObject* PyAnimation::get_is_delta(PyAnimationObject* self, void* closure) {
|
||||||
return PyBool_FromLong(self->data->isDelta());
|
return PyBool_FromLong(self->data->isDelta());
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* PyAnimation::get_is_looping(PyAnimationObject* self, void* closure) {
|
|
||||||
return PyBool_FromLong(self->data->isLooping());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to convert Python string to AnimationConflictMode
|
// Helper to convert Python string to AnimationConflictMode
|
||||||
static bool parseConflictMode(const char* mode_str, AnimationConflictMode& mode) {
|
static bool parseConflictMode(const char* mode_str, AnimationConflictMode& mode) {
|
||||||
if (!mode_str || strcmp(mode_str, "replace") == 0) {
|
if (!mode_str || strcmp(mode_str, "replace") == 0) {
|
||||||
|
|
@ -361,8 +356,6 @@ PyGetSetDef PyAnimation::getsetters[] = {
|
||||||
MCRF_PROPERTY(is_complete, "Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called."), NULL},
|
MCRF_PROPERTY(is_complete, "Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called."), NULL},
|
||||||
{"is_delta", (getter)get_is_delta, NULL,
|
{"is_delta", (getter)get_is_delta, NULL,
|
||||||
MCRF_PROPERTY(is_delta, "Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value."), NULL},
|
MCRF_PROPERTY(is_delta, "Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value."), NULL},
|
||||||
{"is_looping", (getter)get_is_looping, NULL,
|
|
||||||
MCRF_PROPERTY(is_looping, "Whether animation loops (bool, read-only). Looping animations repeat from the start when they reach the end."), NULL},
|
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ public:
|
||||||
static PyObject* get_elapsed(PyAnimationObject* self, void* closure);
|
static PyObject* get_elapsed(PyAnimationObject* self, void* closure);
|
||||||
static PyObject* get_is_complete(PyAnimationObject* self, void* closure);
|
static PyObject* get_is_complete(PyAnimationObject* self, void* closure);
|
||||||
static PyObject* get_is_delta(PyAnimationObject* self, void* closure);
|
static PyObject* get_is_delta(PyAnimationObject* self, void* closure);
|
||||||
static PyObject* get_is_looping(PyAnimationObject* self, void* closure);
|
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
static PyObject* start(PyAnimationObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* start(PyAnimationObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
@ -48,7 +47,7 @@ namespace mcrfpydef {
|
||||||
.tp_repr = (reprfunc)PyAnimation::repr,
|
.tp_repr = (reprfunc)PyAnimation::repr,
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
.tp_doc = PyDoc_STR(
|
.tp_doc = PyDoc_STR(
|
||||||
"Animation(property: str, target: Any, duration: float, easing: str = 'linear', delta: bool = False, loop: bool = False, callback: Callable = None)\n"
|
"Animation(property: str, target: Any, duration: float, easing: str = 'linear', delta: bool = False, callback: Callable = None)\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Create an animation that interpolates a property value over time.\n"
|
"Create an animation that interpolates a property value over time.\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|
@ -81,18 +80,22 @@ namespace mcrfpydef {
|
||||||
" - 'easeInBack', 'easeOutBack', 'easeInOutBack'\n"
|
" - 'easeInBack', 'easeOutBack', 'easeInOutBack'\n"
|
||||||
" - 'easeInBounce', 'easeOutBounce', 'easeInOutBounce'\n"
|
" - 'easeInBounce', 'easeOutBounce', 'easeInOutBounce'\n"
|
||||||
" delta: If True, target is relative to start value (additive). Default False.\n"
|
" delta: If True, target is relative to start value (additive). Default False.\n"
|
||||||
" loop: If True, animation repeats from start when it reaches the end. Default False.\n"
|
" callback: Function(animation, target) called when animation completes.\n"
|
||||||
" callback: Function(target, property, value) called when animation completes.\n"
|
|
||||||
" Not called for looping animations (since they never complete).\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"Example:\n"
|
"Example:\n"
|
||||||
" # Move a frame from current position to x=500 over 2 seconds\n"
|
" # Move a frame from current position to x=500 over 2 seconds\n"
|
||||||
" anim = mcrfpy.Animation('x', 500.0, 2.0, 'easeInOut')\n"
|
" anim = mcrfpy.Animation('x', 500.0, 2.0, 'easeInOut')\n"
|
||||||
" anim.start(my_frame)\n"
|
" anim.start(my_frame)\n"
|
||||||
"\n"
|
"\n"
|
||||||
" # Looping sprite animation\n"
|
" # Fade out with callback\n"
|
||||||
" walk = mcrfpy.Animation('sprite_index', [0,1,2,3,2,1], 0.6, loop=True)\n"
|
" def on_done(anim, target):\n"
|
||||||
" walk.start(my_sprite)\n"
|
" print('Animation complete!')\n"
|
||||||
|
" fade = mcrfpy.Animation('fill_color.a', 0, 1.0, callback=on_done)\n"
|
||||||
|
" fade.start(my_sprite)\n"
|
||||||
|
"\n"
|
||||||
|
" # Animate through sprite frames\n"
|
||||||
|
" walk_cycle = mcrfpy.Animation('sprite_index', [0,1,2,3,2,1], 0.5, 'linear')\n"
|
||||||
|
" walk_cycle.start(my_entity)\n"
|
||||||
),
|
),
|
||||||
.tp_methods = PyAnimation::methods,
|
.tp_methods = PyAnimation::methods,
|
||||||
.tp_getset = PyAnimation::getsetters,
|
.tp_getset = PyAnimation::getsetters,
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,6 @@ static const EasingEntry easing_table[] = {
|
||||||
{"EASE_IN_BOUNCE", 28, EasingFunctions::easeInBounce},
|
{"EASE_IN_BOUNCE", 28, EasingFunctions::easeInBounce},
|
||||||
{"EASE_OUT_BOUNCE", 29, EasingFunctions::easeOutBounce},
|
{"EASE_OUT_BOUNCE", 29, EasingFunctions::easeOutBounce},
|
||||||
{"EASE_IN_OUT_BOUNCE", 30, EasingFunctions::easeInOutBounce},
|
{"EASE_IN_OUT_BOUNCE", 30, EasingFunctions::easeInOutBounce},
|
||||||
{"PING_PONG", 31, EasingFunctions::pingPong},
|
|
||||||
{"PING_PONG_SMOOTH", 32, EasingFunctions::pingPongSmooth},
|
|
||||||
{"PING_PONG_EASE_IN", 33, EasingFunctions::pingPongEaseIn},
|
|
||||||
{"PING_PONG_EASE_OUT", 34, EasingFunctions::pingPongEaseOut},
|
|
||||||
{"PING_PONG_EASE_IN_OUT", 35, EasingFunctions::pingPongEaseInOut},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Old string names (for backwards compatibility)
|
// Old string names (for backwards compatibility)
|
||||||
|
|
@ -61,9 +56,7 @@ static const char* legacy_names[] = {
|
||||||
"easeInCirc", "easeOutCirc", "easeInOutCirc",
|
"easeInCirc", "easeOutCirc", "easeInOutCirc",
|
||||||
"easeInElastic", "easeOutElastic", "easeInOutElastic",
|
"easeInElastic", "easeOutElastic", "easeInOutElastic",
|
||||||
"easeInBack", "easeOutBack", "easeInOutBack",
|
"easeInBack", "easeOutBack", "easeInOutBack",
|
||||||
"easeInBounce", "easeOutBounce", "easeInOutBounce",
|
"easeInBounce", "easeOutBounce", "easeInOutBounce"
|
||||||
"pingPong", "pingPongSmooth", "pingPongEaseIn",
|
|
||||||
"pingPongEaseOut", "pingPongEaseInOut"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static const int NUM_EASING_ENTRIES = sizeof(easing_table) / sizeof(easing_table[0]);
|
static const int NUM_EASING_ENTRIES = sizeof(easing_table) / sizeof(easing_table[0]);
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds)
|
||||||
UIDRAWABLE_METHODS_BASE, \
|
UIDRAWABLE_METHODS_BASE, \
|
||||||
{"animate", (PyCFunction)UIDrawable_animate<PyObjectType>, METH_VARARGS | METH_KEYWORDS, \
|
{"animate", (PyCFunction)UIDrawable_animate<PyObjectType>, METH_VARARGS | METH_KEYWORDS, \
|
||||||
MCRF_METHOD(Drawable, animate, \
|
MCRF_METHOD(Drawable, animate, \
|
||||||
MCRF_SIG("(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace')", "Animation"), \
|
MCRF_SIG("(property: str, target: Any, duration: float, easing=None, delta=False, callback=None, conflict_mode='replace')", "Animation"), \
|
||||||
MCRF_DESC("Create and start an animation on this drawable's property."), \
|
MCRF_DESC("Create and start an animation on this drawable's property."), \
|
||||||
MCRF_ARGS_START \
|
MCRF_ARGS_START \
|
||||||
MCRF_ARG("property", "Name of the property to animate (e.g., 'x', 'fill_color', 'opacity')") \
|
MCRF_ARG("property", "Name of the property to animate (e.g., 'x', 'fill_color', 'opacity')") \
|
||||||
|
|
@ -118,8 +118,7 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds)
|
||||||
MCRF_ARG("duration", "Animation duration in seconds") \
|
MCRF_ARG("duration", "Animation duration in seconds") \
|
||||||
MCRF_ARG("easing", "Easing function: Easing enum value, string name, or None for linear") \
|
MCRF_ARG("easing", "Easing function: Easing enum value, string name, or None for linear") \
|
||||||
MCRF_ARG("delta", "If True, target is relative to current value; if False, target is absolute") \
|
MCRF_ARG("delta", "If True, target is relative to current value; if False, target is absolute") \
|
||||||
MCRF_ARG("loop", "If True, animation repeats from start when it reaches the end (default False)") \
|
MCRF_ARG("callback", "Optional callable invoked when animation completes") \
|
||||||
MCRF_ARG("callback", "Optional callable invoked when animation completes (not called for looping animations)") \
|
|
||||||
MCRF_ARG("conflict_mode", "'replace' (default), 'queue', or 'error' if property already animating") \
|
MCRF_ARG("conflict_mode", "'replace' (default), 'queue', or 'error' if property already animating") \
|
||||||
MCRF_RETURNS("Animation object for monitoring progress") \
|
MCRF_RETURNS("Animation object for monitoring progress") \
|
||||||
MCRF_RAISES("ValueError", "If property name is not valid for this drawable type") \
|
MCRF_RAISES("ValueError", "If property name is not valid for this drawable type") \
|
||||||
|
|
|
||||||
|
|
@ -1845,20 +1845,19 @@ int UIDrawable::set_on_move(PyObject* self, PyObject* value, void* closure) {
|
||||||
// Animation shorthand helper - creates and starts an animation on a UIDrawable
|
// Animation shorthand helper - creates and starts an animation on a UIDrawable
|
||||||
// This is a free function (not a member) to avoid incomplete type issues in UIBase.h template
|
// This is a free function (not a member) to avoid incomplete type issues in UIBase.h template
|
||||||
PyObject* UIDrawable_animate_impl(std::shared_ptr<UIDrawable> self, PyObject* args, PyObject* kwds) {
|
PyObject* UIDrawable_animate_impl(std::shared_ptr<UIDrawable> self, PyObject* args, PyObject* kwds) {
|
||||||
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "loop", "callback", "conflict_mode", nullptr};
|
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", "conflict_mode", nullptr};
|
||||||
|
|
||||||
const char* property_name;
|
const char* property_name;
|
||||||
PyObject* target_value;
|
PyObject* target_value;
|
||||||
float duration;
|
float duration;
|
||||||
PyObject* easing_arg = Py_None;
|
PyObject* easing_arg = Py_None;
|
||||||
int delta = 0;
|
int delta = 0;
|
||||||
int loop_val = 0;
|
|
||||||
PyObject* callback = nullptr;
|
PyObject* callback = nullptr;
|
||||||
const char* conflict_mode_str = nullptr;
|
const char* conflict_mode_str = nullptr;
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OppOs", const_cast<char**>(keywords),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpOs", const_cast<char**>(keywords),
|
||||||
&property_name, &target_value, &duration,
|
&property_name, &target_value, &duration,
|
||||||
&easing_arg, &delta, &loop_val, &callback, &conflict_mode_str)) {
|
&easing_arg, &delta, &callback, &conflict_mode_str)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1962,7 +1961,7 @@ PyObject* UIDrawable_animate_impl(std::shared_ptr<UIDrawable> self, PyObject* ar
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the Animation
|
// Create the Animation
|
||||||
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, loop_val != 0, callback);
|
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
|
||||||
|
|
||||||
// Start on this drawable
|
// Start on this drawable
|
||||||
animation->start(self);
|
animation->start(self);
|
||||||
|
|
|
||||||
|
|
@ -945,21 +945,19 @@ PyMethodDef UIEntity_all_methods[] = {
|
||||||
UIDRAWABLE_METHODS_BASE,
|
UIDRAWABLE_METHODS_BASE,
|
||||||
{"animate", (PyCFunction)UIEntity::animate, METH_VARARGS | METH_KEYWORDS,
|
{"animate", (PyCFunction)UIEntity::animate, METH_VARARGS | METH_KEYWORDS,
|
||||||
MCRF_METHOD(Entity, animate,
|
MCRF_METHOD(Entity, animate,
|
||||||
MCRF_SIG("(property: str, target: Any, duration: float, easing=None, delta=False, loop=False, callback=None, conflict_mode='replace')", "Animation"),
|
MCRF_SIG("(property: str, target: Any, duration: float, easing=None, delta=False, callback=None, conflict_mode='replace')", "Animation"),
|
||||||
MCRF_DESC("Create and start an animation on this entity's property."),
|
MCRF_DESC("Create and start an animation on this entity's property."),
|
||||||
MCRF_ARGS_START
|
MCRF_ARGS_START
|
||||||
MCRF_ARG("property", "Name of the property to animate: 'draw_x', 'draw_y' (tile coords), 'sprite_scale', 'sprite_index'")
|
MCRF_ARG("property", "Name of the property to animate: 'draw_x', 'draw_y' (tile coords), 'sprite_scale', 'sprite_index'")
|
||||||
MCRF_ARG("target", "Target value - float, int, or list of int (for sprite frame sequences)")
|
MCRF_ARG("target", "Target value - float or int depending on property")
|
||||||
MCRF_ARG("duration", "Animation duration in seconds")
|
MCRF_ARG("duration", "Animation duration in seconds")
|
||||||
MCRF_ARG("easing", "Easing function: Easing enum value, string name, or None for linear")
|
MCRF_ARG("easing", "Easing function: Easing enum value, string name, or None for linear")
|
||||||
MCRF_ARG("delta", "If True, target is relative to current value; if False, target is absolute")
|
MCRF_ARG("delta", "If True, target is relative to current value; if False, target is absolute")
|
||||||
MCRF_ARG("loop", "If True, animation repeats from start when it reaches the end (default False)")
|
MCRF_ARG("callback", "Optional callable invoked when animation completes")
|
||||||
MCRF_ARG("callback", "Optional callable invoked when animation completes (not called for looping animations)")
|
|
||||||
MCRF_ARG("conflict_mode", "'replace' (default), 'queue', or 'error' if property already animating")
|
MCRF_ARG("conflict_mode", "'replace' (default), 'queue', or 'error' if property already animating")
|
||||||
MCRF_RETURNS("Animation object for monitoring progress")
|
MCRF_RETURNS("Animation object for monitoring progress")
|
||||||
MCRF_RAISES("ValueError", "If property name is not valid for Entity (draw_x, draw_y, sprite_scale, sprite_index)")
|
MCRF_RAISES("ValueError", "If property name is not valid for Entity (draw_x, draw_y, sprite_scale, sprite_index)")
|
||||||
MCRF_NOTE("Use 'draw_x'/'draw_y' to animate tile coordinates for smooth movement between grid cells. "
|
MCRF_NOTE("Use 'draw_x'/'draw_y' to animate tile coordinates for smooth movement between grid cells.")
|
||||||
"Use list target with loop=True for repeating sprite frame animations.")
|
|
||||||
)},
|
)},
|
||||||
{"at", (PyCFunction)UIEntity::at, METH_VARARGS | METH_KEYWORDS,
|
{"at", (PyCFunction)UIEntity::at, METH_VARARGS | METH_KEYWORDS,
|
||||||
"at(x, y) or at(pos) -> GridPointState\n\n"
|
"at(x, y) or at(pos) -> GridPointState\n\n"
|
||||||
|
|
@ -1138,20 +1136,19 @@ bool UIEntity::hasProperty(const std::string& name) const {
|
||||||
|
|
||||||
// Animation shorthand for Entity - creates and starts an animation
|
// Animation shorthand for Entity - creates and starts an animation
|
||||||
PyObject* UIEntity::animate(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
PyObject* UIEntity::animate(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||||
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "loop", "callback", "conflict_mode", nullptr};
|
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", "conflict_mode", nullptr};
|
||||||
|
|
||||||
const char* property_name;
|
const char* property_name;
|
||||||
PyObject* target_value;
|
PyObject* target_value;
|
||||||
float duration;
|
float duration;
|
||||||
PyObject* easing_arg = Py_None;
|
PyObject* easing_arg = Py_None;
|
||||||
int delta = 0;
|
int delta = 0;
|
||||||
int loop_val = 0;
|
|
||||||
PyObject* callback = nullptr;
|
PyObject* callback = nullptr;
|
||||||
const char* conflict_mode_str = nullptr;
|
const char* conflict_mode_str = nullptr;
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OppOs", const_cast<char**>(keywords),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpOs", const_cast<char**>(keywords),
|
||||||
&property_name, &target_value, &duration,
|
&property_name, &target_value, &duration,
|
||||||
&easing_arg, &delta, &loop_val, &callback, &conflict_mode_str)) {
|
&easing_arg, &delta, &callback, &conflict_mode_str)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1176,7 +1173,7 @@ PyObject* UIEntity::animate(PyUIEntityObject* self, PyObject* args, PyObject* kw
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert Python target value to AnimationValue
|
// Convert Python target value to AnimationValue
|
||||||
// Entity supports float, int, and list of int (for sprite frame animation)
|
// Entity only supports float and int properties
|
||||||
AnimationValue animValue;
|
AnimationValue animValue;
|
||||||
|
|
||||||
if (PyFloat_Check(target_value)) {
|
if (PyFloat_Check(target_value)) {
|
||||||
|
|
@ -1185,23 +1182,8 @@ PyObject* UIEntity::animate(PyUIEntityObject* self, PyObject* args, PyObject* kw
|
||||||
else if (PyLong_Check(target_value)) {
|
else if (PyLong_Check(target_value)) {
|
||||||
animValue = static_cast<int>(PyLong_AsLong(target_value));
|
animValue = static_cast<int>(PyLong_AsLong(target_value));
|
||||||
}
|
}
|
||||||
else if (PyList_Check(target_value)) {
|
|
||||||
// List of integers for sprite animation
|
|
||||||
std::vector<int> indices;
|
|
||||||
Py_ssize_t size = PyList_Size(target_value);
|
|
||||||
for (Py_ssize_t i = 0; i < size; i++) {
|
|
||||||
PyObject* item = PyList_GetItem(target_value, i);
|
|
||||||
if (PyLong_Check(item)) {
|
|
||||||
indices.push_back(PyLong_AsLong(item));
|
|
||||||
} else {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "Sprite animation list must contain only integers");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
animValue = indices;
|
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
PyErr_SetString(PyExc_TypeError, "Entity animations support float, int, or list of int target values");
|
PyErr_SetString(PyExc_TypeError, "Entity animations only support float or int target values");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1228,7 +1210,7 @@ PyObject* UIEntity::animate(PyUIEntityObject* self, PyObject* args, PyObject* kw
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the Animation
|
// Create the Animation
|
||||||
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, loop_val != 0, callback);
|
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
|
||||||
|
|
||||||
// Start on this entity (uses startEntity, not start)
|
// Start on this entity (uses startEntity, not start)
|
||||||
animation->startEntity(self->data);
|
animation->startEntity(self->data);
|
||||||
|
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
"""Test Animation loop parameter.
|
|
||||||
|
|
||||||
Verifies that loop=True causes animations to cycle instead of completing.
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import sys
|
|
||||||
|
|
||||||
PASS = True
|
|
||||||
def check(name, condition):
|
|
||||||
global PASS
|
|
||||||
if not condition:
|
|
||||||
print(f"FAIL: {name}")
|
|
||||||
PASS = False
|
|
||||||
else:
|
|
||||||
print(f" ok: {name}")
|
|
||||||
|
|
||||||
|
|
||||||
# --- Setup ---
|
|
||||||
scene = mcrfpy.Scene("test")
|
|
||||||
mcrfpy.current_scene = scene
|
|
||||||
|
|
||||||
# --- Test 1: Default loop=False, animation completes ---
|
|
||||||
sprite = mcrfpy.Sprite(pos=(0, 0))
|
|
||||||
scene.children.append(sprite)
|
|
||||||
anim = sprite.animate("x", 100.0, 1.0)
|
|
||||||
check("default loop is False", anim.is_looping == False)
|
|
||||||
|
|
||||||
# Step past duration
|
|
||||||
for _ in range(15):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
check("non-loop animation completes", anim.is_complete == True)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 2: loop=True, animation does NOT complete ---
|
|
||||||
sprite2 = mcrfpy.Sprite(pos=(0, 0))
|
|
||||||
scene.children.append(sprite2)
|
|
||||||
anim2 = sprite2.animate("x", 100.0, 0.5, loop=True)
|
|
||||||
check("loop=True sets is_looping", anim2.is_looping == True)
|
|
||||||
|
|
||||||
# Step well past duration
|
|
||||||
for _ in range(20):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
check("looping animation never completes", anim2.is_complete == False)
|
|
||||||
check("looping animation has valid target", anim2.hasValidTarget() == True)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 3: Sprite frame list with loop ---
|
|
||||||
sprite3 = mcrfpy.Sprite(pos=(0, 0))
|
|
||||||
scene.children.append(sprite3)
|
|
||||||
anim3 = sprite3.animate("sprite_index", [0, 1, 2, 3], 0.4, loop=True)
|
|
||||||
check("frame list loop is_looping", anim3.is_looping == True)
|
|
||||||
|
|
||||||
# Step through multiple cycles
|
|
||||||
for _ in range(20):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
check("frame list loop doesn't complete", anim3.is_complete == False)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 4: Loop animation can be stopped ---
|
|
||||||
sprite4 = mcrfpy.Sprite(pos=(0, 0))
|
|
||||||
scene.children.append(sprite4)
|
|
||||||
anim4 = sprite4.animate("x", 200.0, 0.5, loop=True)
|
|
||||||
|
|
||||||
for _ in range(10):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
check("loop animation running before stop", anim4.is_complete == False)
|
|
||||||
anim4.stop()
|
|
||||||
check("loop animation stopped", anim4.is_complete == True)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 5: Loop animation can be replaced ---
|
|
||||||
sprite5 = mcrfpy.Sprite(pos=(0, 0))
|
|
||||||
scene.children.append(sprite5)
|
|
||||||
anim5a = sprite5.animate("x", 100.0, 0.5, loop=True)
|
|
||||||
|
|
||||||
for _ in range(5):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
# Replace with non-looping
|
|
||||||
anim5b = sprite5.animate("x", 200.0, 0.5)
|
|
||||||
check("replacement anim is not looping", anim5b.is_looping == False)
|
|
||||||
|
|
||||||
for _ in range(10):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
check("replacement anim completes", anim5b.is_complete == True)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 6: Animation object created with loop=True via constructor ---
|
|
||||||
anim6 = mcrfpy.Animation("x", 100.0, 1.0, loop=True)
|
|
||||||
check("Animation constructor loop=True", anim6.is_looping == True)
|
|
||||||
|
|
||||||
anim7 = mcrfpy.Animation("x", 100.0, 1.0)
|
|
||||||
check("Animation constructor default loop=False", anim7.is_looping == False)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Summary ---
|
|
||||||
if PASS:
|
|
||||||
print("PASS")
|
|
||||||
sys.exit(0)
|
|
||||||
else:
|
|
||||||
print("FAIL")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
"""Test Entity.animate() with list of int frame indices.
|
|
||||||
|
|
||||||
Verifies that Entity supports sprite frame list animation,
|
|
||||||
including with loop=True.
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import sys
|
|
||||||
|
|
||||||
PASS = True
|
|
||||||
def check(name, condition):
|
|
||||||
global PASS
|
|
||||||
if not condition:
|
|
||||||
print(f"FAIL: {name}")
|
|
||||||
PASS = False
|
|
||||||
else:
|
|
||||||
print(f" ok: {name}")
|
|
||||||
|
|
||||||
|
|
||||||
# --- Setup ---
|
|
||||||
scene = mcrfpy.Scene("test")
|
|
||||||
mcrfpy.current_scene = scene
|
|
||||||
|
|
||||||
# Create a grid with an entity
|
|
||||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
|
||||||
grid = mcrfpy.Grid(grid_size=(10, 10), texture=tex, pos=(0, 0), size=(320, 320))
|
|
||||||
scene.children.append(grid)
|
|
||||||
|
|
||||||
entity = mcrfpy.Entity(grid_pos=(1, 1), texture=tex, sprite_index=0)
|
|
||||||
grid.entities.append(entity)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 1: Entity.animate with list target ---
|
|
||||||
anim = entity.animate("sprite_index", [0, 1, 2, 3], 0.4)
|
|
||||||
check("entity animate with list returns Animation", anim is not None)
|
|
||||||
check("entity frame list anim has valid target", anim.hasValidTarget() == True)
|
|
||||||
|
|
||||||
# Step to complete
|
|
||||||
for _ in range(10):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
check("entity frame list anim completes", anim.is_complete == True)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 2: Entity.animate with list + loop=True ---
|
|
||||||
entity2 = mcrfpy.Entity(grid_pos=(2, 2), texture=tex, sprite_index=0)
|
|
||||||
grid.entities.append(entity2)
|
|
||||||
|
|
||||||
anim2 = entity2.animate("sprite_index", [10, 11, 12, 13], 0.4, loop=True)
|
|
||||||
check("entity loop frame list is_looping", anim2.is_looping == True)
|
|
||||||
|
|
||||||
# Step well past duration
|
|
||||||
for _ in range(20):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
check("entity loop frame list doesn't complete", anim2.is_complete == False)
|
|
||||||
|
|
||||||
# The sprite_index should be one of the frame values
|
|
||||||
idx = entity2.sprite_index
|
|
||||||
check(f"entity sprite_index is valid frame ({idx})", idx in [10, 11, 12, 13])
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 3: Invalid list items raise TypeError ---
|
|
||||||
try:
|
|
||||||
entity.animate("sprite_index", [1, 2, "bad", 4], 0.5)
|
|
||||||
check("invalid list item raises TypeError", False)
|
|
||||||
except TypeError:
|
|
||||||
check("invalid list item raises TypeError", True)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 4: Entity.animate with int still works (no regression) ---
|
|
||||||
entity3 = mcrfpy.Entity(grid_pos=(3, 3), texture=tex, sprite_index=0)
|
|
||||||
grid.entities.append(entity3)
|
|
||||||
|
|
||||||
anim3 = entity3.animate("sprite_index", 5, 0.2)
|
|
||||||
check("entity animate with int still works", anim3 is not None)
|
|
||||||
|
|
||||||
for _ in range(5):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
check("entity int anim completes", anim3.is_complete == True)
|
|
||||||
check("entity sprite_index set to target", entity3.sprite_index == 5)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 5: Entity.animate with float still works (no regression) ---
|
|
||||||
entity4 = mcrfpy.Entity(grid_pos=(4, 4), texture=tex, sprite_index=0)
|
|
||||||
grid.entities.append(entity4)
|
|
||||||
|
|
||||||
anim4 = entity4.animate("draw_x", 5.0, 0.3)
|
|
||||||
check("entity animate with float still works", anim4 is not None)
|
|
||||||
|
|
||||||
for _ in range(10):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
check("entity float anim completes", anim4.is_complete == True)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Summary ---
|
|
||||||
if PASS:
|
|
||||||
print("PASS")
|
|
||||||
sys.exit(0)
|
|
||||||
else:
|
|
||||||
print("FAIL")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
"""Test ping-pong easing functions.
|
|
||||||
|
|
||||||
Verifies that ping-pong easings oscillate (0 -> 1 -> 0) and that
|
|
||||||
complete()/stop() on ping-pong animations returns to the start value.
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import sys
|
|
||||||
|
|
||||||
PASS = True
|
|
||||||
def check(name, condition):
|
|
||||||
global PASS
|
|
||||||
if not condition:
|
|
||||||
print(f"FAIL: {name}")
|
|
||||||
PASS = False
|
|
||||||
else:
|
|
||||||
print(f" ok: {name}")
|
|
||||||
|
|
||||||
|
|
||||||
# --- Setup ---
|
|
||||||
scene = mcrfpy.Scene("test")
|
|
||||||
mcrfpy.current_scene = scene
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 1: Ping-pong easing enum members exist ---
|
|
||||||
check("PING_PONG exists", hasattr(mcrfpy.Easing, "PING_PONG"))
|
|
||||||
check("PING_PONG_SMOOTH exists", hasattr(mcrfpy.Easing, "PING_PONG_SMOOTH"))
|
|
||||||
check("PING_PONG_EASE_IN exists", hasattr(mcrfpy.Easing, "PING_PONG_EASE_IN"))
|
|
||||||
check("PING_PONG_EASE_OUT exists", hasattr(mcrfpy.Easing, "PING_PONG_EASE_OUT"))
|
|
||||||
check("PING_PONG_EASE_IN_OUT exists", hasattr(mcrfpy.Easing, "PING_PONG_EASE_IN_OUT"))
|
|
||||||
|
|
||||||
# Check enum values are sequential from 31
|
|
||||||
check("PING_PONG value is 31", int(mcrfpy.Easing.PING_PONG) == 31)
|
|
||||||
check("PING_PONG_EASE_IN_OUT value is 35", int(mcrfpy.Easing.PING_PONG_EASE_IN_OUT) == 35)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 2: Ping-pong animation reaches midpoint then returns ---
|
|
||||||
sprite = mcrfpy.Sprite(pos=(0, 0))
|
|
||||||
scene.children.append(sprite)
|
|
||||||
anim = sprite.animate("x", 100.0, 1.0, mcrfpy.Easing.PING_PONG)
|
|
||||||
check("ping-pong anim created", anim is not None)
|
|
||||||
|
|
||||||
# Step to midpoint (t=0.5)
|
|
||||||
for _ in range(5):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
midpoint_x = sprite.x
|
|
||||||
check(f"at midpoint x ({midpoint_x:.1f}) is near 100", midpoint_x > 80.0)
|
|
||||||
|
|
||||||
# Step to end (t=1.0)
|
|
||||||
for _ in range(5):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
final_x = sprite.x
|
|
||||||
check(f"at end x ({final_x:.1f}) returns near 0", final_x < 20.0)
|
|
||||||
check("ping-pong anim completes", anim.is_complete == True)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 3: Ping-pong smooth animation oscillates ---
|
|
||||||
sprite2 = mcrfpy.Sprite(pos=(0, 0))
|
|
||||||
scene.children.append(sprite2)
|
|
||||||
anim2 = sprite2.animate("x", 200.0, 1.0, mcrfpy.Easing.PING_PONG_SMOOTH)
|
|
||||||
|
|
||||||
# Step to midpoint
|
|
||||||
for _ in range(5):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
mid_x2 = sprite2.x
|
|
||||||
check(f"smooth midpoint x ({mid_x2:.1f}) is near 200", mid_x2 > 150.0)
|
|
||||||
|
|
||||||
# Step to end
|
|
||||||
for _ in range(5):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
final_x2 = sprite2.x
|
|
||||||
check(f"smooth end x ({final_x2:.1f}) returns near 0", final_x2 < 20.0)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 4: Ping-pong with loop=True cycles continuously ---
|
|
||||||
sprite3 = mcrfpy.Sprite(pos=(0, 0))
|
|
||||||
scene.children.append(sprite3)
|
|
||||||
anim3 = sprite3.animate("x", 100.0, 0.5, mcrfpy.Easing.PING_PONG, loop=True)
|
|
||||||
check("ping-pong loop is_looping", anim3.is_looping == True)
|
|
||||||
|
|
||||||
# Step through 2 full cycles
|
|
||||||
for _ in range(20):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
check("ping-pong loop doesn't complete", anim3.is_complete == False)
|
|
||||||
check("ping-pong loop has valid target", anim3.hasValidTarget() == True)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 5: complete() on ping-pong returns to start value ---
|
|
||||||
sprite4 = mcrfpy.Sprite(pos=(50, 0))
|
|
||||||
scene.children.append(sprite4)
|
|
||||||
anim4 = sprite4.animate("x", 200.0, 1.0, mcrfpy.Easing.PING_PONG)
|
|
||||||
|
|
||||||
# Step partway through
|
|
||||||
for _ in range(3):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
# Now complete - should return to start (50), not go to target (200)
|
|
||||||
anim4.complete()
|
|
||||||
completed_x = sprite4.x
|
|
||||||
check(f"complete() returns to start ({completed_x:.1f})", abs(completed_x - 50.0) < 5.0)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 6: stop() on ping-pong freezes at current value (no jump) ---
|
|
||||||
sprite5 = mcrfpy.Sprite(pos=(0, 0))
|
|
||||||
scene.children.append(sprite5)
|
|
||||||
anim5 = sprite5.animate("x", 100.0, 1.0, mcrfpy.Easing.PING_PONG_SMOOTH)
|
|
||||||
|
|
||||||
# Step to ~midpoint
|
|
||||||
for _ in range(5):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
pre_stop_x = sprite5.x
|
|
||||||
anim5.stop()
|
|
||||||
|
|
||||||
# Step more - value shouldn't change
|
|
||||||
for _ in range(5):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
post_stop_x = sprite5.x
|
|
||||||
check(f"stop() freezes value ({pre_stop_x:.1f} == {post_stop_x:.1f})",
|
|
||||||
abs(pre_stop_x - post_stop_x) < 1.0)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 7: Callback receives start value for ping-pong ---
|
|
||||||
callback_values = []
|
|
||||||
def on_complete(target, prop, value):
|
|
||||||
callback_values.append(value)
|
|
||||||
|
|
||||||
sprite6 = mcrfpy.Sprite(pos=(10, 0))
|
|
||||||
scene.children.append(sprite6)
|
|
||||||
anim6 = sprite6.animate("x", 300.0, 0.5, mcrfpy.Easing.PING_PONG, callback=on_complete)
|
|
||||||
|
|
||||||
# Step to completion
|
|
||||||
for _ in range(10):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
check("callback was triggered", len(callback_values) == 1)
|
|
||||||
if callback_values:
|
|
||||||
# Callback value should be near start value (10), not target (300)
|
|
||||||
check(f"callback value ({callback_values[0]:.1f}) is near start (10)",
|
|
||||||
abs(callback_values[0] - 10.0) < 5.0)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 8: EaseInOut ping-pong (sin^2) is smooth at boundaries ---
|
|
||||||
sprite7 = mcrfpy.Sprite(pos=(0, 0))
|
|
||||||
scene.children.append(sprite7)
|
|
||||||
anim7 = sprite7.animate("x", 100.0, 1.0, mcrfpy.Easing.PING_PONG_EASE_IN_OUT)
|
|
||||||
|
|
||||||
# Capture values at several timesteps
|
|
||||||
values = []
|
|
||||||
for _ in range(10):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
values.append(sprite7.x)
|
|
||||||
|
|
||||||
# First value should be small (accelerating from 0)
|
|
||||||
check(f"easeInOut starts slow ({values[0]:.1f} < 30)", values[0] < 30.0)
|
|
||||||
# Middle values should be larger
|
|
||||||
check(f"easeInOut peaks in middle ({max(values):.1f} > 70)", max(values) > 70.0)
|
|
||||||
# Last value should be near 0 again
|
|
||||||
check(f"easeInOut returns to start ({values[-1]:.1f} < 10)", values[-1] < 10.0)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 9: Legacy string names work for ping-pong ---
|
|
||||||
sprite8 = mcrfpy.Sprite(pos=(0, 0))
|
|
||||||
scene.children.append(sprite8)
|
|
||||||
anim8 = sprite8.animate("x", 100.0, 0.5, "pingPong")
|
|
||||||
check("legacy string 'pingPong' works", anim8 is not None)
|
|
||||||
|
|
||||||
for _ in range(10):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
check("legacy string anim completes", anim8.is_complete == True)
|
|
||||||
check(f"legacy string returns to start ({sprite8.x:.1f})", abs(sprite8.x) < 5.0)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 10: Standard easing complete() is unaffected (regression) ---
|
|
||||||
sprite9 = mcrfpy.Sprite(pos=(0, 0))
|
|
||||||
scene.children.append(sprite9)
|
|
||||||
anim9 = sprite9.animate("x", 500.0, 1.0, mcrfpy.Easing.EASE_IN_OUT)
|
|
||||||
|
|
||||||
# Step partway
|
|
||||||
for _ in range(3):
|
|
||||||
mcrfpy.step(0.1)
|
|
||||||
|
|
||||||
anim9.complete()
|
|
||||||
check(f"standard easing complete() goes to target ({sprite9.x:.1f})",
|
|
||||||
abs(sprite9.x - 500.0) < 5.0)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Test 11: Animation constructor with ping-pong easing ---
|
|
||||||
anim10 = mcrfpy.Animation("x", 100.0, 1.0, mcrfpy.Easing.PING_PONG, loop=True)
|
|
||||||
check("Animation constructor with PING_PONG", anim10 is not None)
|
|
||||||
check("Animation constructor loop=True", anim10.is_looping == True)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Summary ---
|
|
||||||
if PASS:
|
|
||||||
print("PASS")
|
|
||||||
sys.exit(0)
|
|
||||||
else:
|
|
||||||
print("FAIL")
|
|
||||||
sys.exit(1)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue