@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.
- package/behaviors/lolo/infra/atoms/std-migration-job.lolo +2 -2
- package/behaviors/lolo/ui/core/.hand-authored.json +4 -0
- package/behaviors/lolo/ui/core/atoms/std-browse.lolo +11 -0
- package/behaviors/lolo/ui/core/atoms/std-related.lolo +86 -9
- package/behaviors/lolo/ui/core/atoms/std-rich-editor.lolo +115 -1
- package/behaviors/lolo/ui/core/atoms/std-tag-taxonomy.lolo +62 -2
- package/behaviors/lolo/ui/core/atoms/std-thread.lolo +43 -2
- package/behaviors/lolo/ui/core/atoms/std-tree.lolo +247 -0
- package/behaviors/lolo/ui/core/atoms/std-version-history.lolo +110 -2
- package/behaviors/lolo/ui/core/molecules/ui-file-tree.lolo +18 -2
- package/behaviors/lolo/ui/core/organisms/ui-detail-panel.lolo +6 -2
- package/behaviors/lolo/ui/core/organisms/ui-form-section.lolo +2 -2
- package/behaviors/lolo/ui/core/organisms/ui-form.lolo +2 -2
- package/behaviors/lolo/ui/game/atoms/std-fx-trail.lolo +63 -0
- package/behaviors/lolo/ui/game/atoms/std-gate-lock.lolo +96 -0
- package/behaviors/lolo/ui/game/atoms/std-lives.lolo +121 -0
- package/behaviors/lolo/ui/game/atoms/std-patrol-hazard.lolo +96 -0
- package/behaviors/lolo/ui/game/atoms/std-tutorial-autodemo.lolo +136 -0
- package/behaviors/lolo/ui/game/atoms/std-tutorial-overlay.lolo +197 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-slope-probe.lolo +117 -0
- package/behaviors/lolo/ui/learning/atoms/ui-math-canvas.lolo +18 -0
- package/behaviors/registry/infra/atoms/std-migration-job.orb +2 -2
- package/behaviors/registry/ui/core/atoms/std-browse.orb +17 -0
- package/behaviors/registry/ui/core/atoms/std-related.orb +256 -3
- package/behaviors/registry/ui/core/atoms/std-rich-editor.orb +407 -1
- package/behaviors/registry/ui/core/atoms/std-tag-taxonomy.orb +240 -8
- package/behaviors/registry/ui/core/atoms/std-thread.orb +254 -4
- package/behaviors/registry/ui/core/atoms/std-tree.orb +656 -0
- package/behaviors/registry/ui/core/atoms/std-version-history.orb +479 -6
- package/behaviors/registry/ui/core/molecules/ui-file-tree.orb +43 -19
- package/behaviors/registry/ui/core/organisms/ui-detail-panel.orb +28 -3
- package/behaviors/registry/ui/core/organisms/ui-form-section.orb +8 -2
- package/behaviors/registry/ui/core/organisms/ui-form.orb +8 -2
- package/behaviors/registry/ui/game/atoms/std-fx-trail.orb +376 -0
- package/behaviors/registry/ui/game/atoms/std-gate-lock.orb +852 -0
- package/behaviors/registry/ui/game/atoms/std-lives.orb +739 -0
- package/behaviors/registry/ui/game/atoms/std-patrol-hazard.orb +768 -0
- package/behaviors/registry/ui/game/atoms/std-tutorial-autodemo.orb +732 -0
- package/behaviors/registry/ui/game/atoms/std-tutorial-overlay.orb +1212 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-slope-probe.orb +908 -0
- package/behaviors/registry/ui/learning/atoms/ui-math-canvas.orb +26 -0
- package/dist/behaviors/exports-reader.js +5478 -2954
- package/dist/behaviors/functions/index.d.ts +763 -15
- package/dist/behaviors/functions/index.js +5447 -2953
- package/dist/behaviors/index.d.ts +1 -1
- package/dist/behaviors/index.js +5479 -2955
- package/dist/behaviors/query.js +5478 -2954
- package/dist/behaviors-embeddings.hash.json +3 -3
- package/dist/behaviors-embeddings.json +362 -330
- package/dist/behaviors-registry.json +2812 -542
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5479 -2955
- package/dist/knob-embeddings.hash.json +3 -3
- package/dist/knob-embeddings.json +1 -1
- package/dist/registry/factory-signatures.json +1 -1
- 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
|
+
}
|