@maka/maka-cli 5.378.0 → 5.379.1
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/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +16 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +271 -131
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +26 -31
- package/bundle/typescript/src/commands/game/sideQuest/commands/give.js +24 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +21 -128
- package/bundle/typescript/src/commands/game/sideQuest/commands/transfer.js +92 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/upload.js +108 -0
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +19 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/job-order.js +28 -4
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +125 -32
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/game.js +24 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +26 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +16 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +37 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/data-library.js +158 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/data-work.js +4 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/edit-file-test.js +31 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/fixer-menu.js +10 -8
- package/bundle/typescript/src/commands/game/sideQuest/utilities/gear-tree.js +13 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +4 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/verb-index.js +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { isOutdoorsPlace } from '../utilities/outdoors.js';
|
|
|
6
6
|
import { exitContradictions } from '../utilities/district-plane.js';
|
|
7
7
|
import { clampDeviceKind } from '../utilities/affordances.js';
|
|
8
8
|
import { kitSlugForTier } from '../npc-kits.js';
|
|
9
|
-
import { SIZE_SECURITY_BANDS, SIZE_HOST_BANDS, scopeFor, postsLieutenant, sizeForTier, highestOpposingPool, payFor, rollTwist, requirementsFor, DATA_RUN_SHARE, objectiveRoomFor, } from './job-order.js';
|
|
9
|
+
import { SIZE_SECURITY_BANDS, SIZE_HOST_BANDS, scopeFor, postsLieutenant, sizeForTier, highestOpposingPool, payFor, rollTwist, requirementsFor, DATA_RUN_SHARE, objectiveRoomFor, isDataCourier, courierMustBeUnseen, } from './job-order.js';
|
|
10
10
|
export { DATA_RUN_SHARE } from './job-order.js';
|
|
11
11
|
/**
|
|
12
12
|
* CHUNKED scene generation (see SceneSeedGenerator.generate for the
|
|
@@ -221,7 +221,10 @@ export function settleSecurity(skeleton, campaignTier, _player, crew, order) {
|
|
|
221
221
|
// which rolls Host Rating x 2 (p.247-248); a physical job's is the
|
|
222
222
|
// tier's statblock, lieutenant included when posted.
|
|
223
223
|
const winItem = skeleton.items.find(i => i.name === skeleton.winCondition?.item);
|
|
224
|
-
|
|
224
|
+
// A data courier's opposition is the DROP host's IC (its target), not
|
|
225
|
+
// the client the cargo starts with.
|
|
226
|
+
const vaultName = winItem?.plane === 'matrix' ? (skeleton.winCondition?.target ?? winItem.room) : undefined;
|
|
227
|
+
const vault = vaultName ? skeleton.rooms.find(r => r.name === vaultName) : undefined;
|
|
225
228
|
const pool = vault ? 2 * (vault.hostRating ?? VAULT_HOST_RATING) : highestOpposingPool(settled, lieutenant);
|
|
226
229
|
const heads = partyHeadcount(crew);
|
|
227
230
|
const wrote = skeleton.winCondition?.payout;
|
|
@@ -280,7 +283,27 @@ export function buildSkeletonPrompt(tier, player, previousError, rumor, crew, ca
|
|
|
280
283
|
// winCondition must therefore look like -- validateSkeleton holds the
|
|
281
284
|
// model to exactly that.
|
|
282
285
|
const kind = order?.kind ?? 'datasteal';
|
|
283
|
-
|
|
286
|
+
// THE DATA COURIER (user ruling 2026-10-04): a delivery whose package is
|
|
287
|
+
// a file and whose drop is a host (win-condition 'upload').
|
|
288
|
+
const courier = isDataCourier(kind, shape);
|
|
289
|
+
const unseen = courierMustBeUnseen(size);
|
|
290
|
+
const kindBlock = courier ? `
|
|
291
|
+
THIS JOB IS A DATA COURIER RUN (SR5 p.338, delivery -- the package is DATA and the drop is a
|
|
292
|
+
HOST). Build it accordingly:
|
|
293
|
+
- The winCondition item is the CARGO FILE: role "winCondition", "plane": "matrix", category
|
|
294
|
+
Document, "heldBy" the CLIENT -- they push it onto the runner's device at the meet (the engine
|
|
295
|
+
does that the moment the meet is held). Never place it in a room, never in anyone else's hands.
|
|
296
|
+
- winCondition.target is the DROP: a rooms[].name with "hasNode": true and a "hostRating" of
|
|
297
|
+
${hostBand.min === hostBand.max ? hostBand.min : `${hostBand.min}-${hostBand.max}`} (this is a ${size} job), and NOT the client's room. The runner must get a persona
|
|
298
|
+
INSIDE that host and upload the file there. The host defends itself with its own ice at its
|
|
299
|
+
rating -- write NO "matrix"-plane npc. There is no "toNpc": nobody is handed anything.
|
|
300
|
+
- WHY IT GOES THERE is the fiction: a plant (forged records, a worm, a confession, a backdoor,
|
|
301
|
+
evidence), a dead drop for someone who will come for it, a restore of something stolen.
|
|
302
|
+
${unseen ? `- THIS IS A HARDER JOB: the plant must land UNSEEN -- the engine fails it if the host is on
|
|
303
|
+
alert when the file arrives. The site should make quiet hard: cameras, a spider who works
|
|
304
|
+
the host, staff who notice a runner where none should be.
|
|
305
|
+
` : ''}- The client briefs the drop by its room name and says what the file is for.
|
|
306
|
+
` : kind === 'delivery' ? `
|
|
284
307
|
THIS JOB IS A DELIVERY (SR5 p.338: "take the thing to the place"). Build it accordingly:
|
|
285
308
|
- The winCondition item is the CARGO: role "winCondition", "heldBy" the CLIENT -- they hand
|
|
286
309
|
it over at the meet (the engine does that the moment the meet is held). Never place it in
|
|
@@ -340,7 +363,18 @@ ONE OBJECT.
|
|
|
340
363
|
- There is NO winCondition item: omit "item" and "toNpc", and write NO items[] entry with
|
|
341
364
|
role "winCondition".
|
|
342
365
|
` : '';
|
|
343
|
-
const winConditionSchema =
|
|
366
|
+
const winConditionSchema = courier ? `
|
|
367
|
+
"winCondition": {
|
|
368
|
+
"item": string, // the CARGO FILE: the items[].name with role "winCondition", "plane": "matrix",
|
|
369
|
+
// heldBy the client (see THIS JOB IS A DATA COURIER RUN above)
|
|
370
|
+
"target": string, // the DROP: a rooms[].name with "hasNode": true -- the host it is uploaded to.
|
|
371
|
+
// No "toNpc".
|
|
372
|
+
"payout": number, // the job's pay in nuyen. THE ENGINE SETS THE REAL FIGURE from the
|
|
373
|
+
// site's securityTier (SR5 p.371: 3,000 per runner x the opposition's
|
|
374
|
+
// dice pool / 4, plus the danger modifiers); write ${payFor({ pool: highestOpposingPool(band.min, postsLieutenant(size)), heads: partyHeadcount(crew), hostiles: 0, securityTier: band.min })}
|
|
375
|
+
// here and it will be replaced. It is paid as one lump sum on the wrap.
|
|
376
|
+
"rewardItem"?: string // optional bonus: an items[].name with role "reward"
|
|
377
|
+
},` : kind === 'extraction' ? `
|
|
344
378
|
"winCondition": {
|
|
345
379
|
"target": string, // the PERSON: an npcs[].name, never the client -- see THIS JOB IS AN
|
|
346
380
|
// EXTRACTION above. No "item".
|
|
@@ -389,7 +423,7 @@ the runner's own fixer back home, who brokered this meet and is NOT in this scen
|
|
|
389
423
|
// fixer" possible at all and why the objective below can be a `give`.
|
|
390
424
|
// The two halves have to agree about WHERE the file is, so this block
|
|
391
425
|
// says host-or-offline-server in the same vocabulary room.ts uses.
|
|
392
|
-
const dataRunBlock = shape !== 'data' ? '' : `
|
|
426
|
+
const dataRunBlock = shape !== 'data' || courier ? '' : `
|
|
393
427
|
THIS JOB IS A DATA RUN -- a quiet one. The runner is being paid to STEAL A FILE, not to
|
|
394
428
|
take ground. Build it accordingly:
|
|
395
429
|
- The winCondition item is PAYDATA: a "matrix"-plane item, category Document, sitting
|
|
@@ -405,9 +439,9 @@ take ground. Build it accordingly:
|
|
|
405
439
|
is a smash-and-grab wearing the wrong name. A site can still be WATCHED (cameras) --
|
|
406
440
|
surveillance is pressure without being a gun.
|
|
407
441
|
- Keep it SHORT and tight. One site, one host, one file.
|
|
408
|
-
- The delivery still matters: the file
|
|
409
|
-
|
|
410
|
-
|
|
442
|
+
- The delivery still matters: the file is copied out of the vault onto the runner's device,
|
|
443
|
+
and handed over from there (or burned to a chip first). A "give" objective is the natural
|
|
444
|
+
shape.
|
|
411
445
|
`;
|
|
412
446
|
// THE TWIST (SR5 p.478), epic only: rolled fresh per attempt, and
|
|
413
447
|
// written onto the skeleton by settleSecurity from the same order so
|
|
@@ -711,13 +745,17 @@ Hard structural rules (validated mechanically -- a violation is rejected):
|
|
|
711
745
|
? 'winCondition.target names a real npcs[].name that is NOT "client" and NOT winCondition.toNpc; winCondition.toNpc names a real npcs[].name in a different room; "willing" is a boolean; there is no items[] entry with role "winCondition".'
|
|
712
746
|
: kind === 'assassination' || kind === 'destruction'
|
|
713
747
|
? `winCondition.target names a real ${kind === 'assassination' ? 'npcs[].name that is NOT "client"' : 'devices[].name that holds no door and grants nothing'}; there is no items[] entry with role "winCondition".`
|
|
714
|
-
:
|
|
715
|
-
? 'The winCondition item is heldBy "client"; winCondition.
|
|
716
|
-
:
|
|
748
|
+
: courier
|
|
749
|
+
? 'The winCondition item is a "matrix"-plane file heldBy "client"; winCondition.target is a hasNode room that is NOT the client\'s room; there is no toNpc.'
|
|
750
|
+
: kind === 'delivery'
|
|
751
|
+
? 'The winCondition item is heldBy "client"; winCondition.toNpc is NOT the client and does not stand in the client\'s room.'
|
|
752
|
+
: 'The winCondition item must not be heldBy winCondition.toNpc.'} If a matrix-plane item is the
|
|
717
753
|
winCondition item, its room has hasNode true AND a "hostRating" of at least 4 -- the host
|
|
718
|
-
defends its own files with its own ice; never write an NPC for that. ${
|
|
719
|
-
? 'On THIS job
|
|
720
|
-
:
|
|
754
|
+
defends its own files with its own ice; never write an NPC for that. ${courier
|
|
755
|
+
? 'On THIS job the cargo is that file -- it starts in the client\'s hands, and the HOST it goes to is the drop.'
|
|
756
|
+
: shape === 'data'
|
|
757
|
+
? 'On THIS job (a data run) the winCondition item MUST be that matrix-plane item -- paydata lives on a host, never on a desk.'
|
|
758
|
+
: 'A Datachip/Document winCondition item may be either: a file on a host (then the rule above applies) or a physical chip in the meat world.'}
|
|
721
759
|
- ${isContinuation
|
|
722
760
|
? 'ZERO "starting"-role items -- the runner arrives with everything they own. 4-6'
|
|
723
761
|
: '3-5 "starting"-role items (basics the runner doesn\'t already own -- see the dossier), 4-6'}
|
|
@@ -842,6 +880,9 @@ function objectiveBrief(skeleton) {
|
|
|
842
880
|
if (skeleton.jobKind === 'extraction' && wc?.target) {
|
|
843
881
|
return `the objective (the PERSON: "${wc.target}" -- who they are, where they are held, ${wc.willing ? 'that they want out' : 'that they will not come quietly'}, and that they are to be brought to "${wc.toNpc}" alive)`;
|
|
844
882
|
}
|
|
883
|
+
if (skeleton.jobKind === 'delivery' && wc?.item && wc.target) {
|
|
884
|
+
return `the objective (the CARGO FILE "${wc.item}", which they push onto the runner's device at the meet, and the DROP: the host of "${wc.target}", where it is to be uploaded -- and what the file is for)`;
|
|
885
|
+
}
|
|
845
886
|
if (skeleton.jobKind === 'delivery' && wc?.item) {
|
|
846
887
|
return `the objective (the CARGO "${wc.item}", which they hand over at the meet, and the DROP: "${wc.toNpc}" and where to find them)`;
|
|
847
888
|
}
|
|
@@ -877,11 +918,17 @@ function paydataBriefingRule(skeleton) {
|
|
|
877
918
|
const win = skeleton.items.find(i => i.name === skeleton.winCondition.item);
|
|
878
919
|
if (win?.plane !== 'matrix')
|
|
879
920
|
return '';
|
|
921
|
+
if (skeleton.winCondition.target) {
|
|
922
|
+
return `
|
|
923
|
+
THE CARGO "${win.name}" IS A FILE -- the client pushes it onto the runner's commlink or deck at
|
|
924
|
+
the meet, over the table's PAN. Never describe handing over a physical chip, drive or case.
|
|
925
|
+
The drop is the HOST of "${skeleton.winCondition.target}": the runner gets inside and uploads it.`;
|
|
926
|
+
}
|
|
880
927
|
const host = win.room ? ` the host of "${win.room}"` : ' a host';
|
|
881
928
|
return `
|
|
882
929
|
THE OBJECTIVE "${win.name}" IS PAYDATA -- a FILE held on${host}, not an object in a room.
|
|
883
930
|
The client briefs it as data: a file to be found inside that host and copied out over the
|
|
884
|
-
Matrix
|
|
931
|
+
Matrix onto the runner's own device. Never describe it as a physical
|
|
885
932
|
chip, drive or disk sitting in a room, on a shelf, or in a safe -- nothing in the meat
|
|
886
933
|
world holds it until the runner downloads it.`;
|
|
887
934
|
}
|
|
@@ -1328,6 +1375,12 @@ export function validateSkeleton(skeleton, _player, _crew, order) {
|
|
|
1328
1375
|
if (prizes.length > 0)
|
|
1329
1376
|
fail(`${word} has no prize item, but ${prizes.map(i => `"${i.name}"`).join(', ')} carries role "winCondition" -- make it loot or drop it.`);
|
|
1330
1377
|
}
|
|
1378
|
+
else if (isDataCourier(jobKind, order?.runType)) {
|
|
1379
|
+
if (!skeleton.winCondition.item || !skeleton.winCondition.target)
|
|
1380
|
+
fail('winCondition needs "item" (the cargo file) and "target" (the drop: a hasNode room) -- this job is a data courier run.');
|
|
1381
|
+
if (skeleton.winCondition.toNpc)
|
|
1382
|
+
fail('winCondition on a data courier run carries no "toNpc" -- the drop is a host, named by "target". Drop "toNpc".');
|
|
1383
|
+
}
|
|
1331
1384
|
else if (!skeleton.winCondition.item || !skeleton.winCondition.toNpc) {
|
|
1332
1385
|
fail('winCondition needs "item" and "toNpc".');
|
|
1333
1386
|
}
|
|
@@ -1541,7 +1594,25 @@ export function validateSkeleton(skeleton, _player, _crew, order) {
|
|
|
1541
1594
|
if (!deviceNames.has(target))
|
|
1542
1595
|
fail(`winCondition.target "${target}" is not in devices -- the object to wreck has to be a device in this scene.`);
|
|
1543
1596
|
}
|
|
1544
|
-
if (jobKind
|
|
1597
|
+
if (isDataCourier(jobKind, order?.runType)) {
|
|
1598
|
+
const cargo = skeleton.items.find(i => i.name === skeleton.winCondition.item);
|
|
1599
|
+
if (cargo && cargo.heldBy !== skeleton.client)
|
|
1600
|
+
fail(`the cargo file "${cargo.name}" must be heldBy the client "${skeleton.client}" -- they push it onto the runner's device at the meet. It is ${cargo.heldBy ? `heldBy "${cargo.heldBy}"` : cargo.room ? `in "${cargo.room}"` : 'unplaced'}.`);
|
|
1601
|
+
if (cargo && cargo.plane !== 'matrix')
|
|
1602
|
+
fail(`the cargo "${cargo.name}" is on the meat plane, and this job is a data courier run -- give it "plane": "matrix", category Document.`);
|
|
1603
|
+
const dropName = skeleton.winCondition.target;
|
|
1604
|
+
const drop = skeleton.rooms.find(r => r.name === dropName);
|
|
1605
|
+
const meet = skeleton.npcs.find(n => n.name === skeleton.client);
|
|
1606
|
+
if (!drop)
|
|
1607
|
+
fail(`winCondition.target "${dropName}" is not one of the rooms -- the drop has to be a room whose host takes the upload.`);
|
|
1608
|
+
else if (!drop.hasNode)
|
|
1609
|
+
fail(`winCondition.target "${dropName}" has no "hasNode": true -- the drop is a HOST. Mark that room hasNode.`);
|
|
1610
|
+
else if ((drop.hostRating ?? 0) < VAULT_HOST_RATING)
|
|
1611
|
+
fail(`the drop host "${dropName}" has hostRating ${drop.hostRating ?? 'unset'} -- it defends itself with its own ice, so give it a hostRating of ${VAULT_HOST_RATING} or more.`);
|
|
1612
|
+
if (drop && meet && meet.startLocation === drop.name)
|
|
1613
|
+
fail(`the drop "${dropName}" is the client's own room -- the transit is the job. Put the drop host somewhere the runner has to get into.`);
|
|
1614
|
+
}
|
|
1615
|
+
else if (jobKind === 'delivery') {
|
|
1545
1616
|
const cargo = skeleton.items.find(i => i.name === skeleton.winCondition.item);
|
|
1546
1617
|
if (cargo && cargo.heldBy !== skeleton.client)
|
|
1547
1618
|
fail(`the cargo "${cargo.name}" must be heldBy the client "${skeleton.client}" -- they hand it over at the meet. It is ${cargo.heldBy ? `heldBy "${cargo.heldBy}"` : cargo.room ? `in "${cargo.room}"` : 'unplaced'}.`);
|
|
@@ -1567,7 +1638,7 @@ export function validateSkeleton(skeleton, _player, _crew, order) {
|
|
|
1567
1638
|
// cannot add a hasNode flag. Worded to route to 'skeleton' (no
|
|
1568
1639
|
// "items[", no "Win condition item").
|
|
1569
1640
|
const winItem = skeleton.items.find(i => i.name === skeleton.winCondition.item);
|
|
1570
|
-
if (winItem && order?.runType === 'data' && winItem.plane !== 'matrix') {
|
|
1641
|
+
if (winItem && order?.runType === 'data' && winItem.plane !== 'matrix' && !isDataCourier(jobKind, order?.runType)) {
|
|
1571
1642
|
fail(`the winCondition item "${winItem.name}" is on the meat plane, and this job is a DATA RUN -- its prize is PAYDATA: give it "plane": "matrix", category Document, placed in a hasNode room (not heldBy anyone) with a hostRating of ${VAULT_HOST_RATING} or more. A chip lying on a desk is not a data run.`);
|
|
1572
1643
|
}
|
|
1573
1644
|
// The other half of the same rule, for ANY matrix-plane objective: the
|
|
@@ -1580,7 +1651,7 @@ export function validateSkeleton(skeleton, _player, _crew, order) {
|
|
|
1580
1651
|
// which is how four programs ended up bargaining over a chip in Vex's
|
|
1581
1652
|
// log. Canon has none of that: the host launches its own IC at its
|
|
1582
1653
|
// rating (p.247), so what a vault needs is a rating worth the name.
|
|
1583
|
-
if (winItem?.plane === 'matrix') {
|
|
1654
|
+
if (winItem?.plane === 'matrix' && !isDataCourier(jobKind, order?.runType)) {
|
|
1584
1655
|
if (!winItem.room) {
|
|
1585
1656
|
fail(`the winCondition item "${winItem.name}" is matrix-plane paydata but has no "room" -- data lives on a host, never in a pocket. Place it in a hasNode room.`);
|
|
1586
1657
|
}
|
|
@@ -1626,7 +1697,10 @@ export function validateSkeleton(skeleton, _player, _crew, order) {
|
|
|
1626
1697
|
fail(`room "${room.name}" has "hasNode": true but no "hostPurpose" -- every host is FOR something (what this place runs on it, and so what it protects). Give it a hostPurpose phrase, or drop hasNode if the place would run nothing worth taking.`);
|
|
1627
1698
|
}
|
|
1628
1699
|
const holds = skeleton.items.some(i => i.plane === 'matrix' && i.room === room.name);
|
|
1629
|
-
|
|
1700
|
+
// A DATA COURIER'S DROP is empty until the runner fills it -- the file
|
|
1701
|
+
// coming in is what it is for (normalizeSkeleton keeps it too).
|
|
1702
|
+
const courierDrop = isDataCourier(jobKind, order?.runType) && skeleton.winCondition.target === room.name;
|
|
1703
|
+
if (!holds && !courierDrop) {
|
|
1630
1704
|
fail(`room "${room.name}" is a host (${room.hostPurpose}) with NOTHING inside it -- no "matrix"-plane item placed in that room. A host holds what its business protects: put at least one matrix-plane item there (the ledger, the client list, the camera archive), or drop hasNode.`);
|
|
1631
1705
|
}
|
|
1632
1706
|
// "Ice stationed on it" used to be the third requirement. It is
|
|
@@ -2037,6 +2111,10 @@ export function normalizeSkeleton(skeleton) {
|
|
|
2037
2111
|
const holds = (skeleton.items ?? []).some(i => i.plane === 'matrix' && i.room === room.name);
|
|
2038
2112
|
if (holds)
|
|
2039
2113
|
continue;
|
|
2114
|
+
// A DATA COURIER'S DROP is empty until the runner fills it (item +
|
|
2115
|
+
// target: win-condition 'upload'). Its purpose is the file coming in.
|
|
2116
|
+
if (skeleton.winCondition?.item && skeleton.winCondition?.target === room.name)
|
|
2117
|
+
continue;
|
|
2040
2118
|
delete room.hasNode;
|
|
2041
2119
|
delete room.hostRating;
|
|
2042
2120
|
delete room.hostPurpose;
|
|
@@ -2057,8 +2135,11 @@ export function normalizeSkeleton(skeleton) {
|
|
|
2057
2135
|
}
|
|
2058
2136
|
}
|
|
2059
2137
|
const winItem = (skeleton.items ?? []).find(i => i.name === skeleton.winCondition?.item);
|
|
2060
|
-
|
|
2061
|
-
|
|
2138
|
+
// The vault is where the paydata sits -- or, on a data courier run, the
|
|
2139
|
+
// host the cargo is going to (its target).
|
|
2140
|
+
const vaultRoom = winItem?.plane === 'matrix' ? (skeleton.winCondition?.target ?? winItem.room) : undefined;
|
|
2141
|
+
if (vaultRoom) {
|
|
2142
|
+
const vault = (skeleton.rooms ?? []).find(r => r.name === vaultRoom);
|
|
2062
2143
|
if (vault?.hasNode && (vault.hostRating ?? 0) < VAULT_HOST_RATING) {
|
|
2063
2144
|
const was = vault.hostRating === undefined ? 'no hostRating' : `hostRating ${vault.hostRating}`;
|
|
2064
2145
|
vault.hostRating = VAULT_HOST_RATING;
|
|
@@ -3133,23 +3214,35 @@ export function assembleScene(skeleton, details, playerName) {
|
|
|
3133
3214
|
lifestyleChoices: [{ type: 'residence', metadata: { tier: 'Street', location: startRoom } }],
|
|
3134
3215
|
startingItems,
|
|
3135
3216
|
},
|
|
3136
|
-
winCondition: skeleton.winCondition.target
|
|
3217
|
+
winCondition: skeleton.jobKind === 'delivery' && skeleton.winCondition.item && skeleton.winCondition.target
|
|
3218
|
+
// THE DATA COURIER (win-condition 'upload'): a file to a host. The
|
|
3219
|
+
// engine -- never the model -- decides whether it must land unseen.
|
|
3137
3220
|
? {
|
|
3138
|
-
type:
|
|
3221
|
+
type: 'upload',
|
|
3222
|
+
item: skeleton.winCondition.item,
|
|
3139
3223
|
target: skeleton.winCondition.target,
|
|
3140
|
-
...(skeleton.
|
|
3224
|
+
...(courierMustBeUnseen(skeleton.jobSize) ? { undetected: true } : {}),
|
|
3141
3225
|
rewardItem: skeleton.winCondition.rewardItem,
|
|
3142
3226
|
payout: skeleton.winCondition.payout,
|
|
3143
3227
|
completionMessage: skeleton.completionMessage,
|
|
3144
3228
|
}
|
|
3145
|
-
:
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3229
|
+
: skeleton.winCondition.target
|
|
3230
|
+
? {
|
|
3231
|
+
type: skeleton.jobKind === 'destruction' ? 'destroy' : skeleton.jobKind === 'extraction' ? 'extract' : 'kill',
|
|
3232
|
+
target: skeleton.winCondition.target,
|
|
3233
|
+
...(skeleton.jobKind === 'extraction' ? { toNpc: skeleton.winCondition.toNpc, willing: skeleton.winCondition.willing } : {}),
|
|
3234
|
+
rewardItem: skeleton.winCondition.rewardItem,
|
|
3235
|
+
payout: skeleton.winCondition.payout,
|
|
3236
|
+
completionMessage: skeleton.completionMessage,
|
|
3237
|
+
}
|
|
3238
|
+
: {
|
|
3239
|
+
type: 'give',
|
|
3240
|
+
item: skeleton.winCondition.item,
|
|
3241
|
+
toNpc: skeleton.winCondition.toNpc,
|
|
3242
|
+
rewardItem: skeleton.winCondition.rewardItem,
|
|
3243
|
+
payout: skeleton.winCondition.payout,
|
|
3244
|
+
completionMessage: skeleton.completionMessage,
|
|
3245
|
+
},
|
|
3153
3246
|
client: skeleton.client,
|
|
3154
3247
|
...(skeleton.jobSize ? { jobSize: skeleton.jobSize } : {}),
|
|
3155
3248
|
...(skeleton.jobKind ? { jobKind: skeleton.jobKind } : {}),
|
|
@@ -662,6 +662,15 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
|
|
|
662
662
|
throw new Error(`Win condition references NPC "${wc.toNpc}", which is not in "npcs".`);
|
|
663
663
|
}
|
|
664
664
|
}
|
|
665
|
+
// THE COURIER'S DROP IS A HOST (win-condition 'upload'): the
|
|
666
|
+
// target names a room, and that room must stand under one.
|
|
667
|
+
if (wc.type === 'upload') {
|
|
668
|
+
const drop = wc.target ? scene.getRooms()[wc.target] ?? Object.values(scene.getRooms()).find(r => r.name === wc.target) : undefined;
|
|
669
|
+
if (!drop)
|
|
670
|
+
throw new Error(`Win condition type "upload" requires "target", a room -- "${wc.target}" is not in "rooms".`);
|
|
671
|
+
if (!drop.host || drop.offlineServer)
|
|
672
|
+
throw new Error(`Win condition type "upload" targets "${wc.target}", which has no host to upload to -- mark it hasNode.`);
|
|
673
|
+
}
|
|
665
674
|
}
|
|
666
675
|
let rewardItem;
|
|
667
676
|
if (wc.rewardItem) {
|
|
@@ -676,6 +685,7 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
|
|
|
676
685
|
toNpc: wc.toNpc,
|
|
677
686
|
target: wc.target,
|
|
678
687
|
willing: wc.willing,
|
|
688
|
+
undetected: wc.undetected,
|
|
679
689
|
rewardItem,
|
|
680
690
|
completionMessage: wc.completionMessage,
|
|
681
691
|
payout: wc.payout
|
|
@@ -914,6 +914,11 @@ export class SceneSeedGenerator {
|
|
|
914
914
|
const wc = candidate.winCondition;
|
|
915
915
|
if (!wc || !wc.item)
|
|
916
916
|
return;
|
|
917
|
+
// A COURIER'S CARGO (win-condition 'upload') is a file the client
|
|
918
|
+
// HOLDS -- it starts in their hands, not on a host; the drop host is
|
|
919
|
+
// validated at the skeleton (scene-chunks.ts validateSkeleton).
|
|
920
|
+
if (wc.type === 'upload')
|
|
921
|
+
return;
|
|
917
922
|
const item = (candidate.items ?? []).find(i => i.name === wc.item);
|
|
918
923
|
if (!item || item.plane !== 'matrix')
|
|
919
924
|
return;
|
|
@@ -78,6 +78,9 @@ import { TakeCommand } from './commands/take.js';
|
|
|
78
78
|
import { MessageCommand } from './commands/message.js';
|
|
79
79
|
import { MessagesCommand } from './commands/messages.js';
|
|
80
80
|
import { DownloadCommand } from './commands/download.js';
|
|
81
|
+
import { UploadCommand } from './commands/upload.js';
|
|
82
|
+
import { TransferCommand } from './commands/transfer.js';
|
|
83
|
+
import { receiveData } from './utilities/data-library.js';
|
|
81
84
|
import { LootCommand } from './commands/loot.js';
|
|
82
85
|
import { NoteCommand } from './commands/note.js';
|
|
83
86
|
import { BacklogCommand } from './commands/backlog.js';
|
|
@@ -4513,6 +4516,25 @@ export default class Game {
|
|
|
4513
4516
|
Logger.getInstance().write(`Delivery: ${client.name} handed "${wc.item}" to ${this.player.name} at the meet.`);
|
|
4514
4517
|
}
|
|
4515
4518
|
}
|
|
4519
|
+
// THE DATA COURIER'S CARGO (win-condition 'upload', user ruling
|
|
4520
|
+
// 2026-10-04): the package is a file, so the Johnson does not slide
|
|
4521
|
+
// anything across the table -- they push it onto the runner's
|
|
4522
|
+
// device (data-library receiveData; a chip only for a runner who
|
|
4523
|
+
// carries no device at all). The drop is a HOST, and the meet says
|
|
4524
|
+
// which, on its own line for the same terse() reason as above.
|
|
4525
|
+
if (wc?.type === 'upload' && wc.item && johnson && johnson.inventory.hasItem(wc.item)) {
|
|
4526
|
+
const cargo = johnson.inventory.getItem(wc.item);
|
|
4527
|
+
johnson.inventory.removeItem(cargo.name);
|
|
4528
|
+
const { device } = receiveData(this.player, cargo);
|
|
4529
|
+
this.scene.updateInventory(this.player.inventory);
|
|
4530
|
+
lines.push(device
|
|
4531
|
+
? `THE CARGO: ${client.name} pushes ${cargo.name} onto your ${device.name}.`
|
|
4532
|
+
: `THE CARGO: ${client.name} hands you ${cargo.name} on a chip -- you carry nothing with a library to push it to.`);
|
|
4533
|
+
if (wc.target) {
|
|
4534
|
+
lines.push(`THE DROP: the host at ${wc.target}. Get inside it and "upload ${cargo.name}"${wc.undetected ? ` -- and get it in before anyone knows you're there. A plant they watch land is worth nothing` : ''}.`);
|
|
4535
|
+
}
|
|
4536
|
+
Logger.getInstance().write(`Data courier: ${client.name} pushed "${cargo.name}" to ${this.player.name}${device ? `'s ${device.name}` : ' (chip)'} at the meet.`);
|
|
4537
|
+
}
|
|
4516
4538
|
}
|
|
4517
4539
|
// THE PERSON HANDED OVER TO THE CLIENT (win-condition 'extract'):
|
|
4518
4540
|
// when the client is the delivery point, any face time with them
|
|
@@ -4979,6 +5001,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
4979
5001
|
CommandFactory.registerCommand('message', MessageCommand);
|
|
4980
5002
|
CommandFactory.registerCommand('messages', MessagesCommand);
|
|
4981
5003
|
CommandFactory.registerCommand('download', DownloadCommand);
|
|
5004
|
+
CommandFactory.registerCommand('upload', UploadCommand);
|
|
5005
|
+
CommandFactory.registerCommand('transfer', TransferCommand);
|
|
4982
5006
|
CommandFactory.registerCommand('loot', LootCommand);
|
|
4983
5007
|
CommandFactory.registerCommand('note', NoteCommand);
|
|
4984
5008
|
CommandFactory.registerCommand('backlog', BacklogCommand);
|
|
@@ -221,6 +221,32 @@ export class Host {
|
|
|
221
221
|
return [];
|
|
222
222
|
return this.over.inventory.getAllItems().filter(i => i.plane === 'matrix');
|
|
223
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* THE HOST'S DIGITAL INVENTORY (user ruling 2026-10-04), the write half
|
|
226
|
+
* of files(). Every write to a host's archive goes through these two --
|
|
227
|
+
* an upload lands here, an erase leaves from here -- so the storage
|
|
228
|
+
* underneath (the matrix-plane entries of the room it stands over, which
|
|
229
|
+
* the hub overlay already persists) is one place to change, not ten.
|
|
230
|
+
*/
|
|
231
|
+
addFile(file) {
|
|
232
|
+
file.plane = 'matrix';
|
|
233
|
+
file.atSpot = undefined;
|
|
234
|
+
this.over.inventory.addItem(file);
|
|
235
|
+
}
|
|
236
|
+
/** Takes a file out of the archive by name; true when one was there. */
|
|
237
|
+
removeFile(name) {
|
|
238
|
+
const lower = name.toLowerCase();
|
|
239
|
+
if (!this.files().some(f => f.name.toLowerCase() === lower))
|
|
240
|
+
return false;
|
|
241
|
+
this.over.inventory.removeItem(name);
|
|
242
|
+
this.over.plainSightItems.delete(lower);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
/** True when the archive holds a file of that name. */
|
|
246
|
+
hasFile(name) {
|
|
247
|
+
const lower = name.toLowerCase();
|
|
248
|
+
return this.files().some(f => f.name.toLowerCase() === lower);
|
|
249
|
+
}
|
|
224
250
|
/** The locked doors of the room it stands over -- what a crack releases. */
|
|
225
251
|
maglocks() {
|
|
226
252
|
return Array.from(this.over.exits.values())
|
|
@@ -971,6 +971,18 @@ export class Item extends AbstractItem {
|
|
|
971
971
|
* here; utilities/sin.ts reads them from here.
|
|
972
972
|
*/
|
|
973
973
|
papers = [];
|
|
974
|
+
/**
|
|
975
|
+
* THE DEVICE'S DATA LIBRARY (digital inventory, user ruling 2026-10-04).
|
|
976
|
+
* A file copied off a host (Edit File, SR5 p.239: "you are the new
|
|
977
|
+
* file's owner") lives on the device that carried the persona -- "all
|
|
978
|
+
* of it fits on your commlink (or cyberdeck if you prefer)", p.222-223
|
|
979
|
+
* -- not as a chip in the pack. Each entry stays plane 'matrix'. A
|
|
980
|
+
* commlink, a deck or an RCC holds one (holdsData); a file moves
|
|
981
|
+
* between them, onto a host (upload) or onto a datachip ("transfer
|
|
982
|
+
* <file> to chip") -- utilities/data-library.ts. Canon gives storage no
|
|
983
|
+
* practical cap (p.222), so neither does this.
|
|
984
|
+
*/
|
|
985
|
+
files = [];
|
|
974
986
|
/** On a license: the name of the fake SIN it was bought for. */
|
|
975
987
|
linkedSin;
|
|
976
988
|
/** On a license: the TYPE it permits, by `license` row slug (v24,
|
|
@@ -990,6 +1002,10 @@ export class Item extends AbstractItem {
|
|
|
990
1002
|
holdsPapers() {
|
|
991
1003
|
return this.category === Category.Commlink || this.isCyberdeck() || this.isRcc();
|
|
992
1004
|
}
|
|
1005
|
+
/** A device with a data library (files): the same three that hold papers. */
|
|
1006
|
+
holdsData() {
|
|
1007
|
+
return this.holdsPapers();
|
|
1008
|
+
}
|
|
993
1009
|
/** A data-soft chip: Mapsoft, Shopsoft, Datasoft (utilities/programs.ts DATA_SOFTS). */
|
|
994
1010
|
get softKey() {
|
|
995
1011
|
return this.category === Category.Software ? findSoft(this.name)?.key : undefined;
|
|
@@ -1539,6 +1539,43 @@ export class Scene extends AbstractScene {
|
|
|
1539
1539
|
this.logger.logWithColor(banner, 'green');
|
|
1540
1540
|
return banner;
|
|
1541
1541
|
}
|
|
1542
|
+
/**
|
|
1543
|
+
* THE COURIER'S PLANT (win-condition 'upload', user ruling 2026-10-04):
|
|
1544
|
+
* met when the named file is written onto the named room's host by
|
|
1545
|
+
* Edit File's create (commands/upload.ts). A delivery whose package is
|
|
1546
|
+
* data (p.338): nothing changes hands in person, so this is a state
|
|
1547
|
+
* check on the host, like erase -- but it is a CLIENT job and pays like
|
|
1548
|
+
* one: completeObjective leaves the money with the Johnson until the
|
|
1549
|
+
* runner reports in (Game.onClientInteraction).
|
|
1550
|
+
*
|
|
1551
|
+
* `undetected` (the harder jobs): a host on alert has its spider and
|
|
1552
|
+
* its Patrol looking (host.ts `alert`, which canon never stands down),
|
|
1553
|
+
* and a file that lands under those eyes is found. The upload still
|
|
1554
|
+
* happens -- the runner made the edit -- but the plant is blown and the
|
|
1555
|
+
* objective is not met; the line returned says so. Returns null when
|
|
1556
|
+
* this is not the objective, it is already met, or the file or host do
|
|
1557
|
+
* not match.
|
|
1558
|
+
*/
|
|
1559
|
+
checkUploaded(itemName, roomName, byName, hostAlert) {
|
|
1560
|
+
const condition = this._winCondition;
|
|
1561
|
+
if (!condition || this._completed || condition.type !== 'upload')
|
|
1562
|
+
return null;
|
|
1563
|
+
if (!condition.item || condition.item.toLowerCase() !== itemName.toLowerCase())
|
|
1564
|
+
return null;
|
|
1565
|
+
if (!condition.target || condition.target.toLowerCase() !== roomName.toLowerCase())
|
|
1566
|
+
return null;
|
|
1567
|
+
if (condition.undetected && hostAlert) {
|
|
1568
|
+
this.addWorldEvent(`${byName} planted ${itemName} on an alerted host -- the spider saw it land.`, { plane: 'matrix' });
|
|
1569
|
+
this.logger.write(`Scene upload objective blown: "${itemName}" landed on ${roomName}'s host while it was on alert.`);
|
|
1570
|
+
const blown = `The file lands -- and the host is awake. Its spider watches ${itemName} arrive in real time, and a plant somebody saw land is no plant at all. The job wanted it in clean.`;
|
|
1571
|
+
this.logger.logWithColor(blown, 'yellow');
|
|
1572
|
+
return blown;
|
|
1573
|
+
}
|
|
1574
|
+
const possessorPlayer = this.getPlayerByName(byName);
|
|
1575
|
+
this.addWorldEvent(`${byName} planted ${itemName} on ${roomName}'s host -- the job is done.`, { plane: 'matrix' });
|
|
1576
|
+
this.logger.write(`Scene upload objective met: "${byName}" uploaded "${itemName}" to ${roomName}'s host.`);
|
|
1577
|
+
return this.completeObjective(condition, possessorPlayer);
|
|
1578
|
+
}
|
|
1542
1579
|
/**
|
|
1543
1580
|
* THE ERASE OBJECTIVE (loose ends, 2026-09-14): met when the named file
|
|
1544
1581
|
* is DELETED by Edit File (commands/edit-file.ts) -- a state change on
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Category } from '../types/shared/item-enum.js';
|
|
2
|
+
import { serializeItem, restoreItem } from './persistence.js';
|
|
3
|
+
import { fuzzyPickName } from './fuzzy-match.js';
|
|
4
|
+
/**
|
|
5
|
+
* Every device on this runner with a data library: the housed device, the
|
|
6
|
+
* worn commlink, then every commlink / deck / RCC in the pack. A device
|
|
7
|
+
* appears once however many readers would have named it.
|
|
8
|
+
*/
|
|
9
|
+
export function dataDevices(player) {
|
|
10
|
+
const out = [];
|
|
11
|
+
const add = (d) => {
|
|
12
|
+
if (d && d.holdsData() && !out.includes(d))
|
|
13
|
+
out.push(d);
|
|
14
|
+
};
|
|
15
|
+
add(player.housedDeck());
|
|
16
|
+
add(player.equipment?.head?.commlink ?? undefined);
|
|
17
|
+
for (const i of player.inventory.getAllItems())
|
|
18
|
+
add(i);
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* WHERE A DOWNLOAD LANDS: the device carrying the persona (p.239, the
|
|
23
|
+
* copier owns the copy -- on the hardware they are running), else the
|
|
24
|
+
* first data device on you. Undefined for a runner with none at all -- a
|
|
25
|
+
* technomancer's living persona has no storage to write to (p.250-251),
|
|
26
|
+
* and download.ts refuses the copy rather than inventing a chip.
|
|
27
|
+
*/
|
|
28
|
+
export function libraryDevice(player) {
|
|
29
|
+
const persona = player.personaDevice();
|
|
30
|
+
if (persona && persona.holdsData() && dataDevices(player).includes(persona))
|
|
31
|
+
return persona;
|
|
32
|
+
return dataDevices(player)[0];
|
|
33
|
+
}
|
|
34
|
+
/** Every file on every device, device by device. */
|
|
35
|
+
export function deviceFiles(player) {
|
|
36
|
+
return dataDevices(player).flatMap(device => device.files.map(file => ({ file, device })));
|
|
37
|
+
}
|
|
38
|
+
/** Data chips in the pack: a file written to physical media. */
|
|
39
|
+
export function dataChips(player) {
|
|
40
|
+
return player.inventory.getAllItems().filter(i => i.category === Category.Datachip);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A file this runner holds BY EXACT NAME, wherever it sits: a device
|
|
44
|
+
* library first, then the pack (a chip, or any carried item -- the
|
|
45
|
+
* historical shape a delivery looked for). The objective readers ask this.
|
|
46
|
+
*/
|
|
47
|
+
export function heldByName(player, name) {
|
|
48
|
+
const lower = name.toLowerCase();
|
|
49
|
+
const onDevice = deviceFiles(player).find(h => h.file.name.toLowerCase() === lower);
|
|
50
|
+
if (onDevice)
|
|
51
|
+
return onDevice;
|
|
52
|
+
const carried = player.inventory.getItem(name);
|
|
53
|
+
return carried ? { file: carried } : undefined;
|
|
54
|
+
}
|
|
55
|
+
/** A device file by a player's fuzzy query. */
|
|
56
|
+
export function findDeviceFile(player, query) {
|
|
57
|
+
const all = deviceFiles(player);
|
|
58
|
+
const picked = fuzzyPickName(query, all.map(h => h.file.name));
|
|
59
|
+
return picked ? all.find(h => h.file.name === picked) : undefined;
|
|
60
|
+
}
|
|
61
|
+
/** A device on this runner by a player's fuzzy query ("deck", "commlink", the full name). */
|
|
62
|
+
export function findDataDevice(player, query) {
|
|
63
|
+
const devices = dataDevices(player);
|
|
64
|
+
const q = query.toLowerCase().trim();
|
|
65
|
+
if (!q)
|
|
66
|
+
return undefined;
|
|
67
|
+
const exact = devices.find(d => d.name.toLowerCase() === q);
|
|
68
|
+
if (exact)
|
|
69
|
+
return exact;
|
|
70
|
+
const byKind = devices.find(d => (q === 'deck' || q === 'cyberdeck') ? d.isCyberdeck()
|
|
71
|
+
: q === 'rcc' ? d.isRcc()
|
|
72
|
+
: (q === 'commlink' || q === 'comm' || q === 'link') ? d.category === Category.Commlink : false);
|
|
73
|
+
if (byKind)
|
|
74
|
+
return byKind;
|
|
75
|
+
const picked = fuzzyPickName(query, devices.map(d => d.name));
|
|
76
|
+
return picked ? devices.find(d => d.name === picked) : undefined;
|
|
77
|
+
}
|
|
78
|
+
/** True when the device already holds a file of that name. */
|
|
79
|
+
export function deviceHas(device, name) {
|
|
80
|
+
const lower = name.toLowerCase();
|
|
81
|
+
return device.files.some(f => f.name.toLowerCase() === lower);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A FRESH COPY of a file (Edit File's copy, p.239): same name, same
|
|
85
|
+
* content, owned by whoever made it. The original is untouched.
|
|
86
|
+
*/
|
|
87
|
+
export function copyFile(file, owner) {
|
|
88
|
+
const copy = restoreItem(serializeItem(file));
|
|
89
|
+
copy.plane = 'matrix';
|
|
90
|
+
copy.atSpot = undefined;
|
|
91
|
+
if (owner !== undefined)
|
|
92
|
+
copy.owner = owner;
|
|
93
|
+
return copy;
|
|
94
|
+
}
|
|
95
|
+
/** Writes a file into a device's library (plane 'matrix', no spot). */
|
|
96
|
+
export function fileOnto(device, file) {
|
|
97
|
+
file.plane = 'matrix';
|
|
98
|
+
file.atSpot = undefined;
|
|
99
|
+
device.files.push(file);
|
|
100
|
+
}
|
|
101
|
+
/** Takes a file out of whatever shelf holds it; true when it moved. */
|
|
102
|
+
export function releaseHeld(player, held) {
|
|
103
|
+
if (held.device) {
|
|
104
|
+
const at = held.device.files.indexOf(held.file);
|
|
105
|
+
if (at < 0)
|
|
106
|
+
return false;
|
|
107
|
+
held.device.files.splice(at, 1);
|
|
108
|
+
player.noteConditionChanged();
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return player.removeInventory(held.file);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Burns a device file onto a datachip in the pack -- a MOVE, the file
|
|
115
|
+
* leaves the library (user ruling: physical media is one move, not two).
|
|
116
|
+
* Throws when the pack cannot take the weight, before anything moves.
|
|
117
|
+
*/
|
|
118
|
+
export function burnToChip(player, held) {
|
|
119
|
+
const chip = held.file;
|
|
120
|
+
if (player.currentWeight + chip.weight * chip.quantity > player.maxCarryingWeight) {
|
|
121
|
+
throw new Error('too heavy');
|
|
122
|
+
}
|
|
123
|
+
releaseHeld(player, held);
|
|
124
|
+
chip.plane = 'meat';
|
|
125
|
+
chip.category = Category.Datachip;
|
|
126
|
+
player.inventory.addItem(chip);
|
|
127
|
+
player.noteConditionChanged();
|
|
128
|
+
return chip;
|
|
129
|
+
}
|
|
130
|
+
/** Reads a chip from the pack onto a device -- a MOVE, the chip is consumed. */
|
|
131
|
+
export function loadChip(player, chip, device) {
|
|
132
|
+
player.removeInventory(chip);
|
|
133
|
+
fileOnto(device, chip);
|
|
134
|
+
player.noteConditionChanged();
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* WHERE DATA HANDED TO A RUNNER LANDS: a device library when they carry
|
|
138
|
+
* one, else -- a runner with no commlink, no deck, no RCC -- the pack, as
|
|
139
|
+
* a chip. The second arm is not a loophole: something has to hold the
|
|
140
|
+
* cargo, and physical media is the only thing a deviceless runner owns.
|
|
141
|
+
*/
|
|
142
|
+
export function receiveData(player, file) {
|
|
143
|
+
const device = libraryDevice(player);
|
|
144
|
+
if (device) {
|
|
145
|
+
fileOnto(device, file);
|
|
146
|
+
player.noteConditionChanged();
|
|
147
|
+
return { device };
|
|
148
|
+
}
|
|
149
|
+
file.plane = 'meat';
|
|
150
|
+
file.category = Category.Datachip;
|
|
151
|
+
player.inventory.addItem(file);
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
/** Every file name this runner holds, devices and chips -- for the runner ops. */
|
|
155
|
+
export function heldDataNames(player) {
|
|
156
|
+
return deviceFiles(player).map(h => h.file.name);
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=data-library.js.map
|