@auraindustry/aurajs 0.0.7 → 0.1.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 (334) hide show
  1. package/README.md +98 -2
  2. package/benchmarks/perf-thresholds.json +54 -0
  3. package/package.json +4 -7
  4. package/src/asset-pack.mjs +5 -1
  5. package/src/authored-project.mjs +1449 -0
  6. package/src/authored-runtime.mjs +2016 -0
  7. package/src/authoring/avatar-animation-graph.mjs +648 -0
  8. package/src/bin-integrity.mjs +272 -0
  9. package/src/build-contract/assets.mjs +130 -0
  10. package/src/build-contract/capabilities.mjs +116 -0
  11. package/src/build-contract/constants.mjs +6 -0
  12. package/src/build-contract/helpers.mjs +44 -0
  13. package/src/build-contract/web-templates.mjs +5993 -0
  14. package/src/build-contract.mjs +27 -2910
  15. package/src/bundler.mjs +188 -55
  16. package/src/cli.mjs +4825 -1512
  17. package/src/commands/project-authoring.mjs +434 -0
  18. package/src/config.mjs +27 -0
  19. package/src/conformance/cases/app-and-ui-runtime-cases.mjs +3309 -0
  20. package/src/conformance/cases/core-runtime-cases.mjs +1431 -0
  21. package/src/conformance/cases/index.mjs +11 -0
  22. package/src/conformance/cases/scene3d-and-media-cases.mjs +2094 -0
  23. package/src/conformance/cases/systems-and-gameplay-cases.mjs +1776 -0
  24. package/src/conformance/shared.mjs +27 -0
  25. package/src/conformance-runner.mjs +25 -13
  26. package/src/conformance.mjs +619 -4020
  27. package/src/cutscene.mjs +362 -5
  28. package/src/dev-cli-action.mjs +249 -0
  29. package/src/dev-cli-inspect.mjs +92 -0
  30. package/src/dev-cli-state.mjs +80 -0
  31. package/src/external-asset-cache.mjs +587 -0
  32. package/src/external-asset-policy.mjs +217 -0
  33. package/src/external-package-surface.mjs +206 -0
  34. package/src/game-action-runtime.mjs +869 -0
  35. package/src/game-state-runtime.mjs +206 -6
  36. package/src/headless-action.mjs +186 -0
  37. package/src/headless-test/runtime-animation.mjs +1173 -0
  38. package/src/headless-test/runtime-coordinator.mjs +1514 -0
  39. package/src/headless-test/runtime-primitives.mjs +320 -0
  40. package/src/headless-test/runtime-world.mjs +2253 -0
  41. package/src/headless-test.mjs +392 -4298
  42. package/src/host-binary.mjs +342 -14
  43. package/src/icon-discovery.mjs +64 -0
  44. package/src/make-catalog.mjs +109 -0
  45. package/src/make.mjs +197 -0
  46. package/src/package-integrity.mjs +586 -0
  47. package/src/perf-benchmark.mjs +353 -0
  48. package/src/postinstall.mjs +5 -5
  49. package/src/prefabs/index.mjs +34 -0
  50. package/src/prefabs/scene-serialization.mjs +184 -0
  51. package/src/project-importer.mjs +620 -0
  52. package/src/project-registry.mjs +24 -0
  53. package/src/publish-command.mjs +195 -0
  54. package/src/publish-env-example.mjs +83 -0
  55. package/src/publish-validation.mjs +708 -0
  56. package/src/retro/assets/compile.mjs +232 -0
  57. package/src/retro/backend-gba/authoring.mjs +1029 -0
  58. package/src/retro/backend-gba/rom.mjs +363 -0
  59. package/src/retro/backend-gbc/rom.mjs +85 -0
  60. package/src/retro/build.mjs +278 -0
  61. package/src/retro/cli/commands.mjs +292 -0
  62. package/src/retro/cli/templates.mjs +84 -0
  63. package/src/retro/diagnostics/catalog.mjs +110 -0
  64. package/src/retro/diagnostics/emit.mjs +72 -0
  65. package/src/retro/emulator/case-overlay.mjs +64 -0
  66. package/src/retro/emulator/discovery.mjs +158 -0
  67. package/src/retro/emulator/macos-case-overlay.swift +220 -0
  68. package/src/retro/emulator/profiles.mjs +146 -0
  69. package/src/retro/emulator/runner.mjs +289 -0
  70. package/src/retro/frontend/load-project.mjs +98 -0
  71. package/src/retro/index.mjs +30 -0
  72. package/src/retro/ir/build-ir.mjs +108 -0
  73. package/src/retro/runtime-gba/contract.mjs +151 -0
  74. package/src/retro/runtime-gbc/contract.mjs +117 -0
  75. package/src/retro/shared/span.mjs +26 -0
  76. package/src/retro/shared/targets.mjs +64 -0
  77. package/src/retro/validator/check-project.mjs +114 -0
  78. package/src/runtime-hotspot-audit.mjs +707 -0
  79. package/src/scaffold/config.mjs +1000 -0
  80. package/src/scaffold/fs.mjs +56 -0
  81. package/src/scaffold/layout.mjs +318 -0
  82. package/src/scaffold/project-docs.mjs +438 -0
  83. package/src/scaffold.mjs +93 -596
  84. package/src/scene-composition/index.mjs +326 -0
  85. package/src/scene-composition/runtime.mjs +751 -0
  86. package/src/self-hosted-assets.mjs +604 -0
  87. package/src/session-client.mjs +750 -0
  88. package/src/session-native-launcher.mjs +74 -0
  89. package/src/session-protocol.mjs +75 -0
  90. package/src/session-runtime.mjs +321 -0
  91. package/src/session-server.mjs +360 -0
  92. package/src/shader-kits/index.mjs +773 -0
  93. package/src/starter-content-registry.mjs +292 -0
  94. package/src/state-artifacts.mjs +662 -24
  95. package/src/state-dev-reload.mjs +99 -2
  96. package/src/terminal-ui.mjs +245 -0
  97. package/src/web-conformance.mjs +219 -0
  98. package/templates/create/2d/config/gameplay/shooter.config.js +26 -0
  99. package/templates/create/2d/content/gameplay/waves.json +26 -0
  100. package/templates/create/2d/content/registries/.gitkeep +1 -0
  101. package/templates/create/2d/docs/design/.gitkeep +1 -0
  102. package/templates/create/2d/docs/design/loop.md +5 -0
  103. package/templates/create/2d/prefabs/enemies.prefab.js +90 -0
  104. package/templates/create/2d/prefabs/enemy-basic.prefab.js +18 -0
  105. package/templates/create/2d/prefabs/player.prefab.js +36 -0
  106. package/templates/create/2d/prefabs/projectiles.prefab.js +35 -0
  107. package/templates/create/2d/scenes/boot.scene.js +12 -0
  108. package/templates/create/2d/scenes/gameplay.scene.js +230 -0
  109. package/templates/create/2d/scenes/menu.scene.js +9 -0
  110. package/templates/create/2d/src/main.js +6 -185
  111. package/templates/create/2d/src/runtime/app.js +49 -0
  112. package/templates/create/2d/src/runtime/capabilities.js +35 -0
  113. package/templates/create/2d/ui/hud.screen.js +40 -0
  114. package/templates/create/2d/ui/pause.screen.js +149 -0
  115. package/templates/create/2d/ui/settings.screen.js +347 -0
  116. package/templates/create/2d/ui/title.screen.js +13 -0
  117. package/templates/create/2d-adventure/aura.config.json +28 -0
  118. package/templates/create/2d-adventure/config/gameplay/adventure.config.js +14 -0
  119. package/templates/create/2d-adventure/content/gameplay/world.js +46 -0
  120. package/templates/create/2d-adventure/content/registries/.gitkeep +1 -0
  121. package/templates/create/2d-adventure/docs/design/loop.md +5 -0
  122. package/templates/create/2d-adventure/prefabs/player.prefab.js +54 -0
  123. package/templates/create/2d-adventure/prefabs/relic.prefab.js +38 -0
  124. package/templates/create/2d-adventure/prefabs/world.prefab.js +125 -0
  125. package/templates/create/2d-adventure/scenes/gameplay.scene.js +256 -0
  126. package/templates/create/2d-adventure/src/runtime/capabilities.js +34 -0
  127. package/templates/create/2d-adventure/ui/hud.screen.js +60 -0
  128. package/templates/create/2d-survivor/config/gameplay/survivor.config.js +33 -0
  129. package/templates/create/2d-survivor/content/gameplay/spawn-zones.json +29 -0
  130. package/templates/create/2d-survivor/content/registries/.gitkeep +1 -0
  131. package/templates/create/2d-survivor/docs/design/.gitkeep +1 -0
  132. package/templates/create/2d-survivor/docs/design/loop.md +5 -0
  133. package/templates/create/2d-survivor/prefabs/enemies.prefab.js +178 -0
  134. package/templates/create/2d-survivor/prefabs/enemy-swarm.prefab.js +18 -0
  135. package/templates/create/2d-survivor/prefabs/player.prefab.js +42 -0
  136. package/templates/create/2d-survivor/prefabs/projectiles.prefab.js +56 -0
  137. package/templates/create/2d-survivor/scenes/boot.scene.js +12 -0
  138. package/templates/create/2d-survivor/scenes/gameplay.scene.js +314 -0
  139. package/templates/create/2d-survivor/scenes/menu.scene.js +9 -0
  140. package/templates/create/2d-survivor/src/main.js +5 -332
  141. package/templates/create/2d-survivor/src/runtime/app.js +49 -0
  142. package/templates/create/2d-survivor/src/runtime/capabilities.js +35 -0
  143. package/templates/create/2d-survivor/ui/hud.screen.js +45 -0
  144. package/templates/create/2d-survivor/ui/title.screen.js +13 -0
  145. package/templates/create/3d/assets/models/starter-avatar.gltf +184 -0
  146. package/templates/create/3d/config/gameplay/.gitkeep +1 -0
  147. package/templates/create/3d/content/gameplay/checkpoints.json +33 -0
  148. package/templates/create/3d/content/gameplay/course.js +40 -0
  149. package/templates/create/3d/content/registries/.gitkeep +1 -0
  150. package/templates/create/3d/docs/design/.gitkeep +1 -0
  151. package/templates/create/3d/docs/design/loop.md +5 -0
  152. package/templates/create/3d/prefabs/checkpoint.prefab.js +15 -0
  153. package/templates/create/3d/prefabs/player.prefab.js +204 -0
  154. package/templates/create/3d/prefabs/world.prefab.js +112 -0
  155. package/templates/create/3d/scenes/boot.scene.js +12 -0
  156. package/templates/create/3d/scenes/checkpoint.scene.js +9 -0
  157. package/templates/create/3d/scenes/gameplay.scene.js +292 -0
  158. package/templates/create/3d/src/main.js +6 -295
  159. package/templates/create/3d/src/runtime/app.js +49 -0
  160. package/templates/create/3d/src/runtime/capabilities.js +53 -0
  161. package/templates/create/3d/src/runtime/materials.js +34 -0
  162. package/templates/create/3d/src/runtime/state.js +39 -0
  163. package/templates/create/3d/ui/hud.screen.js +75 -0
  164. package/templates/create/3d/ui/pause.screen.js +166 -0
  165. package/templates/create/3d/ui/settings.screen.js +387 -0
  166. package/templates/create/3d-adventure/assets/models/starter-avatar.gltf +184 -0
  167. package/templates/create/3d-adventure/aura.config.json +28 -0
  168. package/templates/create/3d-adventure/config/gameplay/adventure.config.js +9 -0
  169. package/templates/create/3d-adventure/content/gameplay/course.js +62 -0
  170. package/templates/create/3d-adventure/content/registries/.gitkeep +1 -0
  171. package/templates/create/3d-adventure/docs/design/loop.md +5 -0
  172. package/templates/create/3d-adventure/prefabs/player.prefab.js +168 -0
  173. package/templates/create/3d-adventure/prefabs/relic.prefab.js +35 -0
  174. package/templates/create/3d-adventure/prefabs/world.prefab.js +119 -0
  175. package/templates/create/3d-adventure/scenes/gameplay.scene.js +358 -0
  176. package/templates/create/3d-adventure/src/runtime/capabilities.js +56 -0
  177. package/templates/create/3d-adventure/src/runtime/materials.js +39 -0
  178. package/templates/create/3d-adventure/src/runtime/state.js +31 -0
  179. package/templates/create/3d-adventure/ui/hud.screen.js +70 -0
  180. package/templates/create/3d-adventure/ui/pause.screen.js +437 -0
  181. package/templates/create/3d-collectathon/assets/models/starter-avatar.gltf +184 -0
  182. package/templates/create/3d-collectathon/config/gameplay/.gitkeep +1 -0
  183. package/templates/create/3d-collectathon/content/gameplay/collectibles.json +26 -0
  184. package/templates/create/3d-collectathon/content/gameplay/course.js +46 -0
  185. package/templates/create/3d-collectathon/content/registries/.gitkeep +1 -0
  186. package/templates/create/3d-collectathon/docs/design/.gitkeep +1 -0
  187. package/templates/create/3d-collectathon/docs/design/loop.md +5 -0
  188. package/templates/create/3d-collectathon/prefabs/collectible.prefab.js +15 -0
  189. package/templates/create/3d-collectathon/prefabs/player.prefab.js +207 -0
  190. package/templates/create/3d-collectathon/prefabs/world.prefab.js +112 -0
  191. package/templates/create/3d-collectathon/scenes/boot.scene.js +12 -0
  192. package/templates/create/3d-collectathon/scenes/checkpoint.scene.js +9 -0
  193. package/templates/create/3d-collectathon/scenes/gameplay.scene.js +200 -0
  194. package/templates/create/3d-collectathon/src/main.js +5 -355
  195. package/templates/create/3d-collectathon/src/runtime/app.js +49 -0
  196. package/templates/create/3d-collectathon/src/runtime/capabilities.js +53 -0
  197. package/templates/create/3d-collectathon/src/runtime/materials.js +34 -0
  198. package/templates/create/3d-collectathon/src/runtime/state.js +27 -0
  199. package/templates/create/3d-collectathon/ui/hud.screen.js +66 -0
  200. package/templates/create/3d-collectathon/ui/pause.screen.js +13 -0
  201. package/templates/create/blank/config/gameplay/.gitkeep +1 -0
  202. package/templates/create/blank/content/gameplay/.gitkeep +1 -0
  203. package/templates/create/blank/content/registries/.gitkeep +1 -0
  204. package/templates/create/blank/docs/design/.gitkeep +1 -0
  205. package/templates/create/blank/docs/design/loop.md +5 -0
  206. package/templates/create/blank/prefabs/.gitkeep +1 -0
  207. package/templates/create/blank/scenes/.gitkeep +1 -0
  208. package/templates/create/blank/src/runtime/.gitkeep +1 -0
  209. package/templates/create/blank/ui/.gitkeep +1 -0
  210. package/templates/create/deckbuilder-2d/assets/audio/.gitkeep +1 -0
  211. package/templates/create/deckbuilder-2d/assets/fonts/.gitkeep +1 -0
  212. package/templates/create/deckbuilder-2d/assets/sprites/.gitkeep +1 -0
  213. package/templates/create/deckbuilder-2d/assets/starter/README.md +11 -0
  214. package/templates/create/deckbuilder-2d/assets/ui/.gitkeep +1 -0
  215. package/templates/create/deckbuilder-2d/aura.config.json +28 -0
  216. package/templates/create/deckbuilder-2d/config/gameplay/deckbuilder.config.js +26 -0
  217. package/templates/create/deckbuilder-2d/content/cards/guard.card.js +19 -0
  218. package/templates/create/deckbuilder-2d/content/cards/spark.card.js +20 -0
  219. package/templates/create/deckbuilder-2d/content/cards/starter.deck.js +69 -0
  220. package/templates/create/deckbuilder-2d/content/cards/strike.card.js +19 -0
  221. package/templates/create/deckbuilder-2d/content/cards/survey.card.js +20 -0
  222. package/templates/create/deckbuilder-2d/content/encounters/training-battle.encounter.js +14 -0
  223. package/templates/create/deckbuilder-2d/content/encounters/training-battle.js +65 -0
  224. package/templates/create/deckbuilder-2d/content/enemies/training-automaton.enemy.js +48 -0
  225. package/templates/create/deckbuilder-2d/content/gameplay/.gitkeep +1 -0
  226. package/templates/create/deckbuilder-2d/content/registries/cards.registry.js +26 -0
  227. package/templates/create/deckbuilder-2d/content/registries/encounters.registry.js +20 -0
  228. package/templates/create/deckbuilder-2d/content/registries/enemies.registry.js +20 -0
  229. package/templates/create/deckbuilder-2d/content/registries/relics.registry.js +20 -0
  230. package/templates/create/deckbuilder-2d/content/relics/ember-charm.relic.js +18 -0
  231. package/templates/create/deckbuilder-2d/docs/design/loop.md +12 -0
  232. package/templates/create/deckbuilder-2d/prefabs/.gitkeep +1 -0
  233. package/templates/create/deckbuilder-2d/scenes/boot.scene.js +84 -0
  234. package/templates/create/deckbuilder-2d/scenes/gameplay.scene.js +641 -0
  235. package/templates/create/deckbuilder-2d/src/components/.gitkeep +1 -0
  236. package/templates/create/deckbuilder-2d/src/main.js +17 -0
  237. package/templates/create/deckbuilder-2d/src/runtime/capabilities.js +22 -0
  238. package/templates/create/deckbuilder-2d/src/shared/.gitkeep +1 -0
  239. package/templates/create/deckbuilder-2d/src/systems/.gitkeep +1 -0
  240. package/templates/create/deckbuilder-2d/tests/smoke/.gitkeep +1 -0
  241. package/templates/create/deckbuilder-2d/ui/hud.screen.js +80 -0
  242. package/templates/create/deckbuilder-2d/ui/pause.screen.js +146 -0
  243. package/templates/create/deckbuilder-2d/ui/settings.screen.js +342 -0
  244. package/templates/create/local-multiplayer/aura.config.json +40 -0
  245. package/templates/create/local-multiplayer/config/gameplay/local-multiplayer.config.js +26 -0
  246. package/templates/create/local-multiplayer/content/gameplay/room-layout.js +13 -0
  247. package/templates/create/local-multiplayer/content/registries/.gitkeep +1 -0
  248. package/templates/create/local-multiplayer/docs/design/loop.md +14 -0
  249. package/templates/create/local-multiplayer/prefabs/player.prefab.js +99 -0
  250. package/templates/create/local-multiplayer/scenes/boot.scene.js +12 -0
  251. package/templates/create/local-multiplayer/scenes/gameplay.scene.js +443 -0
  252. package/templates/create/local-multiplayer/src/main.js +17 -0
  253. package/templates/create/local-multiplayer/src/runtime/capabilities.js +28 -0
  254. package/templates/create/local-multiplayer/ui/hud.screen.js +60 -0
  255. package/templates/create/shared/src/runtime/project-inspector.js +105 -0
  256. package/templates/create/shared/src/runtime/scene-flow.js +290 -0
  257. package/templates/create/shared/src/runtime/screen-shell.js +222 -0
  258. package/templates/create/shared/src/runtime/ui-forms.js +209 -0
  259. package/templates/create/shared/src/runtime/ui-settings.js +237 -0
  260. package/templates/create/shared/src/runtime/ui-theme.js +352 -0
  261. package/templates/create/shared/src/starter-utils/adventure-objectives.js +102 -0
  262. package/templates/create/shared/src/starter-utils/animation-2d.js +337 -0
  263. package/templates/create/shared/src/starter-utils/avatar-3d.js +404 -0
  264. package/templates/create/shared/src/starter-utils/combat-feedback-2d.js +320 -0
  265. package/templates/create/shared/src/starter-utils/core.js +39 -3
  266. package/templates/create/shared/src/starter-utils/index.js +8 -2
  267. package/templates/create/shared/src/starter-utils/platformer-3d.js +34 -3
  268. package/templates/create/shared/src/starter-utils/triggers.js +662 -0
  269. package/templates/create/shared/src/starter-utils/tween-2d.js +615 -0
  270. package/templates/create/video-cutscene/assets/video/.gitkeep +0 -0
  271. package/templates/create/video-cutscene/aura.config.json +28 -0
  272. package/templates/create/video-cutscene/config/gameplay/.gitkeep +0 -0
  273. package/templates/create/video-cutscene/content/gameplay/.gitkeep +0 -0
  274. package/templates/create/video-cutscene/content/registries/.gitkeep +0 -0
  275. package/templates/create/video-cutscene/docs/design/loop.md +22 -0
  276. package/templates/create/video-cutscene/prefabs/.gitkeep +0 -0
  277. package/templates/create/video-cutscene/scenes/boot.scene.js +11 -0
  278. package/templates/create/video-cutscene/scenes/cutscene.scene.js +113 -0
  279. package/templates/create/video-cutscene/scenes/gameplay.scene.js +50 -0
  280. package/templates/create/video-cutscene/src/main.js +17 -0
  281. package/templates/create/video-cutscene/src/runtime/app.js +52 -0
  282. package/templates/create/video-cutscene/src/runtime/capabilities.js +35 -0
  283. package/templates/create/video-cutscene/src/runtime/state.js +13 -0
  284. package/templates/create/video-cutscene/ui/.gitkeep +0 -0
  285. package/templates/create-bin/play.js +1187 -0
  286. package/templates/make/README.md +46 -0
  287. package/templates/make/catalog.json +51 -0
  288. package/templates/make/component/files/{{MAKE_NAME}}.component.js +20 -0
  289. package/templates/make/component/manifest.json +9 -0
  290. package/templates/make/data/files/{{MAKE_NAME}}.json +14 -0
  291. package/templates/make/data/manifest.json +9 -0
  292. package/templates/make/material/files/{{MAKE_NAME}}.material.json +17 -0
  293. package/templates/make/material/manifest.json +9 -0
  294. package/templates/make/prefab/files/{{MAKE_NAME}}.prefab.js +20 -0
  295. package/templates/make/prefab/manifest.json +9 -0
  296. package/templates/make/scene/files/{{MAKE_NAME}}.scene.js +31 -0
  297. package/templates/make/scene/manifest.json +9 -0
  298. package/templates/make/shader/files/{{MAKE_NAME}}.shader.js +23 -0
  299. package/templates/make/shader/manifest.json +9 -0
  300. package/templates/make/system/files/{{MAKE_NAME}}.system.js +15 -0
  301. package/templates/make/system/manifest.json +9 -0
  302. package/templates/make/ui-screen/files/{{MAKE_NAME}}.screen.js +16 -0
  303. package/templates/make/ui-screen/files/{{MAKE_NAME}}.screen.json +23 -0
  304. package/templates/make/ui-screen/manifest.json +10 -0
  305. package/templates/make-starters/deckbuilder-2d/card/files/{{MAKE_NAME}}.card.js +22 -0
  306. package/templates/make-starters/deckbuilder-2d/card/manifest.json +9 -0
  307. package/templates/make-starters/deckbuilder-2d/catalog.json +34 -0
  308. package/templates/make-starters/deckbuilder-2d/encounter/files/{{MAKE_NAME}}.encounter.js +18 -0
  309. package/templates/make-starters/deckbuilder-2d/encounter/manifest.json +9 -0
  310. package/templates/make-starters/deckbuilder-2d/enemy/files/{{MAKE_NAME}}.enemy.js +28 -0
  311. package/templates/make-starters/deckbuilder-2d/enemy/manifest.json +9 -0
  312. package/templates/make-starters/deckbuilder-2d/relic/files/{{MAKE_NAME}}.relic.js +23 -0
  313. package/templates/make-starters/deckbuilder-2d/relic/manifest.json +9 -0
  314. package/templates/retro/platformer/README.md +10 -0
  315. package/templates/retro/platformer/assets/retro/assets.json +91 -0
  316. package/templates/retro/platformer/aura.config.json +7 -0
  317. package/templates/retro/platformer/package.json +5 -0
  318. package/templates/retro/platformer/src/main.js +40 -0
  319. package/templates/retro/puzzle-grid/README.md +10 -0
  320. package/templates/retro/puzzle-grid/assets/retro/assets.json +90 -0
  321. package/templates/retro/puzzle-grid/aura.config.json +7 -0
  322. package/templates/retro/puzzle-grid/package.json +5 -0
  323. package/templates/retro/puzzle-grid/src/main.js +29 -0
  324. package/templates/retro/tactics-grid/README.md +10 -0
  325. package/templates/retro/tactics-grid/assets/retro/assets.json +90 -0
  326. package/templates/retro/tactics-grid/aura.config.json +7 -0
  327. package/templates/retro/tactics-grid/package.json +5 -0
  328. package/templates/retro/tactics-grid/src/main.js +35 -0
  329. package/templates/retro/topdown-adventure/README.md +10 -0
  330. package/templates/retro/topdown-adventure/assets/retro/assets.json +95 -0
  331. package/templates/retro/topdown-adventure/aura.config.json +7 -0
  332. package/templates/retro/topdown-adventure/package.json +5 -0
  333. package/templates/retro/topdown-adventure/src/main.js +29 -0
  334. package/templates/skills/aurajs/SKILL.md +61 -5
@@ -0,0 +1,2253 @@
1
+ export function createHeadlessWorldRuntime({ loadJsonAsset } = {}) {
2
+ let nextEcsEntityId = 1;
3
+ const ecsEntities = new Set();
4
+ const ecsComponentStores = new Map();
5
+ const ecsSystems = [];
6
+ const sortEcsSystems = () => {
7
+ ecsSystems.sort((a, b) => (a.order - b.order) || a.name.localeCompare(b.name));
8
+ };
9
+ const normalizeEntityId = (value) => (
10
+ Number.isInteger(value) && value > 0 ? value : null
11
+ );
12
+ const normalizeComponentName = (value) => (
13
+ typeof value === 'string' && value.length > 0 ? value : null
14
+ );
15
+ const normalizeSystemName = (value) => (
16
+ typeof value === 'string' && value.length > 0 ? value : null
17
+ );
18
+
19
+ let nextScene3dNodeId = 1;
20
+ const scene3dNodes = new Map();
21
+ const scene3dIsValidNodeId = (value) => Number.isInteger(value) && value > 0;
22
+ const scene3dCloneVec3 = (value, fallback) => ({
23
+ x: Number.isFinite(value?.x) ? Number(value.x) : fallback.x,
24
+ y: Number.isFinite(value?.y) ? Number(value.y) : fallback.y,
25
+ z: Number.isFinite(value?.z) ? Number(value.z) : fallback.z,
26
+ });
27
+ const scene3dNormalizeTransform = (input) => {
28
+ const src = (input && typeof input === 'object') ? input : {};
29
+ return {
30
+ position: scene3dCloneVec3(src.position, { x: 0, y: 0, z: 0 }),
31
+ rotation: scene3dCloneVec3(src.rotation, { x: 0, y: 0, z: 0 }),
32
+ scale: scene3dCloneVec3(src.scale, { x: 1, y: 1, z: 1 }),
33
+ };
34
+ };
35
+ const scene3dCloneTransform = (transform) => ({
36
+ position: { ...transform.position },
37
+ rotation: { ...transform.rotation },
38
+ scale: { ...transform.scale },
39
+ });
40
+ const scene3dSortedChildren = (node) => [...node.children].sort((a, b) => a - b);
41
+ const scene3dDetachFromParent = (node) => {
42
+ if (node.parentId == null) return;
43
+ const parent = scene3dNodes.get(node.parentId);
44
+ if (parent) parent.children.delete(node.id);
45
+ node.parentId = null;
46
+ };
47
+ const scene3dWouldCreateCycle = (nodeId, parentId) => {
48
+ let cursor = parentId;
49
+ while (cursor != null) {
50
+ if (cursor === nodeId) return true;
51
+ const cursorNode = scene3dNodes.get(cursor);
52
+ if (!cursorNode) break;
53
+ cursor = cursorNode.parentId;
54
+ }
55
+ return false;
56
+ };
57
+ const scene3dComposeWorldTransform = (nodeId) => {
58
+ const chain = [];
59
+ let cursor = scene3dNodes.get(nodeId);
60
+ while (cursor) {
61
+ chain.push(cursor);
62
+ if (cursor.parentId == null) break;
63
+ cursor = scene3dNodes.get(cursor.parentId);
64
+ }
65
+ chain.reverse();
66
+
67
+ const world = {
68
+ position: { x: 0, y: 0, z: 0 },
69
+ rotation: { x: 0, y: 0, z: 0 },
70
+ scale: { x: 1, y: 1, z: 1 },
71
+ };
72
+
73
+ for (const node of chain) {
74
+ world.position.x += node.local.position.x * world.scale.x;
75
+ world.position.y += node.local.position.y * world.scale.y;
76
+ world.position.z += node.local.position.z * world.scale.z;
77
+ world.rotation.x += node.local.rotation.x;
78
+ world.rotation.y += node.local.rotation.y;
79
+ world.rotation.z += node.local.rotation.z;
80
+ world.scale.x *= node.local.scale.x;
81
+ world.scale.y *= node.local.scale.y;
82
+ world.scale.z *= node.local.scale.z;
83
+ }
84
+ return world;
85
+ };
86
+ const scene3dNormalizeRaycastVec3 = (value) => {
87
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
88
+ const x = Number(value.x);
89
+ const y = Number(value.y);
90
+ const z = Number(value.z);
91
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return null;
92
+ return { x, y, z };
93
+ };
94
+ const scene3dNormalizeRaycastArgs = (originOrOptions, maybeDirection, maybeOptions) => {
95
+ let origin = null;
96
+ let direction = null;
97
+ let options = null;
98
+ if (
99
+ originOrOptions
100
+ && typeof originOrOptions === 'object'
101
+ && !Array.isArray(originOrOptions)
102
+ && (
103
+ Object.prototype.hasOwnProperty.call(originOrOptions, 'origin')
104
+ || Object.prototype.hasOwnProperty.call(originOrOptions, 'direction')
105
+ )
106
+ ) {
107
+ origin = scene3dNormalizeRaycastVec3(originOrOptions.origin);
108
+ direction = scene3dNormalizeRaycastVec3(originOrOptions.direction);
109
+ options = originOrOptions;
110
+ } else {
111
+ origin = scene3dNormalizeRaycastVec3(originOrOptions);
112
+ direction = scene3dNormalizeRaycastVec3(maybeDirection);
113
+ options = maybeOptions;
114
+ }
115
+ if (!origin || !direction) return { ok: false, reason: 'invalid_raycast_args' };
116
+ const length = Math.hypot(direction.x, direction.y, direction.z);
117
+ if (!Number.isFinite(length) || length <= 1e-9) return { ok: false, reason: 'invalid_raycast_args' };
118
+ let maxDistance = 1000;
119
+ if (options && typeof options === 'object' && !Array.isArray(options) && Object.prototype.hasOwnProperty.call(options, 'maxDistance')) {
120
+ maxDistance = Number(options.maxDistance);
121
+ }
122
+ if (!Number.isFinite(maxDistance) || !(maxDistance > 0)) return { ok: false, reason: 'invalid_raycast_args' };
123
+ return {
124
+ ok: true,
125
+ origin,
126
+ direction: { x: direction.x / length, y: direction.y / length, z: direction.z / length },
127
+ maxDistance,
128
+ };
129
+ };
130
+ const scene3d = {
131
+ createNode: (initialTransform = null) => {
132
+ const id = nextScene3dNodeId++;
133
+ scene3dNodes.set(id, {
134
+ id,
135
+ parentId: null,
136
+ children: new Set(),
137
+ local: scene3dNormalizeTransform(initialTransform),
138
+ });
139
+ return id;
140
+ },
141
+ removeNode: (nodeId) => {
142
+ if (!scene3dIsValidNodeId(nodeId)) return false;
143
+ const node = scene3dNodes.get(nodeId);
144
+ if (!node) return false;
145
+ scene3dDetachFromParent(node);
146
+ for (const childId of scene3dSortedChildren(node)) {
147
+ const child = scene3dNodes.get(childId);
148
+ if (child) child.parentId = null;
149
+ }
150
+ scene3dNodes.delete(nodeId);
151
+ return true;
152
+ },
153
+ setParent: (nodeId, parentId) => {
154
+ if (!scene3dIsValidNodeId(nodeId)) return false;
155
+ const node = scene3dNodes.get(nodeId);
156
+ if (!node) return false;
157
+ if (parentId == null) {
158
+ scene3dDetachFromParent(node);
159
+ return true;
160
+ }
161
+ if (!scene3dIsValidNodeId(parentId) || parentId === nodeId) return false;
162
+ const parent = scene3dNodes.get(parentId);
163
+ if (!parent) return false;
164
+ if (scene3dWouldCreateCycle(nodeId, parentId)) return false;
165
+ scene3dDetachFromParent(node);
166
+ node.parentId = parentId;
167
+ parent.children.add(nodeId);
168
+ return true;
169
+ },
170
+ getParent: (nodeId) => {
171
+ if (!scene3dIsValidNodeId(nodeId)) return null;
172
+ const node = scene3dNodes.get(nodeId);
173
+ if (!node) return null;
174
+ return node.parentId;
175
+ },
176
+ setLocalTransform: (nodeId, transform) => {
177
+ if (!scene3dIsValidNodeId(nodeId)) return false;
178
+ const node = scene3dNodes.get(nodeId);
179
+ if (!node || !transform || typeof transform !== 'object') return false;
180
+ node.local = scene3dNormalizeTransform(transform);
181
+ return true;
182
+ },
183
+ getLocalTransform: (nodeId) => {
184
+ if (!scene3dIsValidNodeId(nodeId)) return null;
185
+ const node = scene3dNodes.get(nodeId);
186
+ if (!node) return null;
187
+ return scene3dCloneTransform(node.local);
188
+ },
189
+ getWorldTransform: (nodeId) => {
190
+ if (!scene3dIsValidNodeId(nodeId)) return null;
191
+ if (!scene3dNodes.has(nodeId)) return null;
192
+ return scene3dComposeWorldTransform(nodeId);
193
+ },
194
+ traverse: (rootOrCallback, maybeCallback) => {
195
+ let rootId = null;
196
+ let callback = maybeCallback;
197
+ if (typeof rootOrCallback === 'function') {
198
+ callback = rootOrCallback;
199
+ } else if (rootOrCallback != null) {
200
+ rootId = rootOrCallback;
201
+ }
202
+ if (typeof callback !== 'function') return false;
203
+ if (rootId != null && (!scene3dIsValidNodeId(rootId) || !scene3dNodes.has(rootId))) {
204
+ return false;
205
+ }
206
+
207
+ const walk = (nodeId) => {
208
+ const node = scene3dNodes.get(nodeId);
209
+ if (!node) return;
210
+ callback(
211
+ nodeId,
212
+ scene3dComposeWorldTransform(nodeId),
213
+ scene3dCloneTransform(node.local),
214
+ node.parentId,
215
+ );
216
+ for (const childId of scene3dSortedChildren(node)) {
217
+ walk(childId);
218
+ }
219
+ };
220
+
221
+ if (rootId != null) {
222
+ walk(rootId);
223
+ return true;
224
+ }
225
+
226
+ const roots = [...scene3dNodes.values()]
227
+ .filter((node) => node.parentId == null)
228
+ .map((node) => node.id)
229
+ .sort((a, b) => a - b);
230
+ for (const nodeId of roots) {
231
+ walk(nodeId);
232
+ }
233
+ return true;
234
+ },
235
+ queryRaycast: (originOrOptions, maybeDirection, maybeOptions) => {
236
+ const parsed = scene3dNormalizeRaycastArgs(originOrOptions, maybeDirection, maybeOptions);
237
+ if (!parsed.ok) {
238
+ return { ok: false, reason: parsed.reason };
239
+ }
240
+ const hits = [];
241
+ const nodeIds = [...scene3dNodes.keys()].sort((a, b) => a - b);
242
+ for (const nodeId of nodeIds) {
243
+ const world = scene3dComposeWorldTransform(nodeId);
244
+ const radius = Math.max(
245
+ 0.05,
246
+ (Math.max(
247
+ Math.abs(world.scale.x),
248
+ Math.abs(world.scale.y),
249
+ Math.abs(world.scale.z),
250
+ ) * 0.5),
251
+ );
252
+ const ox = parsed.origin.x - world.position.x;
253
+ const oy = parsed.origin.y - world.position.y;
254
+ const oz = parsed.origin.z - world.position.z;
255
+ const b = 2 * ((ox * parsed.direction.x) + (oy * parsed.direction.y) + (oz * parsed.direction.z));
256
+ const c = (ox * ox) + (oy * oy) + (oz * oz) - (radius * radius);
257
+ const discriminant = (b * b) - (4 * c);
258
+ if (!Number.isFinite(discriminant) || discriminant < 0) continue;
259
+ const root = Math.sqrt(discriminant);
260
+ let toi = (-b - root) / 2;
261
+ if (toi < 0) toi = (-b + root) / 2;
262
+ if (!Number.isFinite(toi) || toi < 0 || toi > parsed.maxDistance) continue;
263
+ hits.push({
264
+ nodeId,
265
+ toi: Number(toi.toFixed(6)),
266
+ point: {
267
+ x: Number((parsed.origin.x + (parsed.direction.x * toi)).toFixed(6)),
268
+ y: Number((parsed.origin.y + (parsed.direction.y * toi)).toFixed(6)),
269
+ z: Number((parsed.origin.z + (parsed.direction.z * toi)).toFixed(6)),
270
+ },
271
+ radius: Number(radius.toFixed(6)),
272
+ });
273
+ }
274
+ hits.sort((a, b) => (a.toi - b.toi) || (a.nodeId - b.nodeId));
275
+ return {
276
+ ok: true,
277
+ reason: null,
278
+ hit: hits.length > 0,
279
+ hitCount: hits.length,
280
+ firstHit: hits.length > 0 ? hits[0] : null,
281
+ hits,
282
+ maxDistance: Number(parsed.maxDistance.toFixed(6)),
283
+ };
284
+ },
285
+ };
286
+
287
+ const createSceneHelper = () => {
288
+ let lifecycleSeq = 1;
289
+ let nextTimedEventId = 1;
290
+ let globalTicks = 0;
291
+ let activeScene = null;
292
+ const sceneDefinitions = new Map();
293
+ const timedEvents = new Map();
294
+ const lifecycleTrace = [];
295
+
296
+ const isPlainObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
297
+ const normalizeSceneKey = (value) => {
298
+ if (typeof value !== 'string') return null;
299
+ const trimmed = value.trim();
300
+ return trimmed.length > 0 ? trimmed : null;
301
+ };
302
+ const resultOk = (reasonCode, extra = {}) => ({ ok: true, reasonCode, ...extra });
303
+ const resultErr = (reasonCode, extra = {}) => ({ ok: false, reasonCode, ...extra });
304
+ const cloneValue = (value) => {
305
+ if (value == null) return value;
306
+ if (Array.isArray(value)) return value.map((entry) => cloneValue(entry));
307
+ if (typeof value !== 'object') return value;
308
+ const out = {};
309
+ for (const key of Object.keys(value)) out[key] = cloneValue(value[key]);
310
+ return out;
311
+ };
312
+ const callbackContext = (sceneRecord) => ({
313
+ key: sceneRecord.key,
314
+ state: sceneRecord.state,
315
+ data: sceneRecord.data,
316
+ updates: sceneRecord.updates,
317
+ elapsed: sceneRecord.elapsed,
318
+ });
319
+ const recordLifecycle = (sceneKey, phase) => {
320
+ lifecycleTrace.push({
321
+ seq: lifecycleSeq++,
322
+ sceneKey,
323
+ phase,
324
+ });
325
+ };
326
+ const invokeSceneCallback = (sceneRecord, phase, ...args) => {
327
+ const callback = sceneRecord.callbacks[phase];
328
+ if (typeof callback !== 'function') return;
329
+ recordLifecycle(sceneRecord.key, phase);
330
+ try {
331
+ callback(callbackContext(sceneRecord), ...args);
332
+ } catch {
333
+ // Preserve deterministic progression when callbacks throw.
334
+ }
335
+ };
336
+ const sortedSceneKeys = () => [...sceneDefinitions.keys()].sort((a, b) => a.localeCompare(b));
337
+ const sortedEventIds = () => [...timedEvents.keys()].sort((a, b) => a - b);
338
+ const snapshotEvent = (event) => ({
339
+ eventId: event.id,
340
+ delay: event.delay,
341
+ interval: event.interval,
342
+ remaining: event.remaining,
343
+ repeat: event.repeat,
344
+ paused: event.paused,
345
+ dispatchCount: event.dispatchCount,
346
+ sceneKey: event.sceneKey,
347
+ createdTick: event.createdTick,
348
+ });
349
+ const resolveEvent = (eventId) => {
350
+ if (!Number.isInteger(eventId) || eventId <= 0) {
351
+ return { event: null, reasonCode: 'invalid_event_id' };
352
+ }
353
+ const event = timedEvents.get(eventId);
354
+ if (!event) {
355
+ return { event: null, reasonCode: 'missing_event' };
356
+ }
357
+ return { event, reasonCode: null };
358
+ };
359
+ const normalizeSceneDefinition = (definition) => {
360
+ if (!isPlainObject(definition)) {
361
+ return { callbacks: null, reasonCode: 'invalid_scene_definition' };
362
+ }
363
+ const callbacks = {};
364
+ for (const phase of ['preload', 'create', 'update', 'shutdown']) {
365
+ if (!Object.prototype.hasOwnProperty.call(definition, phase)) continue;
366
+ const callback = definition[phase];
367
+ if (callback != null && typeof callback !== 'function') {
368
+ return { callbacks: null, reasonCode: 'invalid_scene_callbacks' };
369
+ }
370
+ callbacks[phase] = callback;
371
+ }
372
+ return { callbacks, reasonCode: null };
373
+ };
374
+ const startInternal = (sceneKey, data = null, mode = 'start') => {
375
+ const key = normalizeSceneKey(sceneKey);
376
+ if (key == null) return resultErr('invalid_scene_key');
377
+ const definition = sceneDefinitions.get(key);
378
+ if (!definition) return resultErr('missing_scene_definition');
379
+
380
+ if (activeScene) {
381
+ const previous = activeScene;
382
+ activeScene = null;
383
+ invokeSceneCallback(previous, 'shutdown', mode === 'switch' ? 'switch' : 'start');
384
+ }
385
+
386
+ activeScene = {
387
+ key,
388
+ callbacks: definition.callbacks,
389
+ state: {},
390
+ data: cloneValue(data),
391
+ updates: 0,
392
+ elapsed: 0,
393
+ lastDt: 0,
394
+ startedTick: globalTicks,
395
+ };
396
+ invokeSceneCallback(activeScene, 'preload');
397
+ invokeSceneCallback(activeScene, 'create');
398
+ return resultOk(mode === 'switch' ? 'scene_switched' : 'scene_started', {
399
+ activeScene: key,
400
+ });
401
+ };
402
+ const advanceTimedEvents = (dt) => {
403
+ const due = [];
404
+ for (const eventId of sortedEventIds()) {
405
+ const event = timedEvents.get(eventId);
406
+ if (!event || event.paused) continue;
407
+ event.remaining -= dt;
408
+ while (event.remaining <= 1e-9) {
409
+ event.dispatchCount += 1;
410
+ due.push({
411
+ id: event.id,
412
+ sceneKey: event.sceneKey,
413
+ callback: event.callback,
414
+ dispatchCount: event.dispatchCount,
415
+ });
416
+ if (!event.repeat) {
417
+ timedEvents.delete(event.id);
418
+ break;
419
+ }
420
+ event.remaining += event.interval;
421
+ }
422
+ }
423
+ due.sort((a, b) => a.id - b.id);
424
+ for (const entry of due) {
425
+ try {
426
+ entry.callback({
427
+ eventId: entry.id,
428
+ sceneKey: entry.sceneKey,
429
+ dispatchCount: entry.dispatchCount,
430
+ });
431
+ } catch {
432
+ // Preserve deterministic progression when callbacks throw.
433
+ }
434
+ }
435
+ return due;
436
+ };
437
+
438
+ const scene = {
439
+ define(sceneKey, definition) {
440
+ const key = normalizeSceneKey(sceneKey);
441
+ if (key == null) return resultErr('invalid_scene_key');
442
+ const parsed = normalizeSceneDefinition(definition);
443
+ if (!parsed.callbacks) return resultErr(parsed.reasonCode || 'invalid_scene_definition');
444
+ const replaced = sceneDefinitions.has(key);
445
+ sceneDefinitions.set(key, {
446
+ key,
447
+ callbacks: parsed.callbacks,
448
+ });
449
+ return resultOk('scene_defined', { sceneKey: key, replaced });
450
+ },
451
+ register(sceneKey, definition) {
452
+ return scene.define(sceneKey, definition);
453
+ },
454
+ start(sceneKey, data = null) {
455
+ return startInternal(sceneKey, data, 'start');
456
+ },
457
+ switchTo(sceneKey, data = null) {
458
+ return startInternal(sceneKey, data, 'switch');
459
+ },
460
+ update(dt) {
461
+ const stepDt = Number(dt);
462
+ if (!Number.isFinite(stepDt) || !(stepDt > 0)) {
463
+ return resultErr('invalid_dt');
464
+ }
465
+ if (!activeScene) {
466
+ return resultErr('no_active_scene');
467
+ }
468
+
469
+ globalTicks += 1;
470
+ const firedEvents = advanceTimedEvents(stepDt);
471
+ const current = activeScene;
472
+ current.updates += 1;
473
+ current.elapsed += stepDt;
474
+ current.lastDt = stepDt;
475
+ invokeSceneCallback(current, 'update', stepDt);
476
+ return resultOk('scene_updated', {
477
+ activeScene: activeScene ? activeScene.key : null,
478
+ firedEvents: firedEvents.length,
479
+ firedEventIds: firedEvents.map((entry) => entry.id),
480
+ });
481
+ },
482
+ getState() {
483
+ return {
484
+ activeScene: activeScene ? activeScene.key : null,
485
+ sceneCount: sceneDefinitions.size,
486
+ scenes: sortedSceneKeys(),
487
+ tick: globalTicks,
488
+ active: activeScene ? {
489
+ key: activeScene.key,
490
+ updates: activeScene.updates,
491
+ elapsed: activeScene.elapsed,
492
+ lastDt: activeScene.lastDt,
493
+ startedTick: activeScene.startedTick,
494
+ state: cloneValue(activeScene.state),
495
+ data: cloneValue(activeScene.data),
496
+ } : null,
497
+ lifecycle: lifecycleTrace.map((entry) => ({ ...entry })),
498
+ timedEvents: sortedEventIds()
499
+ .map((eventId) => timedEvents.get(eventId))
500
+ .filter(Boolean)
501
+ .map((event) => snapshotEvent(event)),
502
+ };
503
+ },
504
+ schedule(delaySeconds, callback, options = {}) {
505
+ const delay = Number(delaySeconds);
506
+ if (!Number.isFinite(delay) || !(delay > 0)) {
507
+ return resultErr('invalid_delay');
508
+ }
509
+ if (typeof callback !== 'function') {
510
+ return resultErr('invalid_callback');
511
+ }
512
+ const normalizedOptions = options == null ? {} : options;
513
+ if (!isPlainObject(normalizedOptions)) {
514
+ return resultErr('invalid_event_options');
515
+ }
516
+
517
+ let repeat = false;
518
+ if (Object.prototype.hasOwnProperty.call(normalizedOptions, 'repeat')) {
519
+ if (typeof normalizedOptions.repeat !== 'boolean') {
520
+ return resultErr('invalid_repeat_flag');
521
+ }
522
+ repeat = normalizedOptions.repeat;
523
+ }
524
+
525
+ let interval = delay;
526
+ if (Object.prototype.hasOwnProperty.call(normalizedOptions, 'interval')) {
527
+ interval = Number(normalizedOptions.interval);
528
+ if (!Number.isFinite(interval) || !(interval > 0)) {
529
+ return resultErr('invalid_interval');
530
+ }
531
+ }
532
+
533
+ let paused = false;
534
+ if (Object.prototype.hasOwnProperty.call(normalizedOptions, 'paused')) {
535
+ if (typeof normalizedOptions.paused !== 'boolean') {
536
+ return resultErr('invalid_paused_flag');
537
+ }
538
+ paused = normalizedOptions.paused;
539
+ }
540
+
541
+ const event = {
542
+ id: nextTimedEventId++,
543
+ delay,
544
+ interval,
545
+ remaining: delay,
546
+ repeat,
547
+ paused,
548
+ callback,
549
+ dispatchCount: 0,
550
+ sceneKey: activeScene ? activeScene.key : null,
551
+ createdTick: globalTicks,
552
+ };
553
+ timedEvents.set(event.id, event);
554
+ return resultOk('scene_event_scheduled', { eventId: event.id });
555
+ },
556
+ pauseEvent(eventId) {
557
+ const resolved = resolveEvent(eventId);
558
+ if (!resolved.event) return resultErr(resolved.reasonCode);
559
+ resolved.event.paused = true;
560
+ return resultOk('scene_event_paused', {
561
+ eventId: resolved.event.id,
562
+ paused: true,
563
+ });
564
+ },
565
+ resumeEvent(eventId) {
566
+ const resolved = resolveEvent(eventId);
567
+ if (!resolved.event) return resultErr(resolved.reasonCode);
568
+ resolved.event.paused = false;
569
+ return resultOk('scene_event_resumed', {
570
+ eventId: resolved.event.id,
571
+ paused: false,
572
+ });
573
+ },
574
+ cancelEvent(eventId) {
575
+ const resolved = resolveEvent(eventId);
576
+ if (!resolved.event) return resultErr(resolved.reasonCode);
577
+ timedEvents.delete(resolved.event.id);
578
+ return resultOk('scene_event_cancelled', {
579
+ eventId: resolved.event.id,
580
+ cancelled: true,
581
+ });
582
+ },
583
+ getEventState(eventId) {
584
+ const resolved = resolveEvent(eventId);
585
+ if (!resolved.event) return null;
586
+ return snapshotEvent(resolved.event);
587
+ },
588
+ };
589
+
590
+ return scene;
591
+ };
592
+ const scene = createSceneHelper();
593
+
594
+ let nextTilemapId = 1;
595
+ const tilemaps = new Map();
596
+ const tilemapIsPosInt = (value) => Number.isInteger(value) && value > 0;
597
+ const tilemapIsNonNegInt = (value) => Number.isInteger(value) && value >= 0;
598
+ const tilemapFiniteOr = (value, fallback) => (Number.isFinite(value) ? Number(value) : fallback);
599
+ const tilemapIntOr = (value, fallback) => (Number.isInteger(value) ? Number(value) : fallback);
600
+ const tilemapFail = (reason, detail = '') => {
601
+ const suffix = detail ? `: ${detail}` : '';
602
+ throw new Error(`aura.tilemap.import: ${reason}${suffix}`);
603
+ };
604
+ const tilemapResolveRawMap = (source) => {
605
+ if (source && typeof source === 'object') return source;
606
+ if (typeof source === 'string') {
607
+ const trimmed = source.trim();
608
+ if (trimmed.startsWith('{')) {
609
+ try {
610
+ return JSON.parse(trimmed);
611
+ } catch (error) {
612
+ tilemapFail('invalid-json', String(error));
613
+ }
614
+ }
615
+ if (aura?.assets && typeof aura.assets.loadJson === 'function') {
616
+ return aura.assets.loadJson(source);
617
+ }
618
+ if (aura?.assets && typeof aura.assets.json === 'function') {
619
+ return aura.assets.json(source);
620
+ }
621
+ tilemapFail('unsupported-source-string', 'assets.loadJson/json unavailable');
622
+ }
623
+ tilemapFail('invalid-source', 'expected object or string');
624
+ };
625
+ const tilemapResolveTilesetForGid = (tilesets, gid) => {
626
+ let selected = null;
627
+ for (const tileset of tilesets) {
628
+ if (tileset.firstGid <= gid) {
629
+ selected = tileset;
630
+ } else {
631
+ break;
632
+ }
633
+ }
634
+ if (!selected) return null;
635
+ const localIndex = gid - selected.firstGid;
636
+ if (selected.tileCount != null && localIndex >= selected.tileCount) {
637
+ return null;
638
+ }
639
+ return selected;
640
+ };
641
+ const tilemapNormalizePropertyValue = (value, contextLabel) => {
642
+ if (value == null) return null;
643
+ if (typeof value === 'string' || typeof value === 'boolean') return value;
644
+ if (typeof value === 'number' && Number.isFinite(value)) return Number(value);
645
+ tilemapFail('invalid-property-value', `${contextLabel} must be string/number/boolean/null`);
646
+ };
647
+ const tilemapNormalizeProperties = (rawProperties, contextLabel) => {
648
+ if (rawProperties == null) return {};
649
+ const normalized = {};
650
+ if (Array.isArray(rawProperties)) {
651
+ for (let i = 0; i < rawProperties.length; i += 1) {
652
+ const entry = rawProperties[i];
653
+ if (!entry || typeof entry !== 'object') {
654
+ tilemapFail('invalid-property-entry', `${contextLabel}.properties[${i}] must be an object`);
655
+ }
656
+ const name = typeof entry.name === 'string' ? entry.name.trim() : '';
657
+ if (!name) {
658
+ tilemapFail('invalid-property-name', `${contextLabel}.properties[${i}].name must be a non-empty string`);
659
+ }
660
+ if (Object.hasOwn(normalized, name)) {
661
+ tilemapFail('duplicate-property-name', `${contextLabel}.properties[${i}] duplicate name ${name}`);
662
+ }
663
+ normalized[name] = tilemapNormalizePropertyValue(entry.value, `${contextLabel}.properties[${i}].value`);
664
+ }
665
+ return Object.keys(normalized).sort().reduce((acc, key) => {
666
+ acc[key] = normalized[key];
667
+ return acc;
668
+ }, {});
669
+ }
670
+ if (typeof rawProperties !== 'object') {
671
+ tilemapFail('invalid-properties', `${contextLabel}.properties must be an object or array`);
672
+ }
673
+ const keys = Object.keys(rawProperties).sort();
674
+ for (const key of keys) {
675
+ if (typeof key !== 'string' || key.length === 0) {
676
+ tilemapFail('invalid-property-name', `${contextLabel}.properties contains an invalid key`);
677
+ }
678
+ normalized[key] = tilemapNormalizePropertyValue(rawProperties[key], `${contextLabel}.properties.${key}`);
679
+ }
680
+ return normalized;
681
+ };
682
+ const tilemapNormalizeObjectPoints = (rawPoints, contextLabel) => {
683
+ if (!Array.isArray(rawPoints) || rawPoints.length === 0) {
684
+ tilemapFail('invalid-object-points', `${contextLabel} must be a non-empty array`);
685
+ }
686
+ return rawPoints.map((point, pointIndex) => {
687
+ if (!point || typeof point !== 'object') {
688
+ tilemapFail('invalid-object-point', `${contextLabel}[${pointIndex}] must be an object`);
689
+ }
690
+ const x = tilemapFiniteOr(point.x, Number.NaN);
691
+ const y = tilemapFiniteOr(point.y, Number.NaN);
692
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
693
+ tilemapFail('invalid-object-point', `${contextLabel}[${pointIndex}] x/y must be finite`);
694
+ }
695
+ return { x, y };
696
+ });
697
+ };
698
+ const tilemapNormalizeTilesets = (rawTilesets, mapTileWidth, mapTileHeight) => {
699
+ if (!Array.isArray(rawTilesets) || rawTilesets.length === 0) {
700
+ tilemapFail('invalid-tilesets', 'expected non-empty tilesets array');
701
+ }
702
+ const normalized = [];
703
+ for (let i = 0; i < rawTilesets.length; i += 1) {
704
+ const raw = rawTilesets[i];
705
+ if (!raw || typeof raw !== 'object') {
706
+ tilemapFail('invalid-tileset', `tileset[${i}] must be an object`);
707
+ }
708
+ const firstGid = Number(raw.firstgid);
709
+ if (!tilemapIsPosInt(firstGid)) {
710
+ tilemapFail('invalid-tileset-firstgid', `tileset[${i}].firstgid must be a positive integer`);
711
+ }
712
+ const image = typeof raw.image === 'string' && raw.image.length > 0 ? raw.image : null;
713
+ if (!image) {
714
+ tilemapFail('invalid-tileset-image', `tileset[${i}].image must be a non-empty string`);
715
+ }
716
+ const tileWidth = tilemapIsPosInt(raw.tilewidth) ? Number(raw.tilewidth) : mapTileWidth;
717
+ const tileHeight = tilemapIsPosInt(raw.tileheight) ? Number(raw.tileheight) : mapTileHeight;
718
+ const columns = Number(raw.columns);
719
+ if (!tilemapIsPosInt(columns)) {
720
+ tilemapFail('invalid-tileset-columns', `tileset[${i}].columns must be a positive integer`);
721
+ }
722
+ const tileCount = raw.tilecount == null
723
+ ? null
724
+ : (tilemapIsPosInt(raw.tilecount) ? Number(raw.tilecount) : null);
725
+ if (raw.tilecount != null && tileCount == null) {
726
+ tilemapFail('invalid-tileset-tilecount', `tileset[${i}].tilecount must be a positive integer`);
727
+ }
728
+ normalized.push({
729
+ firstGid,
730
+ image,
731
+ tileWidth,
732
+ tileHeight,
733
+ columns,
734
+ tileCount,
735
+ margin: tilemapIsNonNegInt(raw.margin) ? Number(raw.margin) : 0,
736
+ spacing: tilemapIsNonNegInt(raw.spacing) ? Number(raw.spacing) : 0,
737
+ properties: tilemapNormalizeProperties(raw.properties, `tilesets[${i}]`),
738
+ });
739
+ }
740
+ normalized.sort((a, b) => a.firstGid - b.firstGid);
741
+ for (let i = 1; i < normalized.length; i += 1) {
742
+ if (normalized[i - 1].firstGid === normalized[i].firstGid) {
743
+ tilemapFail('duplicate-tileset-firstgid', String(normalized[i].firstGid));
744
+ }
745
+ }
746
+ return normalized;
747
+ };
748
+ const tilemapNormalizeObjectLayer = (rawLayer, layerIndex, mapWidth, mapHeight, tilesets) => {
749
+ const objectContext = `layers[${layerIndex}]`;
750
+ if (rawLayer.compression) {
751
+ tilemapFail('unsupported-layer-compression', `${objectContext} compression is not supported`);
752
+ }
753
+ if (rawLayer.encoding && rawLayer.encoding !== 'csv') {
754
+ tilemapFail('unsupported-layer-encoding', `${objectContext} encoding must be omitted or csv`);
755
+ }
756
+ if (rawLayer.objects != null && !Array.isArray(rawLayer.objects)) {
757
+ tilemapFail('invalid-object-layer-objects', `${objectContext}.objects must be an array when provided`);
758
+ }
759
+ const rawObjects = Array.isArray(rawLayer.objects) ? rawLayer.objects : [];
760
+ const normalizedObjects = [];
761
+ for (let objectIndex = 0; objectIndex < rawObjects.length; objectIndex += 1) {
762
+ const rawObject = rawObjects[objectIndex];
763
+ const context = `${objectContext}.objects[${objectIndex}]`;
764
+ if (!rawObject || typeof rawObject !== 'object') {
765
+ tilemapFail('invalid-object', `${context} must be an object`);
766
+ }
767
+ if (rawObject.template != null) {
768
+ tilemapFail('unsupported-object-template', `${context}.template is not supported`);
769
+ }
770
+ if (rawObject.text != null) {
771
+ tilemapFail('unsupported-object-text', `${context}.text is not supported`);
772
+ }
773
+
774
+ const id = rawObject.id == null ? (objectIndex + 1) : Number(rawObject.id);
775
+ if (!tilemapIsNonNegInt(id)) {
776
+ tilemapFail('invalid-object-id', `${context}.id must be a non-negative integer`);
777
+ }
778
+ const gid = rawObject.gid == null ? 0 : Number(rawObject.gid);
779
+ if (!tilemapIsNonNegInt(gid)) {
780
+ tilemapFail('invalid-object-gid', `${context}.gid must be a non-negative integer`);
781
+ }
782
+ if (gid > 0 && !tilemapResolveTilesetForGid(tilesets, gid)) {
783
+ tilemapFail('unmapped-object-gid', `${context}.gid ${gid} has no matching tileset`);
784
+ }
785
+
786
+ const x = tilemapFiniteOr(rawObject.x, Number.NaN);
787
+ const y = tilemapFiniteOr(rawObject.y, Number.NaN);
788
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
789
+ tilemapFail('invalid-object-position', `${context}.x/y must be finite numbers`);
790
+ }
791
+ const width = rawObject.width == null ? 0 : tilemapFiniteOr(rawObject.width, Number.NaN);
792
+ const height = rawObject.height == null ? 0 : tilemapFiniteOr(rawObject.height, Number.NaN);
793
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width < 0 || height < 0) {
794
+ tilemapFail('invalid-object-size', `${context}.width/height must be finite numbers >= 0`);
795
+ }
796
+ const rotation = rawObject.rotation == null ? 0 : tilemapFiniteOr(rawObject.rotation, Number.NaN);
797
+ if (!Number.isFinite(rotation)) {
798
+ tilemapFail('invalid-object-rotation', `${context}.rotation must be finite`);
799
+ }
800
+
801
+ const hasPolygon = Array.isArray(rawObject.polygon);
802
+ const hasPolyline = Array.isArray(rawObject.polyline);
803
+ if (hasPolygon && hasPolyline) {
804
+ tilemapFail('invalid-object-shape', `${context} cannot include both polygon and polyline`);
805
+ }
806
+ const properties = tilemapNormalizeProperties(rawObject.properties, context);
807
+ normalizedObjects.push({
808
+ id,
809
+ name: typeof rawObject.name === 'string' ? rawObject.name : '',
810
+ className: typeof rawObject.class === 'string'
811
+ ? rawObject.class
812
+ : (typeof rawObject.type === 'string' ? rawObject.type : ''),
813
+ type: typeof rawObject.type === 'string' ? rawObject.type : '',
814
+ x,
815
+ y,
816
+ width,
817
+ height,
818
+ rotation,
819
+ visible: rawObject.visible !== false,
820
+ gid,
821
+ point: rawObject.point === true,
822
+ ellipse: rawObject.ellipse === true,
823
+ polygon: hasPolygon ? tilemapNormalizeObjectPoints(rawObject.polygon, `${context}.polygon`) : null,
824
+ polyline: hasPolyline ? tilemapNormalizeObjectPoints(rawObject.polyline, `${context}.polyline`) : null,
825
+ properties,
826
+ _seq: objectIndex,
827
+ });
828
+ }
829
+ normalizedObjects.sort((a, b) => (a.id - b.id) || (a._seq - b._seq));
830
+ const objects = normalizedObjects.map((entry) => {
831
+ const shape = entry.polygon ? 'polygon'
832
+ : (entry.polyline ? 'polyline'
833
+ : (entry.point ? 'point'
834
+ : (entry.ellipse ? 'ellipse'
835
+ : (entry.gid > 0 ? 'tile' : 'rectangle'))));
836
+ return {
837
+ id: entry.id,
838
+ name: entry.name,
839
+ className: entry.className,
840
+ type: entry.type,
841
+ x: entry.x,
842
+ y: entry.y,
843
+ width: entry.width,
844
+ height: entry.height,
845
+ rotation: entry.rotation,
846
+ visible: entry.visible,
847
+ gid: entry.gid,
848
+ point: entry.point,
849
+ ellipse: entry.ellipse,
850
+ polygon: entry.polygon,
851
+ polyline: entry.polyline,
852
+ properties: entry.properties,
853
+ shape,
854
+ };
855
+ });
856
+ return {
857
+ index: layerIndex,
858
+ name: typeof rawLayer.name === 'string' && rawLayer.name.length > 0 ? rawLayer.name : `layer_${layerIndex}`,
859
+ width: tilemapIsPosInt(rawLayer.width) ? Number(rawLayer.width) : mapWidth,
860
+ height: tilemapIsPosInt(rawLayer.height) ? Number(rawLayer.height) : mapHeight,
861
+ visible: rawLayer.visible !== false,
862
+ opacity: Number.isFinite(rawLayer.opacity) ? Number(rawLayer.opacity) : 1,
863
+ offsetX: tilemapFiniteOr(rawLayer.offsetx, 0),
864
+ offsetY: tilemapFiniteOr(rawLayer.offsety, 0),
865
+ drawOrder: typeof rawLayer.draworder === 'string' && rawLayer.draworder.length > 0
866
+ ? rawLayer.draworder
867
+ : 'topdown',
868
+ properties: tilemapNormalizeProperties(rawLayer.properties, objectContext),
869
+ objects,
870
+ };
871
+ };
872
+ const tilemapNormalizeLayers = (rawLayers, mapWidth, mapHeight, tilesets) => {
873
+ if (!Array.isArray(rawLayers) || rawLayers.length === 0) {
874
+ tilemapFail('invalid-layers', 'expected non-empty layers array');
875
+ }
876
+ const tileLayers = [];
877
+ const objectLayers = [];
878
+ for (let i = 0; i < rawLayers.length; i += 1) {
879
+ const raw = rawLayers[i];
880
+ if (!raw || typeof raw !== 'object') {
881
+ tilemapFail('invalid-layer', `layers[${i}] must be an object`);
882
+ }
883
+ if (raw.type === 'objectgroup') {
884
+ objectLayers.push(tilemapNormalizeObjectLayer(raw, i, mapWidth, mapHeight, tilesets));
885
+ continue;
886
+ }
887
+ if (raw.type !== 'tilelayer') continue;
888
+ if (raw.compression) {
889
+ tilemapFail('unsupported-layer-compression', `layers[${i}] compression is not supported`);
890
+ }
891
+ if (raw.encoding && raw.encoding !== 'csv') {
892
+ tilemapFail('unsupported-layer-encoding', `layers[${i}] encoding must be omitted or csv`);
893
+ }
894
+ const width = tilemapIsPosInt(raw.width) ? Number(raw.width) : mapWidth;
895
+ const height = tilemapIsPosInt(raw.height) ? Number(raw.height) : mapHeight;
896
+ if (!Array.isArray(raw.data) || raw.data.length !== width * height) {
897
+ tilemapFail('invalid-layer-data', `layers[${i}] data length must equal width*height`);
898
+ }
899
+ const data = new Array(raw.data.length);
900
+ for (let index = 0; index < raw.data.length; index += 1) {
901
+ const gid = Number(raw.data[index]);
902
+ if (!tilemapIsNonNegInt(gid)) {
903
+ tilemapFail('invalid-layer-gid', `layers[${i}] gid at index ${index} must be a non-negative integer`);
904
+ }
905
+ if (gid > 0 && !tilemapResolveTilesetForGid(tilesets, gid)) {
906
+ tilemapFail('unmapped-layer-gid', `layers[${i}] gid ${gid} has no matching tileset`);
907
+ }
908
+ data[index] = gid;
909
+ }
910
+ const properties = tilemapNormalizeProperties(raw.properties, `layers[${i}]`);
911
+ const layerQueryEnabled = raw.solid === true || raw.collision === true || properties.solid === true;
912
+ tileLayers.push({
913
+ index: i,
914
+ name: typeof raw.name === 'string' && raw.name.length > 0 ? raw.name : `layer_${i}`,
915
+ width,
916
+ height,
917
+ data,
918
+ solid: layerQueryEnabled,
919
+ collision: raw.collision === true,
920
+ queryEnabled: layerQueryEnabled,
921
+ properties,
922
+ visible: raw.visible !== false,
923
+ opacity: Number.isFinite(raw.opacity) ? Number(raw.opacity) : 1,
924
+ offsetX: tilemapFiniteOr(raw.offsetx, 0),
925
+ offsetY: tilemapFiniteOr(raw.offsety, 0),
926
+ });
927
+ }
928
+ if (tileLayers.length === 0) {
929
+ tilemapFail('missing-tile-layers', 'expected at least one tilelayer');
930
+ }
931
+ return { tileLayers, objectLayers };
932
+ };
933
+ const tilemapNormalizeMap = (rawMap) => {
934
+ if (!rawMap || typeof rawMap !== 'object') {
935
+ tilemapFail('invalid-map', 'expected object map payload');
936
+ }
937
+ const mapWidth = Number(rawMap.width);
938
+ const mapHeight = Number(rawMap.height);
939
+ const tileWidth = Number(rawMap.tilewidth);
940
+ const tileHeight = Number(rawMap.tileheight);
941
+ if (!tilemapIsPosInt(mapWidth) || !tilemapIsPosInt(mapHeight)) {
942
+ tilemapFail('invalid-map-size', 'width and height must be positive integers');
943
+ }
944
+ if (!tilemapIsPosInt(tileWidth) || !tilemapIsPosInt(tileHeight)) {
945
+ tilemapFail('invalid-map-tile-size', 'tilewidth and tileheight must be positive integers');
946
+ }
947
+ const tilesets = tilemapNormalizeTilesets(rawMap.tilesets, tileWidth, tileHeight);
948
+ const layers = tilemapNormalizeLayers(rawMap.layers, mapWidth, mapHeight, tilesets);
949
+ const solidLayerNames = new Set();
950
+ const appendSolidLayerNames = (value) => {
951
+ if (!Array.isArray(value)) return;
952
+ for (const entry of value) {
953
+ if (typeof entry === 'string' && entry.length > 0) {
954
+ solidLayerNames.add(entry);
955
+ }
956
+ }
957
+ };
958
+ appendSolidLayerNames(rawMap.solidLayerNames);
959
+ appendSolidLayerNames(rawMap.solidLayers);
960
+ const normalizedMap = {
961
+ width: mapWidth,
962
+ height: mapHeight,
963
+ tileWidth,
964
+ tileHeight,
965
+ tilesets,
966
+ layers: layers.tileLayers,
967
+ objectLayers: layers.objectLayers,
968
+ properties: tilemapNormalizeProperties(rawMap.properties, 'map'),
969
+ solidLayerNames: [...solidLayerNames],
970
+ };
971
+ for (const layer of normalizedMap.layers) {
972
+ if (solidLayerNames.has(layer.name)) {
973
+ layer.queryEnabled = true;
974
+ }
975
+ }
976
+ return normalizedMap;
977
+ };
978
+ const tilemapResolveMap = (mapId) => {
979
+ const id = tilemapIntOr(mapId, -1);
980
+ if (id <= 0) return null;
981
+ return tilemaps.get(id) || null;
982
+ };
983
+ const tilemapResolveLayerRef = (map, layerRef) => {
984
+ if (!map) return null;
985
+ if (typeof layerRef === 'number' && Number.isInteger(layerRef)) {
986
+ if (layerRef < 0 || layerRef >= map.layers.length) return null;
987
+ return { layer: map.layers[layerRef], layerIndex: layerRef };
988
+ }
989
+ if (typeof layerRef === 'string') {
990
+ const layerIndex = map.layers.findIndex((layer) => layer.name === layerRef);
991
+ if (layerIndex < 0) return null;
992
+ return { layer: map.layers[layerIndex], layerIndex };
993
+ }
994
+ return null;
995
+ };
996
+ const tilemapResolveLayerMutationTarget = (mapId, layerRef) => {
997
+ const map = tilemapResolveMap(mapId);
998
+ if (!map) {
999
+ return { ok: false, error: 'invalid tilemap handle', reasonCode: 'invalid_map_handle' };
1000
+ }
1001
+ const resolved = tilemapResolveLayerRef(map, layerRef);
1002
+ if (!resolved) {
1003
+ return { ok: false, error: 'invalid tilemap layer', reasonCode: 'invalid_layer_ref' };
1004
+ }
1005
+ return {
1006
+ ok: true,
1007
+ map,
1008
+ mapId: tilemapIntOr(mapId, -1),
1009
+ layer: resolved.layer,
1010
+ layerIndex: resolved.layerIndex,
1011
+ };
1012
+ };
1013
+ const tilemapMutationFailResult = (error, reasonCode) => ({
1014
+ ok: false,
1015
+ error,
1016
+ reasonCode,
1017
+ mutatedTiles: 0,
1018
+ changedTiles: 0,
1019
+ });
1020
+ const tilemapMutationOkResult = (payload = {}) => ({
1021
+ ok: true,
1022
+ error: null,
1023
+ reasonCode: null,
1024
+ ...payload,
1025
+ });
1026
+ const tilemapToPositiveInteger = (value) => {
1027
+ if (!Number.isFinite(value)) return null;
1028
+ const parsed = Math.floor(Number(value));
1029
+ return parsed > 0 ? parsed : null;
1030
+ };
1031
+ const tilemapToFinite = (value) => (
1032
+ Number.isFinite(value) ? Number(value) : null
1033
+ );
1034
+ const tilemapParseBooleanFlag = (value) => (typeof value === 'boolean' ? value : null);
1035
+ const tilemapParseNonNegGid = (value) => {
1036
+ const gid = Number(value);
1037
+ if (!tilemapIsNonNegInt(gid)) return null;
1038
+ return gid;
1039
+ };
1040
+ const tilemapParseRegionArgs = (args, offset = 0) => {
1041
+ const value = args[offset];
1042
+ let x = null;
1043
+ let y = null;
1044
+ let w = null;
1045
+ let h = null;
1046
+ let nextIndex = offset;
1047
+ if (value && typeof value === 'object') {
1048
+ x = Number(value.x);
1049
+ y = Number(value.y);
1050
+ w = Number(value.w);
1051
+ h = Number(value.h);
1052
+ nextIndex = offset + 1;
1053
+ } else {
1054
+ x = Number(args[offset]);
1055
+ y = Number(args[offset + 1]);
1056
+ w = Number(args[offset + 2]);
1057
+ h = Number(args[offset + 3]);
1058
+ nextIndex = offset + 4;
1059
+ }
1060
+ if (!Number.isInteger(x) || !Number.isInteger(y) || !Number.isInteger(w) || !Number.isInteger(h) || w <= 0 || h <= 0) {
1061
+ return null;
1062
+ }
1063
+ return { x, y, w, h, nextIndex };
1064
+ };
1065
+ const tilemapApplyRegionMutation = (layer, region, mutateCell) => {
1066
+ const startX = Math.max(0, region.x);
1067
+ const startY = Math.max(0, region.y);
1068
+ const endX = Math.min(layer.width, region.x + region.w);
1069
+ const endY = Math.min(layer.height, region.y + region.h);
1070
+ let mutatedTiles = 0;
1071
+ let changedTiles = 0;
1072
+
1073
+ for (let y = startY; y < endY; y += 1) {
1074
+ for (let x = startX; x < endX; x += 1) {
1075
+ mutatedTiles += 1;
1076
+ const index = (y * layer.width) + x;
1077
+ const previousGid = layer.data[index];
1078
+ const nextGid = mutateCell(previousGid, x, y);
1079
+ if (nextGid === previousGid) continue;
1080
+ layer.data[index] = nextGid;
1081
+ changedTiles += 1;
1082
+ }
1083
+ }
1084
+
1085
+ return {
1086
+ mutatedTiles,
1087
+ changedTiles,
1088
+ requestedRegion: { x: region.x, y: region.y, w: region.w, h: region.h },
1089
+ appliedRegion: {
1090
+ x: startX,
1091
+ y: startY,
1092
+ w: Math.max(0, endX - startX),
1093
+ h: Math.max(0, endY - startY),
1094
+ },
1095
+ };
1096
+ };
1097
+ const tilemapParseTilePointArgs = (args, offset = 0) => {
1098
+ const value = args[offset];
1099
+ let x = null;
1100
+ let y = null;
1101
+ let nextIndex = offset;
1102
+ if (value && typeof value === 'object') {
1103
+ x = Number(value.x);
1104
+ y = Number(value.y);
1105
+ nextIndex = offset + 1;
1106
+ } else {
1107
+ x = Number(args[offset]);
1108
+ y = Number(args[offset + 1]);
1109
+ nextIndex = offset + 2;
1110
+ }
1111
+ if (!Number.isInteger(x) || !Number.isInteger(y)) return null;
1112
+ return { x, y, nextIndex };
1113
+ };
1114
+ const tilemapParseTileOrRegionArgs = (args, offset = 0) => {
1115
+ const value = args[offset];
1116
+ if (value && typeof value === 'object') {
1117
+ const x = Number(value.x);
1118
+ const y = Number(value.y);
1119
+ const hasRegion = Object.hasOwn(value, 'w') || Object.hasOwn(value, 'h');
1120
+ if (hasRegion) {
1121
+ const w = Number(value.w);
1122
+ const h = Number(value.h);
1123
+ if (!Number.isInteger(x) || !Number.isInteger(y) || !Number.isInteger(w) || !Number.isInteger(h) || w <= 0 || h <= 0) {
1124
+ return null;
1125
+ }
1126
+ return { x, y, w, h, nextIndex: offset + 1 };
1127
+ }
1128
+ if (!Number.isInteger(x) || !Number.isInteger(y)) return null;
1129
+ return { x, y, w: 1, h: 1, nextIndex: offset + 1 };
1130
+ }
1131
+
1132
+ const x = Number(args[offset]);
1133
+ const y = Number(args[offset + 1]);
1134
+ if (!Number.isInteger(x) || !Number.isInteger(y)) return null;
1135
+
1136
+ const w = Number(args[offset + 2]);
1137
+ const h = Number(args[offset + 3]);
1138
+ if (Number.isInteger(w) && Number.isInteger(h) && w > 0 && h > 0 && args[offset + 4] !== undefined) {
1139
+ return { x, y, w, h, nextIndex: offset + 4 };
1140
+ }
1141
+ return { x, y, w: 1, h: 1, nextIndex: offset + 2 };
1142
+ };
1143
+ const tilemapTileIndexOrNull = (layer, x, y) => {
1144
+ if (!Number.isInteger(x) || !Number.isInteger(y)) return null;
1145
+ if (x < 0 || y < 0 || x >= layer.width || y >= layer.height) return null;
1146
+ return (y * layer.width) + x;
1147
+ };
1148
+ const tilemapEnsureCollisionOverrideMap = (layer) => {
1149
+ if (!(layer.collisionOverrides instanceof Map)) {
1150
+ layer.collisionOverrides = new Map();
1151
+ }
1152
+ return layer.collisionOverrides;
1153
+ };
1154
+ const tilemapNormalizeLayerNameSet = (model) => {
1155
+ const names = new Set();
1156
+ const append = (value) => {
1157
+ if (!Array.isArray(value)) return;
1158
+ for (const entry of value) {
1159
+ if (typeof entry === 'string' && entry.length > 0) {
1160
+ names.add(entry);
1161
+ }
1162
+ }
1163
+ };
1164
+ append(model?.solidLayerNames);
1165
+ append(model?.solidLayers);
1166
+ return names;
1167
+ };
1168
+ const tilemapSortQueryHits = (hits) => {
1169
+ hits.sort((a, b) => {
1170
+ if (a.layerIndex !== b.layerIndex) return a.layerIndex - b.layerIndex;
1171
+ if (a.y !== b.y) return a.y - b.y;
1172
+ if (a.x !== b.x) return a.x - b.x;
1173
+ if (a.layerName === b.layerName) return 0;
1174
+ return a.layerName < b.layerName ? -1 : 1;
1175
+ });
1176
+ return hits;
1177
+ };
1178
+ const tilemapNormalizeQueryModel = (model) => {
1179
+ if (!model || typeof model !== 'object') {
1180
+ return { ok: false, error: 'invalid tilemap model', reasonCode: 'invalid_model' };
1181
+ }
1182
+
1183
+ const width = tilemapToPositiveInteger(model.width);
1184
+ const height = tilemapToPositiveInteger(model.height);
1185
+ const tileWidth = tilemapToFinite(model.tileWidth ?? model.tilewidth);
1186
+ const tileHeight = tilemapToFinite(model.tileHeight ?? model.tileheight);
1187
+ if (width == null || height == null || tileWidth == null || tileHeight == null || tileWidth <= 0 || tileHeight <= 0) {
1188
+ return { ok: false, error: 'invalid tilemap model', reasonCode: 'invalid_model' };
1189
+ }
1190
+
1191
+ const solidNameSet = tilemapNormalizeLayerNameSet(model);
1192
+ const cells = [];
1193
+ const seen = new Set();
1194
+ const pushCell = (x, y, layerIndex, layerName) => {
1195
+ if (!Number.isInteger(x) || !Number.isInteger(y)) return;
1196
+ if (x < 0 || y < 0 || x >= width || y >= height) return;
1197
+ const normalizedLayerIndex = Number.isInteger(layerIndex) ? layerIndex : 0;
1198
+ const normalizedLayerName = typeof layerName === 'string' && layerName.length > 0
1199
+ ? layerName
1200
+ : `layer-${normalizedLayerIndex}`;
1201
+ const key = `${normalizedLayerIndex}|${y}|${x}|${normalizedLayerName}`;
1202
+ if (seen.has(key)) return;
1203
+ seen.add(key);
1204
+ cells.push({
1205
+ x,
1206
+ y,
1207
+ layerIndex: normalizedLayerIndex,
1208
+ layerName: normalizedLayerName,
1209
+ worldX: x * tileWidth,
1210
+ worldY: y * tileHeight,
1211
+ worldW: tileWidth,
1212
+ worldH: tileHeight,
1213
+ });
1214
+ };
1215
+
1216
+ if (Array.isArray(model.solidCells)) {
1217
+ for (const entry of model.solidCells) {
1218
+ if (!entry || typeof entry !== 'object') continue;
1219
+ pushCell(
1220
+ Number(entry.x),
1221
+ Number(entry.y),
1222
+ Number(entry.layerIndex),
1223
+ entry.layerName,
1224
+ );
1225
+ }
1226
+ }
1227
+
1228
+ if (Array.isArray(model.layers)) {
1229
+ for (let layerIndex = 0; layerIndex < model.layers.length; layerIndex += 1) {
1230
+ const layer = model.layers[layerIndex];
1231
+ if (!layer || typeof layer !== 'object') continue;
1232
+ const layerName = typeof layer.name === 'string' && layer.name.length > 0
1233
+ ? layer.name
1234
+ : `layer-${layerIndex}`;
1235
+ const layerWidth = tilemapToPositiveInteger(layer.width) ?? width;
1236
+ const layerHeight = tilemapToPositiveInteger(layer.height) ?? height;
1237
+ if (layerWidth !== width || layerHeight !== height) continue;
1238
+
1239
+ const layerSolid = typeof layer.queryEnabled === 'boolean'
1240
+ ? layer.queryEnabled
1241
+ : (
1242
+ layer.solid === true
1243
+ || layer.collision === true
1244
+ || layer.properties?.solid === true
1245
+ || solidNameSet.has(layerName)
1246
+ );
1247
+ if (!Array.isArray(layer.data)) continue;
1248
+ const overrideMap = layer.collisionOverrides instanceof Map
1249
+ ? layer.collisionOverrides
1250
+ : null;
1251
+ const hasOverrides = !!overrideMap && overrideMap.size > 0;
1252
+ if (!layerSolid && !hasOverrides) continue;
1253
+
1254
+ const maxEntries = Math.min(layer.data.length, width * height);
1255
+ for (let index = 0; index < maxEntries; index += 1) {
1256
+ const collisionOverride = overrideMap && overrideMap.has(index)
1257
+ ? overrideMap.get(index)
1258
+ : null;
1259
+ const collidable = typeof collisionOverride === 'boolean'
1260
+ ? collisionOverride
1261
+ : layerSolid;
1262
+ if (!collidable) continue;
1263
+ const gid = Number(layer.data[index]);
1264
+ if (!Number.isFinite(gid) || gid <= 0) continue;
1265
+ const x = index % width;
1266
+ const y = Math.floor(index / width);
1267
+ pushCell(x, y, layerIndex, layerName);
1268
+ }
1269
+ }
1270
+ }
1271
+
1272
+ tilemapSortQueryHits(cells);
1273
+ return {
1274
+ ok: true,
1275
+ width,
1276
+ height,
1277
+ tileWidth,
1278
+ tileHeight,
1279
+ cells,
1280
+ };
1281
+ };
1282
+ const tilemapResolveQueryModelInput = (input) => {
1283
+ if (typeof input === 'number' && Number.isInteger(input)) {
1284
+ if (input <= 0) {
1285
+ return { ok: false, error: 'invalid tilemap handle', reasonCode: 'invalid_map_handle' };
1286
+ }
1287
+ const map = tilemapResolveMap(input);
1288
+ if (!map) {
1289
+ return { ok: false, error: 'invalid tilemap handle', reasonCode: 'invalid_map_handle' };
1290
+ }
1291
+ return { ok: true, model: map };
1292
+ }
1293
+ if (!input || typeof input !== 'object') {
1294
+ return { ok: false, error: 'invalid tilemap model', reasonCode: 'invalid_model' };
1295
+ }
1296
+ return { ok: true, model: input };
1297
+ };
1298
+ const tilemapParsePointArgs = (args, offset = 0) => {
1299
+ const value = args[offset];
1300
+ if (value && typeof value === 'object') {
1301
+ const x = tilemapToFinite(value.x);
1302
+ const y = tilemapToFinite(value.y);
1303
+ if (x == null || y == null) return null;
1304
+ return { x, y };
1305
+ }
1306
+ const x = tilemapToFinite(args[offset]);
1307
+ const y = tilemapToFinite(args[offset + 1]);
1308
+ if (x == null || y == null) return null;
1309
+ return { x, y };
1310
+ };
1311
+ const tilemapParseAabbArgs = (args, offset = 0) => {
1312
+ const value = args[offset];
1313
+ if (value && typeof value === 'object') {
1314
+ const x = tilemapToFinite(value.x);
1315
+ const y = tilemapToFinite(value.y);
1316
+ const w = tilemapToFinite(value.w);
1317
+ const h = tilemapToFinite(value.h);
1318
+ if (x == null || y == null || w == null || h == null) return null;
1319
+ return { x, y, w, h };
1320
+ }
1321
+ const x = tilemapToFinite(args[offset]);
1322
+ const y = tilemapToFinite(args[offset + 1]);
1323
+ const w = tilemapToFinite(args[offset + 2]);
1324
+ const h = tilemapToFinite(args[offset + 3]);
1325
+ if (x == null || y == null || w == null || h == null) return null;
1326
+ return { x, y, w, h };
1327
+ };
1328
+ const tilemapParseRayArgs = (args, offset = 0) => {
1329
+ const value = args[offset];
1330
+ if (value && typeof value === 'object') {
1331
+ const x = tilemapToFinite(value.x);
1332
+ const y = tilemapToFinite(value.y);
1333
+ const dx = tilemapToFinite(value.dx);
1334
+ const dy = tilemapToFinite(value.dy);
1335
+ const maxDistance = tilemapToFinite(value.maxDistance ?? value.maxToi ?? 10000);
1336
+ if (x == null || y == null || dx == null || dy == null || maxDistance == null) return null;
1337
+ return { x, y, dx, dy, maxDistance };
1338
+ }
1339
+ const x = tilemapToFinite(args[offset]);
1340
+ const y = tilemapToFinite(args[offset + 1]);
1341
+ const dx = tilemapToFinite(args[offset + 2]);
1342
+ const dy = tilemapToFinite(args[offset + 3]);
1343
+ const maxDistance = tilemapToFinite(args[offset + 4] ?? 10000);
1344
+ if (x == null || y == null || dx == null || dy == null || maxDistance == null) return null;
1345
+ return { x, y, dx, dy, maxDistance };
1346
+ };
1347
+ const tilemapParsePointArgsWithNext = (args, offset = 0) => {
1348
+ const point = tilemapParsePointArgs(args, offset);
1349
+ if (!point) return null;
1350
+ const value = args[offset];
1351
+ return {
1352
+ ...point,
1353
+ nextIndex: value && typeof value === 'object' ? offset + 1 : offset + 2,
1354
+ };
1355
+ };
1356
+ const tilemapParseAabbArgsWithNext = (args, offset = 0) => {
1357
+ const rect = tilemapParseAabbArgs(args, offset);
1358
+ if (!rect) return null;
1359
+ const value = args[offset];
1360
+ return {
1361
+ ...rect,
1362
+ nextIndex: value && typeof value === 'object' ? offset + 1 : offset + 4,
1363
+ };
1364
+ };
1365
+ const tilemapIsPrimitivePropertyValue = (value) => (
1366
+ value == null
1367
+ || typeof value === 'string'
1368
+ || typeof value === 'boolean'
1369
+ || (typeof value === 'number' && Number.isFinite(value))
1370
+ );
1371
+ const tilemapParseObjectPropertyFilter = (rawFilter) => {
1372
+ if (typeof rawFilter === 'string') {
1373
+ const name = rawFilter.trim();
1374
+ if (!name) return null;
1375
+ return { name, hasValue: false, value: null };
1376
+ }
1377
+ if (!rawFilter || typeof rawFilter !== 'object' || Array.isArray(rawFilter)) {
1378
+ return null;
1379
+ }
1380
+ const name = typeof rawFilter.name === 'string' ? rawFilter.name.trim() : '';
1381
+ if (!name) return null;
1382
+ if (Object.hasOwn(rawFilter, 'value')) {
1383
+ if (!tilemapIsPrimitivePropertyValue(rawFilter.value)) return null;
1384
+ return { name, hasValue: true, value: rawFilter.value };
1385
+ }
1386
+ return { name, hasValue: false, value: null };
1387
+ };
1388
+ const tilemapParseObjectPropertiesFilter = (rawFilter) => {
1389
+ if (!rawFilter || typeof rawFilter !== 'object' || Array.isArray(rawFilter)) {
1390
+ return null;
1391
+ }
1392
+ const entries = Object.entries(rawFilter);
1393
+ const normalized = {};
1394
+ for (const [key, value] of entries) {
1395
+ const normalizedKey = typeof key === 'string' ? key.trim() : '';
1396
+ if (!normalizedKey || !tilemapIsPrimitivePropertyValue(value)) {
1397
+ return null;
1398
+ }
1399
+ normalized[normalizedKey] = value;
1400
+ }
1401
+ return normalized;
1402
+ };
1403
+ const tilemapParseObjectQueryOptions = (rawOptions) => {
1404
+ if (rawOptions == null) {
1405
+ return {
1406
+ ok: true,
1407
+ options: {
1408
+ includeHidden: false,
1409
+ layerRef: null,
1410
+ name: null,
1411
+ type: null,
1412
+ className: null,
1413
+ visible: null,
1414
+ propertyFilter: null,
1415
+ propertiesFilter: null,
1416
+ },
1417
+ };
1418
+ }
1419
+ if (!rawOptions || typeof rawOptions !== 'object' || Array.isArray(rawOptions)) {
1420
+ return { ok: false, error: 'invalid object query options', reasonCode: 'invalid_object_query_options' };
1421
+ }
1422
+
1423
+ let layerRef = null;
1424
+ if (Object.hasOwn(rawOptions, 'layer')) {
1425
+ const rawLayer = rawOptions.layer;
1426
+ if (typeof rawLayer === 'string') {
1427
+ const trimmed = rawLayer.trim();
1428
+ if (!trimmed) {
1429
+ return { ok: false, error: 'invalid tilemap object layer', reasonCode: 'invalid_object_layer_ref' };
1430
+ }
1431
+ layerRef = trimmed;
1432
+ } else if (Number.isInteger(rawLayer) && rawLayer >= 0) {
1433
+ layerRef = Number(rawLayer);
1434
+ } else {
1435
+ return { ok: false, error: 'invalid tilemap object layer', reasonCode: 'invalid_object_layer_ref' };
1436
+ }
1437
+ }
1438
+
1439
+ let includeHidden = false;
1440
+ if (Object.hasOwn(rawOptions, 'includeHidden')) {
1441
+ if (typeof rawOptions.includeHidden !== 'boolean') {
1442
+ return { ok: false, error: 'invalid object query options', reasonCode: 'invalid_object_query_options' };
1443
+ }
1444
+ includeHidden = rawOptions.includeHidden;
1445
+ }
1446
+
1447
+ let visible = null;
1448
+ if (Object.hasOwn(rawOptions, 'visible')) {
1449
+ if (typeof rawOptions.visible !== 'boolean') {
1450
+ return { ok: false, error: 'invalid object query options', reasonCode: 'invalid_object_query_options' };
1451
+ }
1452
+ visible = rawOptions.visible;
1453
+ includeHidden = true;
1454
+ }
1455
+
1456
+ const name = typeof rawOptions.name === 'string' && rawOptions.name.trim().length > 0
1457
+ ? rawOptions.name.trim()
1458
+ : null;
1459
+ const type = typeof rawOptions.type === 'string' && rawOptions.type.trim().length > 0
1460
+ ? rawOptions.type.trim()
1461
+ : null;
1462
+ const classNameRaw = typeof rawOptions.className === 'string'
1463
+ ? rawOptions.className
1464
+ : (typeof rawOptions.class === 'string' ? rawOptions.class : '');
1465
+ const className = classNameRaw.trim().length > 0 ? classNameRaw.trim() : null;
1466
+
1467
+ let propertyFilter = null;
1468
+ if (Object.hasOwn(rawOptions, 'property')) {
1469
+ propertyFilter = tilemapParseObjectPropertyFilter(rawOptions.property);
1470
+ if (!propertyFilter) {
1471
+ return { ok: false, error: 'invalid object property filter', reasonCode: 'invalid_object_property_filter' };
1472
+ }
1473
+ }
1474
+
1475
+ let propertiesFilter = null;
1476
+ if (Object.hasOwn(rawOptions, 'properties')) {
1477
+ propertiesFilter = tilemapParseObjectPropertiesFilter(rawOptions.properties);
1478
+ if (!propertiesFilter) {
1479
+ return { ok: false, error: 'invalid object property filter', reasonCode: 'invalid_object_property_filter' };
1480
+ }
1481
+ }
1482
+
1483
+ return {
1484
+ ok: true,
1485
+ options: {
1486
+ includeHidden,
1487
+ layerRef,
1488
+ name,
1489
+ type,
1490
+ className,
1491
+ visible,
1492
+ propertyFilter,
1493
+ propertiesFilter,
1494
+ },
1495
+ };
1496
+ };
1497
+ const tilemapResolveObjectLayerRef = (map, layerRef) => {
1498
+ if (!map || !Array.isArray(map.objectLayers)) return null;
1499
+ if (typeof layerRef === 'number' && Number.isInteger(layerRef)) {
1500
+ if (layerRef < 0) return null;
1501
+ let selected = map.objectLayers.find((layer) => layer && layer.index === layerRef) || null;
1502
+ if (!selected && layerRef >= 0 && layerRef < map.objectLayers.length) {
1503
+ selected = map.objectLayers[layerRef] || null;
1504
+ }
1505
+ if (!selected) return null;
1506
+ const resolvedIndex = Number.isInteger(selected.index)
1507
+ ? selected.index
1508
+ : map.objectLayers.indexOf(selected);
1509
+ return { layer: selected, layerIndex: resolvedIndex };
1510
+ }
1511
+ if (typeof layerRef === 'string') {
1512
+ const selected = map.objectLayers.find((layer) => layer && layer.name === layerRef) || null;
1513
+ if (!selected) return null;
1514
+ const resolvedIndex = Number.isInteger(selected.index)
1515
+ ? selected.index
1516
+ : map.objectLayers.indexOf(selected);
1517
+ return { layer: selected, layerIndex: resolvedIndex };
1518
+ }
1519
+ return null;
1520
+ };
1521
+ const tilemapCollectObjectLayers = (map, layerRef) => {
1522
+ if (!map || !Array.isArray(map.objectLayers)) {
1523
+ return { ok: false, error: 'invalid tilemap handle', reasonCode: 'invalid_map_handle' };
1524
+ }
1525
+ if (layerRef == null) {
1526
+ const layers = map.objectLayers
1527
+ .map((layer, index) => ({
1528
+ layer,
1529
+ layerIndex: Number.isInteger(layer?.index) ? layer.index : index,
1530
+ }))
1531
+ .filter((entry) => !!entry.layer);
1532
+ layers.sort((a, b) => {
1533
+ if (a.layerIndex !== b.layerIndex) return a.layerIndex - b.layerIndex;
1534
+ const aName = typeof a.layer.name === 'string' ? a.layer.name : '';
1535
+ const bName = typeof b.layer.name === 'string' ? b.layer.name : '';
1536
+ if (aName === bName) return 0;
1537
+ return aName < bName ? -1 : 1;
1538
+ });
1539
+ return { ok: true, layers };
1540
+ }
1541
+ const resolved = tilemapResolveObjectLayerRef(map, layerRef);
1542
+ if (!resolved) {
1543
+ return { ok: false, error: 'invalid tilemap object layer', reasonCode: 'invalid_object_layer_ref' };
1544
+ }
1545
+ return { ok: true, layers: [resolved] };
1546
+ };
1547
+ const tilemapCloneObjectProperties = (properties) => {
1548
+ if (!properties || typeof properties !== 'object' || Array.isArray(properties)) {
1549
+ return {};
1550
+ }
1551
+ const out = {};
1552
+ for (const key of Object.keys(properties).sort()) {
1553
+ out[key] = properties[key];
1554
+ }
1555
+ return out;
1556
+ };
1557
+ const tilemapSortObjectQueryHits = (hits) => {
1558
+ hits.sort((a, b) => {
1559
+ if (a.layerIndex !== b.layerIndex) return a.layerIndex - b.layerIndex;
1560
+ if (a.id !== b.id) return a.id - b.id;
1561
+ if (a.objectIndex !== b.objectIndex) return a.objectIndex - b.objectIndex;
1562
+ if (a.name !== b.name) return a.name < b.name ? -1 : 1;
1563
+ if (a.type !== b.type) return a.type < b.type ? -1 : 1;
1564
+ if (a.className !== b.className) return a.className < b.className ? -1 : 1;
1565
+ if (a.y !== b.y) return a.y - b.y;
1566
+ if (a.x !== b.x) return a.x - b.x;
1567
+ return 0;
1568
+ });
1569
+ return hits;
1570
+ };
1571
+ const tilemapNormalizeObjectHit = (layer, layerIndex, object, objectIndex) => ({
1572
+ layerName: typeof layer.name === 'string' ? layer.name : `layer_${layerIndex}`,
1573
+ layerIndex,
1574
+ objectIndex,
1575
+ id: Number.isInteger(object.id) ? object.id : 0,
1576
+ name: typeof object.name === 'string' ? object.name : '',
1577
+ type: typeof object.type === 'string' ? object.type : '',
1578
+ className: typeof object.className === 'string' ? object.className : '',
1579
+ shape: typeof object.shape === 'string' ? object.shape : 'rectangle',
1580
+ x: tilemapToFinite(object.x) ?? 0,
1581
+ y: tilemapToFinite(object.y) ?? 0,
1582
+ width: Math.max(0, tilemapToFinite(object.width) ?? 0),
1583
+ height: Math.max(0, tilemapToFinite(object.height) ?? 0),
1584
+ rotation: tilemapToFinite(object.rotation) ?? 0,
1585
+ visible: object.visible !== false,
1586
+ gid: Number.isInteger(object.gid) ? object.gid : 0,
1587
+ properties: tilemapCloneObjectProperties(object.properties),
1588
+ });
1589
+ const tilemapObjectBounds = (object) => {
1590
+ let minX = tilemapToFinite(object.x) ?? 0;
1591
+ let minY = tilemapToFinite(object.y) ?? 0;
1592
+ let maxX = minX + Math.max(0, tilemapToFinite(object.width) ?? 0);
1593
+ let maxY = minY + Math.max(0, tilemapToFinite(object.height) ?? 0);
1594
+ const points = Array.isArray(object.polygon)
1595
+ ? object.polygon
1596
+ : (Array.isArray(object.polyline) ? object.polyline : null);
1597
+ if (points && points.length > 0) {
1598
+ minX = Number.POSITIVE_INFINITY;
1599
+ minY = Number.POSITIVE_INFINITY;
1600
+ maxX = Number.NEGATIVE_INFINITY;
1601
+ maxY = Number.NEGATIVE_INFINITY;
1602
+ for (const point of points) {
1603
+ if (!point || typeof point !== 'object') continue;
1604
+ const px = (tilemapToFinite(object.x) ?? 0) + (tilemapToFinite(point.x) ?? 0);
1605
+ const py = (tilemapToFinite(object.y) ?? 0) + (tilemapToFinite(point.y) ?? 0);
1606
+ minX = Math.min(minX, px);
1607
+ minY = Math.min(minY, py);
1608
+ maxX = Math.max(maxX, px);
1609
+ maxY = Math.max(maxY, py);
1610
+ }
1611
+ if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
1612
+ const fallbackX = tilemapToFinite(object.x) ?? 0;
1613
+ const fallbackY = tilemapToFinite(object.y) ?? 0;
1614
+ minX = fallbackX;
1615
+ minY = fallbackY;
1616
+ maxX = fallbackX;
1617
+ maxY = fallbackY;
1618
+ }
1619
+ }
1620
+ return { minX, minY, maxX, maxY };
1621
+ };
1622
+ const tilemapPointInObject = (object, point) => {
1623
+ const bounds = tilemapObjectBounds(object);
1624
+ const epsilon = 1e-9;
1625
+ if (Math.abs(bounds.maxX - bounds.minX) <= epsilon && Math.abs(bounds.maxY - bounds.minY) <= epsilon) {
1626
+ return Math.abs(point.x - bounds.minX) <= epsilon && Math.abs(point.y - bounds.minY) <= epsilon;
1627
+ }
1628
+ return point.x >= (bounds.minX - epsilon)
1629
+ && point.x <= (bounds.maxX + epsilon)
1630
+ && point.y >= (bounds.minY - epsilon)
1631
+ && point.y <= (bounds.maxY + epsilon);
1632
+ };
1633
+ const tilemapAabbIntersectsObject = (object, rect) => {
1634
+ const bounds = tilemapObjectBounds(object);
1635
+ if (rect.w <= 0 || rect.h <= 0) return false;
1636
+ const epsilon = 1e-9;
1637
+ const rectMaxX = rect.x + rect.w;
1638
+ const rectMaxY = rect.y + rect.h;
1639
+ const isPoint = Math.abs(bounds.maxX - bounds.minX) <= epsilon && Math.abs(bounds.maxY - bounds.minY) <= epsilon;
1640
+ if (isPoint) {
1641
+ return bounds.minX >= (rect.x - epsilon)
1642
+ && bounds.minX <= (rectMaxX + epsilon)
1643
+ && bounds.minY >= (rect.y - epsilon)
1644
+ && bounds.minY <= (rectMaxY + epsilon);
1645
+ }
1646
+ return bounds.minX < rectMaxX
1647
+ && bounds.maxX > rect.x
1648
+ && bounds.minY < rectMaxY
1649
+ && bounds.maxY > rect.y;
1650
+ };
1651
+ const tilemapObjectMatchesFilter = (object, options) => {
1652
+ if (typeof options.visible === 'boolean' && (object.visible !== options.visible)) {
1653
+ return false;
1654
+ }
1655
+ if (options.name && object.name !== options.name) return false;
1656
+ if (options.type && object.type !== options.type) return false;
1657
+ if (options.className && object.className !== options.className) return false;
1658
+
1659
+ const properties = object && typeof object.properties === 'object' && !Array.isArray(object.properties)
1660
+ ? object.properties
1661
+ : {};
1662
+
1663
+ if (options.propertyFilter) {
1664
+ const key = options.propertyFilter.name;
1665
+ if (!Object.hasOwn(properties, key)) return false;
1666
+ if (options.propertyFilter.hasValue && properties[key] !== options.propertyFilter.value) {
1667
+ return false;
1668
+ }
1669
+ }
1670
+ if (options.propertiesFilter) {
1671
+ for (const [key, value] of Object.entries(options.propertiesFilter)) {
1672
+ if (!Object.hasOwn(properties, key) || properties[key] !== value) return false;
1673
+ }
1674
+ }
1675
+ return true;
1676
+ };
1677
+ const tilemapCollectFilteredObjectHits = (map, rawOptions, geometryPredicate = null) => {
1678
+ const parsedOptions = tilemapParseObjectQueryOptions(rawOptions);
1679
+ if (!parsedOptions.ok) return parsedOptions;
1680
+ const options = parsedOptions.options;
1681
+ const layerResult = tilemapCollectObjectLayers(map, options.layerRef);
1682
+ if (!layerResult.ok) return layerResult;
1683
+
1684
+ const hits = [];
1685
+ for (const entry of layerResult.layers) {
1686
+ const layer = entry.layer;
1687
+ const layerIndex = entry.layerIndex;
1688
+ if (!options.includeHidden && layer.visible === false) continue;
1689
+ if (!Array.isArray(layer.objects)) continue;
1690
+
1691
+ for (let objectIndex = 0; objectIndex < layer.objects.length; objectIndex += 1) {
1692
+ const object = layer.objects[objectIndex];
1693
+ if (!object || typeof object !== 'object') continue;
1694
+ if (!options.includeHidden && object.visible === false) continue;
1695
+ if (!tilemapObjectMatchesFilter(object, options)) continue;
1696
+ if (geometryPredicate && !geometryPredicate(object)) continue;
1697
+ hits.push(tilemapNormalizeObjectHit(layer, layerIndex, object, objectIndex));
1698
+ }
1699
+ }
1700
+ tilemapSortObjectQueryHits(hits);
1701
+ return { ok: true, hits };
1702
+ };
1703
+ const tilemapQueryHitsAtTile = (normalizedModel, tileX, tileY) => {
1704
+ if (!Number.isInteger(tileX) || !Number.isInteger(tileY)) return [];
1705
+ return normalizedModel.cells.filter((cell) => cell.x === tileX && cell.y === tileY);
1706
+ };
1707
+ const tilemapQueryFailResult = (error, reasonCode) => ({
1708
+ ok: false,
1709
+ hit: false,
1710
+ hits: [],
1711
+ error,
1712
+ reasonCode,
1713
+ });
1714
+ const tilemapQueryOkResult = (payload = {}) => ({
1715
+ ok: true,
1716
+ error: null,
1717
+ reasonCode: null,
1718
+ ...payload,
1719
+ });
1720
+ const tilemapBuildViewRect = (options) => {
1721
+ if (!options || typeof options !== 'object') return null;
1722
+ if (options.cull === false) return null;
1723
+ const camera = (options.camera && typeof options.camera === 'object')
1724
+ ? options.camera
1725
+ : ((options.view && typeof options.view === 'object') ? options.view : null);
1726
+ if (!camera) return null;
1727
+ const widthValue = tilemapFiniteOr(camera.width, Number.NaN);
1728
+ const heightValue = tilemapFiniteOr(camera.height, Number.NaN);
1729
+ if (!(widthValue > 0) || !(heightValue > 0)) return null;
1730
+ return {
1731
+ x: tilemapFiniteOr(camera.x, 0),
1732
+ y: tilemapFiniteOr(camera.y, 0),
1733
+ width: widthValue,
1734
+ height: heightValue,
1735
+ };
1736
+ };
1737
+ const tilemapIntersects = (x, y, w, h, view) => {
1738
+ if (!view) return true;
1739
+ return x < (view.x + view.width)
1740
+ && (x + w) > view.x
1741
+ && y < (view.y + view.height)
1742
+ && (y + h) > view.y;
1743
+ };
1744
+ const tilemapDrawLayerInternal = (map, mapId, layer, layerIndex, options) => {
1745
+ const includeHidden = options && options.includeHidden === true;
1746
+ const originX = options && Number.isFinite(options.x) ? Number(options.x) : 0;
1747
+ const originY = options && Number.isFinite(options.y) ? Number(options.y) : 0;
1748
+ const view = tilemapBuildViewRect(options);
1749
+ const stats = {
1750
+ mapId,
1751
+ layerName: layer.name,
1752
+ layerIndex,
1753
+ consideredTiles: 0,
1754
+ drawnTiles: 0,
1755
+ culledTiles: 0,
1756
+ };
1757
+ if (!includeHidden && (!layer.visible || layer.opacity <= 0)) {
1758
+ return stats;
1759
+ }
1760
+ for (let y = 0; y < layer.height; y += 1) {
1761
+ for (let x = 0; x < layer.width; x += 1) {
1762
+ const gid = layer.data[(y * layer.width) + x];
1763
+ if (!gid || gid <= 0) continue;
1764
+ stats.consideredTiles += 1;
1765
+ const drawX = originX + layer.offsetX + (x * map.tileWidth);
1766
+ const drawY = originY + layer.offsetY + (y * map.tileHeight);
1767
+ if (!tilemapIntersects(drawX, drawY, map.tileWidth, map.tileHeight, view)) {
1768
+ stats.culledTiles += 1;
1769
+ continue;
1770
+ }
1771
+ const tileset = tilemapResolveTilesetForGid(map.tilesets, gid);
1772
+ if (!tileset) continue;
1773
+ const localIndex = gid - tileset.firstGid;
1774
+ const frameX = tileset.margin + ((localIndex % tileset.columns) * (tileset.tileWidth + tileset.spacing));
1775
+ const frameY = tileset.margin + (Math.floor(localIndex / tileset.columns) * (tileset.tileHeight + tileset.spacing));
1776
+ stats.drawnTiles += 1;
1777
+ const draw2d = globalThis.aura?.draw2d;
1778
+ if (draw2d && typeof draw2d.sprite === 'function') {
1779
+ draw2d.sprite(
1780
+ { path: tileset.image },
1781
+ drawX,
1782
+ drawY,
1783
+ {
1784
+ width: map.tileWidth,
1785
+ height: map.tileHeight,
1786
+ frameX,
1787
+ frameY,
1788
+ frameW: tileset.tileWidth,
1789
+ frameH: tileset.tileHeight,
1790
+ },
1791
+ );
1792
+ }
1793
+ }
1794
+ }
1795
+ return stats;
1796
+ };
1797
+ const tilemap = {
1798
+ import: (source) => {
1799
+ const rawMap = tilemapResolveRawMap(source);
1800
+ const normalizedMap = tilemapNormalizeMap(rawMap);
1801
+ const id = nextTilemapId++;
1802
+ tilemaps.set(id, normalizedMap);
1803
+ return id;
1804
+ },
1805
+ unload: (mapId) => {
1806
+ const id = tilemapIntOr(mapId, -1);
1807
+ if (id <= 0) return false;
1808
+ return tilemaps.delete(id);
1809
+ },
1810
+ getInfo: (mapId) => {
1811
+ const map = tilemapResolveMap(mapId);
1812
+ if (!map) return null;
1813
+ const objectLayerCount = Array.isArray(map.objectLayers) ? map.objectLayers.length : 0;
1814
+ const objectCount = objectLayerCount > 0
1815
+ ? map.objectLayers.reduce((sum, layer) => sum + (Array.isArray(layer.objects) ? layer.objects.length : 0), 0)
1816
+ : 0;
1817
+ return {
1818
+ mapId: tilemapIntOr(mapId, -1),
1819
+ width: map.width,
1820
+ height: map.height,
1821
+ tileWidth: map.tileWidth,
1822
+ tileHeight: map.tileHeight,
1823
+ layerCount: map.layers.length,
1824
+ objectLayerCount,
1825
+ objectCount,
1826
+ tilesetCount: map.tilesets.length,
1827
+ mapPropertyCount: map.properties ? Object.keys(map.properties).length : 0,
1828
+ };
1829
+ },
1830
+ queryPoint: (mapOrModel, ...args) => {
1831
+ const resolvedModel = tilemapResolveQueryModelInput(mapOrModel);
1832
+ if (!resolvedModel.ok) return tilemapQueryFailResult(resolvedModel.error, resolvedModel.reasonCode);
1833
+ const normalizedModel = tilemapNormalizeQueryModel(resolvedModel.model);
1834
+ if (!normalizedModel.ok) return tilemapQueryFailResult(normalizedModel.error, normalizedModel.reasonCode);
1835
+ const point = tilemapParsePointArgs(args, 0);
1836
+ if (!point) return tilemapQueryFailResult('invalid point query args', 'invalid_point_args');
1837
+ const tileX = Math.floor(point.x / normalizedModel.tileWidth);
1838
+ const tileY = Math.floor(point.y / normalizedModel.tileHeight);
1839
+ const hits = tilemapQueryHitsAtTile(normalizedModel, tileX, tileY);
1840
+ return tilemapQueryOkResult({ hit: hits.length > 0, hits, tileX, tileY });
1841
+ },
1842
+ queryAABB: (mapOrModel, ...args) => {
1843
+ const resolvedModel = tilemapResolveQueryModelInput(mapOrModel);
1844
+ if (!resolvedModel.ok) return tilemapQueryFailResult(resolvedModel.error, resolvedModel.reasonCode);
1845
+ const normalizedModel = tilemapNormalizeQueryModel(resolvedModel.model);
1846
+ if (!normalizedModel.ok) return tilemapQueryFailResult(normalizedModel.error, normalizedModel.reasonCode);
1847
+ const rect = tilemapParseAabbArgs(args, 0);
1848
+ if (!rect || rect.w <= 0 || rect.h <= 0) {
1849
+ return tilemapQueryFailResult('invalid aabb query args', 'invalid_aabb_args');
1850
+ }
1851
+
1852
+ const epsilon = 1e-9;
1853
+ const minTileX = Math.floor(rect.x / normalizedModel.tileWidth);
1854
+ const minTileY = Math.floor(rect.y / normalizedModel.tileHeight);
1855
+ const maxTileX = Math.floor((rect.x + rect.w - epsilon) / normalizedModel.tileWidth);
1856
+ const maxTileY = Math.floor((rect.y + rect.h - epsilon) / normalizedModel.tileHeight);
1857
+ const hits = normalizedModel.cells.filter((cell) => (
1858
+ cell.x >= minTileX
1859
+ && cell.x <= maxTileX
1860
+ && cell.y >= minTileY
1861
+ && cell.y <= maxTileY
1862
+ ));
1863
+ return tilemapQueryOkResult({
1864
+ hit: hits.length > 0,
1865
+ hits,
1866
+ range: { minTileX, minTileY, maxTileX, maxTileY },
1867
+ });
1868
+ },
1869
+ queryRay: (mapOrModel, ...args) => {
1870
+ const resolvedModel = tilemapResolveQueryModelInput(mapOrModel);
1871
+ if (!resolvedModel.ok) return tilemapQueryFailResult(resolvedModel.error, resolvedModel.reasonCode);
1872
+ const normalizedModel = tilemapNormalizeQueryModel(resolvedModel.model);
1873
+ if (!normalizedModel.ok) return tilemapQueryFailResult(normalizedModel.error, normalizedModel.reasonCode);
1874
+ const ray = tilemapParseRayArgs(args, 0);
1875
+ const directionMagnitude = ray ? Math.hypot(ray.dx, ray.dy) : 0;
1876
+ if (!ray || !Number.isFinite(directionMagnitude) || directionMagnitude <= 0 || ray.maxDistance <= 0) {
1877
+ return tilemapQueryFailResult('invalid ray query args', 'invalid_ray_args');
1878
+ }
1879
+
1880
+ const dirX = ray.dx / directionMagnitude;
1881
+ const dirY = ray.dy / directionMagnitude;
1882
+ const step = Math.max(1e-4, Math.min(normalizedModel.tileWidth, normalizedModel.tileHeight) * 0.25);
1883
+ const steps = Math.max(1, Math.ceil(ray.maxDistance / step));
1884
+ for (let index = 0; index <= steps; index += 1) {
1885
+ const distance = Math.min(ray.maxDistance, index * step);
1886
+ const sampleX = ray.x + (dirX * distance);
1887
+ const sampleY = ray.y + (dirY * distance);
1888
+ const tileX = Math.floor(sampleX / normalizedModel.tileWidth);
1889
+ const tileY = Math.floor(sampleY / normalizedModel.tileHeight);
1890
+ const hits = tilemapQueryHitsAtTile(normalizedModel, tileX, tileY);
1891
+ if (hits.length > 0) {
1892
+ return tilemapQueryOkResult({
1893
+ hit: true,
1894
+ hits,
1895
+ hitCell: hits[0],
1896
+ distance,
1897
+ point: { x: sampleX, y: sampleY },
1898
+ tileX,
1899
+ tileY,
1900
+ });
1901
+ }
1902
+ }
1903
+ return tilemapQueryOkResult({
1904
+ hit: false,
1905
+ hits: [],
1906
+ hitCell: null,
1907
+ distance: null,
1908
+ point: null,
1909
+ tileX: null,
1910
+ tileY: null,
1911
+ });
1912
+ },
1913
+ queryRaycast: (mapOrModel, ...args) => tilemap.queryRay(mapOrModel, ...args),
1914
+ drawLayer: (mapId, layerRef, options = {}) => {
1915
+ const map = tilemapResolveMap(mapId);
1916
+ if (!map) return null;
1917
+ const resolved = tilemapResolveLayerRef(map, layerRef);
1918
+ if (!resolved) return null;
1919
+ return tilemapDrawLayerInternal(map, tilemapIntOr(mapId, -1), resolved.layer, resolved.layerIndex, options);
1920
+ },
1921
+ draw: (mapId, options = {}) => {
1922
+ const map = tilemapResolveMap(mapId);
1923
+ if (!map) return null;
1924
+ const includeHidden = options && options.includeHidden === true;
1925
+ const layers = [];
1926
+ const layerOrder = [];
1927
+ for (let i = 0; i < map.layers.length; i += 1) {
1928
+ const layer = map.layers[i];
1929
+ if (!includeHidden && (!layer.visible || layer.opacity <= 0)) continue;
1930
+ const layerStats = tilemapDrawLayerInternal(map, tilemapIntOr(mapId, -1), layer, i, options);
1931
+ layers.push(layerStats);
1932
+ layerOrder.push(layer.name);
1933
+ }
1934
+ let consideredTiles = 0;
1935
+ let drawnTiles = 0;
1936
+ let culledTiles = 0;
1937
+ for (const layerStats of layers) {
1938
+ consideredTiles += layerStats.consideredTiles;
1939
+ drawnTiles += layerStats.drawnTiles;
1940
+ culledTiles += layerStats.culledTiles;
1941
+ }
1942
+ return {
1943
+ mapId: tilemapIntOr(mapId, -1),
1944
+ layerOrder,
1945
+ consideredTiles,
1946
+ drawnTiles,
1947
+ culledTiles,
1948
+ layers,
1949
+ };
1950
+ },
1951
+ setTile: (mapId, layerRef, ...args) => {
1952
+ const target = tilemapResolveLayerMutationTarget(mapId, layerRef);
1953
+ if (!target.ok) return tilemapMutationFailResult(target.error, target.reasonCode);
1954
+ const point = tilemapParseTilePointArgs(args, 0);
1955
+ if (!point) return tilemapMutationFailResult('invalid tile coords args', 'invalid_tile_args');
1956
+ const gid = tilemapParseNonNegGid(args[point.nextIndex]);
1957
+ if (gid == null) return tilemapMutationFailResult('invalid tile gid args', 'invalid_gid_args');
1958
+ if (gid > 0 && !tilemapResolveTilesetForGid(target.map.tilesets, gid)) {
1959
+ return tilemapMutationFailResult('invalid tile gid args', 'unmapped_layer_gid');
1960
+ }
1961
+ const index = tilemapTileIndexOrNull(target.layer, point.x, point.y);
1962
+ if (index == null) {
1963
+ return tilemapMutationFailResult('tile coordinates out of bounds', 'tile_out_of_bounds');
1964
+ }
1965
+ const previousGid = Number(target.layer.data[index]) || 0;
1966
+ const changed = previousGid !== gid;
1967
+ if (changed) {
1968
+ target.layer.data[index] = gid;
1969
+ }
1970
+ return tilemapMutationOkResult({
1971
+ mapId: target.mapId,
1972
+ layerName: target.layer.name,
1973
+ layerIndex: target.layerIndex,
1974
+ x: point.x,
1975
+ y: point.y,
1976
+ previousGid,
1977
+ gid,
1978
+ changed,
1979
+ mutatedTiles: 1,
1980
+ changedTiles: changed ? 1 : 0,
1981
+ });
1982
+ },
1983
+ setRegion: (mapId, layerRef, ...args) => {
1984
+ const target = tilemapResolveLayerMutationTarget(mapId, layerRef);
1985
+ if (!target.ok) return tilemapMutationFailResult(target.error, target.reasonCode);
1986
+ const region = tilemapParseRegionArgs(args, 0);
1987
+ if (!region) return tilemapMutationFailResult('invalid tilemap region args', 'invalid_region_args');
1988
+ const gid = tilemapParseNonNegGid(args[region.nextIndex]);
1989
+ if (gid == null) return tilemapMutationFailResult('invalid tile gid args', 'invalid_gid_args');
1990
+ if (gid > 0 && !tilemapResolveTilesetForGid(target.map.tilesets, gid)) {
1991
+ return tilemapMutationFailResult('invalid tile gid args', 'unmapped_layer_gid');
1992
+ }
1993
+ const mutation = tilemapApplyRegionMutation(target.layer, region, () => gid);
1994
+ return tilemapMutationOkResult({
1995
+ mapId: target.mapId,
1996
+ layerName: target.layer.name,
1997
+ layerIndex: target.layerIndex,
1998
+ gid,
1999
+ ...mutation,
2000
+ });
2001
+ },
2002
+ removeRegion: (mapId, layerRef, ...args) => {
2003
+ const target = tilemapResolveLayerMutationTarget(mapId, layerRef);
2004
+ if (!target.ok) return tilemapMutationFailResult(target.error, target.reasonCode);
2005
+ const region = tilemapParseRegionArgs(args, 0);
2006
+ if (!region) return tilemapMutationFailResult('invalid tilemap region args', 'invalid_region_args');
2007
+ const mutation = tilemapApplyRegionMutation(target.layer, region, () => 0);
2008
+ return tilemapMutationOkResult({
2009
+ mapId: target.mapId,
2010
+ layerName: target.layer.name,
2011
+ layerIndex: target.layerIndex,
2012
+ gid: 0,
2013
+ ...mutation,
2014
+ });
2015
+ },
2016
+ replaceRegion: (mapId, layerRef, ...args) => {
2017
+ const target = tilemapResolveLayerMutationTarget(mapId, layerRef);
2018
+ if (!target.ok) return tilemapMutationFailResult(target.error, target.reasonCode);
2019
+ const region = tilemapParseRegionArgs(args, 0);
2020
+ if (!region) return tilemapMutationFailResult('invalid tilemap region args', 'invalid_region_args');
2021
+ const fromGid = tilemapParseNonNegGid(args[region.nextIndex]);
2022
+ const toGid = tilemapParseNonNegGid(args[region.nextIndex + 1]);
2023
+ if (fromGid == null || toGid == null) {
2024
+ return tilemapMutationFailResult('invalid replace gid args', 'invalid_replace_args');
2025
+ }
2026
+ if (toGid > 0 && !tilemapResolveTilesetForGid(target.map.tilesets, toGid)) {
2027
+ return tilemapMutationFailResult('invalid replace gid args', 'unmapped_layer_gid');
2028
+ }
2029
+ const mutation = tilemapApplyRegionMutation(
2030
+ target.layer,
2031
+ region,
2032
+ (previousGid) => (previousGid === fromGid ? toGid : previousGid),
2033
+ );
2034
+ return tilemapMutationOkResult({
2035
+ mapId: target.mapId,
2036
+ layerName: target.layer.name,
2037
+ layerIndex: target.layerIndex,
2038
+ fromGid,
2039
+ toGid,
2040
+ replacedTiles: mutation.changedTiles,
2041
+ ...mutation,
2042
+ });
2043
+ },
2044
+ setTileCollision: (mapId, layerRef, ...args) => {
2045
+ const target = tilemapResolveLayerMutationTarget(mapId, layerRef);
2046
+ if (!target.ok) return tilemapMutationFailResult(target.error, target.reasonCode);
2047
+ const region = tilemapParseTileOrRegionArgs(args, 0);
2048
+ if (!region) return tilemapMutationFailResult('invalid tile collision args', 'invalid_tile_collision_args');
2049
+ const nextCollision = tilemapParseBooleanFlag(args[region.nextIndex]);
2050
+ if (nextCollision == null) {
2051
+ return tilemapMutationFailResult('invalid tile collision flag', 'invalid_collision_flag');
2052
+ }
2053
+ const overrides = tilemapEnsureCollisionOverrideMap(target.layer);
2054
+ const startX = Math.max(0, region.x);
2055
+ const startY = Math.max(0, region.y);
2056
+ const endX = Math.min(target.layer.width, region.x + region.w);
2057
+ const endY = Math.min(target.layer.height, region.y + region.h);
2058
+ let mutatedTiles = 0;
2059
+ let changedTiles = 0;
2060
+
2061
+ for (let y = startY; y < endY; y += 1) {
2062
+ for (let x = startX; x < endX; x += 1) {
2063
+ mutatedTiles += 1;
2064
+ const index = (y * target.layer.width) + x;
2065
+ const previous = overrides.has(index) ? overrides.get(index) : null;
2066
+ if (previous === nextCollision) continue;
2067
+ overrides.set(index, nextCollision);
2068
+ changedTiles += 1;
2069
+ }
2070
+ }
2071
+
2072
+ return tilemapMutationOkResult({
2073
+ mapId: target.mapId,
2074
+ layerName: target.layer.name,
2075
+ layerIndex: target.layerIndex,
2076
+ collision: nextCollision,
2077
+ mutatedTiles,
2078
+ changedTiles,
2079
+ requestedRegion: { x: region.x, y: region.y, w: region.w, h: region.h },
2080
+ appliedRegion: {
2081
+ x: startX,
2082
+ y: startY,
2083
+ w: Math.max(0, endX - startX),
2084
+ h: Math.max(0, endY - startY),
2085
+ },
2086
+ });
2087
+ },
2088
+ setLayerFlags: (mapId, layerRef, flags = {}) => {
2089
+ const target = tilemapResolveLayerMutationTarget(mapId, layerRef);
2090
+ if (!target.ok) return tilemapMutationFailResult(target.error, target.reasonCode);
2091
+ if (!flags || typeof flags !== 'object') {
2092
+ return tilemapMutationFailResult('invalid layer flags args', 'invalid_layer_flags');
2093
+ }
2094
+ const hasVisible = Object.hasOwn(flags, 'visible');
2095
+ const hasCollision = Object.hasOwn(flags, 'collision');
2096
+ if (!hasVisible && !hasCollision) {
2097
+ return tilemapMutationFailResult('invalid layer flags args', 'invalid_layer_flags');
2098
+ }
2099
+
2100
+ const nextVisible = hasVisible ? tilemapParseBooleanFlag(flags.visible) : target.layer.visible;
2101
+ const nextCollision = hasCollision ? tilemapParseBooleanFlag(flags.collision) : (target.layer.queryEnabled !== false);
2102
+ if (hasVisible && nextVisible == null) {
2103
+ return tilemapMutationFailResult('invalid layer visibility flag', 'invalid_visibility_flag');
2104
+ }
2105
+ if (hasCollision && nextCollision == null) {
2106
+ return tilemapMutationFailResult('invalid layer collision flag', 'invalid_collision_flag');
2107
+ }
2108
+
2109
+ const changed = (target.layer.visible !== nextVisible)
2110
+ || ((target.layer.queryEnabled !== false) !== nextCollision);
2111
+ target.layer.visible = nextVisible;
2112
+ target.layer.collision = nextCollision;
2113
+ target.layer.queryEnabled = nextCollision;
2114
+
2115
+ return tilemapMutationOkResult({
2116
+ mapId: target.mapId,
2117
+ layerName: target.layer.name,
2118
+ layerIndex: target.layerIndex,
2119
+ visible: target.layer.visible,
2120
+ collision: target.layer.queryEnabled !== false,
2121
+ changed,
2122
+ mutatedTiles: 0,
2123
+ changedTiles: 0,
2124
+ });
2125
+ },
2126
+ setLayerVisibility: (mapId, layerRef, visible) => tilemap.setLayerFlags(
2127
+ mapId,
2128
+ layerRef,
2129
+ { visible },
2130
+ ),
2131
+ setLayerCollision: (mapId, layerRef, collision) => tilemap.setLayerFlags(
2132
+ mapId,
2133
+ layerRef,
2134
+ { collision },
2135
+ ),
2136
+ queryObjects: (mapId, options = null) => {
2137
+ const map = tilemapResolveMap(mapId);
2138
+ if (!map) return tilemapQueryFailResult('invalid tilemap handle', 'invalid_map_handle');
2139
+ const result = tilemapCollectFilteredObjectHits(map, options, null);
2140
+ if (!result.ok) return tilemapQueryFailResult(result.error, result.reasonCode);
2141
+ return tilemapQueryOkResult({
2142
+ hit: result.hits.length > 0,
2143
+ count: result.hits.length,
2144
+ hits: result.hits,
2145
+ });
2146
+ },
2147
+ queryObjectsAtPoint: (mapId, ...args) => {
2148
+ const map = tilemapResolveMap(mapId);
2149
+ if (!map) return tilemapQueryFailResult('invalid tilemap handle', 'invalid_map_handle');
2150
+ const point = tilemapParsePointArgsWithNext(args, 0);
2151
+ if (!point) return tilemapQueryFailResult('invalid object point query args', 'invalid_object_point_args');
2152
+ const options = args[point.nextIndex] ?? null;
2153
+ const result = tilemapCollectFilteredObjectHits(
2154
+ map,
2155
+ options,
2156
+ (object) => tilemapPointInObject(object, point),
2157
+ );
2158
+ if (!result.ok) return tilemapQueryFailResult(result.error, result.reasonCode);
2159
+ return tilemapQueryOkResult({
2160
+ hit: result.hits.length > 0,
2161
+ count: result.hits.length,
2162
+ hits: result.hits,
2163
+ point: { x: point.x, y: point.y },
2164
+ });
2165
+ },
2166
+ queryObjectsInAABB: (mapId, ...args) => {
2167
+ const map = tilemapResolveMap(mapId);
2168
+ if (!map) return tilemapQueryFailResult('invalid tilemap handle', 'invalid_map_handle');
2169
+ const rect = tilemapParseAabbArgsWithNext(args, 0);
2170
+ if (!rect || rect.w <= 0 || rect.h <= 0) {
2171
+ return tilemapQueryFailResult('invalid object aabb query args', 'invalid_object_aabb_args');
2172
+ }
2173
+ const options = args[rect.nextIndex] ?? null;
2174
+ const result = tilemapCollectFilteredObjectHits(
2175
+ map,
2176
+ options,
2177
+ (object) => tilemapAabbIntersectsObject(object, rect),
2178
+ );
2179
+ if (!result.ok) return tilemapQueryFailResult(result.error, result.reasonCode);
2180
+ return tilemapQueryOkResult({
2181
+ hit: result.hits.length > 0,
2182
+ count: result.hits.length,
2183
+ hits: result.hits,
2184
+ range: {
2185
+ x: rect.x,
2186
+ y: rect.y,
2187
+ w: rect.w,
2188
+ h: rect.h,
2189
+ },
2190
+ });
2191
+ },
2192
+ };
2193
+
2194
+ const ecs = {
2195
+ createEntity: () => {
2196
+ const id = nextEcsEntityId++;
2197
+ ecsEntities.add(id);
2198
+ return id;
2199
+ },
2200
+ removeEntity: (entityId) => {
2201
+ const id = normalizeEntityId(entityId);
2202
+ if (id == null || !ecsEntities.has(id)) return false;
2203
+ ecsEntities.delete(id);
2204
+ for (const store of ecsComponentStores.values()) {
2205
+ store.delete(id);
2206
+ }
2207
+ return true;
2208
+ },
2209
+ addComponent: (entityId, componentName, componentValue) => {
2210
+ const id = normalizeEntityId(entityId);
2211
+ const name = normalizeComponentName(componentName);
2212
+ if (id == null || name == null || !ecsEntities.has(id)) return false;
2213
+ if (!ecsComponentStores.has(name)) {
2214
+ ecsComponentStores.set(name, new Map());
2215
+ }
2216
+ ecsComponentStores.get(name).set(id, componentValue);
2217
+ return true;
2218
+ },
2219
+ getComponent: (entityId, componentName) => {
2220
+ const id = normalizeEntityId(entityId);
2221
+ const name = normalizeComponentName(componentName);
2222
+ if (id == null || name == null) return undefined;
2223
+ const store = ecsComponentStores.get(name);
2224
+ return store ? store.get(id) : undefined;
2225
+ },
2226
+ system: (name, fn, order = 0) => {
2227
+ const normalizedName = normalizeSystemName(name);
2228
+ if (normalizedName == null || typeof fn !== 'function') return false;
2229
+ const normalizedOrder = Number.isFinite(order) ? Number(order) : 0;
2230
+ const existing = ecsSystems.find((entry) => entry.name === normalizedName);
2231
+ if (existing) {
2232
+ existing.fn = fn;
2233
+ existing.order = normalizedOrder;
2234
+ } else {
2235
+ ecsSystems.push({ name: normalizedName, fn, order: normalizedOrder });
2236
+ }
2237
+ sortEcsSystems();
2238
+ return true;
2239
+ },
2240
+ run: (dt) => {
2241
+ for (const entry of ecsSystems) {
2242
+ entry.fn(dt);
2243
+ }
2244
+ },
2245
+ };
2246
+
2247
+ return {
2248
+ ecs,
2249
+ scene,
2250
+ scene3d,
2251
+ tilemap,
2252
+ };
2253
+ }