feat(composition): add set_tag + accept container={kind="list"} block
- set_tag(entity, tag, present): adds/removes entity from index_by_tag and entity_meta.tags. Idempotent (double-add and double-remove are no-ops). Loud-Error on entity not created by composition. - define_template now accepts an optional container block. Validates kind="list" (only supported value). Loud-Errors individually on each unimplemented constraint field (weight_max, volume_max, grid, accepts_fluid, accepts_gas, restrictions) and on unknown container keys, preventing silent capability mismatches. - create() copies tpl.tags into entity_meta so per-entity set_tag calls do not mutate the shared template tag-set. - manifest.lib version bumped to 0.2.0. - README and header comment updated to reflect new surface and revised deferred-list.
This commit is contained in:
61
README.md
61
README.md
@@ -5,7 +5,7 @@ property defaults; `create` spawns an `engine.entity` with template
|
||||
defaults + per-instance overrides; entities are indexed by template-id
|
||||
and tag for cheap reverse-lookup.
|
||||
|
||||
**Version:** 0.1.0
|
||||
**Version:** 0.2.0
|
||||
**Lib-ID:** lib-core.composition
|
||||
**Requires:** (none — uses engine `entity.*`, `domain.contribute` only)
|
||||
**Tags:** composition, entity, template, tag-index
|
||||
@@ -21,19 +21,22 @@ graph LR
|
||||
```
|
||||
<!-- topology:end -->
|
||||
|
||||
## Scope (v0.1.0 — Spine)
|
||||
## Scope (v0.2.0)
|
||||
|
||||
Template-Only-Subset per [Phase-A Spec](../../../sporel-meta/docs/superpowers/specs/2026-06-09-phase-A-inactive-entities-composition-actor-reentry-design.md).
|
||||
Template-Only-Subset + set_tag + container declaration.
|
||||
|
||||
**Supported:**
|
||||
- `define_template{id, properties, tags}`
|
||||
- `define_template{..., container={kind="list"}}` — v0.2: `kind="list"` accepted
|
||||
- `create{template, properties}`
|
||||
- `list_by_template(id)`, `list_by_tag(tag)`
|
||||
- `destroy(entity)`
|
||||
- `set_tag(entity, tag, present)` — v0.2: runtime tag add/remove
|
||||
|
||||
**Deferred (Loud-Error if attempted):**
|
||||
- `slots` block → Phase D trigger (Composite Items with Sub-Items)
|
||||
- `container` block → Phase B trigger (Items + Inventory)
|
||||
- `container` constraint fields (`weight_max`, `volume_max`, `grid`,
|
||||
`accepts_fluid`, `accepts_gas`, `restrictions`) → Phase F/G trigger
|
||||
- `quality`, `condition` → Phase J trigger (Damage / Wear)
|
||||
- `parent:` template-inheritance → no consumer yet
|
||||
|
||||
@@ -63,10 +66,31 @@ to dotted scalar keys (e.g. `position = {x, y}` → declares `position.x`
|
||||
and `position.y` as separate `number` properties). Tag-list registers
|
||||
the template in the tag-index used by `list_by_tag`.
|
||||
|
||||
The optional `container` block declares this template as an item container.
|
||||
Only `kind = "list"` is supported in v0.2. Any constraint field
|
||||
(`weight_max`, `volume_max`, `grid`, `accepts_fluid`, `accepts_gas`,
|
||||
`restrictions`) loud-errors at template-load time (Capability-by-Declaration:
|
||||
no constraint is silently ignored).
|
||||
|
||||
**Example with container:**
|
||||
```lua
|
||||
composition.define_template{
|
||||
id = "backpack",
|
||||
properties = {
|
||||
sprite_atlas = "sprites/items.png",
|
||||
sprite_uv = "0 0 32 32",
|
||||
position = {x = 0, y = 0},
|
||||
},
|
||||
tags = {"renderable"},
|
||||
container = { kind = "list" },
|
||||
}
|
||||
```
|
||||
|
||||
Loud `error(...)` on: missing/non-string `id`, duplicate `id`,
|
||||
non-table `properties`, malformed `tags`, attempted `slots` /
|
||||
`container` / `quality` / `condition` / `parent:` block (each pointing
|
||||
to its re-entry phase).
|
||||
`quality` / `condition` / `parent:` block (each pointing to its
|
||||
re-entry phase); `container.kind` other than `"list"`; any
|
||||
unimplemented constraint field in `container`.
|
||||
|
||||
### `composition.create(spec)`
|
||||
|
||||
@@ -126,6 +150,28 @@ end
|
||||
declared `tag` in its `tags` array. Used by `lib-core.render`
|
||||
`draw_entities{tag="..."}` (see A.2). Empty list if tag has no entities.
|
||||
|
||||
### `composition.set_tag(entity, tag, present)`
|
||||
|
||||
**Syntax:** `composition.set_tag(entity, tag: string, present: boolean) -> void`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
-- Remove from renderable index (e.g. item picked up into inventory)
|
||||
composition.set_tag(item_entity, "renderable", false)
|
||||
|
||||
-- Restore renderable index (e.g. item dropped back into world)
|
||||
composition.set_tag(item_entity, "renderable", true)
|
||||
```
|
||||
|
||||
**Description:** Adds or removes `entity` from the tag-index for `tag`.
|
||||
`present=true` inserts the entity (idempotent — calling again with the
|
||||
same tag is a no-op). `present=false` removes it (idempotent — calling
|
||||
again when already absent is a no-op). Both `entity_meta` and
|
||||
`index_by_tag` are updated atomically. Loud-Error if `entity` was not
|
||||
created by composition (e.g. a raw `entity.create()` handle). Used by
|
||||
`lib-core.inventory` `add`/`remove` to flip the `"renderable"` tag
|
||||
when items move between world and inventory.
|
||||
|
||||
### `composition.destroy(entity)`
|
||||
|
||||
**Syntax:** `composition.destroy(entity) -> void`
|
||||
@@ -159,6 +205,7 @@ template-ids.
|
||||
|
||||
| Phase | Triggers in Template | Spec |
|
||||
|---|---|---|
|
||||
| B (Items) | `container = true` | inventory-model.md |
|
||||
| B (Items) | `container={kind="list"}` — accepted in v0.2; constraint fields deferred to F/G | inventory-model.md |
|
||||
| D (Composites) | `slots = {...}` | composition-model.md (full Slot-System) |
|
||||
| F/G (Constraints) | `weight_max`, `volume_max`, `grid`, `accepts_fluid`, `accepts_gas`, `restrictions` | constraint-model.md |
|
||||
| J (Damage/Wear) | `quality`, `condition` | damage-model.md |
|
||||
|
||||
103
init.lua
103
init.lua
@@ -1,5 +1,5 @@
|
||||
-- =====================================================================
|
||||
-- lib-core.composition v0.1.0 — Template + Instantiation + Tag-Index
|
||||
-- lib-core.composition v0.2.0 — Template + Instantiation + Tag-Index
|
||||
-- See: meta/docs/superpowers/specs/2026-06-09-phase-A-inactive-entities-...
|
||||
--
|
||||
-- v0.1.0 Template-Only-Subset:
|
||||
@@ -15,9 +15,18 @@
|
||||
-- - composition.destroy(entity) — removes from indices + destroys
|
||||
-- engine.entity.
|
||||
--
|
||||
-- DEFERRED (loud-error if attempted in v0.1):
|
||||
-- v0.2.0 additions:
|
||||
-- - composition.set_tag(entity, tag, present) — adds (present=true) or
|
||||
-- removes (present=false) entity in index_by_tag + entity_meta.
|
||||
-- Idempotent. Loud-Error on entity not created by composition.
|
||||
-- - define_template accepts optional container={kind="list"} block.
|
||||
-- All constraint fields (weight_max, volume_max, grid, accepts_fluid,
|
||||
-- accepts_gas, restrictions) → Loud-Error (Phase F/G deferred).
|
||||
--
|
||||
-- DEFERRED (loud-error if attempted):
|
||||
-- - slots → Phase D Trigger (Composite Items with Sub-Items)
|
||||
-- - container → Phase B Trigger (Items + Inventory)
|
||||
-- - container constraint fields (weight_max, volume_max, grid,
|
||||
-- accepts_fluid, accepts_gas, restrictions) → Phase F/G Trigger
|
||||
-- - quality → Phase J Trigger (Damage / Wear)
|
||||
-- - condition → Phase J Trigger (Damage / Wear)
|
||||
-- - parent: → Template-Inheritance, no consumer yet
|
||||
@@ -145,25 +154,59 @@ function M.define_template(def)
|
||||
if def.slots ~= nil then
|
||||
error(string.format(
|
||||
"composition.define_template '%s': 'slots' block is Phase-D " ..
|
||||
"trigger (Composite Items with Sub-Items); not supported in v0.1",
|
||||
"trigger (Composite Items with Sub-Items); not supported in v0.2",
|
||||
def.id))
|
||||
end
|
||||
if def.container ~= nil then
|
||||
error(string.format(
|
||||
"composition.define_template '%s': 'container' block is " ..
|
||||
"Phase-B trigger (Items + Inventory); not supported in v0.1",
|
||||
def.id))
|
||||
if type(def.container) ~= "table" then
|
||||
error(string.format(
|
||||
"composition.define_template '%s': 'container' must be a table",
|
||||
def.id))
|
||||
end
|
||||
if def.container.kind ~= "list" then
|
||||
error(string.format(
|
||||
"composition.define_template '%s': container.kind must be " ..
|
||||
"'list' (only kind='list' is supported in v0.2; got %s)",
|
||||
def.id, tostring(def.container.kind)))
|
||||
end
|
||||
-- Reject each unimplemented constraint field individually so the
|
||||
-- error message names the offending field (Capability-by-Declaration:
|
||||
-- no silent ignorance of declared constraints).
|
||||
local deferred_fields = {
|
||||
"weight_max", "volume_max", "grid",
|
||||
"accepts_fluid", "accepts_gas", "restrictions",
|
||||
}
|
||||
for _, field in ipairs(deferred_fields) do
|
||||
if def.container[field] ~= nil then
|
||||
error(string.format(
|
||||
"composition.define_template '%s': container.%s is a " ..
|
||||
"Phase-F/G trigger (Constraint fields not implemented in v0.2)",
|
||||
def.id, field))
|
||||
end
|
||||
end
|
||||
-- Reject any unknown container keys (forward-protects against typos).
|
||||
local known_container_keys = { kind = true,
|
||||
weight_max = true, volume_max = true, grid = true,
|
||||
accepts_fluid = true, accepts_gas = true, restrictions = true }
|
||||
for k, _ in pairs(def.container) do
|
||||
if not known_container_keys[k] then
|
||||
error(string.format(
|
||||
"composition.define_template '%s': unknown container " ..
|
||||
"field '%s'",
|
||||
def.id, tostring(k)))
|
||||
end
|
||||
end
|
||||
end
|
||||
if def.quality ~= nil or def.condition ~= nil then
|
||||
error(string.format(
|
||||
"composition.define_template '%s': 'quality' / 'condition' " ..
|
||||
"are Phase-J triggers (Damage / Wear); not supported in v0.1",
|
||||
"are Phase-J triggers (Damage / Wear); not supported in v0.2",
|
||||
def.id))
|
||||
end
|
||||
if def.parent ~= nil then
|
||||
error(string.format(
|
||||
"composition.define_template '%s': 'parent:' inheritance has " ..
|
||||
"no consumer in v0.1; declare inline until a real use-case appears",
|
||||
"no consumer in v0.2; declare inline until a real use-case appears",
|
||||
def.id))
|
||||
end
|
||||
|
||||
@@ -206,6 +249,7 @@ function M.define_template(def)
|
||||
id = def.id,
|
||||
props_flat = props_flat,
|
||||
tags = tags_set,
|
||||
container = def.container, -- nil when not declared; B.2 reads this
|
||||
}
|
||||
index_by_template[def.id] = index_by_template[def.id] or {}
|
||||
end
|
||||
@@ -264,9 +308,13 @@ function M.create(spec)
|
||||
end
|
||||
|
||||
-- Register in indices + meta.
|
||||
-- Copy tpl.tags so per-entity set_tag calls don't mutate the template's
|
||||
-- tag-set (which would affect future create() calls for the same template).
|
||||
local reg_id = next_reg_id
|
||||
next_reg_id = next_reg_id + 1
|
||||
entity_meta[reg_id] = { template_id = tpl.id, tags = tpl.tags, handle = entity }
|
||||
local entity_tags = {}
|
||||
for t, v in pairs(tpl.tags) do entity_tags[t] = v end
|
||||
entity_meta[reg_id] = { template_id = tpl.id, tags = entity_tags, handle = entity }
|
||||
|
||||
table.insert(index_by_template[tpl.id], entity)
|
||||
for tag, _ in pairs(tpl.tags) do
|
||||
@@ -335,6 +383,39 @@ function M.destroy(target_entity)
|
||||
entity.destroy(target_entity)
|
||||
end
|
||||
|
||||
function M.set_tag(target_entity, tag, present)
|
||||
-- Validate tag argument.
|
||||
if type(tag) ~= "string" or tag == "" then
|
||||
error("composition.set_tag: 'tag' must be a non-empty string")
|
||||
end
|
||||
-- Validate present argument.
|
||||
if type(present) ~= "boolean" then
|
||||
error("composition.set_tag: 'present' must be a boolean")
|
||||
end
|
||||
-- Resolve entity → meta via the composition.reg_id property.
|
||||
local reg_id = target_entity and target_entity:get_property("composition.reg_id")
|
||||
if not reg_id or reg_id == 0 then
|
||||
error("composition.set_tag: entity was not created by composition")
|
||||
end
|
||||
local meta = entity_meta[reg_id]
|
||||
if not meta then
|
||||
error("composition.set_tag: entity was not created by composition")
|
||||
end
|
||||
|
||||
if present then
|
||||
-- Idempotent add.
|
||||
if meta.tags[tag] then return end
|
||||
meta.tags[tag] = true
|
||||
index_by_tag[tag] = index_by_tag[tag] or {}
|
||||
table.insert(index_by_tag[tag], target_entity)
|
||||
else
|
||||
-- Idempotent remove.
|
||||
if meta.tags[tag] == nil then return end
|
||||
meta.tags[tag] = nil
|
||||
remove_from_list(index_by_tag[tag], target_entity)
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------- introspection (debug/testing) ----------
|
||||
|
||||
-- Returns the set of declared template-ids (for tests + debug).
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"id":"lib-core.composition","version":"0.1.0","api_min":"0.1","deps":[]}
|
||||
{"id":"lib-core.composition","version":"0.2.0","api_min":"0.1","deps":[]}
|
||||
|
||||
Reference in New Issue
Block a user