@maka/maka-cli 5.196.0 → 5.198.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/advance.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js +6 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +8 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +5 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +62 -13
- package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/install.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +5 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/quality-trade.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/register.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +19 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/sell.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/shout.js +69 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +7 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +45 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +13 -5
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +80 -57
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +24 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/action-cost.js +36 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/earshot.js +116 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +83 -6
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +14 -14
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-style.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +8 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.198.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.",
|
|
@@ -2,6 +2,7 @@ import { Command } from './command.js';
|
|
|
2
2
|
import { hint } from '../utilities/hints.js';
|
|
3
3
|
import { SKILL_NAMES } from '../models/player.js';
|
|
4
4
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
5
|
+
import { inLiveCombat } from '../utilities/action-cost.js';
|
|
5
6
|
/**
|
|
6
7
|
* Karma spending: "advance" lists every skill AND attribute with its
|
|
7
8
|
* current rating and the cost to raise it; "advance <name>" pays and
|
|
@@ -27,7 +28,11 @@ export class AdvanceCommand extends Command {
|
|
|
27
28
|
];
|
|
28
29
|
async execute(args = []) {
|
|
29
30
|
const actor = this.actor;
|
|
30
|
-
|
|
31
|
+
// THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
|
|
32
|
+
// inLiveCombat: `inExchange` is a re-entrancy lock that is false for
|
|
33
|
+
// most of a Combat Turn, so this refusal said "firefight" and meant
|
|
34
|
+
// "for the half-second one trade of fire is resolving".
|
|
35
|
+
if (inLiveCombat(this.scene, actor)) {
|
|
31
36
|
return `Mid-exchange is no time for training montages.`;
|
|
32
37
|
}
|
|
33
38
|
if (actor.currentLocation?.roomType !== 'Residence') {
|
|
@@ -64,14 +64,14 @@ export class ArCommand extends Command {
|
|
|
64
64
|
return `Overlay back on -- over nothing. No commlink, no deck, no AR: you're reading the street with your eyes like it's 2050.`;
|
|
65
65
|
}
|
|
66
66
|
const tags = aroTags(this.scene, actor, actor.currentLocation);
|
|
67
|
-
const head = `{
|
|
67
|
+
const head = `{light-blue-fg}The overlay blinks back in.{/light-blue-fg}`;
|
|
68
68
|
// Turning AR back on while silent is the other moment the question
|
|
69
69
|
// lands -- you have just done the thing you were unsure was allowed.
|
|
70
70
|
const note = silentAndSeeingNote(actor);
|
|
71
71
|
if (tags.length === 0) {
|
|
72
|
-
return [head, `{
|
|
72
|
+
return [head, `{light-blue-fg}Nothing here is talking -- a dead patch of street.{/light-blue-fg}`, note].filter(Boolean).join('\n');
|
|
73
73
|
}
|
|
74
|
-
return [head, ...tags.map(t => ` {
|
|
74
|
+
return [head, ...tags.map(t => ` {light-blue-fg}${t}{/light-blue-fg}`), note].filter(Boolean).join('\n');
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
@@ -105,11 +105,11 @@ export class ArosCommand extends Command {
|
|
|
105
105
|
const note = silentAndSeeingNote(actor);
|
|
106
106
|
const tags = aroTags(this.scene, actor, actor.currentLocation);
|
|
107
107
|
if (tags.length === 0) {
|
|
108
|
-
return [`{
|
|
108
|
+
return [`{light-blue-fg}Nothing here is talking -- a dead patch of street.{/light-blue-fg}`, note].filter(Boolean).join('\n');
|
|
109
109
|
}
|
|
110
110
|
return [
|
|
111
|
-
`{
|
|
112
|
-
...tags.map(t => ` {
|
|
111
|
+
`{light-blue-fg}AR OVERLAY:{/light-blue-fg}`,
|
|
112
|
+
...tags.map(t => ` {light-blue-fg}${t}{/light-blue-fg}`),
|
|
113
113
|
note,
|
|
114
114
|
].filter(Boolean).join('\n');
|
|
115
115
|
}
|
|
@@ -4,6 +4,7 @@ import { priceOf, vendorsInRoom, waresOf, shelfNote } from '../utilities/commerc
|
|
|
4
4
|
import { findCatalogItem, ROLE_DOMAINS } from '../utilities/catalog.js';
|
|
5
5
|
import { optionMark } from '../utilities/log-style.js';
|
|
6
6
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
7
|
+
import { inLiveCombat } from '../utilities/action-cost.js';
|
|
7
8
|
/**
|
|
8
9
|
* Diegetic shopping: "browse" (or bare "buy") in a vendor's room reads
|
|
9
10
|
* their shelves with live prices; "buy <item>" slides the nuyen across
|
|
@@ -20,7 +21,11 @@ export class BuyCommand extends Command {
|
|
|
20
21
|
if (actor.plane !== 'meat') {
|
|
21
22
|
return `Commerce is a meat-world ritual -- come back to your body first.`;
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
// THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
|
|
25
|
+
// inLiveCombat: `inExchange` is a re-entrancy lock that is false for
|
|
26
|
+
// most of a Combat Turn, so this refusal said "firefight" and meant
|
|
27
|
+
// "for the half-second one trade of fire is resolving".
|
|
28
|
+
if (inLiveCombat(this.scene, actor)) {
|
|
24
29
|
return `Nobody rings up a sale in a firefight.`;
|
|
25
30
|
}
|
|
26
31
|
const room = actor.currentLocation;
|
|
@@ -12,6 +12,7 @@ import { meetLocationFor } from '../utilities/meet-location.js';
|
|
|
12
12
|
import { asksForDataWork, noDeckForDataWork } from '../utilities/data-work.js';
|
|
13
13
|
import { partyCanDeck } from '../factories/scene-chunks.js';
|
|
14
14
|
import { RecallCommand } from './companions.js';
|
|
15
|
+
import { inLiveCombat } from '../utilities/action-cost.js';
|
|
15
16
|
export class CallCommand extends Command {
|
|
16
17
|
static verb = 'call';
|
|
17
18
|
static description = 'Call someone remotely over a commlink (no need to be in the same room; commlink must be equipped).';
|
|
@@ -86,8 +87,13 @@ export class CallCommand extends Command {
|
|
|
86
87
|
? `Cabs take bodies, not personas -- "jack out" first.`
|
|
87
88
|
: `Cabs take bodies, not ghosts -- "return" first.`;
|
|
88
89
|
}
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
// THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED -- a cab
|
|
91
|
+
// hailed mid-firefight from the web client, with Initiative rolled
|
|
92
|
+
// and a Halloweener swinging). See inLiveCombat: `inExchange`
|
|
93
|
+
// alone let this through for every beat of a Combat Turn that was
|
|
94
|
+
// not a trade of fire resolving right that instant.
|
|
95
|
+
if (inLiveCombat(this.scene, this.actor)) {
|
|
96
|
+
return `Mid-fight is no time to hail a cab. Finish this, then call.`;
|
|
91
97
|
}
|
|
92
98
|
// THE FARE COMES FIRST (playtest 2026-08-26: "I was able to call a
|
|
93
99
|
// taxi before calling Mr. Krow. I shouldn't be able to until I get
|
|
@@ -92,7 +92,7 @@ export class DownloadCommand extends Command {
|
|
|
92
92
|
// is that nobody could find the file should say what it can see.
|
|
93
93
|
const query = args.join(' ').trim();
|
|
94
94
|
if (query.length === 0) {
|
|
95
|
-
const lines = [`{
|
|
95
|
+
const lines = [`{light-blue-fg}Within reach to pull down:{/light-blue-fg}`];
|
|
96
96
|
for (const i of loose)
|
|
97
97
|
lines.push(` ${planeTintItemName(i, actor.plane)} -- loose on the grid here`);
|
|
98
98
|
for (const c of carried)
|
|
@@ -140,11 +140,11 @@ export class DownloadCommand extends Command {
|
|
|
140
140
|
this.scene.updateStatus();
|
|
141
141
|
return fromSprite
|
|
142
142
|
? [
|
|
143
|
-
`{
|
|
143
|
+
`{light-blue-fg}${fromSprite.npc.name} opens its hands and the data pours out of them.{/light-blue-fg}`,
|
|
144
144
|
`You pull {bold}{underline}${item.name}{/underline}{/bold} down onto a chip -- meat-world cargo now.${hint(` (You can "give ${item.name}" to somebody standing in front of you.)`)}`,
|
|
145
145
|
].join('\n')
|
|
146
146
|
: [
|
|
147
|
-
`{
|
|
147
|
+
`{light-blue-fg}You pull {bold}{underline}${item.name}{/underline}{/bold} down off the grid -- written to a chip.{/light-blue-fg}`,
|
|
148
148
|
hint(`(Meat-world cargo now: carry it, stash it, or "give" it to somebody.)`),
|
|
149
149
|
].join('\n');
|
|
150
150
|
}
|
|
@@ -115,7 +115,7 @@ export class EditFileCommand extends Command {
|
|
|
115
115
|
const banner = this.scene.checkErased?.(file.name, actor.name);
|
|
116
116
|
this.scene.updateStatus();
|
|
117
117
|
return [
|
|
118
|
-
`{
|
|
118
|
+
`{light-blue-fg}${file.name} comes apart under your hand -- sectors zeroed, index entry gone, the host's own checksum rewritten to agree. ${hostLabel(room, { capital: true })} no longer has it.{/light-blue-fg}`,
|
|
119
119
|
...(banner ? [banner] : []),
|
|
120
120
|
].join('\n');
|
|
121
121
|
}
|
|
@@ -64,7 +64,7 @@ export class EnterHostCommand extends Command {
|
|
|
64
64
|
const marks = target.hostMarksBy.get(actor.name) ?? 0;
|
|
65
65
|
if (marks < 1 && !target.hostCracked && !target.hostSanctioned) {
|
|
66
66
|
return [
|
|
67
|
-
`{
|
|
67
|
+
`{light-blue-fg}${hostLabel(target, { capital: true })} doesn't know you. Its architecture has no door shaped like your icon.{/light-blue-fg}`,
|
|
68
68
|
hint(`A host opens to anyone holding a MARK on it (p.239) -- "mark ${target.name}" softens it, or "hack" takes one by force.`),
|
|
69
69
|
].join('\n');
|
|
70
70
|
}
|
|
@@ -85,7 +85,7 @@ export class EnterHostCommand extends Command {
|
|
|
85
85
|
// ONE LINE (2026-09-13, "too much text"): the crossing, and the
|
|
86
86
|
// coaching behind the hints toggle.
|
|
87
87
|
return [
|
|
88
|
-
`{
|
|
88
|
+
`{light-blue-fg}You cross into ${hostLabel(target, { capital: false })} -- the outside grid folds shut behind you.{/light-blue-fg}`,
|
|
89
89
|
hint(`(In here there is only what the host keeps. "look" reads it; "exit" puts you back on the grid.)`),
|
|
90
90
|
].filter(Boolean).join('\n');
|
|
91
91
|
}
|
|
@@ -110,14 +110,14 @@ export class EnterHostCommand extends Command {
|
|
|
110
110
|
const over = hostOver(gridVicinity(this.actor)) ?? hostOver(here);
|
|
111
111
|
if (over)
|
|
112
112
|
return over;
|
|
113
|
-
return `{
|
|
113
|
+
return `{light-blue-fg}Thin grid here -- no host stands over ${gridVicinity(this.actor).name}.{/light-blue-fg} ${hint(`Overhead: ${roster} -- "enter <name>" crosses into any of them from here.`)}`;
|
|
114
114
|
}
|
|
115
115
|
const host = resolveHostInReach(rooms, this.actor, named);
|
|
116
116
|
if (host)
|
|
117
117
|
return host;
|
|
118
118
|
const street = Object.values(rooms).find(r => r.name.toLowerCase() === named.toLowerCase());
|
|
119
119
|
if (street)
|
|
120
|
-
return `{
|
|
120
|
+
return `{light-blue-fg}${street.name} has no host over it -- nothing to enter. Overhead: ${roster}.{/light-blue-fg}`;
|
|
121
121
|
return `No host called "${named}" overhead. ${hint(`Overhead: ${roster}.`)}`;
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -152,7 +152,7 @@ export class ExitHostCommand extends Command {
|
|
|
152
152
|
actor.performAction('exits a host', `icon surfacing out of ${host.name}'s architecture`, { quiet: this.scene.isHumanControlled(actor) });
|
|
153
153
|
this.logger.write(`${actor.name} exited the host at ${host.name}.`);
|
|
154
154
|
this.scene.updateStatus();
|
|
155
|
-
return `{
|
|
155
|
+
return `{light-blue-fg}You step back out of ${hostLabel(host, { capital: false })} onto the grid; the host seals behind you. Your body still sits in ${actor.currentLocation.name}.{/light-blue-fg}`;
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
//# sourceMappingURL=enter-host.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BypassCommand } from './bypass-command.js';
|
|
2
2
|
import { gridActionModifier, gridNote } from '../utilities/grids.js';
|
|
3
|
-
import { resolveHostInReach, hostsInReach, hostOver, gridVicinity, roomsInReach, noiseNote } from '../utilities/grid-reach.js';
|
|
3
|
+
import { resolveHostInReach, hostsInReach, hostOver, gridVicinity, roomsInReach, noiseNote, namesHostExactly } from '../utilities/grid-reach.js';
|
|
4
4
|
import { accrueOverwatch } from '../utilities/overwatch.js';
|
|
5
5
|
import { hint } from '../utilities/hints.js';
|
|
6
6
|
import { dropActiveCall } from '../utilities/comm-style.js';
|
|
@@ -86,7 +86,20 @@ export class HackCommand extends BypassCommand {
|
|
|
86
86
|
// working deck in hand (which is also how enemy deckers get YOU).
|
|
87
87
|
// Tried first so a name never falls through to the barrier-bypass
|
|
88
88
|
// path; misses fall through untouched.
|
|
89
|
-
|
|
89
|
+
// ...UNLESS THE PLAYER NAMED A HOST OUTRIGHT (WTLfRHxJEnPmN2ht9:
|
|
90
|
+
// "this keeps matching on Torque's PAN even though I gave it exactly
|
|
91
|
+
// the host name"). tryPanHack resolves with fuzzyPickName over every
|
|
92
|
+
// body in reach, whose tier 2 is substring containment in EITHER
|
|
93
|
+
// direction -- so any runner whose name is contained in the host's
|
|
94
|
+
// (or contains a word of it) swallowed the query before the host
|
|
95
|
+
// pass below ever ran, and no spelling of the host's name could get
|
|
96
|
+
// past them. An exact alias hit settles it here instead; anything
|
|
97
|
+
// less falls through and the old order stands, so a genuinely
|
|
98
|
+
// ambiguous word still reaches the PAN it always did.
|
|
99
|
+
const namedHost = rest.length > 0
|
|
100
|
+
? namesHostExactly(this.rooms ?? this.scene.getRooms(), this.actor, rest.join(' '))
|
|
101
|
+
: undefined;
|
|
102
|
+
if (rest && rest.length > 0 && !namedHost) {
|
|
90
103
|
const panResult = await this.tryPanHack(rest, mode);
|
|
91
104
|
if (panResult)
|
|
92
105
|
return panResult;
|
|
@@ -145,9 +158,34 @@ export class HackCommand extends BypassCommand {
|
|
|
145
158
|
// before allowing the crack would leave an unmarked decker with no
|
|
146
159
|
// legal move against a host at all. That is a playability ruling
|
|
147
160
|
// this engine owns, not something SR5 says.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
161
|
+
// A CRACKED HOST IS NOT A HOST YOU STILL HOLD MARKS ON
|
|
162
|
+
// (spcebZTKN24GYRHXW). What stood here was a blanket refusal --
|
|
163
|
+
// "already cracked open, whatever it guarded is yours to take" --
|
|
164
|
+
// keyed on the room-global crack flag and nothing else. That is
|
|
165
|
+
// wrong twice over.
|
|
166
|
+
//
|
|
167
|
+
// Canon first: marks are per-persona and "only last a single
|
|
168
|
+
// Matrix session ... deleted when you reboot" (p.236, Recognition
|
|
169
|
+
// Keys; p.242, Reboot Device erases them in both directions). A
|
|
170
|
+
// decker who takes a host to three marks and then reboots holds
|
|
171
|
+
// NOTHING on it -- "if they want access again, they must re-place
|
|
172
|
+
// the marks" via Brute Force or Hack on the Fly. The host is never
|
|
173
|
+
// permanently keyed to them.
|
|
174
|
+
//
|
|
175
|
+
// Playability second, and this is what the report actually hit: a
|
|
176
|
+
// reboot mid-run left the persona on 0 marks with the flag still
|
|
177
|
+
// up, so `mark`/`hack` refused, and every mark-gated device action
|
|
178
|
+
// on that node then refused for the opposite reason ("takes orders
|
|
179
|
+
// from 2 marks and you hold 0", Control Device p.238). No verb in
|
|
180
|
+
// the game could hand the marks back. The node was dead to that
|
|
181
|
+
// runner for the rest of the run.
|
|
182
|
+
//
|
|
183
|
+
// The crack's WORLD consequences stay permanent and stay here --
|
|
184
|
+
// maglocks released, vault unsealed, archive listed. Those are
|
|
185
|
+
// facts about the site, not about whose keys are in the lattice.
|
|
186
|
+
// Only the standing is per-persona, and the guard for that is the
|
|
187
|
+
// MAX_MARKS check below, which already says the right thing.
|
|
188
|
+
const alreadyCracked = room.hostCracked;
|
|
151
189
|
// What the host masters, through the room it stands over (models/host.ts).
|
|
152
190
|
const lockedDoors = room.host.maglocks();
|
|
153
191
|
// MARKS ARE THE CURRENCY, AND THIS IS AN OPPOSED TEST NOW
|
|
@@ -279,7 +317,7 @@ export class HackCommand extends BypassCommand {
|
|
|
279
317
|
if (marksNow >= 1)
|
|
280
318
|
lines.push(` One mark is the door: "enter" walks your persona inside (p.239).`);
|
|
281
319
|
if (perception > 0)
|
|
282
|
-
lines.push(` {
|
|
320
|
+
lines.push(` {light-blue-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what this node is (p.240).{/light-blue-fg}`);
|
|
283
321
|
if (forcedDv > 0)
|
|
284
322
|
lines.push(` The forcing burns ${forcedDv} DV into the architecture -- a host has no boards to brick, so it only announces you louder.`);
|
|
285
323
|
lines.push(` ${MAX_MARKS - marksNow} more and its architecture stops asking who you are.${hint(` ("hack for ${MAX_MARKS - marksNow}" reaches for the rest at once -- ${declarationPenalty(MAX_MARKS - marksNow, this.actor.hasQuality(GO_BIG_QUALITY))} dice.)`)}`);
|
|
@@ -314,7 +352,14 @@ export class HackCommand extends BypassCommand {
|
|
|
314
352
|
// its feeds over. Computed AFTER hostCracked is set, so this is
|
|
315
353
|
// the state the player is now standing in, not the one they left.
|
|
316
354
|
const feedsNow = canSnoopFeeds(this.rooms ?? {}, room, this.actor);
|
|
317
|
-
const lines = [
|
|
355
|
+
const lines = [alreadyCracked
|
|
356
|
+
// RE-KEYING A NODE THAT IS ALREADY IN SHREDS. Everything below
|
|
357
|
+
// still runs -- a door the last crack released is already
|
|
358
|
+
// unlocked, a file already spilled is already listed -- so the
|
|
359
|
+
// only thing that changes is the sentence that would otherwise
|
|
360
|
+
// claim you just broke in somewhere you had broken before.
|
|
361
|
+
? `You're back in. The wreckage of the last break-in is still hanging where you left it, and your keys are in the lattice again --`
|
|
362
|
+
: `You're in. The host's sculpted security folds back layer by layer --`];
|
|
318
363
|
// WHICH DOORS ARE ICONS, AND WHICH ARE JUST THE HOST'S OWN WALLS.
|
|
319
364
|
//
|
|
320
365
|
// A door held by a DEVICE has that device's back-reference on it
|
|
@@ -343,13 +388,13 @@ export class HackCommand extends BypassCommand {
|
|
|
343
388
|
lines.push(` ${iconLocks.length} ${iconLocks.length === 1 ? 'lock hangs' : 'locks hang'} off it, still sealed -- each its own icon behind its own rating: ${iconLocks.map(d => d.name).join(', ')}.${hint(` ("hack <lock>" from in here keys one; the host's key is not the door's.)`)}`);
|
|
344
389
|
}
|
|
345
390
|
if (vaultedFiles.length > 0) {
|
|
346
|
-
lines.push(` {
|
|
391
|
+
lines.push(` {light-blue-fg}The data vault unseals -- ${vaultedFiles.map(i => i.name).join(', ')} spill${vaultedFiles.length === 1 ? 's' : ''} into the open.${hint(` Yours to "take".`)}{/light-blue-fg}`);
|
|
347
392
|
}
|
|
348
393
|
// AND WHAT THE CRACK DID NOT REACH: a wired site's data sits on an
|
|
349
394
|
// air-gapped box the host never held (p.233) -- say so, or the next
|
|
350
395
|
// "take" reads as a contradiction.
|
|
351
396
|
if (room.offlineServer && room.inventory.getAllItems().some(i => i.plane === 'matrix')) {
|
|
352
|
-
lines.push(` {
|
|
397
|
+
lines.push(` {light-blue-fg}Nothing on the grid side of the vault -- whatever this place keeps sits on the ${room.offlineServer}, an air-gapped box with no Matrix presence. Your BODY has to be in ${room.name} to cable in (p.233).{/light-blue-fg}`);
|
|
353
398
|
}
|
|
354
399
|
// THE FEEDS ARE A REAL GAIN AND WERE NEVER MENTIONED
|
|
355
400
|
// (BpR5cjhYuvgCyDSZc, 7aNWnDSHeZMYmS2De: "what have I gained?").
|
|
@@ -365,9 +410,13 @@ export class HackCommand extends BypassCommand {
|
|
|
365
410
|
if (lockedDoors.length === 0 && vaultedFiles.length === 0 && !feedsNow) {
|
|
366
411
|
lines.push(` This node guarded nothing -- no maglocks, no vault, no eyes.`);
|
|
367
412
|
}
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
|
|
413
|
+
// WHAT PERSISTS AND WHAT DOES NOT, said in one breath because the
|
|
414
|
+
// old line here said only the first half ("the host stays open
|
|
415
|
+
// from here -- no cracking it twice") and a player who then
|
|
416
|
+
// rebooted had been told, in as many words, that they would not
|
|
417
|
+
// need to do this again. They did. See the alreadyCracked comment
|
|
418
|
+
// above: the damage is permanent, the keys are not (p.236).
|
|
419
|
+
lines.push(` The damage stays done -- the vault does not re-seal behind you. Your MARKS do not last: reboot or jack out and the lattice forgets your keys (p.236), and you key back in from scratch.`);
|
|
371
420
|
if (mode === 'attack') {
|
|
372
421
|
// Brute-force success is ANNOUNCED (canon: the target knows it's
|
|
373
422
|
// under attack and screams to its owner -- it just can't see
|
|
@@ -634,7 +683,7 @@ export class HackCommand extends BypassCommand {
|
|
|
634
683
|
else {
|
|
635
684
|
const perception = freeMatrixPerceptionHits(netHits);
|
|
636
685
|
if (perception > 0)
|
|
637
|
-
lines.push(` {
|
|
686
|
+
lines.push(` {light-blue-fg}And you read it on the way past -- ${perception} Matrix Perception hit${perception === 1 ? '' : 's'} of what it guards (p.240).{/light-blue-fg}`);
|
|
638
687
|
}
|
|
639
688
|
// TWO MARKS COMMAND IT (p.238 Control Device: 1 mark for a Free
|
|
640
689
|
// action, 2 for a Simple, 3 for a Complex -- and unlocking a maglock
|
|
@@ -101,7 +101,7 @@ export class HideCommand extends Command {
|
|
|
101
101
|
this.scene.addWorldEvent(`${host.name}'s ice lost ${actor.name} in the traffic.`);
|
|
102
102
|
this.scene.updateStatus();
|
|
103
103
|
return [
|
|
104
|
-
`{
|
|
104
|
+
`{light-blue-fg}You drop into the host's own traffic and become one more packet in a million. ${hostLabel(room, { capital: true })} loses you.{/light-blue-fg}`,
|
|
105
105
|
` Its ice has nothing to swing at -- but the host is still awake, and Patrol is looking every action now.${hint(` ("silent" keeps a sweep from seeing you for free; "search" is possible while it looks.)`)}`,
|
|
106
106
|
...god.lines,
|
|
107
107
|
].join('\n');
|
|
@@ -71,7 +71,7 @@ export class HopCommand extends Command {
|
|
|
71
71
|
actor.performAction('hops grids', `persona blinking across to ${target.name}`);
|
|
72
72
|
this.logger.write(`${actor.name} hopped to ${target.key} (legal, ${tier}).`);
|
|
73
73
|
this.scene.updateStatus();
|
|
74
|
-
return `{
|
|
74
|
+
return `{light-blue-fg}The sky changes texture and you're on ${target.name}.{/light-blue-fg}${target.userPenalty ? ` ${hint(`Public grid: -2 on everything you do out here.`)}` : ''}${await land()}`;
|
|
75
75
|
}
|
|
76
76
|
// ILLEGAL HOP (p.240): Hack on the Fly against the grid itself.
|
|
77
77
|
const burned = actor.burnedAttributeRefusal('sleaze');
|
|
@@ -105,7 +105,7 @@ export class HopCommand extends Command {
|
|
|
105
105
|
this.logger.write(`${actor.name} hopped to ${target.key} (illegal, ${attempt.hits} v ${defence.hits}).`);
|
|
106
106
|
this.scene.updateStatus();
|
|
107
107
|
return [
|
|
108
|
-
`{
|
|
108
|
+
`{light-blue-fg}You slip through a seam nobody sold you and come up on ${target.name}.{/light-blue-fg}`,
|
|
109
109
|
...bill.lines,
|
|
110
110
|
...(isLooseEnd ? [await land()] : []),
|
|
111
111
|
].join('\n');
|
|
@@ -2,6 +2,7 @@ import { Command } from './command.js';
|
|
|
2
2
|
import { hint } from '../utilities/hints.js';
|
|
3
3
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
4
4
|
import { catalogMetamagics } from '../utilities/catalog.js';
|
|
5
|
+
import { inLiveCombat } from '../utilities/action-cost.js';
|
|
5
6
|
// THE PICKS ARE CATALOG ROWS (kind 'metamagic' on maka-cli.com), read
|
|
6
7
|
// through catalogMetamagics(); the shape is ../metamagics.ts. The four
|
|
7
8
|
// metamagics whose rule is not expressible as dice (overclocking, the
|
|
@@ -15,7 +16,11 @@ export class InitiateCommand extends Command {
|
|
|
15
16
|
static description = 'Initiation/submersion at your hideout (10 + grade x3 karma): each grade lifts Magic/Resonance past 6 and grants a metamagic (or echo). "initiate" lists; "initiate <pick>" pays.';
|
|
16
17
|
async execute(args = []) {
|
|
17
18
|
const actor = this.actor;
|
|
18
|
-
|
|
19
|
+
// THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
|
|
20
|
+
// inLiveCombat: `inExchange` is a re-entrancy lock that is false for
|
|
21
|
+
// most of a Combat Turn, so this refusal said "firefight" and meant
|
|
22
|
+
// "for the half-second one trade of fire is resolving".
|
|
23
|
+
if (inLiveCombat(this.scene, actor)) {
|
|
19
24
|
return `Transcendence waits for the shooting to stop.`;
|
|
20
25
|
}
|
|
21
26
|
if (actor.currentLocation?.roomType !== 'Residence') {
|
|
@@ -4,6 +4,7 @@ import { WARE_GRADES, getGrade, gradedAug, STANDARD_GRADE, formatEssence, } from
|
|
|
4
4
|
import { catalogAugmentations, findAug } from '../utilities/catalog.js';
|
|
5
5
|
import { venueSinCheck, venueBustResponse } from '../utilities/sin.js';
|
|
6
6
|
import { Category } from '../types/shared/item-enum.js';
|
|
7
|
+
import { inLiveCombat } from '../utilities/action-cost.js';
|
|
7
8
|
/**
|
|
8
9
|
* The chrome shop: "install" at a street clinic lists the augmentation
|
|
9
10
|
* catalog against your essence and nuyen; "install <name>" pays, goes
|
|
@@ -21,7 +22,11 @@ export class InstallCommand extends Command {
|
|
|
21
22
|
if (actor.plane !== 'meat') {
|
|
22
23
|
return `Chrome goes into meat. Come back to your body first.`;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
// THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
|
|
26
|
+
// inLiveCombat: `inExchange` is a re-entrancy lock that is false for
|
|
27
|
+
// most of a Combat Turn, so this refusal said "firefight" and meant
|
|
28
|
+
// "for the half-second one trade of fire is resolving".
|
|
29
|
+
if (inLiveCombat(this.scene, actor)) {
|
|
25
30
|
return `Nobody operates in a firefight.`;
|
|
26
31
|
}
|
|
27
32
|
if (!InstallCommand.isClinic(actor.currentLocation)) {
|
|
@@ -387,7 +387,7 @@ export class LookCommand extends Command {
|
|
|
387
387
|
const dim = this.scene.getActorsInRoom(far)
|
|
388
388
|
.find(a => a !== this.actor && a.name.toLowerCase() === target.toLowerCase() && this.actor.canPerceive(a));
|
|
389
389
|
if (dim)
|
|
390
|
-
return `${this.inspectPersona(dim)}\n{
|
|
390
|
+
return `${this.inspectPersona(dim)}\n{light-blue-fg} -- a far icon, ${far.name}${noiseNote(noise)}{/light-blue-fg}`;
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
393
|
// 3. THE HOST, as a thing you can name. gridIcons already renders it
|
|
@@ -404,8 +404,8 @@ export class LookCommand extends Command {
|
|
|
404
404
|
// `deck` reads out your own array.
|
|
405
405
|
const a = namedHost.hostAttributes();
|
|
406
406
|
return [
|
|
407
|
-
`{
|
|
408
|
-
`{
|
|
407
|
+
`{light-blue-fg}${hostIcon}{/light-blue-fg}`,
|
|
408
|
+
`{light-blue-fg} Attack ${a.attack} Sleaze ${a.sleaze} Data Processing ${a.dataProcessing} Firewall ${a.firewall}{/light-blue-fg}`,
|
|
409
409
|
].join('\n');
|
|
410
410
|
}
|
|
411
411
|
}
|
|
@@ -417,7 +417,7 @@ export class LookCommand extends Command {
|
|
|
417
417
|
if (matchesCameraName(target)) {
|
|
418
418
|
const eye = gridIcons(this.scene, this.actor, room).find(i => i.startsWith('◎'));
|
|
419
419
|
if (eye)
|
|
420
|
-
return `{
|
|
420
|
+
return `{light-blue-fg}${eye}{/light-blue-fg}`;
|
|
421
421
|
// Named correctly, and there is simply nothing here to name.
|
|
422
422
|
// Worth its own line: the old answer said the WORDS were wrong,
|
|
423
423
|
// which sent the player hunting for a synonym that never
|
|
@@ -533,7 +533,7 @@ export class LookCommand extends Command {
|
|
|
533
533
|
* there's a deck anywhere in the shape of it.
|
|
534
534
|
*/
|
|
535
535
|
inspectPersona(target) {
|
|
536
|
-
const lines = [`{
|
|
536
|
+
const lines = [`{light-blue-fg}◆ ${target.name} -- a persona on this grid.{/light-blue-fg}`];
|
|
537
537
|
const companion = this.scene.ownerGame?.companions.find(c => c.npc === target);
|
|
538
538
|
if (companion) {
|
|
539
539
|
lines.push(`Yours -- it goes where you go, and it answers when you speak.`);
|
|
@@ -93,7 +93,7 @@ export class MapCommand extends Command {
|
|
|
93
93
|
actor.insideHost
|
|
94
94
|
? `THE GRID -- INSIDE ${hostLabel(actor.insideHost, { capital: true }).toUpperCase()}`
|
|
95
95
|
: `THE GRID -- ${actor.currentGrid ? `on ${actor.currentGrid.name}, ` : ''}jacked in from ${body.name}`,
|
|
96
|
-
`{
|
|
96
|
+
`{light-blue-fg}Black flatland under a black sky. No streets out here -- only icons. The hosts hang overhead; ${actor.insideHost ? '"exit" puts you back under them' : '"go <host>" (or "go <number>") crosses into one you hold a mark on, from anywhere'}.{/light-blue-fg}`,
|
|
97
97
|
];
|
|
98
98
|
if (hosts.length === 0) {
|
|
99
99
|
lines.push(' Nothing overhead -- no host stands over this district.');
|
|
@@ -51,7 +51,7 @@ export class MarkCommand extends Command {
|
|
|
51
51
|
screen: this.screen,
|
|
52
52
|
game: this.game,
|
|
53
53
|
}).execute(args);
|
|
54
|
-
return `{
|
|
54
|
+
return `{light-blue-fg}Placing a mark IS ${action} -- running it (p.238/p.240).{/light-blue-fg}\n${result}`;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
//# sourceMappingURL=mark.js.map
|
|
@@ -2,6 +2,7 @@ import { Command } from './command.js';
|
|
|
2
2
|
import { hint } from '../utilities/hints.js';
|
|
3
3
|
import { catalogQualities } from '../utilities/catalog.js';
|
|
4
4
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
5
|
+
import { inLiveCombat } from '../utilities/action-cost.js';
|
|
5
6
|
/** The qualities on sale: every row that is not retired (a retired one
|
|
6
7
|
* is dormant on its holder's sheet, never on the board). */
|
|
7
8
|
const onSale = () => catalogQualities().filter(q => !q.retired);
|
|
@@ -22,7 +23,11 @@ export class QualityTradeCommand extends Command {
|
|
|
22
23
|
static description = 'Trade qualities with karma at your hideout (SR5e x2 rates): "qualities" lists, "qualities <name>" buys a positive or buys off a negative; "qualities <name> <rating>" buys or raises a rated one.';
|
|
23
24
|
async execute(args = []) {
|
|
24
25
|
const actor = this.actor;
|
|
25
|
-
|
|
26
|
+
// THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
|
|
27
|
+
// inLiveCombat: `inExchange` is a re-entrancy lock that is false for
|
|
28
|
+
// most of a Combat Turn, so this refusal said "firefight" and meant
|
|
29
|
+
// "for the half-second one trade of fire is resolving".
|
|
30
|
+
if (inLiveCombat(this.scene, actor)) {
|
|
26
31
|
return `Reinventing yourself mid-firefight is called dying.`;
|
|
27
32
|
}
|
|
28
33
|
if (actor.currentLocation?.roomType !== 'Residence') {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from './command.js';
|
|
2
2
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
3
3
|
import { hint } from '../utilities/hints.js';
|
|
4
|
+
import { inLiveCombat } from '../utilities/action-cost.js';
|
|
4
5
|
/**
|
|
5
6
|
* Registering (SR5e p.254-256, engine-adapted): make a compiled sprite
|
|
6
7
|
* PERMANENT. An opposed Resonance + REGISTERING test against TWICE the
|
|
@@ -45,7 +46,11 @@ export class RegisterCommand extends Command {
|
|
|
45
46
|
}
|
|
46
47
|
return `No sprite rides with you -- "compile" one first.`;
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
// THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
|
|
50
|
+
// inLiveCombat: `inExchange` is a re-entrancy lock that is false for
|
|
51
|
+
// most of a Combat Turn, so this refusal said "firefight" and meant
|
|
52
|
+
// "for the half-second one trade of fire is resolving".
|
|
53
|
+
if (inLiveCombat(this.scene, actor)) {
|
|
49
54
|
return `Registering takes more focus than the fight will give you.`;
|
|
50
55
|
}
|
|
51
56
|
// THE LOGIC CEILING (p.254-256): "You can register a number of
|
|
@@ -87,6 +87,7 @@ export class SayCommand extends Command {
|
|
|
87
87
|
}
|
|
88
88
|
const message = args?.join(' ');
|
|
89
89
|
const currentPlayer = this.actor;
|
|
90
|
+
let farOff = "";
|
|
90
91
|
const scene = this.context.scene;
|
|
91
92
|
// Speaking out loud ends a sneak (see sneak.ts).
|
|
92
93
|
currentPlayer.sneaking = false;
|
|
@@ -245,8 +246,23 @@ export class SayCommand extends Command {
|
|
|
245
246
|
// (matrix-roster.ts); a body's reach the room, as always.
|
|
246
247
|
if (currentPlayer.plane === 'matrix')
|
|
247
248
|
speakOnTheGrid(this.scene, currentPlayer, message);
|
|
248
|
-
else
|
|
249
|
-
currentPlayer.currentLocation.say(currentPlayer, message);
|
|
249
|
+
else {
|
|
250
|
+
const heard = currentPlayer.currentLocation.say(currentPlayer, message);
|
|
251
|
+
// NOBODY CLOSE ENOUGH is now a thing that can happen, and has to
|
|
252
|
+
// be said (en3p6eCpiRuqsTSKm). Speech used to escalate to a
|
|
253
|
+
// room-wide shout the moment nobody shared your spot, so this
|
|
254
|
+
// case did not exist; with `say` bounded by each listener's
|
|
255
|
+
// hearing range it does, and a player whose words reached nobody
|
|
256
|
+
// would otherwise see their own echo and assume they landed.
|
|
257
|
+
//
|
|
258
|
+
// Only when there was somebody TO miss: an empty room is not a
|
|
259
|
+
// failure to be heard, it is an empty room, and nagging about it
|
|
260
|
+
// every time a runner mutters to themselves would be noise.
|
|
261
|
+
const others = currentPlayer.currentLocation.getActors().filter(a => a !== currentPlayer);
|
|
262
|
+
if ((heard ?? []).length === 0 && others.length > 0 && !(this.actor instanceof NPC)) {
|
|
263
|
+
farOff = `\n Nobody's close enough to catch that.${hint(` ("shout" carries the whole room; "move to <place>" gets you near someone.)`)}`;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
250
266
|
// Talking in the CLIENT's presence is the meet (a real session
|
|
251
267
|
// walked up to Ms. Veil, spoke with plain "say", and the advance
|
|
252
268
|
// never triggered -- only talk-to/call/give were hooked). The
|
|
@@ -294,7 +310,7 @@ export class SayCommand extends Command {
|
|
|
294
310
|
? `Your persona speaks and the grid carries it: ${heard.join(', ')} ${heard.length === 1 ? 'is' : 'are'} on this side of the architecture and hears you. Bodies in the room hear nothing -- you have no voice out there.`
|
|
295
311
|
: `Your words go out over the grid and find no persona to land on. Anyone here in the FLESH hears nothing -- a PAN icon is a commlink in somebody's pocket, not a person you can talk to. ("call <name>" reaches a body.)`;
|
|
296
312
|
}
|
|
297
|
-
return
|
|
313
|
+
return farOff;
|
|
298
314
|
}
|
|
299
315
|
/**
|
|
300
316
|
* "say private" -- the answer to "can anyone hear me right now",
|
|
@@ -313,8 +313,8 @@ export class SearchCommand extends Command {
|
|
|
313
313
|
// tells a player standing next to an offline server that it is
|
|
314
314
|
// there and what it wants from them.
|
|
315
315
|
description += room.offlineServer
|
|
316
|
-
? `\n{
|
|
317
|
-
: `\n{
|
|
316
|
+
? `\n{light-blue-fg}A ${room.offlineServer} sits here, dark and off the network -- no aerial, no marquee, nothing broadcasting. Whatever it holds comes out through a cable and no other way.${hint(` "jack in" RIGHT HERE to touch its files.`)}{/light-blue-fg}`
|
|
317
|
+
: `\n{light-blue-fg}The node here hums with locked files -- whatever's worth taking lives on the grid, inside the host.${hint(` "jack in", then "enter" the host.`)}{/light-blue-fg}`;
|
|
318
318
|
}
|
|
319
319
|
description += describeContainers(room, this.actor);
|
|
320
320
|
description += describeBarriers(room, this.actor);
|
|
@@ -2,6 +2,7 @@ import { Command } from './command.js';
|
|
|
2
2
|
import { hint } from '../utilities/hints.js';
|
|
3
3
|
import { priceOf, fenceValueOf, isSellable, isFence } from '../utilities/commerce.js';
|
|
4
4
|
import { sameSpot, spotOf, spotRefusal } from '../utilities/spots.js';
|
|
5
|
+
import { inLiveCombat } from '../utilities/action-cost.js';
|
|
5
6
|
/**
|
|
6
7
|
* Fencing loot: "sell <item>" in front of a fence (Whisper's pawnshop, or
|
|
7
8
|
* any generated NPC/room in the trade -- see commerce.isFence) quotes 40%
|
|
@@ -18,7 +19,11 @@ export class SellCommand extends Command {
|
|
|
18
19
|
if (actor.plane !== 'meat') {
|
|
19
20
|
return `The fence deals in things a hand can pass over a counter. Come back to your body.`;
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
+
// THE WHOLE FIGHT, not one exchange (hHvR2kxrqAKuuXwED). See
|
|
23
|
+
// inLiveCombat: `inExchange` is a re-entrancy lock that is false for
|
|
24
|
+
// most of a Combat Turn, so this refusal said "firefight" and meant
|
|
25
|
+
// "for the half-second one trade of fire is resolving".
|
|
26
|
+
if (inLiveCombat(this.scene, actor)) {
|
|
22
27
|
return `Mid-firefight is a seller's market for exactly nothing.`;
|
|
23
28
|
}
|
|
24
29
|
const fence = this.scene.getActorsInRoom(actor.currentLocation)
|