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:
24
LICENSE
Normal file
24
LICENSE
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Copyright (c) 2026 Calic. All rights reserved.
|
||||||
|
|
||||||
|
This software is part of the Sporel platform — **Tier 1 (Official /
|
||||||
|
Proprietary)** content per the Three-Tier Licensing Model documented in
|
||||||
|
`meta/docs/archive/design/vision.md §Licensing Model` (current source;
|
||||||
|
migration to `meta/docs/architecture/licensing-model.md` pending).
|
||||||
|
|
||||||
|
⚠ **WIP — Legal review required before public launch.** The terms below
|
||||||
|
reflect design intent only; the formalized license framework will be
|
||||||
|
finalized through legal counsel before the first public release. Until
|
||||||
|
then, this notice serves as a placeholder defending the platform owner's
|
||||||
|
rights against unintentional re-licensing.
|
||||||
|
|
||||||
|
No license is granted to copy, modify, distribute, sublicense, or otherwise
|
||||||
|
use this software in any form without prior written permission from the
|
||||||
|
copyright holder.
|
||||||
|
|
||||||
|
References:
|
||||||
|
- Tier 1 (this file): all rights reserved, proprietary, sold/distributed
|
||||||
|
via official channels (Steam, etc.)
|
||||||
|
- Tier 2 (Semi-Commercial Co-Development): bilateral contracts, revenue-
|
||||||
|
share — see vision.md §Licensing Model
|
||||||
|
- Tier 3 (Community Content): CC BY-NC-SA 4.0 + asymmetric CLA — applies
|
||||||
|
to community-uploaded libs/modules/assets, not this repo
|
||||||
164
README.md
Normal file
164
README.md
Normal 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 |
|
||||||
349
init.lua
Normal file
349
init.lua
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- lib-core.composition v0.1.0 — Template + Instantiation + Tag-Index
|
||||||
|
-- See: meta/docs/superpowers/specs/2026-06-09-phase-A-inactive-entities-...
|
||||||
|
--
|
||||||
|
-- v0.1.0 Template-Only-Subset:
|
||||||
|
-- - composition.define_template{id, properties, tags} — registers a
|
||||||
|
-- template; auto-declares all properties as inert via
|
||||||
|
-- domain.contribute. Nested tables (e.g. position = {x, y}) are
|
||||||
|
-- flattened to dotted keys (position.x, position.y).
|
||||||
|
-- - composition.create{template, properties} — instantiates an
|
||||||
|
-- engine.entity, applies template-defaults + per-instance overrides
|
||||||
|
-- via set_property. Registers entity in template-index + tag-index.
|
||||||
|
-- - composition.list_by_template(id) -> {entity, ...}
|
||||||
|
-- - composition.list_by_tag(tag) -> {entity, ...}
|
||||||
|
-- - composition.destroy(entity) — removes from indices + destroys
|
||||||
|
-- engine.entity.
|
||||||
|
--
|
||||||
|
-- DEFERRED (loud-error if attempted in v0.1):
|
||||||
|
-- - slots → Phase D Trigger (Composite Items with Sub-Items)
|
||||||
|
-- - container → Phase B Trigger (Items + Inventory)
|
||||||
|
-- - quality → Phase J Trigger (Damage / Wear)
|
||||||
|
-- - condition → Phase J Trigger (Damage / Wear)
|
||||||
|
-- - parent: → Template-Inheritance, no consumer yet
|
||||||
|
-- - composite-property values (e.g. position = {x, y} as single
|
||||||
|
-- property) — engine PROPERTY_TABLE doesn't exist; nested-tables
|
||||||
|
-- are auto-flattened to dotted scalar keys.
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
-- ---------- internal state ----------
|
||||||
|
|
||||||
|
-- template_id (string) → template-record:
|
||||||
|
-- { id, properties_flat (key → default-value), tags (set) }
|
||||||
|
local templates = {}
|
||||||
|
|
||||||
|
-- template_id (string) → list of engine.entity handles
|
||||||
|
local index_by_template = {}
|
||||||
|
|
||||||
|
-- tag (string) → list of engine.entity handles
|
||||||
|
local index_by_tag = {}
|
||||||
|
|
||||||
|
-- weak reverse-lookup: entity → { template, tags } (for destroy())
|
||||||
|
-- Lua-tables can't key on userdata reliably across GC, so we store
|
||||||
|
-- by stable entity-id derived from get_property("id"). engine assigns id.
|
||||||
|
-- For v0.1 we keep the parallel structure simpler: each entity records
|
||||||
|
-- its own template + tags in a Lua-side table keyed by a per-entity
|
||||||
|
-- monotonic registration-id we issue in create().
|
||||||
|
local entity_meta = {} -- reg_id → { template_id, tags, handle }
|
||||||
|
local next_reg_id = 1
|
||||||
|
|
||||||
|
-- ---------- internal helpers ----------
|
||||||
|
|
||||||
|
local function is_array(t)
|
||||||
|
if type(t) ~= "table" then return false end
|
||||||
|
local n = 0
|
||||||
|
for k, _ in pairs(t) do
|
||||||
|
if type(k) ~= "number" then return false end
|
||||||
|
n = n + 1
|
||||||
|
end
|
||||||
|
return n == #t
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Flatten nested-table property-values into dotted scalar keys.
|
||||||
|
-- Input: { position = {x=144, y=200}, text = "Hi" }
|
||||||
|
-- Output: { ["position.x"] = 144, ["position.y"] = 200, text = "Hi" }
|
||||||
|
-- Arrays inside properties (e.g. tags) are NOT flattened; they belong
|
||||||
|
-- in `tags`, not `properties`. Reject arrays here loudly.
|
||||||
|
local function flatten_props(t, prefix, out)
|
||||||
|
out = out or {}
|
||||||
|
for k, v in pairs(t) do
|
||||||
|
if type(k) ~= "string" then
|
||||||
|
error(string.format(
|
||||||
|
"composition: property keys must be strings, got %s",
|
||||||
|
type(k)))
|
||||||
|
end
|
||||||
|
local key = prefix and (prefix .. "." .. k) or k
|
||||||
|
if type(v) == "table" then
|
||||||
|
if is_array(v) then
|
||||||
|
error(string.format(
|
||||||
|
"composition: property '%s' is an array; arrays in " ..
|
||||||
|
"properties are not supported (use tags=... at " ..
|
||||||
|
"template-level for tag-lists)", key))
|
||||||
|
end
|
||||||
|
flatten_props(v, key, out)
|
||||||
|
elseif type(v) == "number" or type(v) == "string" or type(v) == "boolean" then
|
||||||
|
out[key] = v
|
||||||
|
elseif v == nil then
|
||||||
|
-- nil default means "declared but no default" — skip
|
||||||
|
else
|
||||||
|
error(string.format(
|
||||||
|
"composition: property '%s' has unsupported value-type '%s' " ..
|
||||||
|
"(supported: number, string, boolean, or nested table for flattening)",
|
||||||
|
key, type(v)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Infer the engine property-type from a default-value.
|
||||||
|
local function infer_type(v)
|
||||||
|
local t = type(v)
|
||||||
|
if t == "number" then return "number" end
|
||||||
|
if t == "string" then return "string" end
|
||||||
|
if t == "boolean" then return "boolean" end
|
||||||
|
error("composition: cannot infer property type from value of type " .. t)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Set the engine property using the auto-detected setter (engine handles
|
||||||
|
-- typed writes via set_property based on Lua type).
|
||||||
|
local function set_prop(entity, key, value)
|
||||||
|
entity:set_property(key, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Declare a property via domain.contribute if not already declared.
|
||||||
|
-- Type is inferred from the default-value. inert=true so subsequent
|
||||||
|
-- set_property writes are accepted (per engine §11 inert-write rule).
|
||||||
|
local declared = {} -- "key" → true, idempotent across multiple templates
|
||||||
|
local function declare_inert(key, default_value)
|
||||||
|
if declared[key] then return end
|
||||||
|
domain.contribute("property", {
|
||||||
|
id = key,
|
||||||
|
type = infer_type(default_value),
|
||||||
|
inert = true,
|
||||||
|
})
|
||||||
|
declared[key] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------- public API ----------
|
||||||
|
|
||||||
|
function M.define_template(def)
|
||||||
|
if type(def) ~= "table" then
|
||||||
|
error("composition.define_template: expected table, got " .. type(def))
|
||||||
|
end
|
||||||
|
if type(def.id) ~= "string" or def.id == "" then
|
||||||
|
error("composition.define_template: 'id' must be a non-empty string")
|
||||||
|
end
|
||||||
|
if templates[def.id] then
|
||||||
|
error(string.format(
|
||||||
|
"composition.define_template: template '%s' already defined",
|
||||||
|
def.id))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Phase B/D/J reject — explicit Loud-Error pointing to re-entry phase.
|
||||||
|
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",
|
||||||
|
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))
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
def.id))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Properties block (optional but recommended).
|
||||||
|
local props_in = def.properties or {}
|
||||||
|
if type(props_in) ~= "table" then
|
||||||
|
error(string.format(
|
||||||
|
"composition.define_template '%s': 'properties' must be a table",
|
||||||
|
def.id))
|
||||||
|
end
|
||||||
|
local props_flat = flatten_props(props_in)
|
||||||
|
|
||||||
|
-- Tags block (optional list of strings).
|
||||||
|
local tags_set = {}
|
||||||
|
if def.tags ~= nil then
|
||||||
|
if type(def.tags) ~= "table" or not is_array(def.tags) then
|
||||||
|
error(string.format(
|
||||||
|
"composition.define_template '%s': 'tags' must be an array " ..
|
||||||
|
"of strings",
|
||||||
|
def.id))
|
||||||
|
end
|
||||||
|
for _, tag in ipairs(def.tags) do
|
||||||
|
if type(tag) ~= "string" or tag == "" then
|
||||||
|
error(string.format(
|
||||||
|
"composition.define_template '%s': tag entries must be " ..
|
||||||
|
"non-empty strings",
|
||||||
|
def.id))
|
||||||
|
end
|
||||||
|
tags_set[tag] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Declare each property as inert via domain.contribute. Type inferred
|
||||||
|
-- from default-value. nil-defaults skipped (no inference possible).
|
||||||
|
for key, val in pairs(props_flat) do
|
||||||
|
declare_inert(key, val)
|
||||||
|
end
|
||||||
|
|
||||||
|
templates[def.id] = {
|
||||||
|
id = def.id,
|
||||||
|
props_flat = props_flat,
|
||||||
|
tags = tags_set,
|
||||||
|
}
|
||||||
|
index_by_template[def.id] = index_by_template[def.id] or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.create(spec)
|
||||||
|
if type(spec) ~= "table" then
|
||||||
|
error("composition.create: expected table, got " .. type(spec))
|
||||||
|
end
|
||||||
|
if type(spec.template) ~= "string" or spec.template == "" then
|
||||||
|
error("composition.create: 'template' must be a non-empty string")
|
||||||
|
end
|
||||||
|
local tpl = templates[spec.template]
|
||||||
|
if not tpl then
|
||||||
|
error(string.format(
|
||||||
|
"composition.create: unknown template '%s' (was define_template " ..
|
||||||
|
"called?)",
|
||||||
|
spec.template))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Build flat merged property-bag: template-defaults overridden by
|
||||||
|
-- per-instance properties. Nested-tables in overrides are flattened
|
||||||
|
-- too. Override-flatten happens AFTER template-flatten, so nested
|
||||||
|
-- overrides naturally land on dotted keys.
|
||||||
|
local overrides_flat = {}
|
||||||
|
if spec.properties ~= nil then
|
||||||
|
if type(spec.properties) ~= "table" then
|
||||||
|
error("composition.create: 'properties' must be a table")
|
||||||
|
end
|
||||||
|
overrides_flat = flatten_props(spec.properties)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Any override-key that wasn't declared via define_template gets
|
||||||
|
-- declared on-the-fly with type inferred from the override value.
|
||||||
|
-- This lets per-instance properties live without forcing the template
|
||||||
|
-- to enumerate every possible override.
|
||||||
|
for key, val in pairs(overrides_flat) do
|
||||||
|
if templates[spec.template].props_flat[key] == nil then
|
||||||
|
declare_inert(key, val)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Spawn engine entity.
|
||||||
|
local entity = entity.create()
|
||||||
|
|
||||||
|
-- Apply template defaults first.
|
||||||
|
for key, val in pairs(tpl.props_flat) do
|
||||||
|
local final = overrides_flat[key]
|
||||||
|
if final == nil then final = val end
|
||||||
|
set_prop(entity, key, final)
|
||||||
|
end
|
||||||
|
-- Apply override-only properties (those not in template defaults).
|
||||||
|
for key, val in pairs(overrides_flat) do
|
||||||
|
if tpl.props_flat[key] == nil then
|
||||||
|
set_prop(entity, key, val)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Register in indices + meta.
|
||||||
|
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 }
|
||||||
|
|
||||||
|
table.insert(index_by_template[tpl.id], entity)
|
||||||
|
for tag, _ in pairs(tpl.tags) do
|
||||||
|
index_by_tag[tag] = index_by_tag[tag] or {}
|
||||||
|
table.insert(index_by_tag[tag], entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Stash reg_id on the entity via a reserved property so destroy()
|
||||||
|
-- can find the meta-record. We use a composition-internal key.
|
||||||
|
declare_inert("composition.reg_id", 0)
|
||||||
|
set_prop(entity, "composition.reg_id", reg_id)
|
||||||
|
|
||||||
|
return entity
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.list_by_template(id)
|
||||||
|
if type(id) ~= "string" then
|
||||||
|
error("composition.list_by_template: 'id' must be a string")
|
||||||
|
end
|
||||||
|
local list = index_by_template[id]
|
||||||
|
if not list then return {} end
|
||||||
|
-- Return a shallow copy so callers can't mutate our index.
|
||||||
|
local out = {}
|
||||||
|
for i, e in ipairs(list) do out[i] = e end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.list_by_tag(tag)
|
||||||
|
if type(tag) ~= "string" then
|
||||||
|
error("composition.list_by_tag: 'tag' must be a string")
|
||||||
|
end
|
||||||
|
local list = index_by_tag[tag]
|
||||||
|
if not list then return {} end
|
||||||
|
local out = {}
|
||||||
|
for i, e in ipairs(list) do out[i] = e end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Remove entity from indices + destroy engine-side. Idempotent.
|
||||||
|
local function remove_from_list(list, entity)
|
||||||
|
if not list then return end
|
||||||
|
for i, e in ipairs(list) do
|
||||||
|
if e == entity then
|
||||||
|
table.remove(list, i)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.destroy(target_entity)
|
||||||
|
if not target_entity then return end
|
||||||
|
local reg_id = target_entity:get_property("composition.reg_id")
|
||||||
|
if not reg_id or reg_id == 0 then
|
||||||
|
-- Not composition-created or already destroyed.
|
||||||
|
entity.destroy(target_entity)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local meta = entity_meta[reg_id]
|
||||||
|
if meta then
|
||||||
|
remove_from_list(index_by_template[meta.template_id], target_entity)
|
||||||
|
for tag, _ in pairs(meta.tags) do
|
||||||
|
remove_from_list(index_by_tag[tag], target_entity)
|
||||||
|
end
|
||||||
|
entity_meta[reg_id] = nil
|
||||||
|
end
|
||||||
|
entity.destroy(target_entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------- introspection (debug/testing) ----------
|
||||||
|
|
||||||
|
-- Returns the set of declared template-ids (for tests + debug).
|
||||||
|
function M.list_templates()
|
||||||
|
local out = {}
|
||||||
|
for id, _ in pairs(templates) do
|
||||||
|
table.insert(out, id)
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
1
manifest.lib
Normal file
1
manifest.lib
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"id":"lib-core.composition","version":"0.1.0","api_min":"0.1","deps":[]}
|
||||||
Reference in New Issue
Block a user