@almadar/std 16.187.0 → 16.189.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 (102) hide show
  1. package/behaviors/lolo/ui/core/.hand-authored.json +4 -4
  2. package/behaviors/lolo/ui/core/atoms/std-board.lolo +138 -4
  3. package/behaviors/lolo/ui/core/atoms/std-browse.lolo +148 -8
  4. package/behaviors/lolo/ui/core/atoms/std-calendar.lolo +161 -11
  5. package/behaviors/lolo/ui/core/atoms/std-event-log.lolo +255 -2
  6. package/behaviors/lolo/ui/core/atoms/std-gallery.lolo +148 -1
  7. package/behaviors/lolo/ui/core/atoms/std-list.lolo +193 -1
  8. package/behaviors/lolo/ui/core/atoms/std-record-detail.lolo +731 -0
  9. package/behaviors/lolo/ui/core/atoms/std-related.lolo +5 -1
  10. package/behaviors/lolo/ui/core/atoms/std-selection.lolo +182 -1
  11. package/behaviors/lolo/ui/core/atoms/std-tree.lolo +113 -3
  12. package/behaviors/lolo/ui/core/atoms/std-version-history.lolo +1 -1
  13. package/behaviors/lolo/ui/core/molecules/ui-document-details.lolo +80 -0
  14. package/behaviors/lolo/ui/core/molecules/ui-document-panel.lolo +213 -0
  15. package/behaviors/lolo/ui/core/molecules/{ui-rich-block-editor.lolo → ui-rich-text-editor.lolo} +25 -25
  16. package/behaviors/lolo/ui/game/atoms/std-area-fill.lolo +93 -0
  17. package/behaviors/lolo/ui/game/atoms/std-choice-gate.lolo +100 -0
  18. package/behaviors/lolo/ui/game/atoms/std-coef-pad.lolo +114 -0
  19. package/behaviors/lolo/ui/game/atoms/std-curve-fog.lolo +50 -0
  20. package/behaviors/lolo/ui/game/atoms/std-loop-lock.lolo +79 -0
  21. package/behaviors/lolo/ui/game/atoms/std-pad-lock.lolo +97 -0
  22. package/behaviors/lolo/ui/game/atoms/std-patrol-hazard.lolo +14 -7
  23. package/behaviors/lolo/ui/game/atoms/std-platformer-body.lolo +48 -5
  24. package/behaviors/lolo/ui/game/atoms/std-velocity-gate.lolo +101 -0
  25. package/behaviors/lolo/ui/game/atoms/std-zone-card.lolo +95 -0
  26. package/behaviors/lolo/ui/game/organisms/std-platformer-board-2d.lolo +9 -0
  27. package/behaviors/registry/infra/atoms/std-approval-gate.orb +16 -8
  28. package/behaviors/registry/infra/atoms/std-cache-aside.orb +62 -31
  29. package/behaviors/registry/infra/atoms/std-circuit-breaker.orb +32 -16
  30. package/behaviors/registry/infra/atoms/std-data-erasure.orb +16 -8
  31. package/behaviors/registry/infra/atoms/std-mod-queue.orb +16 -8
  32. package/behaviors/registry/infra/atoms/std-rate-limiter.orb +28 -14
  33. package/behaviors/registry/infra/organisms/std-status-lifecycle-control.orb +2 -1
  34. package/behaviors/registry/ml/organisms/std-escalating-decision.orb +10 -5
  35. package/behaviors/registry/ml/organisms/std-knowledge-tracing.orb +4 -2
  36. package/behaviors/registry/ui/core/atoms/std-billable-hour.orb +2 -1
  37. package/behaviors/registry/ui/core/atoms/std-board.orb +355 -2
  38. package/behaviors/registry/ui/core/atoms/std-browse.orb +304 -31
  39. package/behaviors/registry/ui/core/atoms/std-calendar.orb +393 -25
  40. package/behaviors/registry/ui/core/atoms/std-dashboard-grid-embedded.orb +2 -1
  41. package/behaviors/registry/ui/core/atoms/std-esign-flow.orb +4 -2
  42. package/behaviors/registry/ui/core/atoms/std-esign-request.orb +6 -3
  43. package/behaviors/registry/ui/core/atoms/std-event-log.orb +773 -0
  44. package/behaviors/registry/ui/core/atoms/std-export.orb +4 -2
  45. package/behaviors/registry/ui/core/atoms/std-gallery.orb +390 -0
  46. package/behaviors/registry/ui/core/atoms/std-import.orb +4 -2
  47. package/behaviors/registry/ui/core/atoms/std-list.orb +451 -0
  48. package/behaviors/registry/ui/core/atoms/std-record-detail.orb +3360 -0
  49. package/behaviors/registry/ui/core/atoms/std-related.orb +23 -1
  50. package/behaviors/registry/ui/core/atoms/std-selection.orb +436 -0
  51. package/behaviors/registry/ui/core/atoms/std-tree.orb +292 -1
  52. package/behaviors/registry/ui/core/atoms/std-version-history.orb +1 -1
  53. package/behaviors/registry/ui/core/molecules/ui-document-details.orb +342 -0
  54. package/behaviors/registry/ui/core/molecules/ui-document-panel.orb +655 -0
  55. package/behaviors/registry/ui/core/molecules/{ui-rich-block-editor.orb → ui-rich-text-editor.orb} +84 -97
  56. package/behaviors/registry/ui/game/atoms/std-area-fill.orb +955 -0
  57. package/behaviors/registry/ui/game/atoms/std-choice-gate.orb +855 -0
  58. package/behaviors/registry/ui/game/atoms/std-coef-pad.orb +928 -0
  59. package/behaviors/registry/ui/game/atoms/std-curve-fog.orb +337 -0
  60. package/behaviors/registry/ui/game/atoms/std-fx-trail.orb +1 -1
  61. package/behaviors/registry/ui/game/atoms/std-gate-lock.orb +1 -1
  62. package/behaviors/registry/ui/game/atoms/std-lives.orb +1 -1
  63. package/behaviors/registry/ui/game/atoms/std-loop-lock.orb +785 -0
  64. package/behaviors/registry/ui/game/atoms/std-pad-lock.orb +933 -0
  65. package/behaviors/registry/ui/game/atoms/std-patrol-hazard.orb +190 -44
  66. package/behaviors/registry/ui/game/atoms/std-platformer-body.orb +1330 -653
  67. package/behaviors/registry/ui/game/atoms/std-velocity-gate.orb +1040 -0
  68. package/behaviors/registry/ui/game/atoms/std-zone-card.orb +642 -0
  69. package/behaviors/registry/ui/game/organisms/std-arcade-board-3d.orb +8 -4
  70. package/behaviors/registry/ui/game/organisms/std-crew-board-2d.orb +6 -3
  71. package/behaviors/registry/ui/game/organisms/std-dungeon-board-2d.orb +10 -5
  72. package/behaviors/registry/ui/game/organisms/std-market-board-3d.orb +8 -4
  73. package/behaviors/registry/ui/game/organisms/std-objective-board-3d.orb +8 -4
  74. package/behaviors/registry/ui/game/organisms/std-platformer-board-2d.orb +36 -1
  75. package/behaviors/registry/ui/game/organisms/std-survival-board-3d.orb +8 -4
  76. package/behaviors/registry/ui/game/organisms/std-tactics-board-2d.orb +12 -6
  77. package/behaviors/registry/ui/game/organisms/std-tactics-board-3d.orb +8 -4
  78. package/behaviors/registry/ui/game/organisms/std-worldmap-board-2d.orb +6 -3
  79. package/behaviors/registry/ui/learning/atoms/std-learn-slope-probe.orb +1 -1
  80. package/behaviors/registry/ui/marketing/atoms/std-marketing-article.orb +2 -1
  81. package/behaviors/registry/ui/marketing/atoms/std-marketing-footer.orb +2 -1
  82. package/behaviors/registry/ui/marketing/atoms/std-marketing-split.orb +2 -1
  83. package/behaviors/registry/ui/marketing/atoms/std-marketing-stats.orb +2 -1
  84. package/behaviors/registry/ui/marketing/atoms/std-marketing-steps.orb +2 -1
  85. package/behaviors/registry/ui/marketing/atoms/std-marketing-team.orb +2 -1
  86. package/dist/behaviors/exports-reader.js +10971 -6255
  87. package/dist/behaviors/functions/index.d.ts +1205 -256
  88. package/dist/behaviors/functions/index.js +10932 -6245
  89. package/dist/behaviors/index.d.ts +1 -1
  90. package/dist/behaviors/index.js +10972 -6256
  91. package/dist/behaviors/query.js +10971 -6255
  92. package/dist/behaviors-embeddings.hash.json +3 -3
  93. package/dist/behaviors-embeddings.json +485 -469
  94. package/dist/behaviors-registry.json +6125 -2940
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.js +10972 -6256
  97. package/dist/knob-embeddings.hash.json +3 -3
  98. package/dist/knob-embeddings.json +1 -1
  99. package/dist/registry/factory-signatures.json +1 -1
  100. package/package.json +4 -4
  101. package/behaviors/lolo/ui/core/atoms/std-rich-editor.lolo +0 -790
  102. package/behaviors/registry/ui/core/atoms/std-rich-editor.orb +0 -2225
@@ -0,0 +1,79 @@
1
+ app std-loop-lock "1.0.0"
2
+ "Headless winding-lock mechanic: locks that open only after the player travels a full revolution around their anchor. Listens BODY_MOVED and, while the player stays within a radial band of a lock's ring, accumulates the wrapped angular delta of their position into swept (radians, signed — either direction counts); when |swept| reaches turns × 2π the lock flips unlocked and emits LOOP_UNLOCKED once. Entering the band reseeds the angle so leaving and returning never jumps the sweep; progress is kept across exits, so a revolution can be completed in stages. RESTART re-locks every lock. Renders nothing; boards draw the ring/chest and read swept from the shared entity (the render authority)."
3
+ orbital LoopLockOrbital {
4
+ type LoopLock = {
5
+ id : string!
6
+ x : number!
7
+ y : number!
8
+ radius : number!
9
+ band : number!
10
+ turns : number!
11
+ swept : number
12
+ lastAngle : number
13
+ inside : boolean
14
+ unlocked : boolean
15
+ reported : boolean
16
+ }
17
+
18
+ entity LoopLockData [runtime, shared] {
19
+ id : string!
20
+ locks : [LoopLock] = []
21
+ }
22
+
23
+ trait LoopLock -> @rebindable LoopLockData [lifecycle] {
24
+ initial: active
25
+
26
+ state active {
27
+ INIT -> active
28
+ (set @entity.locks (array/map @config.locks (fn l (object/merge @l { swept: 0, lastAngle: 0, inside: false, unlocked: false, reported: false }))))
29
+
30
+ BODY_MOVED -> active when @config.running
31
+ (set @entity.locks (array/map @entity.locks (fn l
32
+ (let ((dx (- @payload.x (object/get @l x))) (dy (- @payload.y (object/get @l y))))
33
+ (let ((dist (math/hypot @dx @dy)) (ang (math/atan2 @dy @dx)))
34
+ (let ((in (<= (math/abs (- @dist (object/get @l radius))) (object/get @l band))))
35
+ (let ((d (if (and @in (== (object/get @l inside) true))
36
+ (math/atan2 (math/sin (- @ang (object/get @l lastAngle))) (math/cos (- @ang (object/get @l lastAngle))))
37
+ 0)))
38
+ (let ((swept (+ (object/get @l swept) @d)))
39
+ (object/merge @l { inside: @in, lastAngle: (if @in @ang (object/get @l lastAngle)), swept: @swept, unlocked: (or (object/get @l unlocked) (>= (math/abs @swept) (* (object/get @l turns) 6.283185307179586))) })))))))))
40
+ (let ((newly (array/filter @entity.locks (fn l (and (== (object/get @l unlocked) true) (== (object/get @l reported) false))))))
41
+ (if (> (array/len @newly) 0)
42
+ (do
43
+ (set @entity.locks (array/map @entity.locks (fn l (if (== (object/get @l id) (object/get (array/first @newly) id)) (object/merge @l { reported: true }) @l))))
44
+ (emit LOOP_UNLOCKED { id: (object/get (array/first @newly) id), x: (object/get (array/first @newly) x), y: (object/get (array/first @newly) y) }))
45
+ null))
46
+
47
+ RESTART -> active
48
+ (set @entity.locks (array/map @config.locks (fn l (object/merge @l { swept: 0, lastAngle: 0, inside: false, unlocked: false, reported: false }))))
49
+ }
50
+
51
+ emits {
52
+ LOOP_UNLOCKED -> external {
53
+ id : string!
54
+ x : number!
55
+ y : number!
56
+ }
57
+ @description "A lock's angular sweep reached turns × 2π and it flipped unlocked; boards layer treasure fx and rewards. When several locks open on one move, the first reports and the shared entity carries the rest."
58
+ @tier "domain"
59
+ }
60
+
61
+ listens {
62
+ BODY_MOVED { x : number!, y : number!, vx : number!, vy : number!, grounded : boolean!, skating : boolean!, speed : number! }
63
+ RESTART { id : string }
64
+ }
65
+
66
+ config {
67
+ running : boolean = true
68
+ @label "Running"
69
+ @description "When true, BODY_MOVED accumulates angular sweep and unlocks locks; toggle to freeze every lock as-is."
70
+ @tier "domain"
71
+ locks : [LoopLock] = []
72
+ @label "Locks"
73
+ @description "One row per winding lock: id, anchor (x, y in world units), ring radius, radial tolerance band, and revolutions required. swept/inside/unlocked are stamped at runtime."
74
+ @tier "domain"
75
+ }
76
+ }
77
+
78
+ page "/loop-lock" as LoopLockPage -> LoopLock
79
+ }
@@ -0,0 +1,97 @@
1
+ app std-pad-lock "1.0.0"
2
+ "Headless pad-lock mechanic: floor pads that light when the player steps on them, completing a set when every pad is lit. Listens BODY_MOVED and lights any unlit live pad the player's rect touches (|dx| <= reachX, |dy| <= reachY of the pad center); a lit pad stays lit. Pads may carry a group number (grp 0 = ungrouped, always live): when groups are used, only pads of the lowest uncompleted group are live — pads of later groups refuse to light until the current group finishes, so one sum must be finished before the next. When a group completes the trait emits PAD_GROUP_COMPLETED once for it (first newly completed group reports; the shared entity carries the rest); when every pad in the set is lit it stamps completed and emits PAD_SET_COMPLETED once. The lowest uncompleted group is stamped on the shared entity as currentGrp so boards can pulse exactly the live pads (0 = no group open / ungrouped pads). RESTART unlights every pad and resets the set. Renders nothing; boards draw the pads and read lit from the shared entity (the render authority)."
3
+ orbital PadLockOrbital {
4
+ type PadLock = {
5
+ id : string!
6
+ x : number!
7
+ y : number!
8
+ grp : number!
9
+ lit : boolean
10
+ }
11
+
12
+ entity PadLockData [runtime, shared] {
13
+ id : string!
14
+ pads : [PadLock] = []
15
+ groupsDone : [number] = []
16
+ currentGrp : number = 0
17
+ completed : boolean = false
18
+ }
19
+
20
+ trait PadLock -> @rebindable PadLockData [lifecycle] {
21
+ initial: active
22
+
23
+ state active {
24
+ INIT -> active
25
+ (set @entity.pads (array/map @config.pads (fn p (object/merge @p { lit: false }))))
26
+ (set @entity.groupsDone [])
27
+ (set @entity.currentGrp 0)
28
+ (set @entity.completed false)
29
+
30
+ BODY_MOVED -> active when @config.running
31
+ (let ((grpIds (array/reduce (array/map (array/filter @entity.pads (fn p (> (object/get @p grp) 0))) (fn p (object/get @p grp))) [] (fn acc g (if (> (array/len (array/filter @acc (fn x (== @x @g)))) 0) @acc (array/append @acc @g))))))
32
+ (let ((openGrps (array/filter @grpIds (fn g (< (array/len (array/filter @entity.pads (fn p (and (== (object/get @p grp) @g) (== (object/get @p lit) true))))) (array/len (array/filter @entity.pads (fn p (== (object/get @p grp) @g)))))))))
33
+ (let ((currentGrp (array/reduce @openGrps 9999 (fn acc g (math/min @acc @g)))))
34
+ (let ((newPads (array/map @entity.pads (fn p
35
+ (object/merge @p { lit: (or (object/get @p lit) (and (or (== (object/get @p grp) 0) (== (object/get @p grp) @currentGrp)) (and (<= (math/abs (- @payload.x (object/get @p x))) @config.reachX) (<= (math/abs (- @payload.y (object/get @p y))) @config.reachY)))) })))))
36
+ (let ((doneNow (array/filter @grpIds (fn g (== (array/len (array/filter @newPads (fn p (and (== (object/get @p grp) @g) (== (object/get @p lit) true))))) (array/len (array/filter @newPads (fn p (== (object/get @p grp) @g)))))))))
37
+ (let ((newlyGrp (array/filter @doneNow (fn g (== (array/len (array/filter @entity.groupsDone (fn d (== @d @g)))) 0)))))
38
+ (do
39
+ (set @entity.pads @newPads)
40
+ (set @entity.currentGrp (if (== @currentGrp 9999) 0 @currentGrp))
41
+ (if (> (array/len @newlyGrp) 0)
42
+ (do
43
+ (set @entity.groupsDone @doneNow)
44
+ (emit PAD_GROUP_COMPLETED { grp: (array/first @newlyGrp) }))
45
+ null)
46
+ (if (and (== @entity.completed false) (== (array/len (array/filter @newPads (fn p (== (object/get @p lit) false)))) 0))
47
+ (do
48
+ (set @entity.completed true)
49
+ (emit PAD_SET_COMPLETED {}))
50
+ null))))))))
51
+
52
+ RESTART -> active
53
+ (set @entity.pads (array/map @config.pads (fn p (object/merge @p { lit: false }))))
54
+ (set @entity.groupsDone [])
55
+ (set @entity.currentGrp 0)
56
+ (set @entity.completed false)
57
+ }
58
+
59
+ emits {
60
+ PAD_GROUP_COMPLETED -> external {
61
+ grp : number!
62
+ }
63
+ @description "Every pad of one group is lit; fires once per group, in group order — later groups stay dead until the lowest uncompleted group finishes. Boards layer a small fx and unlock the next group's pads."
64
+ @tier "domain"
65
+ PAD_SET_COMPLETED -> external {
66
+ }
67
+ @description "Every pad in the set is lit; fires once per run. Boards layer the treasure fx and spill the bonus reward."
68
+ @tier "domain"
69
+ }
70
+
71
+ listens {
72
+ BODY_MOVED { x : number!, y : number!, vx : number!, vy : number!, grounded : boolean!, skating : boolean!, speed : number! }
73
+ RESTART { id : string }
74
+ }
75
+
76
+ config {
77
+ running : boolean = true
78
+ @label "Running"
79
+ @description "When true, BODY_MOVED lights touched live pads and reports group/set completion; toggle to freeze every pad as-is."
80
+ @tier "domain"
81
+ pads : [PadLock] = []
82
+ @label "Pads"
83
+ @description "One row per floor pad: id, center (x, y in world units), and grp (0 = ungrouped, always live; N = live only while it is the lowest uncompleted group). lit is stamped at runtime."
84
+ @tier "domain"
85
+ reachX : number = 0.7
86
+ @label "Reach X"
87
+ @description "Horizontal tolerance (world units) around a pad's center when testing player contact (approximates the body's half-width plus a step)."
88
+ @tier "domain"
89
+ reachY : number = 1.2
90
+ @label "Reach Y"
91
+ @description "Vertical tolerance (world units) around a pad's center when testing player contact (covers skating over the surface the pad sits on)."
92
+ @tier "domain"
93
+ }
94
+ }
95
+
96
+ page "/pad-lock" as PadLockPage -> PadLock
97
+ }
@@ -1,5 +1,5 @@
1
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."
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; a patrol declaring waveAmp/wavePeriod also bobs vertically — cy = y + waveAmp·sin(2πt/wavePeriod from a per-row time accumulator — and the hit test and render read cy, so a waveAmp of 0 keeps the flat beat), 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
3
  orbital PatrolHazardOrbital {
4
4
  type Patrol = {
5
5
  id : string!
@@ -7,8 +7,12 @@ orbital PatrolHazardOrbital {
7
7
  x1 : number!
8
8
  y : number!
9
9
  speed : number!
10
+ waveAmp : number
11
+ wavePeriod : number
10
12
  x : number
11
13
  dir : number
14
+ t : number
15
+ cy : number
12
16
  touching : boolean
13
17
  hit : boolean
14
18
  }
@@ -25,7 +29,7 @@ orbital PatrolHazardOrbital {
25
29
 
26
30
  state active {
27
31
  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 }))))
32
+ (set @entity.patrols (array/map @config.patrols (fn p (object/merge @p { x: (object/get @p x0), dir: 1, t: 0, cy: (object/get @p y), touching: false, hit: false }))))
29
33
  (set @entity.px @config.playerSpawnX)
30
34
  (set @entity.py @config.playerSpawnY)
31
35
 
@@ -34,7 +38,7 @@ orbital PatrolHazardOrbital {
34
38
  (set @entity.py @payload.y)
35
39
 
36
40
  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 }))))
41
+ (set @entity.patrols (array/map @config.patrols (fn p (object/merge @p { x: (object/get @p x0), dir: 1, t: 0, cy: (object/get @p y), touching: false, hit: false }))))
38
42
  }
39
43
 
40
44
  ticks {
@@ -45,11 +49,14 @@ orbital PatrolHazardOrbital {
45
49
  (let ((overshoot (> @nx (object/get @p x1))) (undershoot (< @nx (object/get @p x0))))
46
50
  (let ((cx (if @overshoot (object/get @p x1) (if @undershoot (object/get @p x0) @nx)))
47
51
  (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 }))))))))
52
+ (let ((nt (+ (object/get @p t) 0.033)))
53
+ (let ((amp (if (== (object/get @p waveAmp) null) 0 (object/get @p waveAmp))) (period (if (== (object/get @p wavePeriod) null) 3 (object/get @p wavePeriod))))
54
+ (let ((cy (+ (object/get @p y) (* @amp (math/sin (/ (* 6.283185307179586 @nt) @period))))))
55
+ (let ((near (and (<= (math/abs (- @entity.px @cx)) @config.hitRadius) (<= (math/abs (- @entity.py @cy)) @config.hitRadius))))
56
+ (object/merge @p { x: @cx, dir: @cdir, t: @nt, cy: @cy, hit: (and @near (== (object/get @p touching) false)), touching: @near })))))))))))
50
57
  (let ((hits (array/filter @entity.patrols (fn p (== (object/get @p hit) true)))))
51
58
  (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) })
59
+ (emit PATROL_HIT { id: (object/get (array/first @hits) id), x: (object/get (array/first @hits) x), y: (object/get (array/first @hits) cy) })
53
60
  null))
54
61
  }
55
62
 
@@ -75,7 +82,7 @@ orbital PatrolHazardOrbital {
75
82
  @tier "domain"
76
83
  patrols : [Patrol] = []
77
84
  @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."
85
+ @description "One row per patroller: id, the [x0, x1] beat it walks, its base y, its speed in units/s, and optionally waveAmp/wavePeriod for a vertical sine bob (waveAmp 0 = flat ping-pong). Current x/dir/t/cy/touching/hit are stamped at runtime."
79
86
  @tier "domain"
80
87
  hitRadius : number = 0.7
81
88
  @label "Hit Radius"
@@ -1,5 +1,5 @@
1
1
  app std-platformer-body "1.0.0"
2
- "Headless run/jump platformer-physics authority: the SOLE writer of body + fell + hazardId + goalId on side-view boards. Where std-motion-body owns free 2D drift (no collision), this atom owns the platformer contract: every 33ms physics tick it integrates gravity into the body's vertical velocity, steps position in per-second units, and collides the body rect against the config platforms with one-way semantics — a body moving in the gravity direction that overlaps a ground/platform tile lands on its anti-gravity face (vy zeroed, grounded set), while a body moving against gravity passes through, so jumping up through a platform and walking up a staircase both just work. The sign of gravity IS the vertical convention: positive pulls toward +y (screen boards, y down — sprite canvases), negative toward −y (math canvases, y up); jumps always fire against it. Intents: STEER sets horizontal velocity (dir −1/0/1 × moveSpeed, hold-to-run: keydown steers, keyup steers 0), JUMP kicks vertical velocity when grounded, PLACE teleports (spawns, hazard respawns), RESTART reseeds. Emits BODY_MOVED every tick for the render layer (boards that don't subscribe pay nothing), plus the sparse rule signals a board needs to score: BODY_JUMPED, BODY_LANDED (with impact speed), BODY_HAZARD (once per hazard entry, re-arms on exit), BODY_GOAL (same arming), BODY_FELL (once per fall past killY, re-armed by PLACE/RESTART). Curve skating: when config.skate is on and the grounded body comes within skateSnap of the config.skateCurve surface, the body rides the curve — y snaps to the interpolated surface and tangential gravity (g·slope/(1+slope²)) accelerates vx, so the body slides downhill along the graph itself and launches ballistically off its ends; jumping kicks free. Sparse BODY_SKATE_START/BODY_SKATE_END signals fire on engage/disengage so boards can layer fx. Lives, score, and win/lose stay downstream — this atom simulates, it never judges. Renders nothing."
2
+ "Headless run/jump platformer-physics authority: the SOLE writer of body + fell + hazardId + goalId on side-view boards. Where std-motion-body owns free 2D drift (no collision), this atom owns the platformer contract: every 33ms physics tick it integrates gravity into the body's vertical velocity, steps position in per-second units, and collides the body rect against the config platforms with one-way semantics — a body moving in the gravity direction that overlaps a ground/platform tile lands on its anti-gravity face (vy zeroed, grounded set), while a body moving against gravity passes through, so jumping up through a platform and walking up a staircase both just work. The sign of gravity IS the vertical convention: positive pulls toward +y (screen boards, y down — sprite canvases), negative toward −y (math canvases, y up); jumps always fire against it. Intents: STEER sets horizontal velocity (dir −1/0/1 × moveSpeed, hold-to-run: keydown steers, keyup steers 0), JUMP kicks vertical velocity when grounded, PLACE teleports (spawns, hazard respawns), RESTART reseeds. Emits BODY_MOVED every tick for the render layer (boards that don't subscribe pay nothing), plus the sparse rule signals a board needs to score: BODY_JUMPED, BODY_LANDED (with impact speed), BODY_HAZARD (once per hazard entry, re-arms on exit), BODY_GOAL (same arming), BODY_FELL (once per fall past killY, re-armed by PLACE/RESTART). Drift regions: config.driftRegions lists axis-aligned rects each carrying a constant acceleration (ax, ay) applied with the same 0.033s dt whenever the body's center is inside (empty list = zero behavior change). Curve skating: when config.skate is on and the grounded body comes within skateSnap of the config.skateCurve surface, the body rides the curve — y snaps to the interpolated surface and tangential gravity (g·slope/(1+slope²)) accelerates vx, and the vertical velocity is the tangential projection (s = vx·cos θ, vy = s·sin θ — no discontinuous jumps at sample boundaries), so the body slides downhill along the graph itself and launches ballistically off its ends; jumping kicks free. Sparse BODY_SKATE_START/BODY_SKATE_END signals fire on engage/disengage so boards can layer fx. Lives, score, and win/lose stay downstream — this atom simulates, it never judges. Renders nothing."
3
3
  orbital PlatformerBodyOrbital {
4
4
  type PlatformerBody = {
5
5
  x : number!
@@ -23,12 +23,29 @@ orbital PlatformerBodyOrbital {
23
23
  x : number!
24
24
  y : number!
25
25
  }
26
+ type BodyDriftRegion = {
27
+ id : string!
28
+ x0 : number!
29
+ x1 : number!
30
+ y0 : number!
31
+ y1 : number!
32
+ ax : number!
33
+ ay : number!
34
+ }
35
+ type CurveMod = {
36
+ x0 : number!
37
+ x1 : number!
38
+ a : number!
39
+ h : number!
40
+ k : number!
41
+ }
26
42
 
27
43
  entity PlatformerBodyData [runtime, shared] {
28
44
  id : string!
29
45
  body : PlatformerBody = { x: 0, y: 0, width: 1, height: 1, vx: 0, vy: 0, grounded: false, facingRight: true }
30
46
  fell : boolean = false
31
47
  hazardId : string = ""
48
+ curveMods : [CurveMod] = []
32
49
  goalId : string = ""
33
50
  skating : boolean = false
34
51
  }
@@ -57,8 +74,12 @@ orbital PlatformerBodyOrbital {
57
74
  (set @entity.hazardId "")
58
75
  (set @entity.skating false)
59
76
 
77
+ CURVE_MOD_ADD -> active when @config.curveModsEnabled
78
+ (set @entity.curveMods (array/append @entity.curveMods { x0: @payload.x0, x1: @payload.x1, a: @payload.a, h: @payload.h, k: @payload.k }))
79
+
60
80
  RESTART -> active
61
81
  (set @entity.body @config.spawn)
82
+ (set @entity.curveMods [])
62
83
  (set @entity.fell false)
63
84
  (set @entity.hazardId "")
64
85
  (set @entity.goalId "")
@@ -69,7 +90,10 @@ orbital PlatformerBodyOrbital {
69
90
  physicsTick every 33ms
70
91
  when (and @config.running (> @entity.body.width 0))
71
92
  (let ((gdir (if (> @config.gravity 0) 1 -1)))
72
- (let ((vy1 (math/clamp (+ @entity.body.vy (* @config.gravity 0.033)) (- 0 @config.maxFallSpeed) @config.maxFallSpeed)))
93
+ (let ((bcx (+ (object/get @entity.body x) (/ (object/get @entity.body width) 2))) (bcy (+ (object/get @entity.body y) (/ (object/get @entity.body height) 2))))
94
+ (let ((drift (array/find @config.driftRegions (fn r (and (>= @bcx (object/get @r x0)) (and (<= @bcx (object/get @r x1)) (and (>= @bcy (object/get @r y0)) (<= @bcy (object/get @r y1)))))))))
95
+ (let ((dax (if (== @drift null) 0 (object/get @drift ax))) (day (if (== @drift null) 0 (object/get @drift ay))))
96
+ (let ((vy1 (math/clamp (+ @entity.body.vy (* (+ @config.gravity @day) 0.033)) (- 0 @config.maxFallSpeed) @config.maxFallSpeed)))
73
97
  (let ((bw (object/get @entity.body width)) (bh (object/get @entity.body height)))
74
98
  (let ((nx (math/clamp (+ @entity.body.x (* @entity.body.vx 0.033)) @config.xMin @config.xMax)))
75
99
  (let ((ny (+ @entity.body.y (* @vy1 0.033))))
@@ -83,15 +107,25 @@ orbital PlatformerBodyOrbital {
83
107
  (let ((rightSamp (if @inRange (array/first (array/filter @config.skateCurve (fn s (> (object/get @s x) @cx)))) { x: 1, y: 0 })))
84
108
  (let ((slope (if @inRange (/ (- (object/get @rightSamp y) (object/get @leftSamp y)) (- (object/get @rightSamp x) (object/get @leftSamp x))) 0)))
85
109
  (let ((curveY (if @inRange (+ (object/get @leftSamp y) (* @slope (- @cx (object/get @leftSamp x)))) 0)))
110
+ (let ((curveMod (if (and @config.curveModsEnabled @inRange) (array/find @entity.curveMods (fn m (and (>= @cx (object/get @m x0)) (<= @cx (object/get @m x1))))) null)))
111
+ (let ((modX (if (== @curveMod null) @cx (- @cx (object/get @curveMod h)))))
112
+ (let ((modInRange (and (!= @curveMod null) (and (>= @modX (object/get (array/first @config.skateCurve) x)) (< @modX (object/get (array/last @config.skateCurve) x))))))
113
+ (let ((modLeft (if @modInRange (array/last (array/filter @config.skateCurve (fn s (<= (object/get @s x) @modX)))) @leftSamp)))
114
+ (let ((modRight (if @modInRange (array/first (array/filter @config.skateCurve (fn s (> (object/get @s x) @modX)))) @rightSamp)))
115
+ (let ((modSlope (if @modInRange (/ (- (object/get @modRight y) (object/get @modLeft y)) (- (object/get @modRight x) (object/get @modLeft x))) @slope)))
116
+ (let ((modCurveY (if @modInRange (+ (object/get @modLeft y) (* @modSlope (- @modX (object/get @modLeft x)))) @curveY)))
117
+ (let ((slope (if (== @curveMod null) @slope (* (object/get @curveMod a) @modSlope))))
118
+ (let ((curveY (if (== @curveMod null) @curveY (+ (* (object/get @curveMod a) @modCurveY) (object/get @curveMod k)))))
86
119
  (let ((movingUp (< (* @vy1 @gdir) -0.01)))
87
120
  (let ((skateNow (and @inRange (and (<= (math/abs (- @ny2 @curveY)) @config.skateSnap) (and (or @landed @entity.skating) (or @entity.body.grounded (not @movingUp)))))))
88
121
  (let ((vx2 (if @skateNow (math/clamp (+ @entity.body.vx (* (/ (* @config.gravity @slope) (+ 1 (* @slope @slope))) 0.033)) (- 0 @config.maxFallSpeed) @config.maxFallSpeed) @entity.body.vx)))
89
- (let ((fy (if @skateNow @curveY @ny2)) (fvy (if @skateNow (* @slope @vx2) @vy2)) (fgrounded (or @landed @skateNow)))
122
+ (let ((vx3 (if (== @drift null) @vx2 (math/clamp (+ @vx2 (* @dax 0.033)) (- 0 @config.maxFallSpeed) @config.maxFallSpeed))))
123
+ (let ((fy (if @skateNow @curveY @ny2)) (fvy (if @skateNow (* (* @vx2 (math/cos (math/atan @slope))) (math/sin (math/atan @slope))) @vy2)) (fgrounded (or @landed @skateNow)))
90
124
  (let ((hazardHit (array/find @config.platforms (fn p (and (== (object/get @p type) "hazard") (geo/aabb-overlap @rect { x: @p.x, y: @p.y, w: @p.width, h: @p.height }))))))
91
125
  (let ((goalHit (array/find @config.platforms (fn p (and (== (object/get @p type) "goal") (geo/aabb-overlap @rect { x: @p.x, y: @p.y, w: @p.width, h: @p.height }))))))
92
126
  (let ((fellNow (if (> @gdir 0) (> @fy @config.killY) (< @fy @config.killY))))
93
127
  (do
94
- (set @entity.body (object/merge @entity.body { x: @nx, y: @fy, vx: (if (or (== @nx @config.xMin) (== @nx @config.xMax)) 0 @vx2), vy: @fvy, grounded: @fgrounded }))
128
+ (set @entity.body (object/merge @entity.body { x: @nx, y: @fy, vx: (if (or (== @nx @config.xMin) (== @nx @config.xMax)) 0 @vx3), vy: @fvy, grounded: @fgrounded }))
95
129
  (set @entity.skating @skateNow)
96
130
  (if (and @skateNow (== @entity.skating false))
97
131
  (emit BODY_SKATE_START { x: @nx, y: @fy, slope: @slope })
@@ -121,7 +155,7 @@ orbital PlatformerBodyOrbital {
121
155
  (set @entity.fell true)
122
156
  (emit BODY_FELL { x: @nx, y: @fy }))
123
157
  null)
124
- (emit BODY_MOVED { x: @nx, y: @fy, vx: @entity.body.vx, vy: @fvy, grounded: @fgrounded, skating: @skateNow, speed: (math/hypot @entity.body.vx @fvy) }))))))))))))))))))))))))
158
+ (emit BODY_MOVED { x: @nx, y: @fy, vx: @entity.body.vx, vy: @fvy, grounded: @fgrounded, skating: @skateNow, speed: (math/hypot @entity.body.vx @fvy) })))))))))))))))))))))))))))))))))))))
125
159
  }
126
160
 
127
161
  emits {
@@ -188,6 +222,7 @@ orbital PlatformerBodyOrbital {
188
222
  STEER { dir : number! }
189
223
  JUMP { id : string }
190
224
  PLACE { x : number!, y : number! }
225
+ CURVE_MOD_ADD { x0 : number!, x1 : number!, a : number!, h : number!, k : number! }
191
226
  RESTART { id : string }
192
227
  }
193
228
 
@@ -208,6 +243,10 @@ orbital PlatformerBodyOrbital {
208
243
  @label "Gravity"
209
244
  @description "Units/s² added to vertical velocity; the SIGN is the vertical convention — positive pulls toward +y (screen boards, y down), negative toward −y (math canvases, y up) — and jumps always fire against it"
210
245
  @tier "domain"
246
+ curveModsEnabled : boolean = false
247
+ @label "Curve Mods Enabled"
248
+ @description "When true, entity.curveMods reshape the skate surface inside each mod's x-range: the road samples the base curve at (x − h) and applies a·y + k (slope scales by a) — coefficient pads lift/tilt the segment the player is riding. Default off: boards without coef pads pay nothing."
249
+ @tier "domain"
211
250
  jumpStrength : number = 11
212
251
  @label "Jump Strength"
213
252
  @description "Vertical speed in units/s imparted by JUMP, directed against gravity; apex height ≈ jumpStrength² / (2·|gravity|)"
@@ -244,6 +283,10 @@ orbital PlatformerBodyOrbital {
244
283
  @label "Skate Snap"
245
284
  @description "Max vertical gap (world units) between the body's min corner and the curve surface for skating to engage or stay engaged; larger values grab earlier on landing, smaller values release sooner on bumps"
246
285
  @tier "domain"
286
+ driftRegions : [BodyDriftRegion] = []
287
+ @label "Drift Regions"
288
+ @description "Axis-aligned rects (x0..x1, y0..y1 in world units) each carrying a constant acceleration (ax, ay in units/s²) applied with the physics dt whenever the body's center is inside — winds, currents, gradient pushes. Empty list = zero behavior change."
289
+ @tier "domain"
247
290
  }
248
291
  }
249
292
 
@@ -0,0 +1,101 @@
1
+ app std-velocity-gate "1.0.0"
2
+ "Headless velocity-gate mechanic: gates that open only when the player crosses them inside a speed band, optionally in a required direction, optionally sustained for a dwell. Listens BODY_MOVED and, per closed gate, tests the player's rect (padded by reachMargin), their speed against [minSpeed, maxSpeed], and their horizontal direction against dir (0 = either). Instant gates (dwellMs 0) open on the first passing cross; dwell gates (dwellMs > 0) accumulate ~33ms per passing move and open once held reaches dwellMs — a failing move resets the hold. The first newly opened gate emits VELOCITY_GATE_OPENED (the shared entity carries the rest). Entering a closed INSTANT gate while NOT passing emits VELOCITY_GATE_DENIED once per approach — re-arming only after the player steps out — so the board can answer with a PLACE back and a hint fx; dwell gates never deny (a hot entry is fine — coming to rest inside is the point), a failed hold simply resets. RESTART re-closes every gate. Renders nothing; boards draw the gate band/chest and read open/held from the shared entity (the render authority)."
3
+ orbital VelocityGateOrbital {
4
+ type VelocityGate = {
5
+ id : string!
6
+ x : number!
7
+ y : number!
8
+ width : number!
9
+ height : number!
10
+ minSpeed : number!
11
+ maxSpeed : number!
12
+ dwellMs : number!
13
+ dir : number!
14
+ hint : string
15
+ open : boolean
16
+ touching : boolean
17
+ denied : boolean
18
+ held : number
19
+ reported : boolean
20
+ }
21
+
22
+ entity VelocityGateData [runtime, shared] {
23
+ id : string!
24
+ gates : [VelocityGate] = []
25
+ }
26
+
27
+ trait VelocityGate -> @rebindable VelocityGateData [lifecycle] {
28
+ initial: active
29
+
30
+ state active {
31
+ INIT -> active
32
+ (set @entity.gates (array/map @config.gates (fn g (object/merge @g { open: false, touching: false, denied: false, held: 0, reported: false }))))
33
+
34
+ BODY_MOVED -> active when @config.running
35
+ (set @entity.gates (array/map @entity.gates (fn g
36
+ (let ((inside (and (== (object/get @g open) false)
37
+ (and (>= @payload.x (- (object/get @g x) @config.reachMargin))
38
+ (and (<= @payload.x (+ (object/get @g x) (object/get @g width) @config.reachMargin))
39
+ (and (>= @payload.y (- (object/get @g y) @config.reachMargin))
40
+ (<= @payload.y (+ (object/get @g y) (object/get @g height) @config.reachMargin))))))))
41
+ (let ((dirOk (or (== (object/get @g dir) 0) (== (math/sign @payload.vx) (object/get @g dir)))))
42
+ (let ((pass (and @inside (and (>= @payload.speed (object/get @g minSpeed)) (and (<= @payload.speed (object/get @g maxSpeed)) @dirOk)))))
43
+ (let ((held (if @pass (+ (object/get @g held) 33) 0)))
44
+ (let ((openNow (or (object/get @g open) (if (> (object/get @g dwellMs) 0) (>= @held (object/get @g dwellMs)) @pass))))
45
+ (object/merge @g { touching: @inside, held: @held, open: @openNow, denied: (and (== (object/get @g dwellMs) 0) (and @inside (and (== @pass false) (== (object/get @g touching) false)))) })))))))))
46
+ (let ((newly (array/filter @entity.gates (fn g (and (== (object/get @g open) true) (== (object/get @g reported) false))))))
47
+ (if (> (array/len @newly) 0)
48
+ (do
49
+ (set @entity.gates (array/map @entity.gates (fn g (if (== (object/get @g id) (object/get (array/first @newly) id)) (object/merge @g { reported: true }) @g))))
50
+ (emit VELOCITY_GATE_OPENED { id: (object/get (array/first @newly) id), x: (object/get (array/first @newly) x), y: (object/get (array/first @newly) y) }))
51
+ null))
52
+ (let ((denied (array/filter @entity.gates (fn g (== (object/get @g denied) true)))))
53
+ (if (> (array/len @denied) 0)
54
+ (emit VELOCITY_GATE_DENIED { id: (object/get (array/first @denied) id), x: (object/get (array/first @denied) x), y: (object/get (array/first @denied) y), speed: @payload.speed })
55
+ null))
56
+
57
+ RESTART -> active
58
+ (set @entity.gates (array/map @config.gates (fn g (object/merge @g { open: false, touching: false, denied: false, held: 0, reported: false }))))
59
+ }
60
+
61
+ emits {
62
+ VELOCITY_GATE_OPENED -> external {
63
+ id : string!
64
+ x : number!
65
+ y : number!
66
+ }
67
+ @description "A gate's speed band was satisfied (instantly, or held for its dwell) and it flipped open; boards layer treasure fx and rewards. When several gates open on one move, the first reports and the shared entity carries the rest."
68
+ @tier "domain"
69
+ VELOCITY_GATE_DENIED -> external {
70
+ id : string!
71
+ x : number!
72
+ y : number!
73
+ speed : number!
74
+ }
75
+ @description "The player entered a closed gate outside its speed band or against its direction; fires once per approach and re-arms after they step out. Boards answer with a PLACE back plus a hint fx."
76
+ @tier "domain"
77
+ }
78
+
79
+ listens {
80
+ BODY_MOVED { x : number!, y : number!, vx : number!, vy : number!, grounded : boolean!, skating : boolean!, speed : number! }
81
+ RESTART { id : string }
82
+ }
83
+
84
+ config {
85
+ running : boolean = true
86
+ @label "Running"
87
+ @description "When true, BODY_MOVED tests every gate's band and opens/denies; toggle to freeze every gate as-is."
88
+ @tier "domain"
89
+ gates : [VelocityGate] = []
90
+ @label "Gates"
91
+ @description "One row per velocity gate: id, min-corner rect (x, y, width, height in world units), the speed band [minSpeed, maxSpeed] that opens it, the hold required (dwellMs 0 = instant), the horizontal direction required (dir -1/0/+1), and an optional hint shown when denied. open/touching/denied/held/reported are stamped at runtime."
92
+ @tier "domain"
93
+ reachMargin : number = 0.6
94
+ @label "Reach Margin"
95
+ @description "Extra world units padded around a closed gate's rect when testing player contact (approximates the body's size)."
96
+ @tier "domain"
97
+ }
98
+ }
99
+
100
+ page "/velocity-gate" as VelocityGatePage -> VelocityGate
101
+ }
@@ -0,0 +1,95 @@
1
+ app std-zone-card "1.0.0"
2
+ "Headless zone-guidance mechanic for side-view boards: tracks which band of the world the player is in and holds up that zone's card — name, concept, objective — for a while after every crossing. Listens BODY_MOVED and, when the player's x crosses into a different config zone, stamps the shared entity (zoneId, card, enteredAt) and emits ZONE_CHANGED carrying the card fields; the card self-clears ttlTicks BODY_MOVED ticks later (a per-move tick counter on the entity, so pauses at speed zero still age it out) and boards clear it early via CARD_DISMISS (renamed to a board-scoped progress event) on any mechanic completion. RESTART clears the card and the clock. Renders nothing; boards draw the card from the shared entity (the render authority)."
3
+ orbital ZoneCardOrbital {
4
+ type ZoneCard = {
5
+ name : string!
6
+ concept : string
7
+ objective : string
8
+ }
9
+ type ZoneCardZone = {
10
+ id : string!
11
+ name : string!
12
+ concept : string
13
+ objective : string
14
+ x0 : number!
15
+ x1 : number!
16
+ }
17
+
18
+ entity ZoneCardData [runtime, shared] {
19
+ id : string!
20
+ zoneId : string = ""
21
+ card : ZoneCard
22
+ enteredAt : number = 0
23
+ ticks : number = 0
24
+ }
25
+
26
+ trait ZoneCard -> @rebindable ZoneCardData [lifecycle] {
27
+ initial: active
28
+
29
+ state active {
30
+ INIT -> active
31
+ (set @entity.zoneId "")
32
+ (set @entity.card null)
33
+ (set @entity.enteredAt 0)
34
+ (set @entity.ticks 0)
35
+
36
+ BODY_MOVED -> active when @config.running
37
+ (set @entity.ticks (+ @entity.ticks 1))
38
+ (let ((zmatch (array/filter @config.zones (fn z (and (>= @payload.x (object/get @z x0)) (< @payload.x (object/get @z x1)))))))
39
+ (do
40
+ (if (and (> (array/len @zmatch) 0) (!= @entity.zoneId (object/get (array/first @zmatch) id)))
41
+ (do
42
+ (set @entity.zoneId (object/get (array/first @zmatch) id))
43
+ (set @entity.card { name: (object/get (array/first @zmatch) name), concept: (object/get (array/first @zmatch) concept), objective: (object/get (array/first @zmatch) objective) })
44
+ (set @entity.enteredAt @entity.ticks)
45
+ (emit ZONE_CHANGED { id: (object/get (array/first @zmatch) id), name: (object/get (array/first @zmatch) name), concept: (object/get (array/first @zmatch) concept), objective: (object/get (array/first @zmatch) objective) }))
46
+ null)
47
+ (if (and (!= @entity.card null) (> (- @entity.ticks @entity.enteredAt) @config.ttlTicks))
48
+ (set @entity.card null)
49
+ null)))
50
+
51
+ CARD_DISMISS -> active
52
+ (set @entity.card null)
53
+
54
+ RESTART -> active
55
+ (set @entity.zoneId "")
56
+ (set @entity.card null)
57
+ (set @entity.enteredAt 0)
58
+ (set @entity.ticks 0)
59
+ }
60
+
61
+ emits {
62
+ ZONE_CHANGED -> external {
63
+ id : string!
64
+ name : string!
65
+ concept : string
66
+ objective : string
67
+ }
68
+ @description "The player crossed into a different zone; carries the zone's card fields (name, concept, objective) so boards can swap banners/guides and hold the card up. Fires once per crossing."
69
+ @tier "domain"
70
+ }
71
+
72
+ listens {
73
+ BODY_MOVED { x : number!, y : number!, vx : number!, vy : number!, grounded : boolean!, skating : boolean!, speed : number! }
74
+ CARD_DISMISS { id : string }
75
+ RESTART { id : string }
76
+ }
77
+
78
+ config {
79
+ running : boolean = true
80
+ @label "Running"
81
+ @description "When true, BODY_MOVED tracks the current zone and ages the card; toggle to freeze the card as-is."
82
+ @tier "domain"
83
+ zones : [ZoneCardZone] = []
84
+ @label "Zones"
85
+ @description "One row per world band: id, display name, the one-line concept, the zone's objective text, and the x-range (x0..x1) the band covers."
86
+ @tier "domain"
87
+ ttlTicks : number = 120
88
+ @label "TTL Ticks"
89
+ @description "BODY_MOVED ticks the card stays up after a zone crossing (≈ 4s at 30 moves/s) before it clears itself."
90
+ @tier "domain"
91
+ }
92
+ }
93
+
94
+ page "/zone-card" as ZoneCardPage -> ZoneCard
95
+ }
@@ -79,6 +79,14 @@ orbital PlatformerBoard2DOrbital {
79
79
  right : Asset
80
80
  }
81
81
 
82
+ type BoardCurveMod = {
83
+ x0 : number!
84
+ x1 : number!
85
+ a : number!
86
+ h : number!
87
+ k : number!
88
+ }
89
+
82
90
  entity GameState [runtime, shared] {
83
91
  id : string!
84
92
  body : PlatformerBody = { x: 80, y: 320, width: 32, height: 48, vx: 0, vy: 0, grounded: false, facingRight: true }
@@ -86,6 +94,7 @@ orbital PlatformerBoard2DOrbital {
86
94
  hazardId : string = ""
87
95
  goalId : string = ""
88
96
  skating : boolean = false
97
+ curveMods : [BoardCurveMod] = []
89
98
  fx : [Fx] = []
90
99
  result : string = "none"
91
100
  score : number = 0