docs(puppet): README sync to v0.4.3 + multi-target/slerp/inherit_scale/virtual/sprite_rot APIs
Updates README to reflect the Subterrain-runtime-control model adopted across v0.3.1–v0.4.3. Previous README described the original v0.3.0 substrate-port state and omitted all v0.4.x features. Added to API section: set_look_target (multi-target, named target_id), clear_look_target (per-target or all), write_bone (per-channel conflict guard description). Updated description of bone_world_transform (returns radians, not angle). Added conventions note on camera.begin/finish requirement for render. CHANGELOG entries added for v0.3.1, v0.3.2, v0.3.3, v0.4.0, v0.4.1, v0.4.2, v0.4.3 covering all intermediate fixes and the model pivot. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
217
README.md
217
README.md
@@ -1,13 +1,15 @@
|
||||
# lib-core.puppet
|
||||
|
||||
Skeletal animation primitive for top-down 2D characters: bones,
|
||||
tracks, rest pose, keyframe animations, procedural animations
|
||||
(Lua-side), and look-at constraint. No footplant IK in v0.1
|
||||
(deferred).
|
||||
Skeletal animation primitive for top-down 2D characters. Manages a
|
||||
bone hierarchy with cascaded world-transforms, keyframe animation
|
||||
playback (multi-channel: rot/pos/scl), multi-target look-at constraints
|
||||
(slerp or rate-limit or instant-snap), procedural animation callbacks,
|
||||
and a per-channel conflict guard that allows animation and procedural
|
||||
layers to coexist on the same bones.
|
||||
|
||||
**Version:** 0.3.0
|
||||
**Version:** 0.4.3
|
||||
**Lib-ID:** lib-core.puppet
|
||||
**Requires:** (none — pure Lua + engine.render.*)
|
||||
**Requires:** (none — pure Lua + engine.render.* + engine.asset.*)
|
||||
**Tags:** animation, skeleton, puppet, character
|
||||
|
||||
## Topology
|
||||
@@ -19,135 +21,184 @@ tracks, rest pose, keyframe animations, procedural animations
|
||||
|
||||
### `puppet.build_rig(rig_table)`
|
||||
|
||||
Validates a rig table (parsed from `*.rig.json`) and returns an
|
||||
internal representation with `bones_by_id`, parent-resolved object
|
||||
references, and track-membership maps. Raises on invalid data
|
||||
(missing parents, bones in 2 tracks, etc.).
|
||||
Validates a rig table (parsed from `*.rig.json`) and returns an internal
|
||||
representation with `bones_by_id`, parent-resolved object references,
|
||||
track-membership maps, z-sorted bone list, and optional locomotion config.
|
||||
Raises on invalid data (missing parents, bones in 2 tracks, legacy
|
||||
`rest.angle` field, etc.).
|
||||
|
||||
### `puppet.build_animation(anim_table, rig)`
|
||||
|
||||
Validates an animation table against a built rig (bones referenced
|
||||
in keyframes must exist and be in the animation's declared track).
|
||||
Validates an animation table against a built rig. Bones referenced in
|
||||
keyframes must exist and be in the animation's declared track. Raises on
|
||||
unknown bones, wrong-track bones, or legacy `angle` channel name.
|
||||
|
||||
### `puppet.sample_animation(animation, t)`
|
||||
### `puppet.sample_animation(anim, t)`
|
||||
|
||||
Returns a frame `{ <bone_id> = { angle = <number>, ... }, ... }`
|
||||
sampling the animation at time `t` with linear interpolation. Wraps
|
||||
on `t > duration` if `animation.loop` is true.
|
||||
Returns a frame `{ <bone_id> = { rot?, pos?, scl? }, ... }` sampling the
|
||||
animation at time `t` with linear interpolation per channel. Wraps on
|
||||
`t > duration` if `animation.loop` is true. Each channel is sampled
|
||||
independently; absent channel means no override (hold-on-single-side
|
||||
semantics).
|
||||
|
||||
### `puppet.spawn(rig, {x, y})`, `puppet.despawn(handle)`
|
||||
|
||||
Spawn a new puppet instance at world position (x, y). Returns an
|
||||
opaque handle. `despawn` removes the instance.
|
||||
Spawn a new puppet instance at world position (x, y). Returns an opaque
|
||||
handle. `despawn` removes the instance and all its state.
|
||||
|
||||
### `puppet.update(dt)`, `puppet.render(handle)`
|
||||
|
||||
`update(dt)` runs the pipeline for ALL spawned puppets (rest →
|
||||
look-at → keyframes → procedural). `render(handle)` draws ONE
|
||||
puppet via `engine.render.draw_rect_rotated` per bone.
|
||||
`update(dt)` runs the full pipeline for ALL spawned puppets (rest-reset
|
||||
→ keyframes → look-at → procedural → compute_world → apply_locomotion).
|
||||
`render(handle)` draws ONE puppet via `engine.render.draw_sprite_transform`
|
||||
per non-virtual bone. Call `render` inside a `camera.begin()/finish()`
|
||||
block.
|
||||
|
||||
### `puppet.set_look_target(p, world_x, world_y)`, `puppet.clear_look_target(p)`
|
||||
### `puppet.set_look_target(handle, target_id, x, y)`, `puppet.clear_look_target(handle, target_id?)`
|
||||
|
||||
Set or clear the look-at target. Bones marked `look_at: true` in the
|
||||
rig rotate to face the target.
|
||||
Set or clear a named look-at target. Bones with `look_at: "target_id"`
|
||||
in the rig rotate to face the matching target position each frame. The
|
||||
2-arg legacy form `set_look_target(handle, x, y)` sets the "default"
|
||||
target. `clear_look_target(handle)` without a target_id clears all targets.
|
||||
|
||||
### `puppet.bone_angle(p, bone_id)`
|
||||
### `puppet.set_procedural(handle, name, callback)`, `puppet.clear_procedural(handle, name)`
|
||||
|
||||
Returns the current world-space angle of `bone_id` (after the
|
||||
full pipeline tick). Used by tests + by callers needing bone-state.
|
||||
Register or unregister a per-frame procedural callback. Callback signature:
|
||||
`function(handle, dt)`. Inside the callback, mutate bone transforms via
|
||||
`puppet.write_bone`. Callbacks are invoked once per puppet per frame, at
|
||||
root-visit time (before compute_world for that frame).
|
||||
|
||||
### `puppet.set_procedural(p, name, callback)`, `puppet.clear_procedural(p, name)`
|
||||
### `puppet.write_bone(handle, bone_id, {rot?, pos?, scl?})`
|
||||
|
||||
Register or unregister a per-frame procedural callback. Callback
|
||||
signature: `function(handle, dt)`. Inside the callback, mutate
|
||||
bones via `puppet.write_bone(handle, bone_id, { angle = <number> })`.
|
||||
Write one or more channels of a bone's local transform from a procedural
|
||||
callback. Per-channel conflict guard: raises if the written channel is also
|
||||
written by a keyframe in any active animation on that bone's track. Allows
|
||||
procedural to write `rot` while an animation writes `scl` on the same bone.
|
||||
|
||||
### `puppet.play(p, animation_name, { loop, speed })`, `puppet.stop(p, animation_name)`, `puppet.stop_all(p)`, `puppet.is_playing(p, animation_name)`
|
||||
### `puppet.play(handle, anim_id, {loop?, speed?})`, `puppet.stop(handle, anim_id)`, `puppet.stop_all(handle)`, `puppet.is_playing(handle, anim_id)`
|
||||
|
||||
Keyframe-layer controls. Animation must be registered via
|
||||
`puppet.register_animation(p, anim)` first.
|
||||
`puppet.register_animation(handle, anim)` first. `speed` defaults to 1.0;
|
||||
pass a value like `0.45` for a slower cadence.
|
||||
|
||||
### `puppet.register_animation(p, animation)`
|
||||
### `puppet.register_animation(handle, anim)`
|
||||
|
||||
Binds a built animation (from `build_animation`) to a puppet
|
||||
instance, making it available to `play`.
|
||||
Binds a built animation (from `build_animation`) to a puppet instance,
|
||||
making it available to `play`.
|
||||
|
||||
### `puppet.load_rig(path, asset_aliases?)`
|
||||
|
||||
Loads a rig from a JSON file and optionally resolves its bone textures
|
||||
via `puppet.load_textures`. Pass the module's `asset_aliases` table
|
||||
(from `engine.module.asset_aliases()`) to enable sprite-mode rendering;
|
||||
omit for colored-rect-only rigs.
|
||||
Loads a rig from a JSON file and optionally resolves bone textures via
|
||||
`puppet.load_textures`. Pass `engine.module.asset_aliases()` to enable
|
||||
sprite-mode rendering; omit for colored-rect-only rigs.
|
||||
|
||||
### `puppet.load_textures(rig, asset_aliases)`
|
||||
|
||||
For each bone with a `texture` field, resolves the atlas-id to a
|
||||
texture-handle via the asset-lib indirection: read `asset_aliases[rig.asset_pack]`
|
||||
to get the lib-id, load `<lib-id>/assets/atlas.json`, look up the
|
||||
atlas-entry by id, and call `engine.asset.load_texture` on the file
|
||||
path. Also resolves bone anchors from the atlas (unless bone has an
|
||||
explicit `anchor` field).
|
||||
|
||||
### `puppet.bone_world_transform(handle, bone_id)`
|
||||
|
||||
Returns the bone's cached world-transform `(x, y, angle)` from the
|
||||
last `puppet.update` tick. Used by tests and by modules needing world-
|
||||
space queries (e.g., for spawning effects at a bone position).
|
||||
|
||||
### `puppet.write_bone_test_only(handle, bone_id, angle_rad)`
|
||||
|
||||
Test-setup helper: directly write a bone angle bypassing the track-
|
||||
conflict-guard. Not for production use; modules should write via
|
||||
procedural callbacks.
|
||||
texture-handle via the asset-lib indirection. Also resolves bone anchors
|
||||
from the atlas (unless the bone has an explicit `anchor` field). Caches
|
||||
texture dimensions for locomotion sprite_reach computation.
|
||||
|
||||
### `puppet.load_animation(path, rig)`
|
||||
|
||||
Convenience wrapper: reads a JSON file and calls `build_animation`.
|
||||
|
||||
### `puppet.position(p)`, `puppet.facing(p)`, `puppet.move_to(p, x, y)`
|
||||
### `puppet.bone_world_transform(handle, bone_id)`
|
||||
|
||||
Puppet owns its world position. `move_to` sets it instantly (caller
|
||||
applies speed). `facing` returns the last-movement-direction angle.
|
||||
Returns `x, y, rot` — the bone's cached world-transform from the last
|
||||
`puppet.update` tick. World rot is in radians. Used by procedural callbacks
|
||||
to read parent bone state (e.g., `body.world.rot` for leg orientation math).
|
||||
|
||||
### `puppet.write_bone(handle, bone_id, { angle })`
|
||||
### `puppet.bone_world_scale(handle, bone_id)`
|
||||
|
||||
Procedural-callback-only API to mutate a bone. Validates track
|
||||
ownership at write time.
|
||||
Returns `scl_x, scl_y` — the bone's cached world scale. Accounts for
|
||||
`inherit_scale` flag (false → returns local.scl, not parent.scl * local.scl).
|
||||
|
||||
### `puppet.position(handle)`, `puppet.facing(handle)`, `puppet.move_to(handle, x, y)`
|
||||
|
||||
Puppet world position management. `move_to` sets position instantly and
|
||||
updates `facing` (last-movement-direction angle, radians). `facing` returns
|
||||
the last movement direction.
|
||||
|
||||
### `puppet.write_bone_test_only(handle, bone_id, angle_rad)`
|
||||
|
||||
Test-setup helper: directly write a bone angle bypassing the per-channel
|
||||
conflict guard. Stored in `test_overrides`; applied after rest-reset and
|
||||
keyframe sampling. Not for production use.
|
||||
|
||||
### `puppet.set_foot_sprite_reach_test_only(handle, leg_index, value)`, `puppet.foot_state_test_only(handle, leg_index)`, `puppet.bone_animated_test_only(handle, bone_id)`
|
||||
|
||||
Test-only access to locomotion internal state and per-frame keyframe flags.
|
||||
Not for production use.
|
||||
|
||||
## Conventions
|
||||
|
||||
- World-coords + pixel-units throughout (per ADR-0031).
|
||||
- Bone angles in radians (internal). JSON rest-angles and animation
|
||||
keyframe angles are in degrees, converted on load.
|
||||
- Bone angles in radians internally. JSON `rest.rot` and keyframe `rot`
|
||||
are in degrees; converted on load via `math.rad`.
|
||||
- `look_at_speed` is degrees/s in JSON; stored as radians/s.
|
||||
- `look_at_slerp` is a unitless rate (not converted).
|
||||
- `sprite_rot` is degrees in JSON; stored as radians.
|
||||
- `rest.rot_min` / `rest.rot_max` are degrees in JSON; stored as radians.
|
||||
- Update pipeline runs once per frame (`puppet.update(dt)`) for ALL
|
||||
spawned puppets. `render` is per-puppet so callers can interleave
|
||||
with other rendering.
|
||||
- `render` must be called inside a `camera.begin()` / `camera.finish()`
|
||||
block to render in world-space.
|
||||
|
||||
## CHANGELOG
|
||||
|
||||
### v0.4.3
|
||||
- `inherit_scale: false` bone flag: when false, `world.scl = local.scl`
|
||||
(parent.world.scl not multiplied). Position cascade still uses parent.scl.
|
||||
Primary use case: feet that track leg-end positionally without Y-flip
|
||||
inheritance when leg.scl_y inverts during walk cycle.
|
||||
|
||||
### v0.4.2
|
||||
- Per-channel write_bone conflict check (was per-track). Procedural can now
|
||||
write channels (e.g., rot) that no active animation writes on the same bone
|
||||
(e.g., animation writes scl). Enables animation + procedural on same bone
|
||||
without conflict.
|
||||
|
||||
### v0.4.1
|
||||
- target_local normalization to (-π, π] in look-at block. Fixes head-flick
|
||||
when body world.rot crosses ±π (south-facing direction).
|
||||
|
||||
### v0.4.0
|
||||
- Multi-look-target API: `puppet.set_look_target(handle, target_id, x, y)`.
|
||||
Bones declare `look_at: "target_id"` (string). Legacy `look_at: true`
|
||||
maps to "default" target.
|
||||
- Slerp smoothing: `look_at_slerp` field (unitless rate, exponential approach).
|
||||
`look_at_speed` (linear rate-limit) kept for backward-compat.
|
||||
- Virtual bones: `virtual: true` — participates in compute_world, skipped
|
||||
in render.
|
||||
- Legs re-parented to body (was root in v0.3.0).
|
||||
|
||||
### v0.3.3
|
||||
- Per-bone `sprite_rot` (degrees in JSON): render-time rotation offset
|
||||
applied as `world.rot + sprite_rot`. Matches substrate puppet.c:2101
|
||||
convention.
|
||||
|
||||
### v0.3.2
|
||||
- Persistent `look_at_state` per bone. Fixes rate-limit reset-bug where
|
||||
each frame's rest-reset wiped the accumulated look-at rotation.
|
||||
|
||||
### v0.3.1
|
||||
- Look-at math fix: `world_angle = atan(dx, -dy)` (sprite-top convention:
|
||||
rot=0 = top faces target, not game-forward).
|
||||
|
||||
### v0.3.0
|
||||
- BoneState multi-channel (rot/pos/scl) with world.scl_x/y and world.rot
|
||||
- BoneState multi-channel (rot/pos/scl) with world.scl_x/y
|
||||
- Cascaded world-transform with scale propagation (compute_world)
|
||||
- Procedural locomotion layer via rig.locomotion block (apply_locomotion):
|
||||
step-trigger, step-placement, step-height, leg-aim + leg-stretch
|
||||
- Idle-return logic in apply_locomotion: after (now - last_move_time) >
|
||||
idle_return_delay, the most-displaced planted leg steps back to its hip
|
||||
position over idle_step_duration; group-stagger via idle_return_stagger
|
||||
- M.move_to tracks p.last_move_time when position actually changes
|
||||
- Schema changes: rest.rot/scl required; legacy rest.angle rejected with
|
||||
clear error message (Substrate-Parity v0.3.0 schema)
|
||||
- Bone-rig top-level scale field removed; per-frame world state replaces it
|
||||
- Procedural locomotion layer via rig.locomotion block (apply_locomotion)
|
||||
- Schema: rest.rot (renamed from rest.angle), rest.scl added
|
||||
- New APIs: bone_world_scale, set_foot_sprite_reach_test_only,
|
||||
foot_state_test_only, bone_animated_test_only
|
||||
|
||||
### v0.2.0
|
||||
- Rig-format extension (texture, z_order, scale, anchor, asset_pack
|
||||
fields on bones; rig.bones_z_sorted for render-order)
|
||||
- Multi-level parent-chain transform with depth-first update pipeline
|
||||
- Sprite-mode render via engine.render.draw_sprite_transform
|
||||
- Rig-format extension (texture, z_order, anchor, asset_pack)
|
||||
- Multi-level parent-chain transform + sprite-mode render
|
||||
- New API: load_textures, bone_world_transform, write_bone_test_only
|
||||
- All v0.1.0 rigs continue to work (texture-less bones fall back to
|
||||
colored-rect render)
|
||||
|
||||
### v0.1.0
|
||||
- Initial release: skeleton + bones + tracks + rest + keyframe +
|
||||
@@ -155,6 +206,8 @@ ownership at write time.
|
||||
|
||||
## References
|
||||
|
||||
- `architecture/puppet.md` (Reference)
|
||||
- `architecture/puppet.md` (Reference — updated to v0.4.3)
|
||||
- `superpowers/specs/2026-05-18-puppet-subterrain-model-design.md` (Baseline Spec v0.4.3)
|
||||
- `docs/adrs/0040-puppet-subterrain-model-pivot.md` (Pivot rationale)
|
||||
- ADR-0037 (Tests-as-Libs)
|
||||
- ADR-0038 (API-Doc-Convention)
|
||||
|
||||
Reference in New Issue
Block a user