@almadar/std 16.182.0 → 16.183.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/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-overlay.lolo +164 -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/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-overlay.orb +1060 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-slope-probe.orb +908 -0
- package/dist/behaviors/exports-reader.js +2674 -2410
- package/dist/behaviors/functions/index.d.ts +534 -1
- package/dist/behaviors/functions/index.js +2657 -2411
- package/dist/behaviors/index.d.ts +1 -1
- package/dist/behaviors/index.js +2675 -2411
- package/dist/behaviors/query.js +2674 -2410
- package/dist/behaviors-embeddings.hash.json +3 -3
- package/dist/behaviors-embeddings.json +294 -270
- package/dist/behaviors-registry.json +1873 -390
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2675 -2411
- 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
|
@@ -297,9 +297,9 @@ orbital MigrationJobOrbital {
|
|
|
297
297
|
@description "Name of the host entity this import writes into (e.g. the organism's own entity). The migration service derives the full target descriptor from the running app's schema."
|
|
298
298
|
@synonyms "destination, import into, target type"
|
|
299
299
|
@tier "domain"
|
|
300
|
-
connectionRef :
|
|
300
|
+
connectionRef : secret = ""
|
|
301
301
|
@label "Connection reference"
|
|
302
|
-
@description "Secret-store / environment KEY holding the source credentials. The reference is stored; the secret itself never enters config, traces, or snapshots — the migration service resolves it at run time. (Typed `
|
|
302
|
+
@description "Secret-store / environment KEY holding the source credentials. The reference is stored; the secret itself never enters config, traces, or snapshots — the migration service resolves it at run time. (Typed `secret` — the config-field type that marks a credential reference; UI password-renders it and traces mask it.)"
|
|
303
303
|
@synonyms "credential key, connection string reference, secret name"
|
|
304
304
|
@tier "internal"
|
|
305
305
|
reviewSlot : slot = modal
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
app std-fx-trail "1.0.0"
|
|
2
|
+
"Headless motion-trail mechanic: records the moving body's recent positions as timestamped dots on a [runtime, shared] entity so the render layer can draw a fading comet trail. Rides the body authority's BODY_MOVED — appends a dot only when the body's speed exceeds minSpeed (standing still leaves no trail) and prunes dots older than ttl on the same pass, so no decay tick is needed and a stopped body's trail simply stops growing (the render side fades each dot by bornAt/ttl). RESTART clears. Renders nothing."
|
|
3
|
+
orbital FxTrailOrbital {
|
|
4
|
+
type TrailDot = {
|
|
5
|
+
id : string!
|
|
6
|
+
x : number!
|
|
7
|
+
y : number!
|
|
8
|
+
bornAt : number!
|
|
9
|
+
ttl : number!
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
entity FxTrailData [runtime, shared] {
|
|
13
|
+
id : string!
|
|
14
|
+
trail : [TrailDot] = []
|
|
15
|
+
seq : number = 0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
trait FxTrail -> @rebindable FxTrailData [lifecycle] {
|
|
19
|
+
initial: active
|
|
20
|
+
|
|
21
|
+
state active {
|
|
22
|
+
INIT -> active
|
|
23
|
+
(set @entity.trail [])
|
|
24
|
+
(set @entity.seq 0)
|
|
25
|
+
|
|
26
|
+
BODY_MOVED -> active when @config.running
|
|
27
|
+
(set @entity.trail
|
|
28
|
+
(array/filter
|
|
29
|
+
(if (> @payload.speed @config.minSpeed)
|
|
30
|
+
(array/concat @entity.trail [ { id: (str/concat "trail-" (+ @entity.seq 1)), x: @payload.x, y: @payload.y, bornAt: (time/now), ttl: @config.ttl } ])
|
|
31
|
+
@entity.trail)
|
|
32
|
+
(fn d (< (- (time/now) (object/get @d bornAt)) (object/get @d ttl)))))
|
|
33
|
+
(if (> @payload.speed @config.minSpeed)
|
|
34
|
+
(set @entity.seq (+ @entity.seq 1))
|
|
35
|
+
null)
|
|
36
|
+
|
|
37
|
+
RESTART -> active
|
|
38
|
+
(set @entity.trail [])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
listens {
|
|
42
|
+
BODY_MOVED { x : number!, y : number!, vx : number!, vy : number!, grounded : boolean!, skating : boolean!, speed : number! }
|
|
43
|
+
RESTART { id : string }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
config {
|
|
47
|
+
running : boolean = true
|
|
48
|
+
@label "Running"
|
|
49
|
+
@description "When true, BODY_MOVED appends trail dots; toggle to freeze the trail (existing dots still age out)."
|
|
50
|
+
@tier "domain"
|
|
51
|
+
ttl : number = 600
|
|
52
|
+
@label "Trail Ttl (ms)"
|
|
53
|
+
@description "Milliseconds a trail dot lives; at a 33ms move cadence ~600ms keeps the last eighteen positions. The ttl is also the natural length cap — no separate limit needed."
|
|
54
|
+
@tier "presentation"
|
|
55
|
+
minSpeed : number = 0.5
|
|
56
|
+
@label "Min Speed"
|
|
57
|
+
@description "Body speed (units/s, from BODY_MOVED) below which no dot is appended, so a resting body doesn't pile dots on one spot."
|
|
58
|
+
@tier "domain"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
page "/fx-trail" as FxTrailPage -> FxTrail
|
|
63
|
+
}
|
|
@@ -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,164 @@
|
|
|
1
|
+
app std-tutorial-overlay "1.0.0"
|
|
2
|
+
"Tutorial modal with media steps for game boards and learning shells: a paged overlay — title, an animated GIF/clip showing exactly how the mechanic is done, one line of body copy, a step counter, and Back / Next / Skip — composed entirely from registered render types (stack + text + image + button) 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, media } row."
|
|
3
|
+
orbital TutorialOverlayOrbital {
|
|
4
|
+
type TutorialStep = {
|
|
5
|
+
id : string!
|
|
6
|
+
title : string!
|
|
7
|
+
body : string
|
|
8
|
+
media : string
|
|
9
|
+
mediaAlt : string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
entity TutorialState [runtime, shared] {
|
|
13
|
+
id : string!
|
|
14
|
+
step : number = 0
|
|
15
|
+
done : boolean = false
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
trait TutorialOverlay -> @rebindable TutorialState [interaction, instance] {
|
|
19
|
+
initial: hidden
|
|
20
|
+
|
|
21
|
+
state hidden {
|
|
22
|
+
INIT -> hidden
|
|
23
|
+
(if @config.autoShow
|
|
24
|
+
(emit TUT_AUTO { id: "auto" })
|
|
25
|
+
(render-ui overlay null))
|
|
26
|
+
|
|
27
|
+
TUT_AUTO -> showing
|
|
28
|
+
(set @entity.step 0)
|
|
29
|
+
(render-ui overlay {
|
|
30
|
+
type: stack
|
|
31
|
+
direction: vertical
|
|
32
|
+
className: "bg-card text-card-foreground border border-border rounded-lg shadow-lg p-6 max-w-md mx-auto mt-16"
|
|
33
|
+
children: [
|
|
34
|
+
{ type: typography, content: (object/get (array/nth @config.steps 0) title), variant: "h4", weight: "bold" }
|
|
35
|
+
{ type: atlas-image, asset: { url: (object/get (array/nth @config.steps 0) media), name: (object/get (array/nth @config.steps 0) id), category: "tutorial" }, fit: "contain", alt: (object/get (array/nth @config.steps 0) mediaAlt), className: "rounded-md" }
|
|
36
|
+
{ type: typography, content: (object/get (array/nth @config.steps 0) body), variant: "body" }
|
|
37
|
+
{ type: typography, content: (str/concat "1 / " (array/len @config.steps)), variant: "caption", align: "center" }
|
|
38
|
+
{ type: stack, direction: horizontal, justify: "center", className: "pt-2", children: [
|
|
39
|
+
{ type: button, label: (if (> (array/len @config.steps) 1) "Next ▶" "Got it"), action: "TUT_NEXT", variant: "primary" }
|
|
40
|
+
{ type: button, label: "Skip", action: "TUT_DISMISS", variant: "secondary" }
|
|
41
|
+
] }
|
|
42
|
+
]
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
SHOW -> showing
|
|
46
|
+
(set @entity.step 0)
|
|
47
|
+
(render-ui overlay {
|
|
48
|
+
type: stack
|
|
49
|
+
direction: vertical
|
|
50
|
+
className: "bg-card text-card-foreground border border-border rounded-lg shadow-lg p-6 max-w-md mx-auto mt-16"
|
|
51
|
+
children: [
|
|
52
|
+
{ type: typography, content: (object/get (array/nth @config.steps 0) title), variant: "h4", weight: "bold" }
|
|
53
|
+
{ type: atlas-image, asset: { url: (object/get (array/nth @config.steps 0) media), name: (object/get (array/nth @config.steps 0) id), category: "tutorial" }, fit: "contain", alt: (object/get (array/nth @config.steps 0) mediaAlt), className: "rounded-md" }
|
|
54
|
+
{ type: typography, content: (object/get (array/nth @config.steps 0) body), variant: "body" }
|
|
55
|
+
{ type: typography, content: (str/concat "1 / " (array/len @config.steps)), variant: "caption", align: "center" }
|
|
56
|
+
{ type: stack, direction: horizontal, justify: "center", className: "pt-2", children: [
|
|
57
|
+
{ type: button, label: (if (> (array/len @config.steps) 1) "Next ▶" "Got it"), action: "TUT_NEXT", variant: "primary" }
|
|
58
|
+
{ type: button, label: "Skip", action: "TUT_DISMISS", variant: "secondary" }
|
|
59
|
+
] }
|
|
60
|
+
]
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
state showing {
|
|
65
|
+
TUT_NEXT -> showing when (< (+ @entity.step 1) (array/len @config.steps))
|
|
66
|
+
(set @entity.step (+ @entity.step 1))
|
|
67
|
+
(render-ui overlay {
|
|
68
|
+
type: stack
|
|
69
|
+
direction: vertical
|
|
70
|
+
className: "bg-card text-card-foreground border border-border rounded-lg shadow-lg p-6 max-w-md mx-auto mt-16"
|
|
71
|
+
children: [
|
|
72
|
+
{ type: typography, content: (object/get (array/nth @config.steps @entity.step) title), variant: "h4", weight: "bold" }
|
|
73
|
+
{ type: atlas-image, asset: { url: (object/get (array/nth @config.steps @entity.step) media), name: (object/get (array/nth @config.steps @entity.step) id), category: "tutorial" }, fit: "contain", alt: (object/get (array/nth @config.steps @entity.step) mediaAlt), className: "rounded-md" }
|
|
74
|
+
{ type: typography, content: (object/get (array/nth @config.steps @entity.step) body), variant: "body" }
|
|
75
|
+
{ type: typography, content: (str/concat (+ @entity.step 1) " / " (array/len @config.steps)), variant: "caption", align: "center" }
|
|
76
|
+
{ type: stack, direction: horizontal, justify: "center", className: "pt-2", children: [
|
|
77
|
+
{ type: button, label: "◀ Back", action: "TUT_BACK", variant: "secondary" }
|
|
78
|
+
{ type: button, label: (if (< (+ @entity.step 1) (array/len @config.steps)) "Next ▶" "Got it"), action: "TUT_NEXT", variant: "primary" }
|
|
79
|
+
{ type: button, label: "Skip", action: "TUT_DISMISS", variant: "secondary" }
|
|
80
|
+
] }
|
|
81
|
+
]
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
TUT_NEXT -> hidden when (>= (+ @entity.step 1) (array/len @config.steps))
|
|
85
|
+
(set @entity.done true)
|
|
86
|
+
(render-ui overlay null)
|
|
87
|
+
(emit TUTORIAL_DONE { id: "tutorial" })
|
|
88
|
+
|
|
89
|
+
TUT_BACK -> showing when (> @entity.step 0)
|
|
90
|
+
(set @entity.step (- @entity.step 1))
|
|
91
|
+
(render-ui overlay {
|
|
92
|
+
type: stack
|
|
93
|
+
direction: vertical
|
|
94
|
+
className: "bg-card text-card-foreground border border-border rounded-lg shadow-lg p-6 max-w-md mx-auto mt-16"
|
|
95
|
+
children: [
|
|
96
|
+
{ type: typography, content: (object/get (array/nth @config.steps @entity.step) title), variant: "h4", weight: "bold" }
|
|
97
|
+
{ type: atlas-image, asset: { url: (object/get (array/nth @config.steps @entity.step) media), name: (object/get (array/nth @config.steps @entity.step) id), category: "tutorial" }, fit: "contain", alt: (object/get (array/nth @config.steps @entity.step) mediaAlt), className: "rounded-md" }
|
|
98
|
+
{ type: typography, content: (object/get (array/nth @config.steps @entity.step) body), variant: "body" }
|
|
99
|
+
{ type: typography, content: (str/concat (+ @entity.step 1) " / " (array/len @config.steps)), variant: "caption", align: "center" }
|
|
100
|
+
{ type: stack, direction: horizontal, justify: "center", className: "pt-2", children: [
|
|
101
|
+
{ type: button, label: "◀ Back", action: "TUT_BACK", variant: "secondary" }
|
|
102
|
+
{ type: button, label: (if (< (+ @entity.step 1) (array/len @config.steps)) "Next ▶" "Got it"), action: "TUT_NEXT", variant: "primary" }
|
|
103
|
+
{ type: button, label: "Skip", action: "TUT_DISMISS", variant: "secondary" }
|
|
104
|
+
] }
|
|
105
|
+
]
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
TUT_DISMISS -> hidden
|
|
109
|
+
(set @entity.done true)
|
|
110
|
+
(render-ui overlay null)
|
|
111
|
+
(emit TUTORIAL_DONE { id: "tutorial" })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
emits {
|
|
115
|
+
TUT_AUTO -> external {
|
|
116
|
+
id : string
|
|
117
|
+
}
|
|
118
|
+
@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.)"
|
|
119
|
+
@tier "essential"
|
|
120
|
+
TUT_NEXT -> external {
|
|
121
|
+
id : string
|
|
122
|
+
}
|
|
123
|
+
@description "Advance one step; on the last step it finishes the tutorial (marks done, clears the overlay, emits TUTORIAL_DONE)."
|
|
124
|
+
@tier "essential"
|
|
125
|
+
TUT_BACK -> external {
|
|
126
|
+
id : string
|
|
127
|
+
}
|
|
128
|
+
@description "Return one step (inert on the first)."
|
|
129
|
+
@tier "essential"
|
|
130
|
+
TUT_DISMISS -> external {
|
|
131
|
+
id : string
|
|
132
|
+
}
|
|
133
|
+
@description "Skip/close the tutorial; marks done and clears the overlay."
|
|
134
|
+
@tier "essential"
|
|
135
|
+
SHOW -> external {
|
|
136
|
+
id : string
|
|
137
|
+
}
|
|
138
|
+
@description "Re-open the tutorial from step one — wire a board's help button here."
|
|
139
|
+
@tier "essential"
|
|
140
|
+
TUTORIAL_DONE -> external {
|
|
141
|
+
id : string!
|
|
142
|
+
}
|
|
143
|
+
@description "The tutorial was finished or skipped; boards unpause or log completion."
|
|
144
|
+
@tier "domain"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
listens {
|
|
148
|
+
SHOW { id : string }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
config {
|
|
152
|
+
steps : [TutorialStep] = []
|
|
153
|
+
@label "Steps"
|
|
154
|
+
@description "One row per tutorial page: title, one line of body copy, and media — the URL of an animated GIF/clip showing exactly how the mechanic is performed (recorded from real gameplay)."
|
|
155
|
+
@tier "presentation"
|
|
156
|
+
autoShow : boolean = true
|
|
157
|
+
@label "Auto Show"
|
|
158
|
+
@description "When true the tutorial opens on INIT for this run; when false it stays hidden until SHOW."
|
|
159
|
+
@tier "domain"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
page "/tutorial-overlay" as TutorialOverlayPage -> TutorialOverlay
|
|
164
|
+
}
|