- 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.
219 lines
8.5 KiB
Lua
219 lines
8.5 KiB
Lua
-- 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.
|
|
|
|
local input = require("lib-core.input")
|
|
local camera = require("lib-core.camera")
|
|
local r_lib = require("lib-core.render")
|
|
local maps = require("lib-core.maps")
|
|
local puppet = require("lib-core.puppet")
|
|
|
|
local M = {}
|
|
|
|
local state = {
|
|
player = nil,
|
|
player_speed = 120,
|
|
map = nil,
|
|
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
|
|
}
|
|
|
|
-- CI hook gating (mirrors Phase 1 convention).
|
|
local CI_FRAME_PERF = (os.getenv("SPOREL_CI") == "1")
|
|
local CI_DRIVE = (os.getenv("SPOREL_VAGRANT_PHASE1_DRIVE") == "1")
|
|
local ci_frame_count = 0
|
|
local ci_t_start = nil
|
|
local ci_drive_frame = 0
|
|
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
|
|
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 })
|
|
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)
|
|
local aliases = engine.module.asset_aliases()
|
|
|
|
-- Map + sprite-tilemap.
|
|
state.map = maps.load("maps/vagrant_test.map.json")
|
|
maps.set_current(state.map)
|
|
maps.load_textures(aliases)
|
|
|
|
-- 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_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
|
|
end)
|
|
|
|
-- Furniture textures (preload to avoid first-frame load stutter).
|
|
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")
|
|
state.backpack_tex = engine.asset.load_texture(world_lib .. "/assets/world/backpack_001.png")
|
|
|
|
-- Input bindings.
|
|
input.bind("walk_left", { "a" })
|
|
input.bind("walk_right", { "d" })
|
|
input.bind("walk_up", { "w" })
|
|
input.bind("walk_down", { "s" })
|
|
input.bind("quit_to_launcher", { "escape" })
|
|
|
|
-- CI trace: assets loaded count (puppet+tilemap+furniture textures).
|
|
if CI_FRAME_PERF and not ci_assets_traced then
|
|
-- 7 puppet textures + 4 tile textures + 3 furniture textures = 14
|
|
engine.print("vagrant: assets_loaded=14")
|
|
ci_assets_traced = true
|
|
end
|
|
|
|
if CI_FRAME_PERF then
|
|
ci_t_start = engine.time.now()
|
|
end
|
|
end
|
|
|
|
function M.update(ctx, dt)
|
|
if input.was_action_pressed("quit_to_launcher") then
|
|
engine.switch_module("lib-sporel.launcher")
|
|
return
|
|
end
|
|
|
|
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)
|
|
|
|
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)
|
|
else
|
|
moving = false
|
|
end
|
|
end
|
|
|
|
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
|
|
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.
|
|
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)
|
|
|
|
-- Camera follows puppet position each frame.
|
|
local pos = puppet.position(state.player)
|
|
camera.set_target(pos.x, pos.y)
|
|
camera.update(dt)
|
|
|
|
puppet.update(dt)
|
|
|
|
if CI_FRAME_PERF and ci_t_start then
|
|
ci_frame_count = ci_frame_count + 1
|
|
if ci_frame_count == 60 then
|
|
local elapsed_ms = (engine.time.now() - ci_t_start) * 1000.0
|
|
local avg = elapsed_ms / 60.0
|
|
print(string.format("vagrant: frame_avg_ms=%.2f", avg))
|
|
engine.exit(0)
|
|
end
|
|
end
|
|
end
|
|
|
|
function M.draw(ctx)
|
|
-- Sprite-tilemap (maps.draw_map with sprite-mode + tile_rotations).
|
|
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)
|
|
engine.render.draw_sprite_transform(state.bench_tex, 400, 300, 0, 1, 1, 32, 32, 0xFFFFFFFF)
|
|
engine.render.draw_sprite_transform(state.backpack_tex, 350, 400, 0, 1, 1, 16, 16, 0xFFFFFFFF)
|
|
-- Puppet (sprite-mode via texture_handles populated by load_rig+load_textures).
|
|
puppet.render(state.player)
|
|
if CI_FRAME_PERF then
|
|
engine.print("vagrant: render_frame_ok")
|
|
if not ci_sprite_traced then
|
|
engine.print("vagrant: sprite_mode=on")
|
|
ci_sprite_traced = true
|
|
end
|
|
end
|
|
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
|
|
|
|
return M
|