initial: actor v0.1.0 — composition-wrapper + movement-speed-contract
Phase-A.5 implementation per sporel-meta/docs/superpowers/specs/2026-06-09-phase-A-... Re-Entry from P.0-Actor-Deferral (Trigger 1: Composition-Aktivierung). Slim wrapper: actor.create validates properties.movement_speed > 0, delegates to composition.create. actor.position reads dotted position back as two numbers. actor.move does direct inert-write to position.x + position.y (engine §11 inert-write; Action-routing deferred). DEFERRED in v0.1: Body-Slot-Aggregator, Walk-Capability, Movement-State on entity, Action-mediated move. All have re-entry-trigger notes. 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
|
||||
132
README.md
Normal file
132
README.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# lib-core.actor
|
||||
|
||||
Thin wrapper over `lib-core.composition` that enforces a baseline-speed
|
||||
contract for "actor" entities. Provides convenience helpers for
|
||||
position-read + position-write (inert-mediated). Doesn't track any
|
||||
ephemeral movement-state (vel/dir) — that stays in `player_control`
|
||||
or whatever drives the actor each frame.
|
||||
|
||||
**Version:** 0.1.0
|
||||
**Lib-ID:** lib-core.actor
|
||||
**Requires:** lib-core.composition v>=0.1.0
|
||||
**Tags:** actor, entity, movement
|
||||
|
||||
## Topology
|
||||
|
||||
<!-- topology:start (auto-generated; do not edit) -->
|
||||
```mermaid
|
||||
graph LR
|
||||
this["lib-core.actor"]
|
||||
lib_core_composition["lib-core.composition"]
|
||||
this --> lib_core_composition
|
||||
engine["engine.*"]
|
||||
this --> engine
|
||||
```
|
||||
<!-- topology:end -->
|
||||
|
||||
## Scope (v0.1.0 — Spine)
|
||||
|
||||
Re-Entry from P.0-Actor-Deferral (Trigger 1: Composition-Aktivierung).
|
||||
v0.1 is a minimal slice; intentional non-goals:
|
||||
|
||||
- **No Body-Slot-Aggregator.** Phase D / J trigger.
|
||||
- **No Walk-Capability** (per ADR-0002 capabilities-as-derived).
|
||||
Waits for a second actor-consumer that needs capability-routing.
|
||||
- **No Movement-State on the entity.** vel / dir / direction-flags are
|
||||
ephemeral; kept in `player_control` (or whatever driver). Persisted
|
||||
ones come with a re-entry-trigger.
|
||||
- **No Action-mediated move.** v0.1 uses engine-§11 inert-write directly.
|
||||
Action-routing is the "right" path for game-state-weighty mutations
|
||||
but waits until a "move" Action is registered and event/effect
|
||||
routing matters.
|
||||
|
||||
## API
|
||||
|
||||
### `actor.create(spec)`
|
||||
|
||||
**Syntax:** `actor.create({template: string, properties: table}) -> entity`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
-- Caller defines a template via composition first:
|
||||
composition.define_template{
|
||||
id = "player",
|
||||
properties = {
|
||||
position = {x = 0, y = 0},
|
||||
movement_speed = 200,
|
||||
sprite_color = engine.render.rgb(200, 60, 80),
|
||||
sprite_w = 24,
|
||||
sprite_h = 24,
|
||||
},
|
||||
tags = {"renderable", "player"},
|
||||
}
|
||||
|
||||
-- Then actor.create instantiates:
|
||||
local p = actor.create{
|
||||
template = "player",
|
||||
properties = {
|
||||
position = {x = 400, y = 300},
|
||||
movement_speed = 200,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Description:** Validates that `properties.movement_speed` is a
|
||||
positive number, then delegates to `composition.create`. Loud-Error
|
||||
if `movement_speed` is missing, non-number, or non-positive.
|
||||
|
||||
The caller is responsible for `composition.define_template{id=...}`
|
||||
before invoking. Actor doesn't auto-define templates.
|
||||
|
||||
### `actor.position(handle)`
|
||||
|
||||
**Syntax:** `actor.position(handle) -> x, y`
|
||||
|
||||
**Description:** Returns the actor's current position as two numbers
|
||||
(x, y). Reads via `entity:get_property("position.x"/"position.y")`.
|
||||
Loud-Error if `handle` is nil.
|
||||
|
||||
### `actor.move(handle, dx, dy)`
|
||||
|
||||
**Syntax:** `actor.move(handle, dx: number, dy: number) -> void`
|
||||
|
||||
**Example:**
|
||||
```lua
|
||||
-- Typical caller (player_control update-loop):
|
||||
local speed = actor.movement_speed(player)
|
||||
local move_dx, move_dy = compute_input_direction()
|
||||
actor.move(player, move_dx * speed * dt, move_dy * speed * dt)
|
||||
```
|
||||
|
||||
**Description:** Adds (dx, dy) to the actor's current position via
|
||||
direct inert-write. Caller does dt-scaling and any speed-multiplication
|
||||
upstream. v0.1 uses engine §11 inert-write — Action-routing is a future
|
||||
re-entry trigger (see Scope above).
|
||||
|
||||
### `actor.movement_speed(handle)`
|
||||
|
||||
**Syntax:** `actor.movement_speed(handle) -> number`
|
||||
|
||||
**Description:** Convenience read of the actor's `movement_speed`
|
||||
property (in pixels/sec by convention; caller decides the unit).
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Position-Format**: composite-table at authoring-time
|
||||
(`properties = { position = {x, y} }`), flattened by composition to
|
||||
scalar properties `position.x` and `position.y`. See Phase-A Spec §5
|
||||
A-Q5 revision (2026-06-09).
|
||||
- **movement_speed**: pixels per second. dt-scaling applied at the
|
||||
caller, NOT by `actor.move`.
|
||||
|
||||
## References
|
||||
|
||||
- Spec: `meta/docs/superpowers/specs/2026-06-09-phase-A-inactive-entities-composition-actor-reentry-design.md`
|
||||
- Origin: `meta/docs/superpowers/specs/2026-05-10-p0-actor-deferral.md`
|
||||
(Trigger 1: Composition-Aktivierung)
|
||||
- Architecture: `meta/docs/architecture/composition-model.md` (Actor as
|
||||
Item-with-Body-Slot in Phase D; v0.1 is composition-only wrapper)
|
||||
- ADR-0001 (engine knows verbs, libs bring nouns)
|
||||
- ADR-0002 (capabilities as derived properties — Walk-Capability deferred)
|
||||
- ADR-0010 (read/write asymmetry — v0.1 inert-write escape hatch)
|
||||
- ADR-0038 (API-Doc-Convention)
|
||||
86
init.lua
Normal file
86
init.lua
Normal file
@@ -0,0 +1,86 @@
|
||||
-- =====================================================================
|
||||
-- lib-core.actor v0.1.0 — Position + Movement-Speed wrapper over composition
|
||||
-- See: meta/docs/superpowers/specs/2026-06-09-phase-A-inactive-entities-...
|
||||
--
|
||||
-- v0.1.0 Spine-Cut:
|
||||
-- - actor.create(spec) — validates 'properties.movement_speed' is set
|
||||
-- (positive number) then delegates to composition.create. Caller is
|
||||
-- responsible for composition.define_template{id=<template>, ...}
|
||||
-- before invoking; actor doesn't auto-define templates.
|
||||
-- - actor.position(handle) -> x, y — convenience read for the
|
||||
-- dotted position-properties.
|
||||
-- - actor.move(handle, dx, dy) — direct inert-write to position.x /
|
||||
-- position.y. dt-scaling is the caller's job (typically
|
||||
-- player_control multiplies dx/dy by movement_speed * dt before
|
||||
-- calling).
|
||||
--
|
||||
-- DEFERRED (no consumer yet — wait for re-entry trigger):
|
||||
-- - Body-Slot-Aggregator → Phase D / J trigger
|
||||
-- - Walk-Capability (per ADR-0002)→ second-actor-consumer trigger
|
||||
-- - Movement-State on the entity → kept ephemeral in player_control
|
||||
-- - Action-mediated move → when "move" Action exists and
|
||||
-- event/effect routing matters
|
||||
-- (engine-primitives §11 inert-write
|
||||
-- is the v0.1 escape hatch)
|
||||
-- =====================================================================
|
||||
|
||||
local composition = require("lib-core.composition")
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.create(spec)
|
||||
if type(spec) ~= "table" then
|
||||
error("actor.create: expected table, got " .. type(spec))
|
||||
end
|
||||
if type(spec.properties) ~= "table" then
|
||||
error("actor.create: 'properties' table is required " ..
|
||||
"(must contain at least 'movement_speed' and 'position')")
|
||||
end
|
||||
local ms = spec.properties.movement_speed
|
||||
if type(ms) ~= "number" or ms <= 0 then
|
||||
error("actor.create: 'properties.movement_speed' must be a positive " ..
|
||||
"number (got " .. tostring(ms) .. "); actors need a baseline " ..
|
||||
"speed; callers typically apply dt-scaling before actor.move")
|
||||
end
|
||||
-- composition.create handles position-flattening + template-lookup
|
||||
-- + tag-index registration. We don't add anything else here in v0.1;
|
||||
-- this wrapper exists primarily to enforce the movement_speed
|
||||
-- contract and to mark the entity as an "actor" semantically for
|
||||
-- future Phase-D extensions.
|
||||
return composition.create(spec)
|
||||
end
|
||||
|
||||
function M.position(handle)
|
||||
if handle == nil then
|
||||
error("actor.position: handle must not be nil")
|
||||
end
|
||||
local x = handle:get_property("position.x")
|
||||
local y = handle:get_property("position.y")
|
||||
return x, y
|
||||
end
|
||||
|
||||
-- Direct inert-write to position.x / position.y. Use composition's already-
|
||||
-- declared inert properties; no Action-routing in v0.1 (see DEFERRED note).
|
||||
function M.move(handle, dx, dy)
|
||||
if handle == nil then
|
||||
error("actor.move: handle must not be nil")
|
||||
end
|
||||
if type(dx) ~= "number" or type(dy) ~= "number" then
|
||||
error("actor.move: dx and dy must be numbers")
|
||||
end
|
||||
local x = handle:get_property("position.x")
|
||||
local y = handle:get_property("position.y")
|
||||
handle:set_property("position.x", x + dx)
|
||||
handle:set_property("position.y", y + dy)
|
||||
end
|
||||
|
||||
-- Convenience: read the actor's movement_speed property (in pixels/sec
|
||||
-- by convention; caller decides what "pixel" means).
|
||||
function M.movement_speed(handle)
|
||||
if handle == nil then
|
||||
error("actor.movement_speed: handle must not be nil")
|
||||
end
|
||||
return handle:get_property("movement_speed")
|
||||
end
|
||||
|
||||
return M
|
||||
1
manifest.lib
Normal file
1
manifest.lib
Normal file
@@ -0,0 +1 @@
|
||||
{"id":"lib-core.actor","version":"0.1.0","api_min":"0.1","deps":[{"id":"lib-core.composition","version":"0.1.0"}]}
|
||||
Reference in New Issue
Block a user