@maka/maka-cli 5.196.0 → 5.197.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/buy.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +8 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +58 -9
- 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/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/sell.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +22 -1
- 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/grid-reach.js +82 -5
- 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.197.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') {
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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)) {
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -482,5 +482,26 @@
|
|
|
482
482
|
// hub, characterSheet.hydrate -- silently ATE the lifestyle point of a
|
|
483
483
|
// runner who left fed and slept. The tier is on the save; it is read
|
|
484
484
|
// there now (commerce.lifestyleTierFor, Squatter when unnamed).
|
|
485
|
-
|
|
485
|
+
// 1.56.0 (2026-09-15): TWO FLAGS THAT WERE STANDING IN FOR STATE THEY
|
|
486
|
+
// DO NOT HOLD.
|
|
487
|
+
// - A CRACKED HOST IS NOT A HOST YOU HOLD MARKS ON
|
|
488
|
+
// (spcebZTKN24GYRHXW). `hack`/`mark` refused outright on the
|
|
489
|
+
// room-global hostCracked, so a persona that rebooted mid-run -- which
|
|
490
|
+
// canon wipes marks on (p.236, p.242) -- could never re-place one,
|
|
491
|
+
// while every device on the node went on refusing them for want of
|
|
492
|
+
// marks. Command semantics in shared scenes: the refusal is gone, the
|
|
493
|
+
// standing guard is 3 marks held, a repeat break-in is worded as one,
|
|
494
|
+
// and the host line prints the site's state and the persona's marks
|
|
495
|
+
// apart. The crack's world consequences stay permanent.
|
|
496
|
+
// - NO CAB MID-FIGHT (hHvR2kxrqAKuuXwED). The downtime verbs (buy,
|
|
497
|
+
// sell, install, register, initiate, quality-trade, advance, call
|
|
498
|
+
// taxi) gated on Player.inExchange, a re-entrancy lock that is false
|
|
499
|
+
// for most of a Combat Turn, so each refused for the half-second an
|
|
500
|
+
// exchange resolved and permitted itself the rest of the fight. They
|
|
501
|
+
// ask inLiveCombat (action-cost.ts) now. NEW EMBEDDER SURFACE:
|
|
502
|
+
// Scene.anyLiveEncounter(), which the site reads off the live handle
|
|
503
|
+
// to refuse sideQuest.settle -- on a shared run the cab is intercepted
|
|
504
|
+
// before the engine ever sees it, so the server is the only place
|
|
505
|
+
// that path can be stopped.
|
|
506
|
+
export const ENGINE_VERSION = '1.56.0';
|
|
486
507
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -185,6 +185,30 @@ export class Scene extends AbstractScene {
|
|
|
185
185
|
const enc = this.encounterAt(arenaOf(actor));
|
|
186
186
|
return enc?.has(actor) ? enc : undefined;
|
|
187
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* IS ANYBODY AT THIS TABLE IN A FIGHT, asked across every arena.
|
|
190
|
+
*
|
|
191
|
+
* For the EMBEDDER, not for a verb (hHvR2kxrqAKuuXwED: "I was able to
|
|
192
|
+
* call a taxi in the middle of a fight"). On a shared run the ride
|
|
193
|
+
* home is not an engine verb at all -- both the CLI
|
|
194
|
+
* (utilities/shared-run.ts) and the browser (the site's run-exit.ts)
|
|
195
|
+
* intercept "call taxi" before it reaches the table and call
|
|
196
|
+
* sideQuest.settle, which ends the run for EVERYONE. So the engine's
|
|
197
|
+
* own gate in commands/call.ts, correct as it is, never ran on the
|
|
198
|
+
* path the report came in on, and the site had no way to ask this
|
|
199
|
+
* question without reaching into `encounters`.
|
|
200
|
+
*
|
|
201
|
+
* Every arena, not just the leader's: settling the table yanks the
|
|
202
|
+
* whole party home, so a decker in a host trading with ice is reason
|
|
203
|
+
* enough to refuse even if the leader is standing in an empty street.
|
|
204
|
+
*/
|
|
205
|
+
anyLiveEncounter() {
|
|
206
|
+
for (const enc of this.encounters.values()) {
|
|
207
|
+
if (!enc.ended)
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
188
212
|
/**
|
|
189
213
|
* Who is in the fight when `aggressor` opens on `target`: the two of
|
|
190
214
|
* them, every human and bound shell in the room (the party fights as
|
|
@@ -3,6 +3,42 @@ import { hint } from './hints.js';
|
|
|
3
3
|
export function encounterOf(scene, actor) {
|
|
4
4
|
return scene.encounterFor?.(actor);
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* IS THERE A FIGHT ON, for a verb that simply cannot happen during one.
|
|
8
|
+
*
|
|
9
|
+
* `Player.inExchange` is NOT this question, and a family of verbs was
|
|
10
|
+
* asking it as though it were (hHvR2kxrqAKuuXwED: "I was able to call a
|
|
11
|
+
* taxi in the middle of a fight"). That flag is a re-entrancy lock --
|
|
12
|
+
* true only while one exchange is mid-RESOLUTION, set by
|
|
13
|
+
* CombatExchange.beginExchange and cleared by finishExchange -- so it is
|
|
14
|
+
* false for almost all of a Combat Turn, including the whole of your own
|
|
15
|
+
* Action Phase. Every verb that gated on it alone was therefore refusing
|
|
16
|
+
* during the narration beats of a single trade of fire and permitting
|
|
17
|
+
* itself freely the rest of the fight, while its refusal line said
|
|
18
|
+
* "firefight".
|
|
19
|
+
*
|
|
20
|
+
* The Combat Turn (utilities/combat-turn.ts) is what actually knows a
|
|
21
|
+
* fight is on: an encounter exists for the actor's arena and has not
|
|
22
|
+
* ended. That is the primary test here. The two exchange checks stay
|
|
23
|
+
* underneath it because an exchange can resolve outside an encounter
|
|
24
|
+
* (an NPC AI loop, a scene with no Combat Turn running), and because
|
|
25
|
+
* NPCs have no encounter phase of their own to consult.
|
|
26
|
+
*
|
|
27
|
+
* This is the "no" answer, not the "not yet" answer. Use requirePhase
|
|
28
|
+
* for something that IS legal in a fight but only on your own phase
|
|
29
|
+
* (walking out of the room); use this for something that is not a combat
|
|
30
|
+
* action at all and has no phase that could pay for it -- hailing a cab,
|
|
31
|
+
* ringing up a sale, a training montage.
|
|
32
|
+
*/
|
|
33
|
+
export function inLiveCombat(scene, actor) {
|
|
34
|
+
const enc = encounterOf(scene, actor);
|
|
35
|
+
if (enc && !enc.ended)
|
|
36
|
+
return true;
|
|
37
|
+
if (actor.inExchange)
|
|
38
|
+
return true;
|
|
39
|
+
return scene.isHumanControlled?.(actor) === true
|
|
40
|
+
&& scene.isPlayerExchangeActive?.(actor.name) === true;
|
|
41
|
+
}
|
|
6
42
|
/**
|
|
7
43
|
* The refusal when it is not this actor's Action Phase -- the wording
|
|
8
44
|
* every verb shares, so a player learns one sentence.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { hostConcealedFrom } from './jackpoint.js';
|
|
2
|
-
import { matchesHostName, hostLabel } from './grid-names.js';
|
|
3
|
-
import { fuzzyPickName } from './fuzzy-match.js';
|
|
2
|
+
import { matchesHostName, hostLabel, hostAliases } from './grid-names.js';
|
|
3
|
+
import { fuzzyPickName, normalizeLoose } from './fuzzy-match.js';
|
|
4
|
+
import { MAX_MARKS } from './marks.js';
|
|
4
5
|
/**
|
|
5
6
|
* THE GRID HAS NO STREETS (user canon check 2026-09-02; SR5 pp.217-218,
|
|
6
7
|
* p.246). Jacked in and outside a host, a persona is on its grid: "a
|
|
@@ -88,6 +89,64 @@ export function resolveHostInReach(rooms, actor, query) {
|
|
|
88
89
|
const picked = fuzzyPickName(bare, hosts.map(r => r.name));
|
|
89
90
|
return picked ? hosts.find(r => r.name === picked) : undefined;
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* DOES THIS QUERY NAME A HOST OUTRIGHT -- no fuzz, no guessing.
|
|
94
|
+
*
|
|
95
|
+
* resolveHostInReach ends in a fuzzy fallback, which is right for it:
|
|
96
|
+
* once a player has committed to meaning a host, a near miss should
|
|
97
|
+
* still find one. This is the other question, and it exists because
|
|
98
|
+
* `hack`/`mark` resolve PANs FIRST (WTLfRHxJEnPmN2ht9: "this keeps
|
|
99
|
+
* matching on Torque's PAN even though I gave it exactly the host
|
|
100
|
+
* name"). The PAN pass runs fuzzyPickName over the names of every body
|
|
101
|
+
* in reach, and that matcher's tier 2 is substring containment in
|
|
102
|
+
* either direction -- so a runner standing near anyone whose name is
|
|
103
|
+
* contained in the host's could never reach the host by typing its
|
|
104
|
+
* name, because the PAN pass answered before the host pass was asked.
|
|
105
|
+
*
|
|
106
|
+
* So the caller asks this first: an EXACT alias hit (grid-names.ts's
|
|
107
|
+
* hostAliases, loosely normalized -- case, punctuation and a trailing
|
|
108
|
+
* "host"/"node" all forgiven) means the player named a host and the PAN
|
|
109
|
+
* pass must not get to reinterpret it. Anything less falls through and
|
|
110
|
+
* the old order stands, so a genuinely ambiguous word still reaches the
|
|
111
|
+
* PAN it always did.
|
|
112
|
+
*
|
|
113
|
+
* The bare aliases ("host", "the host", "node", "ice") resolve to the
|
|
114
|
+
* host over the vicinity, exactly as resolveHostInReach resolves them
|
|
115
|
+
* -- one query, one answer, whichever door asks.
|
|
116
|
+
*/
|
|
117
|
+
export function namesHostExactly(rooms, actor, query) {
|
|
118
|
+
const q = query.trim().toLowerCase();
|
|
119
|
+
if (q === '')
|
|
120
|
+
return undefined;
|
|
121
|
+
if (q === 'host' || q === 'the host')
|
|
122
|
+
return hostOver(gridVicinity(actor));
|
|
123
|
+
const near = hostOver(gridVicinity(actor));
|
|
124
|
+
if (near && hostNamed(near, q))
|
|
125
|
+
return near;
|
|
126
|
+
return hostsInReach(rooms, actor).find(r => hostNamed(r, q));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* THE APOSTROPHE, which a player does not type and the matcher insisted
|
|
130
|
+
* on. normalizeLoose SPLITS punctuation rather than dropping it -- right,
|
|
131
|
+
* and load-bearing, because "Mr.Krow" has to become "mr krow" and not
|
|
132
|
+
* "mrkrow". The side effect is that "Torque's Garage" normalizes to
|
|
133
|
+
* "torque s garage", so a player typing the name they READ, minus the
|
|
134
|
+
* apostrophe they did not bother with, missed every alias.
|
|
135
|
+
*
|
|
136
|
+
* So the comparison is tried twice: matchesHostName first (the shared
|
|
137
|
+
* rule, unchanged and still the authority), then again with whitespace
|
|
138
|
+
* squashed out of both sides, which forgives exactly the punctuation the
|
|
139
|
+
* player dropped and nothing else. "torques garage" reaches the host;
|
|
140
|
+
* "torque" -- a whole word shorter -- still does not, which is the line
|
|
141
|
+
* that keeps this from stealing the PAN pass.
|
|
142
|
+
*/
|
|
143
|
+
function hostNamed(room, q) {
|
|
144
|
+
if (matchesHostName(q, room))
|
|
145
|
+
return true;
|
|
146
|
+
const squash = (s) => normalizeLoose(s).replace(/\s+/g, '');
|
|
147
|
+
const squashed = squash(q);
|
|
148
|
+
return squashed.length > 0 && hostAliases(room).some(a => squash(a) === squashed);
|
|
149
|
+
}
|
|
91
150
|
/** Noise between the persona and a room's icons: 0 for the vicinity (and inside a host), -DISTRICT_NOISE dice elsewhere. */
|
|
92
151
|
export function noiseFor(actor, room) {
|
|
93
152
|
if (actor.insideHost)
|
|
@@ -113,9 +172,20 @@ export function noiseNote(noise) {
|
|
|
113
172
|
export function hostLine(room, actor) {
|
|
114
173
|
const inside = actor.insideHost === room;
|
|
115
174
|
const marks = room.hostMarksBy.get(actor.name) ?? 0;
|
|
175
|
+
// STANDING IS TWO FACTS, NOT ONE (spcebZTKN24GYRHXW: "it's saying
|
|
176
|
+
// I've already cracked open (hold 3 marks) but I don't").
|
|
177
|
+
//
|
|
178
|
+
// "cracked open" is the SITE's state -- somebody took this node to
|
|
179
|
+
// three marks and its vault has been hanging open ever since. The
|
|
180
|
+
// marks count is YOUR state, and canon wipes it on every reboot
|
|
181
|
+
// (p.236). The two used to be one string with the crack winning, so a
|
|
182
|
+
// rebooted persona holding nothing read a line that says the place is
|
|
183
|
+
// yours -- while every device on it refused them for want of marks.
|
|
184
|
+
// Print both whenever they disagree.
|
|
185
|
+
const held = `${marks}/${MAX_MARKS} marks`;
|
|
116
186
|
const standing = room.hostSanctioned ? 'haven'
|
|
117
|
-
: room.hostCracked ? 'cracked open'
|
|
118
|
-
: marks > 0 ?
|
|
187
|
+
: room.hostCracked ? (marks >= MAX_MARKS ? 'cracked open' : `cracked open, but you hold ${held}`)
|
|
188
|
+
: marks > 0 ? held
|
|
119
189
|
: 'sealed';
|
|
120
190
|
const alert = room.hostAlert && !room.hostSanctioned ? ', walls UP' : '';
|
|
121
191
|
const over = hostOver(gridVicinity(actor)) === room ? ', over you' : '';
|
|
@@ -133,7 +203,14 @@ export function hostLine(room, actor) {
|
|
|
133
203
|
export function hostDockLine(room, actor, width) {
|
|
134
204
|
const inside = actor.insideHost === room;
|
|
135
205
|
const marks = room.hostMarksBy.get(actor.name) ?? 0;
|
|
136
|
-
|
|
206
|
+
// Same split as hostLine, cut to the dock's width: "open" alone is the
|
|
207
|
+
// site's state and was reading as the persona's. A crack you no longer
|
|
208
|
+
// hold marks on shows the marks, because that is the number every
|
|
209
|
+
// device on this node is about to check (spcebZTKN24GYRHXW).
|
|
210
|
+
const tag = room.hostCracked && marks >= MAX_MARKS ? ' open'
|
|
211
|
+
: room.hostCracked ? ` open ${marks}/${MAX_MARKS}`
|
|
212
|
+
: marks > 0 ? ` ${marks}/${MAX_MARKS}`
|
|
213
|
+
: '';
|
|
137
214
|
const raw = `${inside ? '▣' : '●'} ${room.name} HR${room.hostRating}${tag}`;
|
|
138
215
|
const cut = [...raw].slice(0, Math.max(4, width)).join('');
|
|
139
216
|
return `{lightblue-fg}${cut}{/lightblue-fg}`;
|
|
@@ -1253,6 +1253,14 @@ export class SharedRunSession {
|
|
|
1253
1253
|
catch { /* the name is a courtesy; the warning stands without it */ }
|
|
1254
1254
|
return `The dispatcher picks up -- and you hesitate. You never settled with ${who}: ${owed}¥ still on the table. ("call taxi" again to leave it and go.)`;
|
|
1255
1255
|
}
|
|
1256
|
+
// MID-FIGHT (hHvR2kxrqAKuuXwED). This shortcut skips the engine's
|
|
1257
|
+
// CallCommand exactly as the unclaimed-pay case above does, so
|
|
1258
|
+
// the server carries the refusal and this renders it in the
|
|
1259
|
+
// engine's own words. It arms NOTHING: unlike `unsettled`, no
|
|
1260
|
+
// second call gets you a cab.
|
|
1261
|
+
if (e.error === 'in-combat') {
|
|
1262
|
+
return `Mid-fight is no time to hail a cab. Finish this, then call.`;
|
|
1263
|
+
}
|
|
1256
1264
|
if (e.error === 'bad-status' || e.error === 'interrupted') {
|
|
1257
1265
|
await this.homecoming();
|
|
1258
1266
|
return '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.197.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.",
|