@maka/maka-cli 5.195.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/edge.js +5 -5
- 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/commands/sheet.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +33 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/headless.js +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +12 -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/commerce.js +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +1 -1
- 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
|
|
@@ -15,7 +15,7 @@ export class EdgeCommand extends Command {
|
|
|
15
15
|
return `Luck has left the building, and so have you.`;
|
|
16
16
|
}
|
|
17
17
|
if (actor.edgeBoostArmed) {
|
|
18
|
-
return `Your edge is already burning -- land the strike. (${actor.edgeRemaining}/${actor.
|
|
18
|
+
return `Your edge is already burning -- land the strike. (${actor.edgeRemaining}/${actor.edgeMax} left after this.)`;
|
|
19
19
|
}
|
|
20
20
|
if (actor.edgeRemaining <= 0) {
|
|
21
21
|
return `You're out of luck -- literally. Edge refreshes when the next job starts.`;
|
|
@@ -26,16 +26,16 @@ export class EdgeCommand extends Command {
|
|
|
26
26
|
if (actor.hasQuality('bad-luck')) {
|
|
27
27
|
const die = 1 + Math.floor(Math.random() * 6);
|
|
28
28
|
if (die === 1) {
|
|
29
|
-
this.logger.write(`${actor.name} burned an edge point and BAD LUCK ate it (${actor.edgeRemaining}/${actor.
|
|
29
|
+
this.logger.write(`${actor.name} burned an edge point and BAD LUCK ate it (${actor.edgeRemaining}/${actor.edgeMax} left).`);
|
|
30
30
|
this.scene.updateStatus();
|
|
31
|
-
return `You reach for the luck that got you this far -- and it CURDLES. The point is spent; nothing comes. (Bad Luck. ${actor.edgeRemaining}/${actor.
|
|
31
|
+
return `You reach for the luck that got you this far -- and it CURDLES. The point is spent; nothing comes. (Bad Luck. ${actor.edgeRemaining}/${actor.edgeMax} edge left this job.)`;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
actor.edgeBoostArmed = true;
|
|
35
|
-
this.logger.write(`${actor.name} burned an edge point (${actor.edgeRemaining}/${actor.
|
|
35
|
+
this.logger.write(`${actor.name} burned an edge point (${actor.edgeRemaining}/${actor.edgeMax} left).`);
|
|
36
36
|
this.actor.performAction('pushes the limit', '');
|
|
37
37
|
this.scene.updateStatus();
|
|
38
|
-
return `You reach for the luck that got you this far. The next strike you throw rolls +${actor.edge} dice. (${actor.edgeRemaining}/${actor.
|
|
38
|
+
return `You reach for the luck that got you this far. The next strike you throw rolls +${actor.edge} dice. (${actor.edgeRemaining}/${actor.edgeMax} edge left this job.)`;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
//# sourceMappingURL=edge.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
|
|
@@ -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)
|
|
@@ -105,7 +105,7 @@ export class SheetCommand extends Command {
|
|
|
105
105
|
//
|
|
106
106
|
// The rule is canon and stays. What changes is that the number now
|
|
107
107
|
// states its own ceiling and explains its own shortfall.
|
|
108
|
-
const edgeMax = a.
|
|
108
|
+
const edgeMax = a.edgeMax;
|
|
109
109
|
const edgeSource = a.lifestyleEdgeBonus > 0
|
|
110
110
|
? ` -- ${a.edge} base +${a.lifestyleEdgeBonus} from where you sleep`
|
|
111
111
|
: '';
|
|
@@ -471,5 +471,37 @@
|
|
|
471
471
|
// taxi, no pay. Left one full homecoming, the desk reviews the feeds:
|
|
472
472
|
// +1 Public Awareness. New SaveReason 'cleanup'; new win condition
|
|
473
473
|
// type 'erase'. The site reads Game.footageLeft() off the live handle.
|
|
474
|
-
|
|
474
|
+
// 1.55.1 (2026-09-15): THE EDGE CEILING IS ONE NUMBER. Player.edgeMax
|
|
475
|
+
// (attribute + lifestyle bonus) replaces the sum four readers were
|
|
476
|
+
// writing out by hand, three of them dropping the bonus -- the `edge`
|
|
477
|
+
// command's denominator and the condition report's published `max`,
|
|
478
|
+
// which the play page's HUD rendered as "edge 4/3" on a FULL pool.
|
|
479
|
+
// Underneath them, buildPlayerFromSave stood up a bare Player with no
|
|
480
|
+
// Game to set lifestyleEdgeBonus, so its refreshEdge filled the pool to
|
|
481
|
+
// the plain attribute: every headless boot -- shared runs, the browser
|
|
482
|
+
// hub, characterSheet.hydrate -- silently ATE the lifestyle point of a
|
|
483
|
+
// runner who left fed and slept. The tier is on the save; it is read
|
|
484
|
+
// there now (commerce.lifestyleTierFor, Squatter when unnamed).
|
|
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';
|
|
475
507
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -27,7 +27,7 @@ import { heartbeatPresence, heartbeatPresenceNow } from './utilities/social.js';
|
|
|
27
27
|
import { presenceBeatAllowed } from './utilities/presence-gate.js';
|
|
28
28
|
import { pushSave, burnCloudSaveFor, enqueueBoundPush, flushPushQueue, writeSpill, takeLastMemorial, authToken, clearSpill, hasPendingPush, } from './utilities/cloud-saves.js';
|
|
29
29
|
import { DOCWAGON_TIERS, RESUSC_FEE, DEATH_COMP } from './utilities/docwagon.js';
|
|
30
|
-
import { isFence, LIFESTYLE_TIERS, LIFESTYLE_OPTIONS, canonicalLifestyleTier, canonicalLifestyleOption, lifestyleOptionAllowed } from './utilities/commerce.js';
|
|
30
|
+
import { isFence, LIFESTYLE_TIERS, LIFESTYLE_OPTIONS, canonicalLifestyleTier, canonicalLifestyleOption, lifestyleOptionAllowed, lifestyleTierFor } from './utilities/commerce.js';
|
|
31
31
|
import { reconcileGarage } from './models/home-containers.js';
|
|
32
32
|
import { ownedDrones } from './utilities/owned-drones.js';
|
|
33
33
|
import { attachBrandMark, attachBrandMarkBottomRight } from './utilities/brand-mark.js';
|
|
@@ -2024,7 +2024,7 @@ export default class Game {
|
|
|
2024
2024
|
rentDebt = 0;
|
|
2025
2025
|
/** The active lifestyle tier's row (falls back to Squatter). */
|
|
2026
2026
|
get lifestyle() {
|
|
2027
|
-
return
|
|
2027
|
+
return lifestyleTierFor(this.lifestyleTier);
|
|
2028
2028
|
}
|
|
2029
2029
|
hasLifestyleOption(key) {
|
|
2030
2030
|
return this.lifestyleOptions.includes(key);
|
|
@@ -4776,7 +4776,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
4776
4776
|
if (fed && slept) {
|
|
4777
4777
|
// Good-news status with a rules lesson inside -- hint-gated
|
|
4778
4778
|
// (player request); the HUD's edge count carries the number anyway.
|
|
4779
|
-
const note = hint(`Fed and slept -- you head out SHARP. (Edge ${p.edgeRemaining}/${p.
|
|
4779
|
+
const note = hint(`Fed and slept -- you head out SHARP. (Edge ${p.edgeRemaining}/${p.edgeMax}.)`);
|
|
4780
4780
|
if (note)
|
|
4781
4781
|
lines.push(note);
|
|
4782
4782
|
}
|
|
@@ -22,6 +22,7 @@ import { hostileByStance, enemyInFight, renderRoomLayoutCompact, crewOfTable, ro
|
|
|
22
22
|
import { serializeRoomGrid } from './utilities/room-grid.js';
|
|
23
23
|
import { entrySpotFor, spotOf } from './utilities/spots.js';
|
|
24
24
|
import { equipIntoEmptySlots } from './archetypes.js';
|
|
25
|
+
import { lifestyleTierFor } from './utilities/commerce.js';
|
|
25
26
|
/**
|
|
26
27
|
* The session's Logger: same shape the whole engine already talks to,
|
|
27
28
|
* but every player-visible line becomes a GameEvent through `sink`.
|
|
@@ -186,6 +187,16 @@ export function buildPlayerFromSave(save) {
|
|
|
186
187
|
// has no arrival beat to narrate into; porting it would apply locked
|
|
187
188
|
// fatigue to a rider on someone else's schedule. Per-job flags are the
|
|
188
189
|
// half that is unambiguously wrong to skip.
|
|
190
|
+
// THE LIFESTYLE POINT SURVIVES THE BOOT (mjmcee, 2026-09-15, seen as
|
|
191
|
+
// "edge 4/3" on the web sheet). A Middle+ hideout banks a bonus edge
|
|
192
|
+
// each job, and `lifestyleEdgeBonus` is set by Game from the active
|
|
193
|
+
// tier -- but this path builds a BARE Player, with no Game, so it
|
|
194
|
+
// stood at 0 and the refreshEdge below filled the pool to the plain
|
|
195
|
+
// attribute. Every headless boot -- shared runs, the browser hub, the
|
|
196
|
+
// site's characterSheet.hydrate -- therefore QUIETLY ATE the lifestyle
|
|
197
|
+
// point of a runner who left fed and slept, and published a ceiling one
|
|
198
|
+
// short for everyone else. The tier is on the save; read it.
|
|
199
|
+
player.lifestyleEdgeBonus = lifestyleTierFor(save.game?.lifestyleTier).edgeBonus;
|
|
189
200
|
player.refreshEdge(player.ateThisCycle && player.sleptThisCycle);
|
|
190
201
|
return player;
|
|
191
202
|
}
|
|
@@ -3591,6 +3591,17 @@ export class Player extends AbstractPlayer {
|
|
|
3591
3591
|
// A comfortable home sharpens the luck: Middle+ lifestyles bank a bonus
|
|
3592
3592
|
// edge each job (set by Game from the active tier; 0 for NPCs).
|
|
3593
3593
|
lifestyleEdgeBonus = 0;
|
|
3594
|
+
/**
|
|
3595
|
+
* THE EDGE CEILING -- the pool's real maximum, attribute plus whatever
|
|
3596
|
+
* the lifestyle banks. ONE getter because the sum was being written out
|
|
3597
|
+
* by hand at every call site and three of them dropped the bonus: the
|
|
3598
|
+
* sheet said 4, the `edge` command said /3, and the condition report
|
|
3599
|
+
* published max 3 to the web HUD, which rendered a full pool as "4/3"
|
|
3600
|
+
* (mjmcee, 2026-09-15). refreshEdge fills to THIS, so anything printing
|
|
3601
|
+
* a ceiling must read THIS -- `edge` alone is the attribute, which is
|
|
3602
|
+
* what the burn adds as dice and what karma raises, not the cap.
|
|
3603
|
+
*/
|
|
3604
|
+
get edgeMax() { return this.edge + this.lifestyleEdgeBonus; }
|
|
3594
3605
|
// ==================== Needs (SR5e p.56 / p.172) ====================
|
|
3595
3606
|
// Edge comes back from "a fulfilling meal and a good night's sleep" --
|
|
3596
3607
|
// so eating (use.ts) and sleeping (rest.ts, at a Residence) are tracked
|
|
@@ -3644,7 +3655,7 @@ export class Player extends AbstractPlayer {
|
|
|
3644
3655
|
*/
|
|
3645
3656
|
refreshEdge(wellRested = true) {
|
|
3646
3657
|
if (wellRested) {
|
|
3647
|
-
this.edgeRemaining = this.
|
|
3658
|
+
this.edgeRemaining = this.edgeMax;
|
|
3648
3659
|
}
|
|
3649
3660
|
this.edgeBoostArmed = false;
|
|
3650
3661
|
this.haggledThisJob = false;
|
|
@@ -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.
|
|
@@ -199,6 +199,16 @@ export function canonicalLifestyleTier(raw) {
|
|
|
199
199
|
return 'Middle';
|
|
200
200
|
return LIFESTYLE_TIERS.find(t => t.name.toLowerCase() === wanted)?.name;
|
|
201
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* The tier a stored name names, Squatter when it names nothing -- the
|
|
204
|
+
* same default Game.lifestyle has always used, lifted out of that getter
|
|
205
|
+
* so a BARE Player (buildPlayerFromSave, with no Game around it) can
|
|
206
|
+
* resolve its own lifestyle instead of silently reading a zero bonus.
|
|
207
|
+
*/
|
|
208
|
+
export function lifestyleTierFor(raw) {
|
|
209
|
+
const name = canonicalLifestyleTier(raw) ?? raw;
|
|
210
|
+
return LIFESTYLE_TIERS.find(t => t.name === name) ?? LIFESTYLE_TIERS[1];
|
|
211
|
+
}
|
|
202
212
|
/** One-line flavor note per consumable category, shown on shop shelves. */
|
|
203
213
|
export function shelfNote(item) {
|
|
204
214
|
switch (item.category) {
|
|
@@ -220,7 +220,7 @@ export function buildConditionReport(player, deps) {
|
|
|
220
220
|
// fields, verbatim; edge is BOTH halves or neither on the web side.
|
|
221
221
|
karma: player.karma,
|
|
222
222
|
nuyen: player.currency,
|
|
223
|
-
edge: { remaining: player.edgeRemaining, max: player.
|
|
223
|
+
edge: { remaining: player.edgeRemaining, max: player.edgeMax },
|
|
224
224
|
// THE ROOM'S OTHER DOTS (user: the map becomes a tactical
|
|
225
225
|
// readout). Current room only, read at fire time; spot-seated
|
|
226
226
|
// actors carry `spot` and the web places them on the grid it
|
|
@@ -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.",
|