exp(vagrant): try procedural lower-body, drop WASD-to-root coupling

- 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.
This commit is contained in:
Axel Meyer
2026-05-18 20:13:44 +02:00
parent 03a536984d
commit 9de4924706
2 changed files with 48 additions and 39 deletions

View File

@@ -1,4 +1,4 @@
-- sporel-module-vagrant-skeleton v0.3.1
-- sporel-module-vagrant-skeleton v0.3.2
-- Sprite-mode test-chamber: 13-bone humanoid puppet + sprite-tilemap +
-- hardcoded furniture. F.vagrant Tier-1 + Sprite-Upgrade-Bridge state.
@@ -17,7 +17,10 @@ local state = {
bed_tex = nil,
bench_tex = nil,
backpack_tex = nil,
last_walk_rot_deg = 0, -- persisted root.rot (degrees) across idle frames
is_walking = false,
walk_phase = 0, -- 0..1 cycle position
walk_dir_rad = 0, -- world-angle of walk direction (sprite-top convention)
walk_dir_set = false, -- true after first move; legs orient even at idle once set
}
-- CI hook gating (mirrors Phase 1 convention).
@@ -33,18 +36,14 @@ local ci_assets_traced = false
local function set_walk_or_idle(moving)
local new_state = moving and "walk" or "idle"
if moving then
if not puppet.is_playing(state.player, "walk") then
if not puppet.is_playing(state.player, "walk_upper") then
puppet.stop(state.player, "idle")
puppet.stop(state.player, "idle_lower")
puppet.play(state.player, "walk", { loop = true })
puppet.play(state.player, "walk_upper", { loop = true })
end
else
if not puppet.is_playing(state.player, "idle") then
puppet.stop(state.player, "walk")
puppet.stop(state.player, "walk_upper")
puppet.play(state.player, "idle", { loop = true })
puppet.play(state.player, "idle_lower", { loop = true })
puppet.play(state.player, "idle", { loop = true })
end
end
if CI_FRAME_PERF and new_state ~= ci_last_state then
@@ -64,22 +63,43 @@ function M.init(ctx)
-- Puppet rig + sprite textures + animations.
local rig = puppet.load_rig("rigs/humanoid.rig.json", aliases)
state.player = puppet.spawn(rig, { x = 320, y = 240 })
local walk = puppet.load_animation("animations/walk.anim.json", rig)
local walk_upper = puppet.load_animation("animations/walk_upper.anim.json", rig)
local idle = puppet.load_animation("animations/idle.anim.json", rig)
local idle_lower = puppet.load_animation("animations/idle_lower.anim.json", rig)
puppet.register_animation(state.player, idle)
puppet.register_animation(state.player, walk)
puppet.register_animation(state.player, walk_upper)
puppet.register_animation(state.player, idle_lower)
puppet.play(state.player, "idle", { loop = true })
puppet.play(state.player, "idle_lower", { loop = true })
puppet.register_animation(state.player, idle)
puppet.play(state.player, "idle", { loop = true })
-- Procedural facing: root.rot = state.last_walk_rot_deg every frame.
-- Fires inside update_bone_recursive at root-visit (after reset, before compute_world).
-- Root is not in any track, so write_bone bypasses track-guard.
puppet.set_procedural(state.player, "facing", function(handle, dt)
puppet.write_bone(handle, "root", { rot = state.last_walk_rot_deg })
-- Procedural lower-body: leg-cycle driven per-frame by walk-direction + phase.
-- Replaces keyframe walk.anim + idle_lower.anim. Runs every frame in
-- update_bone_recursive's procedural callback step.
puppet.set_procedural(state.player, "walk_cycle", function(handle, dt)
if state.is_walking then
-- Advance phase. 0.8s full cycle (1/0.8 = 1.25 Hz, matches substrate walk_fwd_lower).
state.walk_phase = (state.walk_phase + dt * 1.25) % 1.0
local phase_rad = state.walk_phase * 2 * math.pi
local cycle_l = math.sin(phase_rad)
local cycle_r = -cycle_l
local walk_dir_deg = math.deg(state.walk_dir_rad)
-- Leg-rotation orients along walk direction (root stays at 0; legs rotate directly)
puppet.write_bone(handle, "leg_l", { rot = walk_dir_deg, scl = {-1, cycle_l} })
puppet.write_bone(handle, "leg_r", { rot = walk_dir_deg, scl = { 1, cycle_r} })
-- Foot: scale follows leg-extension absolute value; pos along leg axis
puppet.write_bone(handle, "foot_l", {
scl = { 1, math.abs(cycle_l) },
pos = { 0, cycle_l > 0 and 25 or 0 }
})
puppet.write_bone(handle, "foot_r", {
scl = { 1, math.abs(cycle_r) },
pos = { 0, cycle_r > 0 and 25 or 0 }
})
else
-- Idle: legs collapsed (scl=0), feet collapsed and centered
local walk_dir_deg = state.walk_dir_set and math.deg(state.walk_dir_rad) or 0
puppet.write_bone(handle, "leg_l", { rot = walk_dir_deg, scl = {-1, 0} })
puppet.write_bone(handle, "leg_r", { rot = walk_dir_deg, scl = { 1, 0} })
puppet.write_bone(handle, "foot_l", { scl = { 1, 0 }, pos = { 0, 0 } })
puppet.write_bone(handle, "foot_r", { scl = { 1, 0 }, pos = { 0, 0 } })
end
end)
-- Furniture textures (preload to avoid first-frame load stutter).
@@ -126,7 +146,8 @@ function M.update(ctx, dt)
if ci_drive_frame > 20 and ci_drive_frame <= 40 then
dx = 1; dy = 0
moving = true
state.last_walk_rot_deg = math.deg(math.atan(dx, -dy)) -- atan(1, 0) = pi/2 = 90deg
state.walk_dir_rad = math.atan(dx, -dy) -- atan(1, 0) = pi/2 = 90deg
state.walk_dir_set = true
local pos = puppet.position(state.player)
puppet.move_to(state.player,
pos.x + dx * state.player_speed * dt, pos.y)
@@ -137,13 +158,15 @@ function M.update(ctx, dt)
if moving and not CI_DRIVE then
local len = math.sqrt(dx * dx + dy * dy)
-- Update last_walk_rot_deg (sprite-top convention: angle=atan(dx,-dy)).
state.last_walk_rot_deg = math.deg(math.atan(dx / len, -dy / len))
-- Update walk_dir_rad (sprite-top convention: angle=atan(dx,-dy)).
state.walk_dir_rad = math.atan(dx / len, -dy / len)
state.walk_dir_set = true
local pos = puppet.position(state.player)
puppet.move_to(state.player,
pos.x + dx / len * state.player_speed * dt,
pos.y + dy / len * state.player_speed * dt)
end
state.is_walking = moving
set_walk_or_idle(moving)
-- Headtrack: head looks at mouse world-position.