@almadar/std 16.178.0 → 16.179.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 (73) hide show
  1. package/behaviors/lolo/ui/core/molecules/ui-fx-overlay.lolo +63 -0
  2. package/behaviors/lolo/ui/core/molecules/ui-page-header.lolo +1 -1
  3. package/behaviors/lolo/ui/core/organisms/ui-form-section.lolo +2 -2
  4. package/behaviors/lolo/ui/core/organisms/ui-form.lolo +2 -2
  5. package/behaviors/lolo/ui/core/templates/ui-dashboard-layout.lolo +2 -2
  6. package/behaviors/lolo/ui/game/atoms/std-fx-particles.lolo +23 -4
  7. package/behaviors/lolo/ui/game/atoms/std-platformer-body.lolo +251 -0
  8. package/behaviors/lolo/ui/game/atoms/ui-draw-fx-layer.lolo +109 -0
  9. package/behaviors/lolo/ui/game/organisms/std-arcade-board-3d.lolo +32 -2
  10. package/behaviors/lolo/ui/game/organisms/std-crew-board-2d.lolo +32 -2
  11. package/behaviors/lolo/ui/game/organisms/std-dungeon-board-2d.lolo +32 -2
  12. package/behaviors/lolo/ui/game/organisms/std-market-board-3d.lolo +32 -2
  13. package/behaviors/lolo/ui/game/organisms/std-objective-board-3d.lolo +32 -2
  14. package/behaviors/lolo/ui/game/organisms/std-platformer-board-2d.lolo +391 -0
  15. package/behaviors/lolo/ui/game/organisms/std-survival-board-3d.lolo +32 -2
  16. package/behaviors/lolo/ui/game/organisms/std-tactics-board-2d.lolo +34 -6
  17. package/behaviors/lolo/ui/game/organisms/std-tactics-board-3d.lolo +34 -4
  18. package/behaviors/lolo/ui/learning/atoms/std-learn-arrangement.lolo +8 -0
  19. package/behaviors/lolo/ui/learning/atoms/std-learn-classify.lolo +8 -0
  20. package/behaviors/lolo/ui/learning/atoms/std-learn-derivation.lolo +8 -0
  21. package/behaviors/lolo/ui/learning/atoms/std-learn-doser.lolo +9 -0
  22. package/behaviors/lolo/ui/learning/atoms/std-learn-estimate.lolo +8 -0
  23. package/behaviors/lolo/ui/learning/atoms/std-learn-fx-cues.lolo +147 -0
  24. package/behaviors/lolo/ui/learning/atoms/std-learn-goal-seek.lolo +8 -0
  25. package/behaviors/lolo/ui/learning/atoms/std-learn-hotspot.lolo +8 -0
  26. package/behaviors/lolo/ui/learning/atoms/std-learn-predict.lolo +8 -0
  27. package/behaviors/lolo/ui/learning/atoms/std-learn-scatter-fit.lolo +9 -0
  28. package/behaviors/lolo/ui/learning/atoms/std-learn-speed-drill.lolo +8 -0
  29. package/behaviors/lolo/ui/learning/atoms/std-learn-tour-3d.lolo +9 -0
  30. package/behaviors/registry/ui/core/molecules/ui-fx-overlay.orb +281 -0
  31. package/behaviors/registry/ui/core/molecules/ui-page-header.orb +1 -1
  32. package/behaviors/registry/ui/core/organisms/ui-form-section.orb +6 -0
  33. package/behaviors/registry/ui/core/organisms/ui-form.orb +6 -0
  34. package/behaviors/registry/ui/core/templates/ui-dashboard-layout.orb +2 -2
  35. package/behaviors/registry/ui/game/atoms/std-fx-particles.orb +265 -4
  36. package/behaviors/registry/ui/game/atoms/std-platformer-body.orb +2136 -0
  37. package/behaviors/registry/ui/game/atoms/ui-draw-fx-layer.orb +523 -0
  38. package/behaviors/registry/ui/game/organisms/std-arcade-board-3d.orb +159 -70
  39. package/behaviors/registry/ui/game/organisms/std-crew-board-2d.orb +148 -58
  40. package/behaviors/registry/ui/game/organisms/std-dungeon-board-2d.orb +169 -58
  41. package/behaviors/registry/ui/game/organisms/std-market-board-3d.orb +160 -70
  42. package/behaviors/registry/ui/game/organisms/std-objective-board-3d.orb +151 -60
  43. package/behaviors/registry/ui/game/organisms/std-platformer-board-2d.orb +2981 -0
  44. package/behaviors/registry/ui/game/organisms/std-survival-board-3d.orb +150 -60
  45. package/behaviors/registry/ui/game/organisms/std-tactics-board-2d.orb +163 -316
  46. package/behaviors/registry/ui/game/organisms/std-tactics-board-3d.orb +156 -184
  47. package/behaviors/registry/ui/learning/atoms/std-learn-arrangement.orb +62 -0
  48. package/behaviors/registry/ui/learning/atoms/std-learn-classify.orb +73 -0
  49. package/behaviors/registry/ui/learning/atoms/std-learn-derivation.orb +66 -0
  50. package/behaviors/registry/ui/learning/atoms/std-learn-doser.orb +71 -0
  51. package/behaviors/registry/ui/learning/atoms/std-learn-estimate.orb +72 -0
  52. package/behaviors/registry/ui/learning/atoms/std-learn-fx-cues.orb +1546 -0
  53. package/behaviors/registry/ui/learning/atoms/std-learn-goal-seek.orb +58 -0
  54. package/behaviors/registry/ui/learning/atoms/std-learn-hotspot.orb +72 -0
  55. package/behaviors/registry/ui/learning/atoms/std-learn-predict.orb +72 -0
  56. package/behaviors/registry/ui/learning/atoms/std-learn-scatter-fit.orb +71 -0
  57. package/behaviors/registry/ui/learning/atoms/std-learn-speed-drill.orb +72 -0
  58. package/behaviors/registry/ui/learning/atoms/std-learn-tour-3d.orb +176 -0
  59. package/dist/behaviors/exports-reader.js +8369 -5005
  60. package/dist/behaviors/functions/index.d.ts +604 -14
  61. package/dist/behaviors/functions/index.js +8354 -5006
  62. package/dist/behaviors/index.d.ts +1 -1
  63. package/dist/behaviors/index.js +8370 -5006
  64. package/dist/behaviors/query.js +8369 -5005
  65. package/dist/behaviors-embeddings.hash.json +3 -3
  66. package/dist/behaviors-embeddings.json +351 -331
  67. package/dist/behaviors-registry.json +1879 -344
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.js +8370 -5006
  70. package/dist/knob-embeddings.hash.json +3 -3
  71. package/dist/knob-embeddings.json +1 -1
  72. package/dist/registry/factory-signatures.json +1 -1
  73. package/package.json +2 -2
@@ -0,0 +1,63 @@
1
+ app ui-fx-overlay "1.0.0"
2
+ "FxOverlay — UI factory (1:1 with the @almadar/ui fx-overlay pattern)."
3
+ orbital FxOverlayOrbital {
4
+ type FxOverlayItemsItem = {
5
+ id : string!
6
+ type : string!
7
+ effect : "confetti" | "flash" | "shake" | "sparkle" | "float-text" | "streak-glow"
8
+ message : string
9
+ color : string
10
+ size : number
11
+ x : number
12
+ z : number
13
+ y : number
14
+ ttl : number!
15
+ maxTtl : number
16
+ bornAt : number
17
+ space : "world" | "screen"
18
+ particleCount : number
19
+ vx : number
20
+ vy : number
21
+ vz : number
22
+ }
23
+
24
+ entity FxOverlayItem [runtime] {
25
+ id : string!
26
+ }
27
+
28
+ trait FxOverlayRender -> @rebindable FxOverlayItem [interaction, instance] {
29
+ initial: idle
30
+
31
+ state idle {
32
+ INIT -> idle
33
+ (render-ui main {
34
+ items: @config.items
35
+ tickMs: @config.tickMs
36
+ children: @config.children
37
+ className: @config.className
38
+ type: fx-overlay
39
+ })
40
+ }
41
+
42
+ config {
43
+ items : [FxOverlayItemsItem] = [{ id: "e1", x: 2, y: 2, type: "spawn", message: "Unit spawned" }, { id: "e2", x: 3, y: 1, type: "effect", message: "Attack landed" }]
44
+ @label "Items"
45
+ @description "Live screen-space fx entries (the mechanic's fx list filtered to `space == 'screen'`); mount starts each item's animation, ttl decay unmounts it."
46
+ @tier "presentation"
47
+ tickMs : number
48
+ @label "Tick Ms"
49
+ @description "The fx mechanic's decay tick period in ms; per-item animation duration = maxTtl × tickMs. Default 500."
50
+ @tier "presentation"
51
+ children : node
52
+ @label "Children"
53
+ @description "Wrapped content — the `shake` target and the overlay's bounds."
54
+ @tier "presentation"
55
+ className : string
56
+ @label "Class Name"
57
+ @description "Additional class names on the outermost element."
58
+ @tier "presentation"
59
+ }
60
+ }
61
+
62
+ page "/fx-overlay" as FxOverlayPage -> FxOverlayRender
63
+ }
@@ -94,7 +94,7 @@ orbital PageHeaderOrbital {
94
94
  @tier "presentation"
95
95
  status : PageHeaderStatus
96
96
  @label "Status"
97
- @description "Status badge"
97
+ @description "Status badge — a bare string (an entity's status field bound directly, e.g. `status: @entity.status`) renders as a default-variant badge"
98
98
  @tier "presentation"
99
99
  actions : [PageHeaderActionsItem] = []
100
100
  @label "Actions"
@@ -69,10 +69,10 @@ orbital FormSectionOrbital {
69
69
  message : string!
70
70
  }
71
71
  type FormSectionEvaluationContext = {
72
- formValues : object!
72
+ formValues : Map string object!
73
73
  globalVariables : Map string json!
74
74
  localVariables : Map string json
75
- entity : object
75
+ entity : Map string object
76
76
  }
77
77
  type FormSectionSectionsItem = {
78
78
  id : string!
@@ -69,10 +69,10 @@ orbital FormOrbital {
69
69
  message : string!
70
70
  }
71
71
  type FormEvaluationContext = {
72
- formValues : object!
72
+ formValues : Map string object!
73
73
  globalVariables : Map string json!
74
74
  localVariables : Map string json
75
- entity : object
75
+ entity : Map string object
76
76
  }
77
77
  type FormSectionsItem = {
78
78
  id : string!
@@ -10,7 +10,7 @@ orbital DashboardLayoutOrbital {
10
10
  }
11
11
  type DashboardLayoutUser = {
12
12
  name : string!
13
- email : string!
13
+ email : string
14
14
  avatar : asset
15
15
  }
16
16
  type DashboardLayoutTopBarActionsItem = {
@@ -98,7 +98,7 @@ orbital DashboardLayoutOrbital {
98
98
  @tier "presentation"
99
99
  user : DashboardLayoutUser
100
100
  @label "User"
101
- @description "Current user info (optional - auto-populated from auth context if not provided)"
101
+ @description "Current user info (optional - auto-populated from auth context if not provided). Accepts `null` as well as `undefined`: the `.lolo` substrate's signed-out spelling is `(if @config.viewerName {…} null)`, and the compiled path passes that `null` through verbatim."
102
102
  @tier "presentation"
103
103
  headerActions : node
104
104
  @label "Header Actions"
@@ -1,5 +1,5 @@
1
- app std-fx-particles "1.0.0"
2
- "Headless floating-text/particle mechanic: decays each fx entry's ttl every tick and drops the dead ones, and appends spawned particles on BURST. Composed onto a [runtime, shared] game entity; renders nothing (the board owns the single render authority)."
1
+ app std-fx-particles "1.1.0"
2
+ "Headless transient-fx mechanic serving BOTH game boards and learning shells: appends spawned particles on BURST — stamping maxTtl (fade envelope) and bornAt (epoch ms, smooth render interpolation) when absent — decays each entry's ttl every tick, drops the dead ones, and clears on RESTART. Name-blind mechanism: the effect-kind vocabulary is consumer-declared (FxPreset tables at the render call sites). Composed onto a [runtime, shared] entity; renders nothing the board/shell owns the single render authority (draw-fx-layer for space \"world\", fx-overlay for space \"screen\")."
3
3
  orbital FxParticlesOrbital {
4
4
  type Fx = {
5
5
  id : string!
@@ -9,6 +9,16 @@ orbital FxParticlesOrbital {
9
9
  y : number
10
10
  message : string
11
11
  ttl : number!
12
+ maxTtl : number
13
+ bornAt : number
14
+ space : string
15
+ effect : string
16
+ color : string
17
+ size : number
18
+ vx : number
19
+ vy : number
20
+ vz : number
21
+ particleCount : number
12
22
  }
13
23
 
14
24
  entity FxState [runtime, shared] {
@@ -21,9 +31,17 @@ orbital FxParticlesOrbital {
21
31
 
22
32
  state active {
23
33
  INIT -> active
24
- (set @entity.fx @config.fx)
34
+ (set @entity.fx (array/map @config.fx (fn p (object/merge @p {
35
+ maxTtl: (if (object/get @p maxTtl) (object/get @p maxTtl) (object/get @p ttl))
36
+ bornAt: (if (object/get @p bornAt) (object/get @p bornAt) (time/now))
37
+ }))))
25
38
  BURST -> active
26
- (set @entity.fx (array/concat @entity.fx @payload.particles))
39
+ (set @entity.fx (array/concat @entity.fx (array/map @payload.particles (fn p (object/merge @p {
40
+ maxTtl: (if (object/get @p maxTtl) (object/get @p maxTtl) (object/get @p ttl))
41
+ bornAt: (if (object/get @p bornAt) (object/get @p bornAt) (time/now))
42
+ })))))
43
+ RESTART -> active
44
+ (set @entity.fx [])
27
45
  }
28
46
 
29
47
  ticks {
@@ -36,6 +54,7 @@ orbital FxParticlesOrbital {
36
54
 
37
55
  listens {
38
56
  BURST { particles : [Fx] }
57
+ RESTART { }
39
58
  }
40
59
 
41
60
  config {
@@ -0,0 +1,251 @@
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."
3
+ orbital PlatformerBodyOrbital {
4
+ type PlatformerBody = {
5
+ x : number!
6
+ y : number!
7
+ width : number!
8
+ height : number!
9
+ vx : number
10
+ vy : number
11
+ grounded : boolean
12
+ facingRight : boolean
13
+ }
14
+ type PlatformerPlatform = {
15
+ id : string
16
+ x : number!
17
+ y : number!
18
+ width : number!
19
+ height : number!
20
+ type : "ground" | "platform" | "hazard" | "goal"
21
+ }
22
+ type PlatformerCurveSample = {
23
+ x : number!
24
+ y : number!
25
+ }
26
+
27
+ entity PlatformerBodyData [runtime, shared] {
28
+ id : string!
29
+ body : PlatformerBody = { x: 0, y: 0, width: 1, height: 1, vx: 0, vy: 0, grounded: false, facingRight: true }
30
+ fell : boolean = false
31
+ hazardId : string = ""
32
+ goalId : string = ""
33
+ skating : boolean = false
34
+ }
35
+
36
+ trait PlatformerBody -> @rebindable PlatformerBodyData [lifecycle] {
37
+ initial: active
38
+
39
+ state active {
40
+ INIT -> active
41
+ (set @entity.body @config.spawn)
42
+ (set @entity.fell false)
43
+ (set @entity.hazardId "")
44
+ (set @entity.goalId "")
45
+ (set @entity.skating false)
46
+
47
+ STEER -> active when @config.running
48
+ (set @entity.body (object/merge @entity.body { vx: (* @payload.dir @config.moveSpeed), facingRight: (if (!= @payload.dir 0) (> @payload.dir 0) @entity.body.facingRight) }))
49
+
50
+ JUMP -> active when (and @config.running (== @entity.body.grounded true))
51
+ (set @entity.body (object/merge @entity.body { vy: (if (> @config.gravity 0) (- 0 @config.jumpStrength) @config.jumpStrength), grounded: false }))
52
+ (emit BODY_JUMPED { x: @entity.body.x, y: @entity.body.y })
53
+
54
+ PLACE -> active
55
+ (set @entity.body (object/merge @entity.body { x: @payload.x, y: @payload.y, vx: 0, vy: 0, grounded: false }))
56
+ (set @entity.fell false)
57
+ (set @entity.hazardId "")
58
+ (set @entity.skating false)
59
+
60
+ RESTART -> active
61
+ (set @entity.body @config.spawn)
62
+ (set @entity.fell false)
63
+ (set @entity.hazardId "")
64
+ (set @entity.goalId "")
65
+ (set @entity.skating false)
66
+ }
67
+
68
+ ticks {
69
+ physicsTick every 33ms
70
+ when (and @config.running (> @entity.body.width 0))
71
+ (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)))
73
+ (let ((bw (object/get @entity.body width)) (bh (object/get @entity.body height)))
74
+ (let ((nx (math/clamp (+ @entity.body.x (* @entity.body.vx 0.033)) @config.xMin @config.xMax)))
75
+ (let ((ny (+ @entity.body.y (* @vy1 0.033))))
76
+ (let ((rect { x: @nx, y: @ny, w: @bw, h: @bh }))
77
+ (let ((hit (array/find @config.platforms (fn p (and (or (== (object/get @p type) "ground") (== (object/get @p type) "platform")) (geo/aabb-overlap @rect { x: @p.x, y: @p.y, w: @p.width, h: @p.height }))))))
78
+ (let ((landed (and (!= @hit null) (> (* @vy1 @gdir) 0))))
79
+ (let ((ny2 (if @landed (if (> @gdir 0) (- (object/get @hit y) @bh) (+ (object/get @hit y) (object/get @hit height))) @ny)) (vy2 (if @landed 0 @vy1)))
80
+ (let ((cx (+ @nx (/ @bw 2))))
81
+ (let ((inRange (and @config.skate (and (> (array/len @config.skateCurve) 1) (and (>= @cx (object/get (array/first @config.skateCurve) x)) (< @cx (object/get (array/last @config.skateCurve) x)))))))
82
+ (let ((leftSamp (if @inRange (array/last (array/filter @config.skateCurve (fn s (<= (object/get @s x) @cx)))) { x: 0, y: 0 })))
83
+ (let ((rightSamp (if @inRange (array/first (array/filter @config.skateCurve (fn s (> (object/get @s x) @cx)))) { x: 1, y: 0 })))
84
+ (let ((slope (if @inRange (/ (- (object/get @rightSamp y) (object/get @leftSamp y)) (- (object/get @rightSamp x) (object/get @leftSamp x))) 0)))
85
+ (let ((curveY (if @inRange (+ (object/get @leftSamp y) (* @slope (- @cx (object/get @leftSamp x)))) 0)))
86
+ (let ((movingUp (< (* @vy1 @gdir) -0.01)))
87
+ (let ((skateNow (and @inRange (and (<= (math/abs (- @ny2 @curveY)) @config.skateSnap) (and (or @landed @entity.skating) (or @entity.body.grounded (not @movingUp)))))))
88
+ (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)))
90
+ (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
+ (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
+ (let ((fellNow (if (> @gdir 0) (> @fy @config.killY) (< @fy @config.killY))))
93
+ (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 }))
95
+ (set @entity.skating @skateNow)
96
+ (if (and @skateNow (== @entity.skating false))
97
+ (emit BODY_SKATE_START { x: @nx, y: @fy, slope: @slope })
98
+ null)
99
+ (if (and (== @skateNow false) (== @entity.skating true))
100
+ (emit BODY_SKATE_END { x: @nx, y: @fy })
101
+ null)
102
+ (if (and @landed (== @entity.body.grounded false))
103
+ (emit BODY_LANDED { x: @nx, y: @fy, impact: (* @vy1 @gdir) })
104
+ null)
105
+ (if (!= @hazardHit null)
106
+ (if (!= @entity.hazardId (object/get @hazardHit "id" ""))
107
+ (do
108
+ (set @entity.hazardId (object/get @hazardHit "id" ""))
109
+ (emit BODY_HAZARD { id: (object/get @hazardHit "id" ""), x: @nx, y: @fy }))
110
+ null)
111
+ (set @entity.hazardId ""))
112
+ (if (!= @goalHit null)
113
+ (if (!= @entity.goalId (object/get @goalHit "id" ""))
114
+ (do
115
+ (set @entity.goalId (object/get @goalHit "id" ""))
116
+ (emit BODY_GOAL { id: (object/get @goalHit "id" ""), x: @nx, y: @fy }))
117
+ null)
118
+ (set @entity.goalId ""))
119
+ (if (and @fellNow (== @entity.fell false))
120
+ (do
121
+ (set @entity.fell true)
122
+ (emit BODY_FELL { x: @nx, y: @fy }))
123
+ 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) }))))))))))))))))))))))))
125
+ }
126
+
127
+ emits {
128
+ BODY_MOVED -> external {
129
+ x : number!
130
+ y : number!
131
+ vx : number!
132
+ vy : number!
133
+ grounded : boolean!
134
+ skating : boolean!
135
+ speed : number!
136
+ }
137
+ @description "The body after this physics tick: position, velocity, grounded and skating flags, and scalar speed; render layers subscribe (or read the shared entity), rule layers stay on the sparse signals"
138
+ @tier "domain"
139
+ BODY_SKATE_START -> external {
140
+ x : number!
141
+ y : number!
142
+ slope : number!
143
+ }
144
+ @description "Body engaged curve skating this tick (grounded within skateSnap of the skateCurve surface); slope is the local dy/dx — boards layer fx and equation readouts"
145
+ @tier "domain"
146
+ BODY_SKATE_END -> external {
147
+ x : number!
148
+ y : number!
149
+ }
150
+ @description "Body left the curve surface (jumped, walked past an end, or drifted beyond skateSnap); boards tear down skate fx"
151
+ @tier "domain"
152
+ BODY_JUMPED -> external {
153
+ x : number!
154
+ y : number!
155
+ }
156
+ @description "Jump accepted (grounded at intent time) and vertical velocity kicked against gravity; boards layer squash/sound fx"
157
+ @tier "domain"
158
+ BODY_LANDED -> external {
159
+ x : number!
160
+ y : number!
161
+ impact : number!
162
+ }
163
+ @description "Body transitioned airborne → standing on a ground/platform tile this tick; impact is the pre-landing fall speed in units/s so boards can scale dust or damage"
164
+ @tier "domain"
165
+ BODY_HAZARD -> external {
166
+ id : string!
167
+ x : number!
168
+ y : number!
169
+ }
170
+ @description "Body entered overlap with a hazard tile; fires once per entry and re-arms only after the body leaves, so damage rules stay sparse"
171
+ @tier "domain"
172
+ BODY_GOAL -> external {
173
+ id : string!
174
+ x : number!
175
+ y : number!
176
+ }
177
+ @description "Body entered overlap with a goal tile; fires once per entry and re-arms only after the body leaves — the board's win-rule trigger"
178
+ @tier "domain"
179
+ BODY_FELL -> external {
180
+ x : number!
181
+ y : number!
182
+ }
183
+ @description "Body crossed killY in the gravity direction (pit fall); fires once per fall and re-arms on PLACE/RESTART, leaving the respawn rule to the board"
184
+ @tier "domain"
185
+ }
186
+
187
+ listens {
188
+ STEER { dir : number! }
189
+ JUMP { id : string }
190
+ PLACE { x : number!, y : number! }
191
+ RESTART { id : string }
192
+ }
193
+
194
+ config {
195
+ running : boolean = true
196
+ @label "Running"
197
+ @description "When true the 33ms physics tick integrates and collides; toggle to freeze the body"
198
+ @tier "domain"
199
+ spawn : PlatformerBody = { x: 2, y: 8, width: 0.6, height: 0.6, vx: 0, vy: 0, grounded: false, facingRight: true }
200
+ @label "Spawn Body"
201
+ @description "Initial body rect (min-corner x/y plus width/height) seeded on INIT/RESTART; width/height also define the collision box"
202
+ @tier "domain"
203
+ platforms : [PlatformerPlatform] = []
204
+ @label "Platforms"
205
+ @description "Level geometry as min-corner rects: ground/platform tiles are solid one-way landing surfaces, hazard tiles emit BODY_HAZARD on touch, goal tiles emit BODY_GOAL"
206
+ @tier "domain"
207
+ gravity : number = 24
208
+ @label "Gravity"
209
+ @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
+ @tier "domain"
211
+ jumpStrength : number = 11
212
+ @label "Jump Strength"
213
+ @description "Vertical speed in units/s imparted by JUMP, directed against gravity; apex height ≈ jumpStrength² / (2·|gravity|)"
214
+ @tier "domain"
215
+ moveSpeed : number = 5
216
+ @label "Move Speed"
217
+ @description "Horizontal run speed in units/s applied by STEER dir ±1"
218
+ @tier "domain"
219
+ maxFallSpeed : number = 14
220
+ @label "Max Fall Speed"
221
+ @description "Terminal velocity cap in units/s applied to vertical speed each tick"
222
+ @tier "presentation"
223
+ xMin : number = -1000000
224
+ @label "Left Wall"
225
+ @description "Smallest x the body's min corner may hold (world bounds, not level geometry)"
226
+ @tier "presentation"
227
+ xMax : number = 1000000
228
+ @label "Right Wall"
229
+ @description "Largest x the body's min corner may hold (world bounds, not level geometry)"
230
+ @tier "presentation"
231
+ killY : number = 1000000
232
+ @description "Fall line: crossing it in the gravity direction fires BODY_FELL once (past it downward on y-down boards, below it on y-up canvases); y-up consumers must set a value below their world floor"
233
+ @label "Kill Y"
234
+ @tier "domain"
235
+ skate : boolean = false
236
+ @label "Curve Skating"
237
+ @description "When true, a grounded body within skateSnap of the skateCurve surface 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 and launches off its ends; JUMP kicks free"
238
+ @tier "domain"
239
+ skateCurve : [PlatformerCurveSample] = []
240
+ @label "Skate Curve"
241
+ @description "Surface samples (x strictly increasing) the body skates when skate is on; y is the surface height for the body's MIN corner, linearly interpolated between bracketing samples — mirror the render-side curve so what you see is what you ride"
242
+ @tier "domain"
243
+ skateSnap : number = 0.75
244
+ @label "Skate Snap"
245
+ @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
+ @tier "domain"
247
+ }
248
+ }
249
+
250
+ page "/platformer-body" as PlatformerBodyPage -> PlatformerBody
251
+ }
@@ -0,0 +1,109 @@
1
+ app ui-draw-fx-layer "1.0.0"
2
+ "DrawFxLayer — UI factory (1:1 with the @almadar/ui draw-fx-layer pattern)."
3
+ orbital DrawFxLayerOrbital {
4
+ type DrawFxLayerItemsItem = {
5
+ id : string!
6
+ type : string!
7
+ position : DrawFxLayerItemsItemPosition
8
+ x : number!
9
+ z : number
10
+ y : number
11
+ message : string
12
+ ttl : number!
13
+ maxTtl : number
14
+ bornAt : number
15
+ space : "world" | "screen"
16
+ effect : "confetti" | "flash" | "shake" | "sparkle" | "float-text" | "streak-glow"
17
+ color : string
18
+ size : number
19
+ vx : number
20
+ vy : number
21
+ vz : number
22
+ particleCount : number
23
+ }
24
+ type DrawFxLayerItemsItemPosition = {
25
+ x : number!
26
+ y : number!
27
+ z : number
28
+ }
29
+ type DrawFxLayerPresetsItem = {
30
+ type : string!
31
+ color : string
32
+ color2 : string
33
+ size : number
34
+ shape : "spark" | "ring" | "puff" | "streak"
35
+ count : number
36
+ glow : number
37
+ gravity : number
38
+ vx : number
39
+ vy : number
40
+ vz : number
41
+ space : "world" | "screen"
42
+ effect : "confetti" | "flash" | "shake" | "sparkle" | "float-text" | "streak-glow"
43
+ particleCount : number
44
+ }
45
+ type DrawFxLayerSpritesValue = {
46
+ url : asset!
47
+ role : string!
48
+ category : string!
49
+ animations : [string]
50
+ style : "pixel" | "vector" | "hd" | "1-bit" | "isometric"
51
+ variant : string
52
+ dimension : "2d" | "3d"
53
+ aspect : "1:1" | "16:9" | "5:7" | "8:1"
54
+ atlas : asset
55
+ sprite : string
56
+ name : string
57
+ thumbnailUrl : string
58
+ }
59
+
60
+ entity DrawFxLayerItem [runtime] {
61
+ id : string!
62
+ }
63
+
64
+ trait DrawFxLayerRender -> @rebindable DrawFxLayerItem [interaction, instance] {
65
+ initial: idle
66
+
67
+ state idle {
68
+ INIT -> idle
69
+ (render-ui main {
70
+ items: @config.items
71
+ presets: @config.presets
72
+ art: @config.art
73
+ sprites: @config.sprites
74
+ tickMs: @config.tickMs
75
+ textColor: @config.textColor
76
+ type: draw-fx-layer
77
+ })
78
+ }
79
+
80
+ config {
81
+ items : [DrawFxLayerItemsItem] = [{ id: "e1", x: 2, y: 2, type: "spawn", message: "Unit spawned" }, { id: "e2", x: 3, y: 1, type: "effect", message: "Attack landed" }]
82
+ @label "Items"
83
+ @description "The live fx entries — the mechanic's `@entity.fx` list passed whole; `space: 'screen'` entries are skipped (the overlay owns those)."
84
+ @tier "presentation"
85
+ presets : [DrawFxLayerPresetsItem] = []
86
+ @label "Presets"
87
+ @description "Consumer-declared effect vocabulary — one row per fx `type` (look + recipe). Unknown types fall back to a neutral spark burst."
88
+ @tier "presentation"
89
+ art : Map string Map string [object] = { item: {} }
90
+ @label "Art"
91
+ @description "Vector art per fx type (an asset-art map: type → anim-state → drawables); a type's `idle` state wins over sprites/procedural."
92
+ @tier "presentation"
93
+ sprites : Map string DrawFxLayerSpritesValue = { item: { url: "https://almadar-kflow-assets.web.app/shared/ui-roguelike-board/default/features/sword.png", role: "item", category: "item", animations: ["static"], style: "pixel", dimension: "2d", aspect: "1:1", name: "item", thumbnailUrl: "" } }
94
+ @label "Sprites"
95
+ @description "Sprite asset per fx type (an asset-manifest effects map) — used when no vector art matches."
96
+ @tier "presentation"
97
+ tickMs : number
98
+ @label "Tick Ms"
99
+ @description "The fx mechanic's decay tick period in ms; lifetime = maxTtl × tickMs. Default 500."
100
+ @tier "presentation"
101
+ textColor : string
102
+ @label "Text Color"
103
+ @description "Floating-message text color when an item declares none. Default `#ffe066`."
104
+ @tier "presentation"
105
+ }
106
+ }
107
+
108
+ page "/draw-fx-layer" as DrawFxLayerPage -> DrawFxLayerRender
109
+ }
@@ -50,6 +50,32 @@ orbital ArcadeBoard3DFrameOrbital {
50
50
  y : number
51
51
  message : string
52
52
  ttl : number!
53
+ maxTtl : number
54
+ bornAt : number
55
+ space : string
56
+ effect : string
57
+ color : string
58
+ size : number
59
+ vx : number
60
+ vy : number
61
+ vz : number
62
+ particleCount : number
63
+ }
64
+ type FxPreset = {
65
+ type : string!
66
+ color : string
67
+ color2 : string
68
+ size : number
69
+ shape : string
70
+ count : number
71
+ glow : number
72
+ gravity : number
73
+ vx : number
74
+ vy : number
75
+ vz : number
76
+ space : string
77
+ effect : string
78
+ particleCount : number
53
79
  }
54
80
  type ValidMove = { x : number! z : number! }
55
81
  type Machine = {
@@ -159,7 +185,7 @@ orbital ArcadeBoard3DFrameOrbital {
159
185
  { type: draw-shape-layer, items: (array/map @entity.units (fn u { type: draw-shape, shape: "rect", position: { x: (object/get (object/get @u position) x), y: (object/get (object/get @u position) y), z: (object/get @u z) }, offsetY: -0.6, width: 0.8, height: 0.12, fill: "rgba(0,0,0,0.6)" })) }
160
186
  { type: draw-shape-layer, items: (array/map @entity.units (fn u { type: draw-shape, shape: "rect", position: { x: (object/get (object/get @u position) x), y: (object/get (object/get @u position) y), z: (object/get @u z) }, offsetY: -0.6, width: (* 0.8 (/ (object/get @u health) (object/get @u maxHealth))), height: 0.12, fill: "#22c55e" })) }
161
187
  { type: draw-text-layer, items: (array/map @entity.units (fn u { type: draw-text, text: (object/get @u name), position: { x: (object/get (object/get @u position) x), y: (object/get (object/get @u position) y), z: (object/get @u z) }, offsetY: -0.85, color: "#ffffff" })) }
162
- { type: draw-text-layer, items: (array/map @entity.fx (fn f { type: draw-text, text: (object/get @f message), position: { x: (object/get @f x), y: (object/get @f y), z: (object/get @f z) }, color: "#ffe066" })) }
188
+ { type: draw-fx-layer, items: @entity.fx, tickMs: 500, presets: @config.fxPresets }
163
189
  ] } ]
164
190
  })
165
191
 
@@ -205,7 +231,7 @@ orbital ArcadeBoard3DFrameOrbital {
205
231
  { type: draw-shape-layer, items: (array/map @entity.units (fn u { type: draw-shape, shape: "rect", position: { x: (object/get (object/get @u position) x), y: (object/get (object/get @u position) y), z: (object/get @u z) }, offsetY: -0.6, width: 0.8, height: 0.12, fill: "rgba(0,0,0,0.6)" })) }
206
232
  { type: draw-shape-layer, items: (array/map @entity.units (fn u { type: draw-shape, shape: "rect", position: { x: (object/get (object/get @u position) x), y: (object/get (object/get @u position) y), z: (object/get @u z) }, offsetY: -0.6, width: (* 0.8 (/ (object/get @u health) (object/get @u maxHealth))), height: 0.12, fill: "#22c55e" })) }
207
233
  { type: draw-text-layer, items: (array/map @entity.units (fn u { type: draw-text, text: (object/get @u name), position: { x: (object/get (object/get @u position) x), y: (object/get (object/get @u position) y), z: (object/get @u z) }, offsetY: -0.85, color: "#ffffff" })) }
208
- { type: draw-text-layer, items: (array/map @entity.fx (fn f { type: draw-text, text: (object/get @f message), position: { x: (object/get @f x), y: (object/get @f y), z: (object/get @f z) }, color: "#ffe066" })) }
234
+ { type: draw-fx-layer, items: @entity.fx, tickMs: 500, presets: @config.fxPresets }
209
235
  ] } ]
210
236
  })
211
237
 
@@ -315,6 +341,10 @@ orbital ArcadeBoard3DFrameOrbital {
315
341
  @label "Features"
316
342
  @description "Arcade machines + decorations rendered on the board (static; matched against machines for playability)."
317
343
  @tier "presentation"
344
+ fxPresets : [FxPreset] = [ { type: "score", color: "#ffd23f", color2: "#ff6b35", size: 0.55, shape: "spark", count: 7, glow: 0.16, vy: -0.3 }, { type: "info", color: "#38bdf8", color2: "#0c4a6e", size: 0.4, shape: "puff", count: 4 } ]
345
+ @label "Fx Presets"
346
+ @description "This board's effect vocabulary — one look/recipe row per fx type its mechanics emit; unknown types fall back to a neutral spark."
347
+ @tier "presentation"
318
348
  assetManifest : AssetManifest = { terrains: {}, units: {}, features: {}, effects: {} }
319
349
  @label "Asset Manifest"
320
350
  @description "Per-role model manifest resolving tile/unit/feature models by terrain/category key."
@@ -48,6 +48,32 @@ orbital CrewBoard2DOrbital {
48
48
  y : number
49
49
  message : string
50
50
  ttl : number!
51
+ maxTtl : number
52
+ bornAt : number
53
+ space : string
54
+ effect : string
55
+ color : string
56
+ size : number
57
+ vx : number
58
+ vy : number
59
+ vz : number
60
+ particleCount : number
61
+ }
62
+ type FxPreset = {
63
+ type : string!
64
+ color : string
65
+ color2 : string
66
+ size : number
67
+ shape : string
68
+ count : number
69
+ glow : number
70
+ gravity : number
71
+ vx : number
72
+ vy : number
73
+ vz : number
74
+ space : string
75
+ effect : string
76
+ particleCount : number
51
77
  }
52
78
  type ValidMove = { x : number! z : number! }
53
79
  type Feature = {
@@ -141,7 +167,7 @@ orbital CrewBoard2DOrbital {
141
167
  { type: draw-shape-layer, items: (array/map @entity.units (fn u { type: draw-shape, shape: "rect", position: (object/get @u position), anchor: "ground", offsetX: -0.25, offsetY: -0.6, width: 0.5, height: 0.12, fill: "rgba(0,0,0,0.6)" })) }
142
168
  { type: draw-shape-layer, items: (array/map @entity.units (fn u { type: draw-shape, shape: "rect", position: (object/get @u position), anchor: "ground", offsetX: -0.25, offsetY: -0.6, width: (* 0.5 (/ (object/get @u health) (object/get @u maxHealth))), height: 0.12, fill: "#22c55e" })) }
143
169
  { type: draw-text-layer, items: (array/map @entity.units (fn u { type: draw-text, text: (object/get @u name), position: (object/get @u position), offsetY: -0.85, color: "#ffffff" })) }
144
- { type: draw-text-layer, items: (array/map @entity.fx (fn f { type: draw-text, text: (object/get @f message), position: { x: (object/get @f x), y: (object/get @f y) }, color: "#ffe066" })) }
170
+ { type: draw-fx-layer, items: @entity.fx, tickMs: 500, presets: @config.fxPresets, sprites: (object/get @config.assetManifest effects) }
145
171
  { type: draw-sprite-layer, items: (array/map (array/filter (array/flatten (array/map @entity.units (fn u (array/flatten (array/map [ ["idle", 4], ["walk", 2], ["attack", 1], ["hit", 1], ["death", 1] ] (fn st (array/map (array/range 0 (array/nth @st 1)) (fn i (if (and (object/get (object/get @u modelUrl) url) (str/includes (object/get (object/get @u modelUrl) url) ".svg")) (str/concat (array/nth (str/split (object/get (object/get @u modelUrl) url) ".svg") 0) (str/concat "." (str/concat (array/nth @st 0) (str/concat "-" (str/concat @i ".svg"))))) ""))))))))) (fn w (!= @w ""))) (fn warmUrl { type: draw-sprite, position: { x: 0, y: 0 }, asset: { url: @warmUrl }, opacity: 0, width: 0.5, height: 0.5 })) }
146
172
  ] } ]
147
173
  })
@@ -193,7 +219,7 @@ orbital CrewBoard2DOrbital {
193
219
  { type: draw-shape-layer, items: (array/map @entity.units (fn u { type: draw-shape, shape: "rect", position: (object/get @u position), anchor: "ground", offsetX: -0.25, offsetY: -0.6, width: 0.5, height: 0.12, fill: "rgba(0,0,0,0.6)" })) }
194
220
  { type: draw-shape-layer, items: (array/map @entity.units (fn u { type: draw-shape, shape: "rect", position: (object/get @u position), anchor: "ground", offsetX: -0.25, offsetY: -0.6, width: (* 0.5 (/ (object/get @u health) (object/get @u maxHealth))), height: 0.12, fill: "#22c55e" })) }
195
221
  { type: draw-text-layer, items: (array/map @entity.units (fn u { type: draw-text, text: (object/get @u name), position: (object/get @u position), offsetY: -0.85, color: "#ffffff" })) }
196
- { type: draw-text-layer, items: (array/map @entity.fx (fn f { type: draw-text, text: (object/get @f message), position: { x: (object/get @f x), y: (object/get @f y) }, color: "#ffe066" })) }
222
+ { type: draw-fx-layer, items: @entity.fx, tickMs: 500, presets: @config.fxPresets, sprites: (object/get @config.assetManifest effects) }
197
223
  { type: draw-sprite-layer, items: (array/map (array/filter (array/flatten (array/map @entity.units (fn u (array/flatten (array/map [ ["idle", 4], ["walk", 2], ["attack", 1], ["hit", 1], ["death", 1] ] (fn st (array/map (array/range 0 (array/nth @st 1)) (fn i (if (and (object/get (object/get @u modelUrl) url) (str/includes (object/get (object/get @u modelUrl) url) ".svg")) (str/concat (array/nth (str/split (object/get (object/get @u modelUrl) url) ".svg") 0) (str/concat "." (str/concat (array/nth @st 0) (str/concat "-" (str/concat @i ".svg"))))) ""))))))))) (fn w (!= @w ""))) (fn warmUrl { type: draw-sprite, position: { x: 0, y: 0 }, asset: { url: @warmUrl }, opacity: 0, width: 0.5, height: 0.5 })) }
198
224
  ] } ]
199
225
  })
@@ -250,6 +276,10 @@ orbital CrewBoard2DOrbital {
250
276
  @label "Features"
251
277
  @description "Decorative environment features (e.g. consoles) drawn on the board."
252
278
  @tier "presentation"
279
+ fxPresets : [FxPreset] = []
280
+ @label "Fx Presets"
281
+ @description "This board's effect vocabulary — one look/recipe row per fx type; its composed mechanics currently emit none, so the channel is forward-compat only."
282
+ @tier "presentation"
253
283
  assetManifest : AssetManifest = { terrains: {}, units: {}, features: {}, effects: {} }
254
284
  @label "Asset Manifest"
255
285
  @description "Terrain/unit/feature/effect sprite manifest."