feat: sprite-mode test-chamber with 13-bone puppet + sprite-tilemap

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)
This commit is contained in:
Axel Meyer
2026-05-18 03:29:54 +02:00
parent 161913795b
commit 3457a7eab8
7 changed files with 151 additions and 70 deletions

View File

@@ -1,5 +1,6 @@
-- sporel-module-vagrant-skeleton v0.1.0
-- F.vagrant Tier-1 Phase 1: single-player + puppet + WASD + maps v0.1.
-- sporel-module-vagrant-skeleton v0.2.0
-- 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")
@@ -13,15 +14,20 @@ local state = {
player = nil,
player_speed = 120,
map = nil,
bed_tex = nil,
bench_tex = nil,
backpack_tex = nil,
}
-- CI hook gating (mirrors squad-prototype convention)
-- 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 = (os.getenv("SPOREL_VAGRANT_PHASE1_DRIVE") == "1")
local ci_drive_frame = 0
local ci_last_state = nil
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"
@@ -43,29 +49,41 @@ local function set_walk_or_idle(moving)
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)
-- Load puppet rig + animations
local rig = puppet.load_rig("rigs/humanoid.rig.json")
-- 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 idle = puppet.load_animation("animations/idle.anim.json", rig)
local walk = puppet.load_animation("animations/walk.anim.json", rig)
puppet.register_animation(state.player, idle)
puppet.register_animation(state.player, walk)
puppet.play(state.player, "idle", { loop = true })
-- Input bindings (WASD direct movement + ESC)
-- 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" })
-- Camera follows player on first frame
local init_pos = puppet.position(state.player)
camera.set_target(init_pos.x, init_pos.y)
-- 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()
@@ -85,17 +103,9 @@ function M.update(ctx, dt)
if input.is_action_down("walk_down") then dy = dy + 1 end
local moving = (dx ~= 0 or dy ~= 0)
if moving then
local len = math.sqrt(dx * dx + dy * dy)
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
if CI_DRIVE then
ci_drive_frame = ci_drive_frame + 1
-- Frames 1-20: idle (no move), 21-40: move-right, 41-60: idle again
if ci_drive_frame > 20 and ci_drive_frame <= 40 then
dx = 1; dy = 0
moving = true
@@ -107,21 +117,27 @@ function M.update(ctx, dt)
end
end
if moving and not CI_DRIVE then
local len = math.sqrt(dx * dx + dy * dy)
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
set_walk_or_idle(moving)
-- Headtrack: head looks at mouse world-position
-- 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 player position each frame (vagrant-style no-pan)
local cam_pos = puppet.position(state.player)
camera.set_target(cam_pos.x, cam_pos.y)
-- 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)
-- CI: perf canary. After 60 frames emit avg ms, exit.
if CI_FRAME_PERF and ci_t_start then
ci_frame_count = ci_frame_count + 1
if ci_frame_count == 60 then
@@ -134,21 +150,24 @@ function M.update(ctx, dt)
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)
-- Render-hook trace for smoke (P0-S25a). MUST emit from render(), not update().
-- Lesson from P.4.c: smoke stages that exit in update never prove the render
-- hook works.
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 hooks (global functions; engine calls these by name)
-- See P.4.c discovery: engine looks up lua_getglobal "init", "update",
-- "render" — not M.init etc. The module-table pattern is bridged here.
-- ====================================================================
-- 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