@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,1173 @@
1
+ export function createHeadlessAnimationRuntime() {
2
+ let nextAnimationTimelineId = 1;
3
+ let nextAnimationListenerSeq = 1;
4
+ const animationTimelines = new Map();
5
+ const ANIMATION_PHASE_TRANSITION_COMPLETE = 0;
6
+ const ANIMATION_PHASE_MARKER_EVENT = 1;
7
+ const ANIMATION_PHASE_CLIP_COMPLETE = 2;
8
+ const animationResultOk = (extra = {}) => ({ ok: true, reason: null, ...extra });
9
+ const animationResultErr = (reason) => ({ ok: false, reason });
10
+ const animationIsValidTimelineId = (value) => Number.isInteger(value) && value > 0;
11
+ const animationNormalizeDuration = (value) => (
12
+ Number.isFinite(value) && Number(value) > 0 ? Number(value) : null
13
+ );
14
+ const animationNormalizeTime = (value) => (
15
+ Number.isFinite(value) && Number(value) >= 0 ? Number(value) : null
16
+ );
17
+ const animationNormalizeSpeed = (value) => (
18
+ Number.isFinite(value) && Number(value) >= 0 ? Number(value) : null
19
+ );
20
+ const animationNormalizeOrder = (value) => (
21
+ Number.isFinite(value) ? Number(value) : 0
22
+ );
23
+ const animationNormalizeTransitionDuration = (value) => (
24
+ Number.isFinite(value) && Number(value) > 0 ? Number(value) : null
25
+ );
26
+ const animationNormalizeEventTag = (value) => (
27
+ typeof value === 'string' && value.length > 0 ? value : null
28
+ );
29
+ const animationClampTime = (time, duration) => Math.max(0, Math.min(time, duration));
30
+ const animationSortedTimelineIds = () => [...animationTimelines.keys()].sort((a, b) => a - b);
31
+ const animationResolveTimeline = (timelineId) => {
32
+ if (!animationIsValidTimelineId(timelineId)) {
33
+ return { timeline: null, reason: 'invalid_timeline_id' };
34
+ }
35
+ const timeline = animationTimelines.get(timelineId);
36
+ if (!timeline) {
37
+ return { timeline: null, reason: 'missing_timeline' };
38
+ }
39
+ return { timeline, reason: null };
40
+ };
41
+ const animationTransitionSnapshot = (transition) => {
42
+ if (!transition) return null;
43
+ const blend = transition.duration > 0
44
+ ? Math.max(0, Math.min(1, transition.elapsed / transition.duration))
45
+ : 1;
46
+ return {
47
+ active: true,
48
+ duration: transition.duration,
49
+ elapsed: transition.elapsed,
50
+ targetTime: transition.targetTime,
51
+ blend,
52
+ paused: transition.paused,
53
+ };
54
+ };
55
+ const animationSnapshot = (timeline) => ({
56
+ timelineId: timeline.id,
57
+ duration: timeline.duration,
58
+ time: timeline.time,
59
+ normalizedTime: timeline.duration > 0 ? timeline.time / timeline.duration : 0,
60
+ playing: timeline.playing,
61
+ loop: timeline.loop,
62
+ speed: timeline.speed,
63
+ completed: timeline.completed,
64
+ loops: timeline.loops,
65
+ transition: animationTransitionSnapshot(timeline.transition),
66
+ });
67
+ const animationEnqueueCallbacks = (pending, timeline, phase, callbacks, payload) => {
68
+ for (const callback of callbacks) {
69
+ pending.push({
70
+ timelineId: timeline.id,
71
+ phase,
72
+ order: callback.order,
73
+ seq: callback.seq,
74
+ fn: callback.fn,
75
+ payload,
76
+ });
77
+ }
78
+ };
79
+ const animationSortPendingCallbacks = (pending) => {
80
+ pending.sort((a, b) => (
81
+ (a.timelineId - b.timelineId)
82
+ || (a.phase - b.phase)
83
+ || (a.order - b.order)
84
+ || (a.seq - b.seq)
85
+ ));
86
+ };
87
+ const animationDispatchPendingCallbacks = (pending) => {
88
+ for (const entry of pending) {
89
+ try {
90
+ entry.fn(entry.payload);
91
+ } catch (_) {
92
+ // callback exceptions are swallowed to preserve deterministic progression
93
+ }
94
+ }
95
+ };
96
+ let nextAtlasId = 1;
97
+ let nextAtlasClipId = 1;
98
+ const animationAtlases = new Map();
99
+ const animationAtlasClips = new Map();
100
+ const ANIMATION_ATLAS_DEFAULT_FRAME_DURATION = 1 / 12;
101
+ const ANIMATION_ATLAS_EPSILON = 1e-9;
102
+ const ANIMATION_ATLAS_MAX_STEP_ITERATIONS = 2048;
103
+ const animationIsPlainObject = (value) => (
104
+ !!value && typeof value === 'object' && !Array.isArray(value)
105
+ );
106
+ const animationNormalizeAtlasImage = (value) => {
107
+ if (typeof value !== 'string') return null;
108
+ const trimmed = value.trim();
109
+ return trimmed.length > 0 ? trimmed : null;
110
+ };
111
+ const animationNormalizeAtlasKey = (value) => (
112
+ typeof value === 'string' && value.length > 0 ? value : null
113
+ );
114
+ const animationNormalizeAtlasFrameDuration = (value) => (
115
+ Number.isFinite(value) && Number(value) > 0 ? Number(value) : null
116
+ );
117
+ const animationNormalizeAtlasFrameRect = (value) => {
118
+ if (!animationIsPlainObject(value)) return null;
119
+ const x = Number(value.x);
120
+ const y = Number(value.y);
121
+ const w = Number(value.w);
122
+ const h = Number(value.h);
123
+ if (![x, y, w, h].every((entry) => Number.isFinite(entry))) return null;
124
+ if (x < 0 || y < 0 || w <= 0 || h <= 0) return null;
125
+ return { x, y, w, h };
126
+ };
127
+ const animationParseAtlasFrameEntry = (entry) => {
128
+ if (!animationIsPlainObject(entry)) return { ok: false, reason: 'invalid_atlas_frame_entry' };
129
+ if (entry.rotated === true || entry.trimmed === true) {
130
+ return { ok: false, reason: 'unsupported_atlas_frame_flags' };
131
+ }
132
+ const rectSource = animationIsPlainObject(entry.frame) ? entry.frame : entry;
133
+ const rect = animationNormalizeAtlasFrameRect(rectSource);
134
+ if (!rect) return { ok: false, reason: 'invalid_atlas_frame_rect' };
135
+ const hasDuration = Object.prototype.hasOwnProperty.call(entry, 'duration');
136
+ const duration = hasDuration
137
+ ? animationNormalizeAtlasFrameDuration(entry.duration)
138
+ : null;
139
+ if (hasDuration && duration == null) {
140
+ return { ok: false, reason: 'invalid_atlas_frame_duration' };
141
+ }
142
+ return { ok: true, rect, duration };
143
+ };
144
+ const animationParseAtlasFrames = (value) => {
145
+ if (!animationIsPlainObject(value)) return { ok: false, reason: 'invalid_atlas_frames' };
146
+ const entries = Object.entries(value);
147
+ if (entries.length === 0) return { ok: false, reason: 'invalid_atlas_frames' };
148
+ const frames = new Map();
149
+ const frameKeys = [];
150
+ for (const [rawKey, rawEntry] of entries) {
151
+ const frameKey = animationNormalizeAtlasKey(rawKey);
152
+ if (frameKey == null) return { ok: false, reason: 'invalid_atlas_frame_key' };
153
+ const parsedEntry = animationParseAtlasFrameEntry(rawEntry);
154
+ if (!parsedEntry.ok) return parsedEntry;
155
+ frames.set(frameKey, {
156
+ key: frameKey,
157
+ x: parsedEntry.rect.x,
158
+ y: parsedEntry.rect.y,
159
+ w: parsedEntry.rect.w,
160
+ h: parsedEntry.rect.h,
161
+ duration: parsedEntry.duration,
162
+ });
163
+ frameKeys.push(frameKey);
164
+ }
165
+ return { ok: true, frames, frameKeys };
166
+ };
167
+ const animationParseAtlasClipFrames = (value, frames) => {
168
+ if (!Array.isArray(value) || value.length === 0) {
169
+ return { ok: false, reason: 'invalid_atlas_clip_frames' };
170
+ }
171
+ const keys = [];
172
+ for (const rawEntry of value) {
173
+ const key = animationNormalizeAtlasKey(rawEntry);
174
+ if (key == null) return { ok: false, reason: 'invalid_atlas_clip_frames' };
175
+ if (!frames.has(key)) return { ok: false, reason: 'missing_atlas_frame' };
176
+ keys.push(key);
177
+ }
178
+ return { ok: true, keys };
179
+ };
180
+ const animationParseAtlasClips = (value, frames) => {
181
+ if (value == null) return { ok: true, clips: new Map() };
182
+ if (!animationIsPlainObject(value)) return { ok: false, reason: 'invalid_atlas_clips' };
183
+ const clips = new Map();
184
+ for (const [rawClipKey, rawClip] of Object.entries(value)) {
185
+ const clipKey = animationNormalizeAtlasKey(rawClipKey);
186
+ if (clipKey == null) return { ok: false, reason: 'invalid_atlas_clip_key' };
187
+ if (!animationIsPlainObject(rawClip)) return { ok: false, reason: 'invalid_atlas_clip' };
188
+ const frameParse = animationParseAtlasClipFrames(rawClip.frames, frames);
189
+ if (!frameParse.ok) return frameParse;
190
+ let frameDuration = null;
191
+ if (Object.prototype.hasOwnProperty.call(rawClip, 'frameDuration')) {
192
+ frameDuration = animationNormalizeAtlasFrameDuration(rawClip.frameDuration);
193
+ if (frameDuration == null) return { ok: false, reason: 'invalid_atlas_clip_duration' };
194
+ }
195
+ const loop = Object.prototype.hasOwnProperty.call(rawClip, 'loop')
196
+ ? rawClip.loop
197
+ : true;
198
+ if (typeof loop !== 'boolean') return { ok: false, reason: 'invalid_atlas_clip_loop' };
199
+ clips.set(clipKey, {
200
+ key: clipKey,
201
+ frames: frameParse.keys,
202
+ frameDuration,
203
+ loop,
204
+ });
205
+ }
206
+ return { ok: true, clips };
207
+ };
208
+ const animationResolveAtlas = (atlasId) => {
209
+ if (!Number.isInteger(atlasId) || atlasId <= 0) {
210
+ return { atlas: null, reason: 'invalid_atlas_id' };
211
+ }
212
+ const atlas = animationAtlases.get(atlasId);
213
+ if (!atlas) return { atlas: null, reason: 'missing_atlas' };
214
+ return { atlas, reason: null };
215
+ };
216
+ const animationResolveAtlasFrame = (atlas, frameKey) => {
217
+ const normalizedFrameKey = animationNormalizeAtlasKey(frameKey);
218
+ if (normalizedFrameKey == null) return { frame: null, reason: 'invalid_frame_key' };
219
+ const frame = atlas.frames.get(normalizedFrameKey);
220
+ if (!frame) return { frame: null, reason: 'missing_frame' };
221
+ return { frame, reason: null };
222
+ };
223
+ const animationAtlasFrameSnapshot = (atlas, frameKey) => {
224
+ const frame = atlas.frames.get(frameKey);
225
+ if (!frame) return null;
226
+ const duration = frame.duration != null
227
+ ? frame.duration
228
+ : ANIMATION_ATLAS_DEFAULT_FRAME_DURATION;
229
+ return {
230
+ atlasId: atlas.id,
231
+ frameKey,
232
+ image: atlas.image,
233
+ frameX: frame.x,
234
+ frameY: frame.y,
235
+ frameW: frame.w,
236
+ frameH: frame.h,
237
+ duration,
238
+ };
239
+ };
240
+ const animationResolveAtlasClip = (clipId) => {
241
+ if (!Number.isInteger(clipId) || clipId <= 0) {
242
+ return { clip: null, reason: 'invalid_clip_id' };
243
+ }
244
+ const clip = animationAtlasClips.get(clipId);
245
+ if (!clip) return { clip: null, reason: 'missing_clip' };
246
+ return { clip, reason: null };
247
+ };
248
+ const animationAtlasClipFrameDuration = (clip, frameKey) => {
249
+ if (clip.frameDuration != null) return clip.frameDuration;
250
+ const frame = clip.atlas.frames.get(frameKey);
251
+ if (frame && frame.duration != null) return frame.duration;
252
+ return ANIMATION_ATLAS_DEFAULT_FRAME_DURATION;
253
+ };
254
+ const animationAtlasClipSnapshot = (clip) => {
255
+ if (!clip || clip.frameKeys.length === 0) return null;
256
+ const boundedFrameIndex = Math.max(0, Math.min(clip.frameIndex, clip.frameKeys.length - 1));
257
+ const frameKey = clip.frameKeys[boundedFrameIndex];
258
+ const frame = clip.atlas.frames.get(frameKey);
259
+ if (!frame) return null;
260
+ const frameDuration = animationAtlasClipFrameDuration(clip, frameKey);
261
+ return {
262
+ clipId: clip.id,
263
+ atlasId: clip.atlas.id,
264
+ clipKey: clip.clipKey,
265
+ image: clip.atlas.image,
266
+ frameKey,
267
+ frameIndex: boundedFrameIndex,
268
+ frameCount: clip.frameKeys.length,
269
+ frameX: frame.x,
270
+ frameY: frame.y,
271
+ frameW: frame.w,
272
+ frameH: frame.h,
273
+ frameDuration,
274
+ elapsedInFrame: Math.max(0, Math.min(clip.elapsed, frameDuration)),
275
+ playing: clip.playing,
276
+ loop: clip.loop,
277
+ completed: clip.completed,
278
+ loops: clip.loops,
279
+ };
280
+ };
281
+ const animation = {
282
+ create: (options = {}) => {
283
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
284
+ return animationResultErr('invalid_options');
285
+ }
286
+
287
+ const duration = animationNormalizeDuration(options.duration);
288
+ if (duration == null) return animationResultErr('invalid_duration');
289
+
290
+ const time = Object.prototype.hasOwnProperty.call(options, 'time')
291
+ ? animationNormalizeTime(options.time)
292
+ : 0;
293
+ if (time == null) return animationResultErr('invalid_time');
294
+
295
+ const loop = Object.prototype.hasOwnProperty.call(options, 'loop')
296
+ ? options.loop
297
+ : false;
298
+ if (typeof loop !== 'boolean') return animationResultErr('invalid_loop_flag');
299
+
300
+ const speed = Object.prototype.hasOwnProperty.call(options, 'speed')
301
+ ? animationNormalizeSpeed(options.speed)
302
+ : 1;
303
+ if (speed == null) return animationResultErr('invalid_speed');
304
+
305
+ const playing = Object.prototype.hasOwnProperty.call(options, 'playing')
306
+ ? options.playing
307
+ : false;
308
+ if (typeof playing !== 'boolean') return animationResultErr('invalid_playing_flag');
309
+
310
+ const timelineId = nextAnimationTimelineId++;
311
+ const timeline = {
312
+ id: timelineId,
313
+ duration,
314
+ time: animationClampTime(time, duration),
315
+ loop,
316
+ speed,
317
+ playing,
318
+ completed: false,
319
+ loops: 0,
320
+ completionNotified: false,
321
+ transition: null,
322
+ completeCallbacks: [],
323
+ eventCallbacks: [],
324
+ };
325
+
326
+ if (!timeline.loop && timeline.time >= timeline.duration) {
327
+ timeline.time = timeline.duration;
328
+ timeline.completed = true;
329
+ timeline.playing = false;
330
+ timeline.completionNotified = true;
331
+ }
332
+
333
+ animationTimelines.set(timelineId, timeline);
334
+ return animationResultOk({ timelineId });
335
+ },
336
+ play: (timelineId) => {
337
+ const resolved = animationResolveTimeline(timelineId);
338
+ if (!resolved.timeline) return animationResultErr(resolved.reason);
339
+
340
+ const timeline = resolved.timeline;
341
+ if (!timeline.loop && timeline.completed && timeline.time >= timeline.duration) {
342
+ timeline.time = 0;
343
+ timeline.completed = false;
344
+ timeline.completionNotified = false;
345
+ }
346
+ timeline.playing = true;
347
+ if (timeline.transition) timeline.transition.paused = false;
348
+ return animationResultOk({ timelineId: timeline.id });
349
+ },
350
+ pause: (timelineId) => {
351
+ const resolved = animationResolveTimeline(timelineId);
352
+ if (!resolved.timeline) return animationResultErr(resolved.reason);
353
+ const timeline = resolved.timeline;
354
+ timeline.playing = false;
355
+ if (timeline.transition) timeline.transition.paused = true;
356
+ return animationResultOk({ timelineId: timeline.id });
357
+ },
358
+ resume: (timelineId) => {
359
+ const resolved = animationResolveTimeline(timelineId);
360
+ if (!resolved.timeline) return animationResultErr(resolved.reason);
361
+ const timeline = resolved.timeline;
362
+ timeline.playing = true;
363
+ if (timeline.transition) timeline.transition.paused = false;
364
+ return animationResultOk({ timelineId: timeline.id });
365
+ },
366
+ seek: (timelineId, time) => {
367
+ const resolved = animationResolveTimeline(timelineId);
368
+ if (!resolved.timeline) return animationResultErr(resolved.reason);
369
+ const normalizedTime = animationNormalizeTime(time);
370
+ if (normalizedTime == null) return animationResultErr('invalid_time');
371
+
372
+ const timeline = resolved.timeline;
373
+ timeline.time = animationClampTime(normalizedTime, timeline.duration);
374
+ timeline.transition = null;
375
+ if (!timeline.loop && timeline.time >= timeline.duration) {
376
+ timeline.completed = true;
377
+ timeline.playing = false;
378
+ timeline.completionNotified = true;
379
+ } else {
380
+ timeline.completed = false;
381
+ timeline.completionNotified = false;
382
+ }
383
+ return animationResultOk({ timelineId: timeline.id, time: timeline.time });
384
+ },
385
+ transition: (timelineId, options = {}) => {
386
+ const resolved = animationResolveTimeline(timelineId);
387
+ if (!resolved.timeline) return animationResultErr(resolved.reason);
388
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
389
+ return animationResultErr('invalid_transition_options');
390
+ }
391
+
392
+ const duration = animationNormalizeTransitionDuration(options.duration);
393
+ if (duration == null) return animationResultErr('invalid_transition_duration');
394
+ const targetTime = Object.prototype.hasOwnProperty.call(options, 'targetTime')
395
+ ? animationNormalizeTime(options.targetTime)
396
+ : resolved.timeline.time;
397
+ if (targetTime == null) return animationResultErr('invalid_time');
398
+ const eventTag = Object.prototype.hasOwnProperty.call(options, 'eventTag')
399
+ ? animationNormalizeEventTag(options.eventTag)
400
+ : null;
401
+
402
+ const timeline = resolved.timeline;
403
+ timeline.transition = {
404
+ startTime: timeline.time,
405
+ targetTime: animationClampTime(targetTime, timeline.duration),
406
+ duration,
407
+ elapsed: 0,
408
+ paused: !timeline.playing,
409
+ eventTag,
410
+ };
411
+ timeline.completed = false;
412
+ timeline.completionNotified = false;
413
+ return animationResultOk({
414
+ timelineId: timeline.id,
415
+ targetTime: timeline.transition.targetTime,
416
+ duration: timeline.transition.duration,
417
+ });
418
+ },
419
+ onComplete: (timelineId, callback, order = 0) => {
420
+ const resolved = animationResolveTimeline(timelineId);
421
+ if (!resolved.timeline) return animationResultErr(resolved.reason);
422
+ if (typeof callback !== 'function') return animationResultErr('invalid_callback');
423
+ const listenerId = nextAnimationListenerSeq++;
424
+ resolved.timeline.completeCallbacks.push({
425
+ id: listenerId,
426
+ fn: callback,
427
+ order: animationNormalizeOrder(order),
428
+ seq: listenerId,
429
+ });
430
+ return animationResultOk({ timelineId: resolved.timeline.id, listenerId });
431
+ },
432
+ onEvent: (timelineId, callback, order = 0) => {
433
+ const resolved = animationResolveTimeline(timelineId);
434
+ if (!resolved.timeline) return animationResultErr(resolved.reason);
435
+ if (typeof callback !== 'function') return animationResultErr('invalid_callback');
436
+ const listenerId = nextAnimationListenerSeq++;
437
+ resolved.timeline.eventCallbacks.push({
438
+ id: listenerId,
439
+ fn: callback,
440
+ order: animationNormalizeOrder(order),
441
+ seq: listenerId,
442
+ });
443
+ return animationResultOk({ timelineId: resolved.timeline.id, listenerId });
444
+ },
445
+ setLoop: (timelineId, loop) => {
446
+ const resolved = animationResolveTimeline(timelineId);
447
+ if (!resolved.timeline) return animationResultErr(resolved.reason);
448
+ if (typeof loop !== 'boolean') return animationResultErr('invalid_loop_flag');
449
+
450
+ const timeline = resolved.timeline;
451
+ timeline.loop = loop;
452
+ if (!timeline.loop && timeline.time >= timeline.duration) {
453
+ timeline.time = timeline.duration;
454
+ timeline.completed = true;
455
+ timeline.playing = false;
456
+ timeline.completionNotified = true;
457
+ } else if (timeline.loop && timeline.time >= timeline.duration) {
458
+ timeline.time = timeline.duration > 0 ? (timeline.time % timeline.duration) : 0;
459
+ timeline.completed = false;
460
+ timeline.completionNotified = false;
461
+ }
462
+ return animationResultOk({ timelineId: timeline.id, loop: timeline.loop });
463
+ },
464
+ setSpeed: (timelineId, speed) => {
465
+ const resolved = animationResolveTimeline(timelineId);
466
+ if (!resolved.timeline) return animationResultErr(resolved.reason);
467
+ const normalizedSpeed = animationNormalizeSpeed(speed);
468
+ if (normalizedSpeed == null) return animationResultErr('invalid_speed');
469
+ resolved.timeline.speed = normalizedSpeed;
470
+ return animationResultOk({ timelineId: resolved.timeline.id, speed: resolved.timeline.speed });
471
+ },
472
+ update: (dt) => {
473
+ const stepDt = Number.isFinite(dt) && Number(dt) > 0 ? Number(dt) : null;
474
+ if (stepDt == null) return animationResultErr('invalid_dt');
475
+ const pendingCallbacks = [];
476
+
477
+ for (const timelineId of animationSortedTimelineIds()) {
478
+ const timeline = animationTimelines.get(timelineId);
479
+ if (!timeline) continue;
480
+ if (timeline.transition) {
481
+ if (
482
+ timeline.playing
483
+ && !timeline.transition.paused
484
+ && timeline.speed > 0
485
+ ) {
486
+ const transitionAdvanced = stepDt * timeline.speed;
487
+ if (transitionAdvanced > 0) {
488
+ const transition = timeline.transition;
489
+ transition.elapsed = Math.min(
490
+ transition.duration,
491
+ transition.elapsed + transitionAdvanced,
492
+ );
493
+ const blend = transition.duration > 0
494
+ ? (transition.elapsed / transition.duration)
495
+ : 1;
496
+ timeline.time = animationClampTime(
497
+ transition.startTime
498
+ + ((transition.targetTime - transition.startTime) * blend),
499
+ timeline.duration,
500
+ );
501
+ timeline.completed = false;
502
+ if (blend >= 1) {
503
+ timeline.transition = null;
504
+ animationEnqueueCallbacks(
505
+ pendingCallbacks,
506
+ timeline,
507
+ ANIMATION_PHASE_TRANSITION_COMPLETE,
508
+ timeline.eventCallbacks,
509
+ {
510
+ type: 'transition_complete',
511
+ timelineId: timeline.id,
512
+ time: timeline.time,
513
+ targetTime: transition.targetTime,
514
+ tag: transition.eventTag,
515
+ },
516
+ );
517
+ if (transition.eventTag != null) {
518
+ animationEnqueueCallbacks(
519
+ pendingCallbacks,
520
+ timeline,
521
+ ANIMATION_PHASE_MARKER_EVENT,
522
+ timeline.eventCallbacks,
523
+ {
524
+ type: 'marker',
525
+ timelineId: timeline.id,
526
+ time: timeline.time,
527
+ tag: transition.eventTag,
528
+ },
529
+ );
530
+ }
531
+ if (!timeline.loop && timeline.time >= timeline.duration) {
532
+ timeline.completed = true;
533
+ timeline.playing = false;
534
+ if (!timeline.completionNotified) {
535
+ timeline.completionNotified = true;
536
+ animationEnqueueCallbacks(
537
+ pendingCallbacks,
538
+ timeline,
539
+ ANIMATION_PHASE_CLIP_COMPLETE,
540
+ timeline.completeCallbacks,
541
+ animationSnapshot(timeline),
542
+ );
543
+ }
544
+ }
545
+ }
546
+ }
547
+ }
548
+ if (timeline.transition) continue;
549
+ }
550
+ if (!timeline.playing || timeline.speed <= 0) continue;
551
+
552
+ const advanced = stepDt * timeline.speed;
553
+ if (!(advanced > 0)) continue;
554
+
555
+ if (timeline.loop) {
556
+ const next = timeline.time + advanced;
557
+ const wraps = timeline.duration > 0
558
+ ? Math.floor(next / timeline.duration)
559
+ : 0;
560
+ timeline.time = timeline.duration > 0
561
+ ? (next % timeline.duration)
562
+ : 0;
563
+ if (wraps > 0) {
564
+ timeline.loops += wraps;
565
+ animationEnqueueCallbacks(
566
+ pendingCallbacks,
567
+ timeline,
568
+ ANIMATION_PHASE_MARKER_EVENT,
569
+ timeline.eventCallbacks,
570
+ {
571
+ type: 'loop',
572
+ timelineId: timeline.id,
573
+ loops: wraps,
574
+ time: timeline.time,
575
+ tag: null,
576
+ },
577
+ );
578
+ }
579
+ timeline.completed = false;
580
+ timeline.completionNotified = false;
581
+ continue;
582
+ }
583
+
584
+ timeline.time = animationClampTime(timeline.time + advanced, timeline.duration);
585
+ if (timeline.time >= timeline.duration) {
586
+ timeline.completed = true;
587
+ timeline.playing = false;
588
+ if (!timeline.completionNotified) {
589
+ timeline.completionNotified = true;
590
+ animationEnqueueCallbacks(
591
+ pendingCallbacks,
592
+ timeline,
593
+ ANIMATION_PHASE_CLIP_COMPLETE,
594
+ timeline.completeCallbacks,
595
+ animationSnapshot(timeline),
596
+ );
597
+ }
598
+ } else {
599
+ timeline.completed = false;
600
+ timeline.completionNotified = false;
601
+ }
602
+ }
603
+
604
+ animationSortPendingCallbacks(pendingCallbacks);
605
+ animationDispatchPendingCallbacks(pendingCallbacks);
606
+ return animationResultOk({ dispatchedCallbacks: pendingCallbacks.length });
607
+ },
608
+ getState: (timelineId) => {
609
+ const resolved = animationResolveTimeline(timelineId);
610
+ if (!resolved.timeline) return null;
611
+ return animationSnapshot(resolved.timeline);
612
+ },
613
+ registerAtlas: (payload) => {
614
+ if (!animationIsPlainObject(payload)) return animationResultErr('invalid_atlas_payload');
615
+ const image = animationNormalizeAtlasImage(payload.image);
616
+ if (image == null) return animationResultErr('invalid_atlas_image');
617
+ const parsedFrames = animationParseAtlasFrames(payload.frames);
618
+ if (!parsedFrames.ok) return animationResultErr(parsedFrames.reason);
619
+ const parsedClips = animationParseAtlasClips(payload.clips, parsedFrames.frames);
620
+ if (!parsedClips.ok) return animationResultErr(parsedClips.reason);
621
+
622
+ const atlasId = nextAtlasId++;
623
+ animationAtlases.set(atlasId, {
624
+ id: atlasId,
625
+ image,
626
+ frames: parsedFrames.frames,
627
+ frameKeys: parsedFrames.frameKeys,
628
+ clips: parsedClips.clips,
629
+ });
630
+
631
+ return animationResultOk({
632
+ atlasId,
633
+ frameCount: parsedFrames.frameKeys.length,
634
+ clipCount: parsedClips.clips.size,
635
+ });
636
+ },
637
+ resolveAtlasFrame: (atlasId, frameKey) => {
638
+ const resolvedAtlas = animationResolveAtlas(atlasId);
639
+ if (!resolvedAtlas.atlas) return animationResultErr(resolvedAtlas.reason);
640
+ const resolvedFrame = animationResolveAtlasFrame(resolvedAtlas.atlas, frameKey);
641
+ if (!resolvedFrame.frame) return animationResultErr(resolvedFrame.reason);
642
+ const snapshot = animationAtlasFrameSnapshot(resolvedAtlas.atlas, resolvedFrame.frame.key);
643
+ return animationResultOk(snapshot || {});
644
+ },
645
+ createAtlasClip: (options = {}) => {
646
+ if (!animationIsPlainObject(options)) return animationResultErr('invalid_clip_options');
647
+ const resolvedAtlas = animationResolveAtlas(options.atlasId);
648
+ if (!resolvedAtlas.atlas) return animationResultErr(resolvedAtlas.reason);
649
+ const atlas = resolvedAtlas.atlas;
650
+
651
+ const hasClipKey = Object.prototype.hasOwnProperty.call(options, 'clipKey');
652
+ const hasFrames = Object.prototype.hasOwnProperty.call(options, 'frames');
653
+ if (hasClipKey && hasFrames) return animationResultErr('invalid_clip_options');
654
+
655
+ let clipKey = null;
656
+ let frameKeys = null;
657
+ let frameDuration = null;
658
+ let loop = true;
659
+
660
+ if (hasClipKey) {
661
+ clipKey = animationNormalizeAtlasKey(options.clipKey);
662
+ if (clipKey == null) return animationResultErr('invalid_clip_key');
663
+ const clipMeta = atlas.clips.get(clipKey);
664
+ if (!clipMeta) return animationResultErr('missing_clip');
665
+ frameKeys = clipMeta.frames.slice();
666
+ frameDuration = clipMeta.frameDuration;
667
+ loop = clipMeta.loop;
668
+ } else {
669
+ const parsedFrames = animationParseAtlasClipFrames(options.frames, atlas.frames);
670
+ if (!parsedFrames.ok) return animationResultErr(parsedFrames.reason);
671
+ frameKeys = parsedFrames.keys;
672
+ }
673
+
674
+ if (Object.prototype.hasOwnProperty.call(options, 'frameDuration')) {
675
+ const normalizedFrameDuration = animationNormalizeAtlasFrameDuration(options.frameDuration);
676
+ if (normalizedFrameDuration == null) return animationResultErr('invalid_clip_duration');
677
+ frameDuration = normalizedFrameDuration;
678
+ }
679
+
680
+ if (Object.prototype.hasOwnProperty.call(options, 'loop')) {
681
+ if (typeof options.loop !== 'boolean') return animationResultErr('invalid_clip_loop');
682
+ loop = options.loop;
683
+ }
684
+
685
+ const playing = Object.prototype.hasOwnProperty.call(options, 'playing')
686
+ ? options.playing
687
+ : true;
688
+ if (typeof playing !== 'boolean') return animationResultErr('invalid_playing_flag');
689
+
690
+ const frameIndex = Object.prototype.hasOwnProperty.call(options, 'frameIndex')
691
+ ? options.frameIndex
692
+ : 0;
693
+ if (
694
+ !Number.isInteger(frameIndex)
695
+ || frameIndex < 0
696
+ || frameIndex >= frameKeys.length
697
+ ) {
698
+ return animationResultErr('invalid_clip_frame_index');
699
+ }
700
+
701
+ const clipId = nextAtlasClipId++;
702
+ animationAtlasClips.set(clipId, {
703
+ id: clipId,
704
+ atlas,
705
+ clipKey,
706
+ frameKeys,
707
+ frameDuration,
708
+ frameIndex,
709
+ elapsed: 0,
710
+ playing,
711
+ loop,
712
+ completed: false,
713
+ loops: 0,
714
+ });
715
+
716
+ const clipState = animationAtlasClipSnapshot(animationAtlasClips.get(clipId));
717
+ return animationResultOk({
718
+ clipId,
719
+ atlasId: atlas.id,
720
+ frameKey: clipState ? clipState.frameKey : null,
721
+ });
722
+ },
723
+ stepAtlasClip: (clipId, dt) => {
724
+ const resolvedClip = animationResolveAtlasClip(clipId);
725
+ if (!resolvedClip.clip) return animationResultErr(resolvedClip.reason);
726
+
727
+ const stepDt = Number.isFinite(dt) && Number(dt) > 0 ? Number(dt) : null;
728
+ if (stepDt == null) return animationResultErr('invalid_dt');
729
+
730
+ const clip = resolvedClip.clip;
731
+ if (clip.playing && !clip.completed) {
732
+ let remaining = stepDt;
733
+ let iterations = 0;
734
+ while (remaining > ANIMATION_ATLAS_EPSILON && iterations < ANIMATION_ATLAS_MAX_STEP_ITERATIONS) {
735
+ iterations += 1;
736
+ const frameKey = clip.frameKeys[clip.frameIndex];
737
+ const frameDuration = animationAtlasClipFrameDuration(clip, frameKey);
738
+ const available = Math.max(ANIMATION_ATLAS_EPSILON, frameDuration - clip.elapsed);
739
+ if (remaining + ANIMATION_ATLAS_EPSILON < available) {
740
+ clip.elapsed += remaining;
741
+ remaining = 0;
742
+ break;
743
+ }
744
+
745
+ remaining -= available;
746
+ clip.elapsed = 0;
747
+
748
+ if (clip.frameIndex + 1 < clip.frameKeys.length) {
749
+ clip.frameIndex += 1;
750
+ clip.completed = false;
751
+ continue;
752
+ }
753
+
754
+ if (clip.loop) {
755
+ clip.frameIndex = 0;
756
+ clip.loops += 1;
757
+ clip.completed = false;
758
+ continue;
759
+ }
760
+
761
+ clip.frameIndex = clip.frameKeys.length - 1;
762
+ clip.playing = false;
763
+ clip.completed = true;
764
+ remaining = 0;
765
+ break;
766
+ }
767
+ }
768
+
769
+ const clipState = animationAtlasClipSnapshot(clip);
770
+ return animationResultOk({
771
+ clipId: clip.id,
772
+ frameKey: clipState ? clipState.frameKey : null,
773
+ frameIndex: clipState ? clipState.frameIndex : null,
774
+ loops: clip.loops,
775
+ completed: clip.completed,
776
+ });
777
+ },
778
+ getAtlasClipState: (clipId) => {
779
+ const resolvedClip = animationResolveAtlasClip(clipId);
780
+ if (!resolvedClip.clip) return null;
781
+ return animationAtlasClipSnapshot(resolvedClip.clip);
782
+ },
783
+ };
784
+
785
+ const normalizeMachineId = (value) => (
786
+ Number.isInteger(value) && value > 0 ? value : null
787
+ );
788
+
789
+ let nextAnim2dMachineId = 1;
790
+ let nextAnim2dCallbackSeq = 1;
791
+ const anim2dClips = new Map();
792
+ const anim2dMachines = new Map();
793
+ const normalizeAnim2dName = (value) => (
794
+ typeof value === 'string' && value.length > 0 ? value : null
795
+ );
796
+ const normalizeAnim2dFrames = (value) => {
797
+ if (Array.isArray(value) && value.length > 0) return value.slice();
798
+ if (Number.isInteger(value) && value > 0) {
799
+ return Array.from({ length: value }, (_, index) => index);
800
+ }
801
+ return null;
802
+ };
803
+ const normalizeAnim2dFrameDuration = (value) => (
804
+ Number.isFinite(value) && value > 0 ? Number(value) : (1 / 12)
805
+ );
806
+ const normalizeAnim2dOrder = (value) => (
807
+ Number.isFinite(value) ? Number(value) : 0
808
+ );
809
+ const sortedAnim2dMachineIds = () => (
810
+ [...anim2dMachines.keys()].sort((a, b) => a - b)
811
+ );
812
+ const anim2dSnapshot = (machine) => {
813
+ if (!machine) return null;
814
+ if (machine.currentState == null) {
815
+ return {
816
+ machineId: machine.id,
817
+ state: null,
818
+ frameIndex: 0,
819
+ frame: null,
820
+ completed: false,
821
+ loops: machine.loops,
822
+ frameDuration: 1 / 12,
823
+ loop: true,
824
+ };
825
+ }
826
+ const state = machine.states.get(machine.currentState);
827
+ if (!state) {
828
+ return {
829
+ machineId: machine.id,
830
+ state: machine.currentState,
831
+ frameIndex: 0,
832
+ frame: null,
833
+ completed: false,
834
+ loops: machine.loops,
835
+ frameDuration: 1 / 12,
836
+ loop: true,
837
+ };
838
+ }
839
+ const boundedFrameIndex = Math.max(0, Math.min(machine.frameIndex, state.frames.length - 1));
840
+ return {
841
+ machineId: machine.id,
842
+ state: machine.currentState,
843
+ frameIndex: boundedFrameIndex,
844
+ frame: state.frames[boundedFrameIndex],
845
+ completed: machine.completed,
846
+ loops: machine.loops,
847
+ frameDuration: state.frameDuration,
848
+ loop: state.loop,
849
+ };
850
+ };
851
+
852
+ let nextMaterialHandle = 1;
853
+ const defaultMaterialState = Object.freeze({
854
+ color: { r: 1, g: 1, b: 1, a: 1 },
855
+ emissive: { r: 0, g: 0, b: 0 },
856
+ metallic: 0,
857
+ roughness: 1,
858
+ texture: null,
859
+ normalMap: null,
860
+ metallicRoughnessTexture: null,
861
+ occlusionTexture: null,
862
+ emissiveTexture: null,
863
+ alphaMode: 'opaque',
864
+ alphaCutoff: 0.5,
865
+ doubleSided: false,
866
+ clearcoat: 0,
867
+ clearcoatRoughness: 0,
868
+ sheenColor: { r: 0, g: 0, b: 0 },
869
+ sheenRoughness: 0,
870
+ occlusionStrength: 1,
871
+ });
872
+ const materialStore = new Map([[0, { ...defaultMaterialState }]]);
873
+ const normalizeMaterialHandle = (value) => (
874
+ Number.isInteger(value) && value >= 0 ? value : null
875
+ );
876
+ const normalizeMaterialColor = (value) => {
877
+ if (!value || typeof value !== 'object') return null;
878
+ const { r, g, b, a = 1 } = value;
879
+ if (![r, g, b, a].every((entry) => Number.isFinite(entry))) return null;
880
+ return { r: Number(r), g: Number(g), b: Number(b), a: Number(a) };
881
+ };
882
+ const normalizeMaterialRgb = (value) => {
883
+ if (!value || typeof value !== 'object') return null;
884
+ const { r, g, b } = value;
885
+ if (![r, g, b].every((entry) => Number.isFinite(entry))) return null;
886
+ return { r: Number(r), g: Number(g), b: Number(b) };
887
+ };
888
+ const normalizeTexturePath = (value) => (
889
+ typeof value === 'string' && value.length > 0 ? value : null
890
+ );
891
+ const normalizeAlphaMode = (value) => {
892
+ if (typeof value !== 'string') return defaultMaterialState.alphaMode;
893
+ const normalized = value.trim().toLowerCase();
894
+ if (normalized === 'mask' || normalized === 'blend') return normalized;
895
+ return 'opaque';
896
+ };
897
+ const clampUnit = (value, fallback) => {
898
+ if (!Number.isFinite(value)) return fallback;
899
+ return Math.max(0, Math.min(1, Number(value)));
900
+ };
901
+ const getMaterialState = (handle) => {
902
+ const normalized = normalizeMaterialHandle(handle);
903
+ if (normalized == null) return null;
904
+ return materialStore.get(normalized) || null;
905
+ };
906
+ const createMaterialState = (options = undefined) => {
907
+ if (options === undefined) return { ...defaultMaterialState };
908
+ if (options === null || typeof options !== 'object') {
909
+ throw new TypeError('aura.material.create: options must be an object or undefined');
910
+ }
911
+ return {
912
+ color: normalizeMaterialColor(options.color) || { ...defaultMaterialState.color },
913
+ emissive: normalizeMaterialRgb(options.emissive) || { ...defaultMaterialState.emissive },
914
+ metallic: clampUnit(options.metallic, defaultMaterialState.metallic),
915
+ roughness: clampUnit(options.roughness, defaultMaterialState.roughness),
916
+ texture: normalizeTexturePath(options.texture),
917
+ normalMap: normalizeTexturePath(options.normalMap ?? options.normalTexture),
918
+ metallicRoughnessTexture: normalizeTexturePath(options.metallicRoughnessTexture),
919
+ occlusionTexture: normalizeTexturePath(options.aoTexture ?? options.occlusionTexture),
920
+ emissiveTexture: normalizeTexturePath(options.emissiveTexture),
921
+ alphaMode: normalizeAlphaMode(options.alphaMode),
922
+ alphaCutoff: clampUnit(options.alphaCutoff, defaultMaterialState.alphaCutoff),
923
+ doubleSided: options.doubleSided === true,
924
+ clearcoat: clampUnit(options.clearcoat, defaultMaterialState.clearcoat),
925
+ clearcoatRoughness: clampUnit(
926
+ options.clearcoatRoughness,
927
+ defaultMaterialState.clearcoatRoughness,
928
+ ),
929
+ sheenColor: normalizeMaterialRgb(options.sheenColor) || { ...defaultMaterialState.sheenColor },
930
+ sheenRoughness: clampUnit(options.sheenRoughness, defaultMaterialState.sheenRoughness),
931
+ occlusionStrength: clampUnit(
932
+ options.occlusionStrength ?? options.aoStrength,
933
+ defaultMaterialState.occlusionStrength,
934
+ ),
935
+ };
936
+ };
937
+ const material = {
938
+ create: (options = undefined) => {
939
+ const handle = nextMaterialHandle++;
940
+ materialStore.set(handle, createMaterialState(options));
941
+ return handle;
942
+ },
943
+ setColor: (handle, color) => {
944
+ const materialState = getMaterialState(handle);
945
+ if (!materialState) return;
946
+ const normalized = normalizeMaterialColor(color);
947
+ if (!normalized) return;
948
+ materialState.color = normalized;
949
+ },
950
+ setTexture: (handle, texturePath) => {
951
+ const materialState = getMaterialState(handle);
952
+ if (!materialState) return;
953
+ if (texturePath === null) {
954
+ materialState.texture = null;
955
+ return;
956
+ }
957
+ if (typeof texturePath !== 'string') {
958
+ throw new Error('aura.material.setTexture: texturePath must be a string or null');
959
+ }
960
+ materialState.texture = texturePath.length > 0 ? texturePath : null;
961
+ },
962
+ setMetallic: (handle, metallic) => {
963
+ const materialState = getMaterialState(handle);
964
+ if (!materialState) return;
965
+ materialState.metallic = clampUnit(metallic, materialState.metallic);
966
+ },
967
+ setRoughness: (handle, roughness) => {
968
+ const materialState = getMaterialState(handle);
969
+ if (!materialState) return;
970
+ materialState.roughness = clampUnit(roughness, materialState.roughness);
971
+ },
972
+ setMetallicRoughness: (handle, metallic, roughness) => {
973
+ const materialState = getMaterialState(handle);
974
+ if (!materialState) return;
975
+ materialState.metallic = clampUnit(metallic, materialState.metallic);
976
+ materialState.roughness = clampUnit(roughness, materialState.roughness);
977
+ },
978
+ reset: (handle) => {
979
+ const materialState = getMaterialState(handle);
980
+ if (!materialState) return;
981
+ materialState.color = { ...defaultMaterialState.color };
982
+ materialState.emissive = { ...defaultMaterialState.emissive };
983
+ materialState.metallic = defaultMaterialState.metallic;
984
+ materialState.roughness = defaultMaterialState.roughness;
985
+ materialState.texture = defaultMaterialState.texture;
986
+ materialState.normalMap = defaultMaterialState.normalMap;
987
+ materialState.metallicRoughnessTexture = defaultMaterialState.metallicRoughnessTexture;
988
+ materialState.occlusionTexture = defaultMaterialState.occlusionTexture;
989
+ materialState.emissiveTexture = defaultMaterialState.emissiveTexture;
990
+ materialState.alphaMode = defaultMaterialState.alphaMode;
991
+ materialState.alphaCutoff = defaultMaterialState.alphaCutoff;
992
+ materialState.doubleSided = defaultMaterialState.doubleSided;
993
+ materialState.clearcoat = defaultMaterialState.clearcoat;
994
+ materialState.clearcoatRoughness = defaultMaterialState.clearcoatRoughness;
995
+ materialState.sheenColor = { ...defaultMaterialState.sheenColor };
996
+ materialState.sheenRoughness = defaultMaterialState.sheenRoughness;
997
+ materialState.occlusionStrength = defaultMaterialState.occlusionStrength;
998
+ },
999
+ clone: (handle) => {
1000
+ const materialState = getMaterialState(handle);
1001
+ if (!materialState) throw new Error('Invalid material handle');
1002
+ const newHandle = nextMaterialHandle++;
1003
+ materialStore.set(newHandle, {
1004
+ color: { ...materialState.color },
1005
+ emissive: { ...materialState.emissive },
1006
+ metallic: materialState.metallic,
1007
+ roughness: materialState.roughness,
1008
+ texture: materialState.texture,
1009
+ normalMap: materialState.normalMap,
1010
+ metallicRoughnessTexture: materialState.metallicRoughnessTexture,
1011
+ occlusionTexture: materialState.occlusionTexture,
1012
+ emissiveTexture: materialState.emissiveTexture,
1013
+ alphaMode: materialState.alphaMode,
1014
+ alphaCutoff: materialState.alphaCutoff,
1015
+ doubleSided: materialState.doubleSided,
1016
+ clearcoat: materialState.clearcoat,
1017
+ clearcoatRoughness: materialState.clearcoatRoughness,
1018
+ sheenColor: { ...materialState.sheenColor },
1019
+ sheenRoughness: materialState.sheenRoughness,
1020
+ occlusionStrength: materialState.occlusionStrength,
1021
+ });
1022
+ return newHandle;
1023
+ },
1024
+ unload: (handle) => {
1025
+ const normalized = normalizeMaterialHandle(handle);
1026
+ if (normalized == null || normalized === 0) return;
1027
+ materialStore.delete(normalized);
1028
+ },
1029
+ };
1030
+
1031
+ const anim2d = {
1032
+ registerClip: (name, frames, options = {}) => {
1033
+ const clipName = normalizeAnim2dName(name);
1034
+ const normalizedFrames = normalizeAnim2dFrames(frames);
1035
+ if (clipName == null || normalizedFrames == null) return false;
1036
+ anim2dClips.set(clipName, {
1037
+ name: clipName,
1038
+ frames: normalizedFrames,
1039
+ frameDuration: normalizeAnim2dFrameDuration(options?.frameDuration),
1040
+ loop: options?.loop !== false,
1041
+ });
1042
+ return true;
1043
+ },
1044
+ createMachine: (initialState = null) => {
1045
+ const id = nextAnim2dMachineId++;
1046
+ anim2dMachines.set(id, {
1047
+ id,
1048
+ states: new Map(),
1049
+ currentState: normalizeAnim2dName(initialState),
1050
+ frameIndex: 0,
1051
+ elapsed: 0,
1052
+ completed: false,
1053
+ loops: 0,
1054
+ callbacks: [],
1055
+ });
1056
+ return id;
1057
+ },
1058
+ defineState: (machineId, stateName, clipOrFrames, options = {}) => {
1059
+ const id = normalizeMachineId(machineId);
1060
+ const normalizedStateName = normalizeAnim2dName(stateName);
1061
+ if (id == null || normalizedStateName == null) return false;
1062
+ const machine = anim2dMachines.get(id);
1063
+ if (!machine) return false;
1064
+
1065
+ let resolvedFrames = null;
1066
+ let resolvedFrameDuration = normalizeAnim2dFrameDuration(options?.frameDuration);
1067
+ let resolvedLoop = options?.loop !== false;
1068
+
1069
+ if (typeof clipOrFrames === 'string') {
1070
+ const clip = anim2dClips.get(clipOrFrames);
1071
+ if (!clip) return false;
1072
+ resolvedFrames = clip.frames.slice();
1073
+ resolvedFrameDuration = clip.frameDuration;
1074
+ resolvedLoop = clip.loop;
1075
+ if (Object.prototype.hasOwnProperty.call(options, 'frameDuration')) {
1076
+ resolvedFrameDuration = normalizeAnim2dFrameDuration(options.frameDuration);
1077
+ }
1078
+ if (Object.prototype.hasOwnProperty.call(options, 'loop')) {
1079
+ resolvedLoop = options.loop !== false;
1080
+ }
1081
+ } else {
1082
+ resolvedFrames = normalizeAnim2dFrames(clipOrFrames);
1083
+ if (resolvedFrames == null) return false;
1084
+ }
1085
+
1086
+ machine.states.set(normalizedStateName, {
1087
+ name: normalizedStateName,
1088
+ frames: resolvedFrames,
1089
+ frameDuration: resolvedFrameDuration,
1090
+ loop: resolvedLoop,
1091
+ });
1092
+ if (machine.currentState == null) {
1093
+ machine.currentState = normalizedStateName;
1094
+ }
1095
+ return true;
1096
+ },
1097
+ play: (machineId, stateName) => {
1098
+ const id = normalizeMachineId(machineId);
1099
+ const normalizedStateName = normalizeAnim2dName(stateName);
1100
+ if (id == null || normalizedStateName == null) return false;
1101
+ const machine = anim2dMachines.get(id);
1102
+ if (!machine) return false;
1103
+ if (!machine.states.has(normalizedStateName)) return false;
1104
+ machine.currentState = normalizedStateName;
1105
+ machine.frameIndex = 0;
1106
+ machine.elapsed = 0;
1107
+ machine.completed = false;
1108
+ machine.loops = 0;
1109
+ return true;
1110
+ },
1111
+ onComplete: (machineId, callback, order = 0) => {
1112
+ const id = normalizeMachineId(machineId);
1113
+ if (id == null || typeof callback !== 'function') return false;
1114
+ const machine = anim2dMachines.get(id);
1115
+ if (!machine) return false;
1116
+ machine.callbacks.push({
1117
+ fn: callback,
1118
+ order: normalizeAnim2dOrder(order),
1119
+ seq: nextAnim2dCallbackSeq++,
1120
+ });
1121
+ machine.callbacks.sort((a, b) => (a.order - b.order) || (a.seq - b.seq));
1122
+ return true;
1123
+ },
1124
+ getState: (machineId) => {
1125
+ const id = normalizeMachineId(machineId);
1126
+ if (id == null) return null;
1127
+ return anim2dSnapshot(anim2dMachines.get(id));
1128
+ },
1129
+ update: (dt) => {
1130
+ const stepDt = Number.isFinite(dt) && dt > 0 ? Number(dt) : 0;
1131
+ if (stepDt <= 0) return;
1132
+ for (const machineId of sortedAnim2dMachineIds()) {
1133
+ const machine = anim2dMachines.get(machineId);
1134
+ if (!machine || machine.currentState == null) continue;
1135
+ const state = machine.states.get(machine.currentState);
1136
+ if (!state || state.frames.length === 0) continue;
1137
+ machine.elapsed += stepDt;
1138
+ const frameDuration = normalizeAnim2dFrameDuration(state.frameDuration);
1139
+ while (machine.elapsed >= frameDuration) {
1140
+ machine.elapsed -= frameDuration;
1141
+ if (machine.frameIndex + 1 < state.frames.length) {
1142
+ machine.frameIndex += 1;
1143
+ continue;
1144
+ }
1145
+ if (state.loop) {
1146
+ machine.frameIndex = 0;
1147
+ machine.loops += 1;
1148
+ continue;
1149
+ }
1150
+ machine.frameIndex = state.frames.length - 1;
1151
+ if (!machine.completed) {
1152
+ machine.completed = true;
1153
+ const snapshot = anim2dSnapshot(machine);
1154
+ for (const callback of machine.callbacks) {
1155
+ try {
1156
+ callback.fn(snapshot);
1157
+ } catch {
1158
+ // Keep update loop deterministic even if callbacks throw.
1159
+ }
1160
+ }
1161
+ }
1162
+ break;
1163
+ }
1164
+ }
1165
+ },
1166
+ };
1167
+
1168
+ return {
1169
+ animation,
1170
+ anim2d,
1171
+ material,
1172
+ };
1173
+ }