@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.
Files changed (39) hide show
  1. package/behaviors/lolo/ui/game/molecules/std-tactics-board-2d.lolo +35 -21
  2. package/behaviors/lolo/ui/game/molecules/ui-canvas.lolo +5 -0
  3. package/behaviors/lolo/ui/game/organisms/ui-battle-board.lolo +122 -74
  4. package/behaviors/lolo/ui/game/organisms/ui-hex-strategy-board.lolo +153 -9
  5. package/behaviors/lolo/ui/game/organisms/ui-tactics-demo-2d.lolo +130 -8
  6. package/behaviors/lolo/ui/game/organisms/ui-tanks-board.lolo +147 -7
  7. package/behaviors/lolo/ui/game/organisms/ui-topdown-rts-board.lolo +291 -19
  8. package/behaviors/registry/factory-signatures.json +1893 -2294
  9. package/behaviors/registry/ui/game/molecules/std-tactics-board-2d.orb +798 -189
  10. package/behaviors/registry/ui/game/molecules/ui-canvas.orb +7 -0
  11. package/behaviors/registry/ui/game/organisms/ui-battle-board.orb +17 -1
  12. package/behaviors/registry/ui/game/organisms/ui-hex-strategy-board.orb +68 -1180
  13. package/behaviors/registry/ui/game/organisms/ui-tactics-demo-2d.orb +192 -322
  14. package/behaviors/registry/ui/game/organisms/ui-tanks-board.orb +97 -85
  15. package/behaviors/registry/ui/game/organisms/ui-topdown-rts-board.orb +1477 -259
  16. package/dist/behaviors/behaviors-embeddings.json +244 -236
  17. package/dist/behaviors/behaviors-registry.json +143 -772
  18. package/dist/behaviors/exports-reader.js +1099 -2090
  19. package/dist/behaviors/functions/index.d.ts +33 -26
  20. package/dist/behaviors/functions/index.js +1099 -2091
  21. package/dist/behaviors/index.d.ts +1 -1
  22. package/dist/behaviors/index.js +1100 -2091
  23. package/dist/behaviors/knob-embeddings.json +1 -1
  24. package/dist/behaviors/query.js +1099 -2090
  25. package/dist/behaviors/registry/factory-signatures.json +1893 -2294
  26. package/dist/behaviors/registry/ui/game/molecules/std-tactics-board-2d.orb +798 -189
  27. package/dist/behaviors/registry/ui/game/molecules/ui-canvas.orb +7 -0
  28. package/dist/behaviors/registry/ui/game/organisms/ui-battle-board.orb +17 -1
  29. package/dist/behaviors/registry/ui/game/organisms/ui-hex-strategy-board.orb +68 -1180
  30. package/dist/behaviors/registry/ui/game/organisms/ui-tactics-demo-2d.orb +192 -322
  31. package/dist/behaviors/registry/ui/game/organisms/ui-tanks-board.orb +97 -85
  32. package/dist/behaviors/registry/ui/game/organisms/ui-topdown-rts-board.orb +1477 -259
  33. package/dist/behaviors-embeddings.json +244 -236
  34. package/dist/behaviors-registry.json +143 -772
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +1100 -2091
  37. package/dist/knob-embeddings.json +1 -1
  38. package/dist/registry/factory-signatures.json +1893 -2294
  39. 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.uiPhase "action") (and (== (object/get (array/find @entity.units (fn u (== @u.id ?unitId))) team) "enemy") (<= (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)) 1)))
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)) 1))))) (fn u { x: (object/get (object/get @u position) x), z: (object/get (object/get @u position) y) })))
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 shape-character theme supplied via config."
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 is against the CURRENT file.
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
- ;; HOW THE GAME WORKS (the loop): select a unit -> move it (green cells) ->
13
- ;; click it again to arm attack (red cells = adjacent enemies) -> click an
14
- ;; adjacent enemy to strike for 3 -> End Turn -> every living enemy strikes
15
- ;; adjacent players for 1 or steps 1 cell toward the nearest player -> repeat
16
- ;; until one side is wiped. Kills score 10 and grant 25 xp; every 100 xp
17
- ;; (+50 per level) levels up and heals the whole party +2.
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. PlayerIntent renders a game-shell with one canvas, fit: true
26
- ;; (fills the viewport, camera centered on the tile grid's midpoint),
27
- ;; projection "isometric". 16x16 all-grass tiles paint from the
28
- ;; assetManifest; HUD stats come from extraStats (XP, Lvl) appended to
29
- ;; the frame's Result/Phase/Turn/Score. Zoom is wheel/pinch, clamped
30
- ;; [0.05, 10] upstream in @almadar/ui useCamera; minimap on.
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. draw-sprite-layer paints each unit's modelUrl anchored
34
- ;; "ground"; two draw-shape-layers paint the black backing + green
35
- ;; health fraction above it; a draw-text-layer paints the name.
36
- ;; Broken assets would show a magenta fallback square + a
37
- ;; almadar:ui:draw-sprite warn (upstream DrawSprite contract).
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. UNIT_CLICK (team "player") sets selectedUnitId, uiPhase
40
- ;; "selection", and validMoves = passable, unoccupied tiles within
41
- ;; movementRange 2 (manhattan), painted translucent green.
42
- ;; 4. I click a lit cell and the unit moves there.
43
- ;; WORKS. TILE_CLICK (in bounds, within range) emits MOVE; the
44
- ;; grid-tactics authority re-validates (bounds/passable/unoccupied)
45
- ;; and writes the new position. Selection resets to "observation".
46
- ;; 5. I click my selected unit again and adjacent enemies I can strike
47
- ;; light up red.
48
- ;; WORKS. Second UNIT_CLICK on the same id enters uiPhase "action"
49
- ;; and computes attackTargets = living enemies at manhattan distance
50
- ;; <= 1, painted translucent red.
51
- ;; 6. I click a red-lit enemy and it takes a hit — I see the damage
52
- ;; number, its health bar shrinks.
53
- ;; WORKS. UNIT_CLICK (enemy, action phase, adjacent) emits ATTACK;
54
- ;; the authority subtracts attackDamage 3 and emits BURST, which
55
- ;; fx-particles spawns as a floating "-3" at the target (ttl 3,
56
- ;; decayed by FxDecay). The health-bar layer re-derives from
57
- ;; health/maxHealth on the shared frame.
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. The HUD's Cancel button emits CANCEL, clearing selection,
60
- ;; highlights, and phase (guarded on a selection existing).
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 (guarded on result "none") bumps turn and emits
64
- ;; AI_TURN; the authority damages every player adjacent to a living
65
- ;; enemy for 1, then steps each unengaged enemy 1 cell toward the
66
- ;; nearest living player (clamped in-bounds, passable, unoccupied).
67
- ;; 9. When I kill an enemy I am rewarded — score ticks up, xp ticks up.
68
- ;; WORKS. Every authority mutation emits UNITS_CHANGED with live
69
- ;; survivor counts; ScoreKeeper awards (baseline - enemiesAlive) x 10,
70
- ;; XpKeeper the same x 25. Both are headless atoms on the shared frame.
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. XpKeeper at xp >= xpToNext (100, +50/level) bumps level,
73
- ;; raises the threshold, and emits HEAL { team: "player", amount: 2 },
74
- ;; which the authority applies (capped at maxHealth).
75
- ;; 11. When the last enemy falls, I win and the game tells me.
76
- ;; WORKS. RoundLogic derives result from UNITS_CHANGED: enemiesAlive 0
77
- ;; -> "victory" (playersAlive 0 -> "defeat") and emits GAME_END; the
78
- ;; HUD's Result stat shows it and END_TURN is guarded shut.
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
- ;; ASSET INVENTORY (all Kenney, served from almadar-kflow-assets.web.app)
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
- ;; Terrain kenney-isometric-tiles-base/terrain/landscapeTiles_sheet
83
- ;; grass = landscapeTiles_000.png (all 256 cells), stone =
84
- ;; landscapeTiles_001.png (manifest-only, no stone tiles seeded).
85
- ;; Units ui-battle-board/kenney-shape-characters/units/spritesheet_default
86
- ;; (board-local copy; the _shared sheet was a corrupt 49x49 file
87
- ;; whose atlas expects 577x577 repaired there too)
88
- ;; party: p1 Guardian blue_body_square.png 10hp (3,12)
89
- ;; p2 Scrapper red_body_rhombus.png 8hp (2,11)
90
- ;; p3 Worker yellow_body_squircle.png 6hp (4,13)
91
- ;; p4 Mender green_body_circle.png 7hp (2,13)
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 ui-battle-board/kenney-cartography-pack/ui/spritesheet_default
95
- ;; gold_mine = mine.png at (13,1), portal = gate.png at (1,14)
96
- ;; (decorative; drawn from config.features).
97
- ;; Effects _shared/kenney-explosion-pack/effects/spritesheet_pixelExplosion
98
- ;; explosion = pixelExplosion07.png (manifest effects entry).
99
- ;; UI chrome _shared/kenney-ui-adventure-pack/ui/uipack_rpg_sheet
100
- ;; panel_beige.png = game-shell background + HUD panel.
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