From e9085c03179ed03f5f924404e283c708e2d79c24 Mon Sep 17 00:00:00 2001 From: calic Date: Tue, 28 Jul 2026 12:01:45 +0000 Subject: [PATCH] =?UTF-8?q?refactor(vagrant):=20v0.17.0=20=E2=80=94=20extr?= =?UTF-8?q?act=20Stone-Age=20content=20to=20content/stone=5Fage.lua=20(T1-?= =?UTF-8?q?modder=20content=20packs=20via=20content=20facade)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +- content/stone_age.lua | 95 ++++++++++++++ init.lua | 283 +++++++++--------------------------------- manifest.module | 2 +- 4 files changed, 163 insertions(+), 228 deletions(-) create mode 100644 content/stone_age.lua diff --git a/README.md b/README.md index bb94359..878018e 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,20 @@ callback, foot-body-orientation ("tactical twist"), scale-deformation walk cycle, and inheritance-decoupled foot sprites. End-to-end inventory pickup/drop demo: pick up a rock into a backpack container, drop it back into the world. -**Version:** 0.16.0 +**Version:** 0.17.0 **Module-ID:** vagrant-skeleton **Requires:** lib-core.input >=0.4.0, lib-core.camera >=0.3.0, lib-core.render >=0.2.0, lib-core.maps >=0.1.2, lib-core.puppet >=0.4.4, lib-core.interaction >=0.1.0, lib-core.composition >=0.3.0, lib-core.actor >=0.1.0, lib-core.inventory-list >=0.1.0, lib-core.panel >=0.2.0, lib-core.inventory-list-display >=0.1.0, lib-core.notify >=0.1.0, lib-core.notify-display >=0.1.0, lib-core.world-overlay >=0.1.0, lib-core.crafting >=0.3.0, lib-core.crafting-display >=0.2.0, lib-asset.prototype-subterrain >=0.2.0, lib-asset.prototype-tc-basics >=0.1.0 **Tags:** test-chamber, puppet, walking-sim, interaction +> **v0.17.0:** **T1-modder content packs.** Stone-Age items + recipes + world- +> spawns moved OUT of `init.lua` into `content/stone_age.lua` — pure declarative +> data (`c.T{}` item, `c.R{}` recipe, `c.S{}` spawn), loaded via a content facade +> in `init.lua` (`engine.module.dir_of` + `dofile`, the sanctioned multi-file +> pattern). `c.T` auto-registers each craftable's crafting-panel icon from its +> sprite. A T1 modder now adds/edits Stone-Age content by touching only the pack +> file, never engine wiring. Behaviour is unchanged (regression-verified); also +> fixes crafted `sharp_stone` rendering (was a "?" placeholder before). +> > **v0.16.0:** Stone-Age tech chain — a self-bootstrapping crafting graph on > the v0.3 API. `knap_sharp_stone` (rock → sharp_stone, bare-handed) yields a > cutting tool that feeds BOTH `saw_planks` (as the non-consumed tool) AND diff --git a/content/stone_age.lua b/content/stone_age.lua new file mode 100644 index 0000000..22ec555 --- /dev/null +++ b/content/stone_age.lua @@ -0,0 +1,95 @@ +-- ===================================================================== +-- content/stone_age.lua — T1-modder content pack: Stone-Age items + recipes. +-- +-- PURE CONTENT. No engine wiring, no sprite/atlas plumbing — just the +-- authoring API `c` passed in by the module (see init.lua's content facade): +-- c.T{ id, name, desc, weight, vol, material={...}, affordance={...}, +-- atlas="tc"|"stone", uv="", scale? } -- define an item +-- c.R{ ...crafting.define_recipe def... } -- define a recipe +-- c.S{ template, x, y } -- place item in world +-- `c.T` auto-registers the crafting-panel icon from the item's own sprite. +-- +-- A T1 modder adds Stone-Age content by editing THIS file only — never +-- init.lua. Drop a sibling `content/.lua` + list it in init.lua's +-- CONTENT_PACKS to add a new pack. +-- +-- Self-bootstrapping graph: +-- rock --knap--> sharp_stone --+--(tool)--> saw_planks (log -> 4 planks) +-- +--(head)--> stone_axe +-- plant_fiber x3 --twist--> cordage --(binding)--> stone_axe +-- stick --------------------------(haft)--------> stone_axe +-- ===================================================================== +return function(c) + -- ---- items ------------------------------------------------------- + -- World-gathered + c.T{ id = "log", name = "Log", + desc = "A heavy wooden log. Saw it into planks with a cutting edge.", + weight = 20, vol = 0.05, material = { wood = 1.0 }, + atlas = "tc", uv = "log_pile1" } + + c.T{ id = "sharp_stone", name = "Sharp Stone", + desc = "A knapped stone with a cutting edge. A tool — not consumed by sawing.", + weight = 1, vol = 0.0004, material = { stone = 1.0 }, + affordance = { cutting = true }, atlas = "tc", uv = "spade" } + + c.T{ id = "plant_fiber", name = "Plant Fiber", + desc = "A strand of tough plant fiber. Twist three into cordage.", + weight = 0.05, vol = 0.0002, material = { fiber = 1.0 }, + atlas = "tc", uv = "sack1", scale = 0.6 } + + -- Crafted outputs + c.T{ id = "plank", name = "Plank", + desc = "A sawn wooden plank.", + weight = 2, vol = 0.004, material = { wood = 1.0 }, -- light: not re-sawable (<5) + atlas = "tc", uv = "bench1" } + + c.T{ id = "cordage", name = "Cordage", + desc = "A length of twisted plant-fiber cord.", + weight = 0.1, vol = 0.0003, material = { fiber = 1.0 }, + atlas = "tc", uv = "sack1" } + + c.T{ id = "stone_axe", name = "Stone Axe", + desc = "A hafted stone axe. Chops wood.", + weight = 1.8, vol = 0.0012, + material = { stone = 0.6, wood = 0.3, fiber = 0.1 }, + affordance = { chopping = true }, atlas = "tc", uv = "spade" } + + -- ---- recipes ----------------------------------------------------- + -- saw_planks: input matched by MATERIAL (mostly wood AND heavy), a light + -- plank fails weight>5. Needs a cutting TOOL (not consumed). ×4 output. + c.R{ id = "saw_planks", + inputs = { { match = { ["composition.wood"] = ">0.5", weight = ">5" }, count = 1 } }, + tools = { { match = { ["affordance.cutting"] = true } } }, + outputs = { { template = "plank", count = 4 } }, + name = "Saw Planks", + description = "Saw a wooden log into four planks (needs a cutting edge)." } + + c.R{ id = "knap_sharp_stone", + inputs = { { template = "rock", count = 1 } }, + outputs = { { template = "sharp_stone", count = 1 } }, + name = "Knap Sharp Stone", + description = "Strike a rock to flake off a sharp-edged stone." } + + c.R{ id = "twist_cordage", + inputs = { { template = "plant_fiber", count = 3 } }, + outputs = { { template = "cordage", count = 1 } }, + name = "Twist Cordage", + description = "Twist three plant fibers into a length of cordage." } + + c.R{ id = "craft_stone_axe", + inputs = { + { template = "sharp_stone", count = 1 }, -- head (consumed) + { template = "stick", count = 1 }, -- haft + { template = "cordage", count = 1 }, -- binding + }, + outputs = { { template = "stone_axe", count = 1 } }, + name = "Haft Stone Axe", + description = "Bind a sharp stone to a stick with cordage." } + + -- ---- world spawns (backpack sits at 350,400) --------------------- + c.S{ template = "log", x = 110, y = 400 } + c.S{ template = "sharp_stone", x = 110, y = 330 } + for i = 1, 3 do + c.S{ template = "plant_fiber", x = 50, y = 300 + (i - 1) * 28 } + end +end diff --git a/init.lua b/init.lua index 0e24735..26f2055 100644 --- a/init.lua +++ b/init.lua @@ -212,61 +212,8 @@ composition.define_template{ tags = {"renderable", "item"}, } --- Stone-Age crafting slice (crafting v0.3 / ADR-0055 consumer). `log` is a --- heavy wood item; the `saw_planks` recipe matches it by property-constraint --- (composition.wood > 0.5 AND weight > 5) rather than by template-id, so a --- light `plank` (weight 2) is correctly NOT sawable into more planks. -composition.define_template{ - id = "log", - properties = { - stack_mode = "individual", - name = "Log", - description = "A heavy wooden log. Saw it into planks with a cutting edge.", - weight = 20, -- kg — heavy (> saw threshold) - volume = 0.05, - ["composition.wood"] = 1.0, - sprite_atlas = "", - sprite_uv = {x = 0, y = 0, w = 1, h = 1}, - position = {x = 0, y = 0}, - }, - tags = {"renderable", "item"}, -} - --- `sharp_stone` is a NON-consumed cutting TOOL: it carries the derived --- affordance `affordance.cutting`, which `saw_planks` presence-checks. -composition.define_template{ - id = "sharp_stone", - properties = { - stack_mode = "individual", - name = "Sharp Stone", - description = "A knapped stone with a cutting edge. Used as a tool, not consumed.", - weight = 1, - volume = 0.0004, - ["composition.stone"] = 1.0, - ["affordance.cutting"] = true, -- tool affordance (crafting v0.3) - sprite_atlas = "", - sprite_uv = {x = 0, y = 0, w = 1, h = 1}, - position = {x = 0, y = 0}, - }, - tags = {"renderable", "item"}, -} - --- `plant_fiber` is a light gatherable; three twist into cordage (binding). -composition.define_template{ - id = "plant_fiber", - properties = { - stack_mode = "individual", - name = "Plant Fiber", - description = "A strand of tough plant fiber. Twist several into cordage.", - weight = 0.05, - volume = 0.0002, - ["composition.fiber"] = 1.0, - sprite_atlas = "", - sprite_uv = {x = 0, y = 0, w = 1, h = 1}, - position = {x = 0, y = 0}, - }, - tags = {"renderable", "item"}, -} +-- Stone-Age item templates + recipes now live in content/stone_age.lua +-- (T1-modder content pack), loaded in M.init via the content facade. -- NOTE: Crafted-output templates (rock_pick, rock_pile) are defined inside -- M.init rather than at top-level, because their sprite_atlas defaults need @@ -667,6 +614,48 @@ function M.init(ctx) stone_uvs[t.name] = { x = t.uv[1], y = t.uv[2], w = t.uv[3], h = t.uv[4] } end + -- ── T1-modder content facade ────────────────────────────────────────── + -- Content packs (content/*.lua) add items + recipes + world-spawns through + -- this declarative API instead of editing init.lua — the T1 "modder mode". + -- `content.T` also auto-registers each craftable's crafting-panel icon from + -- its own sprite. Packs are loaded (after the world is ready) further down. + local ATLAS = { + tc = { png = tc_atlas_png, uvs = tc_sprites, scale = tc_scale }, + stone = { png = stone_atlas_png, uvs = stone_uvs, scale = nil }, + } + local craft_icons = { + rock_pick = { atlas = tc_atlas_png, uv = tc_sprites.spade }, + rock_pile = { atlas = stone_atlas_png, uv = stone_uvs.slot_13_solid }, + } + local content = {} + function content.T(d) + local a = ATLAS[d.atlas] or error("content.T '" .. tostring(d.id) .. "': unknown atlas '" .. tostring(d.atlas) .. "'") + local uv = a.uvs[d.uv] or error("content.T '" .. tostring(d.id) .. "': unknown uv '" .. tostring(d.uv) .. "'") + local props = { + stack_mode = "individual", + name = d.name, + description = d.desc, + weight = d.weight or 1, + volume = d.vol or 0.001, + sprite_atlas = a.png, + sprite_uv = { x = uv.x, y = uv.y, w = uv.w, h = uv.h }, + position = { x = 0, y = 0 }, + } + if a.scale then props.sprite_scale = a.scale * (d.scale or 1) end + for m, v in pairs(d.material or {}) do props["composition." .. m] = v end + for k, v in pairs(d.affordance or {}) do props["affordance." .. k] = v end + composition.define_template{ id = d.id, properties = props, tags = { "renderable", "item" } } + if d.icon ~= false then craft_icons[d.id] = { atlas = a.png, uv = uv } end + end + function content.R(def) crafting.define_recipe(def) end + function content.S(d) + local e = composition.create{ template = d.template, properties = { position = { x = d.x, y = d.y } } } + register_pickup(e) + return e + end + local MODULE_DIR = engine.module.dir_of("vagrant-skeleton") + local CONTENT_PACKS = { "stone_age" } + -- Crafted-output templates: defined here (not top-level) so their -- sprite_atlas defaults can carry the resolved atlas PNG path. -- crafting.craft calls composition.create with no overrides, so the @@ -713,69 +702,8 @@ function M.init(ctx) tags = {"renderable", "item"}, } - -- plank: crafted output of `saw_planks` (crafting v0.3, ×4 multi-output). - -- Placeholder visual reuses the TC_Basics bench sprite until a dedicated - -- plank decal lands. - local plank_uv = tc_sprites.bench1 - composition.define_template{ - id = "plank", - properties = { - stack_mode = "individual", - name = "Plank", - description = "A sawn wooden plank.", - weight = 2, -- light: NOT re-sawable (< 5) - volume = 0.004, - ["composition.wood"] = 1.0, - sprite_atlas = tc_atlas_png, - sprite_uv = {x = plank_uv.x, y = plank_uv.y, - w = plank_uv.w, h = plank_uv.h}, - sprite_scale = tc_scale, - position = {x = 0, y = 0}, - }, - tags = {"renderable", "item"}, - } - - -- cordage: crafted from 3 plant_fiber; a binding material for hafting. - local cordage_uv = tc_sprites.sack1 - composition.define_template{ - id = "cordage", - properties = { - stack_mode = "individual", - name = "Cordage", - description = "A length of twisted plant-fiber cord.", - weight = 0.1, - volume = 0.0003, - ["composition.fiber"] = 1.0, - sprite_atlas = tc_atlas_png, - sprite_uv = {x = cordage_uv.x, y = cordage_uv.y, w = cordage_uv.w, h = cordage_uv.h}, - sprite_scale = tc_scale, - position = {x = 0, y = 0}, - }, - tags = {"renderable", "item"}, - } - - -- stone_axe: hafted tool (sharp_stone head + stick haft + cordage binding). - -- Carries affordance.chopping for future wood-felling recipes. - local axe_uv = tc_sprites.spade - composition.define_template{ - id = "stone_axe", - properties = { - stack_mode = "individual", - name = "Stone Axe", - description = "A hafted stone axe. Chops wood.", - weight = 1.8, - volume = 0.0012, - ["composition.stone"] = 0.6, - ["composition.wood"] = 0.3, - ["composition.fiber"] = 0.1, - ["affordance.chopping"] = true, - sprite_atlas = tc_atlas_png, - sprite_uv = {x = axe_uv.x, y = axe_uv.y, w = axe_uv.w, h = axe_uv.h}, - sprite_scale = tc_scale, - position = {x = 0, y = 0}, - }, - tags = {"renderable", "item"}, - } + -- (plank / cordage / stone_axe crafted-output templates now live in + -- content/stone_age.lua, defined via the content facade above.) -- Workbench-Template (Phase D v0.2). Container-kind=list so it can -- accept items via inventory.add (crafting output landing spot). @@ -844,52 +772,13 @@ function M.init(ctx) register_pickup(state.stick) if CI_FRAME_PERF then engine.print("vagrant: stick_spawned") end - -- Stone-Age slice: a Log (saw-able) + a Sharp Stone (cutting tool) in the - -- world, so `saw_planks` (crafting v0.3 property-constraint recipe) is - -- demonstrable end-to-end: pick both up, craft at the workbench → 4 planks. - local LOG_X, LOG_Y = BACKPACK_X - 240, BACKPACK_Y - local log_uv = tc_sprites.log_pile1 - state.log = composition.create{ - template = "log", - properties = { - sprite_atlas = tc_atlas_png, - sprite_uv = {x = log_uv.x, y = log_uv.y, w = log_uv.w, h = log_uv.h}, - sprite_scale = tc_scale, - position = {x = LOG_X, y = LOG_Y}, - }, - } - register_pickup(state.log) - - local SAW_X, SAW_Y = BACKPACK_X - 240, BACKPACK_Y - 70 - local saw_uv = tc_sprites.spade - state.sharp_stone = composition.create{ - template = "sharp_stone", - properties = { - sprite_atlas = tc_atlas_png, - sprite_uv = {x = saw_uv.x, y = saw_uv.y, w = saw_uv.w, h = saw_uv.h}, - sprite_scale = tc_scale, - position = {x = SAW_X, y = SAW_Y}, - }, - } - register_pickup(state.sharp_stone) - - -- Three plant fibers (twist into cordage → haft a stone axe). - state.fibers = {} - local fiber_uv = tc_sprites.sack1 - for i = 1, 3 do - local f = composition.create{ - template = "plant_fiber", - properties = { - sprite_atlas = tc_atlas_png, - sprite_uv = {x = fiber_uv.x, y = fiber_uv.y, w = fiber_uv.w, h = fiber_uv.h}, - sprite_scale = tc_scale * 0.6, - position = {x = LOG_X - 60, y = LOG_Y - 100 + (i - 1) * 28}, - }, - } - register_pickup(f) - state.fibers[i] = f + -- Load T1-modder content packs (items + recipes + world-spawns). Runs here, + -- after the base world is ready, so pack spawns land correctly. A modder + -- adds content by editing content/.lua — never init.lua. + for _, pack in ipairs(CONTENT_PACKS) do + dofile(MODULE_DIR .. "/content/" .. pack .. ".lua")(content) end - if CI_FRAME_PERF then engine.print("vagrant: stoneage_spawned") end + if CI_FRAME_PERF then engine.print("vagrant: content_packs_loaded") end -- Workbench spawn (Phase D v0.2). Placed at (350, 380) — 20px above -- the backpack (at 350, 400) so they are visually distinct but still @@ -923,70 +812,12 @@ function M.init(ctx) name = "Rock Pile", description = "Stack rocks for storage.", } - -- saw_planks: first property-constraint recipe (crafting v0.3 / ADR-0055). - -- Input matched by MATERIAL, not template-id: any item that is mostly wood - -- and heavy enough (a log qualifies, a plank does not). Requires a cutting - -- TOOL (sharp_stone) that is checked for presence but NOT consumed. - -- Multi-output: one log yields four planks. - crafting.define_recipe{ - id = "saw_planks", - inputs = { - {match = {["composition.wood"] = ">0.5", weight = ">5"}, count = 1}, - }, - tools = { - {match = {["affordance.cutting"] = true}}, - }, - outputs = { - {template = "plank", count = 4}, - }, - name = "Saw Planks", - description = "Saw a wooden log into four planks (needs a cutting edge).", - } - -- ── Stone-Age tech chain (crafting v0.3) ────────────────────────────── - -- Self-bootstrapping graph: knap a rock into a sharp_stone (bare hands), - -- which is then the cutting TOOL for saw_planks AND the head for a - -- stone_axe. Twist plant fibers into cordage, then haft axe = head + stick - -- + cordage. Tier-0 steps need no tool (the bootstrap entry points). - crafting.define_recipe{ - id = "knap_sharp_stone", - inputs = { {template = "rock", count = 1} }, - outputs = { {template = "sharp_stone", count = 1} }, - name = "Knap Sharp Stone", - description = "Strike a rock to flake off a sharp-edged stone.", - } - crafting.define_recipe{ - id = "twist_cordage", - inputs = { {template = "plant_fiber", count = 3} }, - outputs = { {template = "cordage", count = 1} }, - name = "Twist Cordage", - description = "Twist three plant fibers into a length of cordage.", - } - crafting.define_recipe{ - id = "craft_stone_axe", - inputs = { - {template = "sharp_stone", count = 1}, -- head (consumed) - {template = "stick", count = 1}, -- haft - {template = "cordage", count = 1}, -- binding - }, - outputs = { {template = "stone_axe", count = 1} }, - name = "Haft Stone Axe", - description = "Bind a sharp stone to a stick with cordage.", - } + -- (Stone-Age recipes — saw_planks, knap, twist, haft — now live in + -- content/stone_age.lua, loaded via the content facade above.) - -- v0.14.0: per-recipe icon resolver for the Crafting + Workbench-Crafting - -- widgets. Maps recipe.output.template → {atlas, uv} for the icon - -- column added in crafting-display v0.3.0. composition has no - -- template-property-getter today (ADR-0001 keeps the read-side - -- domain-free), so the module-side that DEFINES the templates is the - -- canonical place to know each output's sprite. - local craft_icons = { - rock_pick = { atlas = tc_atlas_png, uv = tc_sprites.spade }, - rock_pile = { atlas = stone_atlas_png, uv = stone_uvs.slot_13_solid }, - plank = { atlas = tc_atlas_png, uv = tc_sprites.bench1 }, - sharp_stone = { atlas = stone_atlas_png, uv = stone_uvs.slot_13_solid }, - cordage = { atlas = tc_atlas_png, uv = tc_sprites.sack1 }, - stone_axe = { atlas = tc_atlas_png, uv = tc_sprites.spade }, - } + -- Crafting-panel icons live in `craft_icons` (seeded with the base + -- rock_pick/rock_pile in the content facade above; content packs + -- auto-register their own icon from each item's sprite via content.T). local function craft_icon_for(recipe) if recipe and recipe.output and recipe.output.template then return craft_icons[recipe.output.template] diff --git a/manifest.module b/manifest.module index 79fb238..b3156a5 100644 --- a/manifest.module +++ b/manifest.module @@ -1,6 +1,6 @@ { "id": "vagrant-skeleton", - "version": "0.16.0", + "version": "0.17.0", "kind": "module", "api": "^0.1", "ci_frames": 60,