@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.
- package/behaviors/lolo/ui/core/atoms/std-detail-layout.lolo +82 -0
- package/behaviors/lolo/ui/core/molecules/ui-breadcrumb.lolo +20 -2
- package/behaviors/lolo/ui/core/molecules/ui-fx-overlay.lolo +63 -0
- package/behaviors/lolo/ui/core/molecules/ui-page-header.lolo +1 -1
- package/behaviors/lolo/ui/core/organisms/ui-detail-panel.lolo +6 -0
- package/behaviors/lolo/ui/core/organisms/ui-form-section.lolo +2 -2
- package/behaviors/lolo/ui/core/organisms/ui-form.lolo +2 -2
- package/behaviors/lolo/ui/core/templates/ui-dashboard-layout.lolo +2 -2
- package/behaviors/lolo/ui/game/atoms/std-fx-particles.lolo +23 -4
- package/behaviors/lolo/ui/game/atoms/std-platformer-body.lolo +251 -0
- package/behaviors/lolo/ui/game/atoms/ui-draw-fx-layer.lolo +109 -0
- package/behaviors/lolo/ui/game/organisms/std-arcade-board-3d.lolo +32 -2
- package/behaviors/lolo/ui/game/organisms/std-crew-board-2d.lolo +32 -2
- package/behaviors/lolo/ui/game/organisms/std-dungeon-board-2d.lolo +32 -2
- package/behaviors/lolo/ui/game/organisms/std-market-board-3d.lolo +32 -2
- package/behaviors/lolo/ui/game/organisms/std-objective-board-3d.lolo +32 -2
- package/behaviors/lolo/ui/game/organisms/std-platformer-board-2d.lolo +391 -0
- package/behaviors/lolo/ui/game/organisms/std-survival-board-3d.lolo +32 -2
- package/behaviors/lolo/ui/game/organisms/std-tactics-board-2d.lolo +34 -6
- package/behaviors/lolo/ui/game/organisms/std-tactics-board-3d.lolo +34 -4
- package/behaviors/lolo/ui/learning/atoms/std-learn-arrangement.lolo +8 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-classify.lolo +8 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-derivation.lolo +8 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-doser.lolo +9 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-estimate.lolo +8 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-fx-cues.lolo +147 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-goal-seek.lolo +8 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-hotspot.lolo +8 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-predict.lolo +8 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-scatter-fit.lolo +9 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-speed-drill.lolo +8 -0
- package/behaviors/lolo/ui/learning/atoms/std-learn-tour-3d.lolo +9 -0
- package/behaviors/registry/ui/core/atoms/std-detail-layout.orb +265 -0
- package/behaviors/registry/ui/core/molecules/ui-breadcrumb.orb +55 -17
- package/behaviors/registry/ui/core/molecules/ui-fx-overlay.orb +281 -0
- package/behaviors/registry/ui/core/molecules/ui-page-header.orb +1 -1
- package/behaviors/registry/ui/core/organisms/ui-detail-panel.orb +8 -0
- package/behaviors/registry/ui/core/organisms/ui-form-section.orb +6 -0
- package/behaviors/registry/ui/core/organisms/ui-form.orb +6 -0
- package/behaviors/registry/ui/core/templates/ui-dashboard-layout.orb +2 -2
- package/behaviors/registry/ui/game/atoms/std-fx-particles.orb +265 -4
- package/behaviors/registry/ui/game/atoms/std-platformer-body.orb +2136 -0
- package/behaviors/registry/ui/game/atoms/ui-draw-fx-layer.orb +523 -0
- package/behaviors/registry/ui/game/organisms/std-arcade-board-3d.orb +159 -70
- package/behaviors/registry/ui/game/organisms/std-crew-board-2d.orb +148 -58
- package/behaviors/registry/ui/game/organisms/std-dungeon-board-2d.orb +169 -58
- package/behaviors/registry/ui/game/organisms/std-market-board-3d.orb +160 -70
- package/behaviors/registry/ui/game/organisms/std-objective-board-3d.orb +151 -60
- package/behaviors/registry/ui/game/organisms/std-platformer-board-2d.orb +2981 -0
- package/behaviors/registry/ui/game/organisms/std-survival-board-3d.orb +150 -60
- package/behaviors/registry/ui/game/organisms/std-tactics-board-2d.orb +163 -316
- package/behaviors/registry/ui/game/organisms/std-tactics-board-3d.orb +156 -184
- package/behaviors/registry/ui/learning/atoms/std-learn-arrangement.orb +62 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-classify.orb +73 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-derivation.orb +66 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-doser.orb +71 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-estimate.orb +72 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-fx-cues.orb +1546 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-goal-seek.orb +58 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-hotspot.orb +72 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-predict.orb +72 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-scatter-fit.orb +71 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-speed-drill.orb +72 -0
- package/behaviors/registry/ui/learning/atoms/std-learn-tour-3d.orb +176 -0
- package/canonical-operators.json +14 -2
- package/dist/behaviors/exports-reader.js +8655 -5239
- package/dist/behaviors/functions/index.d.ts +686 -16
- package/dist/behaviors/functions/index.js +8637 -5240
- package/dist/behaviors/index.d.ts +1 -1
- package/dist/behaviors/index.js +8656 -5240
- package/dist/behaviors/query.js +8655 -5239
- package/dist/behaviors-embeddings.hash.json +3 -3
- package/dist/behaviors-embeddings.json +381 -353
- package/dist/behaviors-registry.json +2415 -443
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8656 -5240
- package/dist/knob-embeddings.hash.json +3 -3
- package/dist/knob-embeddings.json +1 -1
- package/dist/registry/factory-signatures.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,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] = [
|
|
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.
|
|
2
|
-
"Headless
|
|
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
|
+
}
|