@almadar/std 16.182.0 → 16.184.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/behaviors/lolo/infra/atoms/std-migration-job.lolo +2 -2
  2. package/behaviors/lolo/ui/core/.hand-authored.json +4 -0
  3. package/behaviors/lolo/ui/core/atoms/std-browse.lolo +11 -0
  4. package/behaviors/lolo/ui/core/atoms/std-related.lolo +86 -9
  5. package/behaviors/lolo/ui/core/atoms/std-rich-editor.lolo +115 -1
  6. package/behaviors/lolo/ui/core/atoms/std-tag-taxonomy.lolo +62 -2
  7. package/behaviors/lolo/ui/core/atoms/std-thread.lolo +43 -2
  8. package/behaviors/lolo/ui/core/atoms/std-tree.lolo +247 -0
  9. package/behaviors/lolo/ui/core/atoms/std-version-history.lolo +110 -2
  10. package/behaviors/lolo/ui/core/molecules/ui-file-tree.lolo +18 -2
  11. package/behaviors/lolo/ui/core/organisms/ui-detail-panel.lolo +6 -2
  12. package/behaviors/lolo/ui/core/organisms/ui-form-section.lolo +2 -2
  13. package/behaviors/lolo/ui/core/organisms/ui-form.lolo +2 -2
  14. package/behaviors/lolo/ui/game/atoms/std-fx-trail.lolo +63 -0
  15. package/behaviors/lolo/ui/game/atoms/std-gate-lock.lolo +96 -0
  16. package/behaviors/lolo/ui/game/atoms/std-lives.lolo +121 -0
  17. package/behaviors/lolo/ui/game/atoms/std-patrol-hazard.lolo +96 -0
  18. package/behaviors/lolo/ui/game/atoms/std-tutorial-autodemo.lolo +136 -0
  19. package/behaviors/lolo/ui/game/atoms/std-tutorial-overlay.lolo +197 -0
  20. package/behaviors/lolo/ui/learning/atoms/std-learn-slope-probe.lolo +117 -0
  21. package/behaviors/lolo/ui/learning/atoms/ui-math-canvas.lolo +18 -0
  22. package/behaviors/registry/infra/atoms/std-migration-job.orb +2 -2
  23. package/behaviors/registry/ui/core/atoms/std-browse.orb +17 -0
  24. package/behaviors/registry/ui/core/atoms/std-related.orb +256 -3
  25. package/behaviors/registry/ui/core/atoms/std-rich-editor.orb +407 -1
  26. package/behaviors/registry/ui/core/atoms/std-tag-taxonomy.orb +240 -8
  27. package/behaviors/registry/ui/core/atoms/std-thread.orb +254 -4
  28. package/behaviors/registry/ui/core/atoms/std-tree.orb +656 -0
  29. package/behaviors/registry/ui/core/atoms/std-version-history.orb +479 -6
  30. package/behaviors/registry/ui/core/molecules/ui-file-tree.orb +43 -19
  31. package/behaviors/registry/ui/core/organisms/ui-detail-panel.orb +28 -3
  32. package/behaviors/registry/ui/core/organisms/ui-form-section.orb +8 -2
  33. package/behaviors/registry/ui/core/organisms/ui-form.orb +8 -2
  34. package/behaviors/registry/ui/game/atoms/std-fx-trail.orb +376 -0
  35. package/behaviors/registry/ui/game/atoms/std-gate-lock.orb +852 -0
  36. package/behaviors/registry/ui/game/atoms/std-lives.orb +739 -0
  37. package/behaviors/registry/ui/game/atoms/std-patrol-hazard.orb +768 -0
  38. package/behaviors/registry/ui/game/atoms/std-tutorial-autodemo.orb +732 -0
  39. package/behaviors/registry/ui/game/atoms/std-tutorial-overlay.orb +1212 -0
  40. package/behaviors/registry/ui/learning/atoms/std-learn-slope-probe.orb +908 -0
  41. package/behaviors/registry/ui/learning/atoms/ui-math-canvas.orb +26 -0
  42. package/dist/behaviors/exports-reader.js +5478 -2954
  43. package/dist/behaviors/functions/index.d.ts +763 -15
  44. package/dist/behaviors/functions/index.js +5447 -2953
  45. package/dist/behaviors/index.d.ts +1 -1
  46. package/dist/behaviors/index.js +5479 -2955
  47. package/dist/behaviors/query.js +5478 -2954
  48. package/dist/behaviors-embeddings.hash.json +3 -3
  49. package/dist/behaviors-embeddings.json +362 -330
  50. package/dist/behaviors-registry.json +2812 -542
  51. package/dist/index.d.ts +1 -1
  52. package/dist/index.js +5479 -2955
  53. package/dist/knob-embeddings.hash.json +3 -3
  54. package/dist/knob-embeddings.json +1 -1
  55. package/dist/registry/factory-signatures.json +1 -1
  56. package/package.json +2 -2
@@ -0,0 +1,96 @@
1
+ app std-gate-lock "1.0.0"
2
+ "Headless collectible-gate mechanic: doors that stay shut until the player's collected total reaches their cost. Listens COLLECT (the board's running total — stars, coins, keys) and flips every door whose cost is met, stamping open on the shared door rows and emitting GATE_OPENED for the first newly opened door (the shared entity is the render authority for the rest). While a closed door's rect contains the player it emits GATE_BLOCKED once per approach — re-arming only after the player steps out — so the board can answer with a PLACE back and a locked fx. RESTART re-locks every door and zeroes the total. Renders nothing; boards draw closed doors as bars/walls and open ones as gaps."
3
+ orbital GateLockOrbital {
4
+ type Door = {
5
+ id : string!
6
+ x : number!
7
+ y : number!
8
+ width : number!
9
+ height : number!
10
+ cost : number!
11
+ open : boolean
12
+ touching : boolean
13
+ blocked : boolean
14
+ }
15
+
16
+ entity GateLockData [runtime, shared] {
17
+ id : string!
18
+ doors : [Door] = []
19
+ total : number = 0
20
+ }
21
+
22
+ trait GateLock -> @rebindable GateLockData [lifecycle] {
23
+ initial: active
24
+
25
+ state active {
26
+ INIT -> active
27
+ (set @entity.doors (array/map @config.doors (fn d (object/merge @d { open: false, touching: false, blocked: false }))))
28
+ (set @entity.total 0)
29
+
30
+ COLLECT -> active when @config.running
31
+ (set @entity.total @payload.total)
32
+ (let ((newly (array/filter @entity.doors (fn d (and (== (object/get @d open) false) (<= (object/get @d cost) @payload.total))))))
33
+ (if (> (array/len @newly) 0)
34
+ (do
35
+ (set @entity.doors (array/map @entity.doors (fn d (if (and (== (object/get @d open) false) (<= (object/get @d cost) @payload.total)) (object/merge @d { open: true }) @d))))
36
+ (emit GATE_OPENED { id: (object/get (array/first @newly) id), cost: (object/get (array/first @newly) cost) }))
37
+ null))
38
+
39
+ BODY_MOVED -> active when @config.running
40
+ (set @entity.doors (array/map @entity.doors (fn d
41
+ (let ((inside (and (== (object/get @d open) false)
42
+ (and (>= @payload.x (- (object/get @d x) @config.reachMargin))
43
+ (and (<= @payload.x (+ (object/get @d x) (object/get @d width) @config.reachMargin))
44
+ (and (>= @payload.y (- (object/get @d y) @config.reachMargin))
45
+ (<= @payload.y (+ (object/get @d y) (object/get @d height) @config.reachMargin))))))))
46
+ (object/merge @d { touching: @inside, blocked: (and @inside (== (object/get @d touching) false)) })))))
47
+ (let ((blocked (array/filter @entity.doors (fn d (== (object/get @d blocked) true)))))
48
+ (if (> (array/len @blocked) 0)
49
+ (emit GATE_BLOCKED { id: (object/get (array/first @blocked) id), x: (object/get (array/first @blocked) x), y: (object/get (array/first @blocked) y) })
50
+ null))
51
+
52
+ RESTART -> active
53
+ (set @entity.doors (array/map @config.doors (fn d (object/merge @d { open: false, touching: false, blocked: false }))))
54
+ (set @entity.total 0)
55
+ }
56
+
57
+ emits {
58
+ GATE_OPENED -> external {
59
+ id : string!
60
+ cost : number!
61
+ }
62
+ @description "A door's cost was reached by the collected total and it flipped open; boards layer unlock fx. When several doors open on one total, the first reports and the shared entity carries the rest."
63
+ @tier "domain"
64
+ GATE_BLOCKED -> external {
65
+ id : string!
66
+ x : number!
67
+ y : number!
68
+ }
69
+ @description "The player is inside a closed door's rect; fires once per approach and re-arms after they step out. Boards answer with a PLACE back plus a locked fx."
70
+ @tier "domain"
71
+ }
72
+
73
+ listens {
74
+ COLLECT { total : number! }
75
+ BODY_MOVED { x : number!, y : number!, vx : number!, vy : number!, grounded : boolean!, skating : boolean!, speed : number! }
76
+ RESTART { id : string }
77
+ }
78
+
79
+ config {
80
+ running : boolean = true
81
+ @label "Running"
82
+ @description "When true, COLLECT opens doors and closed doors report contact; toggle to freeze every door as-is."
83
+ @tier "domain"
84
+ doors : [Door] = []
85
+ @label "Doors"
86
+ @description "One row per gate: id, min-corner rect (x, y, width, height in world units) and the collected total that opens it. open/touching are stamped at runtime."
87
+ @tier "domain"
88
+ reachMargin : number = 0.6
89
+ @label "Reach Margin"
90
+ @description "Extra world units padded around a closed door's rect when testing player contact (approximates the body's size)."
91
+ @tier "domain"
92
+ }
93
+ }
94
+
95
+ page "/gate-lock" as GateLockPage -> GateLock
96
+ }
@@ -0,0 +1,121 @@
1
+ app std-lives "1.0.0"
2
+ "Headless lives + checkpoint authority for boards with stakes: owns lives, the current respawn point, and nothing else. DAMAGE (call sites rename their hazard sources — BODY_HAZARD, BODY_FELL, PATROL_HIT — onto it) costs one life, respecting an invulnerability window so overlapping hazards can't chain-drain; losing a life emits LIFE_LOST plus RESPAWN with the current checkpoint (boards route RESPAWN to the body authority's PLACE), and losing the last emits GAME_OVER instead. Checkpoints advance automatically: BODY_MOVED crossing a configured checkpoint's atX moves the respawn point forward and emits CHECKPOINT_REACHED once per checkpoint (tracked by furthest atX reached, so checkpoints are ordered along the run). RESTART reseeds lives and respawn to the spawn. Renders nothing — HUDs read lives off the shared entity."
3
+ orbital LivesOrbital {
4
+ type Checkpoint = {
5
+ id : string!
6
+ atX : number!
7
+ x : number!
8
+ y : number!
9
+ }
10
+
11
+ entity LivesData [runtime, shared] {
12
+ id : string!
13
+ lives : number = 3
14
+ respawnX : number = 0
15
+ respawnY : number = 0
16
+ cpX : number = -1000000
17
+ lastHitAt : number = 0
18
+ }
19
+
20
+ trait Lives -> @rebindable LivesData [lifecycle] {
21
+ initial: active
22
+
23
+ state active {
24
+ INIT -> active
25
+ (set @entity.lives @config.maxLives)
26
+ (set @entity.respawnX @config.spawnX)
27
+ (set @entity.respawnY @config.spawnY)
28
+ (set @entity.cpX (- 0 1000000))
29
+ (set @entity.lastHitAt 0)
30
+
31
+ DAMAGE -> active when (and @config.running (> @entity.lives 0))
32
+ (if (>= (- (time/now) @entity.lastHitAt) @config.invulnMs)
33
+ (do
34
+ (set @entity.lastHitAt (time/now))
35
+ (set @entity.lives (- @entity.lives 1))
36
+ (if (> @entity.lives 0)
37
+ (do
38
+ (emit LIFE_LOST { lives: @entity.lives })
39
+ (emit RESPAWN { x: @entity.respawnX, y: @entity.respawnY }))
40
+ (emit GAME_OVER { id: "lives" })))
41
+ null)
42
+
43
+ BODY_MOVED -> active when @config.running
44
+ (let ((reached (array/filter @config.checkpoints (fn c (and (<= (object/get @c atX) @payload.x) (> (object/get @c atX) @entity.cpX))))))
45
+ (if (> (array/len @reached) 0)
46
+ (let ((cp (array/last @reached)))
47
+ (do
48
+ (set @entity.respawnX (object/get @cp x))
49
+ (set @entity.respawnY (object/get @cp y))
50
+ (set @entity.cpX (object/get @cp atX))
51
+ (emit CHECKPOINT_REACHED { id: (object/get @cp id) })))
52
+ null))
53
+
54
+ RESTART -> active
55
+ (set @entity.lives @config.maxLives)
56
+ (set @entity.respawnX @config.spawnX)
57
+ (set @entity.respawnY @config.spawnY)
58
+ (set @entity.cpX (- 0 1000000))
59
+ (set @entity.lastHitAt 0)
60
+ }
61
+
62
+ emits {
63
+ LIFE_LOST -> external {
64
+ lives : number!
65
+ }
66
+ @description "A life was spent (post-invulnerability check); carries the lives remaining. Boards layer damage fx / HUD heartbeat."
67
+ @tier "domain"
68
+ RESPAWN -> external {
69
+ x : number!
70
+ y : number!
71
+ }
72
+ @description "The checkpoint the body should restart from after this life loss; boards forward it to the body authority's PLACE."
73
+ @tier "domain"
74
+ GAME_OVER -> external {
75
+ id : string!
76
+ }
77
+ @description "The last life was spent; boards show the game-over overlay and wait for RESTART."
78
+ @tier "domain"
79
+ CHECKPOINT_REACHED -> external {
80
+ id : string!
81
+ }
82
+ @description "The player crossed a checkpoint's atX for the first time this run; the respawn point moved to it. Boards layer flag/save fx."
83
+ @tier "domain"
84
+ }
85
+
86
+ listens {
87
+ DAMAGE { source : string }
88
+ BODY_MOVED { x : number!, y : number!, vx : number!, vy : number!, grounded : boolean!, skating : boolean!, speed : number! }
89
+ RESTART { id : string }
90
+ }
91
+
92
+ config {
93
+ running : boolean = true
94
+ @label "Running"
95
+ @description "When true, DAMAGE and checkpoint crossings are processed; toggle to make the run harmless (practice mode)."
96
+ @tier "domain"
97
+ maxLives : number = 3
98
+ @label "Max Lives"
99
+ @description "Lives seeded on INIT/RESTART; reaching zero fires GAME_OVER."
100
+ @tier "domain"
101
+ invulnMs : number = 1500
102
+ @label "Invulnerability Window (ms)"
103
+ @description "Milliseconds after a accepted hit during which further DAMAGE is ignored, so a hazard overlapping the respawn can't chain-drain."
104
+ @tier "domain"
105
+ spawnX : number = 0
106
+ @label "Spawn X"
107
+ @description "Respawn x before any checkpoint is reached (match the body authority's spawn)."
108
+ @tier "domain"
109
+ spawnY : number = 0
110
+ @label "Spawn Y"
111
+ @description "Respawn y before any checkpoint is reached."
112
+ @tier "domain"
113
+ checkpoints : [Checkpoint] = []
114
+ @label "Checkpoints"
115
+ @description "Ordered along the run: when the player's x crosses atX the respawn point moves to (x, y). Must be sorted by atX ascending."
116
+ @tier "domain"
117
+ }
118
+ }
119
+
120
+ page "/lives" as LivesPage -> Lives
121
+ }
@@ -0,0 +1,96 @@
1
+ app std-patrol-hazard "1.0.0"
2
+ "Headless patrol-hazard mechanic for side-view boards: moves each configured patrol back and forth along its [x0, x1] beat at its own speed (33ms tick, ping-pong with clamped turnarounds), tracks the player position off the body authority's BODY_MOVED, and emits PATROL_HIT once per contact — a per-patrol hit flag re-arms only after the player separates, so damage rules stay sparse (when two patrols connect on the same tick the first reports; the other reports on the next tick). Patrols live on a [runtime, shared] entity for the render layer (boards draw them as markers/sprites); damage, lives, and respawn stay downstream (std-lives) — this atom simulates, it never judges. Renders nothing."
3
+ orbital PatrolHazardOrbital {
4
+ type Patrol = {
5
+ id : string!
6
+ x0 : number!
7
+ x1 : number!
8
+ y : number!
9
+ speed : number!
10
+ x : number
11
+ dir : number
12
+ touching : boolean
13
+ hit : boolean
14
+ }
15
+
16
+ entity PatrolState [runtime, shared] {
17
+ id : string!
18
+ patrols : [Patrol] = []
19
+ px : number = 0
20
+ py : number = 0
21
+ }
22
+
23
+ trait PatrolHazard -> @rebindable PatrolState [lifecycle] {
24
+ initial: active
25
+
26
+ state active {
27
+ INIT -> active
28
+ (set @entity.patrols (array/map @config.patrols (fn p (object/merge @p { x: (object/get @p x0), dir: 1, touching: false, hit: false }))))
29
+ (set @entity.px @config.playerSpawnX)
30
+ (set @entity.py @config.playerSpawnY)
31
+
32
+ BODY_MOVED -> active
33
+ (set @entity.px @payload.x)
34
+ (set @entity.py @payload.y)
35
+
36
+ RESTART -> active
37
+ (set @entity.patrols (array/map @config.patrols (fn p (object/merge @p { x: (object/get @p x0), dir: 1, touching: false, hit: false }))))
38
+ }
39
+
40
+ ticks {
41
+ patrolTick every 33ms
42
+ when (and @config.running (> (array/len @entity.patrols) 0))
43
+ (set @entity.patrols (array/map @entity.patrols (fn p
44
+ (let ((nx (+ (object/get @p x) (* (* (object/get @p dir) (object/get @p speed)) 0.033))))
45
+ (let ((overshoot (> @nx (object/get @p x1))) (undershoot (< @nx (object/get @p x0))))
46
+ (let ((cx (if @overshoot (object/get @p x1) (if @undershoot (object/get @p x0) @nx)))
47
+ (cdir (if @overshoot -1 (if @undershoot 1 (object/get @p dir)))))
48
+ (let ((near (and (<= (math/abs (- @entity.px @cx)) @config.hitRadius) (<= (math/abs (- @entity.py (object/get @p y))) @config.hitRadius))))
49
+ (object/merge @p { x: @cx, dir: @cdir, hit: (and @near (== (object/get @p touching) false)), touching: @near }))))))))
50
+ (let ((hits (array/filter @entity.patrols (fn p (== (object/get @p hit) true)))))
51
+ (if (> (array/len @hits) 0)
52
+ (emit PATROL_HIT { id: (object/get (array/first @hits) id), x: (object/get (array/first @hits) x), y: (object/get (array/first @hits) y) })
53
+ null))
54
+ }
55
+
56
+ emits {
57
+ PATROL_HIT -> external {
58
+ id : string!
59
+ x : number!
60
+ y : number!
61
+ }
62
+ @description "A patrol made contact with the player this tick; fires once per contact and re-arms only after separation — boards route it to their damage rule (std-lives DAMAGE)."
63
+ @tier "domain"
64
+ }
65
+
66
+ listens {
67
+ BODY_MOVED { x : number!, y : number!, vx : number!, vy : number!, grounded : boolean!, skating : boolean!, speed : number! }
68
+ RESTART { id : string }
69
+ }
70
+
71
+ config {
72
+ running : boolean = true
73
+ @label "Running"
74
+ @description "When true the 33ms tick moves patrols and tests contact; toggle to freeze every patrol in place."
75
+ @tier "domain"
76
+ patrols : [Patrol] = []
77
+ @label "Patrols"
78
+ @description "One row per patroller: id, the [x0, x1] beat it walks, its fixed y, and its speed in units/s. Current x/dir/touching/hit are stamped at runtime."
79
+ @tier "domain"
80
+ hitRadius : number = 0.7
81
+ @label "Hit Radius"
82
+ @description "Per-axis distance (world units) within which a patrol counts as touching the player; smaller values demand precise contact, larger values forgive."
83
+ @tier "domain"
84
+ playerSpawnX : number = 0
85
+ @label "Player Spawn X"
86
+ @description "Player x assumed before the first BODY_MOVED arrives (match the body authority's spawn)."
87
+ @tier "domain"
88
+ playerSpawnY : number = 0
89
+ @label "Player Spawn Y"
90
+ @description "Player y assumed before the first BODY_MOVED arrives."
91
+ @tier "domain"
92
+ }
93
+ }
94
+
95
+ page "/patrol-hazard" as PatrolHazardPage -> PatrolHazard
96
+ }
@@ -0,0 +1,136 @@
1
+ app std-tutorial-autodemo "1.0.0"
2
+ "Generic scripted input-replay atom: the demo half of a tutorial. Holds one or more named cue scripts (each an ordered list of { at, event, arg } rows where at is a millisecond offset and event is LEFT/RIGHT/JUMP/STOP); listens for TUTORIAL_DEMO_START { demoId } (from std-tutorial-overlay's demoId step) and plays the matching script on a 32ms clock, emitting the semantic DEMO_LEFT / DEMO_RIGHT / DEMO_JUMP / DEMO_STOP events the host board maps onto its own movement. TUTORIAL_DEMO_STOP, DEMO_CANCEL (real user input), or exhausting the cue list end the demo and emit DEMO_STOP so the character stops moving. Renders a small Demo badge while playing. Domain-agnostic: the demo never emits board events (OW_*, etc.) — the organism maps DEMO_* to its own steering, so a demo cannot self-cancel through the board's input path."
3
+ orbital TutorialAutoDemoOrbital {
4
+ type DemoCue = {
5
+ at : number!
6
+ event : string!
7
+ arg : number
8
+ }
9
+ type DemoScript = {
10
+ id : string!
11
+ cues : [DemoCue]!
12
+ }
13
+
14
+ entity AutoDemoData [runtime] {
15
+ id : string!
16
+ playing : boolean = false
17
+ scriptId : string = ""
18
+ elapsed : number = 0
19
+ cueIndex : number = 0
20
+ }
21
+
22
+ trait AutoDemo -> @rebindable AutoDemoData [interaction, instance] {
23
+ initial: idle
24
+
25
+ state idle {
26
+ INIT -> idle
27
+ (render-ui hud-top null)
28
+
29
+ TUTORIAL_DEMO_START -> playing
30
+ (set @entity.scriptId @payload.demoId)
31
+ (set @entity.elapsed 0)
32
+ (set @entity.cueIndex 0)
33
+ (set @entity.playing true)
34
+ (render-ui hud-top {
35
+ type: badge
36
+ label: "Demo"
37
+ variant: "info"
38
+ })
39
+
40
+ TUTORIAL_DEMO_STOP -> idle
41
+ (render-ui hud-top null)
42
+
43
+ DEMO_CANCEL -> idle
44
+ (render-ui hud-top null)
45
+ }
46
+
47
+ state playing {
48
+ TUTORIAL_DEMO_START -> playing
49
+ (set @entity.scriptId @payload.demoId)
50
+ (set @entity.elapsed 0)
51
+ (set @entity.cueIndex 0)
52
+ (render-ui hud-top {
53
+ type: badge
54
+ label: "Demo"
55
+ variant: "info"
56
+ })
57
+
58
+ TUTORIAL_DEMO_STOP -> idle
59
+ (set @entity.playing false)
60
+ (render-ui hud-top null)
61
+ (emit DEMO_STOP {})
62
+
63
+ DEMO_CANCEL -> idle
64
+ (set @entity.playing false)
65
+ (render-ui hud-top null)
66
+ (emit DEMO_STOP {})
67
+ }
68
+
69
+ ticks {
70
+ demoTick every 32ms when (== @entity.playing true)
71
+ (set @entity.elapsed (+ @entity.elapsed 32))
72
+ (let ((scriptMatch (array/filter @config.scripts (fn s (== (object/get @s id) @entity.scriptId)))))
73
+ (let ((cueList (if (> (array/len @scriptMatch) 0) (object/get (array/first @scriptMatch) cues []) [])))
74
+ (let ((cue (if (< @entity.cueIndex (array/len @cueList)) (array/nth @cueList @entity.cueIndex) { at: 999999, event: "" })))
75
+ (do
76
+ (if (<= (object/get @cue at 0) @entity.elapsed)
77
+ (do
78
+ (set @entity.cueIndex (+ @entity.cueIndex 1))
79
+ (if (== (object/get @cue event "") "LEFT") (emit DEMO_LEFT {}) null)
80
+ (if (== (object/get @cue event "") "RIGHT") (emit DEMO_RIGHT {}) null)
81
+ (if (== (object/get @cue event "") "JUMP") (emit DEMO_JUMP {}) null)
82
+ (if (== (object/get @cue event "") "STOP") (emit DEMO_STOP {}) null))
83
+ null)
84
+ (if (>= @entity.cueIndex (array/len @cueList))
85
+ (do
86
+ (set @entity.playing false)
87
+ (render-ui hud-top null)
88
+ (emit DEMO_STOP {}))
89
+ null)))))
90
+ }
91
+
92
+ listens {
93
+ TUTORIAL_DEMO_START {
94
+ demoId : string!
95
+ }
96
+ @description "A tutorial step carrying a demoId was shown (std-tutorial-overlay); begin playing the cue script whose id matches demoId."
97
+ @tier "domain"
98
+ TUTORIAL_DEMO_STOP {
99
+ }
100
+ @description "The tutorial left a demo step or was dismissed; stop the demo and release control."
101
+ @tier "domain"
102
+ DEMO_CANCEL {
103
+ }
104
+ @description "Real user input was received by the host board; stop the demo immediately and hand control back to the player."
105
+ @tier "domain"
106
+ }
107
+
108
+ emits {
109
+ DEMO_LEFT -> external {
110
+ }
111
+ @description "Scripted run-left cue; the host board maps this to its own steer-left."
112
+ @tier "domain"
113
+ DEMO_RIGHT -> external {
114
+ }
115
+ @description "Scripted run-right cue; the host board maps this to its own steer-right."
116
+ @tier "domain"
117
+ DEMO_JUMP -> external {
118
+ }
119
+ @description "Scripted jump cue; the host board maps this to its own jump."
120
+ @tier "domain"
121
+ DEMO_STOP -> external {
122
+ }
123
+ @description "Scripted stop cue (or demo end/cancel); the host board maps this to stop movement."
124
+ @tier "domain"
125
+ }
126
+
127
+ config {
128
+ scripts : [DemoScript] = []
129
+ @label "Scripts"
130
+ @description "Named cue scripts keyed by the tutorial step's demoId; each cue fires its event after `at` milliseconds of the demo clock."
131
+ @tier "domain"
132
+ }
133
+ }
134
+
135
+ page "/tutorial-autodemo" as TutorialAutoDemoPage -> AutoDemo
136
+ }
@@ -0,0 +1,197 @@
1
+ app std-tutorial-overlay "1.0.0"
2
+ "Tutorial modal with live-demo steps for game boards and learning shells: a paged overlay — title, a live previewTrait frame playing the mechanic via std-tutorial-autodemo (a bare game canvas, no shell chrome), one line of body copy, a step counter, and Back / Next / Skip — composed entirely from registered render types (stack + text + button + trait frame) into the overlay slot, so no new UI component is needed. autoShow opens it on INIT for first-time players; a board's help button re-opens it any time via SHOW (which rewinds to step one). DISMISS (or finishing the last step) marks the run done, clears the overlay, and emits TUTORIAL_DONE so the board can unpause or log. Steps are pure config — a board teaches a new mechanic by adding one { id, title, body, demoId } row plus a previewTrait binding."
3
+ orbital TutorialOverlayOrbital {
4
+ type TutorialStep = {
5
+ id : string!
6
+ title : string!
7
+ body : string
8
+ demoId : string
9
+ }
10
+
11
+ entity TutorialState [runtime, shared] {
12
+ id : string!
13
+ step : number = 0
14
+ done : boolean = false
15
+ }
16
+
17
+ trait TutorialEmptyPreview -> TutorialState [interaction, instance] {
18
+ initial: idle
19
+
20
+ state idle {
21
+ INIT -> idle
22
+ (render-ui overlay null)
23
+ }
24
+ }
25
+
26
+ trait TutorialOverlay -> @rebindable TutorialState [interaction, instance] {
27
+ initial: hidden
28
+
29
+ state hidden {
30
+ INIT -> hidden
31
+ (if @config.autoShow
32
+ (emit TUT_AUTO { id: "auto" })
33
+ (render-ui overlay null))
34
+
35
+ TUT_AUTO -> showing
36
+ (set @entity.step 0)
37
+ (render-ui overlay {
38
+ type: stack
39
+ direction: vertical
40
+ className: "bg-card text-card-foreground border border-border rounded-lg shadow-lg p-6 max-w-md mx-auto mt-16"
41
+ children: [
42
+ { type: typography, content: (object/get (array/nth @config.steps 0) title), variant: "h4", weight: "bold" }
43
+ @config.previewTrait
44
+ { type: typography, content: (object/get (array/nth @config.steps 0) body), variant: "body" }
45
+ { type: typography, content: (str/concat "1 / " (array/len @config.steps)), variant: "caption", align: "center" }
46
+ { type: stack, direction: horizontal, justify: "center", className: "pt-2", children: [
47
+ { type: button, label: (if (> (array/len @config.steps) 1) "Next ▶" "Got it"), action: "TUT_NEXT", variant: "primary" }
48
+ { type: button, label: "Skip", action: "TUT_DISMISS", variant: "secondary" }
49
+ ] }
50
+ ]
51
+ })
52
+ (when (!= (object/get (array/nth @config.steps 0) demoId "") "")
53
+ (do (emit TUTORIAL_DEMO_START { demoId: (object/get (array/nth @config.steps 0) demoId "") })))
54
+
55
+ SHOW -> showing
56
+ (set @entity.step 0)
57
+ (render-ui overlay {
58
+ type: stack
59
+ direction: vertical
60
+ className: "bg-card text-card-foreground border border-border rounded-lg shadow-lg p-6 max-w-md mx-auto mt-16"
61
+ children: [
62
+ { type: typography, content: (object/get (array/nth @config.steps 0) title), variant: "h4", weight: "bold" }
63
+ @config.previewTrait
64
+ { type: typography, content: (object/get (array/nth @config.steps 0) body), variant: "body" }
65
+ { type: typography, content: (str/concat "1 / " (array/len @config.steps)), variant: "caption", align: "center" }
66
+ { type: stack, direction: horizontal, justify: "center", className: "pt-2", children: [
67
+ { type: button, label: (if (> (array/len @config.steps) 1) "Next ▶" "Got it"), action: "TUT_NEXT", variant: "primary" }
68
+ { type: button, label: "Skip", action: "TUT_DISMISS", variant: "secondary" }
69
+ ] }
70
+ ]
71
+ })
72
+ (when (!= (object/get (array/nth @config.steps 0) demoId "") "")
73
+ (do (emit TUTORIAL_DEMO_START { demoId: (object/get (array/nth @config.steps 0) demoId "") })))
74
+ }
75
+
76
+ state showing {
77
+ TUT_NEXT -> showing when (< (+ @entity.step 1) (array/len @config.steps))
78
+ (set @entity.step (+ @entity.step 1))
79
+ (render-ui overlay {
80
+ type: stack
81
+ direction: vertical
82
+ className: "bg-card text-card-foreground border border-border rounded-lg shadow-lg p-6 max-w-md mx-auto mt-16"
83
+ children: [
84
+ { type: typography, content: (object/get (array/nth @config.steps @entity.step) title), variant: "h4", weight: "bold" }
85
+ @config.previewTrait
86
+ { type: typography, content: (object/get (array/nth @config.steps @entity.step) body), variant: "body" }
87
+ { type: typography, content: (str/concat (+ @entity.step 1) " / " (array/len @config.steps)), variant: "caption", align: "center" }
88
+ { type: stack, direction: horizontal, justify: "center", className: "pt-2", children: [
89
+ { type: button, label: "◀ Back", action: "TUT_BACK", variant: "secondary" }
90
+ { type: button, label: (if (< (+ @entity.step 1) (array/len @config.steps)) "Next ▶" "Got it"), action: "TUT_NEXT", variant: "primary" }
91
+ { type: button, label: "Skip", action: "TUT_DISMISS", variant: "secondary" }
92
+ ] }
93
+ ]
94
+ })
95
+ (emit TUTORIAL_DEMO_STOP {})
96
+ (when (!= (object/get (array/nth @config.steps @entity.step) demoId "") "")
97
+ (do (emit TUTORIAL_DEMO_START { demoId: (object/get (array/nth @config.steps @entity.step) demoId "") })))
98
+
99
+ TUT_NEXT -> hidden when (>= (+ @entity.step 1) (array/len @config.steps))
100
+ (set @entity.done true)
101
+ (render-ui overlay null)
102
+ (emit TUTORIAL_DEMO_STOP {})
103
+ (emit TUTORIAL_DONE { id: "tutorial" })
104
+
105
+ TUT_BACK -> showing when (> @entity.step 0)
106
+ (set @entity.step (- @entity.step 1))
107
+ (render-ui overlay {
108
+ type: stack
109
+ direction: vertical
110
+ className: "bg-card text-card-foreground border border-border rounded-lg shadow-lg p-6 max-w-md mx-auto mt-16"
111
+ children: [
112
+ { type: typography, content: (object/get (array/nth @config.steps @entity.step) title), variant: "h4", weight: "bold" }
113
+ @config.previewTrait
114
+ { type: typography, content: (object/get (array/nth @config.steps @entity.step) body), variant: "body" }
115
+ { type: typography, content: (str/concat (+ @entity.step 1) " / " (array/len @config.steps)), variant: "caption", align: "center" }
116
+ { type: stack, direction: horizontal, justify: "center", className: "pt-2", children: [
117
+ { type: button, label: "◀ Back", action: "TUT_BACK", variant: "secondary" }
118
+ { type: button, label: (if (< (+ @entity.step 1) (array/len @config.steps)) "Next ▶" "Got it"), action: "TUT_NEXT", variant: "primary" }
119
+ { type: button, label: "Skip", action: "TUT_DISMISS", variant: "secondary" }
120
+ ] }
121
+ ]
122
+ })
123
+ (emit TUTORIAL_DEMO_STOP {})
124
+ (when (!= (object/get (array/nth @config.steps @entity.step) demoId "") "")
125
+ (do (emit TUTORIAL_DEMO_START { demoId: (object/get (array/nth @config.steps @entity.step) demoId "") })))
126
+
127
+ TUT_DISMISS -> hidden
128
+ (set @entity.done true)
129
+ (render-ui overlay null)
130
+ (emit TUTORIAL_DEMO_STOP {})
131
+ (emit TUTORIAL_DONE { id: "tutorial" })
132
+ }
133
+
134
+ emits {
135
+ TUT_AUTO -> external {
136
+ id : string
137
+ }
138
+ @description "Self-emitted on INIT when autoShow is on — opens step one. (A config-valued INIT guard would be constant-folded at compose time and misfire; the self-event keeps the decision in an effect.)"
139
+ @tier "essential"
140
+ TUT_NEXT -> external {
141
+ id : string
142
+ }
143
+ @description "Advance one step; on the last step it finishes the tutorial (marks done, clears the overlay, emits TUTORIAL_DONE)."
144
+ @tier "essential"
145
+ TUT_BACK -> external {
146
+ id : string
147
+ }
148
+ @description "Return one step (inert on the first)."
149
+ @tier "essential"
150
+ TUT_DISMISS -> external {
151
+ id : string
152
+ }
153
+ @description "Skip/close the tutorial; marks done and clears the overlay."
154
+ @tier "essential"
155
+ SHOW -> external {
156
+ id : string
157
+ }
158
+ @description "Re-open the tutorial from step one — wire a board's help button here."
159
+ @tier "essential"
160
+ TUTORIAL_DONE -> external {
161
+ id : string!
162
+ }
163
+ @description "The tutorial was finished or skipped; boards unpause or log completion."
164
+ @tier "domain"
165
+ TUTORIAL_DEMO_START -> external {
166
+ demoId : string!
167
+ }
168
+ @description "Emitted when a tutorial step carrying a demoId is shown; the composed std-tutorial-autodemo atom plays that scripted demo."
169
+ @tier "domain"
170
+ TUTORIAL_DEMO_STOP -> external {
171
+ }
172
+ @description "Emitted when the tutorial leaves a demo step (next/back) or is dismissed/finished; the autodemo atom stops and releases control."
173
+ @tier "domain"
174
+ }
175
+
176
+ listens {
177
+ SHOW { id : string }
178
+ }
179
+
180
+ config {
181
+ steps : [TutorialStep] = []
182
+ @label "Steps"
183
+ @description "One row per tutorial page: title, one line of body copy, and an optional demoId that plays a scripted live demo (std-tutorial-autodemo) for the mechanic being taught."
184
+ @tier "presentation"
185
+ previewTrait : trait = @trait.TutorialEmptyPreview
186
+ @label "Preview trait"
187
+ @description "Trait rendered in the media position of the modal — the live, demo-driven preview of the mechanic (a bare game canvas, no shell chrome). '@trait.TutorialEmptyPreview' leaves the slot blank. Override with '@trait.<TraitName>' to show a board's live frame."
188
+ @tier "presentation"
189
+ autoShow : boolean = true
190
+ @label "Auto Show"
191
+ @description "When true the tutorial opens on INIT for this run; when false it stays hidden until SHOW."
192
+ @tier "domain"
193
+ }
194
+ }
195
+
196
+ page "/tutorial-overlay" as TutorialOverlayPage -> TutorialOverlay
197
+ }