diff --git a/animations/walk_back_lower.anim.json b/animations/walk_back_lower.anim.json new file mode 100644 index 0000000..9a9b64a --- /dev/null +++ b/animations/walk_back_lower.anim.json @@ -0,0 +1,28 @@ +{ + "id": "walk_back_lower", + "track": "lower", + "duration": 0.8, + "loop": true, + "keyframes": [ + { "t": 0.0, + "leg_l": {"scl": [-1, 0]}, "leg_r": {"scl": [ 1, 0]}, + "foot_l": {"pos": [0, 0]}, "foot_r": {"pos": [0, 0]} + }, + { "t": 0.2, + "leg_l": {"scl": [-1, -1]}, "leg_r": {"scl": [ 1, 1]}, + "foot_l": {"pos": [-0.044, 25.47]}, "foot_r": {"pos": [0.249, 25.85]} + }, + { "t": 0.4, + "leg_l": {"scl": [-1, 0]}, "leg_r": {"scl": [ 1, 0]}, + "foot_l": {"pos": [0, 0]}, "foot_r": {"pos": [0, 0]} + }, + { "t": 0.6, + "leg_l": {"scl": [-1, 1]}, "leg_r": {"scl": [ 1, -1]}, + "foot_l": {"pos": [ 0.044, -25.47]}, "foot_r": {"pos": [-0.249, -25.85]} + }, + { "t": 0.8, + "leg_l": {"scl": [-1, 0]}, "leg_r": {"scl": [ 1, 0]}, + "foot_l": {"pos": [0, 0]}, "foot_r": {"pos": [0, 0]} + } + ] +} diff --git a/animations/walk.anim.json b/animations/walk_fwd_lower.anim.json similarity index 97% rename from animations/walk.anim.json rename to animations/walk_fwd_lower.anim.json index 5ad8d0a..d18896b 100644 --- a/animations/walk.anim.json +++ b/animations/walk_fwd_lower.anim.json @@ -1,5 +1,5 @@ { - "id": "walk", + "id": "walk_fwd_lower", "track": "lower", "duration": 0.8, "loop": true, diff --git a/animations/walk_left_lower.anim.json b/animations/walk_left_lower.anim.json new file mode 100644 index 0000000..d9b4a24 --- /dev/null +++ b/animations/walk_left_lower.anim.json @@ -0,0 +1,28 @@ +{ + "id": "walk_left_lower", + "track": "lower", + "duration": 0.8, + "loop": true, + "keyframes": [ + { "t": 0.0, + "leg_l": {"scl": [-1, 0]}, "leg_r": {"scl": [ 1, 0]}, + "foot_l": {"pos": [0, 0]}, "foot_r": {"pos": [0, 0]} + }, + { "t": 0.2, + "leg_l": {"scl": [-1, -1]}, "leg_r": {"scl": [ 1, 1]}, + "foot_l": {"pos": [-0.044, 25.47]}, "foot_r": {"pos": [0.249, 25.85]} + }, + { "t": 0.4, + "leg_l": {"scl": [-1, 0]}, "leg_r": {"scl": [ 1, 0]}, + "foot_l": {"pos": [0, 0]}, "foot_r": {"pos": [0, 0]} + }, + { "t": 0.6, + "leg_l": {"scl": [-1, 1]}, "leg_r": {"scl": [ 1, -1]}, + "foot_l": {"pos": [ 0.044, -25.47]}, "foot_r": {"pos": [-0.249, -25.85]} + }, + { "t": 0.8, + "leg_l": {"scl": [-1, 0]}, "leg_r": {"scl": [ 1, 0]}, + "foot_l": {"pos": [0, 0]}, "foot_r": {"pos": [0, 0]} + } + ] +} diff --git a/animations/walk_right_lower.anim.json b/animations/walk_right_lower.anim.json new file mode 100644 index 0000000..4609a84 --- /dev/null +++ b/animations/walk_right_lower.anim.json @@ -0,0 +1,28 @@ +{ + "id": "walk_right_lower", + "track": "lower", + "duration": 0.8, + "loop": true, + "keyframes": [ + { "t": 0.0, + "leg_l": {"scl": [-1, 0]}, "leg_r": {"scl": [ 1, 0]}, + "foot_l": {"pos": [0, 0]}, "foot_r": {"pos": [0, 0]} + }, + { "t": 0.2, + "leg_l": {"scl": [-1, -1]}, "leg_r": {"scl": [ 1, 1]}, + "foot_l": {"pos": [-0.044, 25.47]}, "foot_r": {"pos": [0.249, 25.85]} + }, + { "t": 0.4, + "leg_l": {"scl": [-1, 0]}, "leg_r": {"scl": [ 1, 0]}, + "foot_l": {"pos": [0, 0]}, "foot_r": {"pos": [0, 0]} + }, + { "t": 0.6, + "leg_l": {"scl": [-1, 1]}, "leg_r": {"scl": [ 1, -1]}, + "foot_l": {"pos": [ 0.044, -25.47]}, "foot_r": {"pos": [-0.249, -25.85]} + }, + { "t": 0.8, + "leg_l": {"scl": [-1, 0]}, "leg_r": {"scl": [ 1, 0]}, + "foot_l": {"pos": [0, 0]}, "foot_r": {"pos": [0, 0]} + } + ] +} diff --git a/init.lua b/init.lua index 8edcc68..87e38d7 100644 --- a/init.lua +++ b/init.lua @@ -1,6 +1,8 @@ --- sporel-module-vagrant-skeleton v0.3.2 +-- sporel-module-vagrant-skeleton v0.4.0 -- Sprite-mode test-chamber: 13-bone humanoid puppet + sprite-tilemap + --- hardcoded furniture. F.vagrant Tier-1 + Sprite-Upgrade-Bridge state. +-- hardcoded furniture. Subterrain-style puppet control model: +-- multi-look-target (aim/soft), slerp body rotation, virtual lower_control bone, +-- 4 directional walk animations, HandleAnimationLower port. local input = require("lib-core.input") local camera = require("lib-core.camera") @@ -17,10 +19,11 @@ local state = { bed_tex = nil, bench_tex = nil, backpack_tex = nil, - 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 + -- Subterrain-style controller state: + walk_anim_current = nil, -- "walk_fwd_lower" / "walk_back_lower" / etc. (nil = idle_lower) + lower_control_rot_deg = 0, + move_x = 0, + move_y = 0, } -- CI hook gating (mirrors Phase 1 convention). @@ -33,23 +36,90 @@ local ci_last_state = nil local ci_sprite_traced = false local ci_assets_traced = false -local function set_walk_or_idle(moving) - local new_state = moving and "walk" or "idle" - if moving then +-- All 4 directional walk animation ids. +local WALK_ANIMS = {"walk_fwd_lower", "walk_back_lower", "walk_left_lower", "walk_right_lower"} + +-- Signed angle between two 2D vectors (in degrees, range [-180, 180]). +-- Returns the signed angle from v1 to v2, positive = counter-clockwise (left). +local function signed_angle_2d(v1x, v1y, v2x, v2y) + local cross = v1x * v2y - v1y * v2x + local dot = v1x * v2x + v1y * v2y + return math.deg(math.atan(cross, dot)) +end + +-- Given signed angle num_deg from body-forward to move-direction, pick the +-- appropriate walk animation and compute the lower_control offset rotation (degrees). +-- Substrate convention: body-forward = sprite top = -Y at rot=0. +local function pick_walk_anim(num_deg) + if num_deg >= -45 and num_deg <= 45 then + -- Movement roughly aligns with body-forward = backpedaling + return "walk_back_lower", math.max(-30, math.min(30, num_deg)) + elseif num_deg > 45 and num_deg < 135 then + -- Movement ~90° left of body-forward = strafe left + return "walk_left_lower", math.max(60, math.min(120, num_deg)) - 90 + elseif num_deg > -135 and num_deg < -45 then + -- Movement ~90° right of body-forward = strafe right + return "walk_right_lower", math.max(-120, math.min(-60, num_deg)) + 90 + elseif num_deg > 0 then + -- Movement roughly opposite body-forward, positive side = walk forward + return "walk_fwd_lower", -(30 - (math.max(150, math.min(180, num_deg)) - 150)) + else + -- Movement roughly opposite body-forward, negative side = walk forward + return "walk_fwd_lower", 30 + (math.max(-180, math.min(-150, num_deg)) + 150) + end +end + +-- HandleAnimationLower port: chooses walk anim based on signed angle between +-- body-forward and move-direction. Manages idle_lower / walk_* transitions. +local function update_lower_anim(moving, move_x, move_y) + if not moving then + -- Idle: stop all walk anims, resume idle_lower. + if state.walk_anim_current ~= nil then + for _, a in ipairs(WALK_ANIMS) do + puppet.stop(state.player, a) + end + state.walk_anim_current = nil + puppet.play(state.player, "idle_lower", { loop = true }) + puppet.stop(state.player, "walk_upper") + if not puppet.is_playing(state.player, "idle") then + puppet.play(state.player, "idle", { loop = true }) + end + if CI_FRAME_PERF and ci_last_state ~= "idle" then + engine.print("vagrant: state=idle") + ci_last_state = "idle" + end + end + state.lower_control_rot_deg = 0 + return + end + + -- Walking: read body's previous-frame world.rot for signed angle computation. + local _, _, body_rot = puppet.bone_world_transform(state.player, "body") + -- Body forward direction (sprite-top-up convention): forward = -Y at rot=0. + -- In world coords: fwd_x = sin(rot), fwd_y = -cos(rot). + local fwd_x = math.sin(body_rot) + local fwd_y = -math.cos(body_rot) + local num = signed_angle_2d(fwd_x, fwd_y, move_x, move_y) + local anim_id, wedge_deg = pick_walk_anim(num) + state.lower_control_rot_deg = wedge_deg + + if state.walk_anim_current ~= anim_id then + -- Swap to new walk anim. + for _, a in ipairs(WALK_ANIMS) do + if a ~= anim_id then puppet.stop(state.player, a) end + end + puppet.stop(state.player, "idle_lower") + puppet.stop(state.player, "idle") + puppet.play(state.player, anim_id, { loop = true }) if not puppet.is_playing(state.player, "walk_upper") then - puppet.stop(state.player, "idle") puppet.play(state.player, "walk_upper", { loop = true }) end - else - if not puppet.is_playing(state.player, "idle") then - puppet.stop(state.player, "walk_upper") - puppet.play(state.player, "idle", { loop = true }) + state.walk_anim_current = anim_id + if CI_FRAME_PERF and ci_last_state ~= "walk" then + engine.print("vagrant: state=walk") + ci_last_state = "walk" end end - if CI_FRAME_PERF and new_state ~= ci_last_state then - engine.print("vagrant: state=" .. new_state) - ci_last_state = new_state - end end function M.init(ctx) @@ -60,49 +130,33 @@ function M.init(ctx) maps.set_current(state.map) maps.load_textures(aliases) - -- Puppet rig + sprite textures + animations. - local rig = puppet.load_rig("rigs/humanoid.rig.json", aliases) + -- Puppet rig + sprite textures. + local rig = puppet.load_rig("rigs/humanoid.rig.json", aliases) state.player = puppet.spawn(rig, { x = 320, y = 240 }) - local walk_upper = puppet.load_animation("animations/walk_upper.anim.json", rig) - local idle = puppet.load_animation("animations/idle.anim.json", rig) - puppet.register_animation(state.player, walk_upper) - puppet.register_animation(state.player, idle) - puppet.play(state.player, "idle", { loop = true }) - -- 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 + -- Load all animations. + local walk_fwd = puppet.load_animation("animations/walk_fwd_lower.anim.json", rig) + local walk_back = puppet.load_animation("animations/walk_back_lower.anim.json", rig) + local walk_left = puppet.load_animation("animations/walk_left_lower.anim.json", rig) + local walk_right = puppet.load_animation("animations/walk_right_lower.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) + + for _, a in ipairs({walk_fwd, walk_back, walk_left, walk_right, walk_upper, idle, idle_lower}) do + puppet.register_animation(state.player, a) + end + + -- Start in idle state. + puppet.play(state.player, "idle", { loop = true }) + puppet.play(state.player, "idle_lower", { loop = true }) + + -- Procedural lower_control: applies the per-frame wedge rotation (set by update_lower_anim). + puppet.set_procedural(state.player, "lower_control", function(handle, dt) + puppet.write_bone(handle, "lower_control", { rot = state.lower_control_rot_deg }) end) - -- Furniture textures (preload to avoid first-frame load stutter). + -- Furniture textures. local world_lib = aliases.world state.bed_tex = engine.asset.load_texture(world_lib .. "/assets/world/bed_001.png") state.bench_tex = engine.asset.load_texture(world_lib .. "/assets/world/bench_001.png") @@ -133,21 +187,20 @@ function M.update(ctx, dt) return end + -- WASD input → normalized move vector. local dx, dy = 0, 0 if input.is_action_down("walk_left") then dx = dx - 1 end if input.is_action_down("walk_right") then dx = dx + 1 end if input.is_action_down("walk_up") then dy = dy - 1 end if input.is_action_down("walk_down") then dy = dy + 1 end - local moving = (dx ~= 0 or dy ~= 0) + -- CI_DRIVE: synthetic scripted move (preserve existing P0-S25b semantics). if CI_DRIVE then ci_drive_frame = ci_drive_frame + 1 if ci_drive_frame > 20 and ci_drive_frame <= 40 then dx = 1; dy = 0 moving = true - 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) @@ -156,23 +209,33 @@ function M.update(ctx, dt) end end + -- Move puppet. if moving and not CI_DRIVE then local len = math.sqrt(dx * dx + dy * dy) - -- 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 + state.move_x = dx / len + state.move_y = dy / len 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) + pos.x + state.move_x * state.player_speed * dt, + pos.y + state.move_y * state.player_speed * dt) + elseif not moving then + state.move_x = 0 + state.move_y = 0 end - state.is_walking = moving - set_walk_or_idle(moving) - -- Headtrack: head looks at mouse world-position. + -- CI_DRIVE move_x/move_y for anim selection. + if CI_DRIVE and moving then + state.move_x = dx + state.move_y = dy + end + + update_lower_anim(moving, state.move_x, state.move_y) + + -- Look-targets: head tracks raw mouse (aim), body tracks same for now (soft). local mx, my = engine.input.get_mouse_pos() local wx, wy = camera.screen_to_world(mx, my) - puppet.set_look_target(state.player, wx, wy) + puppet.set_look_target(state.player, "aim", wx, wy) + puppet.set_look_target(state.player, "soft", wx, wy) -- Camera follows puppet position each frame. local pos = puppet.position(state.player) @@ -194,7 +257,7 @@ end function M.draw(ctx) camera.begin() - -- Sprite-tilemap (maps.draw_map with sprite-mode + tile_rotations). + -- Sprite-tilemap. r_lib.draw_map() -- Hardcoded furniture placements (test-chamber spirit). engine.render.draw_sprite_transform(state.bed_tex, 200, 150, 0, 1, 1, 32, 32, 0xFFFFFFFF) @@ -213,8 +276,8 @@ function M.draw(ctx) end -- Engine lifecycle globals (engine looks up by name; module-table is bridged). -function init(ctx) M.init(ctx) end -function update(ctx, dt) M.update(ctx, dt) end -function render(ctx) M.draw(ctx) end +function init(ctx) M.init(ctx) end +function update(ctx, dt) M.update(ctx, dt) end +function render(ctx) M.draw(ctx) end return M diff --git a/rigs/humanoid.rig.json b/rigs/humanoid.rig.json index 904c222..ce1c8e1 100644 --- a/rigs/humanoid.rig.json +++ b/rigs/humanoid.rig.json @@ -2,18 +2,19 @@ "id": "humanoid", "asset_pack": "player", "bones": [ - { "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", "look_at": true, "look_at_speed": 180 }, - { "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, "sprite_rot": -90 }, - { "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_r", "parent": "shoulders", "rest": {"x": 20, "y": -4, "rot": 0}, "z_order": 3, "texture": "upper_arm_001" }, - { "id": "lower_arm_l", "parent": "upper_arm_l","rest": {"x": 14, "y": -18, "rot": 0}, "z_order": 2, "texture": "lower_arm_001" }, - { "id": "lower_arm_r", "parent": "upper_arm_r","rest": {"x": 13, "y": -17, "rot": 0}, "z_order": 2, "texture": "lower_arm_001" }, - { "id": "leg_l", "parent": "root", "rest": {"x": -9, "y": 1, "rot": 0, "scl": [-1, -1]}, "z_order": 1, "texture": "leg_001" }, - { "id": "leg_r", "parent": "root", "rest": {"x": 9, "y": 1, "rot": 0, "scl": [ 1, -1]}, "z_order": 1, "texture": "leg_001" }, - { "id": "foot_l", "parent": "leg_l", "rest": {"x": 0, "y": 25, "rot": 0, "scl": [ 1, -1]}, "z_order": 0, "texture": "foot_001" }, - { "id": "foot_r", "parent": "leg_r", "rest": {"x": 0, "y": 26, "rot": 0, "scl": [ 1, -1]}, "z_order": 0, "texture": "foot_001" } + { "id": "root", "parent": null, "rest": {"x": 0, "y": 0, "rot": 0}, "z_order": -1 }, + { "id": "lower_control", "parent": "root", "rest": {"x": 0, "y": 0, "rot": 0}, "virtual": true }, + { "id": "body", "parent": "root", "rest": {"x": 0, "y": 0, "rot": 0}, "z_order": 2, "texture": "body_001", "look_at": "soft", "look_at_slerp": 6 }, + { "id": "head", "parent": "body", "rest": {"x": 0, "y": -3, "rot": 0, "rot_min": -60, "rot_max": 60}, "z_order": 6, "texture": "head_001", "look_at": "aim", "sprite_rot": -90 }, + { "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_r", "parent": "shoulders", "rest": {"x": 20, "y": -4, "rot": 0}, "z_order": 3, "texture": "upper_arm_001" }, + { "id": "lower_arm_l", "parent": "upper_arm_l", "rest": {"x": 14, "y": -18, "rot": 0}, "z_order": 2, "texture": "lower_arm_001" }, + { "id": "lower_arm_r", "parent": "upper_arm_r", "rest": {"x": 13, "y": -17, "rot": 0}, "z_order": 2, "texture": "lower_arm_001" }, + { "id": "leg_l", "parent": "lower_control", "rest": {"x": -9, "y": 1, "rot": 0, "scl": [-1, -1]}, "z_order": 1, "texture": "leg_001" }, + { "id": "leg_r", "parent": "lower_control", "rest": {"x": 9, "y": 1, "rot": 0, "scl": [ 1, -1]}, "z_order": 1, "texture": "leg_001" }, + { "id": "foot_l", "parent": "leg_l", "rest": {"x": 0, "y": 25, "rot": 0, "scl": [ 1, -1]}, "z_order": 0, "texture": "foot_001" }, + { "id": "foot_r", "parent": "leg_r", "rest": {"x": 0, "y": 26, "rot": 0, "scl": [ 1, -1]}, "z_order": 0, "texture": "foot_001" } ], "tracks": [ { "id": "lower", "bones": ["leg_l", "leg_r", "foot_l", "foot_r"] },