User feedback after iteration-4: legs SHOULD cycle through +1/0/-1
(substrate-style — leg renders forward, under body, then behind body).
But feet must NOT Y-flip when leg.scl_y goes negative, and must NOT
collapse when leg.scl_y=0. They should follow leg-end position only.
Three changes:
1. walk_fwd_lower keyframes restored to substrate-style (leg.scl_y
sign-flip on both halves: leg_l +1↔-1, leg_r -1↔+1). foot.pos
animations REMOVED — cascade through leg.world.scl now correctly
moves foot to leg-end position automatically (foot.local.pos
stays at rest 25 in y).
2. foot.rest.scl: [1, -1] → [1, 1] (no Y-flip in rest, natural
sprite orientation). inherit_scale=false → foot.world.scl stays
at foot.local.scl=[1,1] regardless of leg.scl_y. Foot sprite
never flips, never collapses.
3. Procedural extended: writes foot.local.rot = -leg.local.rot so
foot.world.rot = body.world.rot (feet stay body-oriented even
when legs rotate to walk-direction).
Speed slowed further from 0.5 → 0.4 (user: still too fast at 0.5).
User feedback: 'anchorpoints sind hüftgelenke und können sich nicht
frei drehen. hüfte bleibt zum körper statisch'.
Architecture:
- legs.parent = body (was lower_control). Hip position cascades from
body.world.rot, so hips stay at body-frame left/right regardless of
body rotation. lower_control virtual bone removed (no longer needed).
- Procedural 'leg_orientation' sets leg.local.rot = walk_dir - body.rot
each frame, so leg.world.rot = walk_dir (independent of body cascade).
Requires puppet v0.4.2 per-channel write_bone (walk_anim writes leg.scl
while procedural writes leg.rot — disjoint channels).
Result: legs visually attach at body's anatomical left/right hips no
matter where body faces; stride oscillation aligns with WASD walk
direction. Hip position no longer drifts with walk direction.
Two fixes from manual interactive verification after iteration-3:
1. Head 90° CCW off → removed sprite_rot=-90 from head bone. The
v0.3.3 sprite_rot fix was based on misobservation: at that point
the persistent-state-rate-limit bug was active and head was
wiggling within ±10° of an arbitrary initial direction, perceived
as 90° CW off. With v0.3.2+ persistent-state fix and v0.4.0 slerp,
head rotates correctly when sprite-natural-face is -Y (matching
body-sprite convention).
2. Legs walked in inverted sector → signed_angle_2d cross-component
was using Y-up math convention but Sporel screen-coords are Y-down.
With Y-down, what's CCW in math is CW visually. Flipping the cross
sign aligns sector selection: body up + player walks right → CW
visually → walk_right_lower (was wrongly returning walk_left).
Front/back leg-anchor issue (#3 from user report) deferred — likely an
optical-overlap artifact from rotated body sprite + world-frame leg
positions, will reassess after #1+#2 fixes are visually verified.
Add lower_control virtual bone (reparents legs for independent orientation).
Update look_at fields: body uses "soft" target with slerp=6, head uses
"aim" target with instant snap and ±60° clamp. Rename walk.anim.json to
walk_fwd_lower.anim.json and add walk_back_lower, walk_left_lower,
walk_right_lower anims ported from substrate. Port HandleAnimationLower:
signed-angle sector selection (±45°/±135°) drives 4-directional walk anim
choice; wedge clamp sets lower_control rotation. Separate aim/soft look
targets both track raw mouse for now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
head_001.png is drawn with face pointing +X (right) at rot=0, but the
look-at math assumes sprite-top (-Y) is forward. Without compensation,
head visually appears 90° clockwise off when math says 'point at mouse'.
sprite_rot=-90 on head bone aligns sprite-top with bone-forward at
render time. Body, shoulder, and arm sprites already match the
sprite-top=forward convention so no sprite_rot needed there.
Also bumps puppet dep to 0.3.3 and removes the temporary debug print.
- humanoid.rig.json: removed locomotion-block (apply_locomotion no-ops
now). head: removed look_at_speed (instant snap). body: unchanged
(look_at_speed=180 lazy).
- init.lua: removed set_procedural("facing", ...) — root stays at
rest.rot=0. walk.anim + idle_lower.anim no longer loaded/played
(files retained on disk for revert). New set_procedural("walk_cycle",
...) drives legs per-frame: walk_phase advances at 1.25Hz, sin-cycle
on leg.scl, walk_dir applied to leg.rot. Idle: legs scl=0.
- Lazy body + instant head + head-leading: head responds immediately
to mouse, body lags via look_at_speed=180.
This is an experiment. If procedural feels worse, revert by re-enabling
walk.anim + idle_lower.anim + locomotion-block.
- humanoid.rig.json: root removed from upper track; body look_at + slow speed
(180 deg/s, lazy); head fast speed (480 deg/s) + +-90 deg limits; puppet
dep bumped to 0.3.1
- animations/idle_lower.anim.json (NEW): leg+foot scl=[mirror, 0] keeps legs
invisible at idle (locomotion skips animated bones; foot cascade zeros out)
- init.lua: set_procedural 'facing' callback writes root.rot from
last_walk_rot_deg each frame; WASD-vector updates last_walk_rot_deg
via atan(dx, -dy); idle_lower played parallel to idle, stopped during walk
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
13-bone humanoid rig (12 visible bones + 1 root) hand-ported from
substrate reference, mapped to PROTOTYPE asset-pack via asset_aliases
indirection (asset_pack: player). Bones use texture/z_order/scale/
anchor fields from the rig-format extension. Tracks split: lower for
legs + feet (walk animation), upper for root/body/head/shoulders/arms
(idle animation).
Animations:
- idle.anim.json: 2.0s loop on upper track, subtle body+head sway
- walk.anim.json: 0.6s loop on lower track, legs+feet cycle
Sprite-tilemap: 4-tile palette (floor_001-003 walkable, floor_004
as wall). Map content: 20x20 with wall-border and interior wall
blocks. tile_rotations parallel array exercises 90 and 180-degree
rotations on a handful of cells for visual rotation-pipeline
verification.
init.lua loads textures via asset_aliases lookup:
- maps.load_textures(aliases) resolves tilemap-tile atlas-ids
- puppet.load_rig(path, aliases) loads + resolves bone textures
- furniture textures preloaded at init (bed, bench, backpack) to
avoid first-frame load stutter; rendered via engine.render.
draw_sprite_transform in the draw hook
Camera follows puppet via per-frame camera.set_target. Headtrack:
head bone (look_at: true) rotates to face mouse world-position.
New SPOREL_CI=1 traces from render and init hooks:
- vagrant: render_frame_ok (per frame from render)
- vagrant: sprite_mode=on (once after first sprite-draw)
- vagrant: assets_loaded=14 (once after init: 7 puppet + 4 tile + 3 furniture)
Composer module: single-player character as puppet (humanoid 4-bone
rig: torso, head, leg_l, leg_r) at world (320, 240) on a 20x20
v0.1-format tilemap.
- WASD direct movement, normalized for diagonals, 120 px/s.
- Camera-chase via lib-core.camera target-provider (camera follows
puppet position frame-to-frame).
- Idle and walk keyframe animations: idle is a 2s torso-sway on the
upper track; walk is a 0.6s leg-cycle on the lower track.
- Animation-state transitions driven by movement input: walk while
WASD-active, idle when released.
- Headtrack: head bone (marked look_at: true in rig) rotates each
frame to face the mouse cursor's world position via
puppet.set_look_target.
- ESC -> launcher.
CI hooks (env-var-gated, no-op in normal play):
- SPOREL_CI=1: emits 'vagrant: frame_avg_ms=N' after 60 frames and
'vagrant: render_frame_ok' per frame from the render hook, then
engine.exit(0). Smoke stages consume these markers.