RUNE-1010: Story Sprite Reuse Proposed
Summary
A .sprite.yaml file compiles into an immutable CompositeDefinition. Each construction materializes an independent CompositeSprite: one public Sprite with a private Sprite composition, persistent Fields, script-backed Actions, lexical execution scopes, and deterministic lifetime.
Design Scope and Style Validation
Composite compilation receives the immutable DesignScope assembled for the containing Story. Before private-script compilation, descriptor discovery reads public members, typed semantic Composite Fields, inherited Sprite Fields, and Primary shape. A style: $name or $design:name initializer is then validated against that descriptor and expanded into canonical sibling construction Initializers; structural children, actions, timelines, nested styles, and unknown fields are invalid.
Style values MUST preserve symbolic DesignValue references through layering. Expansion MUST request the concrete target Field's exact ordinal StoryTypeId; CLR assignability MUST NOT choose among same-CLR facets. Cross-semantic conversion requires explicit typed construction and MUST NOT occur while directly assigning a facet.
Design imports use extensionless package-confined .//../ paths and .design.yaml is implicit. Layering is DFS/post-order with transitive deduplication; compatible later layers win. StyleRef bases resolve late after all layers. Effective precedence is instance Field, instance Primary, style Field, base chain, then intrinsic default. Styles and tokens are compile-time only and never become runtime binding or animation endpoints. Definition/use-site source origins and spans remain attached to expanded values for diagnostics and resource resolution. Animation and voice facets are resolved at compile time into frozen copy-on-use refs; cached Composite definitions do not retain named animation/voice registries.
Definition Shape
sprite: v1beta1
name: split-flap
description: A physical split-flap character cell.
canvas: 52x76
members:
- field@character: string
default: " "
- field@perspective: number
default: 760
minExclusive: 0
animatable: true
- action@flip:
- field@to: string
required: true
- field@duration: duration
default: 0.083167s
script:
- set:
nextUpper.glyph.text: $to
back.glyph.text: $to
- set@turn:
front.rotationX: 180
back.rotationX: 0
with:
tween: $duration
easing: 0,0,1,1
- wait: $turn
script:
- create@front:
layer:
- text@glyph: $characterThe root MUST be sprite: v1beta1. canvas and script are required. name, description, and members are optional. Unknown root Fields MUST fail compilation.
Public Members
members is an ordered heterogeneous list:
- field@name: type
default: ...
required: true
description: ...
animatable: true
typeSpecificConstraint: ...
- action@name:
- field@input: type
script:
# private ActionsComposite Fields are persistent semantic public properties. They MAY be targeted by set, evaluated with $, used as = sources, and animated only when declared animatable: true. They join inherited Sprite Fields in the public construction/property namespace. Phase 1 animation is limited to number Fields.
Action Fields are immutable invocation input. The first declared Field is the Action Primary. Action Fields MUST reject animatable. Each invocation MUST receive a fresh immutable frame and fresh mutable Action graph.
The first Composite Field is construction Primary. Declaration order is public schema. A declared Field and Action MUST NOT share a name. Declared Fields MUST NOT collide with inherited Sprite properties. Public members MUST NOT collide with statically declared private Sprite identities. Direct and indirect Action cycles MUST fail definition compilation.
required: true MUST NOT be combined with default. Declared values and constraints MUST be converted and validated during definition/construction preflight. Intrinsic type values apply only when the type defines one.
Composite Field Type IDs MUST resolve only from the sealed compile-time StoryValueTypeRegistry; arbitrary Story object descriptors MUST NOT be fallback value types. CompositeValueTypeDescriptor MUST remain a thin view over the registered descriptor. The descriptor owns context-aware literal/resolved conversion, CLR/nullability validation, tri-state intrinsic defaults, constraints, and validate/freeze/revalidate behavior for declared defaults, styles, construction values, and Action inputs. Structural array MUST deep-freeze sequences; ref MUST use immutable StoryRefValue.
Construction
- create@cell:
./sprites/split-flap: A
perspective: 900
x: 120
y: 270
scale: 2
with: $fade
- wait: $cellThe local Type ID MUST be explicit, extensionless, and file-relative. A binds the first declared Field. perspective binds another Composite Field. x, y, and scale bind inherited Sprite properties. Declared and inherited Fields share one effective namespace.
The Primary value MUST NOT expand into Initializers. Mapping bags and hybrid projections do not exist.
Every named create MUST expose exactly one aggregate completion source. A synchronous create MUST expose a pre-completed aggregate. Composite construction MUST join:
- recursive nested Composite initialization timelines;
withopacity fade-in;- any other blocking finite construction work.
Anonymous create MUST expose anonymous aggregate work when initialization blocks.
Immutable Definitions and Fresh Instances
Definitions MUST be parsed, compiled, and cached once by canonical path:
CompositeDefinition
dynamic descriptor
initialization StoryScriptTemplate
Action descriptors
StoryScriptTemplateCached definitions MUST contain no mutable instance or invocation state. Construction and Action invocation MUST seed effective values, apply Primary and Initializers, validate, then materialize a fresh runtime graph.
Template value expressions remain symbolic until materialization. Sprite/layout/property references MUST resolve against the invocation execution scope. Source spans and immutable origins MUST survive materialization.
Private Composition and Encapsulation
Each CompositeSprite owns:
- one private
LayerSpritecomposition; - a private layout namespace;
- a private property binding graph;
- a lexical scope containing
self; - one initialization timeline and zero or more Action timelines;
- detached lifetime work;
- permanent-removal cleanup;
- the immutable definition/source origin.
Parent scripts MUST NOT target private Sprite or layout identities. Private lookup MUST NOT fall back to parent/root identities. Public Fields and Actions are the supported boundary.
self.action invokes another Action on the same Composite. child.action invokes a public Action on a private nested CompositeSprite. Each call receives a new Field frame and completion scope while sharing the owning Composite's persistent state and private composition.
Action-created private Sprites MUST persist after Action completion and remain visible to later Actions. Concurrent invocations MUST remain independent except for this shared composition and state; duplicate-name conflicts MUST be diagnosed.
Values and Persistent Bindings
- set:
copy.opacity: $source.opacity
mirror.opacity: =source.opacity$ evaluates the current model target once. = creates a persistent one-way relation whose CLR type and ordinal StoryTypeId both match exactly. Valid sources are the current Composite's public Fields, private Sprite properties, nested Composite public Fields, and endpoint aliases supplied through Action Fields. Parent/root state and nested private internals MUST remain inaccessible.
Binding installation and propagation MUST be transactional and acyclic. Direct target writes MUST break only the target's incoming edge and preserve outgoing propagation. Destroying an endpoint MUST remove incident edges while downstream targets retain their last values.
The animation intent of a source assignment MUST propagate through the complete downstream DAG. The owning assignment MUST aggregate the maximum settle/rest timing across every animated endpoint, including nested private scopes.
Canvas and Bounds
Composite canvas: WxH MUST use finite positive absolute dimensions. It defines private (0,0) and percentage resolution. Nested Composites establish nested canvas contexts. Canvas MUST NOT clip overflow.
CompositeSprite is Layer-like and inherits:
x,y,pos;opacity;scale,rotation,transformOrigin.
It MUST NOT expose width, height, size, align, or fit in Phase 1. Resizing uses scale.
Two bounds channels are required:
| Consumer | Bounds |
|---|---|
| parent layout | fixed canvas NominalBounds |
| relative transform origin | NominalBounds |
| private percentages | canvas dimensions |
| culling/allocation/damage | dynamic VisualBounds |
| visible overflow | VisualBounds outside nominal canvas |
An empty Composite MUST retain canvas NominalBounds and have empty VisualBounds. Private commits MUST update VisualBounds without changing NominalBounds. Outer opacity and transforms MUST apply once around the private composition.
Recursive Construction
Nested Layer children MAY be CompositeSprites:
- create@board:
layer:
- ./sprites/split-flap@left: A
- ./sprites/split-flap@right: B
with: $fadeSpriteConstruction MUST return the runtime Sprite plus pending nested initialization plans. The nearest owning construction Action starts those timelines and joins them to its aggregate. LayerSprite MUST NOT schedule timelines itself.
Any action materializing Sprite construction MUST host recursive initialization or reject Composite input. Phase 1 MUST reject CompositeSprite morph sources/targets, including containing Layer subtrees.
Source Origin and Package Safety
Each definition MUST retain canonical package root, defining file, base directory, and package-relative display path. Nested definitions and their script nodes MUST preserve their own origins.
Local references and resources MUST resolve from the declaring file's base directory and remain inside the entry package root. The implementation MUST cache by canonical path and diagnose cycles with package-relative / paths. Runtime instantiation MUST NOT mutate cached origins.
Completion and Lifetime
Initialization and Action bodies run on Composite-owned child timelines. The caller sees one create or Action aggregate; nested phase/media timing is projected lexically without duplicate done ownership.
Blocking finite work joins the aggregate. Detached lifetime work transfers to the Composite owner and MUST NOT block invocation completion. Permanent removal MUST cancel timelines and detached work, dispose lifetimes, remove bindings, and permanently remove private Sprites. Late callbacks MUST NOT mutate removed state.
Phase 1 Limits
Iteration, ranges, interpolation, standalone Action files, slots, anchors, events, invocation-local automatic Sprite cleanup, independently playable nested stories, and Composite morph are deferred.
Rationale
CompositeSprite preserves the parent Story's Sprite model while encapsulating reusable visual behavior. Fixed nominal canvas gives stable layout, dynamic visual bounds preserve overflow and damage correctness, and lexical timelines/bindings make repeated Actions deterministic without exposing implementation identities.