-- sporel-module-vagrant-skeleton v0.1.0 -- F.vagrant Tier-1 Phase 1: single-player + puppet + WASD + maps v0.1. 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, } -- CI hook gating (mirrors squad-prototype convention) local CI_FRAME_PERF = (os.getenv("SPOREL_CI") == "1") local ci_frame_count = 0 local ci_t_start = nil local function set_walk_or_idle(moving) if moving then if not puppet.is_playing(state.player, "walk") then puppet.stop(state.player, "idle") puppet.play(state.player, "walk", { loop = true }) end else if not puppet.is_playing(state.player, "idle") then puppet.stop(state.player, "walk") puppet.play(state.player, "idle", { loop = true }) end end end function M.init(ctx) state.map = maps.load("maps/vagrant_test.map.json") maps.set_current(state.map) -- Load puppet rig + animations local rig = puppet.load_rig("rigs/humanoid.rig.json") 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) 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 position (no manual pan in vagrant-style) camera.set_target_provider(function() return puppet.position(state.player) 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 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 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.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 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) r_lib.draw_map() 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") 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. -- ==================================================================== 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