@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.
- package/README.md +7 -0
- package/benchmarks/perf-thresholds.json +27 -0
- package/package.json +6 -1
- package/src/ai-guidance.mjs +302 -0
- package/src/authored-project.mjs +498 -2
- package/src/build-contract/capabilities.mjs +87 -1
- package/src/build-contract/constants.mjs +1 -0
- package/src/build-contract.mjs +2 -0
- package/src/bundler.mjs +143 -13
- package/src/cli.mjs +681 -13
- package/src/commands/packs.mjs +741 -0
- package/src/commands/project-authoring.mjs +128 -1
- package/src/conformance/cases/app-and-ui-runtime-cases.mjs +1 -2
- package/src/conformance/cases/core-runtime-cases.mjs +6 -2
- package/src/conformance/cases/scene3d-and-media-cases.mjs +238 -0
- package/src/conformance/cases/systems-and-gameplay-cases.mjs +265 -4
- package/src/conformance-mobile.mjs +166 -0
- package/src/conformance.mjs +89 -30
- package/src/evidence-bundle.mjs +242 -0
- package/src/headless-test/runtime-coordinator.mjs +186 -33
- package/src/headless-test.mjs +2 -0
- package/src/helpers/2d/index.mjs +183 -0
- package/src/helpers/index.mjs +26 -0
- package/src/helpers/starter-utils/adventure-objectives.js +102 -0
- package/src/helpers/starter-utils/adventure-world-2d.js +221 -0
- package/src/helpers/starter-utils/animation-2d.js +337 -0
- package/src/helpers/starter-utils/animation-packaging-2d.js +203 -0
- package/src/helpers/starter-utils/atlas-assets-2d.js +111 -0
- package/src/helpers/starter-utils/autoplay-debug-2d.js +215 -0
- package/src/helpers/starter-utils/avatar-3d.js +404 -0
- package/src/helpers/starter-utils/combat-feedback-2d.js +320 -0
- package/src/helpers/starter-utils/combat-runtime-2d.js +290 -0
- package/src/helpers/starter-utils/core.js +150 -0
- package/src/helpers/starter-utils/dialogue-2d.js +351 -0
- package/src/helpers/starter-utils/enemy-archetypes-2d.js +68 -0
- package/src/helpers/starter-utils/index.js +26 -0
- package/src/helpers/starter-utils/inventory-2d.js +268 -0
- package/src/helpers/starter-utils/journal-2d.js +267 -0
- package/src/helpers/starter-utils/platformer-3d.js +132 -0
- package/src/helpers/starter-utils/scene-audio-2d.js +236 -0
- package/src/helpers/starter-utils/streamed-world-2d.js +378 -0
- package/src/helpers/starter-utils/tilemap-nav-2d.js +499 -0
- package/src/helpers/starter-utils/tilemap-world-2d.js +205 -0
- package/src/helpers/starter-utils/triggers.js +662 -0
- package/src/helpers/starter-utils/tween-2d.js +615 -0
- package/src/helpers/starter-utils/wave-director.js +101 -0
- package/src/helpers/starter-utils/world-compositor-2d.js +253 -0
- package/src/helpers/starter-utils/world-persistence-2d.js +180 -0
- package/src/mobile/android/build.mjs +606 -0
- package/src/mobile/android/host-artifact.mjs +280 -0
- package/src/mobile/ios/build.mjs +1323 -0
- package/src/mobile/ios/host-artifact.mjs +819 -0
- package/src/mobile/shared/capabilities.mjs +174 -0
- package/src/packs/catalog.mjs +259 -0
- package/src/perf-benchmark-runner.mjs +17 -12
- package/src/perf-benchmark.mjs +408 -4
- package/src/publish-command.mjs +303 -6
- package/src/replay-runtime.mjs +257 -0
- package/src/scaffold/config.mjs +2 -0
- package/src/scaffold/fs.mjs +8 -1
- package/src/scaffold/project-docs.mjs +43 -1
- package/src/scaffold.mjs +4 -0
- package/src/session-runtime.mjs +4 -3
- package/src/web-conformance.mjs +0 -36
- package/templates/create/2d-adventure/config/gameplay/adventure.config.js +9 -6
- package/templates/create/2d-adventure/content/gameplay/dialogue.js +85 -0
- package/templates/create/2d-adventure/content/gameplay/world.js +32 -36
- package/templates/create/2d-adventure/content/gameplay/world.tilemap.json +273 -0
- package/templates/create/2d-adventure/docs/design/loop.md +4 -3
- package/templates/create/2d-adventure/prefabs/relic.prefab.js +10 -10
- package/templates/create/2d-adventure/prefabs/world.prefab.js +127 -74
- package/templates/create/2d-adventure/scenes/gameplay.scene.js +603 -112
- package/templates/create/2d-adventure/src/runtime/capabilities.js +16 -0
- package/templates/create/2d-adventure/ui/hud.screen.js +187 -4
- package/templates/create/2d-adventure/ui/journal.screen.js +183 -0
- package/templates/create/3d/scenes/gameplay.scene.js +30 -3
- package/templates/create/3d/src/runtime/capabilities.js +5 -0
- package/templates/create/3d/src/runtime/materials.js +10 -0
- package/templates/create/3d-adventure/scenes/gameplay.scene.js +30 -3
- package/templates/create/3d-adventure/src/runtime/capabilities.js +5 -0
- package/templates/create/3d-adventure/src/runtime/materials.js +11 -0
- package/templates/create/3d-collectathon/scenes/gameplay.scene.js +30 -3
- package/templates/create/3d-collectathon/src/runtime/capabilities.js +5 -0
- package/templates/create/3d-collectathon/src/runtime/materials.js +10 -0
- package/templates/create/shared/src/runtime/ui-forms.js +552 -0
- package/templates/create/shared/src/starter-utils/adventure-world-2d.js +221 -0
- package/templates/create/shared/src/starter-utils/animation-packaging-2d.js +203 -0
- package/templates/create/shared/src/starter-utils/atlas-assets-2d.js +111 -0
- package/templates/create/shared/src/starter-utils/autoplay-debug-2d.js +215 -0
- package/templates/create/shared/src/starter-utils/combat-runtime-2d.js +290 -0
- package/templates/create/shared/src/starter-utils/dialogue-2d.js +351 -0
- package/templates/create/shared/src/starter-utils/index.js +15 -1
- package/templates/create/shared/src/starter-utils/inventory-2d.js +268 -0
- package/templates/create/shared/src/starter-utils/journal-2d.js +267 -0
- package/templates/create/shared/src/starter-utils/scene-audio-2d.js +236 -0
- package/templates/create/shared/src/starter-utils/streamed-world-2d.js +378 -0
- package/templates/create/shared/src/starter-utils/tilemap-nav-2d.js +499 -0
- package/templates/create/shared/src/starter-utils/tilemap-world-2d.js +205 -0
- package/templates/create/shared/src/starter-utils/world-compositor-2d.js +253 -0
- package/templates/create/shared/src/starter-utils/world-persistence-2d.js +180 -0
- package/templates/create-bin/play.js +36 -7
- package/templates/skills/auramaxx/SKILL.md +46 -0
- package/templates/skills/auramaxx/project-requirements.md +68 -0
- package/templates/skills/auramaxx/starter-recipes.md +104 -0
- package/templates/skills/auramaxx/validation-checklist.md +49 -0
- package/templates/skills/aurajs/SKILL.md +0 -96
- package/templates/skills/aurajs/api-contract-3d.md +0 -7
- package/templates/skills/aurajs/api-contract.md +0 -7
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
function finiteNumber(value, fallback) {
|
|
2
|
+
const numeric = Number(value)
|
|
3
|
+
return Number.isFinite(numeric) ? numeric : fallback
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function nonNegativeNumber(value, fallback) {
|
|
7
|
+
return Math.max(0, finiteNumber(value, fallback))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function hasAudioMethod(sceneAudio, name) {
|
|
11
|
+
return !!sceneAudio?.aura?.audio
|
|
12
|
+
&& sceneAudio.aura.audio.supported !== false
|
|
13
|
+
&& typeof sceneAudio.aura.audio[name] === 'function'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function mixerTracksHandle(sceneAudio, handle) {
|
|
17
|
+
if (handle == null || !hasAudioMethod(sceneAudio, 'getMixerState')) {
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const mixer = sceneAudio.aura.audio.getMixerState()
|
|
22
|
+
return Array.isArray(mixer?.tracks)
|
|
23
|
+
? mixer.tracks.some((track) => Number(track?.handle) === Number(handle))
|
|
24
|
+
: true
|
|
25
|
+
} catch {
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createSceneAudio2D(aura, options = {}) {
|
|
31
|
+
return {
|
|
32
|
+
aura,
|
|
33
|
+
configured: false,
|
|
34
|
+
musicHandle: null,
|
|
35
|
+
musicPath: '',
|
|
36
|
+
musicBus: typeof options.musicBus === 'string' && options.musicBus.trim().length > 0
|
|
37
|
+
? options.musicBus.trim()
|
|
38
|
+
: 'music',
|
|
39
|
+
sfxBus: typeof options.sfxBus === 'string' && options.sfxBus.trim().length > 0
|
|
40
|
+
? options.sfxBus.trim()
|
|
41
|
+
: 'sfx',
|
|
42
|
+
busVolumes: {
|
|
43
|
+
music: nonNegativeNumber(options.musicBusVolume, 0.18),
|
|
44
|
+
sfx: nonNegativeNumber(options.sfxBusVolume, 0.82),
|
|
45
|
+
},
|
|
46
|
+
defaultMusicOptions: {
|
|
47
|
+
volume: nonNegativeNumber(options.defaultMusicOptions?.volume, 0.58),
|
|
48
|
+
pitch: finiteNumber(options.defaultMusicOptions?.pitch, 0.98),
|
|
49
|
+
},
|
|
50
|
+
defaultStingerOptions: {
|
|
51
|
+
volume: nonNegativeNumber(options.defaultStingerOptions?.volume, 0.84),
|
|
52
|
+
pitch: finiteNumber(options.defaultStingerOptions?.pitch, 1),
|
|
53
|
+
},
|
|
54
|
+
musicVolume: -1,
|
|
55
|
+
pauseVolume: nonNegativeNumber(options.pauseVolume, 0.08),
|
|
56
|
+
paused: false,
|
|
57
|
+
duckTimer: 0,
|
|
58
|
+
duckVolume: null,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function configureSceneAudio2D(sceneAudio) {
|
|
63
|
+
if (!sceneAudio || sceneAudio.configured || !sceneAudio.aura?.audio) {
|
|
64
|
+
return !!sceneAudio?.aura?.audio
|
|
65
|
+
}
|
|
66
|
+
sceneAudio.configured = true
|
|
67
|
+
if (hasAudioMethod(sceneAudio, 'setBusVolume')) {
|
|
68
|
+
try {
|
|
69
|
+
sceneAudio.aura.audio.setBusVolume(sceneAudio.musicBus, sceneAudio.busVolumes.music)
|
|
70
|
+
} catch {}
|
|
71
|
+
try {
|
|
72
|
+
sceneAudio.aura.audio.setBusVolume(sceneAudio.sfxBus, sceneAudio.busVolumes.sfx)
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function setSceneMusicVolume2D(sceneAudio, volume) {
|
|
79
|
+
if (!sceneAudio || !hasAudioMethod(sceneAudio, 'setBusVolume')) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
const resolvedVolume = nonNegativeNumber(volume, sceneAudio.busVolumes.music)
|
|
83
|
+
if (Math.abs(sceneAudio.musicVolume - resolvedVolume) <= 0.01) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
sceneAudio.musicVolume = resolvedVolume
|
|
87
|
+
try {
|
|
88
|
+
sceneAudio.aura.audio.setBusVolume(sceneAudio.musicBus, resolvedVolume)
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function stopSceneAudio2D(sceneAudio, options = {}) {
|
|
93
|
+
if (!sceneAudio) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (sceneAudio.musicHandle != null && hasAudioMethod(sceneAudio, 'stop')) {
|
|
97
|
+
try {
|
|
98
|
+
sceneAudio.aura.audio.stop(sceneAudio.musicHandle)
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
sceneAudio.musicHandle = null
|
|
102
|
+
sceneAudio.musicPath = ''
|
|
103
|
+
sceneAudio.duckTimer = 0
|
|
104
|
+
sceneAudio.duckVolume = null
|
|
105
|
+
if (options.clearPause !== false) {
|
|
106
|
+
sceneAudio.paused = false
|
|
107
|
+
}
|
|
108
|
+
if (options.resetConfiguration === true) {
|
|
109
|
+
sceneAudio.musicVolume = -1
|
|
110
|
+
sceneAudio.configured = false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function ensureSceneMusic2D(sceneAudio, path, options = {}) {
|
|
115
|
+
configureSceneAudio2D(sceneAudio)
|
|
116
|
+
if (!sceneAudio || typeof path !== 'string' || path.trim().length === 0 || !hasAudioMethod(sceneAudio, 'play')) {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
const resolvedPath = path.trim()
|
|
120
|
+
if (
|
|
121
|
+
sceneAudio.musicHandle != null
|
|
122
|
+
&& sceneAudio.musicPath === resolvedPath
|
|
123
|
+
&& mixerTracksHandle(sceneAudio, sceneAudio.musicHandle)
|
|
124
|
+
) {
|
|
125
|
+
return sceneAudio.musicHandle
|
|
126
|
+
}
|
|
127
|
+
stopSceneAudio2D(sceneAudio, {
|
|
128
|
+
clearPause: false,
|
|
129
|
+
resetConfiguration: false,
|
|
130
|
+
})
|
|
131
|
+
try {
|
|
132
|
+
sceneAudio.musicHandle = sceneAudio.aura.audio.play(resolvedPath, {
|
|
133
|
+
loop: true,
|
|
134
|
+
bus: sceneAudio.musicBus,
|
|
135
|
+
...sceneAudio.defaultMusicOptions,
|
|
136
|
+
...options,
|
|
137
|
+
})
|
|
138
|
+
sceneAudio.musicPath = resolvedPath
|
|
139
|
+
} catch {}
|
|
140
|
+
return sceneAudio.musicHandle
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function duckSceneMusic2D(sceneAudio, options = {}) {
|
|
144
|
+
if (!sceneAudio) {
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
const duration = nonNegativeNumber(options.duration, 0.16)
|
|
148
|
+
const volume = nonNegativeNumber(options.volume, 0.14)
|
|
149
|
+
sceneAudio.duckTimer = Math.max(sceneAudio.duckTimer, duration)
|
|
150
|
+
sceneAudio.duckVolume = sceneAudio.duckVolume == null
|
|
151
|
+
? volume
|
|
152
|
+
: Math.min(sceneAudio.duckVolume, volume)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function playSceneStinger2D(sceneAudio, path, options = {}) {
|
|
156
|
+
configureSceneAudio2D(sceneAudio)
|
|
157
|
+
if (!sceneAudio || typeof path !== 'string' || path.trim().length === 0 || !hasAudioMethod(sceneAudio, 'play')) {
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
const resolvedPath = path.trim()
|
|
161
|
+
const playOptions = {
|
|
162
|
+
loop: false,
|
|
163
|
+
bus: sceneAudio.sfxBus,
|
|
164
|
+
...sceneAudio.defaultStingerOptions,
|
|
165
|
+
...options,
|
|
166
|
+
}
|
|
167
|
+
delete playOptions.duck
|
|
168
|
+
let handle = null
|
|
169
|
+
try {
|
|
170
|
+
handle = sceneAudio.aura.audio.play(resolvedPath, playOptions)
|
|
171
|
+
} catch {}
|
|
172
|
+
if (options.duck) {
|
|
173
|
+
duckSceneMusic2D(sceneAudio, options.duck === true ? {} : options.duck)
|
|
174
|
+
}
|
|
175
|
+
return handle
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function pauseSceneAudio2D(sceneAudio, options = {}) {
|
|
179
|
+
if (!sceneAudio) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
sceneAudio.paused = true
|
|
183
|
+
sceneAudio.pauseVolume = nonNegativeNumber(options.volume, sceneAudio.pauseVolume)
|
|
184
|
+
if (options.pauseMusic !== false && sceneAudio.musicHandle != null && hasAudioMethod(sceneAudio, 'pause')) {
|
|
185
|
+
try {
|
|
186
|
+
sceneAudio.aura.audio.pause(sceneAudio.musicHandle)
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function resumeSceneAudio2D(sceneAudio, options = {}) {
|
|
192
|
+
if (!sceneAudio) {
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
sceneAudio.paused = false
|
|
196
|
+
if (options.resumeMusic !== false && sceneAudio.musicHandle != null && hasAudioMethod(sceneAudio, 'resume')) {
|
|
197
|
+
try {
|
|
198
|
+
sceneAudio.aura.audio.resume(sceneAudio.musicHandle)
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function syncSceneAudio2D(sceneAudio, dt, options = {}) {
|
|
204
|
+
configureSceneAudio2D(sceneAudio)
|
|
205
|
+
if (!sceneAudio) {
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
const safeDt = nonNegativeNumber(dt, 0)
|
|
209
|
+
if (hasAudioMethod(sceneAudio, 'update')) {
|
|
210
|
+
try {
|
|
211
|
+
sceneAudio.aura.audio.update(safeDt)
|
|
212
|
+
} catch {}
|
|
213
|
+
}
|
|
214
|
+
if (typeof options.musicPath === 'string' && options.musicPath.trim().length > 0) {
|
|
215
|
+
ensureSceneMusic2D(sceneAudio, options.musicPath, options.musicOptions)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (sceneAudio.duckTimer > 0) {
|
|
219
|
+
sceneAudio.duckTimer = Math.max(0, sceneAudio.duckTimer - safeDt)
|
|
220
|
+
if (sceneAudio.duckTimer <= 0) {
|
|
221
|
+
sceneAudio.duckVolume = null
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let targetVolume = options.musicVolume != null
|
|
226
|
+
? nonNegativeNumber(options.musicVolume, sceneAudio.busVolumes.music)
|
|
227
|
+
: sceneAudio.busVolumes.music
|
|
228
|
+
if (sceneAudio.paused) {
|
|
229
|
+
targetVolume = Math.min(targetVolume, sceneAudio.pauseVolume)
|
|
230
|
+
}
|
|
231
|
+
if (sceneAudio.duckTimer > 0 && sceneAudio.duckVolume != null) {
|
|
232
|
+
targetVolume = Math.min(targetVolume, sceneAudio.duckVolume)
|
|
233
|
+
}
|
|
234
|
+
setSceneMusicVolume2D(sceneAudio, targetVolume)
|
|
235
|
+
return sceneAudio.musicHandle
|
|
236
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTilemapWorld2D,
|
|
3
|
+
ensureTilemapWorldLoaded2D,
|
|
4
|
+
unloadTilemapWorld2D,
|
|
5
|
+
queryTilemapWorldPoint2D,
|
|
6
|
+
queryTilemapWorldAABB2D,
|
|
7
|
+
} from './tilemap-world-2d.js';
|
|
8
|
+
|
|
9
|
+
function finite(value, fallback = 0) {
|
|
10
|
+
const numeric = Number(value);
|
|
11
|
+
return Number.isFinite(numeric) ? numeric : fallback;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeText(value, fallback = null) {
|
|
15
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
16
|
+
return value.trim();
|
|
17
|
+
}
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveAura(auraRef = globalThis.aura) {
|
|
22
|
+
return auraRef && typeof auraRef === 'object' ? auraRef : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cloneBounds(bounds = {}) {
|
|
26
|
+
return {
|
|
27
|
+
x: finite(bounds.x),
|
|
28
|
+
y: finite(bounds.y),
|
|
29
|
+
w: Math.max(0, finite(bounds.w ?? bounds.width)),
|
|
30
|
+
h: Math.max(0, finite(bounds.h ?? bounds.height)),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function boundsEmpty(bounds) {
|
|
35
|
+
return !bounds || bounds.w <= 0 || bounds.h <= 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pointInBounds(bounds, point = {}) {
|
|
39
|
+
if (boundsEmpty(bounds)) return false;
|
|
40
|
+
const x = finite(point.x, Number.NaN);
|
|
41
|
+
const y = finite(point.y, Number.NaN);
|
|
42
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
|
|
43
|
+
return x >= bounds.x
|
|
44
|
+
&& y >= bounds.y
|
|
45
|
+
&& x < (bounds.x + bounds.w)
|
|
46
|
+
&& y < (bounds.y + bounds.h);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function boundsIntersect(a, b) {
|
|
50
|
+
if (boundsEmpty(a) || boundsEmpty(b)) return false;
|
|
51
|
+
return a.x < (b.x + b.w)
|
|
52
|
+
&& (a.x + a.w) > b.x
|
|
53
|
+
&& a.y < (b.y + b.h)
|
|
54
|
+
&& (a.y + a.h) > b.y;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function focusBounds(point = {}, marginX = 0, marginY = marginX) {
|
|
58
|
+
const x = finite(point.x);
|
|
59
|
+
const y = finite(point.y);
|
|
60
|
+
const w = Math.max(0, finite(marginX)) * 2;
|
|
61
|
+
const h = Math.max(0, finite(marginY)) * 2;
|
|
62
|
+
return {
|
|
63
|
+
x: x - (w * 0.5),
|
|
64
|
+
y: y - (h * 0.5),
|
|
65
|
+
w,
|
|
66
|
+
h,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeRegion(entry, index) {
|
|
71
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
72
|
+
const bounds = cloneBounds(entry.bounds ?? entry);
|
|
73
|
+
if (bounds.w <= 0 || bounds.h <= 0) return null;
|
|
74
|
+
return {
|
|
75
|
+
id: normalizeText(entry.id, `region-${index + 1}`),
|
|
76
|
+
label: normalizeText(entry.label, normalizeText(entry.id, `Region ${index + 1}`)),
|
|
77
|
+
order: Number.isFinite(Number(entry.order)) ? Number(entry.order) : index,
|
|
78
|
+
bounds,
|
|
79
|
+
meta: entry.meta && typeof entry.meta === 'object' ? { ...entry.meta } : null,
|
|
80
|
+
build: typeof entry.build === 'function' ? entry.build : null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sortRegions(entries = []) {
|
|
85
|
+
return [...entries].sort((a, b) => {
|
|
86
|
+
return a.order - b.order
|
|
87
|
+
|| a.bounds.y - b.bounds.y
|
|
88
|
+
|| a.bounds.x - b.bounds.x
|
|
89
|
+
|| a.id.localeCompare(b.id);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeQueryHit(hit, record) {
|
|
94
|
+
if (!hit || typeof hit !== 'object') return hit;
|
|
95
|
+
return {
|
|
96
|
+
...hit,
|
|
97
|
+
localX: finite(hit.x),
|
|
98
|
+
localY: finite(hit.y),
|
|
99
|
+
x: finite(hit.x) + finite(record.state?.tileLeft),
|
|
100
|
+
y: finite(hit.y) + finite(record.state?.tileTop),
|
|
101
|
+
regionId: record.id,
|
|
102
|
+
regionLabel: record.label,
|
|
103
|
+
worldTileX: finite(hit.x) + finite(record.state?.tileLeft),
|
|
104
|
+
worldTileY: finite(hit.y) + finite(record.state?.tileTop),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeRayResult(result, record) {
|
|
109
|
+
if (!result || typeof result !== 'object') return null;
|
|
110
|
+
if (result.hit !== true) {
|
|
111
|
+
return {
|
|
112
|
+
...result,
|
|
113
|
+
regionId: record.id,
|
|
114
|
+
regionLabel: record.label,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const point = result.point && typeof result.point === 'object'
|
|
118
|
+
? {
|
|
119
|
+
x: finite(result.point.x) + finite(record.world.offsetX),
|
|
120
|
+
y: finite(result.point.y) + finite(record.world.offsetY),
|
|
121
|
+
}
|
|
122
|
+
: null;
|
|
123
|
+
const hitCell = result.hitCell && typeof result.hitCell === 'object'
|
|
124
|
+
? {
|
|
125
|
+
...result.hitCell,
|
|
126
|
+
localX: finite(result.hitCell.x),
|
|
127
|
+
localY: finite(result.hitCell.y),
|
|
128
|
+
x: finite(result.hitCell.x) + finite(record.state?.tileLeft),
|
|
129
|
+
y: finite(result.hitCell.y) + finite(record.state?.tileTop),
|
|
130
|
+
regionId: record.id,
|
|
131
|
+
regionLabel: record.label,
|
|
132
|
+
worldTileX: finite(result.hitCell.x) + finite(record.state?.tileLeft),
|
|
133
|
+
worldTileY: finite(result.hitCell.y) + finite(record.state?.tileTop),
|
|
134
|
+
}
|
|
135
|
+
: null;
|
|
136
|
+
return {
|
|
137
|
+
...result,
|
|
138
|
+
regionId: record.id,
|
|
139
|
+
regionLabel: record.label,
|
|
140
|
+
point,
|
|
141
|
+
hitCell,
|
|
142
|
+
hits: Array.isArray(result.hits) ? result.hits.map((hit) => normalizeQueryHit(hit, record)) : [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function createLoadedRecord(stream, descriptor, built) {
|
|
147
|
+
const world = built?.world ?? createTilemapWorld2D({
|
|
148
|
+
mapSource: built?.mapSource ?? null,
|
|
149
|
+
offsetX: descriptor.bounds.x,
|
|
150
|
+
offsetY: descriptor.bounds.y,
|
|
151
|
+
cull: built?.cull,
|
|
152
|
+
includeHidden: built?.includeHidden,
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
id: descriptor.id,
|
|
156
|
+
label: descriptor.label,
|
|
157
|
+
order: descriptor.order,
|
|
158
|
+
descriptor,
|
|
159
|
+
bounds: descriptor.bounds,
|
|
160
|
+
world,
|
|
161
|
+
nav: built?.nav ?? built?.state?.nav ?? null,
|
|
162
|
+
state: built?.state ?? null,
|
|
163
|
+
anchors: built?.anchors ?? null,
|
|
164
|
+
lastTouchedSync: stream.syncCount,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function ensureRegionLoaded(stream, descriptor, auraRef = globalThis.aura) {
|
|
169
|
+
if (!stream || !descriptor) return null;
|
|
170
|
+
const existing = stream.loaded.get(descriptor.id);
|
|
171
|
+
if (existing) {
|
|
172
|
+
existing.lastTouchedSync = stream.syncCount;
|
|
173
|
+
ensureTilemapWorldLoaded2D(existing.world, auraRef);
|
|
174
|
+
return existing;
|
|
175
|
+
}
|
|
176
|
+
const built = typeof descriptor.build === 'function'
|
|
177
|
+
? descriptor.build(descriptor, stream) || null
|
|
178
|
+
: (typeof stream.buildRegion === 'function' ? stream.buildRegion(descriptor, stream) || null : null);
|
|
179
|
+
const record = createLoadedRecord(stream, descriptor, built);
|
|
180
|
+
ensureTilemapWorldLoaded2D(record.world, auraRef);
|
|
181
|
+
stream.loaded.set(record.id, record);
|
|
182
|
+
return record;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function loadedRecordsForBounds(stream, bounds) {
|
|
186
|
+
return listLoadedStreamedWorldRegions2D(stream).filter((record) => boundsIntersect(record.bounds, bounds));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function pickActiveRegion(stream, point = {}) {
|
|
190
|
+
return findStreamedWorldRegionAtPoint2D(stream, point, { loadedOnly: false });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function createStreamedWorld2D(options = {}) {
|
|
194
|
+
const regions = sortRegions((Array.isArray(options.regions) ? options.regions : [])
|
|
195
|
+
.map((entry, index) => normalizeRegion(entry, index))
|
|
196
|
+
.filter(Boolean));
|
|
197
|
+
return {
|
|
198
|
+
regions,
|
|
199
|
+
regionById: new Map(regions.map((entry) => [entry.id, entry])),
|
|
200
|
+
buildRegion: typeof options.buildRegion === 'function' ? options.buildRegion : null,
|
|
201
|
+
focusMarginX: Math.max(0, finite(options.focusMarginX ?? options.focusMargin ?? 0)),
|
|
202
|
+
focusMarginY: Math.max(0, finite(options.focusMarginY ?? options.focusMargin ?? 0)),
|
|
203
|
+
unloadMarginX: Math.max(0, finite(options.unloadMarginX ?? options.unloadMargin ?? options.focusMarginX ?? options.focusMargin ?? 0)),
|
|
204
|
+
unloadMarginY: Math.max(0, finite(options.unloadMarginY ?? options.unloadMargin ?? options.focusMarginY ?? options.focusMargin ?? 0)),
|
|
205
|
+
loaded: new Map(),
|
|
206
|
+
focus: null,
|
|
207
|
+
activeRegionId: null,
|
|
208
|
+
syncCount: 0,
|
|
209
|
+
lastSync: null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function listLoadedStreamedWorldRegions2D(stream) {
|
|
214
|
+
if (!stream || !(stream.loaded instanceof Map)) return [];
|
|
215
|
+
return sortRegions([...stream.loaded.values()]);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function findStreamedWorldRegionAtPoint2D(stream, point = {}, options = {}) {
|
|
219
|
+
if (!stream) return null;
|
|
220
|
+
const entries = options.loadedOnly === false
|
|
221
|
+
? stream.regions
|
|
222
|
+
: listLoadedStreamedWorldRegions2D(stream);
|
|
223
|
+
return entries.find((entry) => pointInBounds(entry.bounds, point)) || null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function syncStreamedWorldFocus2D(stream, auraRef = globalThis.aura, point = {}) {
|
|
227
|
+
if (!stream || !Array.isArray(stream.regions) || stream.regions.length === 0) {
|
|
228
|
+
return {
|
|
229
|
+
activeRegionId: null,
|
|
230
|
+
activeRegion: null,
|
|
231
|
+
enteredRegionIds: [],
|
|
232
|
+
exitedRegionIds: [],
|
|
233
|
+
loadedRegionIds: [],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const aura = resolveAura(auraRef);
|
|
238
|
+
stream.syncCount += 1;
|
|
239
|
+
stream.focus = { x: finite(point.x), y: finite(point.y) };
|
|
240
|
+
|
|
241
|
+
const activeDescriptor = pickActiveRegion(stream, stream.focus);
|
|
242
|
+
const activeRegionId = activeDescriptor?.id ?? null;
|
|
243
|
+
const activeBounds = focusBounds(stream.focus, stream.focusMarginX, stream.focusMarginY);
|
|
244
|
+
const retainBounds = focusBounds(stream.focus, stream.unloadMarginX, stream.unloadMarginY);
|
|
245
|
+
const desired = new Set();
|
|
246
|
+
stream.regions.forEach((descriptor) => {
|
|
247
|
+
if (descriptor.id === activeRegionId || boundsIntersect(descriptor.bounds, activeBounds)) {
|
|
248
|
+
desired.add(descriptor.id);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const enteredRegionIds = [];
|
|
253
|
+
desired.forEach((regionId) => {
|
|
254
|
+
const descriptor = stream.regionById.get(regionId);
|
|
255
|
+
const wasLoaded = stream.loaded.has(regionId);
|
|
256
|
+
const record = ensureRegionLoaded(stream, descriptor, aura);
|
|
257
|
+
if (record) {
|
|
258
|
+
record.lastTouchedSync = stream.syncCount;
|
|
259
|
+
if (!wasLoaded) enteredRegionIds.push(regionId);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const exitedRegionIds = [];
|
|
264
|
+
for (const record of listLoadedStreamedWorldRegions2D(stream)) {
|
|
265
|
+
if (desired.has(record.id)) continue;
|
|
266
|
+
if (boundsIntersect(record.bounds, retainBounds)) continue;
|
|
267
|
+
if (unloadTilemapWorld2D(record.world, aura)) {
|
|
268
|
+
stream.loaded.delete(record.id);
|
|
269
|
+
exitedRegionIds.push(record.id);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
stream.activeRegionId = activeRegionId;
|
|
274
|
+
const activeRegion = activeRegionId ? (stream.loaded.get(activeRegionId) ?? ensureRegionLoaded(stream, activeDescriptor, aura)) : null;
|
|
275
|
+
const loadedRegionIds = listLoadedStreamedWorldRegions2D(stream).map((record) => record.id);
|
|
276
|
+
stream.lastSync = {
|
|
277
|
+
focus: { ...stream.focus },
|
|
278
|
+
activeRegionId,
|
|
279
|
+
enteredRegionIds,
|
|
280
|
+
exitedRegionIds,
|
|
281
|
+
loadedRegionIds,
|
|
282
|
+
};
|
|
283
|
+
return {
|
|
284
|
+
focus: { ...stream.focus },
|
|
285
|
+
activeRegionId,
|
|
286
|
+
activeRegion,
|
|
287
|
+
enteredRegionIds,
|
|
288
|
+
exitedRegionIds,
|
|
289
|
+
loadedRegionIds,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function queryStreamedWorldPoint2D(stream, auraRef = globalThis.aura, point = {}) {
|
|
294
|
+
const record = findStreamedWorldRegionAtPoint2D(stream, point);
|
|
295
|
+
if (!record) {
|
|
296
|
+
return { ok: false, hit: false, hits: [], reasonCode: 'region_not_loaded' };
|
|
297
|
+
}
|
|
298
|
+
const result = queryTilemapWorldPoint2D(record.world, auraRef, point) || null;
|
|
299
|
+
if (!result || typeof result !== 'object') return result;
|
|
300
|
+
return {
|
|
301
|
+
...result,
|
|
302
|
+
regionId: record.id,
|
|
303
|
+
regionLabel: record.label,
|
|
304
|
+
hits: Array.isArray(result.hits) ? result.hits.map((hit) => normalizeQueryHit(hit, record)) : [],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function queryStreamedWorldAABB2D(stream, auraRef = globalThis.aura, bounds = {}) {
|
|
309
|
+
const targetBounds = cloneBounds(bounds);
|
|
310
|
+
const candidates = loadedRecordsForBounds(stream, targetBounds);
|
|
311
|
+
if (candidates.length === 0) {
|
|
312
|
+
return { ok: false, hit: false, hits: [], reasonCode: 'region_not_loaded' };
|
|
313
|
+
}
|
|
314
|
+
const hits = [];
|
|
315
|
+
candidates.forEach((record) => {
|
|
316
|
+
const result = queryTilemapWorldAABB2D(record.world, auraRef, targetBounds) || null;
|
|
317
|
+
if (!Array.isArray(result?.hits)) return;
|
|
318
|
+
result.hits.forEach((hit) => {
|
|
319
|
+
hits.push(normalizeQueryHit(hit, record));
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
return {
|
|
323
|
+
ok: true,
|
|
324
|
+
hit: hits.length > 0,
|
|
325
|
+
hits,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function queryStreamedWorldRay2D(stream, auraRef = globalThis.aura, ray = {}) {
|
|
330
|
+
const aura = resolveAura(auraRef);
|
|
331
|
+
if (!aura?.tilemap || typeof aura.tilemap.queryRay !== 'function') {
|
|
332
|
+
return { ok: false, hit: false, hits: [], reasonCode: 'query_ray_unavailable' };
|
|
333
|
+
}
|
|
334
|
+
const candidates = listLoadedStreamedWorldRegions2D(stream);
|
|
335
|
+
let best = null;
|
|
336
|
+
candidates.forEach((record) => {
|
|
337
|
+
if (!ensureTilemapWorldLoaded2D(record.world, aura)) return;
|
|
338
|
+
const localRay = {
|
|
339
|
+
x: finite(ray.x) - finite(record.world.offsetX),
|
|
340
|
+
y: finite(ray.y) - finite(record.world.offsetY),
|
|
341
|
+
dx: finite(ray.dx),
|
|
342
|
+
dy: finite(ray.dy),
|
|
343
|
+
maxDistance: Math.max(0, finite(ray.maxDistance, 0)),
|
|
344
|
+
};
|
|
345
|
+
const result = aura.tilemap.queryRay(record.world.mapId, localRay) || null;
|
|
346
|
+
if (!result || result.hit !== true || !Number.isFinite(Number(result.distance))) return;
|
|
347
|
+
const normalized = normalizeRayResult(result, record);
|
|
348
|
+
if (!best || Number(normalized.distance) < Number(best.distance)) {
|
|
349
|
+
best = normalized;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
if (best) {
|
|
353
|
+
return best;
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
ok: true,
|
|
357
|
+
hit: false,
|
|
358
|
+
hits: [],
|
|
359
|
+
hitCell: null,
|
|
360
|
+
distance: null,
|
|
361
|
+
point: null,
|
|
362
|
+
reasonCode: null,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function unloadStreamedWorld2D(stream, auraRef = globalThis.aura) {
|
|
367
|
+
if (!stream || !(stream.loaded instanceof Map)) return false;
|
|
368
|
+
const aura = resolveAura(auraRef);
|
|
369
|
+
let unloaded = false;
|
|
370
|
+
for (const record of listLoadedStreamedWorldRegions2D(stream)) {
|
|
371
|
+
unloaded = unloadTilemapWorld2D(record.world, aura) || unloaded;
|
|
372
|
+
}
|
|
373
|
+
stream.loaded.clear();
|
|
374
|
+
stream.focus = null;
|
|
375
|
+
stream.activeRegionId = null;
|
|
376
|
+
stream.lastSync = null;
|
|
377
|
+
return unloaded;
|
|
378
|
+
}
|