@auraindustry/aurajs 0.0.6 → 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 +103 -7
  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,1514 @@
1
+ import { existsSync } from 'node:fs';
2
+
3
+ import {
4
+ GAME_ACTION_SCHEMA_VERSION,
5
+ GAME_ACTION_REQUEST_SCHEMA_VERSION,
6
+ GAME_ACTION_RESULT_SCHEMA_VERSION,
7
+ createGameActionRuntimeHooks,
8
+ } from '../game-action-runtime.mjs';
9
+ import {
10
+ GAME_STATE_SCHEMA_VERSION,
11
+ createGameStateRuntimeHooks,
12
+ } from '../game-state-runtime.mjs';
13
+ import { createHeadlessAnimationRuntime } from './runtime-animation.mjs';
14
+ import { createHeadlessPrimitiveRuntime } from './runtime-primitives.mjs';
15
+ import { createHeadlessWorldRuntime } from './runtime-world.mjs';
16
+
17
+ export function createRuntimeContext(aura, testState) {
18
+ const consoleShim = {
19
+ log: (...args) => {
20
+ testState.logs.push(['log', ...args]);
21
+ console.log(...args);
22
+ },
23
+ warn: (...args) => {
24
+ testState.logs.push(['warn', ...args]);
25
+ console.warn(...args);
26
+ },
27
+ error: (...args) => {
28
+ testState.logs.push(['error', ...args]);
29
+ console.error(...args);
30
+ },
31
+ };
32
+
33
+ return {
34
+ aura,
35
+ console: consoleShim,
36
+ Math,
37
+ Date,
38
+ setTimeout,
39
+ clearTimeout,
40
+ setInterval,
41
+ clearInterval,
42
+ performance: { now: () => Date.now() },
43
+ };
44
+ }
45
+
46
+ export function createHeadlessAura({ width, height, testState, projectRoot = process.cwd() }) {
47
+ let nextDraw2dRenderTargetHandle = 1;
48
+ const draw2dRenderTargets = new Map();
49
+ const draw2dClipStack = [];
50
+ let activeDraw2dRenderTarget = null;
51
+ let activeDraw2dMaskCapture = null;
52
+
53
+ const drawNoop = () => {
54
+ testState.drawCalls += 1;
55
+ if (activeDraw2dRenderTarget) {
56
+ activeDraw2dRenderTarget.commandCount += 1;
57
+ }
58
+ if (activeDraw2dMaskCapture) {
59
+ activeDraw2dMaskCapture.commandCount += 1;
60
+ }
61
+ };
62
+ const stateNoop = () => {};
63
+
64
+ const {
65
+ resolveHeadlessAssetPath,
66
+ measureHeadlessText,
67
+ vec2,
68
+ vec3,
69
+ collision,
70
+ collide,
71
+ math,
72
+ timer,
73
+ color,
74
+ rgb,
75
+ rgba,
76
+ Color,
77
+ colors,
78
+ } = createHeadlessPrimitiveRuntime({ projectRoot });
79
+ const debugInspectorState = {
80
+ enabled: false,
81
+ frameCount: 0,
82
+ elapsedSeconds: 0,
83
+ };
84
+ const debugInspectorSnapshot = () => ({
85
+ enabled: debugInspectorState.enabled,
86
+ frameCount: debugInspectorState.frameCount,
87
+ frame: {
88
+ fps: 60,
89
+ deltaSeconds: 1 / 60,
90
+ elapsedSeconds: debugInspectorState.elapsedSeconds,
91
+ },
92
+ window: {
93
+ width,
94
+ height,
95
+ pixelRatio: 1,
96
+ },
97
+ queues: {
98
+ draw2dPending: 0,
99
+ debugOverlayPending: 0,
100
+ },
101
+ });
102
+ let assets = null;
103
+ const { ecs, scene, scene3d, tilemap } = createHeadlessWorldRuntime({
104
+ loadJsonAsset: (source) => {
105
+ if (assets && typeof assets.loadJson === 'function') return assets.loadJson(source);
106
+ if (assets && typeof assets.json === 'function') return assets.json(source);
107
+ return {};
108
+ },
109
+ });
110
+ const { animation, anim2d, material } = createHeadlessAnimationRuntime();
111
+
112
+ const clamp01 = (value) => {
113
+ const numeric = Number(value);
114
+ if (!Number.isFinite(numeric)) return 1;
115
+ if (numeric <= 0) return 0;
116
+ if (numeric >= 1) return 1;
117
+ return numeric;
118
+ };
119
+ const toFinite = (value, fallback) => {
120
+ const numeric = Number(value);
121
+ return Number.isFinite(numeric) ? numeric : fallback;
122
+ };
123
+ const toPositive = (value, fallback) => {
124
+ const numeric = Number(value);
125
+ return Number.isFinite(numeric) && numeric > 0 ? numeric : fallback;
126
+ };
127
+ const isObject = (value) => value != null && typeof value === 'object';
128
+
129
+ let cameraBaseX = 0;
130
+ let cameraBaseY = 0;
131
+ let cameraBaseZoom = 1;
132
+ let cameraBaseRotation = 0;
133
+ let cameraShakeX = 0;
134
+ let cameraShakeY = 0;
135
+ let cameraFollowState = null;
136
+ let cameraDeadzone = null;
137
+ let cameraBounds = null;
138
+ let nextCameraEffectId = 1;
139
+ let nextCameraListenerId = 1;
140
+ const cameraEffects = [];
141
+ const cameraEffectListeners = [];
142
+
143
+ const applyCameraBounds = () => {
144
+ if (!cameraBounds) return;
145
+ const maxX = cameraBounds.x + cameraBounds.width;
146
+ const maxY = cameraBounds.y + cameraBounds.height;
147
+ cameraBaseX = Math.max(cameraBounds.x, Math.min(cameraBaseX, maxX));
148
+ cameraBaseY = Math.max(cameraBounds.y, Math.min(cameraBaseY, maxY));
149
+ };
150
+ const normalizeFollowOptions = (options) => {
151
+ if (options == null) {
152
+ return {
153
+ lerpX: 1,
154
+ lerpY: 1,
155
+ offsetX: 0,
156
+ offsetY: 0,
157
+ };
158
+ }
159
+ if (!isObject(options)) return null;
160
+ return {
161
+ lerpX: clamp01(options.lerpX),
162
+ lerpY: clamp01(options.lerpY),
163
+ offsetX: toFinite(options.offsetX, 0),
164
+ offsetY: toFinite(options.offsetY, 0),
165
+ };
166
+ };
167
+ const normalizeDeadzone = (value) => {
168
+ const input = isObject(value) ? value : null;
169
+ const zoneWidth = toFinite(input?.width, Number.NaN);
170
+ const zoneHeight = toFinite(input?.height, Number.NaN);
171
+ if (!(zoneWidth > 0) || !(zoneHeight > 0)) return null;
172
+ return {
173
+ x: toFinite(input?.x, 0),
174
+ y: toFinite(input?.y, 0),
175
+ width: zoneWidth,
176
+ height: zoneHeight,
177
+ };
178
+ };
179
+ const normalizeBounds = (value) => {
180
+ const input = isObject(value) ? value : null;
181
+ const zoneWidth = toFinite(input?.width, Number.NaN);
182
+ const zoneHeight = toFinite(input?.height, Number.NaN);
183
+ if (!(zoneWidth >= 0) || !(zoneHeight >= 0)) return null;
184
+ return {
185
+ x: toFinite(input?.x, 0),
186
+ y: toFinite(input?.y, 0),
187
+ width: zoneWidth,
188
+ height: zoneHeight,
189
+ };
190
+ };
191
+ const resolveFollowTarget = (target) => {
192
+ let source = target;
193
+ if (typeof source === 'function') {
194
+ try {
195
+ source = source();
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+ if (!isObject(source)) return null;
201
+ const x = toFinite(source.x, Number.NaN);
202
+ const y = toFinite(source.y, Number.NaN);
203
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
204
+ return { x, y };
205
+ };
206
+ const emitCameraEffectEvent = (event) => {
207
+ if (cameraEffectListeners.length === 0) return;
208
+ const ordered = [...cameraEffectListeners].sort((a, b) => (a.order - b.order) || (a.id - b.id));
209
+ for (const listener of ordered) {
210
+ try {
211
+ listener.callback(event);
212
+ } catch {
213
+ // Keep deterministic execution if listener throws.
214
+ }
215
+ }
216
+ };
217
+
218
+ const camera = {};
219
+ Object.defineProperties(camera, {
220
+ x: {
221
+ enumerable: true,
222
+ configurable: false,
223
+ get: () => cameraBaseX + cameraShakeX,
224
+ set(value) {
225
+ const numeric = Number(value);
226
+ if (Number.isFinite(numeric)) cameraBaseX = numeric;
227
+ },
228
+ },
229
+ y: {
230
+ enumerable: true,
231
+ configurable: false,
232
+ get: () => cameraBaseY + cameraShakeY,
233
+ set(value) {
234
+ const numeric = Number(value);
235
+ if (Number.isFinite(numeric)) cameraBaseY = numeric;
236
+ },
237
+ },
238
+ zoom: {
239
+ enumerable: true,
240
+ configurable: false,
241
+ get: () => cameraBaseZoom,
242
+ set(value) {
243
+ const numeric = Number(value);
244
+ if (Number.isFinite(numeric) && numeric > 0) cameraBaseZoom = numeric;
245
+ },
246
+ },
247
+ rotation: {
248
+ enumerable: true,
249
+ configurable: false,
250
+ get: () => cameraBaseRotation,
251
+ set(value) {
252
+ const numeric = Number(value);
253
+ if (Number.isFinite(numeric)) cameraBaseRotation = numeric;
254
+ },
255
+ },
256
+ });
257
+
258
+ camera.getState = () => ({
259
+ x: cameraBaseX + cameraShakeX,
260
+ y: cameraBaseY + cameraShakeY,
261
+ zoom: cameraBaseZoom,
262
+ rotation: cameraBaseRotation,
263
+ following: !!cameraFollowState,
264
+ activeEffects: cameraEffects.length,
265
+ deadzone: cameraDeadzone ? { ...cameraDeadzone } : null,
266
+ bounds: cameraBounds ? { ...cameraBounds } : null,
267
+ });
268
+
269
+ camera.follow = (target, options = null) => {
270
+ if (!(typeof target === 'function' || isObject(target))) {
271
+ return { ok: false, reasonCode: 'invalid_follow_target' };
272
+ }
273
+ const normalized = normalizeFollowOptions(options);
274
+ if (!normalized) return { ok: false, reasonCode: 'invalid_follow_options' };
275
+ cameraFollowState = {
276
+ target,
277
+ lerpX: normalized.lerpX,
278
+ lerpY: normalized.lerpY,
279
+ offsetX: normalized.offsetX,
280
+ offsetY: normalized.offsetY,
281
+ };
282
+ return { ok: true, reasonCode: 'camera_follow_started' };
283
+ };
284
+
285
+ camera.stopFollow = () => {
286
+ const stopped = !!cameraFollowState;
287
+ cameraFollowState = null;
288
+ return { ok: true, stopped, reasonCode: 'camera_follow_stopped' };
289
+ };
290
+
291
+ camera.setDeadzone = (value, maybeHeight) => {
292
+ const normalized = isObject(value)
293
+ ? normalizeDeadzone(value)
294
+ : normalizeDeadzone({ width: value, height: maybeHeight });
295
+ if (!normalized) return { ok: false, reasonCode: 'invalid_deadzone' };
296
+ cameraDeadzone = normalized;
297
+ return { ok: true, reasonCode: 'camera_deadzone_set' };
298
+ };
299
+
300
+ camera.clearDeadzone = () => {
301
+ cameraDeadzone = null;
302
+ return { ok: true, reasonCode: 'camera_deadzone_cleared' };
303
+ };
304
+
305
+ camera.setBounds = (xOrBounds, y, boundsWidth, boundsHeight) => {
306
+ const normalized = isObject(xOrBounds)
307
+ ? normalizeBounds(xOrBounds)
308
+ : normalizeBounds({ x: xOrBounds, y, width: boundsWidth, height: boundsHeight });
309
+ if (!normalized) return { ok: false, reasonCode: 'invalid_bounds' };
310
+ cameraBounds = normalized;
311
+ applyCameraBounds();
312
+ return { ok: true, reasonCode: 'camera_bounds_set' };
313
+ };
314
+
315
+ camera.clearBounds = () => {
316
+ cameraBounds = null;
317
+ return { ok: true, reasonCode: 'camera_bounds_cleared' };
318
+ };
319
+
320
+ camera.pan = (x, y, options = null) => {
321
+ const targetX = Number(x);
322
+ const targetY = Number(y);
323
+ if (!Number.isFinite(targetX) || !Number.isFinite(targetY)) {
324
+ return { ok: false, reasonCode: 'invalid_pan_target' };
325
+ }
326
+ if (options != null && !isObject(options)) return { ok: false, reasonCode: 'invalid_pan_options' };
327
+ const duration = toPositive(options?.duration, 0.25);
328
+ if (!(duration > 0)) return { ok: false, reasonCode: 'invalid_pan_duration' };
329
+ const effectId = nextCameraEffectId++;
330
+ cameraEffects.push({
331
+ id: effectId,
332
+ type: 'pan',
333
+ duration,
334
+ elapsed: 0,
335
+ startX: cameraBaseX,
336
+ startY: cameraBaseY,
337
+ targetX,
338
+ targetY,
339
+ });
340
+ return { ok: true, effectId, reasonCode: 'camera_pan_started' };
341
+ };
342
+ camera.panTo = camera.pan;
343
+
344
+ camera.zoomTo = (value, options = null) => {
345
+ const targetZoom = Number(value);
346
+ if (!Number.isFinite(targetZoom) || !(targetZoom > 0)) {
347
+ return { ok: false, reasonCode: 'invalid_zoom_target' };
348
+ }
349
+ if (options != null && !isObject(options)) return { ok: false, reasonCode: 'invalid_zoom_options' };
350
+ const duration = toPositive(options?.duration, 0.25);
351
+ if (!(duration > 0)) return { ok: false, reasonCode: 'invalid_zoom_duration' };
352
+ const effectId = nextCameraEffectId++;
353
+ cameraEffects.push({
354
+ id: effectId,
355
+ type: 'zoom',
356
+ duration,
357
+ elapsed: 0,
358
+ startZoom: cameraBaseZoom,
359
+ targetZoom,
360
+ });
361
+ return { ok: true, effectId, reasonCode: 'camera_zoom_started' };
362
+ };
363
+
364
+ camera.rotateTo = (value, options = null) => {
365
+ const targetRotation = Number(value);
366
+ if (!Number.isFinite(targetRotation)) {
367
+ return { ok: false, reasonCode: 'invalid_rotation_target' };
368
+ }
369
+ if (options != null && !isObject(options)) return { ok: false, reasonCode: 'invalid_rotation_options' };
370
+ const duration = toPositive(options?.duration, 0.25);
371
+ if (!(duration > 0)) return { ok: false, reasonCode: 'invalid_rotation_duration' };
372
+ const effectId = nextCameraEffectId++;
373
+ cameraEffects.push({
374
+ id: effectId,
375
+ type: 'rotate',
376
+ duration,
377
+ elapsed: 0,
378
+ startRotation: cameraBaseRotation,
379
+ targetRotation,
380
+ });
381
+ return { ok: true, effectId, reasonCode: 'camera_rotation_started' };
382
+ };
383
+
384
+ camera.shake = (options = null) => {
385
+ if (options == null) options = {};
386
+ if (!isObject(options)) return { ok: false, reasonCode: 'invalid_shake_options' };
387
+ const sharedIntensity = Number.isFinite(Number(options.intensity))
388
+ ? Number(options.intensity)
389
+ : null;
390
+ const intensityX = toFinite(options.intensityX, sharedIntensity != null ? sharedIntensity : 6);
391
+ const intensityY = toFinite(options.intensityY, sharedIntensity != null ? sharedIntensity : 6);
392
+ if (!(intensityX >= 0) || !(intensityY >= 0)) {
393
+ return { ok: false, reasonCode: 'invalid_shake_intensity' };
394
+ }
395
+ const duration = toPositive(options.duration, 0.3);
396
+ if (!(duration > 0)) return { ok: false, reasonCode: 'invalid_shake_duration' };
397
+ const frequency = toPositive(options.frequency, 30);
398
+ if (!(frequency > 0)) return { ok: false, reasonCode: 'invalid_shake_frequency' };
399
+ const effectId = nextCameraEffectId++;
400
+ cameraEffects.push({
401
+ id: effectId,
402
+ type: 'shake',
403
+ duration,
404
+ elapsed: 0,
405
+ intensityX,
406
+ intensityY,
407
+ frequency,
408
+ seed: effectId * 0.61803398875,
409
+ });
410
+ return { ok: true, effectId, reasonCode: 'camera_shake_started' };
411
+ };
412
+
413
+ camera.clearEffects = () => {
414
+ const cleared = cameraEffects.length;
415
+ cameraEffects.length = 0;
416
+ cameraShakeX = 0;
417
+ cameraShakeY = 0;
418
+ return { ok: true, cleared, reasonCode: 'camera_effects_cleared' };
419
+ };
420
+
421
+ camera.onEffectComplete = (callback, order = 0) => {
422
+ if (typeof callback !== 'function') {
423
+ return { ok: false, reasonCode: 'invalid_effect_callback' };
424
+ }
425
+ const listener = {
426
+ id: nextCameraListenerId++,
427
+ callback,
428
+ order: Number.isFinite(Number(order)) ? Number(order) : 0,
429
+ };
430
+ cameraEffectListeners.push(listener);
431
+ return { ok: true, listenerId: listener.id, reasonCode: 'camera_effect_listener_registered' };
432
+ };
433
+
434
+ camera.offEffectComplete = (listenerId) => {
435
+ if (!Number.isInteger(listenerId) || listenerId <= 0) return false;
436
+ const index = cameraEffectListeners.findIndex((entry) => entry.id === listenerId);
437
+ if (index < 0) return false;
438
+ cameraEffectListeners.splice(index, 1);
439
+ return true;
440
+ };
441
+
442
+ camera.update = (dt) => {
443
+ const delta = Number(dt);
444
+ if (!Number.isFinite(delta) || !(delta > 0)) {
445
+ return { ok: false, reasonCode: 'invalid_dt' };
446
+ }
447
+
448
+ cameraShakeX = 0;
449
+ cameraShakeY = 0;
450
+
451
+ if (cameraFollowState) {
452
+ const targetPoint = resolveFollowTarget(cameraFollowState.target);
453
+ if (targetPoint) {
454
+ let targetX = targetPoint.x + cameraFollowState.offsetX;
455
+ let targetY = targetPoint.y + cameraFollowState.offsetY;
456
+
457
+ if (cameraDeadzone) {
458
+ const left = cameraBaseX + cameraDeadzone.x;
459
+ const right = left + cameraDeadzone.width;
460
+ const top = cameraBaseY + cameraDeadzone.y;
461
+ const bottom = top + cameraDeadzone.height;
462
+
463
+ if (targetX < left) targetX = targetX - cameraDeadzone.x;
464
+ else if (targetX > right) targetX = targetX - cameraDeadzone.x - cameraDeadzone.width;
465
+ else targetX = cameraBaseX;
466
+
467
+ if (targetY < top) targetY = targetY - cameraDeadzone.y;
468
+ else if (targetY > bottom) targetY = targetY - cameraDeadzone.y - cameraDeadzone.height;
469
+ else targetY = cameraBaseY;
470
+ }
471
+
472
+ cameraBaseX += (targetX - cameraBaseX) * cameraFollowState.lerpX;
473
+ cameraBaseY += (targetY - cameraBaseY) * cameraFollowState.lerpY;
474
+ }
475
+ }
476
+
477
+ const completedEffects = [];
478
+ for (const effect of cameraEffects) {
479
+ effect.elapsed += delta;
480
+ const progress = effect.duration <= 0 ? 1 : Math.min(effect.elapsed / effect.duration, 1);
481
+ if (effect.type === 'pan') {
482
+ cameraBaseX = effect.startX + ((effect.targetX - effect.startX) * progress);
483
+ cameraBaseY = effect.startY + ((effect.targetY - effect.startY) * progress);
484
+ } else if (effect.type === 'zoom') {
485
+ cameraBaseZoom = effect.startZoom + ((effect.targetZoom - effect.startZoom) * progress);
486
+ } else if (effect.type === 'rotate') {
487
+ cameraBaseRotation = effect.startRotation + ((effect.targetRotation - effect.startRotation) * progress);
488
+ } else if (effect.type === 'shake') {
489
+ const amplitude = 1 - progress;
490
+ const angle = (effect.seed + (effect.elapsed * effect.frequency)) * 6.283185307179586;
491
+ cameraShakeX += Math.sin(angle) * effect.intensityX * amplitude;
492
+ cameraShakeY += Math.cos(angle * 1.17) * effect.intensityY * amplitude;
493
+ }
494
+ if (progress >= 1) completedEffects.push(effect);
495
+ }
496
+
497
+ if (completedEffects.length > 0) {
498
+ for (const completed of completedEffects) {
499
+ const index = cameraEffects.indexOf(completed);
500
+ if (index >= 0) cameraEffects.splice(index, 1);
501
+ }
502
+ completedEffects.sort((a, b) => a.id - b.id);
503
+ for (const completed of completedEffects) {
504
+ emitCameraEffectEvent({
505
+ type: 'effect_complete',
506
+ effectType: completed.type,
507
+ effectId: completed.id,
508
+ reasonCode: 'camera_effect_complete',
509
+ });
510
+ }
511
+ }
512
+
513
+ applyCameraBounds();
514
+
515
+ return {
516
+ ok: true,
517
+ reasonCode: 'camera_updated',
518
+ x: cameraBaseX + cameraShakeX,
519
+ y: cameraBaseY + cameraShakeY,
520
+ zoom: cameraBaseZoom,
521
+ rotation: cameraBaseRotation,
522
+ following: !!cameraFollowState,
523
+ activeEffects: cameraEffects.length,
524
+ };
525
+ };
526
+
527
+ const createHeadlessAudioSurface = () => {
528
+ const normalizeHandle = (value) => {
529
+ const numeric = Number(value);
530
+ if (!Number.isFinite(numeric) || numeric <= 0 || Math.floor(numeric) !== numeric) {
531
+ return null;
532
+ }
533
+ return numeric;
534
+ };
535
+ const normalizeBus = (value) => {
536
+ if (typeof value !== 'string') return null;
537
+ const trimmed = value.trim();
538
+ return trimmed.length > 0 ? trimmed : null;
539
+ };
540
+ const normalizeDuration = (value) => {
541
+ const numeric = Number(value);
542
+ return Number.isFinite(numeric) && numeric > 0 ? numeric : null;
543
+ };
544
+ const clampAudioVolume = (value, fallback = 0) => {
545
+ const numeric = Number(value);
546
+ if (!Number.isFinite(numeric)) return fallback;
547
+ if (numeric < 0) return 0;
548
+ if (numeric > 1) return 1;
549
+ return numeric;
550
+ };
551
+ const resultOk = (extra = {}) => ({ ok: true, reasonCode: null, ...extra });
552
+ const resultErr = (reasonCode, extra = {}) => ({ ok: false, reasonCode, ...extra });
553
+
554
+ let nextHandle = 1;
555
+ const rawTracks = new Map();
556
+
557
+ const rawPlay = () => {
558
+ testState.audioCalls += 1;
559
+ const handle = nextHandle;
560
+ nextHandle += 1;
561
+ rawTracks.set(handle, { paused: false, volume: 1 });
562
+ return handle;
563
+ };
564
+ const rawStop = (handle) => {
565
+ const normalized = normalizeHandle(handle);
566
+ if (normalized == null) return;
567
+ rawTracks.delete(normalized);
568
+ };
569
+ const rawPause = (handle) => {
570
+ const normalized = normalizeHandle(handle);
571
+ if (normalized == null) return;
572
+ const track = rawTracks.get(normalized);
573
+ if (track) track.paused = true;
574
+ };
575
+ const rawResume = (handle) => {
576
+ const normalized = normalizeHandle(handle);
577
+ if (normalized == null) return;
578
+ const track = rawTracks.get(normalized);
579
+ if (track) track.paused = false;
580
+ };
581
+ const rawSetVolume = (handle, volume) => {
582
+ const normalized = normalizeHandle(handle);
583
+ if (normalized == null) return;
584
+ const track = rawTracks.get(normalized);
585
+ if (!track) return;
586
+ track.volume = clampAudioVolume(volume, track.volume);
587
+ };
588
+ const rawSetMasterVolume = (volume, audio) => {
589
+ const numeric = Number(volume);
590
+ if (!Number.isFinite(numeric)) return;
591
+ audio.masterVolume = clampAudioVolume(numeric, audio.masterVolume);
592
+ };
593
+ const rawStopAll = () => {
594
+ rawTracks.clear();
595
+ };
596
+
597
+ const audio = {
598
+ play: rawPlay,
599
+ pause: rawPause,
600
+ resume: rawResume,
601
+ stop: rawStop,
602
+ setVolume: rawSetVolume,
603
+ setMasterVolume(volume) {
604
+ rawSetMasterVolume(volume, audio);
605
+ },
606
+ stopAll: rawStopAll,
607
+ masterVolume: 1,
608
+ };
609
+
610
+ const DEFAULT_BUS = 'master';
611
+ const busVolumes = new Map([[DEFAULT_BUS, 1]]);
612
+ const tracks = new Map();
613
+ const envelopes = [];
614
+ let nextEnvelopeId = 1;
615
+
616
+ const sortedTrackHandles = () => [...tracks.keys()].sort((a, b) => a - b);
617
+ const ensureBus = (bus) => {
618
+ if (!busVolumes.has(bus)) busVolumes.set(bus, 1);
619
+ };
620
+ const busVolume = (bus) => busVolumes.get(bus) ?? 1;
621
+ const effectiveTrackVolume = (track) => clampAudioVolume(track.baseVolume * busVolume(track.bus), 1);
622
+ const applyTrackVolume = (handle) => {
623
+ const track = tracks.get(handle);
624
+ if (!track) return;
625
+ track.effectiveVolume = effectiveTrackVolume(track);
626
+ rawSetVolume(handle, track.effectiveVolume);
627
+ };
628
+ const applyBusVolume = (bus) => {
629
+ for (const handle of sortedTrackHandles()) {
630
+ const track = tracks.get(handle);
631
+ if (!track || track.bus !== bus) continue;
632
+ applyTrackVolume(handle);
633
+ }
634
+ };
635
+ const removeTrackEnvelopes = (handle) => {
636
+ for (let i = envelopes.length - 1; i >= 0; i -= 1) {
637
+ const envelope = envelopes[i];
638
+ if (envelope.kind === 'track' && envelope.handle === handle) {
639
+ envelopes.splice(i, 1);
640
+ }
641
+ }
642
+ };
643
+ const removeBusEnvelopes = (bus) => {
644
+ for (let i = envelopes.length - 1; i >= 0; i -= 1) {
645
+ const envelope = envelopes[i];
646
+ if (envelope.kind === 'bus' && envelope.bus === bus) {
647
+ envelopes.splice(i, 1);
648
+ }
649
+ }
650
+ };
651
+ const dropTrack = (handle) => {
652
+ tracks.delete(handle);
653
+ removeTrackEnvelopes(handle);
654
+ };
655
+ const appendEnvelope = (envelope) => {
656
+ envelope.id = nextEnvelopeId;
657
+ nextEnvelopeId += 1;
658
+ envelopes.push(envelope);
659
+ return envelope.id;
660
+ };
661
+
662
+ const rawPlayMethod = audio.play.bind(audio);
663
+ const rawStopMethod = audio.stop.bind(audio);
664
+ const rawPauseMethod = audio.pause.bind(audio);
665
+ const rawResumeMethod = audio.resume.bind(audio);
666
+ const rawSetVolumeMethod = audio.setVolume.bind(audio);
667
+ const rawSetMasterVolumeMethod = audio.setMasterVolume.bind(audio);
668
+ const rawStopAllMethod = audio.stopAll.bind(audio);
669
+
670
+ audio.play = (path, options = undefined) => {
671
+ const normalizedOptions = options && typeof options === 'object' && !Array.isArray(options)
672
+ ? options
673
+ : null;
674
+ const requestedVolume = normalizedOptions && Object.prototype.hasOwnProperty.call(normalizedOptions, 'volume')
675
+ ? clampAudioVolume(normalizedOptions.volume, 1)
676
+ : 1;
677
+ const normalizedBus = normalizedOptions ? normalizeBus(normalizedOptions.bus) : null;
678
+ const bus = normalizedBus || DEFAULT_BUS;
679
+ ensureBus(bus);
680
+
681
+ let forwardedOptions = options;
682
+ if (normalizedOptions) {
683
+ forwardedOptions = { ...normalizedOptions, volume: requestedVolume };
684
+ delete forwardedOptions.bus;
685
+ }
686
+
687
+ const handle = forwardedOptions === undefined
688
+ ? rawPlayMethod(path)
689
+ : rawPlayMethod(path, forwardedOptions);
690
+ const normalizedHandle = normalizeHandle(handle);
691
+ if (normalizedHandle != null) {
692
+ tracks.set(normalizedHandle, {
693
+ baseVolume: requestedVolume,
694
+ effectiveVolume: requestedVolume,
695
+ bus,
696
+ paused: false,
697
+ });
698
+ applyTrackVolume(normalizedHandle);
699
+ }
700
+ return handle;
701
+ };
702
+
703
+ audio.stop = (handle) => {
704
+ rawStopMethod(handle);
705
+ const normalizedHandle = normalizeHandle(handle);
706
+ if (normalizedHandle != null) {
707
+ dropTrack(normalizedHandle);
708
+ }
709
+ };
710
+
711
+ audio.pause = (handle) => {
712
+ rawPauseMethod(handle);
713
+ const normalizedHandle = normalizeHandle(handle);
714
+ if (normalizedHandle != null && tracks.has(normalizedHandle)) {
715
+ tracks.get(normalizedHandle).paused = true;
716
+ }
717
+ };
718
+
719
+ audio.resume = (handle) => {
720
+ rawResumeMethod(handle);
721
+ const normalizedHandle = normalizeHandle(handle);
722
+ if (normalizedHandle != null && tracks.has(normalizedHandle)) {
723
+ tracks.get(normalizedHandle).paused = false;
724
+ }
725
+ };
726
+
727
+ audio.setVolume = (handle, volume) => {
728
+ rawSetVolumeMethod(handle, volume);
729
+ const normalizedHandle = normalizeHandle(handle);
730
+ if (normalizedHandle == null) return;
731
+ const track = tracks.get(normalizedHandle);
732
+ if (!track) return;
733
+ const numeric = Number(volume);
734
+ if (!Number.isFinite(numeric)) return;
735
+ track.baseVolume = clampAudioVolume(numeric, track.baseVolume);
736
+ applyTrackVolume(normalizedHandle);
737
+ };
738
+
739
+ audio.setMasterVolume = (volume) => {
740
+ rawSetMasterVolumeMethod(volume);
741
+ };
742
+
743
+ audio.stopAll = () => {
744
+ rawStopAllMethod();
745
+ tracks.clear();
746
+ envelopes.length = 0;
747
+ };
748
+
749
+ audio.setBusVolume = (bus, volume) => {
750
+ const normalizedBus = normalizeBus(bus);
751
+ if (!normalizedBus) return resultErr('invalid_bus');
752
+ const numeric = Number(volume);
753
+ if (!Number.isFinite(numeric)) return resultErr('invalid_volume');
754
+ const clamped = clampAudioVolume(numeric, 1);
755
+ ensureBus(normalizedBus);
756
+ busVolumes.set(normalizedBus, clamped);
757
+ applyBusVolume(normalizedBus);
758
+ return resultOk({ bus: normalizedBus, volume: clamped });
759
+ };
760
+
761
+ audio.assignBus = (handle, bus) => {
762
+ const normalizedHandle = normalizeHandle(handle);
763
+ if (normalizedHandle == null) return resultErr('invalid_track_handle');
764
+ const track = tracks.get(normalizedHandle);
765
+ if (!track) return resultErr('missing_track');
766
+ const normalizedBus = normalizeBus(bus);
767
+ if (!normalizedBus) return resultErr('invalid_bus');
768
+ ensureBus(normalizedBus);
769
+ track.bus = normalizedBus;
770
+ applyTrackVolume(normalizedHandle);
771
+ return resultOk({
772
+ handle: normalizedHandle,
773
+ bus: normalizedBus,
774
+ effectiveVolume: track.effectiveVolume,
775
+ });
776
+ };
777
+
778
+ audio.fadeTrack = (handle, options = null) => {
779
+ const normalizedHandle = normalizeHandle(handle);
780
+ if (normalizedHandle == null) return resultErr('invalid_track_handle');
781
+ const track = tracks.get(normalizedHandle);
782
+ if (!track) return resultErr('missing_track');
783
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
784
+ return resultErr('invalid_fade_options');
785
+ }
786
+ const duration = normalizeDuration(options.duration);
787
+ if (duration == null) return resultErr('invalid_duration');
788
+ const targetVolume = Number(options.to);
789
+ if (!Number.isFinite(targetVolume)) return resultErr('invalid_volume');
790
+ const stopOnComplete = options.stopOnComplete === true;
791
+ const target = clampAudioVolume(targetVolume, track.baseVolume);
792
+ removeTrackEnvelopes(normalizedHandle);
793
+ const envelopeId = appendEnvelope({
794
+ kind: 'track',
795
+ handle: normalizedHandle,
796
+ start: track.baseVolume,
797
+ end: target,
798
+ duration,
799
+ elapsed: 0,
800
+ stopOnComplete,
801
+ });
802
+ return resultOk({ envelopeId });
803
+ };
804
+
805
+ audio.fadeBus = (bus, options = null) => {
806
+ const normalizedBus = normalizeBus(bus);
807
+ if (!normalizedBus) return resultErr('invalid_bus');
808
+ if (!busVolumes.has(normalizedBus)) return resultErr('missing_bus');
809
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
810
+ return resultErr('invalid_fade_options');
811
+ }
812
+ const duration = normalizeDuration(options.duration);
813
+ if (duration == null) return resultErr('invalid_duration');
814
+ const targetVolume = Number(options.to);
815
+ if (!Number.isFinite(targetVolume)) return resultErr('invalid_volume');
816
+ const target = clampAudioVolume(targetVolume, busVolume(normalizedBus));
817
+ removeBusEnvelopes(normalizedBus);
818
+ const envelopeId = appendEnvelope({
819
+ kind: 'bus',
820
+ bus: normalizedBus,
821
+ start: busVolume(normalizedBus),
822
+ end: target,
823
+ duration,
824
+ elapsed: 0,
825
+ });
826
+ return resultOk({ envelopeId });
827
+ };
828
+
829
+ audio.crossfade = (fromHandle, toHandle, options = null) => {
830
+ const normalizedFrom = normalizeHandle(fromHandle);
831
+ if (normalizedFrom == null) return resultErr('invalid_from_track_handle');
832
+ const normalizedTo = normalizeHandle(toHandle);
833
+ if (normalizedTo == null) return resultErr('invalid_to_track_handle');
834
+ const fromTrack = tracks.get(normalizedFrom);
835
+ if (!fromTrack) return resultErr('missing_from_track');
836
+ const toTrack = tracks.get(normalizedTo);
837
+ if (!toTrack) return resultErr('missing_to_track');
838
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
839
+ return resultErr('invalid_crossfade_options');
840
+ }
841
+ const duration = normalizeDuration(options.duration);
842
+ if (duration == null) return resultErr('invalid_duration');
843
+
844
+ const fromTargetRaw = Object.prototype.hasOwnProperty.call(options, 'fromVolume')
845
+ ? Number(options.fromVolume)
846
+ : 0;
847
+ if (!Number.isFinite(fromTargetRaw)) return resultErr('invalid_from_volume');
848
+ const toTargetRaw = Object.prototype.hasOwnProperty.call(options, 'toVolume')
849
+ ? Number(options.toVolume)
850
+ : toTrack.baseVolume;
851
+ if (!Number.isFinite(toTargetRaw)) return resultErr('invalid_to_volume');
852
+ const toStartRaw = Object.prototype.hasOwnProperty.call(options, 'toStartVolume')
853
+ ? Number(options.toStartVolume)
854
+ : 0;
855
+ if (!Number.isFinite(toStartRaw)) return resultErr('invalid_to_start_volume');
856
+
857
+ const stopFrom = options.stopFrom !== false;
858
+ const fromTarget = clampAudioVolume(fromTargetRaw, fromTrack.baseVolume);
859
+ const toTarget = clampAudioVolume(toTargetRaw, toTrack.baseVolume);
860
+ const toStart = clampAudioVolume(toStartRaw, 0);
861
+
862
+ toTrack.baseVolume = toStart;
863
+ applyTrackVolume(normalizedTo);
864
+
865
+ removeTrackEnvelopes(normalizedFrom);
866
+ removeTrackEnvelopes(normalizedTo);
867
+ const fromEnvelopeId = appendEnvelope({
868
+ kind: 'track',
869
+ handle: normalizedFrom,
870
+ start: fromTrack.baseVolume,
871
+ end: fromTarget,
872
+ duration,
873
+ elapsed: 0,
874
+ stopOnComplete: stopFrom,
875
+ });
876
+ const toEnvelopeId = appendEnvelope({
877
+ kind: 'track',
878
+ handle: normalizedTo,
879
+ start: toStart,
880
+ end: toTarget,
881
+ duration,
882
+ elapsed: 0,
883
+ stopOnComplete: false,
884
+ });
885
+ return resultOk({ fromEnvelopeId, toEnvelopeId });
886
+ };
887
+
888
+ audio.update = (dt) => {
889
+ const delta = Number(dt);
890
+ if (!Number.isFinite(delta) || !(delta > 0)) {
891
+ return resultErr('invalid_dt');
892
+ }
893
+ if (envelopes.length === 0) {
894
+ return resultOk({ advanced: 0, completed: 0, activeEnvelopes: 0 });
895
+ }
896
+
897
+ const completed = [];
898
+ envelopes.sort((a, b) => a.id - b.id);
899
+
900
+ for (const envelope of envelopes) {
901
+ envelope.elapsed += delta;
902
+ const progress = envelope.duration <= 0 ? 1 : Math.min(envelope.elapsed / envelope.duration, 1);
903
+ const value = envelope.start + ((envelope.end - envelope.start) * progress);
904
+
905
+ if (envelope.kind === 'track') {
906
+ const track = tracks.get(envelope.handle);
907
+ if (!track) {
908
+ completed.push(envelope);
909
+ continue;
910
+ }
911
+ track.baseVolume = clampAudioVolume(value, track.baseVolume);
912
+ applyTrackVolume(envelope.handle);
913
+ } else if (envelope.kind === 'bus') {
914
+ if (!busVolumes.has(envelope.bus)) {
915
+ completed.push(envelope);
916
+ continue;
917
+ }
918
+ busVolumes.set(envelope.bus, clampAudioVolume(value, busVolume(envelope.bus)));
919
+ applyBusVolume(envelope.bus);
920
+ }
921
+
922
+ if (progress >= 1) {
923
+ completed.push(envelope);
924
+ }
925
+ }
926
+
927
+ completed.sort((a, b) => a.id - b.id);
928
+ for (const envelope of completed) {
929
+ const index = envelopes.indexOf(envelope);
930
+ if (index >= 0) envelopes.splice(index, 1);
931
+ if (envelope.kind === 'track' && envelope.stopOnComplete === true && tracks.has(envelope.handle)) {
932
+ audio.stop(envelope.handle);
933
+ }
934
+ }
935
+
936
+ return resultOk({
937
+ advanced: delta,
938
+ completed: completed.length,
939
+ activeEnvelopes: envelopes.length,
940
+ });
941
+ };
942
+
943
+ audio.clearEnvelopes = () => {
944
+ const cleared = envelopes.length;
945
+ envelopes.length = 0;
946
+ return resultOk({ cleared });
947
+ };
948
+
949
+ audio.getMixerState = () => ({
950
+ buses: [...busVolumes.entries()]
951
+ .sort((a, b) => a[0].localeCompare(b[0]))
952
+ .map(([bus, volume]) => ({ bus, volume })),
953
+ tracks: sortedTrackHandles()
954
+ .map((handle) => {
955
+ const track = tracks.get(handle);
956
+ if (!track) return null;
957
+ return {
958
+ handle,
959
+ bus: track.bus,
960
+ baseVolume: track.baseVolume,
961
+ effectiveVolume: track.effectiveVolume,
962
+ paused: track.paused === true,
963
+ };
964
+ })
965
+ .filter(Boolean),
966
+ envelopes: [...envelopes]
967
+ .sort((a, b) => a.id - b.id)
968
+ .map((envelope) => ({
969
+ id: envelope.id,
970
+ kind: envelope.kind,
971
+ handle: envelope.kind === 'track' ? envelope.handle : null,
972
+ bus: envelope.kind === 'bus' ? envelope.bus : null,
973
+ start: envelope.start,
974
+ end: envelope.end,
975
+ duration: envelope.duration,
976
+ elapsed: envelope.elapsed,
977
+ stopOnComplete: envelope.kind === 'track' ? envelope.stopOnComplete === true : false,
978
+ })),
979
+ nextEnvelopeId,
980
+ });
981
+
982
+ return audio;
983
+ };
984
+
985
+ const audio = createHeadlessAudioSurface();
986
+
987
+ const normalizeDraw2dRenderTargetHandle = (value) => {
988
+ const handle = Number(
989
+ value && typeof value === 'object' && value.__renderTarget === true
990
+ ? value.handle
991
+ : value,
992
+ );
993
+ if (!Number.isInteger(handle) || handle <= 0) return null;
994
+ return handle;
995
+ };
996
+ const normalizeClipRect = (xOrRect, y, widthValue, heightValue) => {
997
+ if (isObject(xOrRect)) {
998
+ const normalizedWidth = toFinite(xOrRect.width, Number.NaN);
999
+ const normalizedHeight = toFinite(xOrRect.height, Number.NaN);
1000
+ if (!(normalizedWidth > 0) || !(normalizedHeight > 0)) return null;
1001
+ return {
1002
+ x: toFinite(xOrRect.x, 0),
1003
+ y: toFinite(xOrRect.y, 0),
1004
+ width: normalizedWidth,
1005
+ height: normalizedHeight,
1006
+ };
1007
+ }
1008
+ const normalizedX = toFinite(xOrRect, Number.NaN);
1009
+ const normalizedY = toFinite(y, Number.NaN);
1010
+ const normalizedWidth = toFinite(widthValue, Number.NaN);
1011
+ const normalizedHeight = toFinite(heightValue, Number.NaN);
1012
+ if (![normalizedX, normalizedY, normalizedWidth, normalizedHeight].every(Number.isFinite)) {
1013
+ return null;
1014
+ }
1015
+ if (!(normalizedWidth > 0) || !(normalizedHeight > 0)) return null;
1016
+ return {
1017
+ x: normalizedX,
1018
+ y: normalizedY,
1019
+ width: normalizedWidth,
1020
+ height: normalizedHeight,
1021
+ };
1022
+ };
1023
+ const draw2dOk = (reasonCode, extra = {}) => ({ ok: true, reasonCode, ...extra });
1024
+ const draw2dErr = (reasonCode, extra = {}) => ({ ok: false, reasonCode, ...extra });
1025
+ const normalizeDraw2dSource = (value) => {
1026
+ if (typeof value === 'string') {
1027
+ return value.length > 0 ? value : null;
1028
+ }
1029
+ const handle = normalizeDraw2dRenderTargetHandle(value);
1030
+ if (handle == null) return null;
1031
+ return { handle };
1032
+ };
1033
+ const normalizeSpriteFxShadow = (options) => {
1034
+ if (!options || typeof options !== 'object') return { ok: false, reasonCode: 'missing_sprite_fx_effect' };
1035
+ const shadow = options.shadow;
1036
+ if (shadow == null) return { ok: false, reasonCode: 'missing_sprite_fx_effect' };
1037
+ if (typeof shadow !== 'object') return { ok: false, reasonCode: 'invalid_sprite_fx_shadow' };
1038
+ const offsetX = shadow.offsetX == null ? 2 : Number(shadow.offsetX);
1039
+ const offsetY = shadow.offsetY == null ? 2 : Number(shadow.offsetY);
1040
+ const blur = shadow.blur == null ? 0 : Number(shadow.blur);
1041
+ if (!Number.isFinite(offsetX) || !Number.isFinite(offsetY) || !Number.isFinite(blur)) {
1042
+ return { ok: false, reasonCode: 'invalid_sprite_fx_shadow' };
1043
+ }
1044
+ return { ok: true, shadow: { offsetX, offsetY, blur: Math.max(0, blur) } };
1045
+ };
1046
+ const runSpriteFxNoop = (options = {}) => {
1047
+ const shadowResult = normalizeSpriteFxShadow(options);
1048
+ if (!shadowResult.ok) return shadowResult;
1049
+ let submittedFxDrawCount = 1;
1050
+ if (shadowResult.shadow.blur > 0) {
1051
+ submittedFxDrawCount += 24;
1052
+ }
1053
+ for (let i = 0; i < submittedFxDrawCount; i += 1) {
1054
+ drawNoop();
1055
+ }
1056
+ drawNoop();
1057
+ return { ok: true, submittedFxDrawCount };
1058
+ };
1059
+
1060
+ const aura = {
1061
+ setup: null,
1062
+ update: null,
1063
+ draw: null,
1064
+ onResize: null,
1065
+ onFocus: null,
1066
+ onBlur: null,
1067
+ onQuit: null,
1068
+
1069
+ window: {
1070
+ width,
1071
+ height,
1072
+ pixelRatio: 1,
1073
+ fps: 60,
1074
+ setTitle: () => {},
1075
+ setSize: (w, h) => {
1076
+ aura.window.width = Number(w) || aura.window.width;
1077
+ aura.window.height = Number(h) || aura.window.height;
1078
+ },
1079
+ setFullscreen: () => {},
1080
+ setCursorVisible: () => {},
1081
+ setCursorLocked: () => {},
1082
+ getSize: () => ({ width: aura.window.width, height: aura.window.height }),
1083
+ getPixelRatio: () => aura.window.pixelRatio,
1084
+ getFPS: () => aura.window.fps,
1085
+ close: () => {},
1086
+ },
1087
+
1088
+ draw2d: {
1089
+ clear: drawNoop,
1090
+ rect: drawNoop,
1091
+ rectOutline: drawNoop,
1092
+ circle: drawNoop,
1093
+ circleOutline: drawNoop,
1094
+ line: drawNoop,
1095
+ triangle: drawNoop,
1096
+ image: drawNoop,
1097
+ sprite: drawNoop,
1098
+ tileSprite: drawNoop,
1099
+ nineSlice: drawNoop,
1100
+ text: drawNoop,
1101
+ measureText: measureHeadlessText,
1102
+ push: drawNoop,
1103
+ pop: drawNoop,
1104
+ pushTransform: drawNoop,
1105
+ popTransform: drawNoop,
1106
+ translate: drawNoop,
1107
+ rotate: drawNoop,
1108
+ scale: drawNoop,
1109
+ createRenderTarget: (widthValue, heightValue) => {
1110
+ const normalizedWidth = Number(widthValue);
1111
+ const normalizedHeight = Number(heightValue);
1112
+ if (!Number.isFinite(normalizedWidth) || normalizedWidth <= 0) {
1113
+ return draw2dErr('invalid_render_target_width');
1114
+ }
1115
+ if (!Number.isFinite(normalizedHeight) || normalizedHeight <= 0) {
1116
+ return draw2dErr('invalid_render_target_height');
1117
+ }
1118
+ const handle = nextDraw2dRenderTargetHandle++;
1119
+ const target = {
1120
+ handle,
1121
+ width: Math.floor(normalizedWidth),
1122
+ height: Math.floor(normalizedHeight),
1123
+ };
1124
+ draw2dRenderTargets.set(handle, target);
1125
+ return draw2dOk('draw2d_render_target_created', {
1126
+ handle,
1127
+ width: target.width,
1128
+ height: target.height,
1129
+ created: true,
1130
+ type: 'renderTarget',
1131
+ __renderTarget: true,
1132
+ });
1133
+ },
1134
+ resizeRenderTarget: (targetValue, widthValue, heightValue) => {
1135
+ const handle = normalizeDraw2dRenderTargetHandle(targetValue);
1136
+ if (handle == null) return draw2dErr('invalid_render_target_handle');
1137
+ const target = draw2dRenderTargets.get(handle);
1138
+ if (!target) return draw2dErr('missing_render_target');
1139
+ const normalizedWidth = Number(widthValue);
1140
+ const normalizedHeight = Number(heightValue);
1141
+ if (!Number.isFinite(normalizedWidth) || normalizedWidth <= 0) {
1142
+ return draw2dErr('invalid_render_target_width');
1143
+ }
1144
+ if (!Number.isFinite(normalizedHeight) || normalizedHeight <= 0) {
1145
+ return draw2dErr('invalid_render_target_height');
1146
+ }
1147
+ const width = Math.floor(normalizedWidth);
1148
+ const height = Math.floor(normalizedHeight);
1149
+ const resized = target.width !== width || target.height !== height;
1150
+ target.width = width;
1151
+ target.height = height;
1152
+ return draw2dOk('draw2d_render_target_resized', { handle, width, height, resized });
1153
+ },
1154
+ destroyRenderTarget: (targetValue) => {
1155
+ const handle = normalizeDraw2dRenderTargetHandle(targetValue);
1156
+ if (handle == null) return draw2dErr('invalid_render_target_handle');
1157
+ const destroyed = draw2dRenderTargets.delete(handle);
1158
+ if (!destroyed) return draw2dErr('missing_render_target');
1159
+ if (activeDraw2dRenderTarget && activeDraw2dRenderTarget.handle === handle) {
1160
+ activeDraw2dRenderTarget = null;
1161
+ }
1162
+ return draw2dOk('draw2d_render_target_destroyed', { handle, destroyed: true });
1163
+ },
1164
+ withRenderTarget: (targetValue, callback) => {
1165
+ const handle = normalizeDraw2dRenderTargetHandle(targetValue);
1166
+ if (handle == null) return draw2dErr('invalid_render_target_handle');
1167
+ if (!draw2dRenderTargets.has(handle)) return draw2dErr('missing_render_target');
1168
+ if (typeof callback !== 'function') return draw2dErr('invalid_render_target_callback');
1169
+ if (activeDraw2dRenderTarget) return draw2dErr('nested_render_target_capture');
1170
+ const savedClipDepth = draw2dClipStack.length;
1171
+ activeDraw2dRenderTarget = { handle, commandCount: 0 };
1172
+ draw2dClipStack.length = 0;
1173
+ try {
1174
+ callback();
1175
+ } catch {
1176
+ activeDraw2dRenderTarget = null;
1177
+ draw2dClipStack.length = savedClipDepth;
1178
+ return draw2dErr('render_target_callback_threw');
1179
+ }
1180
+ const commandCount = activeDraw2dRenderTarget.commandCount;
1181
+ activeDraw2dRenderTarget = null;
1182
+ draw2dClipStack.length = savedClipDepth;
1183
+ return draw2dOk('draw2d_render_target_captured', { handle, commandCount });
1184
+ },
1185
+ withRenderTargets: (stages) => {
1186
+ if (!Array.isArray(stages) || stages.length === 0) return draw2dErr('invalid_render_target_stages');
1187
+ const handles = [];
1188
+ let commandCount = 0;
1189
+ const seenHandles = new Set();
1190
+ for (const stage of stages) {
1191
+ if (!stage || typeof stage !== 'object') return draw2dErr('invalid_render_target_stages');
1192
+ const handle = normalizeDraw2dRenderTargetHandle(stage.target);
1193
+ if (handle == null) return draw2dErr('invalid_render_target_handle');
1194
+ if (!draw2dRenderTargets.has(handle)) return draw2dErr('missing_render_target');
1195
+ if (typeof stage.draw !== 'function') return draw2dErr('invalid_render_target_stages');
1196
+ if (seenHandles.has(handle)) return draw2dErr('duplicate_render_target_stage');
1197
+ seenHandles.add(handle);
1198
+ const result = aura.draw2d.withRenderTarget(handle, stage.draw);
1199
+ if (!result?.ok) return result;
1200
+ handles.push(handle);
1201
+ commandCount += Number(result.commandCount || 0);
1202
+ }
1203
+ return draw2dOk('draw2d_render_targets_captured', {
1204
+ stageCount: handles.length,
1205
+ commandCount,
1206
+ handles,
1207
+ });
1208
+ },
1209
+ withMask: (sourceValue, xValue, yValue, widthValue, heightValue, callback) => {
1210
+ const source = normalizeDraw2dSource(sourceValue);
1211
+ if (!source) return draw2dErr('invalid_mask_source');
1212
+ if (typeof source === 'object' && !draw2dRenderTargets.has(source.handle)) {
1213
+ return draw2dErr('invalid_mask_source_handle');
1214
+ }
1215
+ if (!Number.isFinite(Number(xValue))) return draw2dErr('invalid_mask_x');
1216
+ if (!Number.isFinite(Number(yValue))) return draw2dErr('invalid_mask_y');
1217
+ if (!Number.isFinite(Number(widthValue)) || Number(widthValue) <= 0) return draw2dErr('invalid_mask_width');
1218
+ if (!Number.isFinite(Number(heightValue)) || Number(heightValue) <= 0) return draw2dErr('invalid_mask_height');
1219
+ if (typeof callback !== 'function') return draw2dErr('invalid_mask_callback');
1220
+ if (activeDraw2dRenderTarget) return draw2dErr('mask_capture_inside_render_target_unsupported');
1221
+ const savedClipDepth = draw2dClipStack.length;
1222
+ activeDraw2dMaskCapture = { commandCount: 0 };
1223
+ draw2dClipStack.length = 0;
1224
+ try {
1225
+ callback();
1226
+ } catch {
1227
+ activeDraw2dMaskCapture = null;
1228
+ draw2dClipStack.length = savedClipDepth;
1229
+ return draw2dErr('mask_callback_threw');
1230
+ }
1231
+ const commandCount = activeDraw2dMaskCapture.commandCount;
1232
+ activeDraw2dMaskCapture = null;
1233
+ draw2dClipStack.length = savedClipDepth;
1234
+ drawNoop();
1235
+ return draw2dOk('draw2d_mask_captured', { commandCount });
1236
+ },
1237
+ spriteFx: (sourceValue, xValue, yValue, options = {}) => {
1238
+ const source = normalizeDraw2dSource(sourceValue);
1239
+ if (!source) return draw2dErr('invalid_sprite_fx_source');
1240
+ if (typeof source === 'object' && !draw2dRenderTargets.has(source.handle)) {
1241
+ return draw2dErr('invalid_sprite_fx_source_handle');
1242
+ }
1243
+ if (!Number.isFinite(Number(xValue))) return draw2dErr('invalid_sprite_fx_x');
1244
+ if (!Number.isFinite(Number(yValue))) return draw2dErr('invalid_sprite_fx_y');
1245
+ const result = runSpriteFxNoop(options);
1246
+ if (!result.ok) return draw2dErr(result.reasonCode);
1247
+ return draw2dOk('draw2d_sprite_fx_queued', {
1248
+ submittedFxDrawCount: result.submittedFxDrawCount,
1249
+ });
1250
+ },
1251
+ pushClipRect: (xOrRect, y, widthValue, heightValue) => {
1252
+ const clipRect = normalizeClipRect(xOrRect, y, widthValue, heightValue);
1253
+ if (!clipRect) return draw2dErr('invalid_clip_rect');
1254
+ draw2dClipStack.push(clipRect);
1255
+ return draw2dOk('draw2d_clip_pushed');
1256
+ },
1257
+ popClipRect: () => {
1258
+ if (draw2dClipStack.length === 0) return draw2dErr('draw2d_clip_stack_underflow');
1259
+ draw2dClipStack.pop();
1260
+ return draw2dOk('draw2d_clip_popped');
1261
+ },
1262
+ },
1263
+
1264
+ draw3d: {
1265
+ drawMesh: drawNoop,
1266
+ clear3d: drawNoop,
1267
+ drawSkybox: drawNoop,
1268
+ billboard: drawNoop,
1269
+ mesh: drawNoop,
1270
+ setEnvironmentMap: stateNoop,
1271
+ setFog: stateNoop,
1272
+ clearFog: stateNoop,
1273
+ },
1274
+
1275
+ material,
1276
+
1277
+ audio,
1278
+
1279
+ audio,
1280
+
1281
+ assets: {
1282
+ load: async () => {},
1283
+ exists: (name) => {
1284
+ const assetPath = resolveHeadlessAssetPath(name);
1285
+ return assetPath ? existsSync(assetPath) : false;
1286
+ },
1287
+ image: (name) => ({ kind: 'image', name }),
1288
+ sound: (name) => ({ kind: 'sound', name }),
1289
+ mesh: (name) => ({ kind: 'mesh', name }),
1290
+ json: () => ({}),
1291
+ text: () => '',
1292
+ bytes: () => new Uint8Array(),
1293
+ },
1294
+
1295
+ input: {
1296
+ isDown: () => false,
1297
+ isPressed: () => false,
1298
+ isReleased: () => false,
1299
+ isKeyDown: (...args) => aura.input.isDown(...args),
1300
+ isKeyPressed: (...args) => aura.input.isPressed(...args),
1301
+ isKeyReleased: (...args) => aura.input.isReleased(...args),
1302
+ isMouseDown: (...args) => aura.input.mouse.isDown(...args),
1303
+ isMousePressed: (...args) => aura.input.mouse.isPressed(...args),
1304
+ isMouseReleased: (...args) => aura.input.mouse.isReleased(...args),
1305
+ getMousePosition: () => ({ x: aura.input.mouse.x, y: aura.input.mouse.y }),
1306
+ getMouseDelta: () => ({ x: 0, y: 0 }),
1307
+ isGamepadConnected: () => Boolean(aura.input.gamepad.connected),
1308
+ mouse: {
1309
+ x: 0,
1310
+ y: 0,
1311
+ scroll: 0,
1312
+ isDown: () => false,
1313
+ isPressed: () => false,
1314
+ isReleased: () => false,
1315
+ },
1316
+ gamepad: {
1317
+ connected: false,
1318
+ axis: () => 0,
1319
+ isDown: () => false,
1320
+ isPressed: () => false,
1321
+ isReleased: () => false,
1322
+ rumble: () => {},
1323
+ },
1324
+ },
1325
+
1326
+ storage: {
1327
+ _store: new Map(),
1328
+ set(key, value) {
1329
+ this._store.set(key, value);
1330
+ },
1331
+ get(key, fallback = null) {
1332
+ return this._store.has(key) ? this._store.get(key) : fallback;
1333
+ },
1334
+ delete(key) {
1335
+ this._store.delete(key);
1336
+ },
1337
+ },
1338
+
1339
+ math,
1340
+
1341
+ timer,
1342
+
1343
+ collision,
1344
+
1345
+ collision,
1346
+ collide,
1347
+
1348
+ ecs,
1349
+
1350
+ animation,
1351
+
1352
+ animation,
1353
+
1354
+ anim2d,
1355
+
1356
+ scene,
1357
+
1358
+ scene,
1359
+ scene3d,
1360
+ tilemap,
1361
+
1362
+ test: {
1363
+ assert(condition, message = 'Assertion failed') {
1364
+ if (!condition) {
1365
+ testState.failures.push(message);
1366
+ throw new Error(message);
1367
+ }
1368
+ testState.passes += 1;
1369
+ },
1370
+ equal(actual, expected, message = null) {
1371
+ if (!Object.is(actual, expected)) {
1372
+ const detail = message || `Expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`;
1373
+ testState.failures.push(detail);
1374
+ throw new Error(detail);
1375
+ }
1376
+ testState.passes += 1;
1377
+ },
1378
+ fail(message = 'Test failed') {
1379
+ testState.failures.push(message);
1380
+ throw new Error(message);
1381
+ },
1382
+ },
1383
+
1384
+ debug: {
1385
+ log: (...args) => console.log(...args),
1386
+ drawRect: () => {
1387
+ testState.drawCalls += 1;
1388
+ },
1389
+ drawCircle: () => {
1390
+ testState.drawCalls += 1;
1391
+ },
1392
+ drawText: () => {
1393
+ testState.drawCalls += 1;
1394
+ },
1395
+ cacheStats: () => ({
1396
+ assets: {
1397
+ entryCount: 0,
1398
+ estimatedBytes: 0,
1399
+ handleCount: 0,
1400
+ nextHandleId: 1,
1401
+ cacheHitCount: 0,
1402
+ cacheMissCount: 0,
1403
+ resetCount: 0,
1404
+ lastResetSequence: 0,
1405
+ },
1406
+ audio: {
1407
+ decodedCacheEntries: 0,
1408
+ decodedSampleCount: 0,
1409
+ activeHandleCount: 0,
1410
+ nextHandleId: 1,
1411
+ decodedCacheHitCount: 0,
1412
+ decodedCacheMissCount: 0,
1413
+ resetCount: 0,
1414
+ lastResetSequence: 0,
1415
+ },
1416
+ }),
1417
+ resetCaches: () => {},
1418
+ enableInspector: (enabled = true) => {
1419
+ debugInspectorState.enabled = enabled !== false;
1420
+ if (!debugInspectorState.enabled) {
1421
+ debugInspectorState.frameCount = 0;
1422
+ debugInspectorState.elapsedSeconds = 0;
1423
+ }
1424
+ return debugInspectorState.enabled;
1425
+ },
1426
+ inspectorStats: () => debugInspectorSnapshot(),
1427
+ __inspectorTick: (dt) => {
1428
+ if (!debugInspectorState.enabled) return;
1429
+ debugInspectorState.frameCount += 1;
1430
+ if (Number.isFinite(dt) && dt > 0) {
1431
+ debugInspectorState.elapsedSeconds += Number(dt);
1432
+ }
1433
+ },
1434
+ },
1435
+
1436
+ color,
1437
+ rgb,
1438
+ rgba,
1439
+ Color,
1440
+ colors,
1441
+ camera,
1442
+ camera,
1443
+ camera3d: { position: [0, 0, 0], fov: 60, lookAt: () => {} },
1444
+ light: { ambient: () => {}, directional: () => {}, point: () => {} },
1445
+ vec2,
1446
+ vec3,
1447
+ };
1448
+
1449
+ assets = aura.assets;
1450
+
1451
+ const gameStateHooks = createGameStateRuntimeHooks({ aura, mode: 'headless' });
1452
+ const gameActionHooks = createGameActionRuntimeHooks({ aura });
1453
+ aura.action = {
1454
+ schemaVersion: GAME_ACTION_SCHEMA_VERSION,
1455
+ requestSchemaVersion: GAME_ACTION_REQUEST_SCHEMA_VERSION,
1456
+ resultSchemaVersion: GAME_ACTION_RESULT_SCHEMA_VERSION,
1457
+ define: (contract) => gameActionHooks.defineActionContract(contract),
1458
+ schema: () => {
1459
+ const result = gameActionHooks.getActionSchema();
1460
+ return result && result.ok === true ? result.payload : result;
1461
+ },
1462
+ getSchema: () => gameActionHooks.getActionSchema(),
1463
+ run: (request) => gameActionHooks.runAction(request),
1464
+ runAction: (request) => gameActionHooks.runAction(request),
1465
+ };
1466
+ aura.state = {
1467
+ schemaVersion: GAME_STATE_SCHEMA_VERSION,
1468
+ export: (options = {}) => {
1469
+ const result = gameStateHooks.exportState(options);
1470
+ return result && result.ok === true ? result.payload : result;
1471
+ },
1472
+ apply: (payload, options = {}) => gameStateHooks.applyState(payload, options),
1473
+ diff: (beforePayload, afterPayload) => gameStateHooks.diffState(beforePayload, afterPayload),
1474
+ patch: (payload, patchPayload, options = {}) => gameStateHooks.patchState(payload, patchPayload, options),
1475
+ exportState: (options = {}) => gameStateHooks.exportState(options),
1476
+ applyState: (payload, options = {}) => gameStateHooks.applyState(payload, options),
1477
+ diffState: (beforePayload, afterPayload) => gameStateHooks.diffState(beforePayload, afterPayload),
1478
+ patchState: (payload, patchPayload, options = {}) => gameStateHooks.patchState(payload, patchPayload, options),
1479
+ };
1480
+
1481
+ return aura;
1482
+ }
1483
+
1484
+ async function executeLifecycle(aura, frames) {
1485
+ await executeSetup(aura);
1486
+ await executeFrameSteps(aura, frames);
1487
+ }
1488
+
1489
+ export async function executeSetup(aura) {
1490
+ if (typeof aura.setup === 'function') {
1491
+ await Promise.resolve(aura.setup());
1492
+ }
1493
+ }
1494
+
1495
+ export async function executeFrameSteps(aura, frames) {
1496
+ for (let i = 0; i < frames; i += 1) {
1497
+ const dt = 1 / 60;
1498
+ if (aura.debug && typeof aura.debug.__inspectorTick === 'function') {
1499
+ aura.debug.__inspectorTick(dt);
1500
+ }
1501
+ if (aura.anim2d && typeof aura.anim2d.update === 'function') {
1502
+ await Promise.resolve(aura.anim2d.update(dt));
1503
+ }
1504
+ if (aura.ecs && typeof aura.ecs.run === 'function') {
1505
+ await Promise.resolve(aura.ecs.run(dt));
1506
+ }
1507
+ if (typeof aura.update === 'function') {
1508
+ await Promise.resolve(aura.update(dt));
1509
+ }
1510
+ if (typeof aura.draw === 'function') {
1511
+ await Promise.resolve(aura.draw());
1512
+ }
1513
+ }
1514
+ }