@underpostnet/cyberia 3.3.73 → 3.3.77
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/.env.example +17 -5
- package/.github/workflows/coverall.cyberia.ci.yml +10 -0
- package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
- package/.github/workflows/engine-cyberia.ci.yml +1 -1
- package/.github/workflows/publish.cyberia.ci.yml +44 -0
- package/AGENTS.md +61 -12
- package/CHANGELOG.md +159 -161
- package/CLI-HELP.md +20 -13
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +2 -2
- package/README.md +28 -28
- package/bin/build.js +96 -21
- package/bin/cyberia.js +1247 -1373
- package/bin/index.js +1247 -1373
- package/compose.env +17 -5
- package/conf.js +7 -4
- package/deploy/cyberia-client/deploy.sh +10 -11
- package/deploy/cyberia-client/package.sh +0 -1
- package/deploy/cyberia-client/state.sh +3 -2
- package/deploy/cyberia-server/deploy.sh +10 -11
- package/deploy/cyberia-server/package.sh +0 -1
- package/deploy/cyberia-server/state.sh +3 -2
- package/deploy/dd-cyberia/deploy.sh +5 -5
- package/deploy/dd-cyberia/init.sh +15 -14
- package/deploy/dd-cyberia/package.sh +0 -1
- package/deploy/dd-cyberia/state.sh +2 -2
- package/deploy/dd-cyberia/sync-deploy.sh +139 -75
- package/deploy/lib/config.sh +29 -0
- package/deploy/lib/github-actions-logging.sh +92 -76
- package/deploy/lib/host.sh +28 -19
- package/deploy/lib/state.sh +4 -1
- package/deploy/pwa-microservices-template/deploy.sh +10 -8
- package/deploy/release/deploy.sh +0 -2
- package/deployment.yaml +1 -1
- package/docker-compose.yml +23 -17
- package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
- package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
- package/docs/coverage/cyberia/base.css +362 -0
- package/docs/coverage/cyberia/block-navigation.js +82 -0
- package/docs/coverage/cyberia/favicon.png +0 -0
- package/docs/coverage/cyberia/index.html +161 -0
- package/docs/coverage/cyberia/prettify.css +101 -0
- package/docs/coverage/cyberia/prettify.js +937 -0
- package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
- package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
- package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
- package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
- package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
- package/docs/coverage/cyberia/sorter.js +205 -0
- package/hardhat/package-lock.json +2 -2
- package/hardhat/package.json +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/mongo-express/deployment.yaml +46 -27
- package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
- package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
- package/manifests/deployment/mongo-express/service.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
- package/package.json +19 -17
- package/scripts/test-monitor.sh +3 -3
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
- package/src/api/cyberia-action/cyberia-action.router.js +2 -2
- package/src/api/cyberia-action/cyberia-action.service.js +3 -4
- package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
- package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
- package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
- package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
- package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
- package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
- package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
- package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
- package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
- package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
- package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
- package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
- package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
- package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
- package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
- package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
- package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
- package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
- package/src/api/cyberia-map/cyberia-map.service.js +6 -2
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
- package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
- package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
- package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
- package/src/api/file/file.ref.js +153 -0
- package/src/api/file/file.ref.json +11 -2
- package/src/api/object-layer/object-layer.model.js +47 -12
- package/src/api/object-layer/object-layer.router.js +10 -3
- package/src/api/object-layer/object-layer.service.js +113 -120
- package/src/cli/app.js +12 -1
- package/src/cli/client.js +2 -2
- package/src/cli/cluster.js +80 -18
- package/src/cli/deploy.js +215 -34
- package/src/cli/docker-compose.js +1 -1
- package/src/cli/fs-selection.js +115 -0
- package/src/cli/fs.js +158 -342
- package/src/cli/index.js +22 -10
- package/src/cli/release.js +3 -1
- package/src/cli/repository.js +35 -9
- package/src/cli/run.js +80 -199
- package/src/cli/test.js +4 -0
- package/src/cli/wireguard.js +32 -31
- package/src/client/components/core/Badge.js +0 -1
- package/src/client/components/core/CssCore.js +8 -1
- package/src/client/components/core/Docs.js +77 -100
- package/src/client/components/core/Modal.js +77 -23
- package/src/client/components/core/ToolTip.js +15 -0
- package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
- package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
- package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
- package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
- package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
- package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
- package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
- package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
- package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
- package/src/client/components/default/AppShellDefault.js +1 -4
- package/src/client/components/itemledger/AppShellItemledger.js +0 -2
- package/src/client/components/underpost/AppShellUnderpost.js +1 -1
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
- package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
- package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
- package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
- package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
- package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
- package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
- package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
- package/src/client/services/object-layer/object-layer.management.js +6 -10
- package/src/client/services/object-layer/object-layer.service.js +3 -1
- package/src/client/ssr/views/Cyberia404.js +3 -3
- package/src/client-builder/client-build-docs.js +52 -52
- package/src/client-builder/client-build.js +67 -44
- package/src/client-builder/client-bundle.js +347 -0
- package/src/db/mongo/MongoExpress.js +187 -0
- package/src/grpc/cyberia/grpc-server.js +5 -7
- package/src/index.js +1 -1
- package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
- package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
- package/src/projects/cyberia/besu-genesis-generator.js +5 -14
- package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
- package/src/projects/cyberia/catalog-cyberia.js +3 -2
- package/src/projects/cyberia/gemini-client.js +3 -3
- package/src/projects/cyberia/generate-saga.js +4 -11
- package/src/projects/cyberia/hot-reload-trigger.js +17 -28
- package/src/projects/cyberia/instance-backup.js +210 -0
- package/src/projects/cyberia/instance-data.js +177 -257
- package/src/projects/cyberia/map-preview-generator.js +32 -68
- package/src/projects/cyberia/object-layer.js +152 -319
- package/src/projects/cyberia/seed-audio.js +139 -0
- package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
- package/src/projects/cyberia/semantic-layer-generator.js +2 -18
- package/src/projects/cyberia/stat-balance.js +81 -0
- package/src/projects/cyberia/stat-commands.js +23 -0
- package/src/projects/cyberia/stat-contract-generator.js +101 -0
- package/src/runtime/cyberia-client/Dockerfile +8 -4
- package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
- package/src/runtime/cyberia-server/Dockerfile +9 -2
- package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
- package/src/runtime/engine-cyberia/Dockerfile +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
- package/src/runtime/engine-cyberia/compose.env +17 -5
- package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
- package/src/server/build/coverage.js +113 -42
- package/src/server/build/package.js +55 -12
- package/src/server/build/testing.js +59 -9
- package/src/server/network/middlewares.js +8 -2
- package/src/server/network/underpost-gateway.js +113 -14
- package/src/server/ops/logger.js +70 -35
- package/src/server/runtime/conf.js +40 -9
- package/src/server/runtime/start.js +11 -2
- package/src/server/storage/data-query.js +16 -0
- package/src/server/storage/downloader.js +62 -14
- package/src/server/storage/zip.js +153 -0
- package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
- package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
- package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
- package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
- package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
- package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
- package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
- package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
- package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
- package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
- package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
- package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
- package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
- package/test/integration/app/cyberia/seed-audio.test.js +275 -0
- package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
- package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
- package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
- package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
- package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
- package/test/unit/catalog.test.js +13 -2
- package/test/unit/client-build-docs.test.js +97 -70
- package/test/unit/client-bundle.test.js +313 -0
- package/test/unit/conf-resolution.test.js +58 -7
- package/test/unit/coverage-artifact.test.js +120 -29
- package/test/unit/cyberia/instance-backup.test.js +183 -0
- package/test/unit/cyberia/instance-data.test.js +314 -0
- package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
- package/test/unit/cyberia/publish-workflow.test.js +22 -0
- package/test/unit/cyberia/stat-balance.test.js +69 -0
- package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
- package/test/unit/deploy-log-table.test.js +76 -5
- package/test/unit/downloader-integrity.test.js +98 -0
- package/test/unit/file-reference-registry.test.js +141 -0
- package/test/unit/fs-storage-paths.test.js +596 -0
- package/test/unit/gateway-pod-fetch.test.js +95 -0
- package/test/unit/letsencrypt-issuer.test.js +101 -0
- package/test/unit/logger-redaction.test.js +29 -0
- package/test/unit/package.test.js +70 -16
- package/test/unit/prepare-host.test.js +84 -1
- package/test/unit/release-bump.test.js +3 -1
- package/test/unit/start-options.test.js +12 -0
- package/test/unit/test-tiers.test.js +27 -0
- package/test/unit/zip-archive.test.js +142 -0
- package/vitest.config.js +11 -3
- package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
- package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { CyberiaInstanceConfModel } from '../../../../src/api/cyberia-instance-conf/cyberia-instance-conf.model.js';
|
|
3
|
+
import { CyberiaInstanceConfService } from '../../../../src/api/cyberia-instance-conf/cyberia-instance-conf.service.js';
|
|
4
|
+
import { CYBERIA_INSTANCE_CONF_DEFAULTS } from '../../../../src/api/cyberia-server-defaults/cyberia-server-defaults.js';
|
|
5
|
+
|
|
6
|
+
const coerce = (conf) => CyberiaInstanceConfService.coerceToSchema(conf, CyberiaInstanceConfModel);
|
|
7
|
+
|
|
8
|
+
// A backup written before the chance fields became fractions of one, as the exports on disk are.
|
|
9
|
+
const legacy = { instanceCode: 'LEGACY', lifeRegenChance: 300, maxChance: 10000 };
|
|
10
|
+
|
|
11
|
+
describe('instance conf import coercion', () => {
|
|
12
|
+
it('resets a value the schema rejects to its default and says so', async () => {
|
|
13
|
+
const { conf, resets } = await coerce(legacy);
|
|
14
|
+
expect(conf.lifeRegenChance).toBe(CYBERIA_INSTANCE_CONF_DEFAULTS.lifeRegenChance);
|
|
15
|
+
expect(conf.maxChance).toBe(CYBERIA_INSTANCE_CONF_DEFAULTS.maxChance);
|
|
16
|
+
expect(resets.map(({ path, from }) => [path, from])).toEqual([
|
|
17
|
+
['lifeRegenChance', 300],
|
|
18
|
+
['maxChance', 10000],
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('leaves every valid field exactly as it was', async () => {
|
|
23
|
+
const { conf, resets } = await coerce({ ...legacy, tickRate: 12, aoiRadius: 7 });
|
|
24
|
+
expect(conf.tickRate).toBe(12);
|
|
25
|
+
expect(conf.aoiRadius).toBe(7);
|
|
26
|
+
expect(conf.instanceCode).toBe('LEGACY');
|
|
27
|
+
expect(resets).toHaveLength(2);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('is a no-op on a conf that already passes', async () => {
|
|
31
|
+
const { conf, resets } = await coerce({ instanceCode: 'CLEAN', lifeRegenChance: 0.2 });
|
|
32
|
+
expect(resets).toEqual([]);
|
|
33
|
+
expect(conf.lifeRegenChance).toBe(0.2);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('resets a nested field through its dotted path', async () => {
|
|
37
|
+
const { conf, resets } = await coerce({ instanceCode: 'NESTED', skillRules: { projectileSpawnChance: 42 } });
|
|
38
|
+
expect(conf.skillRules.projectileSpawnChance).toBe(CYBERIA_INSTANCE_CONF_DEFAULTS.skillRules.projectileSpawnChance);
|
|
39
|
+
expect(resets[0].path).toBe('skillRules.projectileSpawnChance');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('produces a document the model accepts, so import can never half-fail', async () => {
|
|
43
|
+
const { conf } = await coerce(legacy);
|
|
44
|
+
await expect(new CyberiaInstanceConfModel(conf).validate()).resolves.toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('converges: coercing its own output changes nothing', async () => {
|
|
48
|
+
const { conf } = await coerce(legacy);
|
|
49
|
+
const { conf: again, resets } = await coerce(conf);
|
|
50
|
+
expect(resets).toEqual([]);
|
|
51
|
+
expect(again).toEqual(conf);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -25,7 +25,7 @@ const SCHEMA_FIELDS = [
|
|
|
25
25
|
'playerBaseSpeed',
|
|
26
26
|
'playerBaseLifeRegenMin',
|
|
27
27
|
'playerBaseLifeRegenMax',
|
|
28
|
-
'
|
|
28
|
+
'progressionRules',
|
|
29
29
|
'maxActiveLayers',
|
|
30
30
|
'initialLifeFraction',
|
|
31
31
|
'respawnDurationMs',
|
|
@@ -35,7 +35,6 @@ const SCHEMA_FIELDS = [
|
|
|
35
35
|
'maxChance',
|
|
36
36
|
'entityDefaults',
|
|
37
37
|
'statusIcons',
|
|
38
|
-
'skillConfig',
|
|
39
38
|
'skillRules',
|
|
40
39
|
'equipmentRules',
|
|
41
40
|
];
|
|
@@ -90,33 +89,28 @@ describe('fillInstanceConfDefaults', () => {
|
|
|
90
89
|
});
|
|
91
90
|
|
|
92
91
|
it('keeps a present non-empty array and does not merge defaults into it', () => {
|
|
93
|
-
|
|
92
|
+
// entityDefaults holds CyberiaEntityTypeDefault references, never documents.
|
|
93
|
+
const custom = ['6a9d72430a55742d256c22d6', '6a9d72430a55742d256c22d7'];
|
|
94
94
|
const out = fillInstanceConfDefaults({ entityDefaults: custom });
|
|
95
95
|
expect(out.entityDefaults).to.deep.equal(custom);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
it('
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
expect(entry).to.have.property('logicEventIds');
|
|
105
|
-
expect(entry).to.not.have.property('skills');
|
|
106
|
-
}
|
|
98
|
+
it('carries no skillConfig, because an instance never stores which skills it runs', () => {
|
|
99
|
+
// The CyberiaSkill collection owns the definitions; membership is derived from the content an
|
|
100
|
+
// instance names. A conf from before that still carrying the field has it dropped, so a stale
|
|
101
|
+
// list cannot reach the wire behind the derived one.
|
|
102
|
+
for (const input of [{}, { skillConfig: [] }, { skillConfig: [{ triggerItemId: 'hatchet' }] }]) {
|
|
103
|
+
expect(fillInstanceConfDefaults(input), JSON.stringify(input)).to.not.have.property('skillConfig');
|
|
107
104
|
}
|
|
108
105
|
});
|
|
109
106
|
|
|
110
|
-
it('
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const out = fillInstanceConfDefaults({ entityDefaults: [] });
|
|
118
|
-
expect(out.entityDefaults.length).to.be.greaterThan(0);
|
|
119
|
-
expect(out.entityDefaults).to.deep.equal(CYBERIA_INSTANCE_CONF_DEFAULTS.entityDefaults);
|
|
107
|
+
it('leaves entityDefaults empty, because referencing none is a complete world', () => {
|
|
108
|
+
// A conf owns references, not defaults: naming no document means "add nothing to the
|
|
109
|
+
// canonical ENTITY_TYPE_DEFAULTS", which is what every fresh instance wants.
|
|
110
|
+
for (const input of [{}, { entityDefaults: [] }, { entityDefaults: null }]) {
|
|
111
|
+
expect(fillInstanceConfDefaults(input).entityDefaults, JSON.stringify(input)).to.deep.equal([]);
|
|
112
|
+
}
|
|
113
|
+
expect(CYBERIA_INSTANCE_CONF_DEFAULTS.entityDefaults).to.deep.equal([]);
|
|
120
114
|
});
|
|
121
115
|
|
|
122
116
|
it('preserves DB metadata (_id, instanceCode, timestamps)', () => {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
collectInstanceItemIds,
|
|
4
|
+
collectSummonedItemIds,
|
|
5
|
+
isMaterialItemId,
|
|
6
|
+
selectInstanceSkills,
|
|
7
|
+
} from '../../../../src/api/cyberia-instance/cyberia-instance-items.js';
|
|
8
|
+
|
|
9
|
+
const SKILLS = [
|
|
10
|
+
{ triggerItemId: 'atlas_pistol_mk2', skills: [{ summonedEntityItemId: 'atlas_pistol_mk2_bullet' }] },
|
|
11
|
+
{ triggerItemId: 'hatchet', skills: [{ summonedEntityItemId: 'hatchet-skill' }] },
|
|
12
|
+
{ triggerItemId: 'anon', skills: [{ summonedEntityItemId: '$active_skin' }] },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
describe('what an instance names', () => {
|
|
16
|
+
it('reads the four sources and nothing else', () => {
|
|
17
|
+
const itemIds = collectInstanceItemIds({
|
|
18
|
+
maps: [{ entities: [{ objectLayerItemIds: ['purple', 'atlas_pistol_mk2'] }] }],
|
|
19
|
+
entityDefaults: [
|
|
20
|
+
{ liveItemIds: ['wood-1'], deadItemIds: ['wood-extracted-1'], dropItemIds: ['wood-drop-1'], inventoryItemsIds: ['tim-knife'] },
|
|
21
|
+
],
|
|
22
|
+
actions: [
|
|
23
|
+
{
|
|
24
|
+
shopItems: [{ itemId: 'hatchet', priceItemId: 'coin' }],
|
|
25
|
+
craftRecipes: [{ ingredients: [{ itemId: 'wood-1' }], outputItems: [{ itemId: 'plank' }] }],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
quests: [{ steps: [{ objectives: [{ itemId: 'kishins' }] }], rewards: [{ itemId: 'lain' }] }],
|
|
29
|
+
});
|
|
30
|
+
expect([...itemIds].sort()).toEqual([
|
|
31
|
+
'atlas_pistol_mk2',
|
|
32
|
+
'coin',
|
|
33
|
+
'hatchet',
|
|
34
|
+
'kishins',
|
|
35
|
+
'lain',
|
|
36
|
+
'plank',
|
|
37
|
+
'purple',
|
|
38
|
+
'tim-knife',
|
|
39
|
+
'wood-1',
|
|
40
|
+
'wood-drop-1',
|
|
41
|
+
'wood-extracted-1',
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('is empty for an instance with no content, and survives ragged documents', () => {
|
|
46
|
+
expect([...collectInstanceItemIds()]).toEqual([]);
|
|
47
|
+
expect([...collectInstanceItemIds({ maps: [null, {}], entityDefaults: [{}], actions: [{}], quests: [{}] })]).toEqual(
|
|
48
|
+
[],
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('leaves runtime placeholders out — they name no document', () => {
|
|
53
|
+
expect(isMaterialItemId('$active_skin')).toBe(false);
|
|
54
|
+
expect(isMaterialItemId('anon')).toBe(true);
|
|
55
|
+
expect([...collectInstanceItemIds({ quests: [{ rewards: [{ itemId: '$active_skin' }] }] })]).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('which skills an instance runs', () => {
|
|
60
|
+
it('takes a skill whose trigger the world names, wherever it named it', () => {
|
|
61
|
+
// The reported case: no entity wears a hatchet, a quest asks for one, and the skill belongs.
|
|
62
|
+
const owned = collectInstanceItemIds({
|
|
63
|
+
maps: [{ entities: [{ objectLayerItemIds: ['anon'] }] }],
|
|
64
|
+
quests: [{ steps: [{ objectives: [{ itemId: 'hatchet' }] }] }],
|
|
65
|
+
});
|
|
66
|
+
expect(selectInstanceSkills(SKILLS, owned).map((skill) => skill.triggerItemId)).toEqual(['hatchet', 'anon']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('leaves out a skill nothing in the world triggers', () => {
|
|
70
|
+
expect(selectInstanceSkills(SKILLS, new Set(['purple']))).toEqual([]);
|
|
71
|
+
expect(selectInstanceSkills(SKILLS, new Set())).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('accepts a plain array of ids as well as a set', () => {
|
|
75
|
+
expect(selectInstanceSkills(SKILLS, ['coin', 'hatchet']).map((skill) => skill.triggerItemId)).toEqual(['hatchet']);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('collects what the selected skills summon, minus the placeholders', () => {
|
|
79
|
+
// A summoned bullet needs an atlas; '$active_skin' is resolved by the simulation.
|
|
80
|
+
expect([...collectSummonedItemIds(SKILLS)].sort()).toEqual(['atlas_pistol_mk2_bullet', 'hatchet-skill']);
|
|
81
|
+
expect([...collectSummonedItemIds([])]).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('does not let a summoned item make its own skill belong', () => {
|
|
85
|
+
// Membership is decided before summons are folded in, so a world that merely draws a bullet
|
|
86
|
+
// does not thereby acquire the weapon skill that fires it.
|
|
87
|
+
const owned = new Set(['atlas_pistol_mk2_bullet']);
|
|
88
|
+
expect(selectInstanceSkills(SKILLS, owned)).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -14,8 +14,8 @@ import { setTimeout as sleep } from 'node:timers/promises';
|
|
|
14
14
|
// so a fleet this size only fits an instance started for load — one running with
|
|
15
15
|
// CYBERIA_DISABLE_CONNECTION_LIMITS=1, or with its per-IP admission limits raised
|
|
16
16
|
// past CYBERIA_LOAD_CONNECTIONS. Pointed at an untuned instance the run is capped
|
|
17
|
-
// by
|
|
18
|
-
//
|
|
17
|
+
// by the server's per-IP admission limit and reports a refusal, so the target is
|
|
18
|
+
// named deliberately rather than defaulted to.
|
|
19
19
|
// `underpost cyberia test` sets this, along with the fleet size and the duration.
|
|
20
20
|
const WS_URL = process.env.CYBERIA_LOAD_WS_URL ?? '';
|
|
21
21
|
const CONNECTIONS = Number(process.env.CYBERIA_LOAD_CONNECTIONS ?? 5);
|
|
@@ -62,8 +62,8 @@ const DISPERSAL_MARGIN_CELLS = 3;
|
|
|
62
62
|
// lingers there sits in the crossfire of every joining player.
|
|
63
63
|
const SPAWN_CLEARANCE_CELLS = 14;
|
|
64
64
|
|
|
65
|
-
// Unit vectors for the supported directions, in
|
|
66
|
-
//
|
|
65
|
+
// Unit vectors for the supported directions, in Direction enum order. NONE is
|
|
66
|
+
// excluded: a tap carries movement intent.
|
|
67
67
|
const DIRECTIONS = [
|
|
68
68
|
[0, -1], // UP
|
|
69
69
|
[1, -1], // UP_RIGHT
|
|
@@ -90,9 +90,8 @@ const clamp = (value, max) => (value < 0 ? 0 : value > max ? max : value);
|
|
|
90
90
|
const randomDirection = () => DIRECTIONS[Math.floor(Math.random() * DIRECTIONS.length)];
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
* Picks the supported direction closest to a vector
|
|
94
|
-
* uses
|
|
95
|
-
* UP, so the angle index shifts by two quadrants.
|
|
93
|
+
* Picks the supported direction closest to a vector, the same mapping the
|
|
94
|
+
* server uses. The enum starts at UP, so the angle index shifts two quadrants.
|
|
96
95
|
*/
|
|
97
96
|
function directionToward(dx, dy) {
|
|
98
97
|
const index = (((Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) + 2) % 8) + 8) % 8;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { CyberiaAudioModel } from '../../../../src/api/cyberia-audio/cyberia-audio.model.js';
|
|
8
|
+
import { CyberiaAudioService, readManifest } from '../../../../src/api/cyberia-audio/cyberia-audio.service.js';
|
|
9
|
+
import { CyberiaMapAudioConfModel } from '../../../../src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js';
|
|
10
|
+
import {
|
|
11
|
+
mergeEventBindings,
|
|
12
|
+
parseEventAudioBinding,
|
|
13
|
+
} from '../../../../src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js';
|
|
14
|
+
|
|
15
|
+
// As the recorder writes it: `bus` is the `src/audio-module/<bus-id>/` directory of the module.
|
|
16
|
+
const manifestFixture = (id) => ({
|
|
17
|
+
id,
|
|
18
|
+
bus: 'sfx',
|
|
19
|
+
tags: ['fixture'],
|
|
20
|
+
author: 'cyberia-audio',
|
|
21
|
+
version: '0.1.0',
|
|
22
|
+
duration: 0.42,
|
|
23
|
+
sampleRate: 44100,
|
|
24
|
+
channels: 1,
|
|
25
|
+
loop: false,
|
|
26
|
+
audio: `${id}.wav`,
|
|
27
|
+
defaults: { volume: 0.8 },
|
|
28
|
+
parameters: { volume: { min: 0, max: 1, integer: false } },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('parseEventAudioBinding', () => {
|
|
32
|
+
it('splits a <logic-event-id>:<audio-code> pair and trims both sides', () => {
|
|
33
|
+
expect(parseEventAudioBinding('combat:combat-theme')).to.deep.equal({
|
|
34
|
+
logicEventId: 'combat',
|
|
35
|
+
audioCode: 'combat-theme',
|
|
36
|
+
});
|
|
37
|
+
expect(parseEventAudioBinding(' boss : mystery ')).to.deep.equal({ logicEventId: 'boss', audioCode: 'mystery' });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('splits on the first colon so an audio code may contain one', () => {
|
|
41
|
+
expect(parseEventAudioBinding('shoot:pack:laser')).to.deep.equal({ logicEventId: 'shoot', audioCode: 'pack:laser' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('rejects a value with an empty side or no separator', () => {
|
|
45
|
+
for (const bad of ['combat', ':combat', 'combat:', '', ' : ', undefined, null]) {
|
|
46
|
+
expect(() => parseEventAudioBinding(bad), `accepted: ${bad}`).to.throw(/<logic-event-id>:<audio-code>/);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('mergeEventBindings', () => {
|
|
52
|
+
const stored = [
|
|
53
|
+
{ logicEventId: 'combat', audioCode: 'combat' },
|
|
54
|
+
{ logicEventId: 'boss', audioCode: 'mystery' },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
it('replaces the events it names and keeps the rest', () => {
|
|
58
|
+
expect(mergeEventBindings(stored, [{ logicEventId: 'boss', audioCode: 'victory' }])).to.deep.equal([
|
|
59
|
+
{ logicEventId: 'combat', audioCode: 'combat' },
|
|
60
|
+
{ logicEventId: 'boss', audioCode: 'victory' },
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('appends an event the map does not carry yet', () => {
|
|
65
|
+
expect(mergeEventBindings(stored, [{ logicEventId: 'shoot', audioCode: 'shoot' }])).to.have.length(3);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('preserves playback settings when only the asset reference changes', () => {
|
|
69
|
+
expect(mergeEventBindings(
|
|
70
|
+
[{ logicEventId: 'combat', audioCode: 'combat', settings: { bus: 'music', loop: true } }],
|
|
71
|
+
[{ logicEventId: 'combat', audioCode: 'boss' }],
|
|
72
|
+
)).to.deep.equal([
|
|
73
|
+
{ logicEventId: 'combat', audioCode: 'boss', settings: { bus: 'music', loop: true } },
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('is a no-op with nothing incoming, and works from an empty map', () => {
|
|
78
|
+
expect(mergeEventBindings(stored, [])).to.deep.equal(stored);
|
|
79
|
+
expect(mergeEventBindings(undefined, [{ logicEventId: 'idle', audioCode: 'exploration' }])).to.deep.equal([
|
|
80
|
+
{ logicEventId: 'idle', audioCode: 'exploration' },
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('CyberiaAudio schema', () => {
|
|
86
|
+
it('is the asset and nothing else: code, file, manifest', () => {
|
|
87
|
+
expect(Object.keys(CyberiaAudioModel.schema.paths)).to.include.members(['code', 'fileId', 'manifest']);
|
|
88
|
+
// The asset carries no classification of its own; when it plays belongs to the binding.
|
|
89
|
+
expect(CyberiaAudioModel.schema.paths).to.not.have.property('type');
|
|
90
|
+
expect(CyberiaAudioModel.schema.paths).to.not.have.property('logicEventId');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('identifies an asset by its code alone', () => {
|
|
94
|
+
const [fields, options] = CyberiaAudioModel.schema.indexes().find(([keys]) => keys.code);
|
|
95
|
+
expect(fields).to.deep.equal({ code: 1 });
|
|
96
|
+
expect(options.unique).to.equal(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('records the bus a module was authored on, without constraining it', () => {
|
|
100
|
+
const asset = new CyberiaAudioModel({
|
|
101
|
+
code: 'combat',
|
|
102
|
+
manifest: readManifest(manifestFixture('combat'), 'combat.json'),
|
|
103
|
+
});
|
|
104
|
+
expect(asset.validateSync()).to.equal(undefined);
|
|
105
|
+
expect(asset.manifest.bus).to.equal('sfx');
|
|
106
|
+
// Any bus the package ships is stored as provenance; a map binding decides where an asset
|
|
107
|
+
// actually plays, so nothing here validates the value.
|
|
108
|
+
const ambient = new CyberiaAudioModel({
|
|
109
|
+
code: 'drone',
|
|
110
|
+
manifest: readManifest({ ...manifestFixture('drone'), bus: 'ambient' }, 'drone.json'),
|
|
111
|
+
});
|
|
112
|
+
expect(ambient.validateSync()).to.equal(undefined);
|
|
113
|
+
expect(ambient.manifest.bus).to.equal('ambient');
|
|
114
|
+
// The classification this model no longer carries cannot come back through the manifest.
|
|
115
|
+
expect(ambient.manifest.toObject()).to.not.have.property('type');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('CyberiaMapAudioConf schema', () => {
|
|
120
|
+
it('binds a logic event to an audio code, and nothing more', () => {
|
|
121
|
+
const doc = new CyberiaMapAudioConfModel({
|
|
122
|
+
mapCode: 'FOREST',
|
|
123
|
+
defaultMusic: 'exploration',
|
|
124
|
+
events: [
|
|
125
|
+
{ logicEventId: 'combat', audioCode: 'combat' },
|
|
126
|
+
{ logicEventId: 'shoot', audioCode: 'shoot', settings: { volume: 0.5 } },
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
expect(doc.validateSync()).to.equal(undefined);
|
|
130
|
+
expect(Object.keys(doc.events[0].toObject())).to.deep.equal(['logicEventId', 'audioCode']);
|
|
131
|
+
// No classification is duplicated between the asset layer and the configuration layer.
|
|
132
|
+
expect(JSON.stringify(doc.toObject())).to.not.match(/"type":/);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('keeps omitted overrides absent so map settings can be inherited', () => {
|
|
136
|
+
const doc = new CyberiaMapAudioConfModel({
|
|
137
|
+
mapCode: 'FOREST',
|
|
138
|
+
events: [{ logicEventId: 'combat', audioCode: 'combat' }, { logicEventId: 'shoot', audioCode: 'shoot', settings: { volume: 0.5 } }],
|
|
139
|
+
});
|
|
140
|
+
expect(doc.settings.toObject()).to.deep.equal({});
|
|
141
|
+
expect(doc.events[0].settings.toObject()).to.deep.equal({});
|
|
142
|
+
expect(doc.events[1].settings.volume).to.equal(0.5);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('requires a map code, a logic event and the asset the event resolves to', () => {
|
|
146
|
+
expect(new CyberiaMapAudioConfModel({}).validateSync().errors).to.have.property('mapCode');
|
|
147
|
+
const missing = new CyberiaMapAudioConfModel({ mapCode: 'FOREST', events: [{}] }).validateSync().errors;
|
|
148
|
+
expect(missing).to.have.property('events.0.logicEventId');
|
|
149
|
+
expect(missing).to.have.property('events.0.audioCode');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('resolves a logic event and the default music to asset codes', () => {
|
|
153
|
+
const doc = new CyberiaMapAudioConfModel({
|
|
154
|
+
mapCode: 'FOREST',
|
|
155
|
+
defaultMusic: 'exploration',
|
|
156
|
+
events: [
|
|
157
|
+
{ logicEventId: 'combat', audioCode: 'combat' },
|
|
158
|
+
{ logicEventId: 'boss', audioCode: 'mystery' },
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
const resolve = (logicEventId) => doc.events.find((entry) => entry.logicEventId === logicEventId)?.audioCode;
|
|
162
|
+
expect(resolve('combat')).to.equal('combat');
|
|
163
|
+
expect(resolve('boss')).to.equal('mystery');
|
|
164
|
+
expect(resolve('unbound')).to.equal(undefined);
|
|
165
|
+
expect(doc.defaultMusic).to.equal('exploration');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('lets one asset answer several logic events, on several maps', () => {
|
|
169
|
+
const forest = new CyberiaMapAudioConfModel({
|
|
170
|
+
mapCode: 'FOREST',
|
|
171
|
+
defaultMusic: 'mystery',
|
|
172
|
+
events: [
|
|
173
|
+
{ logicEventId: 'boss', audioCode: 'mystery' },
|
|
174
|
+
{ logicEventId: 'ambush', audioCode: 'mystery', settings: { volume: 0.4, crossfadeMs: 800 } },
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
const cave = new CyberiaMapAudioConfModel({ mapCode: 'CAVE', events: [{ logicEventId: 'idle', audioCode: 'mystery' }] });
|
|
178
|
+
expect(forest.validateSync()).to.equal(undefined);
|
|
179
|
+
expect(cave.validateSync()).to.equal(undefined);
|
|
180
|
+
// The same asset, three contexts, one asset document: the binding carries the difference.
|
|
181
|
+
expect(forest.events[1].settings.crossfadeMs).to.equal(800);
|
|
182
|
+
expect(cave.events[0].settings.crossfadeMs).to.equal(undefined);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('identifies a configuration by its map code alone', () => {
|
|
186
|
+
const [fields, options] = CyberiaMapAudioConfModel.schema.indexes().find(([keys]) => keys.mapCode);
|
|
187
|
+
expect(fields).to.deep.equal({ mapCode: 1 });
|
|
188
|
+
expect(options.unique).to.equal(true);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('readManifest', () => {
|
|
193
|
+
it("keeps the recorder's bus under the one name both sides use", () => {
|
|
194
|
+
expect(readManifest(manifestFixture('coin'), 'coin.json')).to.include({ id: 'coin', bus: 'sfx' });
|
|
195
|
+
expect(readManifest({ ...manifestFixture('combat'), bus: 'music' }, 'combat.json').bus).to.equal('music');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('leaves the bus empty when the recorder wrote none', () => {
|
|
199
|
+
const { bus, ...bare } = manifestFixture('bare');
|
|
200
|
+
expect(readManifest(bare, 'bare.json').bus).to.equal('');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('requires an asset identity, and nothing else', () => {
|
|
204
|
+
expect(() => readManifest({ bus: 'sfx' }, 'bad.json')).to.throw(/no id/);
|
|
205
|
+
expect(() => readManifest(null, 'bad.json')).to.throw(/Invalid audio manifest/);
|
|
206
|
+
expect(readManifest({ id: 'minimal' }, 'minimal.json')).to.deep.equal({ id: 'minimal', bus: '' });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('CyberiaAudioService.importRecords selection', () => {
|
|
211
|
+
let directory;
|
|
212
|
+
|
|
213
|
+
beforeEach(() => {
|
|
214
|
+
directory = fs.mkdtempSync(path.join(os.tmpdir(), 'cyberia-audio-records-'));
|
|
215
|
+
});
|
|
216
|
+
afterEach(() => fs.removeSync(directory));
|
|
217
|
+
|
|
218
|
+
const writePair = (id, { manifest = true } = {}) => {
|
|
219
|
+
fs.writeFileSync(path.join(directory, `${id}.wav`), Buffer.from('RIFF'));
|
|
220
|
+
if (manifest) fs.writeJsonSync(path.join(directory, `${id}.json`), manifestFixture(id));
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
it('skips a WAV that has no manifest beside it, reaching no database', async () => {
|
|
224
|
+
writePair('orphan', { manifest: false });
|
|
225
|
+
expect(await CyberiaAudioService.importRecords({ recordsPath: directory }, { host: 'x', path: '/' })).to.deep.equal(
|
|
226
|
+
[],
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('rejects a records directory that does not exist', async () => {
|
|
231
|
+
const missing = path.join(directory, 'nope');
|
|
232
|
+
let error;
|
|
233
|
+
await CyberiaAudioService.importRecords({ recordsPath: missing }, {}).catch((caught) => (error = caught));
|
|
234
|
+
expect(error).to.be.an('error');
|
|
235
|
+
expect(error.message).to.contain(missing);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('rejects a requested code with no WAV in the directory', async () => {
|
|
239
|
+
writePair('coin');
|
|
240
|
+
let error;
|
|
241
|
+
await CyberiaAudioService.importRecords({ recordsPath: directory, codes: ['laser'] }, {}).catch(
|
|
242
|
+
(caught) => (error = caught),
|
|
243
|
+
);
|
|
244
|
+
expect(error).to.be.an('error');
|
|
245
|
+
expect(error.message).to.contain('No laser.wav found');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('ignores non-WAV files and only considers requested codes', async () => {
|
|
249
|
+
writePair('coin');
|
|
250
|
+
writePair('shoot', { manifest: false });
|
|
251
|
+
fs.writeFileSync(path.join(directory, 'notes.txt'), 'ignored');
|
|
252
|
+
// 'coin' is filtered out, 'shoot' has no manifest: nothing is left to import,
|
|
253
|
+
// so the selection resolves without a provider being loaded.
|
|
254
|
+
expect(await CyberiaAudioService.importRecords({ recordsPath: directory, codes: ['shoot'] }, {})).to.deep.equal([]);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
STAT_TYPES, STAT_DEFAULTS, STAT_MODIFIER_MIN, STAT_MODIFIER_MAX,
|
|
8
|
+
STAT_EFFECTIVE_FLOORS, validateStats, generateRandomStats,
|
|
9
|
+
} from '../../../../src/client/components/cyberia/SharedDefaultsCyberia.js';
|
|
10
|
+
import { resolveProgressionRules } from '../../../../src/api/cyberia-server-defaults/cyberia-server-defaults.js';
|
|
11
|
+
import { generateMultiFrame, registerSemantic } from '../../../../src/projects/cyberia/semantic-layer-generator.js';
|
|
12
|
+
import { ObjectLayerModel } from '../../../../src/api/object-layer/object-layer.model.js';
|
|
13
|
+
import { CyberiaMapModel } from '../../../../src/api/cyberia-map/cyberia-map.model.js';
|
|
14
|
+
import { CyberiaMapService } from '../../../../src/api/cyberia-map/cyberia-map.service.js';
|
|
15
|
+
import { DataBaseProviderService } from '../../../../src/db/DataBaseProvider.js';
|
|
16
|
+
import { CyberiaEntityModel } from '../../../../src/api/cyberia-entity/cyberia-entity.model.js';
|
|
17
|
+
import { CyberiaInstanceConfModel } from '../../../../src/api/cyberia-instance-conf/cyberia-instance-conf.model.js';
|
|
18
|
+
import { toObjectLayerMsg, toEntityMsg, toInstanceConfig } from '../../../../src/projects/cyberia/instance-data.js';
|
|
19
|
+
import { generateStatContract, hasCyberiaSiblings } from '../../../../src/projects/cyberia/stat-contract-generator.js';
|
|
20
|
+
import { registerStatCommands } from '../../../../src/projects/cyberia/stat-commands.js';
|
|
21
|
+
|
|
22
|
+
const block = (value) => Object.fromEntries(STAT_TYPES.map((key) => [key, value]));
|
|
23
|
+
const record = (stats) => ({ data: { stats, item: { id: 'signed', type: 'weapon' }, ledger: { type: 'OFF_CHAIN' } }, sha256: 'a'.repeat(64) });
|
|
24
|
+
|
|
25
|
+
describe('Cyberia signed stat contract', () => {
|
|
26
|
+
it('defines ordered defaults and playable floors once', () => {
|
|
27
|
+
expect(STAT_TYPES).toEqual(['effect', 'resistance', 'agility', 'range', 'intelligence', 'utility']);
|
|
28
|
+
expect(validateStats({})).toEqual(STAT_DEFAULTS);
|
|
29
|
+
expect(Object.isFrozen(STAT_DEFAULTS)).toBe(true);
|
|
30
|
+
expect(STAT_EFFECTIVE_FLOORS.agility).toBeGreaterThan(-100);
|
|
31
|
+
expect(STAT_MODIFIER_MIN).toBe(-100);
|
|
32
|
+
expect(STAT_MODIFIER_MAX).toBe(100);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.each([-100, 0, 100])('accepts %s in authoring, schema, and boot transport', async (value) => {
|
|
36
|
+
const stats = block(value);
|
|
37
|
+
expect(validateStats(stats)).toEqual(stats);
|
|
38
|
+
await new ObjectLayerModel(record(stats)).validate();
|
|
39
|
+
expect(toObjectLayerMsg(record(stats)).stats).toEqual(stats);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it.each([-101, 101, 0.5, Infinity, -Infinity, NaN, null, '2'])('rejects invalid values: %s', (value) => {
|
|
43
|
+
expect(() => validateStats({ effect: value })).toThrow();
|
|
44
|
+
expect(() => toObjectLayerMsg(record({ effect: value }))).toThrow();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it.each([-101, 101, 0.5, Infinity, NaN])('rejects %s in the database schema', async (value) => {
|
|
48
|
+
await expect(new ObjectLayerModel(record({ ...STAT_DEFAULTS, effect: value })).validate()).rejects.toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('rejects XP and level inside item stats', () => {
|
|
52
|
+
expect(() => validateStats({ xp: 100 })).toThrow();
|
|
53
|
+
expect(() => validateStats({ level: 2 })).toThrow();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('generates both boundaries and rejects invalid generator bounds', () => {
|
|
57
|
+
expect(generateRandomStats(undefined, undefined, () => 0)).toEqual(block(-100));
|
|
58
|
+
expect(generateRandomStats(undefined, undefined, () => 0.999999)).toEqual(block(100));
|
|
59
|
+
expect(generateRandomStats(0, 0)).toEqual(STAT_DEFAULTS);
|
|
60
|
+
expect(() => generateRandomStats(-101, 1)).toThrow();
|
|
61
|
+
expect(() => generateRandomStats(0, 101)).toThrow();
|
|
62
|
+
expect(() => generateRandomStats(1, -1)).toThrow();
|
|
63
|
+
expect(() => generateRandomStats(0, 10, () => 1)).toThrow();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('keeps entity level separate from item ownership', async () => {
|
|
67
|
+
const entity = new CyberiaEntityModel({ entityType: 'bot', level: 25, objectLayerItemIds: ['signed'] });
|
|
68
|
+
await entity.validate();
|
|
69
|
+
expect(toEntityMsg(entity.toObject()).level).toBe(25);
|
|
70
|
+
await expect(new CyberiaEntityModel({ level: -1 }).validate()).rejects.toThrow();
|
|
71
|
+
const config = toInstanceConfig({});
|
|
72
|
+
expect(config.progressionRules.maxLevel).toBe(100);
|
|
73
|
+
expect(config).not.toHaveProperty('sumStatsLimit');
|
|
74
|
+
await expect(new CyberiaInstanceConfModel({ instanceCode: 'test', progressionRules: { maxLevel: -1 } }).validate()).rejects.toThrow();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('resolves partial curves and rejects invalid progression configuration', () => {
|
|
78
|
+
const rules = resolveProgressionRules({ baseStats: { effect: 20 }, defaultBotLevel: 7 });
|
|
79
|
+
expect(rules.baseStats).toEqual({ ...resolveProgressionRules().baseStats, effect: 20 });
|
|
80
|
+
expect(toInstanceConfig({ progressionRules: { defaultBotLevel: 7 } }).progressionRules.defaultBotLevel).toBe(7);
|
|
81
|
+
for (const input of [
|
|
82
|
+
{ maxLevel: 5, defaultBotLevel: 6 }, { xpPerLevel: 0 }, { xpPerLevel: '100' },
|
|
83
|
+
{ baseStats: null }, { perLevelStats: [] }, { baseStats: { effect: -1 } },
|
|
84
|
+
{ perLevelStats: { level: 1 } }, { unknown: 1 }, { maxAwardsPerWindow: 0 },
|
|
85
|
+
]) expect(() => resolveProgressionRules(input)).toThrow();
|
|
86
|
+
for (const level of [0, -1, 65536, 1.5, null, '2']) expect(() => toEntityMsg({ level })).toThrow();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('uses signed deterministic stats in the procedural pipeline', () => {
|
|
90
|
+
registerSemantic('stat-contract', { semanticTags: ['test'], paletteHints: [[0, 0, 0, 255]],
|
|
91
|
+
preferredShapes: {}, layers: {}, itemType: 'weapon' });
|
|
92
|
+
const options = { itemId: 'stat-contract-test', seed: 'signed-contract', frameCount: 1 };
|
|
93
|
+
const generated = generateMultiFrame(options).objectLayerData.data.stats;
|
|
94
|
+
expect(validateStats(generated)).toEqual(generated);
|
|
95
|
+
expect(generateMultiFrame(options).objectLayerData.data.stats).toEqual(generated);
|
|
96
|
+
expect(Object.values(generated).some((value) => value < 0)).toBe(true);
|
|
97
|
+
const samples = Array.from({ length: 8 }, (_, index) =>
|
|
98
|
+
Object.values(generateMultiFrame({ ...options, seed: 'signed-' + index }).objectLayerData.data.stats)).flat();
|
|
99
|
+
expect(samples.some((value) => value > 10)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('rejects invalid map levels before file cleanup', async () => {
|
|
103
|
+
const model = vi.spyOn(DataBaseProviderService, 'getModel').mockImplementation((name) => {
|
|
104
|
+
expect(name).toBe('CyberiaMap');
|
|
105
|
+
return CyberiaMapModel;
|
|
106
|
+
});
|
|
107
|
+
const lookup = vi.spyOn(CyberiaMapModel, 'findById').mockResolvedValue(new CyberiaMapModel({ code: 'level-test' }));
|
|
108
|
+
try {
|
|
109
|
+
await expect(CyberiaMapService.put({ params: { id: 'test' }, auth: { user: { role: 'admin' } },
|
|
110
|
+
body: { entities: [{ entityType: 'bot', level: -1 }] } }, {}, {})).rejects.toThrow();
|
|
111
|
+
expect(model).toHaveBeenCalledTimes(1);
|
|
112
|
+
} finally {
|
|
113
|
+
model.mockRestore();
|
|
114
|
+
lookup.mockRestore();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// cyberia-server and cyberia-client are gitignored siblings this repo does not contain (see
|
|
119
|
+
// .gitignore); a checkout that only has this repo's own tracked tree has nothing to check.
|
|
120
|
+
it.skipIf(!hasCyberiaSiblings())('matches generated Go and C contracts', async () => {
|
|
121
|
+
await generateStatContract({ check: true });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('Cyberia stats CLI', () => {
|
|
126
|
+
it('validates files and generates bounded random stats', async () => {
|
|
127
|
+
const directory = await mkdtemp(join(tmpdir(), 'cyberia-stats-test-'));
|
|
128
|
+
const output = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
129
|
+
const run = async (...args) => {
|
|
130
|
+
const command = new Command().exitOverride();
|
|
131
|
+
registerStatCommands(command);
|
|
132
|
+
await command.parseAsync(args, { from: 'user' });
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
const input = join(directory, 'input.json');
|
|
136
|
+
const source = JSON.stringify([record(block(-100))]);
|
|
137
|
+
await writeFile(input, source);
|
|
138
|
+
await run('stats', 'validate', input);
|
|
139
|
+
expect(JSON.parse(output.mock.calls.at(-1)[0])).toEqual({ valid: true, records: 1 });
|
|
140
|
+
await writeFile(input, JSON.stringify([record(block(101))]));
|
|
141
|
+
await expect(run('stats', 'validate', input)).rejects.toThrow();
|
|
142
|
+
await run('stats', 'random', '--min=-100', '--max=-100');
|
|
143
|
+
expect(JSON.parse(output.mock.calls.at(-1)[0])).toEqual(block(-100));
|
|
144
|
+
await expect(run('stats', 'random', '--min=-101')).rejects.toThrow();
|
|
145
|
+
} finally {
|
|
146
|
+
output.mockRestore();
|
|
147
|
+
await rm(directory, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|