@maka/maka-cli 5.378.0 → 5.379.0
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 +248 -132
- 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 +14 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.379.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
|
|
6
6
|
"description": "A command line tool for scaffolding Meteor 3.x applications using React.",
|
|
@@ -19,6 +19,7 @@ import { inLiveCombat } from '../utilities/action-cost.js';
|
|
|
19
19
|
import { radioIsDark } from '../utilities/pan-devices.js';
|
|
20
20
|
import { NPC } from '../models/npc.js';
|
|
21
21
|
import { METERS_PER_LEVEL } from '../utilities/room-grid.js';
|
|
22
|
+
import { heldByName, releaseHeld, receiveData } from '../utilities/data-library.js';
|
|
22
23
|
export class CallCommand extends Command {
|
|
23
24
|
static verb = 'call';
|
|
24
25
|
static description = 'Call someone remotely over a commlink (no need to be in the same room, or the same district; commlink must be equipped).';
|
|
@@ -612,9 +613,21 @@ export class CallCommand extends Command {
|
|
|
612
613
|
let deliveredItemName = '';
|
|
613
614
|
const wc = this.scene.getWinCondition();
|
|
614
615
|
if (wc?.type === 'give' && wc.item && wc.toNpc?.toLowerCase() === target.name.toLowerCase()) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
616
|
+
// A chip in the pack OR a file on a device (digital inventory,
|
|
617
|
+
// 2026-10-04): over a call the data is simply SENT, which is what a
|
|
618
|
+
// device library is for -- the chip was never required.
|
|
619
|
+
const held = heldByName(this.actor, wc.item);
|
|
620
|
+
const item = held?.file;
|
|
621
|
+
if (held && item) {
|
|
622
|
+
let delivered;
|
|
623
|
+
if (held.device) {
|
|
624
|
+
delivered = item.transferable !== false && releaseHeld(this.actor, held);
|
|
625
|
+
if (delivered)
|
|
626
|
+
receiveData(target, item);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
delivered = this.actor.inventory.transferItemTo(item.name, target.inventory);
|
|
630
|
+
}
|
|
618
631
|
if (delivered) {
|
|
619
632
|
this.scene.updateInventory(this.scene.getPlayer().inventory);
|
|
620
633
|
this.scene.checkWinCondition(item.name, target.name);
|
|
@@ -1,178 +1,294 @@
|
|
|
1
1
|
import { Command } from './command.js';
|
|
2
2
|
import { billAction } from '../utilities/action-cost.js';
|
|
3
|
-
import { roomsInReach, gridVicinity } from '../utilities/grid-reach.js';
|
|
3
|
+
import { roomsInReach, gridVicinity, hostDoorOpenFor } from '../utilities/grid-reach.js';
|
|
4
|
+
import { whereIsThat } from '../utilities/matrix-roster.js';
|
|
4
5
|
import { hint } from '../utilities/hints.js';
|
|
5
|
-
import { Category } from '../types/shared/item-enum.js';
|
|
6
6
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
7
7
|
import { fileReach, planeTintItemName } from '../utilities/planes.js';
|
|
8
|
+
import { hostLabel } from '../utilities/grid-names.js';
|
|
9
|
+
import { editFileBlocked, rollEditFile } from '../utilities/edit-file-test.js';
|
|
10
|
+
import { copyFile, deviceHas, fileOnto, findDataDevice, libraryDevice, receiveData } from '../utilities/data-library.js';
|
|
8
11
|
/**
|
|
9
|
-
* "download" --
|
|
10
|
-
* HAND OVER.
|
|
12
|
+
* "download" -- COPY A FILE OFF THE GRID INTO YOUR DEVICE'S LIBRARY.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
14
|
+
* DIGITAL INVENTORY (user ruling 2026-10-04) replaces the chip. A file
|
|
15
|
+
* used to come down as a Datachip in the pack (the 2026-08-30 ruling);
|
|
16
|
+
* now it is written to the library of the device carrying your persona
|
|
17
|
+
* (utilities/data-library.ts), and it is a COPY -- Edit File, p.239:
|
|
18
|
+
* "create, change, COPY, delete ... If you use this action to copy a
|
|
19
|
+
* file, you are the new file's owner." The host keeps its original, so a
|
|
20
|
+
* job that wants the file GONE is still "erase".
|
|
13
21
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
22
|
+
* WHERE FROM. Files inside the host you stand in; files loose on the grid
|
|
23
|
+
* where you are; dim icons out in the district (pp.217-218); and files
|
|
24
|
+
* your own compiled sprites are holding -- the one source nothing else
|
|
25
|
+
* could reach (aNeh5TELvdgeS2vpg, MxiKYFGeStiyvibJN). A sprite's file is
|
|
26
|
+
* already yours, so it MOVES to your device rather than copying.
|
|
17
27
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
28
|
+
* THE TEST (p.239): Computer + Logic [Data Processing] v. the host,
|
|
29
|
+
* Marks Required 1 -- the same roll erase makes (utilities/edit-file-test.ts).
|
|
30
|
+
* Two arms stay free of dice, both deliberate:
|
|
31
|
+
* - a CRACKED host (three marks, hack.ts): its architecture treats you
|
|
32
|
+
* as staff, and its own files open with it -- the line hack.ts draws;
|
|
33
|
+
* - a file not on any host (loose on the grid, or your sprite's): the
|
|
34
|
+
* defender would be the file's owner, and an unowned or self-owned
|
|
35
|
+
* file has nobody to roll. Canon names no defender here; none is
|
|
36
|
+
* invented.
|
|
37
|
+
* Before 2026-10-04 this verb never rolled and never asked for a crack,
|
|
38
|
+
* so it read an uncracked host's archive that `take` refused -- the two
|
|
39
|
+
* now share one path (take routes matrix-plane pickups here).
|
|
20
40
|
*
|
|
21
|
-
* THE
|
|
22
|
-
* matrix-plane file to `meat` and re-categorised it as a Datachip for a
|
|
23
|
-
* while now -- "pulled down ... written to a chip". So the mechanic was
|
|
24
|
-
* built; what was missing was a VERB anybody would think to type, and a
|
|
25
|
-
* source other than the floor.
|
|
41
|
+
* THE TIME: a Complex Action (p.239) wherever an action economy exists.
|
|
26
42
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* the meat plane, and a sprite is `plane: 'matrix'` by construction, so
|
|
31
|
-
* "order sprite give me the chip" could never work. The only route left
|
|
32
|
-
* was `decompile` -- destroying your own sprite to make it drop the
|
|
33
|
-
* loot -- which is exactly the "hidden, non-obvious solution" the vetting
|
|
34
|
-
* described. It is also a lousy trade the player has no way to know they
|
|
35
|
-
* are making.
|
|
36
|
-
*
|
|
37
|
-
* SO THIS VERB READS BOTH SOURCES: files loose on the grid where you are
|
|
38
|
-
* standing, and files carried by YOUR OWN compiled sprites. Somebody
|
|
39
|
-
* else's sprite is not a shelf you may take from -- that is a hack, and
|
|
40
|
-
* hack.ts owns it.
|
|
41
|
-
*
|
|
42
|
-
* DELIBERATELY NOT A TEST -- BUT IT IS AN ACTION. Pulling a file you
|
|
43
|
-
* already have reach on is not an opposed action in this engine --
|
|
44
|
-
* `take` does not roll for it either, and getting past whatever GUARDED
|
|
45
|
-
* the file is what the host crack and the mark already charged for.
|
|
46
|
-
* Adding dice here would price the same obstacle twice.
|
|
47
|
-
*
|
|
48
|
-
* The TIME is a separate bill, and it was going unpaid: copying a file
|
|
49
|
-
* is Edit File, a COMPLEX ACTION (p.239). See the billAction call in
|
|
50
|
-
* execute() for what that was costing in a fight.
|
|
43
|
+
* "download <file> to <device>" picks the library; without it the file
|
|
44
|
+
* lands on the persona's device. A runner with no data device at all
|
|
45
|
+
* gets the old chip, because something has to hold it.
|
|
51
46
|
*/
|
|
52
47
|
export class DownloadCommand extends Command {
|
|
53
48
|
static verb = 'download';
|
|
54
|
-
static description = '
|
|
49
|
+
static description = 'Copy a file off the Matrix into your device\'s library -- "download <file>" (or "download <file> to <device>"). Edit File (SR5 p.239, a Complex Action): Computer + Logic [Data Processing] v. the host, with a mark on it -- no roll once the host is cracked, or for data loose on the grid or held by your own sprites. The host keeps its original. "inv" lists your libraries; "transfer" moves files between devices or onto a chip.';
|
|
55
50
|
async execute(args = []) {
|
|
51
|
+
const actor = this.actor;
|
|
52
|
+
let words = args.join(' ').trim();
|
|
53
|
+
// "... to <device>": split on the LAST " to ", and only when the tail
|
|
54
|
+
// names a data device you carry -- a file called "Letter to Krow"
|
|
55
|
+
// still downloads whole.
|
|
56
|
+
let target;
|
|
57
|
+
const toMatch = words.match(/^(.*)\s+(?:to|onto|into)\s+(.+)$/i);
|
|
58
|
+
if (toMatch) {
|
|
59
|
+
const device = findDataDevice(actor, toMatch[2]);
|
|
60
|
+
if (device) {
|
|
61
|
+
target = device;
|
|
62
|
+
words = toMatch[1].trim();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const { pullable, reach } = this.inReach();
|
|
66
|
+
if (pullable.length === 0 && reach === 'connect') {
|
|
67
|
+
const room = gridVicinity(actor);
|
|
68
|
+
return `Those files are on the ${room.offlineServer} -- off the network entirely. No persona reaches that box; your BODY has to be in ${room.name} and cabled in (p.233).`;
|
|
69
|
+
}
|
|
70
|
+
if (pullable.length === 0 && !words) {
|
|
71
|
+
return `Nothing to pull down -- no data on the grid here, and nothing your sprites are carrying.${reach === 'enter' ? hint(` (This room's files live INSIDE its host -- "enter" first.)`) : ''}`;
|
|
72
|
+
}
|
|
73
|
+
// Bare "download" is the LIST. A verb whose whole reason for existing
|
|
74
|
+
// is that nobody could find the file should say what it can see.
|
|
75
|
+
if (words.length === 0) {
|
|
76
|
+
const lines = [`{light-blue-fg}Within reach to pull down:{/light-blue-fg}`];
|
|
77
|
+
for (const p of pullable) {
|
|
78
|
+
const where = p.kind === 'host' ? `in ${hostLabel(p.host.over)}'s archive`
|
|
79
|
+
: p.kind === 'sprite' ? `held by ${p.npc.name}`
|
|
80
|
+
: p.kind === 'far' ? `a dim icon out on the grid (noise -${p.noise})`
|
|
81
|
+
: 'loose on the grid here';
|
|
82
|
+
lines.push(` ${planeTintItemName(p.item, actor.plane)} -- ${where}`);
|
|
83
|
+
}
|
|
84
|
+
const home = libraryDevice(actor);
|
|
85
|
+
lines.push(hint(`("download <file>" copies it ${home ? `onto your ${home.name}` : 'to a chip'}.)`));
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
88
|
+
const picked = fuzzyPickName(words, pullable.map(p => p.item.name));
|
|
89
|
+
const source = picked ? pullable.find(p => p.item.name === picked) : undefined;
|
|
90
|
+
if (!source) {
|
|
91
|
+
// IN THE ARCHIVE, NOT YET FOUND: the host holds it, but nothing has
|
|
92
|
+
// turned the archive up for this persona (search, or a crack).
|
|
93
|
+
// OUT ON THE GRID, OUTSIDE THE HOST THAT HOLDS IT (p.246): the shell
|
|
94
|
+
// is all a persona reads from out here.
|
|
95
|
+
const here = actor.plane === 'matrix' ? gridVicinity(actor) : actor.currentLocation;
|
|
96
|
+
if (reach === 'enter' && here.host && fuzzyPickName(words, here.host.files().map(f => f.name))) {
|
|
97
|
+
return `That file is INSIDE the host -- you're on the outside grid, reading its shell (p.246). ${hostDoorOpenFor(here, actor) ? `"enter" first.` : `Get a mark on it and "enter" -- or "hack" your way in.`}`;
|
|
98
|
+
}
|
|
99
|
+
const inside = actor.plane === 'matrix' ? actor.hostInside : undefined;
|
|
100
|
+
if (inside && fuzzyPickName(words, inside.files().map(f => f.name))) {
|
|
101
|
+
return `You haven't found ${hostLabel(inside.over)}'s archive yet -- there is nothing on your display to copy. ${hint(`"search" runs a Matrix Search inside the host (p.241); a crack ("hack") opens it outright.`)}`;
|
|
102
|
+
}
|
|
103
|
+
const elsewhere = this.whereElse(words);
|
|
104
|
+
if (elsewhere)
|
|
105
|
+
return elsewhere;
|
|
106
|
+
return `No file called "${words}" within reach.${hint(` ("download" on its own lists what is.)`)}`;
|
|
107
|
+
}
|
|
108
|
+
const out = this.pull(source, target);
|
|
109
|
+
return out.text;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A FILE ELSEWHERE IN THE DISTRICT (matrix model): named from the grid,
|
|
113
|
+
* the answer is where it is and what stands in the way -- the offline
|
|
114
|
+
* server that needs your body, the host that needs a mark. Moved here
|
|
115
|
+
* from take.ts with the rest of the matrix pickup.
|
|
116
|
+
*/
|
|
117
|
+
whereElse(query) {
|
|
118
|
+
const actor = this.actor;
|
|
119
|
+
if (actor.plane !== 'matrix' || actor.insideHost)
|
|
120
|
+
return undefined;
|
|
121
|
+
const room = gridVicinity(actor);
|
|
122
|
+
const far = whereIsThat(this.rooms ?? this.scene.getRooms(), actor, query);
|
|
123
|
+
if (!far || far.room === room || !(far.reach === 'open' || far.remembered))
|
|
124
|
+
return undefined;
|
|
125
|
+
if (far.reach === 'offline')
|
|
126
|
+
return `${far.item.name} is on the ${far.room.offlineServer} in ${far.room.name} -- an offline server, off the network entirely. No persona reaches that box; your BODY has to be in ${far.room.name} (p.233).`;
|
|
127
|
+
if (far.reach === 'host')
|
|
128
|
+
return `${far.item.name} is INSIDE ${far.room.name}'s host, and you're out on the grid looking at its shell (p.246). ${hostDoorOpenFor(far.room, actor) ? `"enter ${far.room.name}" first.` : `Get a mark on it and "enter" -- or "hack" your way in.`}`;
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Every file this persona can pull from where it stands, with the
|
|
133
|
+
* reason the room's own files are or are not in reach.
|
|
134
|
+
*/
|
|
135
|
+
inReach() {
|
|
56
136
|
const actor = this.actor;
|
|
57
137
|
// WHERE THE PERSONA STANDS (matrix model 2026-09-02): inside a host, the host's room; on the grid, the body's.
|
|
58
138
|
const room = actor.plane === 'matrix' ? gridVicinity(actor) : actor.currentLocation;
|
|
59
|
-
// WHAT IS IN REACH, from both sources.
|
|
60
|
-
//
|
|
61
|
-
// The room's own files honour fileReach (utilities/planes.ts) -- a
|
|
62
|
-
// host's contents are inside the host and an offline server wants a
|
|
63
|
-
// body in the room -- because a download that ignored the boundary
|
|
64
|
-
// would be a way to read a sealed host from the street, which is the
|
|
65
|
-
// very thing eJANPpm7qCaCAJBJw was filed about.
|
|
66
139
|
const reach = fileReach(actor, room);
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
140
|
+
const pullable = [];
|
|
141
|
+
// The room's own files honour fileReach (utilities/planes.ts): a host's
|
|
142
|
+
// contents are inside the host, and an offline server wants a body.
|
|
143
|
+
if (reach === 'open') {
|
|
144
|
+
const host = actor.hostInside && actor.hostInside.over === room && !room.offlineServer ? actor.hostInside : undefined;
|
|
145
|
+
if (host) {
|
|
146
|
+
// YOU CANNOT COPY WHAT YOU HAVE NOT SEEN (edit-file.ts's rule):
|
|
147
|
+
// the archive is listed once a Matrix Search turned it up, or the
|
|
148
|
+
// host is cracked.
|
|
149
|
+
if (host.searchedBy.has(actor.name) || host.cracked) {
|
|
150
|
+
for (const item of host.files())
|
|
151
|
+
pullable.push({ item, kind: 'host', room, host });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
for (const item of room.inventory.getAllItems()) {
|
|
156
|
+
if (item.plane === 'matrix')
|
|
157
|
+
pullable.push({ item, kind: 'loose', room });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
70
161
|
// FARTHER OUT (SR5 pp.217-218): data loose on the grid elsewhere in the
|
|
71
162
|
// district is a dim icon in reach -- open where no host guards it.
|
|
72
|
-
const far = [];
|
|
73
163
|
if (actor.plane === 'matrix' && !actor.insideHost) {
|
|
74
164
|
for (const { room: r, noise } of roomsInReach(this.rooms ?? this.scene.getRooms(), actor).slice(1)) {
|
|
75
|
-
if (fileReach(actor, r) !== 'open')
|
|
165
|
+
if (fileReach(actor, r) !== 'open' || r.hasNode)
|
|
76
166
|
continue;
|
|
77
167
|
for (const item of r.inventory.getAllItems())
|
|
78
168
|
if (item.plane === 'matrix')
|
|
79
|
-
|
|
169
|
+
pullable.push({ item, kind: 'far', room: r, noise });
|
|
80
170
|
}
|
|
81
171
|
}
|
|
82
172
|
// YOUR sprites, not everyone's. `compiledBy` is not modelled on the
|
|
83
|
-
// shell, so ownership is "it is in YOUR companion list" --
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
const carried = [];
|
|
87
|
-
for (const entry of mine) {
|
|
173
|
+
// shell, so ownership is "it is in YOUR companion list" -- the same
|
|
174
|
+
// thing the order/decompile verbs already mean by it.
|
|
175
|
+
for (const entry of (this.game?.companions ?? []).filter(c => c.kind === 'sprite')) {
|
|
88
176
|
for (const item of entry.npc.inventory.getAllItems()) {
|
|
89
177
|
if (item.plane === 'matrix')
|
|
90
|
-
|
|
178
|
+
pullable.push({ item, kind: 'sprite', npc: entry.npc });
|
|
91
179
|
}
|
|
92
180
|
}
|
|
93
|
-
|
|
94
|
-
|
|
181
|
+
return { pullable, reach };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Pulls one file: the gates, the bill, the roll, the copy. `ok` is true
|
|
185
|
+
* only when a file actually landed -- take-all stops on a refusal that
|
|
186
|
+
* is about the ACTOR (out of actions), not the file.
|
|
187
|
+
*/
|
|
188
|
+
pull(source, target) {
|
|
189
|
+
const actor = this.actor;
|
|
190
|
+
const item = source.item;
|
|
191
|
+
// A LOOSE END IS ERASED, NOT DOWNLOADED: the desk keeps its copy
|
|
192
|
+
// whatever comes down to yours (win-condition-seed.ts 'erase').
|
|
193
|
+
const wc = this.scene.getWinCondition?.();
|
|
194
|
+
if (wc?.type === 'erase' && wc.item?.toLowerCase() === item.name.toLowerCase()) {
|
|
195
|
+
return { ok: false, text: `Copying ${item.name} changes nothing -- the desk keeps its own. ${hint(`"erase ${item.name}" is what makes it go away (Edit File, p.239).`)}` };
|
|
95
196
|
}
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
lines.push(` ${planeTintItemName(i, actor.plane)} -- loose on the grid here`);
|
|
103
|
-
for (const c of carried)
|
|
104
|
-
lines.push(` ${planeTintItemName(c.item, actor.plane)} -- held by ${c.npc.name}`);
|
|
105
|
-
for (const f of far)
|
|
106
|
-
lines.push(` ${planeTintItemName(f.item, actor.plane)} -- a dim icon out on the grid (noise -${f.noise})`);
|
|
107
|
-
lines.push(hint(`("download <file>" writes it to a chip you carry.)`));
|
|
108
|
-
return lines.join('\n');
|
|
197
|
+
// PROTECTED FILES (p.239: "if the file you want to copy has protection
|
|
198
|
+
// on it, this action automatically fails"). The one protection the
|
|
199
|
+
// engine models is a sprite's Hash (p.257) -- your own sprite's file
|
|
200
|
+
// is yours to move.
|
|
201
|
+
if (item.hashedBy && source.kind !== 'sprite') {
|
|
202
|
+
return { ok: false, text: `${item.name} is wrapped in a Resonance algorithm -- a protected file, and Edit File fails on one outright (p.239). ${hint(`(Only ${item.hashedBy} unseals it.)`)}` };
|
|
109
203
|
}
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return `No file called "${query}" within reach.${hint(` ("download" on its own lists what is.)`)}`;
|
|
204
|
+
const device = target ?? libraryDevice(actor);
|
|
205
|
+
if (device && deviceHas(device, item.name)) {
|
|
206
|
+
return { ok: false, text: `Your ${device.name} already holds ${item.name}.` };
|
|
114
207
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// a
|
|
130
|
-
//
|
|
131
|
-
// correctly refused ("a Matrix Search takes minutes ... a Combat
|
|
132
|
-
// Turn is three seconds") and `download` then emptied the archive
|
|
133
|
-
// for FREE with the ice mid-turn on the runner. That makes the fight
|
|
134
|
-
// the report asked for optional -- stroll in, take the paydata, and
|
|
135
|
-
// the ice never mattered.
|
|
136
|
-
//
|
|
137
|
-
// billAction is a no-op outside an encounter, so the quiet case --
|
|
138
|
-
// an empty host, a file loose on the grid -- is untouched: still one
|
|
139
|
-
// verb, no roll, no friction. It bills only where an action economy
|
|
140
|
-
// exists to bill against, and a refusal returns BEFORE the item
|
|
141
|
-
// moves, so a runner out of actions keeps both the file and the turn.
|
|
208
|
+
// THE HOST'S GATE: cracked opens it (hack.ts); otherwise Edit File
|
|
209
|
+
// wants a mark (p.239) before it can be tried at all.
|
|
210
|
+
const host = source.kind === 'host' ? source.host : undefined;
|
|
211
|
+
const needsRoll = !!host && !host.cracked;
|
|
212
|
+
if (host && needsRoll && (host.marksBy.get(actor.name) ?? 0) < 1) {
|
|
213
|
+
return { ok: false, text: `Edit File needs a MARK (p.239) -- you hold none on ${hostLabel(host.over)}. ${hint(`"hack" the host for one.`)}` };
|
|
214
|
+
}
|
|
215
|
+
if (needsRoll) {
|
|
216
|
+
const burned = editFileBlocked(actor);
|
|
217
|
+
if (burned)
|
|
218
|
+
return { ok: false, text: burned, stop: true };
|
|
219
|
+
}
|
|
220
|
+
// COPYING A FILE IS EDIT FILE, A COMPLEX ACTION (p.239). billAction
|
|
221
|
+
// is a no-op outside an encounter; a refusal returns BEFORE anything
|
|
222
|
+
// moves, so a runner out of actions keeps the turn (jfjjTmJ7NYwDaFiyB:
|
|
223
|
+
// an archive emptied for free with ice mid-turn).
|
|
142
224
|
const bill = billAction(this.scene, actor, 'complex', 'Edit File (copy)');
|
|
143
225
|
if (bill)
|
|
144
|
-
return bill;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
226
|
+
return { ok: false, text: bill, stop: true };
|
|
227
|
+
if (host && needsRoll) {
|
|
228
|
+
const roll = rollEditFile(this.scene, this.logger, actor, host, 'copy');
|
|
229
|
+
if (!roll.success) {
|
|
230
|
+
actor.performAction('edits a file', `${item.name} in ${hostLabel(host.over)}`, { quiet: this.scene.isHumanControlled(actor) });
|
|
231
|
+
this.logger.write(`${actor.name} failed to copy "${item.name}" from ${host.over.name}: ${roll.hits} v ${roll.defence}.`);
|
|
232
|
+
return { ok: false, text: `The copy stalls -- ${hostLabel(host.over)}'s checksum rejects the read and nothing comes down. A failed edit is not a failed sleaze; nobody upstairs noticed.${hint(` (Try again; each attempt is a Complex Action.)`)}` };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// THE MOVE. A sprite's file is already yours and moves; everything
|
|
236
|
+
// else is a copy you now own (p.239), the original left in place.
|
|
237
|
+
let landed;
|
|
238
|
+
if (source.kind === 'sprite') {
|
|
239
|
+
source.npc.inventory.removeItem(item.name);
|
|
240
|
+
landed = item;
|
|
150
241
|
}
|
|
151
|
-
|
|
152
|
-
|
|
242
|
+
else {
|
|
243
|
+
landed = copyFile(item, actor.name);
|
|
244
|
+
// Copying a file off the floor of the grid leaves the icon where it
|
|
245
|
+
// was, but you have now SEEN it -- nothing about plainSight changes.
|
|
153
246
|
}
|
|
154
|
-
|
|
155
|
-
|
|
247
|
+
let onto;
|
|
248
|
+
if (device) {
|
|
249
|
+
fileOnto(device, landed);
|
|
250
|
+
actor.noteConditionChanged();
|
|
251
|
+
onto = device;
|
|
156
252
|
}
|
|
157
253
|
else {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
item.category = Category.Datachip;
|
|
164
|
-
actor.performAction('pulls a file down', `${item.name} writing to a chip`);
|
|
165
|
-
this.logger.write(`${actor.name} downloaded "${item.name}"${fromSprite ? ` from ${fromSprite.npc.name}` : ` from ${room.name}`}.`);
|
|
254
|
+
onto = receiveData(actor, landed).device;
|
|
255
|
+
}
|
|
256
|
+
actor.performAction('copies a file', `${item.name}${onto ? ` onto ${onto.name}` : ' to a chip'}`, { quiet: this.scene.isHumanControlled(actor) });
|
|
257
|
+
this.logger.write(`${actor.name} downloaded "${item.name}" (${source.kind}) ${onto ? `onto ${onto.name}` : 'to a chip'}.`);
|
|
258
|
+
this.game?.requestSave('data');
|
|
166
259
|
this.scene.updateStatus();
|
|
167
|
-
|
|
168
|
-
?
|
|
169
|
-
`{light-blue-fg}${
|
|
170
|
-
`You
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
260
|
+
const text = onto
|
|
261
|
+
? source.kind === 'sprite'
|
|
262
|
+
? `{light-blue-fg}${source.npc.name} opens its hands and the data pours out of them -- {bold}{underline}${item.name}{/underline}{/bold} is on your ${onto.name} now.{/light-blue-fg}`
|
|
263
|
+
: `{light-blue-fg}You copy {bold}{underline}${item.name}{/underline}{/bold} onto your ${onto.name}${host ? ` -- ${hostLabel(host.over)} keeps its original` : ''}.{/light-blue-fg}${hint(` ("give" or "call" hands it over straight from the device; "transfer ${item.name} to chip" writes it to physical media.)`)}`
|
|
264
|
+
: `{light-blue-fg}You pull {bold}{underline}${item.name}{/underline}{/bold} down onto a chip -- you carry no device with a library to hold it.{/light-blue-fg}`;
|
|
265
|
+
// 'possess' objectives complete in the runner's hands; a device library
|
|
266
|
+
// is the runner's hands for data (scene.ts checkWinCondition).
|
|
267
|
+
if (this.scene.isHumanControlled(actor))
|
|
268
|
+
this.logger.log(text, { actor: actor.name });
|
|
269
|
+
if (!this.scene.isWinConditionTarget?.(actor.name)) {
|
|
270
|
+
this.scene.checkWinCondition?.(item.name, actor.name);
|
|
271
|
+
}
|
|
272
|
+
return { ok: true, text: this.scene.isHumanControlled(actor) ? '' : text };
|
|
273
|
+
}
|
|
274
|
+
/** "take all" on the Matrix plane: every file in reach here, one Edit File each. */
|
|
275
|
+
pullAll() {
|
|
276
|
+
const { pullable, reach } = this.inReach();
|
|
277
|
+
const here = pullable.filter(p => p.kind === 'host' || p.kind === 'loose');
|
|
278
|
+
if (here.length === 0) {
|
|
279
|
+
return reach === 'enter'
|
|
280
|
+
? `Those files are INSIDE the host, and you're out on the grid looking at its shell (p.246). "enter" first.`
|
|
281
|
+
: `Nothing here to copy${this.actor.hostInside && !this.actor.hostInside.cracked && !this.actor.hostInside.searchedBy.has(this.actor.name) ? ` that you have found -- "search" runs a Matrix Search (p.241)` : ''}.`;
|
|
282
|
+
}
|
|
283
|
+
const lines = [];
|
|
284
|
+
for (const p of here) {
|
|
285
|
+
const r = this.pull(p);
|
|
286
|
+
if (r.text)
|
|
287
|
+
lines.push(r.text);
|
|
288
|
+
if (r.stop)
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
return lines.join('\n');
|
|
176
292
|
}
|
|
177
293
|
}
|
|
178
294
|
//# sourceMappingURL=download.js.map
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { Command } from './command.js';
|
|
2
|
-
import { gridActionModifier } from '../utilities/grids.js';
|
|
3
2
|
import { hint } from '../utilities/hints.js';
|
|
4
|
-
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
5
3
|
import { billAction } from '../utilities/action-cost.js';
|
|
6
|
-
import {
|
|
4
|
+
import { editFileBlocked, rollEditFile } from '../utilities/edit-file-test.js';
|
|
7
5
|
import { hostLabel } from '../utilities/grid-names.js';
|
|
8
6
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
9
|
-
import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
|
|
10
7
|
/**
|
|
11
8
|
* EDIT FILE (SR5 p.239, RAG-checked 2026-09-14): a Complex Action, Marks
|
|
12
9
|
* Required 1, Computer + Logic [Data Processing] v. Intuition + Firewall.
|
|
@@ -21,9 +18,10 @@ import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-aud
|
|
|
21
18
|
*
|
|
22
19
|
* WHAT SHIPS (loose ends, 2026-09-14): DELETE. The cleanup's objective
|
|
23
20
|
* is a file the site keeps -- camera footage with the runner's face --
|
|
24
|
-
* and copying it
|
|
25
|
-
*
|
|
26
|
-
*
|
|
21
|
+
* and copying it changes nothing about what the desk still holds. COPY
|
|
22
|
+
* and CREATE shipped 2026-10-04 as "download" and "upload" (digital
|
|
23
|
+
* inventory), on the same roll (utilities/edit-file-test.ts); this verb
|
|
24
|
+
* routes "edit copy" / "edit create" to them. Change and protect are not built;
|
|
27
25
|
* Crack File and the Data Bomb family wait on a per-file protection
|
|
28
26
|
* rating (models/item.ts) and are their own item.
|
|
29
27
|
*
|
|
@@ -64,9 +62,21 @@ export class EditFileCommand extends Command {
|
|
|
64
62
|
}).execute(args);
|
|
65
63
|
}
|
|
66
64
|
const { op, word, rest } = this.operation(args);
|
|
65
|
+
// COPY and CREATE are built, under the verbs a player reaches for
|
|
66
|
+
// (digital inventory, 2026-10-04): copy is "download" (host to your
|
|
67
|
+
// device), create is "upload" (your device to the host). Same roll.
|
|
68
|
+
if (word === 'copy' || word === 'create') {
|
|
69
|
+
const ctx = { actor: this.actor, scene: this.scene, rooms: this.rooms, screen: this.screen, game: this.game };
|
|
70
|
+
if (word === 'copy') {
|
|
71
|
+
const { DownloadCommand } = await import('./download.js');
|
|
72
|
+
return new DownloadCommand(ctx).execute(rest);
|
|
73
|
+
}
|
|
74
|
+
const { UploadCommand } = await import('./upload.js');
|
|
75
|
+
return new UploadCommand(ctx).execute(rest);
|
|
76
|
+
}
|
|
67
77
|
if (op !== 'delete') {
|
|
68
78
|
const named = word ? `"${word}"` : 'that';
|
|
69
|
-
return `Edit File (p.239) can create, change, copy, delete or protect a file -- and this engine builds DELETE
|
|
79
|
+
return `Edit File (p.239) can create, change, copy, delete or protect a file -- and this engine builds DELETE ("erase <file>"), COPY ("download <file>") and CREATE ("upload <file>"). ${named} isn't built yet.`;
|
|
70
80
|
}
|
|
71
81
|
if (actor.plane !== 'matrix') {
|
|
72
82
|
return `Files are edited by a persona. ${hint(`"jack in" first.`)}`;
|
|
@@ -103,37 +113,22 @@ export class EditFileCommand extends Command {
|
|
|
103
113
|
return `Edit File needs a MARK (p.239) -- you hold none on ${hostLabel(room)}. ${hint(`"hack" the host for one.`)}`;
|
|
104
114
|
}
|
|
105
115
|
// Binder IC eats Data Processing; a bracket of 0 permits no hits.
|
|
106
|
-
const burned = actor
|
|
116
|
+
const burned = editFileBlocked(actor);
|
|
107
117
|
if (burned)
|
|
108
118
|
return burned;
|
|
109
119
|
const bill = billAction(this.scene, actor, 'complex', 'Edit File');
|
|
110
120
|
if (bill)
|
|
111
121
|
return bill;
|
|
112
|
-
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
// The grid tax was NAMED here and never charged -- gridActionModifier
|
|
116
|
-
// is the helper that answers both halves, returning 0 inside a host
|
|
117
|
-
// and the public grid's -2 out on it (p.233).
|
|
118
|
-
const grid = gridActionModifier(actor.currentGrid, !!actor.hostInside, actor.currentGrid);
|
|
119
|
-
const pool = Math.max(1, actor.logic + (computer > 0 ? computer : -1)
|
|
120
|
-
+ actor.matrixActionPenalty + grid.total + actor.woundModifier - actor.sustainingPenalty);
|
|
121
|
-
const limit = actor.matrixAttribute('dataProcessing');
|
|
122
|
-
const defence = hostDefensePool(host);
|
|
123
|
-
const attempt = rollPool(pool, limit, { gremlins: actor.deckGremlins });
|
|
124
|
-
const resist = rollPool(defence.pool);
|
|
125
|
-
if (showsMechanics(this.scene, actor)) {
|
|
126
|
-
this.logger.meta(`${mechanicsActorPrefix(this.scene, actor)}Edit File (delete) -- Computer + Logic [Data Processing]: ${formatRoll(attempt)}`);
|
|
127
|
-
this.logger.meta(` v. ${hostLabel(room)} (${defence.label}, rating standing in for Intuition): ${formatRoll(resist)}`);
|
|
128
|
-
}
|
|
122
|
+
// The roll is utilities/edit-file-test.ts, shared with download (copy)
|
|
123
|
+
// and upload (create) so the family cannot drift apart.
|
|
124
|
+
const attempt = rollEditFile(this.scene, this.logger, actor, host, 'delete');
|
|
129
125
|
actor.performAction('edits a file', `${file.name} in ${hostLabel(room)}`, { quiet: this.scene.isHumanControlled(actor) });
|
|
130
|
-
if (attempt.
|
|
131
|
-
this.logger.write(`${actor.name} failed to delete "${file.name}" in ${room.name}: ${attempt.hits} v ${
|
|
126
|
+
if (!attempt.success) {
|
|
127
|
+
this.logger.write(`${actor.name} failed to delete "${file.name}" in ${room.name}: ${attempt.hits} v ${attempt.defence}.`);
|
|
132
128
|
return `The file's checksum holds -- your edit rolls off it and the archive keeps ${file.name}. Nothing upstairs noticed: a failed edit is not a failed sleaze.${hint(` (Try again; each attempt is a Complex Action.)`)}`;
|
|
133
129
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.logger.write(`${actor.name} deleted "${file.name}" from ${room.name}'s host: ${attempt.hits} v ${resist.hits}.`);
|
|
130
|
+
host.removeFile(file.name);
|
|
131
|
+
this.logger.write(`${actor.name} deleted "${file.name}" from ${room.name}'s host: ${attempt.hits} v ${attempt.defence}.`);
|
|
137
132
|
this.scene.addWorldEvent(`${file.name} was erased from ${host.name}'s archive.`, { plane: 'matrix' });
|
|
138
133
|
const banner = this.scene.checkErased?.(file.name, actor.name);
|
|
139
134
|
this.scene.updateStatus();
|
|
@@ -2,6 +2,7 @@ import { Command } from './command.js';
|
|
|
2
2
|
import { priceOf, merchantOf, findWare, mintWare, takeRecentSale } from '../utilities/commerce.js';
|
|
3
3
|
import { sameSpot, spotOf, spotRefusal } from '../utilities/spots.js';
|
|
4
4
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
5
|
+
import { findDeviceFile, releaseHeld, receiveData } from '../utilities/data-library.js';
|
|
5
6
|
export class GiveCommand extends Command {
|
|
6
7
|
static verb = 'give';
|
|
7
8
|
/** Touches another actor: authorized per-act for AI-driven actors
|
|
@@ -75,6 +76,18 @@ export class GiveCommand extends Command {
|
|
|
75
76
|
}
|
|
76
77
|
// === Try to give an item ===
|
|
77
78
|
let item = this.actor.inventory.getItem(itemOrAmount);
|
|
79
|
+
// DATA ON A DEVICE IS IN YOUR HANDS TOO (digital inventory, user ruling
|
|
80
|
+
// 2026-10-04): a file in your commlink's library is beamed across the
|
|
81
|
+
// table, no chip required. Pack first -- a chip by that name wins --
|
|
82
|
+
// then the device libraries.
|
|
83
|
+
let fromDevice;
|
|
84
|
+
if (!item) {
|
|
85
|
+
const held = findDeviceFile(this.actor, itemOrAmount);
|
|
86
|
+
if (held) {
|
|
87
|
+
item = held.file;
|
|
88
|
+
fromDevice = held;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
78
91
|
// SERVING SHOP STOCK: a vendor's wares live in the ROOM (owner-tagged),
|
|
79
92
|
// not in their pockets -- so "give bowl of laksa to Maka" from Mama Wu
|
|
80
93
|
// used to fail silently while she stood next to her own wok. A real
|
|
@@ -203,7 +216,17 @@ export class GiveCommand extends Command {
|
|
|
203
216
|
this.scene.checkWinCondition(item.name, recipient.name);
|
|
204
217
|
return `You hand over the ${item.name}.${tillNote}`;
|
|
205
218
|
}
|
|
206
|
-
|
|
219
|
+
// A device file MOVES to the recipient -- onto their own device when
|
|
220
|
+
// they carry one (data-library receiveData), else a chip in hand.
|
|
221
|
+
let didTransfer;
|
|
222
|
+
if (fromDevice) {
|
|
223
|
+
didTransfer = item.transferable !== false && releaseHeld(this.actor, fromDevice);
|
|
224
|
+
if (didTransfer)
|
|
225
|
+
receiveData(recipient, item);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
didTransfer = this.actor.inventory.transferItemTo(item.name, recipient.inventory);
|
|
229
|
+
}
|
|
207
230
|
if (!didTransfer) {
|
|
208
231
|
this.logger.write(`Item "${item.name}" is not transferable.`);
|
|
209
232
|
this.actor.performAction('did not give', `item cannot be given`);
|