@auraindustry/aurajs 0.1.3 → 0.1.5

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 (108) hide show
  1. package/README.md +7 -0
  2. package/benchmarks/perf-thresholds.json +27 -0
  3. package/package.json +6 -1
  4. package/src/ai-guidance.mjs +302 -0
  5. package/src/authored-project.mjs +498 -2
  6. package/src/build-contract/capabilities.mjs +87 -1
  7. package/src/build-contract/constants.mjs +1 -0
  8. package/src/build-contract.mjs +2 -0
  9. package/src/bundler.mjs +143 -13
  10. package/src/cli.mjs +681 -13
  11. package/src/commands/packs.mjs +741 -0
  12. package/src/commands/project-authoring.mjs +128 -1
  13. package/src/conformance/cases/app-and-ui-runtime-cases.mjs +1 -2
  14. package/src/conformance/cases/core-runtime-cases.mjs +6 -2
  15. package/src/conformance/cases/scene3d-and-media-cases.mjs +238 -0
  16. package/src/conformance/cases/systems-and-gameplay-cases.mjs +265 -4
  17. package/src/conformance-mobile.mjs +166 -0
  18. package/src/conformance.mjs +89 -30
  19. package/src/evidence-bundle.mjs +242 -0
  20. package/src/headless-test/runtime-coordinator.mjs +186 -33
  21. package/src/headless-test.mjs +2 -0
  22. package/src/helpers/2d/index.mjs +183 -0
  23. package/src/helpers/index.mjs +26 -0
  24. package/src/helpers/starter-utils/adventure-objectives.js +102 -0
  25. package/src/helpers/starter-utils/adventure-world-2d.js +221 -0
  26. package/src/helpers/starter-utils/animation-2d.js +337 -0
  27. package/src/helpers/starter-utils/animation-packaging-2d.js +203 -0
  28. package/src/helpers/starter-utils/atlas-assets-2d.js +111 -0
  29. package/src/helpers/starter-utils/autoplay-debug-2d.js +215 -0
  30. package/src/helpers/starter-utils/avatar-3d.js +404 -0
  31. package/src/helpers/starter-utils/combat-feedback-2d.js +320 -0
  32. package/src/helpers/starter-utils/combat-runtime-2d.js +290 -0
  33. package/src/helpers/starter-utils/core.js +150 -0
  34. package/src/helpers/starter-utils/dialogue-2d.js +351 -0
  35. package/src/helpers/starter-utils/enemy-archetypes-2d.js +68 -0
  36. package/src/helpers/starter-utils/index.js +26 -0
  37. package/src/helpers/starter-utils/inventory-2d.js +268 -0
  38. package/src/helpers/starter-utils/journal-2d.js +267 -0
  39. package/src/helpers/starter-utils/platformer-3d.js +132 -0
  40. package/src/helpers/starter-utils/scene-audio-2d.js +236 -0
  41. package/src/helpers/starter-utils/streamed-world-2d.js +378 -0
  42. package/src/helpers/starter-utils/tilemap-nav-2d.js +499 -0
  43. package/src/helpers/starter-utils/tilemap-world-2d.js +205 -0
  44. package/src/helpers/starter-utils/triggers.js +662 -0
  45. package/src/helpers/starter-utils/tween-2d.js +615 -0
  46. package/src/helpers/starter-utils/wave-director.js +101 -0
  47. package/src/helpers/starter-utils/world-compositor-2d.js +253 -0
  48. package/src/helpers/starter-utils/world-persistence-2d.js +180 -0
  49. package/src/mobile/android/build.mjs +606 -0
  50. package/src/mobile/android/host-artifact.mjs +280 -0
  51. package/src/mobile/ios/build.mjs +1323 -0
  52. package/src/mobile/ios/host-artifact.mjs +819 -0
  53. package/src/mobile/shared/capabilities.mjs +174 -0
  54. package/src/packs/catalog.mjs +259 -0
  55. package/src/perf-benchmark-runner.mjs +17 -12
  56. package/src/perf-benchmark.mjs +408 -4
  57. package/src/publish-command.mjs +303 -6
  58. package/src/replay-runtime.mjs +257 -0
  59. package/src/scaffold/config.mjs +2 -0
  60. package/src/scaffold/fs.mjs +8 -1
  61. package/src/scaffold/project-docs.mjs +43 -1
  62. package/src/scaffold.mjs +4 -0
  63. package/src/session-runtime.mjs +4 -3
  64. package/src/web-conformance.mjs +0 -36
  65. package/templates/create/2d-adventure/config/gameplay/adventure.config.js +9 -6
  66. package/templates/create/2d-adventure/content/gameplay/dialogue.js +85 -0
  67. package/templates/create/2d-adventure/content/gameplay/world.js +32 -36
  68. package/templates/create/2d-adventure/content/gameplay/world.tilemap.json +273 -0
  69. package/templates/create/2d-adventure/docs/design/loop.md +4 -3
  70. package/templates/create/2d-adventure/prefabs/relic.prefab.js +10 -10
  71. package/templates/create/2d-adventure/prefabs/world.prefab.js +127 -74
  72. package/templates/create/2d-adventure/scenes/gameplay.scene.js +603 -112
  73. package/templates/create/2d-adventure/src/runtime/capabilities.js +16 -0
  74. package/templates/create/2d-adventure/ui/hud.screen.js +187 -4
  75. package/templates/create/2d-adventure/ui/journal.screen.js +183 -0
  76. package/templates/create/3d/scenes/gameplay.scene.js +30 -3
  77. package/templates/create/3d/src/runtime/capabilities.js +5 -0
  78. package/templates/create/3d/src/runtime/materials.js +10 -0
  79. package/templates/create/3d-adventure/scenes/gameplay.scene.js +30 -3
  80. package/templates/create/3d-adventure/src/runtime/capabilities.js +5 -0
  81. package/templates/create/3d-adventure/src/runtime/materials.js +11 -0
  82. package/templates/create/3d-collectathon/scenes/gameplay.scene.js +30 -3
  83. package/templates/create/3d-collectathon/src/runtime/capabilities.js +5 -0
  84. package/templates/create/3d-collectathon/src/runtime/materials.js +10 -0
  85. package/templates/create/shared/src/runtime/ui-forms.js +552 -0
  86. package/templates/create/shared/src/starter-utils/adventure-world-2d.js +221 -0
  87. package/templates/create/shared/src/starter-utils/animation-packaging-2d.js +203 -0
  88. package/templates/create/shared/src/starter-utils/atlas-assets-2d.js +111 -0
  89. package/templates/create/shared/src/starter-utils/autoplay-debug-2d.js +215 -0
  90. package/templates/create/shared/src/starter-utils/combat-runtime-2d.js +290 -0
  91. package/templates/create/shared/src/starter-utils/dialogue-2d.js +351 -0
  92. package/templates/create/shared/src/starter-utils/index.js +15 -1
  93. package/templates/create/shared/src/starter-utils/inventory-2d.js +268 -0
  94. package/templates/create/shared/src/starter-utils/journal-2d.js +267 -0
  95. package/templates/create/shared/src/starter-utils/scene-audio-2d.js +236 -0
  96. package/templates/create/shared/src/starter-utils/streamed-world-2d.js +378 -0
  97. package/templates/create/shared/src/starter-utils/tilemap-nav-2d.js +499 -0
  98. package/templates/create/shared/src/starter-utils/tilemap-world-2d.js +205 -0
  99. package/templates/create/shared/src/starter-utils/world-compositor-2d.js +253 -0
  100. package/templates/create/shared/src/starter-utils/world-persistence-2d.js +180 -0
  101. package/templates/create-bin/play.js +36 -7
  102. package/templates/skills/auramaxx/SKILL.md +46 -0
  103. package/templates/skills/auramaxx/project-requirements.md +68 -0
  104. package/templates/skills/auramaxx/starter-recipes.md +104 -0
  105. package/templates/skills/auramaxx/validation-checklist.md +49 -0
  106. package/templates/skills/aurajs/SKILL.md +0 -96
  107. package/templates/skills/aurajs/api-contract-3d.md +0 -7
  108. package/templates/skills/aurajs/api-contract.md +0 -7
@@ -0,0 +1,221 @@
1
+ import {
2
+ createInteractionPromptState,
3
+ createRectTrigger2D,
4
+ createTriggerTracker,
5
+ stepRectTriggers2D,
6
+ updateInteractionPromptState,
7
+ } from './triggers.js';
8
+ import { addInventoryItem2D, hasInventoryItem2D, removeInventoryItem2D } from './inventory-2d.js';
9
+
10
+ function finite(value, fallback = 0) {
11
+ const numeric = Number(value);
12
+ return Number.isFinite(numeric) ? numeric : fallback;
13
+ }
14
+
15
+ function normalizeText(value, fallback = null) {
16
+ if (typeof value === 'string' && value.trim().length > 0) {
17
+ return value.trim();
18
+ }
19
+ return fallback;
20
+ }
21
+
22
+ function normalizeKind(value) {
23
+ switch (value) {
24
+ case 'pickup':
25
+ case 'lock':
26
+ case 'checkpoint':
27
+ case 'prop':
28
+ return value;
29
+ default:
30
+ return 'prop';
31
+ }
32
+ }
33
+
34
+ function objectProperty(object, name) {
35
+ if (!object || typeof object !== 'object') return null;
36
+ const properties = object.properties && typeof object.properties === 'object' ? object.properties : null;
37
+ return properties && Object.prototype.hasOwnProperty.call(properties, name) ? properties[name] : null;
38
+ }
39
+
40
+ function normalizeInteractable(definition, index) {
41
+ const fallbackId = `interactable-${index + 1}`;
42
+ const id = normalizeText(definition?.id, fallbackId);
43
+ const width = Math.max(4, finite(definition?.w ?? definition?.width, definition?.point === true ? 16 : 0));
44
+ const height = Math.max(4, finite(definition?.h ?? definition?.height, definition?.point === true ? 16 : 0));
45
+ const x = definition?.point === true
46
+ ? finite(definition?.x) - (width * 0.5)
47
+ : finite(definition?.x);
48
+ const y = definition?.point === true
49
+ ? finite(definition?.y) - (height * 0.5)
50
+ : finite(definition?.y);
51
+ return {
52
+ id,
53
+ label: normalizeText(definition?.label ?? definition?.name, id),
54
+ kind: normalizeKind(normalizeText(definition?.kind ?? definition?.type)),
55
+ x,
56
+ y,
57
+ w: width,
58
+ h: height,
59
+ priority: Number.isFinite(Number(definition?.priority)) ? Number(definition.priority) : 0,
60
+ promptText: normalizeText(definition?.promptText ?? definition?.prompt),
61
+ lockedPromptText: normalizeText(definition?.lockedPromptText),
62
+ openedPromptText: normalizeText(definition?.openedPromptText),
63
+ interactionKey: normalizeText(definition?.interactionKey, 'E'),
64
+ requiredItemId: normalizeText(definition?.requiredItemId),
65
+ requiredQuantity: Math.max(1, finite(definition?.requiredQuantity, 1)),
66
+ consumeRequiredItem: definition?.consumeRequiredItem === true,
67
+ grantItem: definition?.grantItem && typeof definition.grantItem === 'object' ? { ...definition.grantItem } : null,
68
+ checkpointId: normalizeText(definition?.checkpointId, id),
69
+ once: definition?.once !== false,
70
+ collected: definition?.collected === true,
71
+ opened: definition?.opened === true,
72
+ activated: definition?.activated === true,
73
+ disabled: definition?.disabled === true,
74
+ };
75
+ }
76
+
77
+ function promptForInteractable(interactable, inventory) {
78
+ if (!interactable || interactable.disabled === true) return null;
79
+ if (interactable.kind === 'pickup' && interactable.collected === true) return null;
80
+ if (interactable.kind === 'lock') {
81
+ if (interactable.opened === true) return interactable.openedPromptText || null;
82
+ if (interactable.requiredItemId && !hasInventoryItem2D(inventory, interactable.requiredItemId, interactable.requiredQuantity)) {
83
+ return interactable.lockedPromptText || `${interactable.label} is locked`;
84
+ }
85
+ }
86
+ if (interactable.kind === 'checkpoint' && interactable.activated === true && interactable.once === true) {
87
+ return null;
88
+ }
89
+ if (interactable.kind === 'prop' && interactable.activated === true && interactable.once === true) {
90
+ return null;
91
+ }
92
+ return interactable.promptText || `Interact with ${interactable.label}`;
93
+ }
94
+
95
+ function buildTrigger(interactable, inventory) {
96
+ const promptText = promptForInteractable(interactable, inventory);
97
+ if (!promptText) return null;
98
+ return createRectTrigger2D(interactable.id, interactable.x, interactable.y, interactable.w, interactable.h, {
99
+ kind: interactable.kind,
100
+ role: interactable.kind,
101
+ priority: interactable.priority,
102
+ promptText,
103
+ interactionKey: interactable.interactionKey,
104
+ });
105
+ }
106
+
107
+ function successfulInteraction(interactable) {
108
+ if (!interactable) return false;
109
+ return interactable.kind === 'pickup'
110
+ ? interactable.collected === true
111
+ : interactable.kind === 'lock'
112
+ ? interactable.opened === true
113
+ : interactable.activated === true;
114
+ }
115
+
116
+ export function createAdventureInteractable2D(definition = {}, index = 0) {
117
+ return normalizeInteractable(definition, index);
118
+ }
119
+
120
+ export function createAdventureInteractablesFromTilemapObjects2D(objects = [], defaults = {}) {
121
+ return (Array.isArray(objects) ? objects : []).map((object, index) => createAdventureInteractable2D({
122
+ id: normalizeText(object?.id != null ? String(object.id) : null, `object-${index + 1}`),
123
+ label: object?.name,
124
+ kind: normalizeText(object?.type, normalizeText(objectProperty(object, 'kind'), defaults.kind)),
125
+ x: finite(object?.worldX ?? object?.x),
126
+ y: finite(object?.worldY ?? object?.y),
127
+ width: finite(object?.width, defaults.width),
128
+ height: finite(object?.height, defaults.height),
129
+ point: object?.point === true,
130
+ promptText: normalizeText(objectProperty(object, 'promptText'), defaults.promptText),
131
+ lockedPromptText: normalizeText(objectProperty(object, 'lockedPromptText'), defaults.lockedPromptText),
132
+ requiredItemId: normalizeText(objectProperty(object, 'requiredItemId'), defaults.requiredItemId),
133
+ requiredQuantity: finite(objectProperty(object, 'requiredQuantity'), defaults.requiredQuantity),
134
+ checkpointId: normalizeText(objectProperty(object, 'checkpointId'), defaults.checkpointId),
135
+ }, index));
136
+ }
137
+
138
+ export function createAdventureInteractionState2D(options = {}) {
139
+ return {
140
+ tracker: createTriggerTracker({ subjectId: normalizeText(options.subjectId, 'player'), dimension: '2d' }),
141
+ prompt: createInteractionPromptState({ prompt: normalizeText(options.prompt, 'Interact') }),
142
+ activeCheckpointId: normalizeText(options.activeCheckpointId),
143
+ lastResult: null,
144
+ };
145
+ }
146
+
147
+ export function stepAdventureInteractables2D(
148
+ state,
149
+ subjectBounds,
150
+ interactables = [],
151
+ options = {},
152
+ ) {
153
+ const inventory = options.inventory || null;
154
+ const normalized = (Array.isArray(interactables) ? interactables : []).map((entry, index) => createAdventureInteractable2D(entry, index));
155
+ const triggers = normalized
156
+ .map((entry) => buildTrigger(entry, inventory))
157
+ .filter(Boolean);
158
+ const triggerStep = stepRectTriggers2D(state?.tracker, subjectBounds, triggers);
159
+ updateInteractionPromptState(state.prompt, triggerStep, {
160
+ interactPressed: options.interactPressed === true,
161
+ });
162
+
163
+ const result = {
164
+ reasonCode: state.prompt.reasonCode,
165
+ acceptedId: state.prompt.acceptedTriggerId || null,
166
+ collectedIds: [],
167
+ openedIds: [],
168
+ activatedIds: [],
169
+ checkpointId: null,
170
+ promptText: state.prompt.prompt,
171
+ };
172
+
173
+ if (!result.acceptedId) {
174
+ state.lastResult = result;
175
+ return result;
176
+ }
177
+
178
+ const interactable = normalized.find((entry) => entry.id === result.acceptedId) || null;
179
+ if (!interactable) {
180
+ state.lastResult = result;
181
+ return result;
182
+ }
183
+
184
+ if (interactable.kind === 'pickup') {
185
+ if (!interactable.collected && interactable.grantItem && inventory) {
186
+ addInventoryItem2D(inventory, interactable.grantItem);
187
+ }
188
+ interactable.collected = true;
189
+ result.collectedIds.push(interactable.id);
190
+ } else if (interactable.kind === 'lock') {
191
+ const unlocked = !interactable.requiredItemId
192
+ || hasInventoryItem2D(inventory, interactable.requiredItemId, interactable.requiredQuantity);
193
+ if (!unlocked) {
194
+ result.reasonCode = 'interaction_locked';
195
+ state.lastResult = result;
196
+ return result;
197
+ }
198
+ if (interactable.consumeRequiredItem === true && interactable.requiredItemId) {
199
+ removeInventoryItem2D(inventory, interactable.requiredItemId, interactable.requiredQuantity);
200
+ }
201
+ interactable.opened = true;
202
+ result.openedIds.push(interactable.id);
203
+ } else if (interactable.kind === 'checkpoint') {
204
+ interactable.activated = true;
205
+ state.activeCheckpointId = interactable.checkpointId;
206
+ result.checkpointId = interactable.checkpointId;
207
+ result.activatedIds.push(interactable.id);
208
+ } else {
209
+ interactable.activated = true;
210
+ result.activatedIds.push(interactable.id);
211
+ }
212
+
213
+ if (interactable.once === true && successfulInteraction(interactable) && interactable.kind !== 'checkpoint') {
214
+ interactable.disabled = true;
215
+ }
216
+ if (result.reasonCode === 'interaction_accepted') {
217
+ result.reasonCode = `interaction_${interactable.kind}_accepted`;
218
+ }
219
+ state.lastResult = result;
220
+ return result;
221
+ }
@@ -0,0 +1,203 @@
1
+ import {
2
+ createSpriteAnimationLibrary2D,
3
+ createSpriteAnimator2D,
4
+ hasSpriteAnimationState2D,
5
+ registerSpriteAnimationLibrary2D,
6
+ } from './animation-2d.js'
7
+
8
+ let nextAnimationPackageId = 1
9
+
10
+ function normalizePackageName(value) {
11
+ if (typeof value === 'string' && value.trim().length > 0) {
12
+ return value.trim()
13
+ }
14
+ return `animation-package-${nextAnimationPackageId}`
15
+ }
16
+
17
+ function normalizePackageClipName(value) {
18
+ const name = typeof value === 'string' ? value.trim() : ''
19
+ if (!name) {
20
+ throw new Error('animation package clip names must be non-empty strings.')
21
+ }
22
+ return name
23
+ }
24
+
25
+ function normalizePackageStateName(value) {
26
+ const name = typeof value === 'string' ? value.trim() : ''
27
+ if (!name) {
28
+ throw new Error('animation package state names must be non-empty strings.')
29
+ }
30
+ return name
31
+ }
32
+
33
+ function normalizePackageStateSpec(stateName, spec, knownClips) {
34
+ if (typeof spec === 'string') {
35
+ const clip = spec.trim()
36
+ if (!knownClips.has(clip)) {
37
+ throw new Error(`animation package state "${stateName}" references unknown clip "${clip}".`)
38
+ }
39
+ return { clip }
40
+ }
41
+
42
+ if (!spec || typeof spec !== 'object') {
43
+ throw new Error(`animation package state "${stateName}" must be a clip name or object.`)
44
+ }
45
+
46
+ const clip = typeof spec.clip === 'string' ? spec.clip.trim() : ''
47
+ if (!clip) {
48
+ throw new Error(`animation package state "${stateName}" must declare a clip.`)
49
+ }
50
+ if (!knownClips.has(clip)) {
51
+ throw new Error(`animation package state "${stateName}" references unknown clip "${clip}".`)
52
+ }
53
+
54
+ const state = { clip }
55
+ if (spec.frameDuration != null) {
56
+ state.frameDuration = spec.frameDuration
57
+ }
58
+ if (spec.loop != null) {
59
+ state.loop = spec.loop
60
+ }
61
+ return state
62
+ }
63
+
64
+ function cloneClipSpec(clipName, spec) {
65
+ if (Array.isArray(spec)) {
66
+ if (spec.length === 0) {
67
+ throw new Error(`animation package clip "${clipName}" must include at least one frame.`)
68
+ }
69
+ return { frames: [...spec] }
70
+ }
71
+
72
+ if (Number.isInteger(spec) && spec >= 0) {
73
+ return { frame: spec }
74
+ }
75
+
76
+ if (!spec || typeof spec !== 'object') {
77
+ throw new Error(`animation package clip "${clipName}" must be an integer, frame array, or object.`)
78
+ }
79
+
80
+ const cloned = { ...spec }
81
+ if (Array.isArray(spec.frames)) {
82
+ if (spec.frames.length === 0) {
83
+ throw new Error(`animation package clip "${clipName}" must include at least one frame.`)
84
+ }
85
+ cloned.frames = [...spec.frames]
86
+ }
87
+ return cloned
88
+ }
89
+
90
+ function buildLibraryStates(packageDefinition) {
91
+ const states = {}
92
+ for (const [stateName, stateSpec] of Object.entries(packageDefinition.states)) {
93
+ const clipSpec = cloneClipSpec(stateSpec.clip, packageDefinition.clips[stateSpec.clip])
94
+ if (stateSpec.frameDuration != null) {
95
+ clipSpec.frameDuration = stateSpec.frameDuration
96
+ }
97
+ if (stateSpec.loop != null) {
98
+ clipSpec.loop = stateSpec.loop
99
+ }
100
+ states[stateName] = clipSpec
101
+ }
102
+ return states
103
+ }
104
+
105
+ export function createAnimationPackage2D(options = {}) {
106
+ const rawClips = options.clips && typeof options.clips === 'object' ? options.clips : null
107
+ const rawStates = options.states && typeof options.states === 'object' ? options.states : null
108
+
109
+ if (!rawClips || Object.keys(rawClips).length === 0) {
110
+ throw new Error('animation packages must declare at least one clip.')
111
+ }
112
+ if (!rawStates || Object.keys(rawStates).length === 0) {
113
+ throw new Error('animation packages must declare at least one state.')
114
+ }
115
+
116
+ const packageId = `anim-pack-${nextAnimationPackageId}`
117
+ nextAnimationPackageId += 1
118
+
119
+ const name = normalizePackageName(options.name)
120
+ const clips = {}
121
+ for (const [rawClipName, spec] of Object.entries(rawClips)) {
122
+ const clipName = normalizePackageClipName(rawClipName)
123
+ clips[clipName] = cloneClipSpec(clipName, spec)
124
+ }
125
+
126
+ const knownClips = new Set(Object.keys(clips))
127
+ const states = {}
128
+ for (const [rawStateName, spec] of Object.entries(rawStates)) {
129
+ const stateName = normalizePackageStateName(rawStateName)
130
+ states[stateName] = normalizePackageStateSpec(stateName, spec, knownClips)
131
+ }
132
+
133
+ const defaultState = (
134
+ typeof options.defaultState === 'string'
135
+ && Object.prototype.hasOwnProperty.call(states, options.defaultState.trim())
136
+ ? options.defaultState.trim()
137
+ : Object.keys(states)[0]
138
+ )
139
+ const source = options.source && typeof options.source === 'object'
140
+ ? { ...options.source }
141
+ : { type: 'inline-animation-package' }
142
+
143
+ const library = createSpriteAnimationLibrary2D({
144
+ image: options.image,
145
+ frameWidth: options.frameWidth,
146
+ frameHeight: options.frameHeight,
147
+ columns: options.columns,
148
+ anchorX: options.anchorX,
149
+ anchorY: options.anchorY,
150
+ defaultFrameDuration: options.defaultFrameDuration,
151
+ defaultState,
152
+ name,
153
+ states: buildLibraryStates({ clips, states }),
154
+ })
155
+
156
+ return {
157
+ packageId,
158
+ name,
159
+ defaultState: library.defaultState,
160
+ clips,
161
+ states,
162
+ library,
163
+ source,
164
+ }
165
+ }
166
+
167
+ export function hasAnimationPackageState2D(animationPackage, stateName) {
168
+ return !!animationPackage && typeof stateName === 'string'
169
+ && Object.prototype.hasOwnProperty.call(animationPackage.states || {}, stateName)
170
+ }
171
+
172
+ export function registerAnimationPackage2D(runtime, animationPackage) {
173
+ if (!animationPackage?.library) {
174
+ throw new Error('animation package is required.')
175
+ }
176
+ return registerSpriteAnimationLibrary2D(runtime, animationPackage.library)
177
+ }
178
+
179
+ export function createPackagedSpriteAnimator2D(runtime, animationPackage, options = {}) {
180
+ if (registerAnimationPackage2D(runtime, animationPackage) !== true) {
181
+ throw new Error(`failed to register animation package "${animationPackage?.packageId || 'unknown'}".`)
182
+ }
183
+
184
+ const initialState = hasAnimationPackageState2D(animationPackage, options.initialState)
185
+ ? options.initialState
186
+ : animationPackage.defaultState
187
+ const animator = createSpriteAnimator2D(runtime, animationPackage.library, { initialState })
188
+
189
+ return {
190
+ ...animator,
191
+ packageId: animationPackage.packageId,
192
+ }
193
+ }
194
+
195
+ export function getAnimationPackageLibrary2D(animationPackage) {
196
+ return animationPackage?.library || null
197
+ }
198
+
199
+ export function getAnimationPackageState2D(animationPackage, stateName) {
200
+ return hasAnimationPackageState2D(animationPackage, stateName)
201
+ ? animationPackage.states[stateName]
202
+ : null
203
+ }
@@ -0,0 +1,111 @@
1
+ import { createAnimationPackage2D } from './animation-packaging-2d.js'
2
+
3
+ let nextAtlasAssetManifestId = 1
4
+
5
+ function cloneSpecMap(value, label) {
6
+ if (!value || typeof value !== 'object') {
7
+ throw new Error(`${label} must be an object.`)
8
+ }
9
+ const entries = Object.entries(value)
10
+ if (entries.length === 0) {
11
+ throw new Error(`${label} must not be empty.`)
12
+ }
13
+ return Object.fromEntries(entries.map(([key, spec]) => {
14
+ if (Array.isArray(spec)) {
15
+ return [key, [...spec]]
16
+ }
17
+ if (spec && typeof spec === 'object') {
18
+ return [key, { ...spec, frames: Array.isArray(spec.frames) ? [...spec.frames] : spec.frames }]
19
+ }
20
+ return [key, spec]
21
+ }))
22
+ }
23
+
24
+ function readPositiveNumber(value, fallback, label) {
25
+ const numeric = Number(value)
26
+ const resolved = Number.isFinite(numeric) ? numeric : fallback
27
+ if (!(resolved > 0)) {
28
+ throw new Error(`${label} must be a positive number.`)
29
+ }
30
+ return resolved
31
+ }
32
+
33
+ function readFiniteNumber(value, fallback) {
34
+ const numeric = Number(value)
35
+ return Number.isFinite(numeric) ? numeric : fallback
36
+ }
37
+
38
+ function normalizeAtlasManifestName(value, fallback) {
39
+ if (typeof value === 'string' && value.trim().length > 0) {
40
+ return value.trim()
41
+ }
42
+ return fallback
43
+ }
44
+
45
+ export function createAtlasAssetManifest2D(options = {}) {
46
+ const image = typeof options.image === 'string' && options.image.trim().length > 0
47
+ ? options.image.trim()
48
+ : null
49
+ if (!image) {
50
+ throw new Error('atlas asset manifest image is required.')
51
+ }
52
+
53
+ const manifest = options.manifest && typeof options.manifest === 'object'
54
+ ? options.manifest
55
+ : options
56
+ const name = normalizeAtlasManifestName(
57
+ options.name ?? manifest.name,
58
+ `atlas-asset-manifest-${nextAtlasAssetManifestId}`,
59
+ )
60
+ const atlasManifestId = `atlas-asset-manifest-${nextAtlasAssetManifestId}`
61
+ nextAtlasAssetManifestId += 1
62
+
63
+ return {
64
+ atlasManifestId,
65
+ assetType: 'atlasAssetManifest2d',
66
+ name,
67
+ image,
68
+ frameWidth: readPositiveNumber(manifest.frameWidth, 0, 'atlas asset manifest frameWidth'),
69
+ frameHeight: readPositiveNumber(manifest.frameHeight, 0, 'atlas asset manifest frameHeight'),
70
+ columns: Math.max(1, Math.floor(readPositiveNumber(manifest.columns, 1, 'atlas asset manifest columns'))),
71
+ defaultFrameDuration: readPositiveNumber(
72
+ manifest.defaultFrameDuration,
73
+ 1 / 12,
74
+ 'atlas asset manifest defaultFrameDuration',
75
+ ),
76
+ anchorX: readFiniteNumber(manifest.anchorX, 0.5),
77
+ anchorY: readFiniteNumber(manifest.anchorY, 0.5),
78
+ defaultState: typeof manifest.defaultState === 'string' ? manifest.defaultState.trim() : '',
79
+ clips: cloneSpecMap(manifest.clips, 'atlas asset manifest clips'),
80
+ states: cloneSpecMap(manifest.states, 'atlas asset manifest states'),
81
+ }
82
+ }
83
+
84
+ export function createAnimationPackageFromAtlasAsset2D(atlasAsset, overrides = {}) {
85
+ if (!atlasAsset?.atlasManifestId) {
86
+ throw new Error('atlas asset manifest is required.')
87
+ }
88
+
89
+ return createAnimationPackage2D({
90
+ name: normalizeAtlasManifestName(overrides.name, atlasAsset.name),
91
+ image: typeof overrides.image === 'string' && overrides.image.trim().length > 0
92
+ ? overrides.image.trim()
93
+ : atlasAsset.image,
94
+ frameWidth: overrides.frameWidth ?? atlasAsset.frameWidth,
95
+ frameHeight: overrides.frameHeight ?? atlasAsset.frameHeight,
96
+ columns: overrides.columns ?? atlasAsset.columns,
97
+ anchorX: overrides.anchorX ?? atlasAsset.anchorX,
98
+ anchorY: overrides.anchorY ?? atlasAsset.anchorY,
99
+ defaultFrameDuration: overrides.defaultFrameDuration ?? atlasAsset.defaultFrameDuration,
100
+ clips: cloneSpecMap(atlasAsset.clips, 'atlas asset manifest clips'),
101
+ states: cloneSpecMap(atlasAsset.states, 'atlas asset manifest states'),
102
+ defaultState: typeof overrides.defaultState === 'string' && overrides.defaultState.trim().length > 0
103
+ ? overrides.defaultState.trim()
104
+ : atlasAsset.defaultState,
105
+ source: {
106
+ type: 'atlas-asset-manifest',
107
+ atlasManifestId: atlasAsset.atlasManifestId,
108
+ image: atlasAsset.image,
109
+ },
110
+ })
111
+ }