fix(vagrant): iteration — body+head look-at, WASD-driven leg-facing, idle-collapse

- 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>
This commit is contained in:
Axel Meyer
2026-05-18 19:51:56 +02:00
parent 00d10bbd47
commit 03a536984d
4 changed files with 36 additions and 7 deletions

View File

@@ -0,0 +1,13 @@
{
"id": "idle_lower",
"track": "lower",
"duration": 1.0,
"loop": true,
"keyframes": [
{ "t": 0.0,
"leg_l": {"scl": [-1, 0]}, "leg_r": {"scl": [ 1, 0]},
"foot_l": {"scl": [ 1, 0], "pos": [0, 0]},
"foot_r": {"scl": [ 1, 0], "pos": [0, 0]}
}
]
}

View File

@@ -1,4 +1,4 @@
-- sporel-module-vagrant-skeleton v0.2.0 -- sporel-module-vagrant-skeleton v0.3.1
-- Sprite-mode test-chamber: 13-bone humanoid puppet + sprite-tilemap + -- Sprite-mode test-chamber: 13-bone humanoid puppet + sprite-tilemap +
-- hardcoded furniture. F.vagrant Tier-1 + Sprite-Upgrade-Bridge state. -- hardcoded furniture. F.vagrant Tier-1 + Sprite-Upgrade-Bridge state.
@@ -17,6 +17,7 @@ local state = {
bed_tex = nil, bed_tex = nil,
bench_tex = nil, bench_tex = nil,
backpack_tex = nil, backpack_tex = nil,
last_walk_rot_deg = 0, -- persisted root.rot (degrees) across idle frames
} }
-- CI hook gating (mirrors Phase 1 convention). -- CI hook gating (mirrors Phase 1 convention).
@@ -34,6 +35,7 @@ local function set_walk_or_idle(moving)
if moving then if moving then
if not puppet.is_playing(state.player, "walk") then if not puppet.is_playing(state.player, "walk") then
puppet.stop(state.player, "idle") puppet.stop(state.player, "idle")
puppet.stop(state.player, "idle_lower")
puppet.play(state.player, "walk", { loop = true }) puppet.play(state.player, "walk", { loop = true })
puppet.play(state.player, "walk_upper", { loop = true }) puppet.play(state.player, "walk_upper", { loop = true })
end end
@@ -42,6 +44,7 @@ local function set_walk_or_idle(moving)
puppet.stop(state.player, "walk") puppet.stop(state.player, "walk")
puppet.stop(state.player, "walk_upper") puppet.stop(state.player, "walk_upper")
puppet.play(state.player, "idle", { loop = true }) puppet.play(state.player, "idle", { loop = true })
puppet.play(state.player, "idle_lower", { loop = true })
end end
end end
if CI_FRAME_PERF and new_state ~= ci_last_state then if CI_FRAME_PERF and new_state ~= ci_last_state then
@@ -64,10 +67,20 @@ function M.init(ctx)
local walk = puppet.load_animation("animations/walk.anim.json", rig) local walk = puppet.load_animation("animations/walk.anim.json", rig)
local walk_upper = puppet.load_animation("animations/walk_upper.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 = 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, idle)
puppet.register_animation(state.player, walk) puppet.register_animation(state.player, walk)
puppet.register_animation(state.player, walk_upper) 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", { loop = true })
puppet.play(state.player, "idle_lower", { 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 })
end)
-- Furniture textures (preload to avoid first-frame load stutter). -- Furniture textures (preload to avoid first-frame load stutter).
local world_lib = aliases.world local world_lib = aliases.world
@@ -113,6 +126,7 @@ function M.update(ctx, dt)
if ci_drive_frame > 20 and ci_drive_frame <= 40 then if ci_drive_frame > 20 and ci_drive_frame <= 40 then
dx = 1; dy = 0 dx = 1; dy = 0
moving = true moving = true
state.last_walk_rot_deg = math.deg(math.atan(dx, -dy)) -- atan(1, 0) = pi/2 = 90deg
local pos = puppet.position(state.player) local pos = puppet.position(state.player)
puppet.move_to(state.player, puppet.move_to(state.player,
pos.x + dx * state.player_speed * dt, pos.y) pos.x + dx * state.player_speed * dt, pos.y)
@@ -123,6 +137,8 @@ function M.update(ctx, dt)
if moving and not CI_DRIVE then if moving and not CI_DRIVE then
local len = math.sqrt(dx * dx + dy * dy) 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))
local pos = puppet.position(state.player) local pos = puppet.position(state.player)
puppet.move_to(state.player, puppet.move_to(state.player,
pos.x + dx / len * state.player_speed * dt, pos.x + dx / len * state.player_speed * dt,

View File

@@ -15,7 +15,7 @@
{"id": "lib-core.camera", "version": "0.3.0"}, {"id": "lib-core.camera", "version": "0.3.0"},
{"id": "lib-core.render", "version": "0.1.0"}, {"id": "lib-core.render", "version": "0.1.0"},
{"id": "lib-core.maps", "version": "0.1.2"}, {"id": "lib-core.maps", "version": "0.1.2"},
{"id": "lib-core.puppet", "version": "0.3.0"}, {"id": "lib-core.puppet", "version": "0.3.1"},
{"id": "lib-asset.prototype-subterrain", "version": "0.1.1"} {"id": "lib-asset.prototype-subterrain", "version": "0.1.1"}
] ]
} }

View File

@@ -3,8 +3,8 @@
"asset_pack": "player", "asset_pack": "player",
"bones": [ "bones": [
{ "id": "root", "parent": null, "rest": {"x": 0, "y": 0, "rot": 0}, "z_order": -1 }, { "id": "root", "parent": null, "rest": {"x": 0, "y": 0, "rot": 0}, "z_order": -1 },
{ "id": "body", "parent": "root", "rest": {"x": 0, "y": 0, "rot": 0}, "z_order": 2, "texture": "body_001" }, { "id": "body", "parent": "root", "rest": {"x": 0, "y": 0, "rot": 0}, "z_order": 2, "texture": "body_001", "look_at": true, "look_at_speed": 180 },
{ "id": "head", "parent": "body", "rest": {"x": 0, "y": -3, "rot": 0}, "z_order": 6, "texture": "head_001", "look_at": true }, { "id": "head", "parent": "body", "rest": {"x": 0, "y": -3, "rot": 0, "rot_min": -90, "rot_max": 90}, "z_order": 6, "texture": "head_001", "look_at": true, "look_at_speed": 480 },
{ "id": "shoulders", "parent": "body", "rest": {"x": 0, "y": 0, "rot": 0}, "z_order": 5, "texture": "shoulder_001" }, { "id": "shoulders", "parent": "body", "rest": {"x": 0, "y": 0, "rot": 0}, "z_order": 5, "texture": "shoulder_001" },
{ "id": "upper_arm_l", "parent": "shoulders", "rest": {"x": -19, "y": -3, "rot": 0, "scl": [-1, 1]}, "z_order": 3, "texture": "upper_arm_001" }, { "id": "upper_arm_l", "parent": "shoulders", "rest": {"x": -19, "y": -3, "rot": 0, "scl": [-1, 1]}, "z_order": 3, "texture": "upper_arm_001" },
{ "id": "upper_arm_r", "parent": "shoulders", "rest": {"x": 20, "y": -4, "rot": 0}, "z_order": 3, "texture": "upper_arm_001" }, { "id": "upper_arm_r", "parent": "shoulders", "rest": {"x": 20, "y": -4, "rot": 0}, "z_order": 3, "texture": "upper_arm_001" },
@@ -17,7 +17,7 @@
], ],
"tracks": [ "tracks": [
{ "id": "lower", "bones": ["leg_l", "leg_r", "foot_l", "foot_r"] }, { "id": "lower", "bones": ["leg_l", "leg_r", "foot_l", "foot_r"] },
{ "id": "upper", "bones": ["root", "body", "head", "shoulders", "upper_arm_l", "upper_arm_r", "lower_arm_l", "lower_arm_r"] } { "id": "upper", "bones": ["body", "head", "shoulders", "upper_arm_l", "upper_arm_r", "lower_arm_l", "lower_arm_r"] }
], ],
"locomotion": { "locomotion": {
"mode": "procedural", "mode": "procedural",