From 3457a7eab8ba5970a434747713b6b6f2d917e906 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Mon, 18 May 2026 03:29:54 +0200 Subject: [PATCH] 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) --- README.md | 27 +++++++- animations/idle.anim.json | 6 +- animations/walk.anim.json | 9 ++- assets/tiles/vagrant_test.tilemap.json | 7 ++- init.lua | 87 ++++++++++++++++---------- maps/vagrant_test.map.json | 62 ++++++++++++------ rigs/humanoid.rig.json | 23 +++++-- 7 files changed, 151 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index bea1de5..ae39007 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ F.vagrant Tier-1 test-chamber: single player as puppet, WASD direct movement, camera-chase, headtrack to mouse-cursor. Phase 1 stage. -**Version:** 0.1.0 +**Version:** 0.2.0 **Module-ID:** vagrant-skeleton -**Requires:** lib-core.input >=0.4.0, lib-core.camera >=0.3.0, lib-core.render >=0.1.0, lib-core.maps >=0.1.1, lib-core.puppet >=0.1.0 +**Requires:** lib-core.input >=0.4.0, lib-core.camera >=0.3.0, lib-core.render >=0.1.0, lib-core.maps >=0.1.2, lib-core.puppet >=0.2.0, lib-asset.prototype-subterrain >=0.1.0 **Tags:** test-chamber, puppet, walking-sim ## Topology @@ -46,3 +46,26 @@ movement, camera-chase, headtrack to mouse-cursor. Phase 1 stage. - ADR-0036 (Lib-Override for multi-module reuse) - ADR-0037 (Tests-as-Libs) - ADR-0038 (API-Doc-Convention) + +## CHANGELOG + +### v0.2.0 +- Sprite-mode rendering throughout: 13-bone humanoid puppet via + lib-core.puppet v0.2 with PROTOTYPE asset-pack textures; sprite-tilemap + via lib-core.maps v0.1.2 with 4 floor variants (floor_001-003 walkable, + floor_004 as wall); hardcoded furniture (bed, bench, backpack) via + engine.render.draw_sprite_transform. +- Asset-pack indirection: manifest.module asset_aliases maps the alias- + keys (player, tiles, world) to lib-asset.prototype-subterrain. Rig + and tilemap atlas-ids resolved at init via puppet.load_textures and + maps.load_textures. +- Tile rotations: vagrant_test.map.json includes a tile_rotations + parallel array exercising 90 and 180-degree rotations for visual + verification of the rotation pipeline. +- Animation refinements: walk on lower track now includes foot bones + (legs + feet alternate); idle on upper track with subtle body+head + sway. +- New smoke traces emitted under SPOREL_CI=1: + vagrant: render_frame_ok (per frame from render hook), + vagrant: sprite_mode=on (once after first sprite draw), + vagrant: assets_loaded=14 (once after init). diff --git a/animations/idle.anim.json b/animations/idle.anim.json index 247ec07..1dd640b 100644 --- a/animations/idle.anim.json +++ b/animations/idle.anim.json @@ -4,8 +4,8 @@ "duration": 2.0, "loop": true, "keyframes": [ - { "t": 0.0, "torso": { "angle": 0 } }, - { "t": 1.0, "torso": { "angle": 3 } }, - { "t": 2.0, "torso": { "angle": 0 } } + { "t": 0.0, "body": {"angle": 0}, "head": {"angle": 0} }, + { "t": 1.0, "body": {"angle": 2}, "head": {"angle": -1} }, + { "t": 2.0, "body": {"angle": 0}, "head": {"angle": 0} } ] } diff --git a/animations/walk.anim.json b/animations/walk.anim.json index 9ffb729..7463c2f 100644 --- a/animations/walk.anim.json +++ b/animations/walk.anim.json @@ -4,8 +4,11 @@ "duration": 0.6, "loop": true, "keyframes": [ - { "t": 0.0, "leg_l": { "angle": -15 }, "leg_r": { "angle": 15 } }, - { "t": 0.3, "leg_l": { "angle": 15 }, "leg_r": { "angle": -15 } }, - { "t": 0.6, "leg_l": { "angle": -15 }, "leg_r": { "angle": 15 } } + { "t": 0.0, "leg_l": {"angle": -20}, "leg_r": {"angle": 20}, + "foot_l": {"angle": 10}, "foot_r": {"angle": -10} }, + { "t": 0.3, "leg_l": {"angle": 20}, "leg_r": {"angle": -20}, + "foot_l": {"angle": -10}, "foot_r": {"angle": 10} }, + { "t": 0.6, "leg_l": {"angle": -20}, "leg_r": {"angle": 20}, + "foot_l": {"angle": 10}, "foot_r": {"angle": -10} } ] } diff --git a/assets/tiles/vagrant_test.tilemap.json b/assets/tiles/vagrant_test.tilemap.json index 495fdb5..6dae3bb 100644 --- a/assets/tiles/vagrant_test.tilemap.json +++ b/assets/tiles/vagrant_test.tilemap.json @@ -1,8 +1,11 @@ { "id": "vagrant_test", "tile_size": 32, + "asset_pack": "tiles", "tiles": [ - { "id": "grass", "walkable": true, "color": [110, 170, 80] }, - { "id": "stone", "walkable": false, "color": [120, 120, 120] } + { "id": "floor_001", "walkable": true, "texture": "floor_001" }, + { "id": "floor_002", "walkable": true, "texture": "floor_002" }, + { "id": "floor_003", "walkable": true, "texture": "floor_003" }, + { "id": "wall", "walkable": false, "texture": "floor_004" } ] } diff --git a/init.lua b/init.lua index 8554107..916cfb4 100644 --- a/init.lua +++ b/init.lua @@ -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 diff --git a/maps/vagrant_test.map.json b/maps/vagrant_test.map.json index a9bb6e8..77412af 100644 --- a/maps/vagrant_test.map.json +++ b/maps/vagrant_test.map.json @@ -3,25 +3,47 @@ "tilemap": "vagrant_test", "size": { "w": 20, "h": 20 }, "tiles": [ - 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, - 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2 + 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, + 4,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,4, + 4,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,3,3,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,3,3,1,1,1,1,1,1,2,1,1,1,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4, + 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4 + ], + "tile_rotations": [ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,90,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,90,0,0,0,0,0,0,180,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ] } diff --git a/rigs/humanoid.rig.json b/rigs/humanoid.rig.json index fff6323..a0471bc 100644 --- a/rigs/humanoid.rig.json +++ b/rigs/humanoid.rig.json @@ -1,13 +1,24 @@ { "id": "humanoid", + "asset_pack": "player", "bones": [ - { "id": "torso", "parent": null, "rest": { "x": 0, "y": 0, "angle": 0 }, "color": [180, 140, 100] }, - { "id": "head", "parent": "torso", "rest": { "x": 0, "y": -16, "angle": 0 }, "look_at": true, "color": [220, 180, 140] }, - { "id": "leg_l", "parent": "torso", "rest": { "x": -4, "y": 8, "angle": 0 }, "color": [80, 60, 40] }, - { "id": "leg_r", "parent": "torso", "rest": { "x": 4, "y": 8, "angle": 0 }, "color": [80, 60, 40] } + { "id": "root", "parent": null, "rest": {"x": 0, "y": 0, "angle": 0}, "z_order": -1 }, + { "id": "body", "parent": "root", "rest": {"x": 0, "y": 0, "angle": 0}, "z_order": 2, "texture": "body_001" }, + { "id": "head", "parent": "body", "rest": {"x": 0, "y": -16, "angle": 0}, "z_order": 6, "texture": "head_001", "look_at": true }, + { "id": "shoulders", "parent": "body", "rest": {"x": 0, "y": 0, "angle": 0}, "z_order": 5, "texture": "shoulder_001" }, + { "id": "upper_arm_l", "parent": "shoulders", "rest": {"x": -19, "y": -3, "angle": 0}, "z_order": 3, "texture": "upper_arm_001", "scale": [-1, 1] }, + { "id": "upper_arm_r", "parent": "shoulders", "rest": {"x": 19, "y": -3, "angle": 0}, "z_order": 3, "texture": "upper_arm_001", "scale": [ 1, 1] }, + { "id": "lower_arm_l", "parent": "upper_arm_l", "rest": {"x": 14, "y": -18, "angle": 0}, "z_order": 2, "texture": "lower_arm_001", "scale": [-1, 1] }, + { "id": "lower_arm_r", "parent": "upper_arm_r", "rest": {"x": 14, "y": -18, "angle": 0}, "z_order": 2, "texture": "lower_arm_001", "scale": [ 1, 1] }, + { "id": "leg_l", "parent": "body", "rest": {"x": -4, "y": 8, "angle": 0}, "z_order": 1, "texture": "leg_001" }, + { "id": "leg_r", "parent": "body", "rest": {"x": 4, "y": 8, "angle": 0}, "z_order": 1, "texture": "leg_001" }, + { "id": "foot_l", "parent": "leg_l", "rest": {"x": 0, "y": 12, "angle": 0}, "z_order": 0, "texture": "foot_001" }, + { "id": "foot_r", "parent": "leg_r", "rest": {"x": 0, "y": 12, "angle": 0}, "z_order": 0, "texture": "foot_001" } ], "tracks": [ - { "id": "lower", "bones": ["leg_l", "leg_r"] }, - { "id": "upper", "bones": ["torso", "head"] } + { "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"] } ] }