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:
Axel Meyer
2026-06-13 13:55:01 +02:00
parent b6a16df6f0
commit 87357571bd
3 changed files with 147 additions and 19 deletions

View File

@@ -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 |