Skip to content

RUNE-1010: Story Sprite Reuse Proposed

Updated2026-08-23

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

yaml
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: $character

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

yaml
- field@name: type
  default: ...
  required: true
  description: ...
  animatable: true
  typeSpecificConstraint: ...

- action@name:
    - field@input: type
  script:
    # private Actions

Composite 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

yaml
- create@cell:
    ./sprites/split-flap: A
    perspective: 900
    x: 120
    y: 270
    scale: 2
  with: $fade

- wait: $cell

The 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;
  • with opacity 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:

text
CompositeDefinition
  dynamic descriptor
  initialization StoryScriptTemplate
  Action descriptors
    StoryScriptTemplate

Cached 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 LayerSprite composition;
  • 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

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

ConsumerBounds
parent layoutfixed canvas NominalBounds
relative transform originNominalBounds
private percentagescanvas dimensions
culling/allocation/damagedynamic VisualBounds
visible overflowVisualBounds 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:

yaml
- create@board:
    layer:
      - ./sprites/split-flap@left: A
      - ./sprites/split-flap@right: B
  with: $fade

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

Rune Project brings Rune Story authoring together with the Rune Engine rendering foundation.