@maka/maka-cli 5.200.0 → 5.201.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.
Files changed (38) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +10 -0
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +2 -2
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js +7 -0
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +7 -6
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +10 -2
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +20 -2
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/erase-mark.js +237 -0
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +20 -11
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +3 -2
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +3 -2
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +3 -2
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +15 -5
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/overwatch.js +3 -2
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +36 -10
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/sprite-power.js +85 -0
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +12 -3
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +3 -2
  19. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +139 -1
  20. package/bundle/typescript/src/commands/game/sideQuest/game.js +90 -2
  21. package/bundle/typescript/src/commands/game/sideQuest/models/host.js +8 -0
  22. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +19 -0
  23. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +8 -0
  24. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +44 -0
  25. package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.js +48 -7
  26. package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-errand.js +67 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-hire.js +113 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/utilities/drone-prose.js +11 -3
  29. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-names.js +42 -0
  30. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +10 -5
  31. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +37 -10
  32. package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +12 -0
  33. package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +22 -0
  34. package/bundle/typescript/src/commands/game/sideQuest/utilities/mechanics-audience.js +71 -0
  35. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +7 -0
  36. package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.js +7 -1
  37. package/bundle/typescript/src/commands/game/sideQuest/utilities/sprite-power-actions.js +270 -0
  38. package/package.json +1 -1
@@ -601,6 +601,9 @@ function serializeRoomOverlay(room, player) {
601
601
  inventory: b.inventory.getAllItems().map(serializeItem),
602
602
  })),
603
603
  searchedBy: [...room.searchedBy],
604
+ // A SANCTIONED HOST KEEPS NO INDEX EITHER, for the same reason it
605
+ // forgets a crack: a haven is not a target you cased.
606
+ hostSearchedBy: room.hostSanctioned ? [] : [...(room.host?.searchedBy ?? [])],
604
607
  visitedBy: [...room.visitedBy],
605
608
  // A SANCTIONED HOST NEVER REMEMBERS A CRACK (Room.hostSanctioned).
606
609
  // The overlay rule is right for run content -- you broke that
@@ -771,6 +774,10 @@ export function applyHubOverlay(scene, saved, player, homeRoom) {
771
774
  return { name: b.name, cell: { ...b.cell }, inventory: held };
772
775
  });
773
776
  room.searchedBy = new Set(overlay.searchedBy);
777
+ // Only onto a room that HAS a host -- a seed that dropped its node
778
+ // between saves must not grow one back through the overlay.
779
+ if (room.host)
780
+ room.host.searchedBy = new Set(overlay.hostSearchedBy ?? []);
774
781
  room.visitedBy = new Set(overlay.visitedBy);
775
782
  // Both ends, so an OLD save written before hostSanctioned existed
776
783
  // cannot restore a cracked haven either.
@@ -14,7 +14,13 @@
14
14
  */
15
15
  export const PROGRAMS = [
16
16
  // ---- Common programs (p.245; pricing canon-approximate) ----
17
- { key: 'browse', name: 'Browse', kind: 'common', cost: 80, availability: 2, effect: '+2 dice on Matrix Search while jacked in.' },
17
+ // BROWSE BUYS TIME, NOT DICE (Splintered State p.54: it "cuts the base
18
+ // time in half"). The catalogue advertised "+2 dice on Matrix Search"
19
+ // and no code ever added them -- utilities/perception.ts has always
20
+ // halved the clock, which is the half that is right. A program whose
21
+ // shop blurb promises a bonus it does not grant is a lie the player
22
+ // pays 80 nuyen for.
23
+ { key: 'browse', name: 'Browse', kind: 'common', cost: 80, availability: 2, effect: 'Halves the base time of a Matrix Search (p.241).' },
18
24
  { key: 'toolbox', name: 'Toolbox', kind: 'common', cost: 80, availability: 2, effect: '+1 Data Processing.' },
19
25
  { key: 'encryption', name: 'Encryption', kind: 'common', cost: 80, availability: 2, effect: '+1 Firewall.' },
20
26
  { key: 'signal-scrub', name: 'Signal Scrub', kind: 'common', cost: 80, availability: 2, effect: 'Cleans your noise floor: +1 die defending your PAN.' },
@@ -0,0 +1,270 @@
1
+ import { rollPool, formatRoll } from './dice.js';
2
+ import { hint } from './hints.js';
3
+ import { hostLabel } from './grid-names.js';
4
+ import { spriteMatrix, spritePower, powersForSprite } from '../sprite-powers.js';
5
+ const refuse = (text) => ({ text, meta: [], spent: false });
6
+ /** Every persona sharing this sprite's place on the grid or in a host. */
7
+ function personasNear(ctx) {
8
+ const here = ctx.sprite.hostInside;
9
+ return ctx.scene.allActors.filter((a) => a !== ctx.sprite && a.plane === 'matrix'
10
+ && (here ? a.hostInside === here : a.currentGrid === ctx.sprite.currentGrid));
11
+ }
12
+ /** The host this sprite is standing inside, if any. */
13
+ function hostOf(ctx) {
14
+ return ctx.sprite.hostInside?.over;
15
+ }
16
+ /** The sprite's pool for a skill, its Level standing in for the attribute
17
+ * the book pairs it with (p.254: a sprite's ratings are all Level-derived). */
18
+ function spritePool(ctx, skill) {
19
+ const rating = ctx.sprite.skillRating(skill);
20
+ return Math.max(1, (rating > 0 ? rating : -1) + ctx.level);
21
+ }
22
+ // ===================== THE FOUR ROLLED POWERS =====================
23
+ /** COOKIE (p.256-257): Hacking + Resonance [Sleaze] v. Intuition + Firewall. */
24
+ function cookie(ctx, targetName) {
25
+ const near = personasNear(ctx);
26
+ const target = near.find(p => p.name.toLowerCase() === targetName.toLowerCase())
27
+ ?? near.find(p => p.name.toLowerCase().includes(targetName.toLowerCase()));
28
+ if (!target) {
29
+ return refuse(`${ctx.sprite.name} finds no persona called "${targetName}" to tag.${near.length > 0 ? hint(` (Here: ${near.map(p => p.name).join(', ')}.)`) : ''}`);
30
+ }
31
+ const m = spriteMatrix(ctx.spriteKey, ctx.level);
32
+ const attack = rollPool(spritePool(ctx, 'hacking') + m.resonance, m.sleaze);
33
+ const defence = rollPool(Math.max(1, target.intuition + target.matrixAttribute('firewall')));
34
+ const net = attack.hits - defence.hits;
35
+ const meta = [
36
+ `${ctx.sprite.name} -- Cookie (Hacking + Resonance [Sleaze ${m.sleaze}]): ${formatRoll(attack)}`,
37
+ ` ${target.name} resists (Intuition + Firewall): ${formatRoll(defence)} -- net ${net >= 0 ? '+' : ''}${net}`,
38
+ ];
39
+ if (net <= 0) {
40
+ return { text: `The tag slides off ${target.name}'s icon and dissolves. Nothing stuck.`, meta, spent: true };
41
+ }
42
+ // Net hits set the DEPTH the report will carry (p.257: one hit a bare
43
+ // outline, four or more a detailed report).
44
+ target.cookiedBy.set(ctx.sprite.name, net);
45
+ const depth = net >= 4 ? 'a detailed report' : net >= 2 ? 'more than an outline' : 'a bare outline';
46
+ return {
47
+ text: `{light-blue-fg}${ctx.sprite.name} writes a silent file into ${target.name}'s persona, rated ${ctx.level} and running quiet. It is logging -- ${depth} when it ships home.{/light-blue-fg}${hint(` (${target.name} is carrying it and does not know. A Matrix Perception on them finds it.)`)}`,
48
+ meta,
49
+ spent: true,
50
+ };
51
+ }
52
+ /** DIAGNOSTICS (p.257): a Teamwork Test, Hardware + Level [Data Processing].
53
+ * Any hits give +1 limit; each hit adds a die. */
54
+ function diagnostics(ctx, targetName) {
55
+ const room = ctx.master.currentLocation;
56
+ const device = deviceNamed(room, targetName) ?? ctx.master.inventory.getAllItems()
57
+ .find(i => i.name.toLowerCase().includes(targetName.toLowerCase()));
58
+ if (!targetName || !device) {
59
+ return refuse(`${ctx.sprite.name} needs a device to read. "order ${ctx.sprite.name} diagnostics <device>".`);
60
+ }
61
+ const m = spriteMatrix(ctx.spriteKey, ctx.level);
62
+ const roll = rollPool(spritePool(ctx, 'hardware'), m.dataProcessing);
63
+ const meta = [`${ctx.sprite.name} -- Diagnostics (Hardware + Level [Data Processing ${m.dataProcessing}]): ${formatRoll(roll)}`];
64
+ if (roll.hits === 0) {
65
+ return { text: `${ctx.sprite.name} reads the ${name(device)} down to its seams and finds nothing useful to say.`, meta, spent: true };
66
+ }
67
+ // It takes the sprite's whole attention: one target at a time, and the
68
+ // bonus lasts until the sprite does anything else (dropped by the
69
+ // caller when the next power is used).
70
+ ctx.master.spriteAssist = { sprite: ctx.sprite.name, device: name(device), dice: roll.hits, limit: 1 };
71
+ return {
72
+ text: `{light-blue-fg}${ctx.sprite.name} folds itself around the ${name(device)} and starts narrating -- tolerances, wear, the one screw somebody cross-threaded. +${roll.hits} dice and +1 limit while you work it.{/light-blue-fg}${hint(` (It holds this until you give ${ctx.sprite.name} another order.)`)}`,
73
+ meta,
74
+ spent: true,
75
+ };
76
+ }
77
+ /** ELECTRON STORM (p.257): Cybercombat + Resonance [Attack] v. Intuition +
78
+ * Firewall, sustained, (Resonance) DV Matrix damage and 2 noise. */
79
+ function electronStorm(ctx, targetName) {
80
+ const near = personasNear(ctx);
81
+ const target = near.find(p => p.name.toLowerCase().includes(targetName.toLowerCase()));
82
+ if (!target) {
83
+ return refuse(`${ctx.sprite.name} finds no persona called "${targetName}" out here.${near.length > 0 ? hint(` (Here: ${near.map(p => p.name).join(', ')}.)`) : ''}`);
84
+ }
85
+ const m = spriteMatrix(ctx.spriteKey, ctx.level);
86
+ const attack = rollPool(spritePool(ctx, 'cybercombat') + m.resonance, m.attack);
87
+ const defence = rollPool(Math.max(1, target.intuition + target.matrixAttribute('firewall')));
88
+ const net = attack.hits - defence.hits;
89
+ const meta = [
90
+ `${ctx.sprite.name} -- Electron Storm (Cybercombat + Resonance [Attack ${m.attack}]): ${formatRoll(attack)}`,
91
+ ` ${target.name} resists (Intuition + Firewall): ${formatRoll(defence)} -- net ${net >= 0 ? '+' : ''}${net}`,
92
+ ];
93
+ if (net <= 0) {
94
+ return { text: `The datastorm breaks over ${target.name} and washes past. Nothing sticks to them.`, meta, spent: true };
95
+ }
96
+ const dv = m.resonance;
97
+ const soak = rollPool(Math.max(1, target.matrixAttribute('firewall') + target.logic));
98
+ const dealt = Math.max(0, dv - soak.hits);
99
+ meta.push(` ${dv} DV Matrix, soaked (Firewall + Logic): ${formatRoll(soak)} -> ${dealt}`);
100
+ if (dealt > 0)
101
+ target.takeMatrixDamage(dealt);
102
+ // Sustained: it keeps burning until the sprite is hurt or redirected.
103
+ ctx.sprite.sustainedStorm = target.name;
104
+ return {
105
+ text: `{light-blue-fg}${ctx.sprite.name} tears open and ${target.name}'s icon is inside it -- corrupting datastreams sheeting off them, ${dealt} Matrix damage and their signal fraying (noise +2). It is holding the storm open.{/light-blue-fg}${hint(` (It burns again every action it sustains this -- and ends the moment ${ctx.sprite.name} takes Matrix damage of its own.)`)}`,
106
+ meta,
107
+ spent: true,
108
+ };
109
+ }
110
+ /** GREMLINS (p.257): Hardware + Level [Attack] v. Device Rating + Firewall.
111
+ * Success is a glitch; 4+ net hits is a critical one. */
112
+ function gremlins(ctx, targetName) {
113
+ const room = ctx.master.currentLocation;
114
+ const device = deviceNamed(room, targetName);
115
+ if (!device) {
116
+ return refuse(`${ctx.sprite.name} finds no device called "${targetName}" to sour.`);
117
+ }
118
+ const m = spriteMatrix(ctx.spriteKey, ctx.level);
119
+ const attack = rollPool(spritePool(ctx, 'hardware'), m.attack);
120
+ const defence = rollPool(Math.max(1, device.rating * 2));
121
+ const net = attack.hits - defence.hits;
122
+ const meta = [
123
+ `${ctx.sprite.name} -- Gremlins (Hardware + Level [Attack ${m.attack}]): ${formatRoll(attack)}`,
124
+ ` ${device.name} resists (Device Rating + Firewall): ${formatRoll(defence)} -- net ${net >= 0 ? '+' : ''}${net}`,
125
+ ];
126
+ if (net <= 0) {
127
+ return { text: `The ${device.name} hiccups once and carries on. Whatever ${ctx.sprite.name} whispered into it did not take.`, meta, spent: true };
128
+ }
129
+ // WHAT A GLITCH IS, ON A DEVICE -- an engine ruling, labelled. The book
130
+ // says "the GM chooses an appropriate malfunction" and at 4+ net hits
131
+ // "the device crashes, burns out, or jolts its user". This engine has
132
+ // exactly one model of a device that has stopped behaving: its Matrix
133
+ // condition monitor (Device.takeMatrixDamage, which is what Data Spike
134
+ // fills). So a glitch is damage scaled by how badly it landed, and a
135
+ // critical glitch fills the track -- the device is dead, which is the
136
+ // book's "crashes, burns out" in the vocabulary the engine already
137
+ // speaks. Inventing a separate "soured" flag would mean a state
138
+ // nothing else in the engine reads.
139
+ const critical = net >= 4;
140
+ const boxes = critical ? device.matrixMonitor : Math.max(1, Math.ceil(net / 2));
141
+ device.takeMatrixDamage(boxes);
142
+ meta.push(` ${critical ? 'CRITICAL glitch' : 'glitch'}: ${boxes} Matrix damage to ${device.name}`);
143
+ return {
144
+ text: critical
145
+ ? `{light-blue-fg}${ctx.sprite.name} gets INTO the ${device.name} and something lets go -- a bang, a smell of hot resin, and the panel goes dark for good.{/light-blue-fg}`
146
+ : `{light-blue-fg}The ${device.name} starts lying. Readings drift, a control sticks, a signal loops back on itself -- ${ctx.sprite.name} has soured it.{/light-blue-fg}`,
147
+ meta,
148
+ spent: true,
149
+ };
150
+ }
151
+ // ===================== THE STATE POWERS =====================
152
+ /** CAMOUFLAGE (p.256): hides a file inside another file, invisible to
153
+ * Matrix searches -- only a Perception specifically hunting it finds it. */
154
+ function camouflage(ctx, targetName) {
155
+ const room = hostOf(ctx);
156
+ const file = room && fileNamed(room, targetName);
157
+ if (!room || !file) {
158
+ return refuse(`${ctx.sprite.name} has no file called "${targetName}" to hide. It works inside a host, on what the archive holds.`);
159
+ }
160
+ file.concealedBy = ctx.sprite.name;
161
+ return {
162
+ text: `{light-blue-fg}${ctx.sprite.name} folds ${file.name} into the body of another file -- same checksum, same size, nothing out of place. A Matrix Search will walk straight past it now.{/light-blue-fg}${hint(` (Only a Matrix Perception hunting this file finds it -- ${ctx.sprite.name} included.)`)}`,
163
+ meta: [],
164
+ spent: true,
165
+ };
166
+ }
167
+ /** HASH (p.257): seals a file with a Resonance algorithm only this sprite
168
+ * can undo, for up to (Level x 10) Combat Turns. */
169
+ function hash(ctx, targetName) {
170
+ const room = hostOf(ctx);
171
+ const file = room && fileNamed(room, targetName);
172
+ if (!room || !file) {
173
+ return refuse(`${ctx.sprite.name} has no file called "${targetName}" to seal.`);
174
+ }
175
+ file.hashedBy = ctx.sprite.name;
176
+ file.hashTurns = ctx.level * 10;
177
+ return {
178
+ text: `{light-blue-fg}${ctx.sprite.name} wraps ${file.name} in an algorithm nothing else in the Matrix can read, and carries it. ${ctx.level * 10} Combat Turns.{/light-blue-fg}${hint(` (Nobody else edits or deletes it while the hash holds -- but kill ${ctx.sprite.name} while it carries this and the file is corrupted for good.)`)}`,
179
+ meta: [],
180
+ spent: true,
181
+ };
182
+ }
183
+ /** SUPPRESSION (p.257): a sprite in a host delays launched IC by
184
+ * (Level / 2) Combat Turns; delayed IC can neither act nor be targeted. */
185
+ function suppression(ctx) {
186
+ const room = hostOf(ctx);
187
+ if (!room?.host) {
188
+ return refuse(`Suppression baffles a HOST, and ${ctx.sprite.name} is not inside one.`);
189
+ }
190
+ const turns = Math.max(1, Math.floor(ctx.level / 2));
191
+ room.host.icLaunchDelay = turns;
192
+ return {
193
+ text: `{light-blue-fg}${ctx.sprite.name} spreads itself thin across ${hostLabel(room)}'s launch paths and starts arguing with them. Anything it tries to bring up comes up ${turns} Combat Turn${turns === 1 ? '' : 's'} late -- and comes up unable to act or be touched while it waits.{/light-blue-fg}`,
194
+ meta: [],
195
+ spent: true,
196
+ };
197
+ }
198
+ /** WATERMARK (p.257): an invisible tag only Resonance-driven things read. */
199
+ function watermark(ctx, rest) {
200
+ const room = hostOf(ctx);
201
+ // LONGEST FILE NAME FIRST, then the rest is the message. Splitting on
202
+ // the first space would read "Client Ledger they know" as a file called
203
+ // "Client" -- and every file in this game has a two-word name.
204
+ const words = rest.split(/\s+/).filter(w => w.length > 0);
205
+ let file;
206
+ let note = '';
207
+ for (let i = words.length; i >= 1 && !file; i--) {
208
+ const candidate = room && fileNamed(room, words.slice(0, i).join(' '));
209
+ if (candidate) {
210
+ file = candidate;
211
+ note = words.slice(i).join(' ').trim();
212
+ }
213
+ }
214
+ if (!room || !file) {
215
+ return refuse(`${ctx.sprite.name} has no icon called "${words[0] ?? ''}" to mark. "order ${ctx.sprite.name} watermark <file> <message>".`);
216
+ }
217
+ file.watermark = { by: ctx.sprite.name, note: note || 'no message, just the mark' };
218
+ return {
219
+ text: `{light-blue-fg}${ctx.sprite.name} writes into ${file.name} something no eye and no program will ever render${note ? `: "${note}"` : ''}. It overwrites whatever was there before, and it lasts as long as the icon does.{/light-blue-fg}${hint(` (Only Resonance reads it -- a technomancer or another sprite.)`)}`,
220
+ meta: [],
221
+ spent: true,
222
+ };
223
+ }
224
+ // ===================== DISPATCH =====================
225
+ function name(x) { return x.name; }
226
+ function deviceNamed(room, query) {
227
+ if (!query)
228
+ return undefined;
229
+ const q = query.toLowerCase();
230
+ return room.allDevices().find(d => d.name.toLowerCase() === q)
231
+ ?? room.allDevices().find(d => d.name.toLowerCase().includes(q));
232
+ }
233
+ function fileNamed(room, query) {
234
+ if (!query)
235
+ return undefined;
236
+ const q = query.toLowerCase();
237
+ const files = room.inventory.getAllItems().filter(i => i.plane === 'matrix');
238
+ return files.find(f => f.name.toLowerCase() === q) ?? files.find(f => f.name.toLowerCase().includes(q));
239
+ }
240
+ /**
241
+ * Run one power. The caller has already established that this actor IS a
242
+ * sprite and has the power; this resolves it and reports whether a task
243
+ * was actually spent.
244
+ */
245
+ export function runSpritePower(key, ctx, rest) {
246
+ const power = spritePower(key);
247
+ if (!power)
248
+ return refuse(`No such sprite power.`);
249
+ if (!power.runnable) {
250
+ // Said plainly rather than failing silently -- the catalogue's own
251
+ // rule (a wrong ability is worse than a missing one).
252
+ return refuse(`${power.name} (${power.page}) is canon this engine lists but does not yet resolve -- it wants the glitch contract changed in one place first. "sprites" says which powers are moves.`);
253
+ }
254
+ if (!powersForSprite(ctx.spriteKey).some(p => p.key === key)) {
255
+ return refuse(`A ${ctx.spriteKey} sprite has no ${power.name} (p.259). ${ctx.sprite.name} can do: ${powersForSprite(ctx.spriteKey).map(p => p.name).join(', ')}.`);
256
+ }
257
+ const target = rest.trim();
258
+ switch (key) {
259
+ case 'cookie': return cookie(ctx, target);
260
+ case 'diagnostics': return diagnostics(ctx, target);
261
+ case 'electron-storm': return electronStorm(ctx, target);
262
+ case 'gremlins': return gremlins(ctx, target);
263
+ case 'camouflage': return camouflage(ctx, target);
264
+ case 'hash': return hash(ctx, target);
265
+ case 'suppression': return suppression(ctx);
266
+ case 'watermark': return watermark(ctx, target);
267
+ default: return refuse(`${power.name} is not resolved here.`);
268
+ }
269
+ }
270
+ //# sourceMappingURL=sprite-power-actions.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.200.0",
3
+ "version": "5.201.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.",