@maka/maka-cli 5.200.0 → 5.202.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/ask.js +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +7 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +10 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +20 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/erase-mark.js +237 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +20 -11
- package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +15 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/overwatch.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +36 -10
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprite-power.js +85 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +12 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +165 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/npc-factory.js +37 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +109 -11
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +41 -9
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +10 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +95 -3
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +38 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +20 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +44 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +11 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +23 -3
- package/bundle/typescript/src/commands/game/sideQuest/npc-kits.js +295 -0
- package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.js +48 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +34 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-errand.js +67 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-hire.js +113 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +87 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/commerce.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/drone-prose.js +11 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js +17 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-names.js +42 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +10 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +37 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/mechanics-audience.js +71 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +36 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.js +7 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js +13 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sprite-power-actions.js +270 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/street-gang.js +25 -13
- package/package.json +1 -1
|
@@ -174,9 +174,17 @@ function postSweepOfficer(game, room, band) {
|
|
|
174
174
|
// not follow.
|
|
175
175
|
until: 'exchange',
|
|
176
176
|
room,
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
177
|
+
// PROFESSIONAL RATING 3, "Police Patrols" (SR5 p.383): the catalog's
|
|
178
|
+
// `police-patrol` npc-kit row -- armor jacket, Ares Predator V,
|
|
179
|
+
// Defiance EX Shocker, stun baton, Renraku Sensei, and a Biometric
|
|
180
|
+
// Reader (p.440), which is what the book has where this prose says
|
|
181
|
+
// "wand" -- SR5 prints no such item. factionOf() already read an
|
|
182
|
+
// 'authority' ephemeral as Knight Errant; the row says so too.
|
|
183
|
+
// A working beat cop, not a boss fight: the player who decides to
|
|
184
|
+
// swing at one is picking a fight with a person, not a set piece.
|
|
185
|
+
kit: SWEEP_OFFICER_KIT,
|
|
186
|
+
// A pre-v21 cache resolves to no kit and gets this instead: the old
|
|
187
|
+
// rent-a-cop numbers, exactly as before.
|
|
180
188
|
combat: {
|
|
181
189
|
body: 3, agility: 3, reaction: 3, strength: 3, willpower: 3,
|
|
182
190
|
logic: 3, intuition: 3, charisma: 3, edge: 1, combatSkill: 3,
|
|
@@ -184,6 +192,8 @@ function postSweepOfficer(game, room, band) {
|
|
|
184
192
|
},
|
|
185
193
|
});
|
|
186
194
|
}
|
|
195
|
+
/** The sweep officer's npc-kit row (npc-kits.ts). */
|
|
196
|
+
export const SWEEP_OFFICER_KIT = 'police-patrol';
|
|
187
197
|
/**
|
|
188
198
|
* THE OFFICERS, and there is more than one of them now.
|
|
189
199
|
*
|
|
@@ -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
|
|
@@ -38,21 +38,32 @@ import { AI } from '../../../../tools/ai/ai.class.js';
|
|
|
38
38
|
* the ephemeral never decides anything.
|
|
39
39
|
*/
|
|
40
40
|
/**
|
|
41
|
-
* PROFESSIONAL RATING 1 GANGER
|
|
42
|
-
*
|
|
43
|
-
* Intimidation
|
|
41
|
+
* PROFESSIONAL RATING 1 GANGER (SR5 p.382, "Gangers & Street Scum"):
|
|
42
|
+
* the catalog's `gangers` npc-kit row -- B/A 4, R 3, S 4, W/I/C 3, L 2,
|
|
43
|
+
* Blades 4 / Clubs 3 / Intimidation 4 / Pistols 4 / Unarmed 3,
|
|
44
|
+
* Toughness, an armor vest, a Browning Ultra-Power, a knife and a Meta
|
|
45
|
+
* Link (npc-kits.ts; the rows live on maka-cli.com, catalog v21).
|
|
44
46
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* THIS USED TO BE A CONSTANT HERE, and the constant was the bug. It
|
|
48
|
+
* said "canon stats" while giving them `firearms: 3` -- a skill that
|
|
49
|
+
* has not existed since stage 3 (skills.ts), written and never read --
|
|
50
|
+
* and no gun, so a "ganger with Pistols" always closed to punching
|
|
51
|
+
* range. The row carries the book's line, gear included, and a
|
|
52
|
+
* pre-v21 cache resolves to no kit, in which case the fallback block
|
|
53
|
+
* below is what they get: the old numbers, minus the dead key.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately NOT scaled up. Canon's own barrens encounter calls a
|
|
56
|
+
* gang ambush "a straightforward, quick, and easy fight" for runners
|
|
57
|
+
* (Splintered State p.11). A mob that threatened a professional crew
|
|
58
|
+
* would be a set piece, and a set piece is a scene NPC's job, not a
|
|
59
|
+
* passing face's.
|
|
50
60
|
*/
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
export const GANGER_KIT = 'gangers';
|
|
62
|
+
const GANGER_FALLBACK_COMBAT = {
|
|
63
|
+
body: 4, agility: 4, reaction: 3, strength: 4,
|
|
64
|
+
willpower: 3, logic: 2, intuition: 3, charisma: 3,
|
|
54
65
|
edge: 1, combatSkill: 3,
|
|
55
|
-
skills: { blades:
|
|
66
|
+
skills: { blades: 4, clubs: 3, intimidation: 4, pistols: 4, unarmed: 3, perception: 2 },
|
|
56
67
|
};
|
|
57
68
|
/** How often a street corner turns out to be somebody's corner. */
|
|
58
69
|
const GANG_CHANCE = 0.08;
|
|
@@ -116,7 +127,8 @@ export function maybeStreetGang(host, actor, room) {
|
|
|
116
127
|
// with it through the doorway.
|
|
117
128
|
until: 'fight',
|
|
118
129
|
room,
|
|
119
|
-
|
|
130
|
+
kit: GANGER_KIT,
|
|
131
|
+
combat: GANGER_FALLBACK_COMBAT,
|
|
120
132
|
});
|
|
121
133
|
if (!npc)
|
|
122
134
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.202.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.",
|