initial: composition v0.1.0 — template + tag-index + instantiation

Phase-A.1 implementation per
sporel-meta/docs/superpowers/specs/2026-06-09-phase-A-...
Template-Only-Subset; slots/container/quality/condition produce
loud-errors with explicit re-entry-phase hints. Position-Format
revised to two flat scalar properties (position.x, position.y) per
engine reality: no PROPERTY_TABLE exists.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-06-09 13:28:52 +00:00
commit b6a16df6f0
4 changed files with 538 additions and 0 deletions

164
README.md Normal file
View File

@@ -0,0 +1,164 @@
# lib-core.composition
Template + Instantiation + Tag-Index over `engine.entity`. Templates define
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
**Lib-ID:** lib-core.composition
**Requires:** (none — uses engine `entity.*`, `domain.contribute` only)
**Tags:** composition, entity, template, tag-index
## Topology
<!-- topology:start (auto-generated; do not edit) -->
```mermaid
graph LR
this["lib-core.composition"]
engine["engine.*"]
this --> engine
```
<!-- topology:end -->
## Scope (v0.1.0 — Spine)
Template-Only-Subset per [Phase-A Spec](../../../sporel-meta/docs/superpowers/specs/2026-06-09-phase-A-inactive-entities-composition-actor-reentry-design.md).
**Supported:**
- `define_template{id, properties, tags}`
- `create{template, properties}`
- `list_by_template(id)`, `list_by_tag(tag)`
- `destroy(entity)`
**Deferred (Loud-Error if attempted):**
- `slots` block → Phase D trigger (Composite Items with Sub-Items)
- `container` block → Phase B trigger (Items + Inventory)
- `quality`, `condition` → Phase J trigger (Damage / Wear)
- `parent:` template-inheritance → no consumer yet
## API
### `composition.define_template(def)`
**Syntax:** `composition.define_template({id: string, properties?: table, tags?: array}) -> void`
**Example:**
```lua
composition.define_template{
id = "sign",
properties = {
sprite_path = "sprites/sign.png",
text = "default sign text",
position = {x = 0, y = 0}, -- auto-flattened to position.x + position.y
},
tags = {"renderable"},
}
```
**Description:** Registers a template under `id`. All `properties` are
auto-declared as `inert` via `domain.contribute` with type inferred
from the default-value. Nested tables in `properties` are flattened
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`.
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).
### `composition.create(spec)`
**Syntax:** `composition.create({template: string, properties?: table}) -> entity`
**Example:**
```lua
local sign = composition.create{
template = "sign",
properties = {
position = {x = 144, y = 200},
text = "Hier steht: Willkommen im Spine.",
},
}
```
**Description:** Spawns an `engine.entity`, applies template-defaults
shallow-overridden by per-instance `properties`. Nested-table
overrides (like `position = {x, y}`) are flattened the same way as in
`define_template`. Per-instance properties that weren't declared by
the template get declared on-the-fly as `inert`. Entity is registered
in the template-index + tag-index of the template. Returns the
`engine.entity` handle.
Loud `error(...)` on: non-table `spec`, missing/unknown `template`,
non-table `properties`.
### `composition.list_by_template(id)`
**Syntax:** `composition.list_by_template(id: string) -> {entity, ...}`
**Example:**
```lua
for _, sign in ipairs(composition.list_by_template("sign")) do
engine.print(sign:get_property("text"))
end
```
**Description:** Returns a shallow-copied list of all currently-alive
entities created with the given template-id. Empty list if template
is unknown or has no instances.
### `composition.list_by_tag(tag)`
**Syntax:** `composition.list_by_tag(tag: string) -> {entity, ...}`
**Example:**
```lua
for _, e in ipairs(composition.list_by_tag("renderable")) do
local x = e:get_property("position.x")
local y = e:get_property("position.y")
-- ... render
end
```
**Description:** Returns all currently-alive entities whose template
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.destroy(entity)`
**Syntax:** `composition.destroy(entity) -> void`
**Description:** Removes entity from template-index + tag-index +
destroys via `engine.entity.destroy`. Idempotent. If entity wasn't
composition-created, falls through to `entity.destroy`.
### `composition.list_templates()`
**Syntax:** `composition.list_templates() -> {id, ...}`
**Description:** Debug/introspection — returns set of declared
template-ids.
## Conventions
- **Property-Value-Types:** number, string, boolean (engine §PROPERTY_*
catalog). Tables are flattened to dotted scalar keys; arrays inside
`properties` → loud error.
- **Position-Format:** stored as two scalar properties `position.x`
and `position.y` (per Phase-A Spec §5 A-Q5 revision 2026-06-09 —
engine has no PROPERTY_TABLE).
- **Tag-Index:** maintained in lib, not in engine. Tag-lookup is O(1)
by tag, list iteration is O(n) per tag.
- **`composition.reg_id`:** internal book-keeping property on each
created entity; used by `destroy` to find the meta-record. Do not
set or read manually.
## Future Phases
| Phase | Triggers in Template | Spec |
|---|---|---|
| B (Items) | `container = true` | inventory-model.md |
| D (Composites) | `slots = {...}` | composition-model.md (full Slot-System) |
| J (Damage/Wear) | `quality`, `condition` | damage-model.md |