Bones now reference atlas-uv rectangles instead of holding per-bone
texture handles. The rig gains an atlas record with a single diffuse
texture handle plus a tiles-by-name dictionary for bone-name lookup.
Bone-draw uses the ADR-0044 source-rect render primitive. Anchor
defaults to tile UV center; rig-level b.anchor overrides. Bumps lib
to 0.5.0 to flag the breaking change in rig.bones shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New public function to change the play-speed of a currently-playing
animation WITHOUT resetting its cycle position. Calling M.play with a
new speed sets t=0, causing visible cycle restart; set_play_speed
mutates only the speed field.
Use case: sprint toggle mid-walk doubles walk-anim cadence smoothly.
Silent no-op when the anim is not currently playing.
New optional bone field 'inherit_scale' (default true). When set false,
world.scl = local.scl (no parent.scl multiplier). Position cascade still
uses parent.scl (so child follows parent's stretch positionally).
Use case: feet that move with leg-end as leg.scl_y cycles +1/0/-1, but
foot sprite stays at constant scale (no Y-flip when leg flips, no
collapse when leg collapses).
Allow procedural callbacks to write channels that no active anim
writes on the same bone. Example: walk_anim writes leg.scl; procedural
can now write leg.rot without track-conflict error. Previously the
guard was track-level (any anim on the track blocked ALL writes).
Enables hip-anchor-stable + leg-orientation-procedural pattern in
vagrant-skeleton (legs parent=body for body-relative hips, leg.rot
overridden per-frame to walk-direction while walk_fwd_lower drives
leg.scl).
Existing Group 4 track-conflict test still passes: walk_anim writes
leg.rot in keyframes; procedural-write of leg.rot still errors (same
channel). 42/42 assertions GREEN.
When body's world.rot crosses ±π (e.g., body facing south), target_local
in the look-at-block could momentarily be ≈±2π. Without normalization,
the snap-branch wrote that raw value to state.angle, then rot_min/rot_max
clamp produced a discontinuous jump (head 'flicked sideways' when body
faced south, per manual interactive test).
Fix: normalize target_local to (-π, π] BEFORE the snap/slerp/rate-limit
branch and BEFORE clamp. Symmetric handling regardless of which smoothing
mode the bone uses.
Multi-look-target API: set_look_target(handle, target_id, x, y),
clear_look_target(handle, target_id). Bones declare look_at as string
target-id; legacy look_at=true maps to target_id="default". Slerp-style
smoothing: look_at_slerp field (exponential approach, clamped to [0,1]
per frame). Linear look_at_speed kept for backward-compat. Virtual bone
flag: virtual=true skips render pass but participates in compute_world.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds optional 'sprite_rot' field (degrees in JSON, radians stored) to
bone schema. Applied at render: world.rot + sprite_rot. Substrate
convention (puppet.c:295, 2101). Use case: head_001.png has face-right
at rot=0 (asymmetric sprite); without sprite_rot=-90 the look-at result
appears 90° off because math computes rotation assuming sprite-top=
forward but the sprite's natural forward is sprite-right.
Default sprite_rot=0 keeps body, shoulder, arm bones unchanged.
Previously the look-at-rate-limit read p.bone_state[bid].rot as 'current'
and advanced from it. But bone_state.rot is reset-to-rest + keyframe-
sampled at frame start, so 'current' was always near rest (~0) and the
rate-limit only advanced one frame's worth (~7deg) per frame. Visual
result: head/body rotation appeared capped at ~10deg.
Fix: persistent p.look_at_state[bid] table carries angle across frames.
Rate-limit operates on look_at_state.angle (not bone_state.rot). After
rate-limit + clamp, value is written to bone_state.rot.
Init: look_at_state[bid].angle = b.rest.rot on first encounter, so the
first frame advances from rest pose toward target (matches substrate
LookAtState init convention).
- Look-at uses math.atan(dx, -dy) (substrate sprite-top convention):
rot=0 means sprite top points up (in -Y), rot=pi/2 means right.
- New rest.rot_min/rot_max fields (degrees in JSON; clamped after look-at)
- New look_at_speed field (deg/s; rate-limits look-at via dt)
- write_bone now accepts rot/pos/scl channels (legacy 'angle' rejected)
- Bones not in any track are allowed; write_bone bypasses track-guard
for them. Enables procedural root-rotation while keyframe-anims play.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- apply_locomotion idle-return: after (now - last_move_time) >
idle_return_delay, the most-displaced planted leg steps back to its
current hip position (over idle_step_duration). Group-stagger via
bumping last_move_time forward by idle_return_stagger.
- M.move_to tracks p.last_move_time when position actually changes.
- README v0.3.0 changelog entry.
Additive extension to build_rig output, preserving backward-compat
with existing rigs that lack the new fields:
- bone.texture: optional atlas-id (string) for sprite-mode rendering;
resolved to a texture-handle by puppet.load_textures(). When absent
the bone falls back to colored-rect render via draw_rect_rotated.
- bone.z_order: render-sort key (lower = back, higher = front);
default 0.
- bone.scale: [sx, sy] pair; default [1, 1]; negative components
mirror the sprite.
- bone.anchor: optional [x, y] override of the atlas-supplied anchor.
- rig.asset_pack: alias-key into the consumer module's asset_aliases
manifest field; lets puppet.load_textures resolve atlas-ids to
texture-handles via the asset-lib.
- rig.bones_z_sorted: array of bones pre-sorted by z_order; render
walks this once per frame.
Version bumped to 0.2.0. Subsequent commits add multi-level parent-
chain transform, sprite-mode render, and load_textures.
Validates and cooks rig + animation data into internal representations:
- build_rig: parses bones (with parent-resolution to object refs) and
tracks (with bone-uniqueness validation). Converts rest angles
from JSON degrees to radians.
- build_animation: validates each keyframe references only bones in
the animation's declared track. Converts keyframe angles from
degrees to radians.
Remaining functions (sample_animation, spawn, update, look-at,
procedural, render, lifecycle) follow in subsequent commits.