Fireball and locked chest
These two components are the long-form proof of typed OpalScript: definition capture, Scene.SpawnConfigured, native Motion tasks, committed Health commands, and a saved GameState transaction. Paste the patterns into Script Studio; [Test] methods run in the Tests panel.
Fireball
Add a typed ability component and native Motion to the caster. Assign a projectile prefab (Sprite Renderer, Collider 2D, a gravity-free rigid body with continuous collision detection, and a projectile script) to a typed Asset<blueprint> property.
Call Task<Entity> cast = ability.Cast(target) from script, or invoke Cast from Flow with a vector target. Here target is the projectile's spawn position. The ability copies its immutable definition, owns a native Motion charge, then uses Scene.SpawnConfigured to set the projectile's damage, speed and owner before Start. Editing the ability's definition during the charge affects the next cast. The released projectile continues independently of the cast.
Repeat calls replace the previous charge. Canceling, destroying the caster, reloading its source or replacing the scene before release cancels the native motion and runs cleanup. An assigned prefab that cannot be resolved or is not prepared produces a failed task and no released projectile. An unset Projectile or missing Motion returns a completed task whose result is null. The charge position and runtime Charging flag are restored after a charge.
Record properties are edited as named fields in the Inspector. Nested assets use pickers filtered by their declared kind, optional values have a presence switch, and lists of records expose the same controls for each item. Each edit replaces the immutable definition; missing assets, unknown enum cases and extra saved record members remain preserved until explicitly edited.
The projectile moves along positive world X at its configured speed through native fixed-step physics. A real collision calls Health.TakeDamageResult; only an applied committed result retires the projectile after a hit. Later contact ticks cannot repeat that hit.
Locked chest
Add LockedChest and native Motion to a chest and LootWallet to an actor. Call chest.Open(actor). Its Task<ChestOutcome> returns Opened, MissingKey, OutOfRange, Busy or AlreadyOpen.
The synchronous wallet operation validates the key and nonnegative reward, consumes one key, increments coins, and records the opened-chest count. The chest then commits Unlocked and CommittedLoot before awaiting its opening motion. No suspension or notification occurs inside that synchronous commit. Canceling or failing presentation afterward leaves the committed loot intact; another Open returns AlreadyOpen. The transient Opening flag is runtime state and is cleared by finally.
The scene-scoped version keeps unlocked, committed loot, keys and coins in component properties. The saved-game variant adds SavedLockedChest and SavedLootWallet. Use one text GameState slot named ChestProgress with saved lifetime and an explicit version-1 ChestSave default. The typed Game API handles its record encoding; gameplay code works with ChestSave values.
Both saved components select that declared field using the same SaveKey. Game.GetData<ChestSave> validates and snapshots its record; Game.SetData<ChestSave> commits keys, coins, opened count, unlock state and loot in one canonical GameState write. Observers and the existing save-store adapter receive the complete transaction. The example rejects unsupported versions with UnsupportedSave; it never silently resets progress.
What these examples prove
Definition capture, native Motion completion, pre-Start configuration, cancellation, owner destruction, typed rejections, and exactly-once loot. The saved-chest path replaces the scene, disposes the world, then opens another world against the same save adapter and proves the loot remains granted exactly once. The example models one chest and wallet in one save record; a larger inventory system should define its own aggregate transaction model.
The examples use explicit Entity arguments and typed assets. Named project input actions, UI element ids, and GameState variable names still use their existing string APIs.
Data definitions have explicit expansion limits: 16 nested levels, 4,096 expanded nodes and 262,144 identifier characters per schema. A compilation allows 65,536 expanded nodes and 2,097,152 identifier characters across declarations and component defaults. Cycles are rejected. These bounds keep shared nested definitions from multiplying into unbounded defaults or editor schemas.