@underpostnet/cyberia 3.2.80 → 3.2.90
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 +34 -15
- package/.github/workflows/cyberia-client.cd.yml +13 -1
- package/.github/workflows/cyberia-server.cd.yml +13 -1
- package/.github/workflows/docker-image.cyberia-client.ci.yml +4 -4
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +4 -4
- package/.github/workflows/docker-image.cyberia-server.ci.yml +4 -4
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +4 -4
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +3 -3
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +3 -3
- package/.github/workflows/engine-cyberia.cd.yml +14 -3
- package/CHANGELOG.md +182 -1
- package/CLI-HELP.md +37 -16
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +1 -1
- package/bin/cyberia.js +203 -49
- package/bin/deploy.js +18 -16
- package/bin/index.js +203 -49
- package/compose.env +34 -15
- package/deployment.yaml +1 -210
- package/docker-compose.yml +73 -62
- package/hardhat/package-lock.json +134 -126
- package/hardhat/package.json +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -210
- package/manifests/deployment/dd-cyberia-development/gateway.yaml +80 -0
- package/manifests/deployment/dd-cyberia-development/httproute.yaml +504 -0
- package/manifests/deployment/dd-cyberia-development/proxy.yaml +12 -12
- package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +0 -82
- package/manifests/deployment/dd-cyberia-development/traffic-service.yaml +121 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/playwright/deployment.yaml +1 -1
- package/manifests/mongodb/kustomization.yaml +4 -1
- package/manifests/mongodb/statefulset.yaml +4 -0
- package/manifests/mongodb/storage-class.yaml +9 -2
- package/nginx.conf +86 -16
- package/package.json +2 -2
- package/proxy.yaml +12 -12
- package/pv-pvc.yaml +0 -82
- package/scripts/nat-iptables.sh +10 -4
- package/scripts/test-monitor.sh +4 -3
- package/src/api/cyberia-action/cyberia-action.model.js +1 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +41 -14
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +8 -12
- package/src/api/cyberia-instance/cyberia-portal-connector.js +7 -5
- package/src/api/cyberia-instance/cyberia-random-source.js +80 -0
- package/src/api/cyberia-instance/cyberia-world-generator.js +4 -3
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +73 -0
- package/src/cli/cluster.js +740 -55
- package/src/cli/db.js +2 -2
- package/src/cli/deploy.js +1679 -174
- package/src/cli/docker-compose.js +19 -178
- package/src/cli/image.js +15 -6
- package/src/cli/index.js +124 -35
- package/src/cli/ipfs.js +82 -11
- package/src/cli/monitor.js +1 -1
- package/src/cli/repository.js +1 -1
- package/src/cli/run.js +2161 -420
- package/src/cli/secrets.js +969 -0
- package/src/cli/ssh.js +8 -28
- package/src/client/components/cyberia/InstanceSelectionView.js +11 -8
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +5 -0
- package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +106 -39
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +18 -0
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +44 -7
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client-builder/client-build.js +94 -11
- package/src/client-builder/ssr.js +27 -73
- package/src/db/mongo/MongoBootstrap.js +295 -54
- package/src/db/mongo/MongooseDB.js +47 -32
- package/src/index.js +1 -1
- package/src/projects/cyberia/besu-genesis-generator.js +3 -2
- package/src/projects/cyberia/hot-reload-trigger.js +3 -3
- package/src/projects/cyberia/instance-data.js +42 -1
- package/src/runtime/cyberia-client/Dockerfile +1 -1
- package/src/runtime/cyberia-client/Dockerfile.dev +1 -1
- package/src/runtime/cyberia-server/Dockerfile +1 -1
- package/src/runtime/cyberia-server/Dockerfile.dev +1 -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 +1 -1
- package/src/runtime/engine-cyberia/compose.env +34 -15
- package/src/runtime/engine-cyberia/docker-compose.yml +73 -62
- package/src/runtime/engine-cyberia/nginx.conf +86 -16
- package/src/server/conf.js +1208 -70
- package/src/server/cri.js +70 -0
- package/src/server/underpost-gateway.js +1073 -0
- package/src/server/underpost-ingress.js +364 -0
- package/test/cluster-instances.test.js +435 -0
- package/test/deploy-node-placement.test.js +45 -0
- package/test/instance-traffic-plan.test.js +710 -0
- package/test/sops-secret-store.test.js +612 -0
- package/test/underpost-gateway.test.js +469 -0
- package/test/underpost-ingress.test.js +253 -0
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { connectPortals } from './cyberia-portal-connector.js';
|
|
22
|
+
import { withSeededRandom } from './cyberia-random-source.js';
|
|
22
23
|
|
|
23
24
|
import {
|
|
24
25
|
generateObstacles,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
DEFAULT_PLAYER_SPAWN,
|
|
39
40
|
ENTITY_TYPE_DEFAULTS,
|
|
40
41
|
RESOURCE_ENTITY_TYPE_DEFAULTS,
|
|
42
|
+
DefaultCyberiaActions,
|
|
41
43
|
DefaultSkillConfig,
|
|
42
44
|
} from '../cyberia-server-defaults/cyberia-server-defaults.js';
|
|
43
45
|
import {
|
|
@@ -92,6 +94,19 @@ function collectReferencedItemIds() {
|
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
}
|
|
97
|
+
// Provider catalogs: the interact modal draws an icon for every item a
|
|
98
|
+
// vendor sells and every ingredient/output an assembler recipe names, so all
|
|
99
|
+
// of them must resolve to an ObjectLayer.
|
|
100
|
+
for (const action of DefaultCyberiaActions) {
|
|
101
|
+
for (const shopItem of action.shopItems || []) {
|
|
102
|
+
push(shopItem.itemId);
|
|
103
|
+
push(shopItem.priceItemId);
|
|
104
|
+
}
|
|
105
|
+
for (const recipe of action.craftRecipes || []) {
|
|
106
|
+
(recipe.ingredients || []).forEach((i) => push(i.itemId));
|
|
107
|
+
(recipe.outputItems || []).forEach((o) => push(o.itemId));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
95
110
|
return ids;
|
|
96
111
|
}
|
|
97
112
|
|
|
@@ -262,20 +277,32 @@ function generateFallbackWorld(opts = {}) {
|
|
|
262
277
|
mapCodes.push(`fallback-map-${i}`);
|
|
263
278
|
}
|
|
264
279
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
280
|
+
// Deterministic layout: seed the shared random source so every call
|
|
281
|
+
// reproduces the SAME world. The instance-map `/static` POIs and the
|
|
282
|
+
// `/preview` node image are built by independent requests (and survive
|
|
283
|
+
// restarts) — they must agree on where every entity sits, so a resource POI
|
|
284
|
+
// lands exactly where the preview draws that resource. The seed folds in the
|
|
285
|
+
// layout-affecting options so a custom count set stays internally consistent.
|
|
286
|
+
const seed = `cyberia-fallback-world-v1:${mapCount}:${gridSize ?? ''}:${obstacleCount ?? ''}:${
|
|
287
|
+
foregroundCount ?? ''
|
|
288
|
+
}:${botCount ?? ''}:${resourceCount ?? ''}:${staticCount ?? ''}`;
|
|
289
|
+
|
|
290
|
+
const { maps, portals, topology } = withSeededRandom(seed, () => {
|
|
291
|
+
// Generate each map (deterministic under the seeded source).
|
|
292
|
+
const genMaps = mapCodes.map((code) =>
|
|
293
|
+
generateFallbackMap(code, colors, {
|
|
294
|
+
gridSize,
|
|
295
|
+
obstacleCount,
|
|
296
|
+
foregroundCount,
|
|
297
|
+
botCount,
|
|
298
|
+
resourceCount,
|
|
299
|
+
staticCount,
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
// Connect maps with portal topology using the portal connector module.
|
|
303
|
+
const { portals: p, topology: t } = connectPortals(mapCodes, genMaps);
|
|
304
|
+
return { maps: genMaps, portals: p, topology: t };
|
|
305
|
+
});
|
|
279
306
|
|
|
280
307
|
// Build the instance shell (never persisted — no mongoId).
|
|
281
308
|
const instance = {
|
|
@@ -31,8 +31,8 @@ import {
|
|
|
31
31
|
DefaultCyberiaActions,
|
|
32
32
|
DefaultCyberiaQuests,
|
|
33
33
|
ENTITY_TYPE_DEFAULTS,
|
|
34
|
-
RESOURCE_ENTITY_TYPE_DEFAULTS,
|
|
35
34
|
} from '../cyberia-server-defaults/cyberia-server-defaults.js';
|
|
35
|
+
import { DefaultCyberiaItems } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
|
|
36
36
|
|
|
37
37
|
const logger = loggerFactory(import.meta);
|
|
38
38
|
|
|
@@ -430,22 +430,18 @@ const resolveInstanceWorld = async (instanceCode, options) => {
|
|
|
430
430
|
const instance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
|
|
431
431
|
|
|
432
432
|
if (!instance) {
|
|
433
|
-
// Fallback world: topology is regenerated (portal cells may drift from
|
|
434
|
-
// the live simulation's world), but map codes and provider POIs come
|
|
435
|
-
// from the deterministic canonical defaults.
|
|
436
433
|
const world = generateFallbackWorld();
|
|
437
434
|
const mapCodes = new Set(world.instance.cyberiaMapCodes);
|
|
438
435
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
// resource defaults (wood-1, wood-2) are not persisted to MongoDB in the
|
|
442
|
-
// fallback path, so we compute a reasonable stats sum from the item type's
|
|
443
|
-
// default stat block (each stat 0-10, 6 stats → ~30 average).
|
|
444
|
-
const resourceItemIds = [
|
|
445
|
-
...new Set(RESOURCE_ENTITY_TYPE_DEFAULTS.flatMap((r) => [...(r.liveItemIds || []), ...(r.dropItemIds || [])])),
|
|
436
|
+
const fallbackItemIds = [
|
|
437
|
+
...new Set(world.maps.flatMap((m) => (m.entities || []).flatMap((e) => e.objectLayerItemIds || []))),
|
|
446
438
|
];
|
|
439
|
+
const itemTypeById = Object.fromEntries(DefaultCyberiaItems.map((entry) => [entry.item.id, entry.item.type]));
|
|
440
|
+
const resolved = await resolveObjectLayerMetadata(fallbackItemIds, options);
|
|
447
441
|
const objectLayerMetadata = Object.fromEntries(
|
|
448
|
-
|
|
442
|
+
Object.entries(resolved).map(([itemId, meta]) => {
|
|
443
|
+
return [itemId, { statsSum: meta.statsSum, type: meta.type }];
|
|
444
|
+
}),
|
|
449
445
|
);
|
|
450
446
|
|
|
451
447
|
return {
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* @module src/api/cyberia-instance/cyberia-portal-connector
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { nextRandom } from './cyberia-random-source.js';
|
|
24
|
+
|
|
23
25
|
// ── Portal mode constants ────────────────────────────────────────────────────
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -117,7 +119,7 @@ function buildTopologyFromSubtypes(orderedCodes, portalIndex) {
|
|
|
117
119
|
|
|
118
120
|
// Target: pick any portal on the target map for landing coordinates
|
|
119
121
|
const tgtAll = portalIndex[tgtCode] || [];
|
|
120
|
-
const tgtEnt = tgtAll.length > 0 ? tgtAll[Math.floor(
|
|
122
|
+
const tgtEnt = tgtAll.length > 0 ? tgtAll[Math.floor(nextRandom() * tgtAll.length)] : null;
|
|
121
123
|
|
|
122
124
|
if (srcEnt) {
|
|
123
125
|
usedInRing.add(srcEnt);
|
|
@@ -139,7 +141,7 @@ function buildTopologyFromSubtypes(orderedCodes, portalIndex) {
|
|
|
139
141
|
if (n < 2) return srcCode;
|
|
140
142
|
let code;
|
|
141
143
|
do {
|
|
142
|
-
code = orderedCodes[Math.floor(
|
|
144
|
+
code = orderedCodes[Math.floor(nextRandom() * n)];
|
|
143
145
|
} while (code === srcCode && n > 1);
|
|
144
146
|
return code;
|
|
145
147
|
};
|
|
@@ -151,13 +153,13 @@ function buildTopologyFromSubtypes(orderedCodes, portalIndex) {
|
|
|
151
153
|
for (const srcEnt of remaining) {
|
|
152
154
|
// If the entity already has an explicit subtype, honour it;
|
|
153
155
|
// otherwise assign a random mode (matching fallback-world behaviour).
|
|
154
|
-
const sub = srcEnt.portalSubtype || PORTAL_MODE_LIST[Math.floor(
|
|
156
|
+
const sub = srcEnt.portalSubtype || PORTAL_MODE_LIST[Math.floor(nextRandom() * PORTAL_MODE_LIST.length)];
|
|
155
157
|
|
|
156
158
|
switch (sub) {
|
|
157
159
|
case PORTAL_MODES.INTER_PORTAL: {
|
|
158
160
|
const tgtCode = otherMap(srcCode);
|
|
159
161
|
const candidates = portalIndex[tgtCode] || [];
|
|
160
|
-
const tgtEnt = candidates.length > 0 ? candidates[Math.floor(
|
|
162
|
+
const tgtEnt = candidates.length > 0 ? candidates[Math.floor(nextRandom() * candidates.length)] : null;
|
|
161
163
|
portals.push({
|
|
162
164
|
sourceMapCode: srcCode,
|
|
163
165
|
sourceCellX: srcEnt.initCellX ?? 0,
|
|
@@ -198,7 +200,7 @@ function buildTopologyFromSubtypes(orderedCodes, portalIndex) {
|
|
|
198
200
|
const candidates = allOnMap.filter(
|
|
199
201
|
(e) => e !== srcEnt && (e.initCellX !== srcEnt.initCellX || e.initCellY !== srcEnt.initCellY),
|
|
200
202
|
);
|
|
201
|
-
const tgtEnt = candidates.length > 0 ? candidates[Math.floor(
|
|
203
|
+
const tgtEnt = candidates.length > 0 ? candidates[Math.floor(nextRandom() * candidates.length)] : null;
|
|
202
204
|
portals.push({
|
|
203
205
|
sourceMapCode: srcCode,
|
|
204
206
|
sourceCellX: srcEnt.initCellX ?? 0,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swappable random source for procedural world generation.
|
|
3
|
+
*
|
|
4
|
+
* The world generator and portal connector draw all randomness through
|
|
5
|
+
* `nextRandom()` instead of `Math.random()` directly. By default it IS
|
|
6
|
+
* `Math.random`, so live/editor generation stays random. The fallback world
|
|
7
|
+
* temporarily installs a seeded PRNG so the SAME layout is produced on every
|
|
8
|
+
* call — the instance-map `/static` POIs and the `/preview` image are built by
|
|
9
|
+
* independent HTTP requests (and survive server restarts), so they must agree
|
|
10
|
+
* on where every entity sits.
|
|
11
|
+
*
|
|
12
|
+
* @module src/api/cyberia-instance/cyberia-random-source.js
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
let source = Math.random;
|
|
16
|
+
|
|
17
|
+
/** Draw the next float in [0, 1). Routes through the installed source. */
|
|
18
|
+
const nextRandom = () => source();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* mulberry32 — a small, fast, well-distributed seeded PRNG. Deterministic:
|
|
22
|
+
* the same seed always yields the same sequence.
|
|
23
|
+
* @param {number} seed 32-bit unsigned integer
|
|
24
|
+
* @returns {() => number}
|
|
25
|
+
*/
|
|
26
|
+
function mulberry32(seed) {
|
|
27
|
+
let a = seed >>> 0;
|
|
28
|
+
return function () {
|
|
29
|
+
a |= 0;
|
|
30
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
31
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
32
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
33
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Derive a stable 32-bit seed from a string (FNV-1a). */
|
|
38
|
+
function hashSeed(str) {
|
|
39
|
+
let h = 0x811c9dc5;
|
|
40
|
+
for (let i = 0; i < String(str).length; i++) {
|
|
41
|
+
h ^= String(str).charCodeAt(i);
|
|
42
|
+
h = Math.imul(h, 0x01000193);
|
|
43
|
+
}
|
|
44
|
+
return h >>> 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Install a deterministic seeded source. Accepts a number or a string
|
|
49
|
+
* (hashed to a 32-bit seed). Call `resetRandomSource()` to restore Math.random.
|
|
50
|
+
* @param {number|string} seed
|
|
51
|
+
*/
|
|
52
|
+
function seedRandomSource(seed) {
|
|
53
|
+
const s = typeof seed === 'number' ? seed >>> 0 : hashSeed(seed);
|
|
54
|
+
source = mulberry32(s);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Restore the default `Math.random` source. */
|
|
58
|
+
function resetRandomSource() {
|
|
59
|
+
source = Math.random;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Run `fn` with a deterministic seeded source, restoring the previous source
|
|
64
|
+
* afterward even if `fn` throws. Returns `fn`'s result.
|
|
65
|
+
* @template T
|
|
66
|
+
* @param {number|string} seed
|
|
67
|
+
* @param {() => T} fn
|
|
68
|
+
* @returns {T}
|
|
69
|
+
*/
|
|
70
|
+
function withSeededRandom(seed, fn) {
|
|
71
|
+
const prev = source;
|
|
72
|
+
seedRandomSource(seed);
|
|
73
|
+
try {
|
|
74
|
+
return fn();
|
|
75
|
+
} finally {
|
|
76
|
+
source = prev;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { nextRandom, seedRandomSource, resetRandomSource, withSeededRandom, hashSeed };
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from '../cyberia-server-defaults/cyberia-server-defaults.js';
|
|
24
24
|
|
|
25
25
|
import { DefaultCyberiaItems, ITEM_TYPES } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
|
|
26
|
+
import { nextRandom } from './cyberia-random-source.js';
|
|
26
27
|
|
|
27
28
|
import { PORTAL_MODES, PORTAL_MODE_COLOR_KEY, EXTRA_PORTAL_MODES } from './cyberia-portal-connector.js';
|
|
28
29
|
|
|
@@ -51,7 +52,7 @@ const findColor = (colors, key) => colors.find((c) => c.key === key);
|
|
|
51
52
|
* @param {number} max
|
|
52
53
|
* @returns {number}
|
|
53
54
|
*/
|
|
54
|
-
const randInt = (min, max) => min + Math.floor(
|
|
55
|
+
const randInt = (min, max) => min + Math.floor(nextRandom() * (max - min + 1));
|
|
55
56
|
|
|
56
57
|
// ── Entity count ranges ──────────────────────────────────────────────────────
|
|
57
58
|
// [min, max] — actual count is random within range on each generation call.
|
|
@@ -438,7 +439,7 @@ function generatePortalEntities(mapDims, colors, opts = {}) {
|
|
|
438
439
|
// First portal is always inter-portal (reserved for the ring topology);
|
|
439
440
|
// extra portals get a random non-ring subtype.
|
|
440
441
|
const subtype =
|
|
441
|
-
i === 0 ? PORTAL_MODES.INTER_PORTAL : EXTRA_PORTAL_MODES[Math.floor(
|
|
442
|
+
i === 0 ? PORTAL_MODES.INTER_PORTAL : EXTRA_PORTAL_MODES[Math.floor(nextRandom() * EXTRA_PORTAL_MODES.length)];
|
|
442
443
|
const portal = generatePortalEntity(mapDims, colors, opts.grid, subtype);
|
|
443
444
|
if (portal) entities.push(portal);
|
|
444
445
|
}
|
|
@@ -484,7 +485,7 @@ function generateBots(mapDims, colors, opts = {}) {
|
|
|
484
485
|
|
|
485
486
|
// const skin = BOT_SKIN_POOL[Math.floor(Math.random() * BOT_SKIN_POOL.length)];
|
|
486
487
|
const skin = 'kishins';
|
|
487
|
-
const hasWeapon =
|
|
488
|
+
const hasWeapon = nextRandom() < BOT_WEAPON_CHANCE;
|
|
488
489
|
const itemIds = hasWeapon ? [skin, 'atlas_pistol_mk2'] : [skin];
|
|
489
490
|
|
|
490
491
|
entities.push({
|
|
@@ -591,6 +591,76 @@ export const DefaultCyberiaActions = [
|
|
|
591
591
|
dialogCode: 'default-agent',
|
|
592
592
|
questDialogueCodes: [{ questCode: 'bounty-quest-alpha', dialogCode: 'quest-talk-agent' }],
|
|
593
593
|
},
|
|
594
|
+
{
|
|
595
|
+
// Shop capability: an action carrying shopItems is a vendor. The client
|
|
596
|
+
// surfaces a Shop tab for it and the simulation validates every purchase
|
|
597
|
+
// against this catalog (price item + quantity).
|
|
598
|
+
code: 'loc-fallback-map-0-18-16',
|
|
599
|
+
label: 'Punk',
|
|
600
|
+
sourceMapCode: 'fallback-map-0',
|
|
601
|
+
sourceCellX: 18,
|
|
602
|
+
sourceCellY: 16,
|
|
603
|
+
dialogCode: 'default-punk',
|
|
604
|
+
shopItems: [{ itemId: 'tim-knife', priceItemId: 'coin', priceQty: 10 }],
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
// Assembler capability: an action carrying craftRecipes is a fabrication
|
|
608
|
+
// terminal. The client surfaces an Assembly tab for it and the simulation
|
|
609
|
+
// validates every synthesis against these recipes (ingredients → outputs).
|
|
610
|
+
// Every id here is obtainable inside the fallback world — wood from the
|
|
611
|
+
// resource nodes, coin from field drops — so the loop is playable on a
|
|
612
|
+
// fresh spawn with no seeding.
|
|
613
|
+
code: 'loc-fallback-map-0-15-16',
|
|
614
|
+
label: 'Eiri',
|
|
615
|
+
sourceMapCode: 'fallback-map-0',
|
|
616
|
+
sourceCellX: 15,
|
|
617
|
+
sourceCellY: 16,
|
|
618
|
+
dialogCode: 'default-eiri',
|
|
619
|
+
craftRecipes: [
|
|
620
|
+
{
|
|
621
|
+
outputItems: [{ itemId: 'hatchet', qty: 1 }],
|
|
622
|
+
ingredients: [
|
|
623
|
+
{ itemId: 'wood-drop-1', qty: 2 },
|
|
624
|
+
{ itemId: 'coin', qty: 5 },
|
|
625
|
+
],
|
|
626
|
+
craftTimeMs: 3000,
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
outputItems: [{ itemId: 'tim-knife', qty: 1 }],
|
|
630
|
+
ingredients: [
|
|
631
|
+
{ itemId: 'wood-drop-2', qty: 1 },
|
|
632
|
+
{ itemId: 'wood-drop-1', qty: 1 },
|
|
633
|
+
{ itemId: 'coin', qty: 10 },
|
|
634
|
+
],
|
|
635
|
+
craftTimeMs: 5000,
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
// Multi-output: one blade breaks down into three component stacks, so
|
|
639
|
+
// the assembly UI and the arrival flights are exercised with more than
|
|
640
|
+
// a single result slot. Closes the loop with Punk's shop — buy a knife,
|
|
641
|
+
// salvage it back into parts.
|
|
642
|
+
outputItems: [
|
|
643
|
+
{ itemId: 'wood-drop-1', qty: 2 },
|
|
644
|
+
{ itemId: 'wood-drop-2', qty: 1 },
|
|
645
|
+
{ itemId: 'coin', qty: 5 },
|
|
646
|
+
],
|
|
647
|
+
ingredients: [{ itemId: 'tim-knife', qty: 1 }],
|
|
648
|
+
craftTimeMs: 2000,
|
|
649
|
+
},
|
|
650
|
+
],
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
// Storage capability: an action carrying storageSlots is a personal vault.
|
|
654
|
+
// The client surfaces a Storage tab whose square grid is sized from the
|
|
655
|
+
// capacity (25 → 5x5) and the simulation owns the contents.
|
|
656
|
+
code: 'loc-fallback-map-0-12-22',
|
|
657
|
+
label: 'Kaneki',
|
|
658
|
+
sourceMapCode: 'fallback-map-0',
|
|
659
|
+
sourceCellX: 12,
|
|
660
|
+
sourceCellY: 22,
|
|
661
|
+
dialogCode: 'default-kaneki',
|
|
662
|
+
storageSlots: 25,
|
|
663
|
+
},
|
|
594
664
|
{
|
|
595
665
|
code: 'loc-fallback-map-0-15-22',
|
|
596
666
|
label: 'Lain',
|
|
@@ -868,6 +938,9 @@ export const ENTITY_TYPE_DEFAULTS = Object.freeze([
|
|
|
868
938
|
{ entityType: ENTITY_TYPES.bot, liveItemIds: ['wason'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
|
|
869
939
|
{ entityType: ENTITY_TYPES.bot, liveItemIds: ['alex'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
|
|
870
940
|
{ entityType: ENTITY_TYPES.bot, liveItemIds: ['agent'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
|
|
941
|
+
{ entityType: ENTITY_TYPES.bot, liveItemIds: ['punk'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
|
|
942
|
+
{ entityType: ENTITY_TYPES.bot, liveItemIds: ['eiri'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider-static' },
|
|
943
|
+
{ entityType: ENTITY_TYPES.bot, liveItemIds: ['kaneki'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider-static' },
|
|
871
944
|
{
|
|
872
945
|
entityType: ENTITY_TYPES.bot,
|
|
873
946
|
liveItemIds: ['lain'],
|