@almadar/std 16.178.0 → 16.180.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 (80) hide show
  1. package/behaviors/lolo/ui/core/atoms/std-detail-layout.lolo +82 -0
  2. package/behaviors/lolo/ui/core/molecules/ui-breadcrumb.lolo +20 -2
  3. package/behaviors/lolo/ui/core/molecules/ui-fx-overlay.lolo +63 -0
  4. package/behaviors/lolo/ui/core/molecules/ui-page-header.lolo +1 -1
  5. package/behaviors/lolo/ui/core/organisms/ui-detail-panel.lolo +6 -0
  6. package/behaviors/lolo/ui/core/organisms/ui-form-section.lolo +2 -2
  7. package/behaviors/lolo/ui/core/organisms/ui-form.lolo +2 -2
  8. package/behaviors/lolo/ui/core/templates/ui-dashboard-layout.lolo +2 -2
  9. package/behaviors/lolo/ui/game/atoms/std-fx-particles.lolo +23 -4
  10. package/behaviors/lolo/ui/game/atoms/std-platformer-body.lolo +251 -0
  11. package/behaviors/lolo/ui/game/atoms/ui-draw-fx-layer.lolo +109 -0
  12. package/behaviors/lolo/ui/game/organisms/std-arcade-board-3d.lolo +32 -2
  13. package/behaviors/lolo/ui/game/organisms/std-crew-board-2d.lolo +32 -2
  14. package/behaviors/lolo/ui/game/organisms/std-dungeon-board-2d.lolo +32 -2
  15. package/behaviors/lolo/ui/game/organisms/std-market-board-3d.lolo +32 -2
  16. package/behaviors/lolo/ui/game/organisms/std-objective-board-3d.lolo +32 -2
  17. package/behaviors/lolo/ui/game/organisms/std-platformer-board-2d.lolo +391 -0
  18. package/behaviors/lolo/ui/game/organisms/std-survival-board-3d.lolo +32 -2
  19. package/behaviors/lolo/ui/game/organisms/std-tactics-board-2d.lolo +34 -6
  20. package/behaviors/lolo/ui/game/organisms/std-tactics-board-3d.lolo +34 -4
  21. package/behaviors/lolo/ui/learning/atoms/std-learn-arrangement.lolo +8 -0
  22. package/behaviors/lolo/ui/learning/atoms/std-learn-classify.lolo +8 -0
  23. package/behaviors/lolo/ui/learning/atoms/std-learn-derivation.lolo +8 -0
  24. package/behaviors/lolo/ui/learning/atoms/std-learn-doser.lolo +9 -0
  25. package/behaviors/lolo/ui/learning/atoms/std-learn-estimate.lolo +8 -0
  26. package/behaviors/lolo/ui/learning/atoms/std-learn-fx-cues.lolo +147 -0
  27. package/behaviors/lolo/ui/learning/atoms/std-learn-goal-seek.lolo +8 -0
  28. package/behaviors/lolo/ui/learning/atoms/std-learn-hotspot.lolo +8 -0
  29. package/behaviors/lolo/ui/learning/atoms/std-learn-predict.lolo +8 -0
  30. package/behaviors/lolo/ui/learning/atoms/std-learn-scatter-fit.lolo +9 -0
  31. package/behaviors/lolo/ui/learning/atoms/std-learn-speed-drill.lolo +8 -0
  32. package/behaviors/lolo/ui/learning/atoms/std-learn-tour-3d.lolo +9 -0
  33. package/behaviors/registry/ui/core/atoms/std-detail-layout.orb +265 -0
  34. package/behaviors/registry/ui/core/molecules/ui-breadcrumb.orb +55 -17
  35. package/behaviors/registry/ui/core/molecules/ui-fx-overlay.orb +281 -0
  36. package/behaviors/registry/ui/core/molecules/ui-page-header.orb +1 -1
  37. package/behaviors/registry/ui/core/organisms/ui-detail-panel.orb +8 -0
  38. package/behaviors/registry/ui/core/organisms/ui-form-section.orb +6 -0
  39. package/behaviors/registry/ui/core/organisms/ui-form.orb +6 -0
  40. package/behaviors/registry/ui/core/templates/ui-dashboard-layout.orb +2 -2
  41. package/behaviors/registry/ui/game/atoms/std-fx-particles.orb +265 -4
  42. package/behaviors/registry/ui/game/atoms/std-platformer-body.orb +2136 -0
  43. package/behaviors/registry/ui/game/atoms/ui-draw-fx-layer.orb +523 -0
  44. package/behaviors/registry/ui/game/organisms/std-arcade-board-3d.orb +159 -70
  45. package/behaviors/registry/ui/game/organisms/std-crew-board-2d.orb +148 -58
  46. package/behaviors/registry/ui/game/organisms/std-dungeon-board-2d.orb +169 -58
  47. package/behaviors/registry/ui/game/organisms/std-market-board-3d.orb +160 -70
  48. package/behaviors/registry/ui/game/organisms/std-objective-board-3d.orb +151 -60
  49. package/behaviors/registry/ui/game/organisms/std-platformer-board-2d.orb +2981 -0
  50. package/behaviors/registry/ui/game/organisms/std-survival-board-3d.orb +150 -60
  51. package/behaviors/registry/ui/game/organisms/std-tactics-board-2d.orb +163 -316
  52. package/behaviors/registry/ui/game/organisms/std-tactics-board-3d.orb +156 -184
  53. package/behaviors/registry/ui/learning/atoms/std-learn-arrangement.orb +62 -0
  54. package/behaviors/registry/ui/learning/atoms/std-learn-classify.orb +73 -0
  55. package/behaviors/registry/ui/learning/atoms/std-learn-derivation.orb +66 -0
  56. package/behaviors/registry/ui/learning/atoms/std-learn-doser.orb +71 -0
  57. package/behaviors/registry/ui/learning/atoms/std-learn-estimate.orb +72 -0
  58. package/behaviors/registry/ui/learning/atoms/std-learn-fx-cues.orb +1546 -0
  59. package/behaviors/registry/ui/learning/atoms/std-learn-goal-seek.orb +58 -0
  60. package/behaviors/registry/ui/learning/atoms/std-learn-hotspot.orb +72 -0
  61. package/behaviors/registry/ui/learning/atoms/std-learn-predict.orb +72 -0
  62. package/behaviors/registry/ui/learning/atoms/std-learn-scatter-fit.orb +71 -0
  63. package/behaviors/registry/ui/learning/atoms/std-learn-speed-drill.orb +72 -0
  64. package/behaviors/registry/ui/learning/atoms/std-learn-tour-3d.orb +176 -0
  65. package/canonical-operators.json +14 -2
  66. package/dist/behaviors/exports-reader.js +8655 -5239
  67. package/dist/behaviors/functions/index.d.ts +686 -16
  68. package/dist/behaviors/functions/index.js +8637 -5240
  69. package/dist/behaviors/index.d.ts +1 -1
  70. package/dist/behaviors/index.js +8656 -5240
  71. package/dist/behaviors/query.js +8655 -5239
  72. package/dist/behaviors-embeddings.hash.json +3 -3
  73. package/dist/behaviors-embeddings.json +381 -353
  74. package/dist/behaviors-registry.json +2415 -443
  75. package/dist/index.d.ts +1 -1
  76. package/dist/index.js +8656 -5240
  77. package/dist/knob-embeddings.hash.json +3 -3
  78. package/dist/knob-embeddings.json +1 -1
  79. package/dist/registry/factory-signatures.json +1 -1
  80. package/package.json +2 -2
@@ -0,0 +1,82 @@
1
+ app std-detail-layout "1.0.0"
2
+ "std-detail-layout — detail-page chrome band (back button + navigation-stack breadcrumb trail) above a single contentTrait slot. Use as the contentTrait of a detail page's AppShell binding: the band renders `← Back | Contracts › Contract details` from the orbital-scoped client navigation stack (session history, seeded from declared page-path ancestry on cold loads — the trail is never empty), and the consumer's detail trait renders below it. Back pops the stack; crumb clicks navigate to their level; a list router's `(navigate path payload { crumb: ?row.title })` labels the detail crumb with the real record title. Bind once per detail page — `XDetailLayout = DetailShell.traits.DetailLayout` with `contentTrait: @trait.XDetail`."
3
+ ;; @capabilities "detail page shell, breadcrumb trail, back navigation, record detail chrome, navigation stack, drill-down hierarchy"
4
+ orbital DetailLayoutOrbital {
5
+ entity DetailLayoutData [runtime] {
6
+ id : string!
7
+ }
8
+
9
+ trait DetailLayout -> @rebindable DetailLayoutData [interaction, instance] {
10
+ initial: composing
11
+
12
+ state composing {
13
+ INIT -> composing
14
+ (render-ui main {
15
+ type: stack
16
+ direction: vertical
17
+ gap: lg
18
+ className: "w-full"
19
+ children: [{
20
+ type: stack
21
+ direction: horizontal
22
+ align: center
23
+ gap: sm
24
+ className: "w-full border-b border-border pb-3"
25
+ children: [{
26
+ type: button
27
+ variant: ghost
28
+ size: sm
29
+ icon: arrow-left
30
+ label: @config.backLabel
31
+ action: BACK
32
+ }, {
33
+ type: breadcrumb
34
+ fromNavStack: true
35
+ itemEvent: CRUMB_NAV
36
+ }]
37
+ },
38
+ @config.contentTrait]
39
+ })
40
+ BACK -> composing
41
+ (navigate-back)
42
+ CRUMB_NAV -> composing
43
+ (navigate ?href)
44
+ }
45
+
46
+ emits {
47
+ BACK
48
+ @description "The shell's back affordance was pressed; the layout pops the navigation stack."
49
+ @synonyms "go back, return, previous page, up one level"
50
+ @tier "internal"
51
+ CRUMB_NAV
52
+ @description "A breadcrumb level was clicked; the layout navigates to that level's href."
53
+ @synonyms "breadcrumb click, jump to level, trail navigation"
54
+ @tier "internal"
55
+ }
56
+
57
+ listens {
58
+ CRUMB_NAV {
59
+ label : string
60
+ href : string!
61
+ index : number
62
+ }
63
+ @description "Breadcrumb level picked from the navigation-stack trail."
64
+ @synonyms "breadcrumb click, trail navigation"
65
+ @tier "internal"
66
+ }
67
+
68
+ config {
69
+ contentTrait : trait
70
+ @label "Content trait"
71
+ @description "The detail trait rendered below the navigation band (the DetailPanel + sections view)."
72
+ @tier "presentation"
73
+ backLabel : string = Back
74
+ @label "Back button label"
75
+ @description "Label on the band's back button, e.g. \"Back to contracts\"."
76
+ @synonyms "back text, return label"
77
+ @tier "presentation"
78
+ }
79
+ }
80
+
81
+ page "/detail-layout" as DetailLayoutPage -> DetailLayout
82
+ }
@@ -21,6 +21,8 @@ orbital BreadcrumbOrbital {
21
21
  INIT -> idle
22
22
  (render-ui main {
23
23
  items: @config.items
24
+ fromNavStack: @config.fromNavStack
25
+ itemEvent: @config.itemEvent
24
26
  separator: @config.separator
25
27
  maxItems: @config.maxItems
26
28
  className: @config.className
@@ -28,10 +30,26 @@ orbital BreadcrumbOrbital {
28
30
  })
29
31
  }
30
32
 
33
+ emits {
34
+ @config.itemEvent -> external {
35
+ id : string
36
+ }
37
+ @description "Event emitted when a non-current crumb is clicked, with payload `{ label, href, index }` — the trait handles it with `(navigate ?href)`. Without it, stack crumbs navigate directly."
38
+ @tier "essential"
39
+ }
40
+
31
41
  config {
32
- items : [BreadcrumbItemsItem] = [{ label: "Label", href: "Href", path: "Path", icon: "circle", isCurrent: false }, { label: "Label 2", href: "Href 2", path: "Path 2", icon: "circle", isCurrent: true }]
42
+ items : [BreadcrumbItemsItem] = []
33
43
  @label "Items"
34
- @description "Breadcrumb items"
44
+ @description "Breadcrumb items. Omit together with `fromNavStack` to render the orbital-scoped navigation stack instead of an authored trail."
45
+ @tier "presentation"
46
+ fromNavStack : boolean = false
47
+ @label "From Nav Stack"
48
+ @description "Render the current orbital's navigation stack (NavStackProvider) as the trail: one crumb per visited/declared level, last entry current. The stack is client-session state both execution paths maintain; outside a provider this renders nothing."
49
+ @tier "presentation"
50
+ itemEvent : string = "ITEM"
51
+ @label "Item Event"
52
+ @description "Event emitted when a non-current crumb is clicked, with payload `{ label, href, index }` — the trait handles it with `(navigate ?href)`. Without it, stack crumbs navigate directly."
35
53
  @tier "presentation"
36
54
  separator : icon
37
55
  @label "Separator"
@@ -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"
@@ -79,6 +79,7 @@ orbital DetailPanelOrbital {
79
79
  avatar: @config.avatar
80
80
  sections: @config.sections
81
81
  actions: @config.actions
82
+ maxInlineActions: @config.maxInlineActions
82
83
  backAction: @config.backAction
83
84
  footer: @config.footer
84
85
  slideOver: @config.slideOver
@@ -113,6 +114,7 @@ orbital DetailPanelOrbital {
113
114
  avatar: @config.avatar
114
115
  sections: @config.sections
115
116
  actions: @config.actions
117
+ maxInlineActions: @config.maxInlineActions
116
118
  backAction: @config.backAction
117
119
  footer: @config.footer
118
120
  slideOver: @config.slideOver
@@ -206,6 +208,10 @@ orbital DetailPanelOrbital {
206
208
  @label "Actions"
207
209
  @description "Unified actions array - first action with variant='primary' is the main action"
208
210
  @tier "presentation"
211
+ maxInlineActions : number
212
+ @label "Max Inline Actions"
213
+ @description "Max inline action buttons before the rest collapse into a '⋯' overflow menu (mirrors DataGrid's maxInlineActions). Omit = all inline."
214
+ @tier "presentation"
209
215
  backAction : DetailPanelBackAction
210
216
  @label "Back Action"
211
217
  @description "Navigation-back affordance. Renders top-LEFT before the title (OS/back convention — Almadar_UX §8.4), spatially separated from the right-aligned action buttons + close X. Never inferred from actions[] labels."
@@ -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
+ }