Files
sporel-lib-core.composition/README.md
Axel Meyer 87357571bd 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.
2026-06-13 14:02:16 +02:00

7.2 KiB

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.2.0 Lib-ID: lib-core.composition Requires: (none — uses engine entity.*, domain.contribute only) Tags: composition, entity, template, tag-index

Topology

graph LR
  this["lib-core.composition"]
  engine["engine.*"]
  this --> engine

Scope (v0.2.0)

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

API

composition.define_template(def)

Syntax: composition.define_template({id: string, properties?: table, tags?: array}) -> void

Example:

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.

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:

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 / 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)

Syntax: composition.create({template: string, properties?: table}) -> entity

Example:

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:

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:

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.set_tag(entity, tag, present)

Syntax: composition.set_tag(entity, tag: string, present: boolean) -> void

Example:

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

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={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