@almadar/std 16.154.0 → 16.155.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/game/molecules/std-tactics-board-2d.lolo +35 -21
- package/behaviors/lolo/ui/game/molecules/ui-canvas.lolo +5 -0
- package/behaviors/lolo/ui/game/organisms/ui-battle-board.lolo +122 -74
- package/behaviors/lolo/ui/game/organisms/ui-hex-strategy-board.lolo +153 -9
- package/behaviors/lolo/ui/game/organisms/ui-tactics-demo-2d.lolo +130 -8
- package/behaviors/lolo/ui/game/organisms/ui-tanks-board.lolo +147 -7
- package/behaviors/lolo/ui/game/organisms/ui-topdown-rts-board.lolo +291 -19
- package/behaviors/registry/factory-signatures.json +1893 -2294
- package/behaviors/registry/ui/game/molecules/std-tactics-board-2d.orb +798 -189
- package/behaviors/registry/ui/game/molecules/ui-canvas.orb +7 -0
- package/behaviors/registry/ui/game/organisms/ui-battle-board.orb +17 -1
- package/behaviors/registry/ui/game/organisms/ui-hex-strategy-board.orb +68 -1180
- package/behaviors/registry/ui/game/organisms/ui-tactics-demo-2d.orb +192 -322
- package/behaviors/registry/ui/game/organisms/ui-tanks-board.orb +97 -85
- package/behaviors/registry/ui/game/organisms/ui-topdown-rts-board.orb +1477 -259
- package/dist/behaviors/behaviors-embeddings.json +244 -236
- package/dist/behaviors/behaviors-registry.json +143 -772
- package/dist/behaviors/exports-reader.js +1099 -2090
- package/dist/behaviors/functions/index.d.ts +33 -26
- package/dist/behaviors/functions/index.js +1099 -2091
- package/dist/behaviors/index.d.ts +1 -1
- package/dist/behaviors/index.js +1100 -2091
- package/dist/behaviors/knob-embeddings.json +1 -1
- package/dist/behaviors/query.js +1099 -2090
- package/dist/behaviors/registry/factory-signatures.json +1893 -2294
- package/dist/behaviors/registry/ui/game/molecules/std-tactics-board-2d.orb +798 -189
- package/dist/behaviors/registry/ui/game/molecules/ui-canvas.orb +7 -0
- package/dist/behaviors/registry/ui/game/organisms/ui-battle-board.orb +17 -1
- package/dist/behaviors/registry/ui/game/organisms/ui-hex-strategy-board.orb +68 -1180
- package/dist/behaviors/registry/ui/game/organisms/ui-tactics-demo-2d.orb +192 -322
- package/dist/behaviors/registry/ui/game/organisms/ui-tanks-board.orb +97 -85
- package/dist/behaviors/registry/ui/game/organisms/ui-topdown-rts-board.orb +1477 -259
- package/dist/behaviors-embeddings.json +244 -236
- package/dist/behaviors-registry.json +143 -772
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1100 -2091
- package/dist/knob-embeddings.json +1 -1
- package/dist/registry/factory-signatures.json +1893 -2294
- package/package.json +2 -2
|
@@ -71,6 +71,7 @@ orbital TacticsBoard2DOrbital {
|
|
|
71
71
|
type HudStat = {
|
|
72
72
|
label : string
|
|
73
73
|
value : number
|
|
74
|
+
format : string
|
|
74
75
|
}
|
|
75
76
|
type HudButton = {
|
|
76
77
|
type : string
|
|
@@ -97,6 +98,8 @@ orbital TacticsBoard2DOrbital {
|
|
|
97
98
|
selectedUnitId : string = ""
|
|
98
99
|
validMoves : [ValidMove] = []
|
|
99
100
|
attackTargets : [ValidMove] = []
|
|
101
|
+
movedIds : [string] = []
|
|
102
|
+
actedIds : [string] = []
|
|
100
103
|
uiPhase : string = "observation"
|
|
101
104
|
turn : number = 0
|
|
102
105
|
score : number = 0
|
|
@@ -159,27 +162,28 @@ orbital TacticsBoard2DOrbital {
|
|
|
159
162
|
type: game-shell
|
|
160
163
|
backgroundAsset: @config.backgroundAsset
|
|
161
164
|
hudBackgroundAsset: @config.hudBackgroundAsset
|
|
162
|
-
hud: { type: game-hud, stats: (array/concat [ { label: "Result", value: @entity.result }, { label: "Phase", value: @entity.uiPhase }, { label: "Turn", value: @entity.turn }, { label: "Score", value: @entity.score } ] @config.extraStats) }
|
|
165
|
+
hud: { type: game-hud, stats: (array/concat [ { label: "Result", value: @entity.result, format: "text" }, { label: "Phase", value: @entity.uiPhase, format: "text" }, { label: "Turn", value: @entity.turn }, { label: "Score", value: @entity.score } ] @config.extraStats) }
|
|
163
166
|
addons: { type: stack, direction: vertical, gap: sm, children: (array/concat [ { type: button, action: END_TURN, label: "End Turn", variant: primary }, { type: button, action: CANCEL, label: "Cancel", variant: secondary } ] @config.extraAddons) }
|
|
164
167
|
children: [ { type: canvas, fit: true, camera: { pos: { x: (/ (+ (array/min @entity.tiles "x") (array/max @entity.tiles "x")) 2), y: (/ (+ (array/min @entity.tiles "y") (array/max @entity.tiles "y")) 2) } }, projection: @config.projection, showMinimap: @config.showMinimap, tileClickEvent: TILE_CLICK, unitClickEvent: UNIT_CLICK, children: [
|
|
165
168
|
{ type: draw-shape-layer, items: (array/map @entity.tiles (fn t { type: draw-shape, shape: "cell", position: { x: (object/get @t x), y: (object/get @t y) }, fill: "#5c94fc", stroke: "rgba(255,255,255,0.35)", strokeWidth: 1 })) }
|
|
166
169
|
{ type: draw-sprite-layer, items: (array/filter (array/map @entity.tiles (fn t { type: draw-sprite, position: { x: (object/get @t x), y: (object/get @t y) }, asset: (if (object/get (object/get @t terrainSprite) url) (object/get @t terrainSprite) (object/get (object/get @config.assetManifest terrains) (object/get @t terrain))), anchor: "center", width: 1, height: 1 })) (fn s (object/get (object/get @s asset) url))) }
|
|
167
|
-
{ type: draw-sprite-layer, items: (array/filter (array/map @config.features (fn f { type: draw-sprite, position: { x: (object/get @f x), y: (object/get @f y) }, asset: (object/get @f assetUrl) })) (fn s (object/get (object/get @s asset) url))) }
|
|
170
|
+
{ type: draw-sprite-layer, items: (array/filter (array/map @config.features (fn f { type: draw-sprite, position: { x: (object/get @f x), y: (object/get @f y) }, asset: (object/get @f assetUrl), anchor: "ground", width: 1, height: 1 })) (fn s (object/get (object/get @s asset) url))) }
|
|
168
171
|
{ type: draw-shape-layer, items: (array/map @entity.validMoves (fn m { type: draw-shape, shape: "cell", position: { x: (object/get @m x), y: (object/get @m z) }, fill: "rgba(74,222,128,0.4)" })) }
|
|
169
172
|
{ type: draw-shape-layer, items: (array/map @entity.attackTargets (fn a { type: draw-shape, shape: "cell", position: { x: (object/get @a x), y: (object/get @a z) }, fill: "rgba(239,68,68,0.4)" })) }
|
|
170
173
|
{ type: draw-shape-layer, items: (array/map (array/filter @entity.units (fn u (== (object/get @u id) @entity.selectedUnitId))) (fn u { type: draw-shape, shape: "ellipse", position: (object/get @u position), radiusX: 0.5, radiusY: 0.2, stroke: "rgba(0,200,255,0.8)", strokeWidth: 3 })) }
|
|
171
|
-
{ type: draw-sprite-layer, items: (array/filter (array/map @entity.units (fn u (let ((unitAsset (if (object/get (object/get @u modelUrl) url) (object/get @u modelUrl) (object/get (object/get @config.assetManifest units) (object/get @u unitType)))) (unitUrl (object/get @unitAsset url)) (unitAnim (if (<= (object/get @u health) 0) "death" (if (and (== (object/get @u id) @entity.animAttackerId) (> @entity.animAtkTicksLeft 0)) "attack" (if (== (object/get @u id) @entity.animUnitId) @entity.animState "idle"))))) { type: draw-sprite, position: (object/get @u position), asset: (if (and @unitUrl (str/includes @unitUrl ".svg")) (object/merge @unitAsset { url: (str/concat (array/nth (str/split @unitUrl ".svg") 0) (str/concat "." (str/concat @unitAnim (str/concat "-" (str/concat (if (== @unitAnim "walk") (% @entity.animCounter 2) (if (== @unitAnim "idle") (% (math/floor (/ @entity.animCounter 3)) 4) 0))) ".svg")))) }) @unitAsset), id: (object/get @u id), anchor: "ground", width: 0.5, height: 0.5 }))) (fn s (object/get (object/get @s asset) url))) }
|
|
172
|
-
{ type: draw-shape-layer, items: (array/map @entity.units (fn u { type: draw-shape, shape: "rect", position: (object/get @u position), anchor: "ground", offsetX: -0.25, offsetY: -0.6, width: 0.5, height: 0.12, fill: "rgba(0,0,0,0.6)" })) }
|
|
173
|
-
{ type: draw-shape-layer, items: (array/map @entity.units (fn u { type: draw-shape, shape: "rect", position: (object/get @u position), anchor: "ground", offsetX: -0.25, offsetY: -0.6, width: (* 0.5 (/ (object/get @u health) (object/get @u maxHealth))), height: 0.12, fill: "#22c55e" })) }
|
|
174
|
-
{ type: draw-text-layer, items: (array/map @entity.units (fn u { type: draw-text, text: (object/get @u name), position: (object/get @u position), offsetY: -0.85, color: "#ffffff" })) }
|
|
174
|
+
{ type: draw-sprite-layer, items: (array/filter (array/map @entity.units (fn u (let ((unitAsset (if (object/get (object/get @u modelUrl) url) (object/get @u modelUrl) (object/get (object/get @config.assetManifest units) (object/get @u unitType)))) (unitUrl (object/get @unitAsset url)) (unitAnim (if (<= (object/get @u health) 0) "death" (if (and (== (object/get @u id) @entity.animAttackerId) (> @entity.animAtkTicksLeft 0)) "attack" (if (== (object/get @u id) @entity.animUnitId) @entity.animState "idle"))))) { type: draw-sprite, position: (object/get @u position), asset: (if (and @unitUrl (str/includes @unitUrl ".svg")) (object/merge @unitAsset { url: (str/concat (array/nth (str/split @unitUrl ".svg") 0) (str/concat "." (str/concat @unitAnim (str/concat "-" (str/concat (if (== @unitAnim "walk") (% @entity.animCounter 2) (if (== @unitAnim "idle") (% (math/floor (/ @entity.animCounter 3)) 4) 0))) ".svg")))) }) @unitAsset), id: (object/get @u id), anchor: "ground", width: 0.5, height: 0.5, opacity: (if (> (array/len (array/filter @entity.actedIds (fn aid (== @aid (object/get @u id))))) 0) 0.45 1) }))) (fn s (object/get (object/get @s asset) url))) }
|
|
175
|
+
{ type: draw-shape-layer, items: (array/map (array/filter @entity.units (fn u (> (object/get @u health) 0))) (fn u { type: draw-shape, shape: "rect", position: (object/get @u position), anchor: "ground", offsetX: -0.25, offsetY: -0.6, width: 0.5, height: 0.12, fill: "rgba(0,0,0,0.6)" })) }
|
|
176
|
+
{ type: draw-shape-layer, items: (array/map (array/filter @entity.units (fn u (> (object/get @u health) 0))) (fn u { type: draw-shape, shape: "rect", position: (object/get @u position), anchor: "ground", offsetX: -0.25, offsetY: -0.6, width: (* 0.5 (/ (object/get @u health) (object/get @u maxHealth))), height: 0.12, fill: "#22c55e" })) }
|
|
177
|
+
{ type: draw-text-layer, items: (array/map (array/filter @entity.units (fn u (> (object/get @u health) 0))) (fn u { type: draw-text, text: (object/get @u name), position: (object/get @u position), offsetY: -0.85, color: "#ffffff" })) }
|
|
175
178
|
{ type: draw-text-layer, items: (array/map @entity.fx (fn f { type: draw-text, text: (object/get @f message), position: { x: (object/get @f x), y: (object/get @f z) }, color: "#ffe066" })) }
|
|
176
179
|
{ type: draw-sprite-layer, items: (array/filter (array/map @entity.fx (fn f { type: draw-sprite, position: { x: (object/get @f x), y: (object/get @f z) }, asset: (object/get (object/get @config.assetManifest effects) (object/get @f type)), anchor: "center", width: 0.6, height: 0.6 })) (fn s (object/get (object/get @s asset) url))) }
|
|
177
180
|
{ type: draw-sprite-layer, items: (array/map (array/filter (array/flatten (array/map @entity.units (fn u (array/flatten (array/map [ ["idle", 4], ["walk", 2], ["attack", 1], ["hit", 1], ["death", 1] ] (fn st (array/map (array/range 0 (array/nth @st 1)) (fn i (if (and (object/get (object/get @u modelUrl) url) (str/includes (object/get (object/get @u modelUrl) url) ".svg")) (str/concat (array/nth (str/split (object/get (object/get @u modelUrl) url) ".svg") 0) (str/concat "." (str/concat (array/nth @st 0) (str/concat "-" (str/concat @i ".svg"))))) ""))))))))) (fn w (!= @w ""))) (fn warmUrl { type: draw-sprite, position: { x: 0, y: 0 }, asset: { url: @warmUrl }, opacity: 0, width: 0.5, height: 0.5 })) }
|
|
178
181
|
] } ]
|
|
179
182
|
})
|
|
180
183
|
|
|
181
|
-
UNIT_CLICK -> playing when (and (== @entity.
|
|
184
|
+
UNIT_CLICK -> playing when (and (== @entity.result "none") (and (!= @entity.selectedUnitId "") (and (== (array/len (array/filter @entity.actedIds (fn aid (== @aid @entity.selectedUnitId)))) 0) (and (== (object/get (array/find @entity.units (fn u (== @u.id ?unitId))) team) "enemy") (and (> (object/get (array/find @entity.units (fn u (== @u.id ?unitId))) health) 0) (<= (grid/manhattan-distance (object/get (array/find @entity.units (fn u (== @u.id @entity.selectedUnitId))) position) (object/get (array/find @entity.units (fn u (== @u.id ?unitId))) position)) @config.attackRange))))))
|
|
182
185
|
(emit ATTACK { targetId: ?unitId })
|
|
186
|
+
(set @entity.actedIds (array/append @entity.actedIds @entity.selectedUnitId))
|
|
183
187
|
(set @entity.animAttackerId @entity.selectedUnitId)
|
|
184
188
|
(set @entity.animAtkTicksLeft 4)
|
|
185
189
|
(set @entity.selectedUnitId "")
|
|
@@ -187,23 +191,23 @@ orbital TacticsBoard2DOrbital {
|
|
|
187
191
|
(set @entity.attackTargets [])
|
|
188
192
|
(set @entity.uiPhase "observation")
|
|
189
193
|
|
|
190
|
-
UNIT_CLICK -> playing when (== @entity.selectedUnitId ?unitId)
|
|
194
|
+
UNIT_CLICK -> playing when (and (== @entity.result "none") (== @entity.selectedUnitId ?unitId))
|
|
191
195
|
(set @entity.uiPhase "action")
|
|
192
196
|
(set @entity.validMoves [])
|
|
193
|
-
(set @entity.attackTargets (array/map (array/filter @entity.units (fn u (and (== @u.team "enemy") (and (> @u.health 0) (<= (grid/manhattan-distance (object/get (array/find @entity.units (fn s (== @s.id @entity.selectedUnitId))) position) (object/get @u position))
|
|
197
|
+
(set @entity.attackTargets (array/map (array/filter @entity.units (fn u (and (== @u.team "enemy") (and (> @u.health 0) (<= (grid/manhattan-distance (object/get (array/find @entity.units (fn s (== @s.id @entity.selectedUnitId))) position) (object/get @u position)) @config.attackRange))))) (fn u { x: (object/get (object/get @u position) x), z: (object/get (object/get @u position) y) })))
|
|
194
198
|
|
|
195
|
-
UNIT_CLICK -> playing when (== (object/get (array/find @entity.units (fn u (== @u.id ?unitId))) team) "player")
|
|
199
|
+
UNIT_CLICK -> playing when (and (== @entity.result "none") (and (> (object/get (array/find @entity.units (fn u (== @u.id ?unitId))) health) 0) (and (== (object/get (array/find @entity.units (fn u (== @u.id ?unitId))) team) "player") (== (array/len (array/filter @entity.actedIds (fn aid (== @aid ?unitId)))) 0))))
|
|
196
200
|
(set @entity.selectedUnitId ?unitId)
|
|
197
201
|
(set @entity.uiPhase "selection")
|
|
198
202
|
(set @entity.attackTargets [])
|
|
199
|
-
(set @entity.validMoves (array/map (array/filter @entity.tiles (fn t (and (== (object/get @t passable) true) (and (<= (grid/manhattan-distance (object/get (array/find @entity.units (fn u (== @u.id ?unitId))) position) { x: (object/get @t x), y: (object/get @t y) }) @config.movementRange) (== (array/len (array/filter @entity.units (fn u (and (== (object/get (object/get @u position) x) (object/get @t x)) (== (object/get (object/get @u position) y) (object/get @t y)))))) 0))))) (fn t { x: (object/get @t x), z: (object/get @t y) })))
|
|
203
|
+
(set @entity.validMoves (if (> (array/len (array/filter @entity.movedIds (fn aid (== @aid ?unitId)))) 0) [] (array/map (array/filter @entity.tiles (fn t (and (== (object/get @t passable) true) (and (<= (grid/manhattan-distance (object/get (array/find @entity.units (fn u (== @u.id ?unitId))) position) { x: (object/get @t x), y: (object/get @t y) }) @config.movementRange) (== (array/len (array/filter @entity.units (fn u (and (> (object/get @u health) 0) (and (== (object/get (object/get @u position) x) (object/get @t x)) (== (object/get (object/get @u position) y) (object/get @t y))))))) 0))))) (fn t { x: (object/get @t x), z: (object/get @t y) }))))
|
|
200
204
|
|
|
201
|
-
TILE_CLICK -> playing when (and (!= @entity.selectedUnitId "") (and (grid/in-bounds { x: ?x, y: ?y } @config.gridWidth @config.gridHeight) (<= (grid/manhattan-distance (object/get (array/find @entity.units (fn u (== @u.id @entity.selectedUnitId))) position) { x: ?x, y: ?y }) @config.movementRange)))
|
|
205
|
+
TILE_CLICK -> playing when (and (== @entity.result "none") (and (!= @entity.selectedUnitId "") (and (== (array/len (array/filter @entity.movedIds (fn aid (== @aid @entity.selectedUnitId)))) 0) (and (> (object/get (array/find @entity.units (fn u (== @u.id @entity.selectedUnitId))) health) 0) (and (grid/in-bounds { x: ?x, y: ?y } @config.gridWidth @config.gridHeight) (and (<= (grid/manhattan-distance (object/get (array/find @entity.units (fn u (== @u.id @entity.selectedUnitId))) position) { x: ?x, y: ?y }) @config.movementRange) (and (== (array/len (array/filter @entity.units (fn u (and (> (object/get @u health) 0) (and (== (object/get (object/get @u position) x) ?x) (== (object/get (object/get @u position) y) ?y)))))) 0) (== (object/get (array/find @entity.tiles (fn t (and (== (object/get @t x) ?x) (== (object/get @t y) ?y)))) passable) true))))))))
|
|
202
206
|
(emit MOVE { unitId: @entity.selectedUnitId, x: ?x, y: ?y })
|
|
203
|
-
(set @entity.selectedUnitId
|
|
207
|
+
(set @entity.movedIds (array/append @entity.movedIds @entity.selectedUnitId))
|
|
208
|
+
(set @entity.uiPhase "action")
|
|
204
209
|
(set @entity.validMoves [])
|
|
205
|
-
(set @entity.attackTargets
|
|
206
|
-
(set @entity.uiPhase "observation")
|
|
210
|
+
(set @entity.attackTargets (array/map (array/filter @entity.units (fn u (and (== @u.team "enemy") (and (> @u.health 0) (<= (grid/manhattan-distance { x: ?x, y: ?y } (object/get @u position)) @config.attackRange))))) (fn u { x: (object/get (object/get @u position) x), z: (object/get (object/get @u position) y) })))
|
|
207
211
|
|
|
208
212
|
CANCEL -> playing when (!= @entity.selectedUnitId "")
|
|
209
213
|
(set @entity.selectedUnitId "")
|
|
@@ -213,6 +217,12 @@ orbital TacticsBoard2DOrbital {
|
|
|
213
217
|
|
|
214
218
|
END_TURN -> playing when (== @entity.result "none")
|
|
215
219
|
(set @entity.turn (+ @entity.turn 1))
|
|
220
|
+
(set @entity.movedIds [])
|
|
221
|
+
(set @entity.actedIds [])
|
|
222
|
+
(set @entity.selectedUnitId "")
|
|
223
|
+
(set @entity.validMoves [])
|
|
224
|
+
(set @entity.attackTargets [])
|
|
225
|
+
(set @entity.uiPhase "observation")
|
|
216
226
|
(emit AI_TURN { turn: @entity.turn })
|
|
217
227
|
|
|
218
228
|
MOVE -> playing
|
|
@@ -237,19 +247,19 @@ orbital TacticsBoard2DOrbital {
|
|
|
237
247
|
type: game-shell
|
|
238
248
|
backgroundAsset: @config.backgroundAsset
|
|
239
249
|
hudBackgroundAsset: @config.hudBackgroundAsset
|
|
240
|
-
hud: { type: game-hud, stats: (array/concat [ { label: "Result", value: @entity.result }, { label: "Phase", value: @entity.uiPhase }, { label: "Turn", value: @entity.turn }, { label: "Score", value: @entity.score } ] @config.extraStats) }
|
|
250
|
+
hud: { type: game-hud, stats: (array/concat [ { label: "Result", value: @entity.result, format: "text" }, { label: "Phase", value: @entity.uiPhase, format: "text" }, { label: "Turn", value: @entity.turn }, { label: "Score", value: @entity.score } ] @config.extraStats) }
|
|
241
251
|
addons: { type: stack, direction: vertical, gap: sm, children: (array/concat [ { type: button, action: END_TURN, label: "End Turn", variant: primary }, { type: button, action: CANCEL, label: "Cancel", variant: secondary } ] @config.extraAddons) }
|
|
242
252
|
children: [ { type: canvas, fit: true, camera: { pos: { x: (/ (+ (array/min @entity.tiles "x") (array/max @entity.tiles "x")) 2), y: (/ (+ (array/min @entity.tiles "y") (array/max @entity.tiles "y")) 2) } }, projection: @config.projection, showMinimap: @config.showMinimap, tileClickEvent: TILE_CLICK, unitClickEvent: UNIT_CLICK, children: [
|
|
243
253
|
{ type: draw-shape-layer, items: (array/map @entity.tiles (fn t { type: draw-shape, shape: "cell", position: { x: (object/get @t x), y: (object/get @t y) }, fill: "#5c94fc", stroke: "rgba(255,255,255,0.35)", strokeWidth: 1 })) }
|
|
244
254
|
{ type: draw-sprite-layer, items: (array/filter (array/map @entity.tiles (fn t { type: draw-sprite, position: { x: (object/get @t x), y: (object/get @t y) }, asset: (if (object/get (object/get @t terrainSprite) url) (object/get @t terrainSprite) (object/get (object/get @config.assetManifest terrains) (object/get @t terrain))), anchor: "center", width: 1, height: 1 })) (fn s (object/get (object/get @s asset) url))) }
|
|
245
|
-
{ type: draw-sprite-layer, items: (array/filter (array/map @config.features (fn f { type: draw-sprite, position: { x: (object/get @f x), y: (object/get @f y) }, asset: (object/get @f assetUrl) })) (fn s (object/get (object/get @s asset) url))) }
|
|
255
|
+
{ type: draw-sprite-layer, items: (array/filter (array/map @config.features (fn f { type: draw-sprite, position: { x: (object/get @f x), y: (object/get @f y) }, asset: (object/get @f assetUrl), anchor: "ground", width: 1, height: 1 })) (fn s (object/get (object/get @s asset) url))) }
|
|
246
256
|
{ type: draw-shape-layer, items: (array/map @entity.validMoves (fn m { type: draw-shape, shape: "cell", position: { x: (object/get @m x), y: (object/get @m z) }, fill: "rgba(74,222,128,0.4)" })) }
|
|
247
257
|
{ type: draw-shape-layer, items: (array/map @entity.attackTargets (fn a { type: draw-shape, shape: "cell", position: { x: (object/get @a x), y: (object/get @a z) }, fill: "rgba(239,68,68,0.4)" })) }
|
|
248
258
|
{ type: draw-shape-layer, items: (array/map (array/filter @entity.units (fn u (== (object/get @u id) @entity.selectedUnitId))) (fn u { type: draw-shape, shape: "ellipse", position: (object/get @u position), radiusX: 0.5, radiusY: 0.2, stroke: "rgba(0,200,255,0.8)", strokeWidth: 3 })) }
|
|
249
|
-
{ type: draw-sprite-layer, items: (array/filter (array/map @entity.units (fn u (let ((unitAsset (if (object/get (object/get @u modelUrl) url) (object/get @u modelUrl) (object/get (object/get @config.assetManifest units) (object/get @u unitType)))) (unitUrl (object/get @unitAsset url)) (unitAnim (if (<= (object/get @u health) 0) "death" (if (and (== (object/get @u id) @entity.animAttackerId) (> @entity.animAtkTicksLeft 0)) "attack" (if (== (object/get @u id) @entity.animUnitId) @entity.animState "idle"))))) { type: draw-sprite, position: (object/get @u position), asset: (if (and @unitUrl (str/includes @unitUrl ".svg")) (object/merge @unitAsset { url: (str/concat (array/nth (str/split @unitUrl ".svg") 0) (str/concat "." (str/concat @unitAnim (str/concat "-" (str/concat (if (== @unitAnim "walk") (% @entity.animCounter 2) (if (== @unitAnim "idle") (% (math/floor (/ @entity.animCounter 3)) 4) 0))) ".svg")))) }) @unitAsset), id: (object/get @u id), anchor: "ground", width: 0.5, height: 0.5 }))) (fn s (object/get (object/get @s asset) url))) }
|
|
250
|
-
{ type: draw-shape-layer, items: (array/map @entity.units (fn u { type: draw-shape, shape: "rect", position: (object/get @u position), anchor: "ground", offsetX: -0.25, offsetY: -0.6, width: 0.5, height: 0.12, fill: "rgba(0,0,0,0.6)" })) }
|
|
251
|
-
{ type: draw-shape-layer, items: (array/map @entity.units (fn u { type: draw-shape, shape: "rect", position: (object/get @u position), anchor: "ground", offsetX: -0.25, offsetY: -0.6, width: (* 0.5 (/ (object/get @u health) (object/get @u maxHealth))), height: 0.12, fill: "#22c55e" })) }
|
|
252
|
-
{ type: draw-text-layer, items: (array/map @entity.units (fn u { type: draw-text, text: (object/get @u name), position: (object/get @u position), offsetY: -0.85, color: "#ffffff" })) }
|
|
259
|
+
{ type: draw-sprite-layer, items: (array/filter (array/map @entity.units (fn u (let ((unitAsset (if (object/get (object/get @u modelUrl) url) (object/get @u modelUrl) (object/get (object/get @config.assetManifest units) (object/get @u unitType)))) (unitUrl (object/get @unitAsset url)) (unitAnim (if (<= (object/get @u health) 0) "death" (if (and (== (object/get @u id) @entity.animAttackerId) (> @entity.animAtkTicksLeft 0)) "attack" (if (== (object/get @u id) @entity.animUnitId) @entity.animState "idle"))))) { type: draw-sprite, position: (object/get @u position), asset: (if (and @unitUrl (str/includes @unitUrl ".svg")) (object/merge @unitAsset { url: (str/concat (array/nth (str/split @unitUrl ".svg") 0) (str/concat "." (str/concat @unitAnim (str/concat "-" (str/concat (if (== @unitAnim "walk") (% @entity.animCounter 2) (if (== @unitAnim "idle") (% (math/floor (/ @entity.animCounter 3)) 4) 0))) ".svg")))) }) @unitAsset), id: (object/get @u id), anchor: "ground", width: 0.5, height: 0.5, opacity: (if (> (array/len (array/filter @entity.actedIds (fn aid (== @aid (object/get @u id))))) 0) 0.45 1) }))) (fn s (object/get (object/get @s asset) url))) }
|
|
260
|
+
{ type: draw-shape-layer, items: (array/map (array/filter @entity.units (fn u (> (object/get @u health) 0))) (fn u { type: draw-shape, shape: "rect", position: (object/get @u position), anchor: "ground", offsetX: -0.25, offsetY: -0.6, width: 0.5, height: 0.12, fill: "rgba(0,0,0,0.6)" })) }
|
|
261
|
+
{ type: draw-shape-layer, items: (array/map (array/filter @entity.units (fn u (> (object/get @u health) 0))) (fn u { type: draw-shape, shape: "rect", position: (object/get @u position), anchor: "ground", offsetX: -0.25, offsetY: -0.6, width: (* 0.5 (/ (object/get @u health) (object/get @u maxHealth))), height: 0.12, fill: "#22c55e" })) }
|
|
262
|
+
{ type: draw-text-layer, items: (array/map (array/filter @entity.units (fn u (> (object/get @u health) 0))) (fn u { type: draw-text, text: (object/get @u name), position: (object/get @u position), offsetY: -0.85, color: "#ffffff" })) }
|
|
253
263
|
{ type: draw-text-layer, items: (array/map @entity.fx (fn f { type: draw-text, text: (object/get @f message), position: { x: (object/get @f x), y: (object/get @f z) }, color: "#ffe066" })) }
|
|
254
264
|
{ type: draw-sprite-layer, items: (array/filter (array/map @entity.fx (fn f { type: draw-sprite, position: { x: (object/get @f x), y: (object/get @f z) }, asset: (object/get (object/get @config.assetManifest effects) (object/get @f type)), anchor: "center", width: 0.6, height: 0.6 })) (fn s (object/get (object/get @s asset) url))) }
|
|
255
265
|
] } ]
|
|
@@ -310,6 +320,10 @@ orbital TacticsBoard2DOrbital {
|
|
|
310
320
|
@label "Movement Range"
|
|
311
321
|
@description "Max manhattan distance a selected unit may move."
|
|
312
322
|
@tier "domain"
|
|
323
|
+
attackRange : number = 1
|
|
324
|
+
@label "Attack Range"
|
|
325
|
+
@description "Max manhattan distance a selected unit may strike an enemy."
|
|
326
|
+
@tier "domain"
|
|
313
327
|
features : [Feature] = []
|
|
314
328
|
@label "Features"
|
|
315
329
|
@description "Decorative environment features drawn on the board."
|
|
@@ -13,6 +13,7 @@ orbital CanvasOrbital {
|
|
|
13
13
|
(render-ui main {
|
|
14
14
|
mode: @config.mode
|
|
15
15
|
drawables: @config.drawables
|
|
16
|
+
children: @config.children
|
|
16
17
|
camera: @config.camera
|
|
17
18
|
projection: @config.projection
|
|
18
19
|
className: @config.className
|
|
@@ -82,6 +83,10 @@ orbital CanvasOrbital {
|
|
|
82
83
|
@label "Drawables"
|
|
83
84
|
@description "Neutral drawable descriptors — the `draw-*` children. Routed to the host's painter."
|
|
84
85
|
@tier "presentation"
|
|
86
|
+
children : node
|
|
87
|
+
@label "Children (drawable composition)"
|
|
88
|
+
@description "Declarative JSX drawable children — semantic atom invocations (`<SvgPrimitive/>`, `<SvgShading/>`, …) composed in paint order. The canvas is a draw-host: these children are routed to the painter as drawable descriptors, not DOM-wrapped. Use this to author a high-fidelity asset as a readable, nested JSX tree an LLM can rasterize."
|
|
89
|
+
@tier "presentation"
|
|
85
90
|
camera : json = {}
|
|
86
91
|
@label "Camera"
|
|
87
92
|
@description "Neutral camera pose. Mapped to the underlying host's framing + zoom."
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
app ui-battle-board "1.0.0"
|
|
2
|
-
"RPG-tactics battle: thin organism composing the std-tactics-board-2d frame + std-score + std-xp (kills grant xp; level-ups heal the party via a HEAL intent). Isometric
|
|
2
|
+
"RPG-tactics battle: thin organism composing the std-tactics-board-2d frame + std-score + std-xp (kills grant xp; level-ups heal the party via a HEAL intent). Isometric hand-authored svg theme supplied via config."
|
|
3
3
|
;; @capabilities "turn-based combat, RPG gameplay, strategy gameplay"
|
|
4
4
|
|
|
5
5
|
;; ============================================================================
|
|
@@ -7,14 +7,27 @@ app ui-battle-board "1.0.0"
|
|
|
7
7
|
;;
|
|
8
8
|
;; Read these before changing anything here, and walk them end to end after.
|
|
9
9
|
;; One persona: the PLAYER, commanding a four-unit party on a 16x16 isometric
|
|
10
|
-
;; grid against four enemies. Status
|
|
10
|
+
;; grid against four enemies. Status re-verified 2026-07-30 by a scripted
|
|
11
|
+
;; full playthrough (32/32 checks) driving the real game on the
|
|
12
|
+
;; runtime-verify catalog through window.__orbitalVerification — events in,
|
|
13
|
+
;; canvas drawables + HUD DOM + bus event log out. No claim below is
|
|
14
|
+
;; untested hearsay.
|
|
11
15
|
;;
|
|
12
|
-
;;
|
|
13
|
-
;;
|
|
14
|
-
;;
|
|
15
|
-
;;
|
|
16
|
-
;;
|
|
17
|
-
;;
|
|
16
|
+
;; THE GENRE (what an RPG-tactics player expects): you command a small party,
|
|
17
|
+
;; not an army. Positioning is the game — you read the grid, move each unit
|
|
18
|
+
;; inside its reach, and commit to strikes when an enemy steps adjacent.
|
|
19
|
+
;; Blows trade back and forth: every End Turn hands the board to the enemy
|
|
20
|
+
;; AI, which closes the distance and punishes exposed units. Kills are the
|
|
21
|
+
;; economy: each one scores, banks xp, and xp thresholds level the party up
|
|
22
|
+
;; and patch their wounds. You win by wiping the enemy; you lose by being
|
|
23
|
+
;; wiped. A round looks like: select a unit -> green reach cells -> move ->
|
|
24
|
+
;; the unit STAYS selected and auto-arms (red cells = enemies in reach) ->
|
|
25
|
+
;; strike for 3 -> the unit is SPENT (dims, cannot act again this turn) ->
|
|
26
|
+
;; activate the next unit -> End Turn -> every living enemy strikes an
|
|
27
|
+
;; adjacent player for 1 or steps 1 cell toward the nearest player -> repeat.
|
|
28
|
+
;; Each unit may move ONCE and attack ONCE per turn (movedIds/actedIds track
|
|
29
|
+
;; the activation; End Turn resets both). Kills score 10 and grant 25 xp;
|
|
30
|
+
;; every 100 xp (+50 per level) levels up and heals the whole party +2.
|
|
18
31
|
;; ============================================================================
|
|
19
32
|
;;
|
|
20
33
|
;; PLAYER — "I command my party and wipe the enemy before they wipe me."
|
|
@@ -22,82 +35,115 @@ app ui-battle-board "1.0.0"
|
|
|
22
35
|
;; 1. I open the board and see the whole battlefield framed on screen —
|
|
23
36
|
;; terrain under every cell, my party at the south, the enemy at the
|
|
24
37
|
;; north, and the HUD showing Result/Phase/Turn/Score/XP/Lvl.
|
|
25
|
-
;; WORKS.
|
|
26
|
-
;;
|
|
27
|
-
;;
|
|
28
|
-
;;
|
|
29
|
-
;;
|
|
30
|
-
;;
|
|
38
|
+
;; WORKS. Boot paints 256 full-cell isometric grass cubes (no gaps),
|
|
39
|
+
;; 8 units (4v4), portal + gold-mine features, minimap on; HUD reads
|
|
40
|
+
;; "none / observation / 0 / 0 / 0 / 1". Verified: 39/39 assets
|
|
41
|
+
;; loaded, zero failed. (Result/Phase used to render "0" — StatBadge
|
|
42
|
+
;; defaults format "number" and parseInt-ed the strings; the frame's
|
|
43
|
+
;; HudStat now carries format: "text" for both.)
|
|
31
44
|
;; 2. Every unit on the board is a visible character sprite with a name
|
|
32
45
|
;; label and a health bar I can read at a glance.
|
|
33
|
-
;; WORKS.
|
|
34
|
-
;; "ground"
|
|
35
|
-
;; health fraction above
|
|
36
|
-
;;
|
|
37
|
-
;;
|
|
46
|
+
;; WORKS. Each unit paints its svg idle frame (4-frame bob, cycled by
|
|
47
|
+
;; the AnimClock tick) anchored "ground", with the black backing +
|
|
48
|
+
;; green health fraction above and the name label. All 8 bars read
|
|
49
|
+
;; full at boot. Dead units drop the bar and label (they are corpses,
|
|
50
|
+
;; not combatants) and switch to the death frame.
|
|
38
51
|
;; 3. I click one of MY units and the cells it can reach light up.
|
|
39
|
-
;; WORKS.
|
|
40
|
-
;;
|
|
41
|
-
;;
|
|
42
|
-
;;
|
|
43
|
-
;;
|
|
44
|
-
;;
|
|
45
|
-
;;
|
|
46
|
-
;;
|
|
47
|
-
;;
|
|
48
|
-
;; WORKS.
|
|
49
|
-
;;
|
|
50
|
-
;;
|
|
51
|
-
;;
|
|
52
|
-
;;
|
|
53
|
-
;;
|
|
54
|
-
;;
|
|
55
|
-
;;
|
|
56
|
-
;;
|
|
57
|
-
;;
|
|
52
|
+
;; WORKS. Clicking p1 sets phase "selection" and lights exactly 9
|
|
53
|
+
;; green cells — the range-2 manhattan diamond minus occupied cells.
|
|
54
|
+
;; Dead units are NOT selectable (guard on health > 0): clicking a
|
|
55
|
+
;; corpse does nothing. SPENT units (already attacked this turn, in
|
|
56
|
+
;; actedIds) are not selectable either — their activation is over.
|
|
57
|
+
;; A unit that already MOVED this turn still selects (it may attack)
|
|
58
|
+
;; but lights zero green cells — one move per turn.
|
|
59
|
+
;; 4. I click a lit cell and the unit moves there — and stays selected,
|
|
60
|
+
;; auto-armed, so I can strike immediately.
|
|
61
|
+
;; WORKS. p1 walked (3,12)->(3,10): the walk animation plays (2-frame
|
|
62
|
+
;; cycle, ~6 ticks), the authority re-validates bounds/passable/
|
|
63
|
+
;; occupied and writes the position, and the unit lands in phase
|
|
64
|
+
;; "action" with any enemies now in reach lit red. Clicking an
|
|
65
|
+
;; OCCUPIED or out-of-reach cell is ignored outright (guard) instead
|
|
66
|
+
;; of silently eating the selection. The move is SPENT: the unit
|
|
67
|
+
;; cannot move again until End Turn (movedIds guard) — there is no
|
|
68
|
+
;; way to chain moves.
|
|
69
|
+
;; 5. Adjacent enemies I can strike light up red — right after my move,
|
|
70
|
+
;; or when I click my selected unit again.
|
|
71
|
+
;; WORKS. Post-move auto-arm computes red cells from the NEW position;
|
|
72
|
+
;; the manual re-click arm (phase "action") still works from a fresh
|
|
73
|
+
;; selection. Living enemies at manhattan distance <= attackRange
|
|
74
|
+
;; (config, default 1) light red.
|
|
75
|
+
;; 6. I click a red-lit enemy — or any enemy in reach, even without arming
|
|
76
|
+
;; first — and it takes a hit; my unit is then spent for the turn.
|
|
77
|
+
;; WORKS. The attack arm fires whenever a living selected unit has a
|
|
78
|
+
;; living enemy in reach, regardless of phase (selection or action) —
|
|
79
|
+
;; no more dead clicks on an enemy you just walked up to. The
|
|
80
|
+
;; attacker plays its attack frame, the target plays its hit frame, a
|
|
81
|
+
;; "-3" damage number + hit-star fx sprite spawn at the target
|
|
82
|
+
;; (BURST, ttl 3 decayed by FxDecay), the health bar shrinks
|
|
83
|
+
;; (verified e3 1.00 -> 0.63 of max), and the attacker joins actedIds:
|
|
84
|
+
;; it dims to 45% opacity and cannot be selected or attack again
|
|
85
|
+
;; until End Turn.
|
|
58
86
|
;; 7. I change my mind mid-selection and back out cleanly.
|
|
59
|
-
;; WORKS.
|
|
60
|
-
;;
|
|
87
|
+
;; WORKS. CANCEL clears selection, highlights, and phase — verified
|
|
88
|
+
;; green cells 9 -> 0 and phase back to "observation".
|
|
61
89
|
;; 8. I end my turn and the enemy takes theirs — they close in and fight
|
|
62
|
-
;; back.
|
|
63
|
-
;; WORKS. END_TURN
|
|
64
|
-
;;
|
|
65
|
-
;;
|
|
66
|
-
;;
|
|
67
|
-
;;
|
|
68
|
-
;;
|
|
69
|
-
;;
|
|
70
|
-
;;
|
|
90
|
+
;; back, and my party is fresh again.
|
|
91
|
+
;; WORKS. END_TURN bumps Turn, clears selection + both activation
|
|
92
|
+
;; lists (movedIds/actedIds — every unit can act again), and emits
|
|
93
|
+
;; AI_TURN; the authority strikes every player adjacent to a living
|
|
94
|
+
;; enemy for 1 and steps each unengaged enemy 1 cell toward the
|
|
95
|
+
;; nearest living player. Verified: enemy positions changed and/or
|
|
96
|
+
;; player hp dropped every turn.
|
|
97
|
+
;; LIMITATION: enemies POP to their new cell — the authority applies
|
|
98
|
+
;; the step silently and the single walk-anim slot is player-facing;
|
|
99
|
+
;; per-enemy MOVE fan-out is atom surgery, deferred.
|
|
100
|
+
;; 9. When I kill an enemy I am rewarded — score ticks up, xp ticks up,
|
|
101
|
+
;; and the corpse stays on the field.
|
|
102
|
+
;; WORKS x4. Each kill: Score +10, XP +25 (ScoreKeeper/XpKeeper on
|
|
103
|
+
;; UNITS_CHANGED), the corpse renders its death frame, and the cell
|
|
104
|
+
;; becomes walkable again — corpses never block movement or green
|
|
105
|
+
;; highlights (authority and highlight filters agree on health > 0;
|
|
106
|
+
;; verified with four corpses piled on two cells).
|
|
71
107
|
;; 10. When my xp crosses the threshold the party levels up and heals.
|
|
72
|
-
;; WORKS.
|
|
73
|
-
;;
|
|
74
|
-
;;
|
|
75
|
-
;; 11. When the last enemy falls, I win and the game tells me
|
|
76
|
-
;;
|
|
77
|
-
;;
|
|
78
|
-
;;
|
|
108
|
+
;; WORKS. The 4th kill crossed 100 xp: Lvl 1 -> 2 on the HUD and a
|
|
109
|
+
;; HEAL { team: "player", amount: 2 } fired on the bus, which the
|
|
110
|
+
;; authority applies capped at maxHealth.
|
|
111
|
+
;; 11. When the last enemy falls, I win and the game tells me — then the
|
|
112
|
+
;; board freezes.
|
|
113
|
+
;; WORKS. Result flips to "victory" (RoundLogic on UNITS_CHANGED),
|
|
114
|
+
;; GAME_END fires, and the board is FROZEN: every UNIT_CLICK /
|
|
115
|
+
;; TILE_CLICK arm is guarded on result == "none", so post-game clicks
|
|
116
|
+
;; select nothing and move nothing (verified by probe). Reload to
|
|
117
|
+
;; play again — an in-game rematch button does not exist yet.
|
|
79
118
|
;;
|
|
80
|
-
;;
|
|
119
|
+
;; MULTIPLAYER NOTE: GameState is [runtime, shared], so every connected
|
|
120
|
+
;; client watches the same battle on the same frame; intents serialize
|
|
121
|
+
;; through the server authority. There is no per-seat turn ownership yet —
|
|
122
|
+
;; any client can drive any player unit.
|
|
81
123
|
;;
|
|
82
|
-
;;
|
|
83
|
-
;;
|
|
84
|
-
;;
|
|
85
|
-
;;
|
|
86
|
-
;;
|
|
87
|
-
;;
|
|
88
|
-
;;
|
|
89
|
-
;;
|
|
90
|
-
;;
|
|
91
|
-
;;
|
|
124
|
+
;; ASSET INVENTORY (hand-authored svg, almadar-kflow-assets.web.app
|
|
125
|
+
;; /shared/ui-battle-board/svg/isometric/, generated by tools/asset-workflow
|
|
126
|
+
;; svg-generator from the sibling .md descriptions)
|
|
127
|
+
;;
|
|
128
|
+
;; Terrain terrain/grass.svg, terrain/stone.svg — full-cell isometric
|
|
129
|
+
;; cubes (top + left + right faces) that tessellate exactly with
|
|
130
|
+
;; the projector's cellPath; drawn at width/height 1.
|
|
131
|
+
;; Units units/{guardian,scrapper,worker,mender}.svg + animation frames
|
|
132
|
+
;; .{idle-0..3, walk-0..1, attack-0, hit-0, death-0}.svg; the
|
|
133
|
+
;; frame swaps url-rewrites name.svg -> name.<anim>-<i>.svg and a
|
|
134
|
+
;; hidden warm-up layer preloads every frame so swaps never pop.
|
|
135
|
+
;; party: p1 Guardian 10hp (3,12) p2 Scrapper 8hp (2,11)
|
|
136
|
+
;; p3 Worker 6hp (4,13) p4 Mender 7hp (2,13)
|
|
92
137
|
;; enemy: e1/e2 Guardian 10hp (12,3),(13,4)
|
|
93
138
|
;; e3/e4 Scrapper 8hp (11,2),(13,2)
|
|
94
|
-
;; Features
|
|
95
|
-
;;
|
|
96
|
-
;;
|
|
97
|
-
;;
|
|
98
|
-
;;
|
|
99
|
-
;;
|
|
100
|
-
;;
|
|
139
|
+
;; Features features/gold_mine.svg at (13,1), features/portal.svg at
|
|
140
|
+
;; (1,14) — the two home bases, drawn from config.features
|
|
141
|
+
;; anchored "ground" at 1x1 so they sit ON their cell (the
|
|
142
|
+
;; draw-sprite default anchor is "top-left", which floated them
|
|
143
|
+
;; off the board corner before the anchor was explicit).
|
|
144
|
+
;; Effects effects/hit.svg (BURST "hit" fx), effects/explosion.svg
|
|
145
|
+
;; (manifest-only).
|
|
146
|
+
;; UI chrome ui/panel-frame.svg = game-shell background + HUD panel.
|
|
101
147
|
;; ============================================================================
|
|
102
148
|
|
|
103
149
|
orbital BattleBoardOrbital {
|
|
@@ -176,6 +222,8 @@ orbital BattleBoardOrbital {
|
|
|
176
222
|
selectedUnitId : string = ""
|
|
177
223
|
validMoves : [ValidMove] = []
|
|
178
224
|
attackTargets : [ValidMove] = []
|
|
225
|
+
movedIds : [string] = []
|
|
226
|
+
actedIds : [string] = []
|
|
179
227
|
uiPhase : string = "observation"
|
|
180
228
|
turn : number = 0
|
|
181
229
|
score : number = 0
|