@maka/maka-cli 5.199.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 (39) 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 +115 -9
  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/presence-gate.js +27 -2
  37. package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.js +7 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/utilities/sprite-power-actions.js +270 -0
  39. package/package.json +1 -1
@@ -0,0 +1,71 @@
1
+ /**
2
+ * WHO IS WATCHING THE DICE (wjFBY3zgyWzJP9adw, Deditri: "the sprite put a
3
+ * mark on the target but there was no output about the rolls in the
4
+ * logs").
5
+ *
6
+ * The sprite's dice WERE rolled. `order <sprite> mark <lock>` runs the
7
+ * real verb with the sprite as actor -- commands/order.ts hands the
8
+ * string to NPC.tryRunCommand, commands/mark.ts re-dispatches into
9
+ * HackCommand, and hack.ts rolls Hack on the Fly against the device
10
+ * exactly as it would for a decker. The roll was then thrown away at the
11
+ * REPORTING layer, by a guard that has been copied into ~30 commands:
12
+ *
13
+ * if (this.scene.isHumanControlled(actor)) this.logger.meta(...)
14
+ *
15
+ * That asks "is the ROLLER a human". The question the Mechanics panel
16
+ * actually wants is "is a HUMAN WATCHING" -- and the codebase already
17
+ * answers it correctly two files over, in CombatExchange.playerWitnesses
18
+ * (utilities/combat-exchange.ts), which is precisely why a sprite's
19
+ * COMBAT rolls have always shown up while the same sprite's ordered
20
+ * mark went silent. One question, asked two ways, in one engine.
21
+ *
22
+ * WHY OWNERSHIP AND NOT PROXIMITY. playerWitnesses is a bystander test:
23
+ * same place, perceivable plane. That is right for a fight, which is an
24
+ * event in a room. It is wrong for an ORDER, which is a thing the player
25
+ * deliberately did: a rigger whose drone is sweeping the floor above,
26
+ * or a technomancer whose sprite is working a host their body is nowhere
27
+ * near, typed that command and is owed its dice. So a companion's rolls
28
+ * are shown to its master wherever it is standing. Anything that is
29
+ * NOT a companion -- IC, a guard, a passer-by -- keeps the old silence
30
+ * and reports through the channels it already had (utilities/ic.ts
31
+ * accumulates into IICTurn.meta; a fight goes through announce).
32
+ *
33
+ * THE FORK MUST STAY EXCLUSIVE. Several commands route the roll to
34
+ * EITHER the Mechanics panel (player) OR their own return string, which
35
+ * for an NPC is its AI history:
36
+ *
37
+ * const isPlayer = scene.isHumanControlled(actor);
38
+ * if (isPlayer) this.logger.meta(`Perception: ${formatRoll(roll)}`);
39
+ * const rollPrefix = isPlayer ? '' : `Perception: ${formatRoll(roll)}\n`;
40
+ *
41
+ * Both halves must read the SAME predicate or an ordered companion
42
+ * prints its dice twice. Callers swapping to showsMechanics must swap
43
+ * both.
44
+ */
45
+ /** The companion ledger entry for this actor, if it is somebody's. */
46
+ function companionEntryFor(scene, actor) {
47
+ return scene.ownerGame?.companions?.find(c => c.npc === actor);
48
+ }
49
+ /**
50
+ * Should this actor's dice anatomy go to the Mechanics panel?
51
+ *
52
+ * True for a human-controlled actor (the old rule, unchanged), and for a
53
+ * companion a human is running -- a sprite, spirit, agent or drone whose
54
+ * master typed the order that got here.
55
+ */
56
+ export function showsMechanics(scene, actor) {
57
+ if (scene.isHumanControlled?.(actor) === true)
58
+ return true;
59
+ return companionEntryFor(scene, actor) !== undefined;
60
+ }
61
+ /**
62
+ * The name to hang on a Mechanics line when the roller is not the player
63
+ * -- "Crack Sprite -- Hack on the Fly (sleaze) on Hatch Maglock: ...".
64
+ * Empty for the player's own rolls, which stay exactly as they read
65
+ * today: a panel that started prefixing the player's own name on every
66
+ * line would be noisier for the common case to serve the rare one.
67
+ */
68
+ export function mechanicsActorPrefix(scene, actor) {
69
+ return scene.isHumanControlled?.(actor) === true ? '' : `${actor.name} -- `;
70
+ }
71
+ //# sourceMappingURL=mechanics-audience.js.map
@@ -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.
@@ -17,12 +17,37 @@
17
17
  * Game, a hub, or a socket. It is deliberately the WHOLE gate: a caller
18
18
  * that reimplements part of it inline is the bug this file exists to
19
19
  * prevent coming back.
20
+ *
21
+ * THE ACCOUNT HALF MOVED IN HERE (TLKZKLQ4rJBCnDjrw, 2026-09-15). It was
22
+ * living in Game.startPresenceHeartbeat, guarding only the process that
23
+ * ARMS the timer -- and `returnToHub` beats once directly at every
24
+ * homecoming, past that guard. So a hosted hub, a repro bench and the
25
+ * jest suite alike all dialed www.maka-cli.com on the way home with
26
+ * whatever token the BOX happened to be logged in as. In the suite that
27
+ * is the developer's own account: `loose-ends.test.ts` boots a fixture
28
+ * runner named Vex over the "Legacy Hub Fixture" seed, goes home, and
29
+ * overwrote the developer's real presence row -- so their web sheet read
30
+ * "You are on the street as Vex", a runner belonging to somebody else's
31
+ * account entirely. Two halves of one rule, in two places, is how that
32
+ * happened; both halves live here now.
33
+ */
34
+ /**
35
+ * IS THIS PROCESS ON THE STREET AT ALL? The account half of the gate --
36
+ * the half that asks whether the credentials on this box belong to the
37
+ * runner in memory. A `false` here means no beat ever, from any caller,
38
+ * at any moment; nothing downstream can re-enable it.
20
39
  */
21
- export function presenceBeatAllowed(atHub, holdsLiveSeat) {
40
+ export function presenceProcessOnTheStreet(f) {
41
+ return !f.anonymous && !f.bench && !f.hostedHub;
42
+ }
43
+ /** The WHOLE gate: the account half, then the position half. */
44
+ export function presenceBeatAllowed(f) {
45
+ if (!presenceProcessOnTheStreet(f))
46
+ return false;
22
47
  // A solo run stays private, exactly as before. Only a LIVE SEAT keeps
23
48
  // the beat alive off-hub -- the narrowest change that fixes the cut,
24
49
  // and at that point other real players are sitting at the table, so
25
50
  // "the run is nobody's business" no longer describes the situation.
26
- return atHub || holdsLiveSeat;
51
+ return f.atHub || f.holdsLiveSeat;
27
52
  }
28
53
  //# sourceMappingURL=presence-gate.js.map
@@ -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.199.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.",