feat: initial implementation v0.1.0 — single player puppet + WASD + map
Composer module: single-player character as puppet (humanoid 4-bone rig: torso, head, leg_l, leg_r) at world (320, 240) on a 20x20 v0.1-format tilemap. - WASD direct movement, normalized for diagonals, 120 px/s. - Camera-chase via lib-core.camera target-provider (camera follows puppet position frame-to-frame). - Idle and walk keyframe animations: idle is a 2s torso-sway on the upper track; walk is a 0.6s leg-cycle on the lower track. - Animation-state transitions driven by movement input: walk while WASD-active, idle when released. - Headtrack: head bone (marked look_at: true in rig) rotates each frame to face the mouse cursor's world position via puppet.set_look_target. - ESC -> launcher. CI hooks (env-var-gated, no-op in normal play): - SPOREL_CI=1: emits 'vagrant: frame_avg_ms=N' after 60 frames and 'vagrant: render_frame_ok' per frame from the render hook, then engine.exit(0). Smoke stages consume these markers.
This commit is contained in:
24
LICENSE
Normal file
24
LICENSE
Normal file
@@ -0,0 +1,24 @@
|
||||
Copyright (c) 2026 Calic. All rights reserved.
|
||||
|
||||
This software is part of the Sporel platform — **Tier 1 (Official /
|
||||
Proprietary)** content per the Three-Tier Licensing Model documented in
|
||||
`meta/docs/archive/design/vision.md §Licensing Model` (current source;
|
||||
migration to `meta/docs/architecture/licensing-model.md` pending).
|
||||
|
||||
⚠ **WIP — Legal review required before public launch.** The terms below
|
||||
reflect design intent only; the formalized license framework will be
|
||||
finalized through legal counsel before the first public release. Until
|
||||
then, this notice serves as a placeholder defending the platform owner's
|
||||
rights against unintentional re-licensing.
|
||||
|
||||
No license is granted to copy, modify, distribute, sublicense, or otherwise
|
||||
use this software in any form without prior written permission from the
|
||||
copyright holder.
|
||||
|
||||
References:
|
||||
- Tier 1 (this file): all rights reserved, proprietary, sold/distributed
|
||||
via official channels (Steam, etc.)
|
||||
- Tier 2 (Semi-Commercial Co-Development): bilateral contracts, revenue-
|
||||
share — see vision.md §Licensing Model
|
||||
- Tier 3 (Community Content): CC BY-NC-SA 4.0 + asymmetric CLA — applies
|
||||
to community-uploaded libs/modules/assets, not this repo
|
||||
48
README.md
Normal file
48
README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# sporel-module-vagrant-skeleton
|
||||
|
||||
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
|
||||
**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
|
||||
**Tags:** test-chamber, puppet, walking-sim
|
||||
|
||||
## Topology
|
||||
|
||||
<!-- topology:start (auto-generated; do not edit) -->
|
||||
<!-- topology:end -->
|
||||
|
||||
## Controls
|
||||
|
||||
- **W A S D** — direct movement (4-way, normalized for diagonals)
|
||||
- **Mouse** — head/torso look-at target
|
||||
- **ESC** — return to launcher
|
||||
|
||||
## Demonstrates
|
||||
|
||||
- lib-core.puppet integration (skeleton, keyframe animation, procedural-ready, look-at constraint)
|
||||
- idle <-> walk animation-state transition driven by movement input
|
||||
- Camera-chase via lib-core.camera target-provider
|
||||
- Throwaway-content test-chamber stance: existing maps v0.1 format,
|
||||
hand-painted humanoid rig + 2 keyframe animations.
|
||||
|
||||
## Interactions
|
||||
|
||||
- WASD active -> walk animation on lower track
|
||||
- WASD released -> idle animation on upper track (subtle torso sway)
|
||||
- Mouse moved -> head-bone angle updates each frame to face cursor's
|
||||
world-position (look-at constraint on bones marked `look_at: true`)
|
||||
|
||||
## CI Hooks
|
||||
|
||||
- `SPOREL_CI=1` -> 60-frame perf canary. Emits `vagrant: frame_avg_ms=N`
|
||||
and `vagrant: render_frame_ok` (per-frame, from render hook), then exits.
|
||||
Used by smoke stages P0-S25a + P0-S25b.
|
||||
|
||||
## References
|
||||
|
||||
- ADR-0028 (Launcher-Flow)
|
||||
- ADR-0036 (Lib-Override for multi-module reuse)
|
||||
- ADR-0037 (Tests-as-Libs)
|
||||
- ADR-0038 (API-Doc-Convention)
|
||||
11
animations/idle.anim.json
Normal file
11
animations/idle.anim.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "idle",
|
||||
"track": "upper",
|
||||
"duration": 2.0,
|
||||
"loop": true,
|
||||
"keyframes": [
|
||||
{ "t": 0.0, "torso": { "angle": 0 } },
|
||||
{ "t": 1.0, "torso": { "angle": 3 } },
|
||||
{ "t": 2.0, "torso": { "angle": 0 } }
|
||||
]
|
||||
}
|
||||
11
animations/walk.anim.json
Normal file
11
animations/walk.anim.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "walk",
|
||||
"track": "lower",
|
||||
"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 } }
|
||||
]
|
||||
}
|
||||
8
assets/tiles/vagrant_test.tilemap.json
Normal file
8
assets/tiles/vagrant_test.tilemap.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "vagrant_test",
|
||||
"tile_size": 32,
|
||||
"tiles": [
|
||||
{ "id": "grass", "walkable": true, "color": [110, 170, 80] },
|
||||
{ "id": "stone", "walkable": false, "color": [120, 120, 120] }
|
||||
]
|
||||
}
|
||||
130
init.lua
Normal file
130
init.lua
Normal file
@@ -0,0 +1,130 @@
|
||||
-- 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
|
||||
6
manifest.core
Normal file
6
manifest.core
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "vagrant-skeleton.core",
|
||||
"kind": "lib",
|
||||
"version": "0.1.0",
|
||||
"license": "Proprietary"
|
||||
}
|
||||
15
manifest.module
Normal file
15
manifest.module
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "vagrant-skeleton",
|
||||
"version": "0.1.0",
|
||||
"kind": "module",
|
||||
"api": "^0.1",
|
||||
"ci_frames": 60,
|
||||
"entry_point": "init.lua",
|
||||
"deps": [
|
||||
{"id": "lib-core.input", "version": "0.4.0"},
|
||||
{"id": "lib-core.camera", "version": "0.3.0"},
|
||||
{"id": "lib-core.render", "version": "0.1.0"},
|
||||
{"id": "lib-core.maps", "version": "0.1.1"},
|
||||
{"id": "lib-core.puppet", "version": "0.1.0"}
|
||||
]
|
||||
}
|
||||
27
maps/vagrant_test.map.json
Normal file
27
maps/vagrant_test.map.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "vagrant_test",
|
||||
"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
|
||||
]
|
||||
}
|
||||
13
rigs/humanoid.rig.json
Normal file
13
rigs/humanoid.rig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "humanoid",
|
||||
"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] }
|
||||
],
|
||||
"tracks": [
|
||||
{ "id": "lower", "bones": ["leg_l", "leg_r"] },
|
||||
{ "id": "upper", "bones": ["torso", "head"] }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user