@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
|
@@ -1404,6 +1404,10 @@ export const SYSTEMS_AND_GAMEPLAY_CONFORMANCE_CASES = [
|
|
|
1404
1404
|
id: 'optional.legacy-steam.not-exposed',
|
|
1405
1405
|
expression: "typeof aura.steam === 'undefined' && !Object.prototype.hasOwnProperty.call(aura, 'steam')",
|
|
1406
1406
|
},
|
|
1407
|
+
{
|
|
1408
|
+
id: 'optional.legacy-steam.provider-not-exposed',
|
|
1409
|
+
expression: "typeof aura.providers === 'undefined' || typeof aura.providers.steam === 'undefined'",
|
|
1410
|
+
},
|
|
1407
1411
|
{
|
|
1408
1412
|
id: 'optional.legacy-steam.physics-disabled-guidance',
|
|
1409
1413
|
expression: "(() => { try { aura.physics.step(0.016); return false; } catch (e) { const msg = String(e); return msg.includes('optional module \"physics\" is disabled') && msg.includes('modules.physics = true') && msg.includes('[reason:optional_module_physics_disabled]'); } })()",
|
|
@@ -1857,15 +1861,15 @@ export const SYSTEMS_AND_GAMEPLAY_CONFORMANCE_CASES = [
|
|
|
1857
1861
|
},
|
|
1858
1862
|
{
|
|
1859
1863
|
id: 'draw3d.postfx.target-chain.custom-pass.deterministic',
|
|
1860
|
-
expression: "(() => { const runSample = () => { const baseline = aura.draw3d.getPostFXState(); aura.draw3d.removePostFXPass('bloom'); aura.draw3d.removePostFXPass('colorGrade'); aura.draw3d.removePostFXPass('vignette'); aura.draw3d.removePostFXPass('fxaa'); aura.draw3d.removePostFXPass('
|
|
1864
|
+
expression: "(() => { const runSample = () => { const baseline = aura.draw3d.getPostFXState(); aura.draw3d.removePostFXPass('bloom'); aura.draw3d.removePostFXPass('colorGrade'); aura.draw3d.removePostFXPass('vignette'); aura.draw3d.removePostFXPass('fxaa'); aura.draw3d.removePostFXPass('filmGrain'); aura.draw3d.removePostFXPass('custom:glow'); const setBloom = aura.draw3d.setPostFXPass('bloom', { strength: 1.1, targetChain: { intermediateTargets: ['mainA', 'mainB', 'mainA'], pingPong: true, composeMode: 'additive' } }); const setCustom = aura.draw3d.setPostFXPass('custom:glow', { customParams: { intensity: 0.75, threshold: 0.33 } }); const updateCustom = aura.draw3d.setPostFXPass('custom:glow', { customParams: { intensity: 0.5, threshold: 0.2 } }); const setFilmgrain = aura.draw3d.setPostFXPass('filmGrain', { customParams: { amount: 0.2, speed: 0.1 } }); const state = aura.draw3d.getPostFXState(); const targetChain = state?.targetChain || {}; const customGlow = Array.isArray(state?.passes) ? state.passes.find((pass) => pass.pass === 'custom:glow') : null; const customParams = customGlow?.customParams || {}; const customParamKeys = Object.keys(customParams).sort().join('|'); return { status: [setBloom?.reasonCode, setCustom?.reasonCode, updateCustom?.reasonCode, setFilmgrain?.reasonCode].join('|'), order: Array.isArray(state?.passes) ? state.passes.map((pass) => pass.pass).join('|') : '', enabled: state?.enabledPasses, total: state?.totalPasses, customPassCount: state?.customPassCount, targetChainTargets: Array.isArray(targetChain?.intermediateTargets) ? targetChain.intermediateTargets.join('|') : '', targetChainCount: targetChain?.intermediateTargetCount, targetChainPingPong: targetChain?.pingPong === true, targetChainComposeMode: targetChain?.composeMode, targetChainFingerprint: state?.targetChainFingerprint, customParamFingerprint: state?.customParamFingerprint, customGlowIsCustom: customGlow?.isCustom === true, customGlowParamKeys: customParamKeys, customGlowIntensity: customParams?.intensity, customGlowThreshold: customParams?.threshold, mutationDelta: Number(state?.mutationCount || 0) - Number(baseline?.mutationCount || 0), lastReasonCode: state?.lastReasonCode, lastOk: state?.lastOk }; }; const first = runSample(); const second = runSample(); return JSON.stringify(first) === JSON.stringify(second) && first.status === 'postfx_ok|postfx_ok|postfx_ok|postfx_ok' && first.order === 'bloom|filmGrain|custom:glow' && first.total === 3 && first.enabled === 3 && first.customPassCount === 1 && first.targetChainTargets === 'maina|mainb' && first.targetChainCount === 2 && first.targetChainPingPong === true && first.targetChainComposeMode === 'additive' && Number.isFinite(first.targetChainFingerprint) && first.targetChainFingerprint > 0 && Number.isFinite(first.customParamFingerprint) && first.customParamFingerprint > 0 && first.customGlowIsCustom === true && first.customGlowParamKeys === 'intensity|threshold' && Math.abs(Number(first.customGlowIntensity) - 0.5) < 1e-3 && Math.abs(Number(first.customGlowThreshold) - 0.2) < 1e-3 && first.mutationDelta === 10 && first.lastReasonCode === 'postfx_ok' && first.lastOk === true; })()",
|
|
1861
1865
|
},
|
|
1862
1866
|
{
|
|
1863
1867
|
id: 'draw3d.postfx.motion-blur.state.deterministic',
|
|
1864
|
-
expression: "(() => { const resetPasses = () => { for (const passName of ['ssr', 'dof', 'motionBlur', 'bloom', 'colorGrade', 'vignette', 'fxaa', 'ssao', '
|
|
1868
|
+
expression: "(() => { const resetPasses = () => { for (const passName of ['ssr', 'dof', 'motionBlur', 'bloom', 'colorGrade', 'vignette', 'fxaa', 'ssao', 'filmGrain', 'custom:glow']) { aura.draw3d.removePostFXPass(passName); } }; const runSample = () => { resetPasses(); const baseline = aura.draw3d.getPostFXState() || {}; const setInitial = aura.draw3d.setPostFXPass('motionBlur', { strength: 0.75, radius: 12, threshold: 1.0, targetChain: { intermediateTargets: [], pingPong: false, composeMode: 'replace' } }); const disable = aura.draw3d.setPostFXEnabled('motionBlur', false); const reenable = aura.draw3d.setPostFXEnabled('motionBlur', true); const update = aura.draw3d.setPostFXPass('motionBlur', { strength: 0.35, radius: 6, threshold: 0.0 }); const state = aura.draw3d.getPostFXState() || {}; const pass = Array.isArray(state?.passes) ? state.passes.find((entry) => entry.pass === 'motionBlur') : null; return { status: [setInitial?.reasonCode, disable?.reasonCode, reenable?.reasonCode, update?.reasonCode].join('|'), order: Array.isArray(state?.passes) ? state.passes.map((entry) => entry.pass).join('|') : '', total: Number(state?.totalPasses || 0), enabled: Number(state?.enabledPasses || 0), strength: Number(pass?.strength || 0), radius: Number(pass?.radius || 0), threshold: Number(pass?.threshold || 0), mutationDelta: Number(state?.mutationCount || 0) - Number(baseline?.mutationCount || 0), lastReasonCode: state?.lastReasonCode, lastOk: state?.lastOk, targetChainCount: Number(state?.targetChain?.intermediateTargetCount || 0), targetChainPingPong: state?.targetChain?.pingPong === true, targetChainComposeMode: state?.targetChain?.composeMode || null }; }; const first = runSample(); const second = runSample(); return JSON.stringify(first) === JSON.stringify(second) && first.status === 'postfx_ok|postfx_ok|postfx_ok|postfx_ok' && first.order === 'motionBlur' && first.total === 1 && first.enabled === 1 && Math.abs(first.strength - 0.35) < 1e-3 && Math.abs(first.radius - 6.0) < 1e-3 && Math.abs(first.threshold - 0.0) < 1e-3 && first.targetChainCount === 0 && first.targetChainPingPong === false && first.targetChainComposeMode === 'replace' && first.mutationDelta === 4 && first.lastReasonCode === 'postfx_ok' && first.lastOk === true; })()",
|
|
1865
1869
|
},
|
|
1866
1870
|
{
|
|
1867
1871
|
id: 'draw3d.postfx.ssao.state.deterministic',
|
|
1868
|
-
expression: "(() => { const resetPasses = () => { for (const passName of ['ssr', 'dof', 'motionBlur', 'bloom', 'colorGrade', 'vignette', 'fxaa', 'ssao', '
|
|
1872
|
+
expression: "(() => { const resetPasses = () => { for (const passName of ['ssr', 'dof', 'motionBlur', 'bloom', 'colorGrade', 'vignette', 'fxaa', 'ssao', 'filmGrain', 'custom:glow']) { aura.draw3d.removePostFXPass(passName); } }; const runSample = () => { resetPasses(); const baseline = aura.draw3d.getPostFXState() || {}; const setInitial = aura.draw3d.setPostFXPass('ssao', { strength: 1.1, radius: 0.42, threshold: 0.18, targetChain: { intermediateTargets: [], pingPong: false, composeMode: 'replace' } }); const disable = aura.draw3d.setPostFXEnabled('ssao', false); const reenable = aura.draw3d.setPostFXEnabled('ssao', true); const update = aura.draw3d.setPostFXPass('ssao', { strength: 0.92, radius: 0.36, threshold: 0.24 }); const state = aura.draw3d.getPostFXState() || {}; const pass = Array.isArray(state?.passes) ? state.passes.find((entry) => entry.pass === 'ssao') : null; return { status: [setInitial?.reasonCode, disable?.reasonCode, reenable?.reasonCode, update?.reasonCode].join('|'), order: Array.isArray(state?.passes) ? state.passes.map((entry) => entry.pass).join('|') : '', total: Number(state?.totalPasses || 0), enabled: Number(state?.enabledPasses || 0), strength: Number(pass?.strength || 0), radius: Number(pass?.radius || 0), threshold: Number(pass?.threshold || 0), mutationDelta: Number(state?.mutationCount || 0) - Number(baseline?.mutationCount || 0), lastReasonCode: state?.lastReasonCode, lastOk: state?.lastOk, targetChainCount: Number(state?.targetChain?.intermediateTargetCount || 0), targetChainPingPong: state?.targetChain?.pingPong === true, targetChainComposeMode: state?.targetChain?.composeMode || null }; }; const first = runSample(); const second = runSample(); return JSON.stringify(first) === JSON.stringify(second) && first.status === 'postfx_ok|postfx_ok|postfx_ok|postfx_ok' && first.order === 'ssao' && first.total === 1 && first.enabled === 1 && Math.abs(first.strength - 0.92) < 1e-3 && Math.abs(first.radius - 0.36) < 1e-3 && Math.abs(first.threshold - 0.24) < 1e-3 && first.targetChainCount === 0 && first.targetChainPingPong === false && first.targetChainComposeMode === 'replace' && first.mutationDelta === 4 && first.lastReasonCode === 'postfx_ok' && first.lastOk === true; })()",
|
|
1869
1873
|
},
|
|
1870
1874
|
{
|
|
1871
1875
|
id: 'draw3d.postfx.reason-codes.stable',
|
|
@@ -1873,7 +1877,7 @@ export const SYSTEMS_AND_GAMEPLAY_CONFORMANCE_CASES = [
|
|
|
1873
1877
|
},
|
|
1874
1878
|
{
|
|
1875
1879
|
id: 'draw3d.postfx.ssr-dof.runtime.setup',
|
|
1876
|
-
expression: `(() => { try { const resetPasses = () => { for (const passName of ['ssr', 'dof', 'motionBlur', 'bloom', 'colorGrade', 'vignette', 'fxaa', 'ssao', '
|
|
1880
|
+
expression: `(() => { try { const resetPasses = () => { for (const passName of ['ssr', 'dof', 'motionBlur', 'bloom', 'colorGrade', 'vignette', 'fxaa', 'ssao', 'filmGrain', 'custom:glow']) { aura.draw3d.removePostFXPass(passName); } }; resetPasses(); const baseline = aura.draw3d.getPostFXState() || {}; const meshHandle = aura.mesh.createBox(1, 1, 1); const materialHandle = aura.material.create({ color: { r: 0.35, g: 0.7, b: 0.95, a: 1 }, metallic: 0.1, roughness: 0.6 }); const nodeId = aura.scene3d.createNode({ position: { x: 0, y: 0, z: -2.5 } }); const bind = aura.scene3d.bindRenderNode(nodeId, meshHandle, materialHandle, { layer: 0, visible: true, cull: false }); aura.camera3d.perspective(60, 0.1, 100); aura.camera3d.setPosition(0, 0, 4); aura.camera3d.lookAt(0, 0, 0); const setSsr = aura.draw3d.setPostFXPass('ssr', { strength: 1.0, threshold: 0.35, maxSteps: 96, thickness: 0.25, fadeEdge: 0.8, targetChain: { intermediateTargets: [], pingPong: false, composeMode: 'replace' } }); const setDof = aura.draw3d.setPostFXPass('dof', { strength: 0.9, focalDistance: 2.5, focalLength: 50, aperture: 2.2, maxBlur: 6.0 }); const setMotionBlur = aura.draw3d.setPostFXPass('motionBlur', { strength: 0.65, radius: 12.0, threshold: 0.0 }); const baseDraw = aura.draw; aura.draw = function () { const frameIndex = Number(globalThis.__postfxRuntimeFrame || 0); aura.camera3d.setPosition(frameIndex * 0.15, 0, 4 - (frameIndex * 0.1)); aura.camera3d.lookAt(0, 0, 0); aura.draw3d.clear3d({ r: 0.02, g: 0.02, b: 0.03, a: 1 }); aura.scene3d.submitRenderBindings(); globalThis.__postfxRuntimeFrame = frameIndex + 1; baseDraw(); }; globalThis.__postfxRuntimeFrame = 0; globalThis.__postfxRuntimeProbe = { meshHandle, materialHandle, nodeId, bindOk: bind?.ok === true, setSsrReason: setSsr?.reasonCode || null, setDofReason: setDof?.reasonCode || null, setMotionBlurReason: setMotionBlur?.reasonCode || null, baselineMutationCount: Number(baseline?.mutationCount || 0) }; return Number.isInteger(meshHandle) && meshHandle > 0 && Number.isInteger(materialHandle) && materialHandle > 0 && Number.isInteger(nodeId) && nodeId > 0 && bind?.ok === true && setSsr?.ok === true && setDof?.ok === true && setMotionBlur?.ok === true; } catch (_) { return false; } })()`,
|
|
1877
1881
|
},
|
|
1878
1882
|
],
|
|
1879
1883
|
nativePostChecks: [
|
|
@@ -2627,5 +2631,262 @@ export const SYSTEMS_AND_GAMEPLAY_CONFORMANCE_CASES = [
|
|
|
2627
2631
|
};
|
|
2628
2632
|
`,
|
|
2629
2633
|
frames: 4,
|
|
2634
|
+
},
|
|
2635
|
+
{
|
|
2636
|
+
id: 'asset-forward-2d-flagship-runtime',
|
|
2637
|
+
modes: ['native'],
|
|
2638
|
+
namespaces: ['animation', 'audio', 'draw2d', 'tilemap', 'debug'],
|
|
2639
|
+
functions: [
|
|
2640
|
+
'aura.animation.registerAtlas',
|
|
2641
|
+
'aura.animation.resolveAtlasFrame',
|
|
2642
|
+
'aura.animation.createAtlasClip',
|
|
2643
|
+
'aura.animation.stepAtlasClip',
|
|
2644
|
+
'aura.animation.getAtlasClipState',
|
|
2645
|
+
'aura.audio.play',
|
|
2646
|
+
'aura.audio.stopAll',
|
|
2647
|
+
'aura.audio.setBusVolume',
|
|
2648
|
+
'aura.audio.update',
|
|
2649
|
+
'aura.audio.getMixerState',
|
|
2650
|
+
'aura.draw2d.createRenderTarget',
|
|
2651
|
+
'aura.draw2d.destroyRenderTarget',
|
|
2652
|
+
'aura.draw2d.runCompositorGraph',
|
|
2653
|
+
'aura.draw2d.sprite',
|
|
2654
|
+
'aura.draw2d.text',
|
|
2655
|
+
'aura.draw2d.clear',
|
|
2656
|
+
'aura.tilemap.import',
|
|
2657
|
+
'aura.tilemap.draw',
|
|
2658
|
+
'aura.tilemap.setTile',
|
|
2659
|
+
'aura.tilemap.removeRegion',
|
|
2660
|
+
'aura.tilemap.queryRay',
|
|
2661
|
+
'aura.tilemap.unload',
|
|
2662
|
+
'aura.debug.inspectorStats',
|
|
2663
|
+
],
|
|
2664
|
+
nativeChecks: [
|
|
2665
|
+
{
|
|
2666
|
+
id: 'asset-forward-2d.surface.methods',
|
|
2667
|
+
expression: "(() => ['registerAtlas','resolveAtlasFrame','createAtlasClip','stepAtlasClip','getAtlasClipState'].every((name) => typeof aura.animation?.[name] === 'function') && ['play','stopAll','setBusVolume','update','getMixerState'].every((name) => typeof aura.audio?.[name] === 'function') && ['createRenderTarget','destroyRenderTarget','runCompositorGraph','sprite','text','clear'].every((name) => typeof aura.draw2d?.[name] === 'function') && ['import','draw','setTile','removeRegion','queryRay','unload'].every((name) => typeof aura.tilemap?.[name] === 'function') && typeof aura.debug?.inspectorStats === 'function')()",
|
|
2668
|
+
},
|
|
2669
|
+
],
|
|
2670
|
+
source: `
|
|
2671
|
+
aura.setup = async function () {
|
|
2672
|
+
if (aura.assets && typeof aura.assets.load === 'function') {
|
|
2673
|
+
await aura.assets.load(['player.png', 'coin.wav']);
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
if (typeof aura.debug?.enableInspector === 'function') {
|
|
2677
|
+
aura.debug.enableInspector(true);
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
const atlas = aura.animation.registerAtlas({
|
|
2681
|
+
image: 'player.png',
|
|
2682
|
+
frames: {
|
|
2683
|
+
drive_0: { x: 0, y: 0, w: 16, h: 16 },
|
|
2684
|
+
drive_1: { x: 0, y: 0, w: 16, h: 16 },
|
|
2685
|
+
drive_2: { x: 0, y: 0, w: 16, h: 16 },
|
|
2686
|
+
},
|
|
2687
|
+
clips: {
|
|
2688
|
+
drive: {
|
|
2689
|
+
frames: ['drive_0', 'drive_1', 'drive_2'],
|
|
2690
|
+
frameDuration: 0.1,
|
|
2691
|
+
loop: true,
|
|
2692
|
+
},
|
|
2693
|
+
},
|
|
2694
|
+
});
|
|
2695
|
+
aura.test.assert(atlas && atlas.ok === true, 'asset-forward atlas registration should succeed');
|
|
2696
|
+
|
|
2697
|
+
const clip = aura.animation.createAtlasClip({
|
|
2698
|
+
atlasId: atlas.atlasId,
|
|
2699
|
+
clipKey: 'drive',
|
|
2700
|
+
playing: true,
|
|
2701
|
+
});
|
|
2702
|
+
aura.test.assert(clip && clip.ok === true, 'asset-forward atlas clip should succeed');
|
|
2703
|
+
|
|
2704
|
+
const mapHandle = aura.tilemap.import({
|
|
2705
|
+
width: 8,
|
|
2706
|
+
height: 4,
|
|
2707
|
+
tilewidth: 16,
|
|
2708
|
+
tileheight: 16,
|
|
2709
|
+
solidLayerNames: ['cover'],
|
|
2710
|
+
layers: [
|
|
2711
|
+
{ name: 'ground', type: 'tilelayer', width: 8, height: 4, data: Array.from({ length: 32 }, () => 1) },
|
|
2712
|
+
{ name: 'cover', type: 'tilelayer', width: 8, height: 4, data: [
|
|
2713
|
+
0, 0, 0, 0, 0, 0, 0, 0,
|
|
2714
|
+
0, 0, 0, 0, 0, 0, 0, 0,
|
|
2715
|
+
0, 0, 1, 1, 0, 0, 0, 0,
|
|
2716
|
+
0, 0, 0, 0, 0, 0, 0, 0,
|
|
2717
|
+
], properties: { solid: true } },
|
|
2718
|
+
{ name: 'decor', type: 'tilelayer', width: 8, height: 4, data: [
|
|
2719
|
+
0, 1, 0, 1, 0, 1, 0, 1,
|
|
2720
|
+
1, 0, 1, 0, 1, 0, 1, 0,
|
|
2721
|
+
0, 1, 0, 1, 0, 1, 0, 1,
|
|
2722
|
+
1, 0, 1, 0, 1, 0, 1, 0,
|
|
2723
|
+
] },
|
|
2724
|
+
],
|
|
2725
|
+
tilesets: [
|
|
2726
|
+
{ firstgid: 1, image: 'player.png', tilewidth: 16, tileheight: 16, tilecount: 1, columns: 1 },
|
|
2727
|
+
],
|
|
2728
|
+
});
|
|
2729
|
+
aura.test.assert(Number.isInteger(mapHandle) && mapHandle > 0, 'asset-forward tilemap import should succeed');
|
|
2730
|
+
|
|
2731
|
+
const panelTarget = aura.draw2d.createRenderTarget(160, 96);
|
|
2732
|
+
const fxTarget = aura.draw2d.createRenderTarget(192, 112);
|
|
2733
|
+
const finalTarget = aura.draw2d.createRenderTarget(224, 128);
|
|
2734
|
+
aura.test.assert(panelTarget?.ok === true && fxTarget?.ok === true && finalTarget?.ok === true, 'asset-forward render targets should succeed');
|
|
2735
|
+
|
|
2736
|
+
const musicHandle = aura.audio.play('coin.wav', { volume: 0.2, loop: true, bus: 'music' });
|
|
2737
|
+
const sfxHandle = aura.audio.play('coin.wav', { volume: 0.08, loop: true, bus: 'sfx' });
|
|
2738
|
+
aura.test.assert(Number.isInteger(musicHandle) && musicHandle > 0, 'asset-forward music handle should be valid');
|
|
2739
|
+
aura.test.assert(Number.isInteger(sfxHandle) && sfxHandle > 0, 'asset-forward sfx handle should be valid');
|
|
2740
|
+
aura.test.assert(aura.audio.setBusVolume('music', 0.55)?.ok === true, 'music bus volume should be configurable');
|
|
2741
|
+
aura.test.assert(aura.audio.setBusVolume('sfx', 0.7)?.ok === true, 'sfx bus volume should be configurable');
|
|
2742
|
+
|
|
2743
|
+
globalThis.__assetForward2dFlagship = {
|
|
2744
|
+
atlasId: atlas.atlasId,
|
|
2745
|
+
clipId: clip.clipId,
|
|
2746
|
+
mapHandle,
|
|
2747
|
+
panelTarget,
|
|
2748
|
+
fxTarget,
|
|
2749
|
+
finalTarget,
|
|
2750
|
+
frames: [],
|
|
2751
|
+
lastStepOk: false,
|
|
2752
|
+
};
|
|
2753
|
+
};
|
|
2754
|
+
|
|
2755
|
+
aura.update = function () {
|
|
2756
|
+
const probe = globalThis.__assetForward2dFlagship;
|
|
2757
|
+
const stepDt = [0.05, 0.06, 0.11, 0.08][probe.frames.length] || 0.05;
|
|
2758
|
+
const step = aura.animation.stepAtlasClip(probe.clipId, stepDt);
|
|
2759
|
+
probe.lastStepOk = !!step && step.ok === true;
|
|
2760
|
+
aura.audio.update(0.125);
|
|
2761
|
+
|
|
2762
|
+
const tileX = 2 + (probe.frames.length % 2);
|
|
2763
|
+
aura.tilemap.setTile(probe.mapHandle, 'cover', { x: tileX, y: 2 }, probe.frames.length >= 2 ? 1 : 0);
|
|
2764
|
+
if (probe.frames.length === 1) {
|
|
2765
|
+
aura.tilemap.removeRegion(probe.mapHandle, 'decor', { x: 6, y: 1, w: 1, h: 1 });
|
|
2766
|
+
}
|
|
2767
|
+
};
|
|
2768
|
+
|
|
2769
|
+
aura.draw = function () {
|
|
2770
|
+
const probe = globalThis.__assetForward2dFlagship;
|
|
2771
|
+
const frameIndex = probe.frames.length;
|
|
2772
|
+
const clipState = aura.animation.getAtlasClipState(probe.clipId);
|
|
2773
|
+
const spriteFrame = aura.animation.resolveAtlasFrame(probe.atlasId, clipState?.frameKey || 'drive_0');
|
|
2774
|
+
const ray = aura.tilemap.queryRay(probe.mapHandle, { x: 0.5, y: 32.5, dx: 1, dy: 0, maxDistance: 128 });
|
|
2775
|
+
const mixer = aura.audio.getMixerState() || {};
|
|
2776
|
+
const musicBus = Array.isArray(mixer.buses)
|
|
2777
|
+
? mixer.buses.find((entry) => entry.bus === 'music')
|
|
2778
|
+
: null;
|
|
2779
|
+
|
|
2780
|
+
aura.draw2d.clear(4, 6, 10);
|
|
2781
|
+
let graph = null;
|
|
2782
|
+
if (frameIndex === 0) {
|
|
2783
|
+
graph = aura.draw2d.runCompositorGraph('asset-forward-2d-proof', [
|
|
2784
|
+
{
|
|
2785
|
+
id: 'panel',
|
|
2786
|
+
target: probe.panelTarget,
|
|
2787
|
+
draw: () => {
|
|
2788
|
+
aura.draw2d.clear(0, 0, 0, 0);
|
|
2789
|
+
aura.tilemap.draw(probe.mapHandle, {
|
|
2790
|
+
camera: { x: 0, y: 0, width: 128, height: 64 },
|
|
2791
|
+
});
|
|
2792
|
+
aura.draw2d.sprite(spriteFrame.image, 34, 22, {
|
|
2793
|
+
width: 32,
|
|
2794
|
+
height: 32,
|
|
2795
|
+
frameX: spriteFrame.frameX,
|
|
2796
|
+
frameY: spriteFrame.frameY,
|
|
2797
|
+
frameW: spriteFrame.frameW,
|
|
2798
|
+
frameH: spriteFrame.frameH,
|
|
2799
|
+
});
|
|
2800
|
+
aura.draw2d.text('ASSET-FORWARD', 10, 10, { size: 10, color: aura.colors.white });
|
|
2801
|
+
},
|
|
2802
|
+
},
|
|
2803
|
+
{
|
|
2804
|
+
id: 'fx',
|
|
2805
|
+
target: probe.fxTarget,
|
|
2806
|
+
draw: () => {
|
|
2807
|
+
aura.draw2d.clear(0, 0, 0, 0);
|
|
2808
|
+
aura.draw2d.sprite(probe.panelTarget, 0, 0, { width: 192, height: 112 });
|
|
2809
|
+
aura.draw2d.text('AUDIO x2', 14, 82, { size: 11, color: aura.colors.white });
|
|
2810
|
+
},
|
|
2811
|
+
},
|
|
2812
|
+
{
|
|
2813
|
+
id: 'final',
|
|
2814
|
+
target: probe.finalTarget,
|
|
2815
|
+
draw: () => {
|
|
2816
|
+
aura.draw2d.clear(0, 0, 0, 0);
|
|
2817
|
+
aura.draw2d.sprite(probe.fxTarget, 0, 0, { width: 224, height: 128 });
|
|
2818
|
+
},
|
|
2819
|
+
},
|
|
2820
|
+
]);
|
|
2821
|
+
if (Array.isArray(graph?.stages) && graph.stages.length === 3) {
|
|
2822
|
+
aura.draw2d.sprite(graph.stages[2], 8, 8, { width: 224, height: 128 });
|
|
2823
|
+
}
|
|
2824
|
+
} else {
|
|
2825
|
+
aura.draw2d.sprite(probe.finalTarget, 8, 8, { width: 224, height: 128 });
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
const stats = aura.debug.inspectorStats() || {};
|
|
2829
|
+
const compositor = stats.draw2dRuntime?.compositor || {};
|
|
2830
|
+
probe.frames.push({
|
|
2831
|
+
frameIndex,
|
|
2832
|
+
graphReason: graph?.reasonCode || null,
|
|
2833
|
+
graphStageCount: Number(graph?.stageCount || 0),
|
|
2834
|
+
clipFrame: clipState?.frameKey || null,
|
|
2835
|
+
clipLoops: Number(clipState?.loops || 0),
|
|
2836
|
+
frameResolved: spriteFrame?.ok === true,
|
|
2837
|
+
lastStepOk: probe.lastStepOk === true,
|
|
2838
|
+
rayOk: ray?.ok === true,
|
|
2839
|
+
rayHit: ray?.hit === true,
|
|
2840
|
+
rayLayer: Number(ray?.hitCell?.layerIndex ?? -1),
|
|
2841
|
+
trackCount: Array.isArray(mixer.tracks) ? mixer.tracks.length : -1,
|
|
2842
|
+
musicBusVolume: musicBus ? Number(musicBus.volume.toFixed(6)) : null,
|
|
2843
|
+
knownGraphCount: Number(compositor.knownGraphCount || 0),
|
|
2844
|
+
lastGraphStageCount: Number(compositor.lastGraphStageCount || 0),
|
|
2845
|
+
lastGraphReasonCode: compositor.lastGraphReasonCode || null,
|
|
2846
|
+
});
|
|
2847
|
+
|
|
2848
|
+
if (probe.frames.length !== 4) {
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
const trace = probe.frames.map((entry) => entry.clipFrame + ':' + entry.clipLoops);
|
|
2853
|
+
const destroyPanel = aura.draw2d.destroyRenderTarget(probe.panelTarget);
|
|
2854
|
+
const destroyFx = aura.draw2d.destroyRenderTarget(probe.fxTarget);
|
|
2855
|
+
const destroyFinal = aura.draw2d.destroyRenderTarget(probe.finalTarget);
|
|
2856
|
+
const missingDestroy = aura.draw2d.destroyRenderTarget(probe.finalTarget);
|
|
2857
|
+
const unloaded = aura.tilemap.unload(probe.mapHandle);
|
|
2858
|
+
aura.audio.stopAll();
|
|
2859
|
+
|
|
2860
|
+
aura.test.equal(
|
|
2861
|
+
trace.join('|'),
|
|
2862
|
+
'drive_0:0|drive_1:0|drive_2:0|drive_0:1',
|
|
2863
|
+
'asset-forward animation trace should stay deterministic',
|
|
2864
|
+
);
|
|
2865
|
+
aura.test.assert(
|
|
2866
|
+
probe.frames.every((entry) => entry.frameResolved === true && entry.lastStepOk === true),
|
|
2867
|
+
'asset-forward atlas stepping should remain resolved each frame',
|
|
2868
|
+
);
|
|
2869
|
+
aura.test.assert(
|
|
2870
|
+
probe.frames.every((entry) => entry.rayOk === true && entry.rayHit === true && entry.rayLayer === 1),
|
|
2871
|
+
'asset-forward tilemap query pressure should remain deterministic',
|
|
2872
|
+
);
|
|
2873
|
+
aura.test.assert(
|
|
2874
|
+
probe.frames.every((entry) => entry.trackCount === 2 && entry.musicBusVolume === 0.55),
|
|
2875
|
+
'asset-forward audio concurrency should keep two live tracks and the expected bus volume',
|
|
2876
|
+
);
|
|
2877
|
+
aura.test.assert(
|
|
2878
|
+
probe.frames[0].graphReason === 'draw2d_compositor_captured'
|
|
2879
|
+
&& probe.frames[0].graphStageCount === 3
|
|
2880
|
+
&& probe.frames[0].knownGraphCount === 1
|
|
2881
|
+
&& probe.frames[0].lastGraphStageCount === 3
|
|
2882
|
+
&& probe.frames[0].lastGraphReasonCode === 'draw2d_compositor_captured',
|
|
2883
|
+
'asset-forward compositor capture should remain stable on the setup frame',
|
|
2884
|
+
);
|
|
2885
|
+
aura.test.assert(destroyPanel?.ok === true && destroyFx?.ok === true && destroyFinal?.ok === true, 'asset-forward render targets should destroy cleanly');
|
|
2886
|
+
aura.test.assert(missingDestroy?.ok === false && missingDestroy?.reasonCode === 'missing_render_target', 'asset-forward render target destroy should remain reason-coded');
|
|
2887
|
+
aura.test.equal(unloaded, true, 'asset-forward tilemap should unload cleanly');
|
|
2888
|
+
};
|
|
2889
|
+
`,
|
|
2890
|
+
nativeFrames: 4,
|
|
2630
2891
|
}
|
|
2631
2892
|
];
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { buildMobileCapabilityAssertion } from './build-contract/capabilities.mjs';
|
|
5
|
+
import { ANDROID_BUILD_MANIFEST_SCHEMA } from './mobile/android/build.mjs';
|
|
6
|
+
import { IOS_BUILD_MANIFEST_SCHEMA } from './mobile/ios/build.mjs';
|
|
7
|
+
import { normalizeMobileBuildTarget } from './mobile/shared/capabilities.mjs';
|
|
8
|
+
|
|
9
|
+
function readJsonFile(path, label) {
|
|
10
|
+
const resolvedPath = resolve(path);
|
|
11
|
+
if (!existsSync(resolvedPath)) {
|
|
12
|
+
throw new Error(`${label} is missing: ${resolvedPath}`);
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(resolvedPath, 'utf8'));
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw new Error(`Failed to parse ${label}: ${error.message}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateAndroidBuildManifest(manifest) {
|
|
22
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
23
|
+
return { ok: false, reasonCode: 'mobile_android_manifest_invalid' };
|
|
24
|
+
}
|
|
25
|
+
if (manifest.schema !== ANDROID_BUILD_MANIFEST_SCHEMA || manifest.buildTarget !== 'android') {
|
|
26
|
+
return { ok: false, reasonCode: 'mobile_android_manifest_invalid' };
|
|
27
|
+
}
|
|
28
|
+
if (
|
|
29
|
+
!manifest.buildStatus
|
|
30
|
+
|| typeof manifest.buildStatus.release !== 'string'
|
|
31
|
+
|| !manifest.toolchain
|
|
32
|
+
|| !Array.isArray(manifest.toolchain.reasonCodes)
|
|
33
|
+
) {
|
|
34
|
+
return { ok: false, reasonCode: 'mobile_android_manifest_invalid' };
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
reasonCode: 'android_manifest_contract_ok',
|
|
39
|
+
buildStatus: manifest.buildStatus,
|
|
40
|
+
toolchain: manifest.toolchain,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function validateIosBuildManifest(manifest) {
|
|
45
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
46
|
+
return { ok: false, reasonCode: 'mobile_ios_manifest_invalid' };
|
|
47
|
+
}
|
|
48
|
+
if (manifest.schema !== IOS_BUILD_MANIFEST_SCHEMA || manifest.buildTarget !== 'ios') {
|
|
49
|
+
return { ok: false, reasonCode: 'mobile_ios_manifest_invalid' };
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
!manifest.toolchain
|
|
53
|
+
|| typeof manifest.toolchain.reasonCode !== 'string'
|
|
54
|
+
|| !manifest.signing
|
|
55
|
+
|| typeof manifest.signing.reasonCode !== 'string'
|
|
56
|
+
|| !manifest.buildStatus
|
|
57
|
+
|| typeof manifest.buildStatus.reasonCode !== 'string'
|
|
58
|
+
) {
|
|
59
|
+
return { ok: false, reasonCode: 'mobile_ios_manifest_invalid' };
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
reasonCode: 'ios_manifest_contract_ok',
|
|
64
|
+
buildStatus: manifest.buildStatus,
|
|
65
|
+
toolchain: manifest.toolchain,
|
|
66
|
+
signing: manifest.signing,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function runMobileTargetConformance(options = {}) {
|
|
71
|
+
const projectRoot = resolve(options.projectRoot || process.cwd());
|
|
72
|
+
const target = normalizeMobileBuildTarget(options.target || 'mobile');
|
|
73
|
+
const capabilityReport = buildMobileCapabilityAssertion({
|
|
74
|
+
projectRoot,
|
|
75
|
+
modules: options.modules || {},
|
|
76
|
+
target,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!capabilityReport.ok) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
reasonCode: 'mobile_required_api_unsupported',
|
|
83
|
+
target,
|
|
84
|
+
capabilityReport,
|
|
85
|
+
manifests: {},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const manifests = {};
|
|
90
|
+
|
|
91
|
+
if (target === 'android' || target === 'mobile') {
|
|
92
|
+
if (!options.androidManifestPath) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reasonCode: 'mobile_android_manifest_missing',
|
|
96
|
+
target,
|
|
97
|
+
capabilityReport,
|
|
98
|
+
manifests,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const resolvedAndroidManifestPath = resolve(options.androidManifestPath);
|
|
102
|
+
if (!existsSync(resolvedAndroidManifestPath)) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
reasonCode: 'mobile_android_manifest_missing',
|
|
106
|
+
target,
|
|
107
|
+
capabilityReport,
|
|
108
|
+
manifests,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const androidManifest = readJsonFile(resolvedAndroidManifestPath, 'Android build manifest');
|
|
112
|
+
const androidReport = validateAndroidBuildManifest(androidManifest);
|
|
113
|
+
if (!androidReport.ok) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
reasonCode: androidReport.reasonCode,
|
|
117
|
+
target,
|
|
118
|
+
capabilityReport,
|
|
119
|
+
manifests,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
manifests.android = androidReport;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (target === 'ios' || target === 'mobile') {
|
|
126
|
+
if (!options.iosManifestPath) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
reasonCode: 'mobile_ios_manifest_missing',
|
|
130
|
+
target,
|
|
131
|
+
capabilityReport,
|
|
132
|
+
manifests,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const resolvedIosManifestPath = resolve(options.iosManifestPath);
|
|
136
|
+
if (!existsSync(resolvedIosManifestPath)) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
reasonCode: 'mobile_ios_manifest_missing',
|
|
140
|
+
target,
|
|
141
|
+
capabilityReport,
|
|
142
|
+
manifests,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const iosManifest = readJsonFile(resolvedIosManifestPath, 'iOS build manifest');
|
|
146
|
+
const iosReport = validateIosBuildManifest(iosManifest);
|
|
147
|
+
if (!iosReport.ok) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
reasonCode: iosReport.reasonCode,
|
|
151
|
+
target,
|
|
152
|
+
capabilityReport,
|
|
153
|
+
manifests,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
manifests.ios = iosReport;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
reasonCode: 'mobile_target_conformance_ok',
|
|
162
|
+
target,
|
|
163
|
+
capabilityReport,
|
|
164
|
+
manifests,
|
|
165
|
+
};
|
|
166
|
+
}
|