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