@maka/maka-cli 5.117.0 → 5.119.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/attack.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +9 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/deck.d.ts +14 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/deck.js +73 -38
- package/bundle/typescript/src/commands/game/sideQuest/commands/deck.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/decompile.js +3 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/decompile.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/docwagon.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/docwagon.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +4 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/download.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.d.ts +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +10 -8
- package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js +17 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.d.ts +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js +72 -20
- package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +8 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +30 -14
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +14 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hop.d.ts +16 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js +95 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/jackpoint.js +5 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/jackpoint.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +24 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/map.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +20 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/reboot.js +7 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/reboot.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js +6 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +8 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/say.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +4 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sin.js +11 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/sin.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +5 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +20 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/take.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +2 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +17 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.d.ts +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js +17 -4
- package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +2 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/game.js +132 -29
- package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/host.d.ts +80 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +98 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +50 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +78 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +133 -3
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +381 -85
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/room.d.ts +40 -9
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +73 -12
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/qualities.d.ts +34 -1
- package/bundle/typescript/src/commands/game/sideQuest/qualities.js +58 -26
- package/bundle/typescript/src/commands/game/sideQuest/qualities.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/types/player-equipment.d.ts +8 -12
- package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +14 -1
- package/bundle/typescript/src/commands/game/sideQuest/types/seed/room-seed.d.ts +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/types/seed/scene-seed.d.ts +2 -0
- package/bundle/typescript/src/commands/game/sideQuest/types/shared/equipment-enum.d.ts +22 -16
- package/bundle/typescript/src/commands/game/sideQuest/types/shared/equipment-enum.js +22 -16
- package/bundle/typescript/src/commands/game/sideQuest/types/shared/equipment-enum.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js +13 -8
- package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.d.ts +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js +8 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +61 -16
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.d.ts +23 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +111 -11
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.d.ts +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js +5 -27
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.d.ts +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +56 -8
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grids.d.ts +72 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grids.js +100 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grids.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/harm-note.d.ts +25 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/harm-note.js +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/harm-note.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/harm-settlement.d.ts +29 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/harm-settlement.js +64 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/harm-settlement.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic-actors.d.ts +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic-actors.js +98 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic-actors.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic.d.ts +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic.js +34 -6
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.d.ts +42 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +75 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-roster.d.ts +62 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-roster.js +97 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-roster.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.d.ts +1 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +85 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.d.ts +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +39 -23
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.d.ts +16 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.d.ts +2 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js +23 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getQuality, resolveQualityKey } from '../qualities.js';
|
|
1
2
|
import { isAspectKey } from '../aspects.js';
|
|
2
3
|
import { hint } from '../utilities/hints.js';
|
|
3
4
|
import { Category } from '../types/shared/item-enum.js';
|
|
@@ -11,45 +12,22 @@ import { physicalLimit } from '../utilities/grapple.js';
|
|
|
11
12
|
import { astralLimit } from '../utilities/limits.js';
|
|
12
13
|
import { Inventory } from './inventory.js';
|
|
13
14
|
import { AbstractPlayer } from '../types/shared/abstracts.js';
|
|
14
|
-
export const STANCE_CYCLE = ['manual', '
|
|
15
|
+
export const STANCE_CYCLE = ['manual', 'non-lethal', 'lethal'];
|
|
16
|
+
/** The old names, still accepted from saves and the stance command. */
|
|
17
|
+
export const STANCE_ALIASES = { guarded: 'non-lethal', aggressive: 'lethal', nonlethal: 'non-lethal', 'non lethal': 'non-lethal' };
|
|
18
|
+
export function normalizeStance(v) {
|
|
19
|
+
const k = (v ?? 'manual').toLowerCase();
|
|
20
|
+
return STANCE_ALIASES[k] ?? (STANCE_CYCLE.includes(k) ? k : 'manual');
|
|
21
|
+
}
|
|
15
22
|
/** Meat and a jumped-in drone share the physical world. */
|
|
16
23
|
export function isPhysicalPlane(plane) {
|
|
17
24
|
return plane === 'meat' || plane === 'drone';
|
|
18
25
|
}
|
|
19
|
-
/**
|
|
20
|
-
* ARE THESE TWO STANDING ON THE SAME SIDE OF A HOST'S WALL?
|
|
21
|
-
*
|
|
22
|
-
* The companion to fileReach, and it exists for the same reason: the
|
|
23
|
-
* boundary was being asked about in more than one place and the copies
|
|
24
|
-
* disagreed. fileReach settled it for FILES; nothing settled it for
|
|
25
|
-
* ICONS, so `look` honoured insideHost while every verb that picks a
|
|
26
|
-
* target read the room's raw actor list and saw straight through the
|
|
27
|
-
* wall. A technomancer standing inside a host was told "Empty
|
|
28
|
-
* architecture -- turning geometry and nothing in it worth the trip" by
|
|
29
|
-
* one surface and offered those same two IC as spike targets by the
|
|
30
|
-
* next.
|
|
31
|
-
*
|
|
32
|
-
* CANON MAKES THE PANEL BINDING, not the target list. Inside a host you
|
|
33
|
-
* see and interact with the icons inside it and not the ones outside,
|
|
34
|
-
* and from the grid the inside is unreachable (p.246) -- and threading
|
|
35
|
-
* adds its own half of the same rule: "You can only use complex forms
|
|
36
|
-
* on icons you've spotted" (p.251). An icon the room refuses to name is
|
|
37
|
-
* an icon nobody has spotted, so targeting it was never legal.
|
|
38
|
-
*
|
|
39
|
-
* BOTH IN THE SAME ROOM IS THE PRECONDITION, and every caller has it --
|
|
40
|
-
* these are room-scoped target lists. Given that, comparing insideHost
|
|
41
|
-
* directly is the whole rule: both undefined means both are out on the
|
|
42
|
-
* open grid, both equal to the room means both are inside its
|
|
43
|
-
* architecture. There is deliberately no third state.
|
|
44
|
-
*
|
|
45
|
-
* MEAT AND ASTRAL ACTORS PASS TRIVIALLY, which is correct and not an
|
|
46
|
-
* oversight: insideHost is persona state, so a body in the room has
|
|
47
|
-
* none. The plane filters the callers already apply are what keep those
|
|
48
|
-
* actors out of a Matrix target list; this function answers one question
|
|
49
|
-
* and is not a second plane check.
|
|
50
|
-
*/
|
|
51
26
|
export function sameHostSide(actor, other) {
|
|
52
|
-
|
|
27
|
+
// A real Player answers with its Host; a bare fixture ({ insideHost })
|
|
28
|
+
// answers with whatever it carries. Both sides read the same way.
|
|
29
|
+
const sideOf = (p) => p.hostInside ?? p.insideHost;
|
|
30
|
+
return sideOf(actor) === sideOf(other);
|
|
53
31
|
}
|
|
54
32
|
export const SKILL_NAMES = [
|
|
55
33
|
'blades', 'clubs', 'unarmed', 'archery', 'free-fall',
|
|
@@ -426,7 +404,68 @@ export class Player extends AbstractPlayer {
|
|
|
426
404
|
* Session state like hostAlert and linkLockedBy: leaving the Matrix
|
|
427
405
|
* ends it, so nothing here is persisted.
|
|
428
406
|
*/
|
|
429
|
-
|
|
407
|
+
/**
|
|
408
|
+
* WHERE THE PERSONA IS (increment 2 of the Matrix position model): on a
|
|
409
|
+
* grid, or inside a host it entered from one. Session-only, like
|
|
410
|
+
* bodyRoom. The body stays in currentLocation on every plane -- the
|
|
411
|
+
* persona never writes it. The setter fires the live-sheet beat, which
|
|
412
|
+
* is the beat the currentLocation setter used to fire on enter/exit.
|
|
413
|
+
* `grid` may be unknown (fixtures, and increment 2 itself): unknown is
|
|
414
|
+
* not "another grid", so it costs nothing.
|
|
415
|
+
*/
|
|
416
|
+
_matrixPosition;
|
|
417
|
+
get matrixPosition() { return this._matrixPosition; }
|
|
418
|
+
set matrixPosition(v) {
|
|
419
|
+
const same = v === this._matrixPosition
|
|
420
|
+
|| (v !== undefined && this._matrixPosition !== undefined && v.kind === this._matrixPosition.kind
|
|
421
|
+
&& (v.kind === 'host' ? v.host === this._matrixPosition.host
|
|
422
|
+
: v.grid?.key === this._matrixPosition.grid?.key));
|
|
423
|
+
if (same)
|
|
424
|
+
return;
|
|
425
|
+
this._matrixPosition = v;
|
|
426
|
+
this.noteConditionChanged();
|
|
427
|
+
}
|
|
428
|
+
/** The grid the persona rides, when it is on one. */
|
|
429
|
+
get currentGrid() {
|
|
430
|
+
return this._matrixPosition?.kind === 'grid' ? this._matrixPosition.grid : undefined;
|
|
431
|
+
}
|
|
432
|
+
/** The host the persona is inside, when it is inside one. */
|
|
433
|
+
get hostInside() {
|
|
434
|
+
return this._matrixPosition?.kind === 'host' ? this._matrixPosition.host : undefined;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* @deprecated compat through increment 4 -- read hostInside / write
|
|
438
|
+
* matrixPosition. The room a host stands over, when inside one. The
|
|
439
|
+
* setter still builds the position so every old writer keeps working.
|
|
440
|
+
*/
|
|
441
|
+
_compatInsideRoom;
|
|
442
|
+
get insideHost() { return this.hostInside?.over ?? this._compatInsideRoom; }
|
|
443
|
+
set insideHost(room) {
|
|
444
|
+
if (room) {
|
|
445
|
+
// A room you are inside of IS a host. Fixtures (and older callers)
|
|
446
|
+
// set insideHost on a plain Room; install the Host they meant.
|
|
447
|
+
if (!room.host)
|
|
448
|
+
room.hasNode = true;
|
|
449
|
+
const host = room.host;
|
|
450
|
+
if (!host) {
|
|
451
|
+
// Not a real Room (a fixture stub): remember it so the getter and
|
|
452
|
+
// sameHostSide still answer, without a position to stand on.
|
|
453
|
+
this._compatInsideRoom = room;
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this._compatInsideRoom = undefined;
|
|
457
|
+
this.matrixPosition = { kind: 'host', host, from: this.currentGrid ?? this.lastGrid };
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
this._compatInsideRoom = undefined;
|
|
461
|
+
if (this._matrixPosition?.kind === 'host')
|
|
462
|
+
this.matrixPosition = { kind: 'grid', grid: this._matrixPosition.from };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/** The grid last stood on, so an exit built through the old setter returns there. */
|
|
466
|
+
get lastGrid() {
|
|
467
|
+
return this._matrixPosition?.kind === 'host' ? this._matrixPosition.from : this._matrixPosition?.grid;
|
|
468
|
+
}
|
|
430
469
|
/**
|
|
431
470
|
* MATRIX ATTRIBUTES BURNED OFF BY IC (p.248): Acid eats Firewall,
|
|
432
471
|
* Binder eats Data Processing, Jammer eats Attack, Marker eats Sleaze.
|
|
@@ -544,7 +583,29 @@ export class Player extends AbstractPlayer {
|
|
|
544
583
|
return true;
|
|
545
584
|
if (this.hasAug('datajack'))
|
|
546
585
|
return true;
|
|
547
|
-
return this.
|
|
586
|
+
return this.dniSource() !== null;
|
|
587
|
+
}
|
|
588
|
+
/** True for the one Electronics item that is a trode net (p.439-440). */
|
|
589
|
+
static isTrodeNet(item) {
|
|
590
|
+
return item.category === Category.Electronics && /\btrodes?\b/i.test(item.name);
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* WHAT CARRIES THE DIRECT NEURAL INTERFACE, for the equipment panel and
|
|
594
|
+
* the jack-in gate: a living persona, an implanted datajack, a trode net
|
|
595
|
+
* worn on the head, or one still in the bag (it works from there too --
|
|
596
|
+
* the engine has never made the player put it on, and a net in the bag
|
|
597
|
+
* was the only place it could live before the head slot existed).
|
|
598
|
+
*/
|
|
599
|
+
dniSource() {
|
|
600
|
+
if (this.isTechnomancer())
|
|
601
|
+
return { kind: 'technomancer' };
|
|
602
|
+
if (this.hasAug('datajack'))
|
|
603
|
+
return { kind: 'datajack' };
|
|
604
|
+
const worn = this._equipment.head?.trodes;
|
|
605
|
+
if (worn)
|
|
606
|
+
return { kind: 'trodes', worn: true, item: worn };
|
|
607
|
+
const bagged = this._inventory.getAllItems().find(i => Player.isTrodeNet(i));
|
|
608
|
+
return bagged ? { kind: 'trodes', worn: false, item: bagged } : null;
|
|
548
609
|
}
|
|
549
610
|
/**
|
|
550
611
|
* THE PRICE OF SILENCE (canon p.235-236 "Running Silent",
|
|
@@ -647,7 +708,13 @@ export class Player extends AbstractPlayer {
|
|
|
647
708
|
dataProcessing: 'Data Processing', firewall: 'Firewall',
|
|
648
709
|
};
|
|
649
710
|
/** The undamaged array, before any IC reduction. */
|
|
711
|
+
/** Set on an IC actor (utilities/ic-actors.ts): "IC uses the Matrix attributes of its host" (p.247). */
|
|
712
|
+
icHost;
|
|
713
|
+
/** The IC kind an IC actor was launched as -- the name the host's running list carries. */
|
|
714
|
+
icKind;
|
|
650
715
|
baseMatrixAttribute(kind) {
|
|
716
|
+
if (this.icHost)
|
|
717
|
+
return this.icHost.attributes()[kind];
|
|
651
718
|
if (this.isTechnomancer()) {
|
|
652
719
|
switch (kind) {
|
|
653
720
|
case 'attack': return this.charisma;
|
|
@@ -922,7 +989,13 @@ export class Player extends AbstractPlayer {
|
|
|
922
989
|
/** Routes Matrix combat damage into the jacked-in deck's hardware track. */
|
|
923
990
|
takeMatrixDamage(boxes) {
|
|
924
991
|
const deck = this.getAnyCyberdeck();
|
|
925
|
-
|
|
992
|
+
const taken = deck ? deck.takeDeckDamage(boxes) : 0;
|
|
993
|
+
// DECK DAMAGE BEATS (web report 2026-09-02: "the damage to the deck is not moving in realtime"):
|
|
994
|
+
// the Item has no path to its owner, so the owner fires here, and the
|
|
995
|
+
// exchange and rest fire on their own deck writes.
|
|
996
|
+
if (taken > 0)
|
|
997
|
+
this.noteConditionChanged();
|
|
998
|
+
return taken;
|
|
926
999
|
}
|
|
927
1000
|
get matrixCrashed() {
|
|
928
1001
|
return this.getAnyCyberdeck()?.isBricked ?? false;
|
|
@@ -1145,7 +1218,23 @@ export class Player extends AbstractPlayer {
|
|
|
1145
1218
|
* the only quality that edits an attribute (RAW raises the Edge ceiling;
|
|
1146
1219
|
* with no attribute point-buy here, the point lands directly).
|
|
1147
1220
|
*/
|
|
1148
|
-
|
|
1221
|
+
/** Rating per rated quality (qualities.ts maxRating); absent = 1. */
|
|
1222
|
+
qualityRatings = new Map();
|
|
1223
|
+
/** The rating held, 0 when the quality is not held; a flat quality reads 1. */
|
|
1224
|
+
qualityRating(key) {
|
|
1225
|
+
if (!this.qualities.has(key))
|
|
1226
|
+
return 0;
|
|
1227
|
+
return this.qualityRatings.get(key) ?? 1;
|
|
1228
|
+
}
|
|
1229
|
+
addQuality(rawKey, rating) {
|
|
1230
|
+
// A legacy "<key>-1" resolves to the rated key at 1 (qualities.ts).
|
|
1231
|
+
const resolved = resolveQualityKey(rawKey);
|
|
1232
|
+
const key = resolved.key;
|
|
1233
|
+
const q = getQuality(key);
|
|
1234
|
+
if (q?.maxRating) {
|
|
1235
|
+
const r = Math.max(1, Math.min(q.maxRating, Math.floor(rating ?? resolved.rating)));
|
|
1236
|
+
this.qualityRatings.set(key, r);
|
|
1237
|
+
}
|
|
1149
1238
|
if (this.qualities.has(key))
|
|
1150
1239
|
return;
|
|
1151
1240
|
this.qualities.add(key);
|
|
@@ -1371,7 +1460,8 @@ export class Player extends AbstractPlayer {
|
|
|
1371
1460
|
ears: null,
|
|
1372
1461
|
mouth: null,
|
|
1373
1462
|
nose: null,
|
|
1374
|
-
commlink: null
|
|
1463
|
+
commlink: null,
|
|
1464
|
+
trodes: null
|
|
1375
1465
|
},
|
|
1376
1466
|
torso: {
|
|
1377
1467
|
clothing: null,
|
|
@@ -1379,31 +1469,26 @@ export class Player extends AbstractPlayer {
|
|
|
1379
1469
|
armorCoat: null,
|
|
1380
1470
|
backpack: null,
|
|
1381
1471
|
tacticalRig: null,
|
|
1382
|
-
|
|
1383
|
-
rightShoulder: null,
|
|
1472
|
+
shoulder: null,
|
|
1384
1473
|
bandolier: null
|
|
1385
1474
|
},
|
|
1386
1475
|
legs: {
|
|
1387
1476
|
pants: null,
|
|
1388
1477
|
thighRig: null,
|
|
1389
|
-
|
|
1390
|
-
rightHip: null
|
|
1478
|
+
hip: null
|
|
1391
1479
|
},
|
|
1392
1480
|
feet: {
|
|
1393
1481
|
boots: null,
|
|
1394
|
-
|
|
1395
|
-
rightAnkle: null
|
|
1482
|
+
ankle: null
|
|
1396
1483
|
},
|
|
1397
1484
|
arms: {
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
leftForearm: null,
|
|
1401
|
-
rightForearm: null,
|
|
1485
|
+
bicep: null,
|
|
1486
|
+
forearm: null,
|
|
1402
1487
|
cyberware: null
|
|
1403
1488
|
},
|
|
1404
1489
|
gloves: {
|
|
1405
|
-
|
|
1406
|
-
|
|
1490
|
+
gloves: null,
|
|
1491
|
+
wrist: null
|
|
1407
1492
|
},
|
|
1408
1493
|
leftHand: {
|
|
1409
1494
|
weapon: null,
|
|
@@ -1598,8 +1683,143 @@ export class Player extends AbstractPlayer {
|
|
|
1598
1683
|
this.logger.write("Failed to add item to inventory due to weight limits.");
|
|
1599
1684
|
throw new Error("The item is too heavy to add to your inventory.");
|
|
1600
1685
|
}
|
|
1686
|
+
// A PROGRAM CHIP IS CONSUMED BY THE DECK (p.227, p.243): programs are
|
|
1687
|
+
// files in deck storage, not gear in the pack. With a deck on you the
|
|
1688
|
+
// chip installs and is gone; with none it waits in the bag for one.
|
|
1689
|
+
// A PAPER GOES ON THE COMMLINK (p.443): with a device on you it lands
|
|
1690
|
+
// there and never touches the pack; a license links to the SIN you
|
|
1691
|
+
// present. With no device it waits in the bag like a chip does.
|
|
1692
|
+
if (item.isPaper()) {
|
|
1693
|
+
const device = this.paperDevice();
|
|
1694
|
+
if (device) {
|
|
1695
|
+
this.filePaper(device, item);
|
|
1696
|
+
this.logger.write(`${item.name} filed on ${device.name}${item.linkedSin ? ` (linked to ${item.linkedSin})` : ''}.`);
|
|
1697
|
+
this.noteConditionChanged();
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
const key = item.programKey ?? item.softKey;
|
|
1702
|
+
const deck = key ? (item.softKey ? this.softDevice() : this.getAnyCyberdeck()) : undefined;
|
|
1703
|
+
if (key && deck) {
|
|
1704
|
+
const fresh = deck.installProgram(key);
|
|
1705
|
+
this.logger.write(`${item.name} ${fresh ? 'installed into' : 'already on'} ${deck.name} -- chip consumed.`);
|
|
1706
|
+
this.noteConditionChanged();
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1601
1709
|
this._inventory.addItem(item);
|
|
1602
1710
|
this.logger.write(`Added ${item.name} to ${this.name}'s inventory.`);
|
|
1711
|
+
// A new deck takes every chip that was waiting for it.
|
|
1712
|
+
if (item.isCyberdeck() || item.category === Category.Commlink) {
|
|
1713
|
+
this.sweepProgramChipsIntoDeck();
|
|
1714
|
+
this.sweepPapersOntoDevice();
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
/** Where papers live (p.443): the commlink, else the deck -- the PAN's master. */
|
|
1718
|
+
paperDevice() {
|
|
1719
|
+
return this.commlinks()[0] ?? this.getAnyCyberdeck();
|
|
1720
|
+
}
|
|
1721
|
+
/** Every device that can hold papers, commlinks first. */
|
|
1722
|
+
paperDevices() {
|
|
1723
|
+
const deck = this.getAnyCyberdeck();
|
|
1724
|
+
return [...this.commlinks(), ...(deck ? [deck] : [])];
|
|
1725
|
+
}
|
|
1726
|
+
/** Every paper on every device. */
|
|
1727
|
+
papers() {
|
|
1728
|
+
return this.paperDevices().flatMap(d => d.papers);
|
|
1729
|
+
}
|
|
1730
|
+
/** Files a paper on a device; a license links to the presented fake SIN (utilities/sin.ts resolves "presented"). */
|
|
1731
|
+
filePaper(device, paper) {
|
|
1732
|
+
if (paper.category === Category.License && !paper.linkedSin) {
|
|
1733
|
+
const sins = this.papers().filter(p => p.category === Category.FakeSIN);
|
|
1734
|
+
const chosen = sins.find(p => p.name === this.activeSin) ?? sins[0];
|
|
1735
|
+
if (chosen)
|
|
1736
|
+
paper.linkedSin = chosen.name;
|
|
1737
|
+
}
|
|
1738
|
+
device.papers.push(paper);
|
|
1739
|
+
// A SIN arriving after an unlinked license takes it.
|
|
1740
|
+
if (paper.category === Category.FakeSIN) {
|
|
1741
|
+
for (const lic of this.papers())
|
|
1742
|
+
if (lic.category === Category.License && !lic.linkedSin)
|
|
1743
|
+
lic.linkedSin = paper.name;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
/** Burns a fake SIN: gone from its device, and every license linked to it goes with it (p.443). Returns the licenses lost. */
|
|
1747
|
+
burnPaper(sin) {
|
|
1748
|
+
const lost = [];
|
|
1749
|
+
for (const device of this.paperDevices()) {
|
|
1750
|
+
device.papers = device.papers.filter(p => {
|
|
1751
|
+
if (p === sin)
|
|
1752
|
+
return false;
|
|
1753
|
+
if (p.category === Category.License && p.linkedSin === sin.name) {
|
|
1754
|
+
lost.push(p);
|
|
1755
|
+
return false;
|
|
1756
|
+
}
|
|
1757
|
+
return true;
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
if (this.activeSin === sin.name)
|
|
1761
|
+
this.activeSin = undefined;
|
|
1762
|
+
this.noteConditionChanged();
|
|
1763
|
+
return lost;
|
|
1764
|
+
}
|
|
1765
|
+
/** Every paper in the pack files onto the carried device; returns how many moved. */
|
|
1766
|
+
sweepPapersOntoDevice() {
|
|
1767
|
+
const device = this.paperDevice();
|
|
1768
|
+
if (!device)
|
|
1769
|
+
return 0;
|
|
1770
|
+
let n = 0;
|
|
1771
|
+
// SINs first, so a loose license has one to link to.
|
|
1772
|
+
const loose = [...this._inventory.getAllItems()].filter(i => i.isPaper())
|
|
1773
|
+
.sort((a, b) => (a.category === Category.License ? 1 : 0) - (b.category === Category.License ? 1 : 0));
|
|
1774
|
+
for (const paper of loose) {
|
|
1775
|
+
this._inventory.removeItem(paper.name);
|
|
1776
|
+
this.filePaper(device, paper);
|
|
1777
|
+
n++;
|
|
1778
|
+
}
|
|
1779
|
+
if (n > 0)
|
|
1780
|
+
this.noteConditionChanged();
|
|
1781
|
+
return n;
|
|
1782
|
+
}
|
|
1783
|
+
/** The commlinks on this runner: worn on the head, or in the pack. */
|
|
1784
|
+
commlinks() {
|
|
1785
|
+
const worn = this._equipment.head?.commlink;
|
|
1786
|
+
return [...(worn ? [worn] : []), ...this._inventory.getAllItems().filter(i => i.category === Category.Commlink)];
|
|
1787
|
+
}
|
|
1788
|
+
/** Where a data soft lives (p.441-442): the deck if there is one, else a commlink. */
|
|
1789
|
+
softDevice() {
|
|
1790
|
+
return this.getAnyCyberdeck() ?? this.commlinks()[0];
|
|
1791
|
+
}
|
|
1792
|
+
/** A soft in the storage of any device on you -- a chip in the bag is not installed. */
|
|
1793
|
+
hasSoft(key) {
|
|
1794
|
+
const deck = this.getAnyCyberdeck();
|
|
1795
|
+
return [...(deck ? [deck] : []), ...this.commlinks()].some(d => d.hasStored(key));
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* THE SOCIAL LIMIT (SR5 p.100-101): [(Charisma x 2) + Willpower + Essence]
|
|
1799
|
+
* / 3, rounded up, with Essence rounded up first (p.95, p.100 -- the
|
|
1800
|
+
* rounding rule is cited; the table row itself the RAG did not return,
|
|
1801
|
+
* see rag-table-rows-missing). A Shopsoft adds one when buying or
|
|
1802
|
+
* selling what it covers (p.442) -- see commands/haggle.ts.
|
|
1803
|
+
*/
|
|
1804
|
+
get socialLimit() {
|
|
1805
|
+
return Math.ceil((this.charisma * 2 + this.willpower + Math.ceil(this.essence - 1e-9)) / 3);
|
|
1806
|
+
}
|
|
1807
|
+
/** Every program chip in the pack installs into the carried deck; returns how many chips were consumed. */
|
|
1808
|
+
sweepProgramChipsIntoDeck() {
|
|
1809
|
+
const deck = this.getAnyCyberdeck();
|
|
1810
|
+
const softHome = this.softDevice();
|
|
1811
|
+
let n = 0;
|
|
1812
|
+
for (const chip of [...this._inventory.getAllItems()]) {
|
|
1813
|
+
const device = chip.programKey ? deck : chip.softKey ? softHome : undefined;
|
|
1814
|
+
if (!device)
|
|
1815
|
+
continue;
|
|
1816
|
+
device.installProgram((chip.programKey ?? chip.softKey));
|
|
1817
|
+
this._inventory.removeItem(chip.name);
|
|
1818
|
+
n++;
|
|
1819
|
+
}
|
|
1820
|
+
if (n > 0)
|
|
1821
|
+
this.noteConditionChanged();
|
|
1822
|
+
return n;
|
|
1603
1823
|
}
|
|
1604
1824
|
removeInventory(item) {
|
|
1605
1825
|
if (this._inventory.removeItem(item.name)) {
|
|
@@ -1616,7 +1836,7 @@ export class Player extends AbstractPlayer {
|
|
|
1616
1836
|
if (!this.canEquip(item)) {
|
|
1617
1837
|
return `You can't equip ${item.name}.`;
|
|
1618
1838
|
}
|
|
1619
|
-
const slotInfo = this.
|
|
1839
|
+
const slotInfo = this.slotForItem(item);
|
|
1620
1840
|
if (!slotInfo) {
|
|
1621
1841
|
return `${item.name} doesn't fit any known equipment slot.`;
|
|
1622
1842
|
}
|
|
@@ -1629,7 +1849,7 @@ export class Player extends AbstractPlayer {
|
|
|
1629
1849
|
let swapNotice = '';
|
|
1630
1850
|
if (previousItem) {
|
|
1631
1851
|
if (previousItem.name === item.name) {
|
|
1632
|
-
return `You already have ${item.name}
|
|
1852
|
+
return `You already have ${item.name} on -- ${Player.slotWord(group, slot)}.`;
|
|
1633
1853
|
}
|
|
1634
1854
|
this.unequipBySlot(group, slot);
|
|
1635
1855
|
swapNotice = ` (unequipped ${previousItem.name})`;
|
|
@@ -1643,7 +1863,21 @@ export class Player extends AbstractPlayer {
|
|
|
1643
1863
|
this.weaponDrawn = false;
|
|
1644
1864
|
return `You slide ${item.name} into your holster.${swapNotice}${hint(` "brandish" when you want it in hand.`)}`;
|
|
1645
1865
|
}
|
|
1646
|
-
return `You
|
|
1866
|
+
return `You put on ${item.name} -- ${Player.slotWord(group, slot)}.${swapNotice}`;
|
|
1867
|
+
}
|
|
1868
|
+
/** The word a player would use for a slot -- never the key ("body armor", not "torso.armorVest"). */
|
|
1869
|
+
static SLOT_WORDS = {
|
|
1870
|
+
head: { eyes: 'eyes', skull: 'head', ears: 'ears', mouth: 'face', nose: 'nose', commlink: 'commlink', trodes: 'trode net' },
|
|
1871
|
+
torso: { clothing: 'clothes', armorVest: 'body armor', armorCoat: 'coat', backpack: 'backpack', tacticalRig: 'rig', shoulder: 'shoulder', bandolier: 'bandolier' },
|
|
1872
|
+
legs: { pants: 'legs', thighRig: 'thigh rig', hip: 'hip' },
|
|
1873
|
+
feet: { boots: 'feet', ankle: 'ankle' },
|
|
1874
|
+
arms: { forearm: 'forearm', bicep: 'upper arm', cyberware: 'arm implant' },
|
|
1875
|
+
gloves: { gloves: 'hands', wrist: 'wrist' },
|
|
1876
|
+
leftHand: { weapon: 'off hand', shield: 'shield arm', ring: 'ring', device: 'off hand' },
|
|
1877
|
+
rightHand: { weapon: 'weapon hand', ring: 'ring', device: 'hand' },
|
|
1878
|
+
};
|
|
1879
|
+
static slotWord(group, slot) {
|
|
1880
|
+
return Player.SLOT_WORDS[group]?.[slot] ?? `${group}.${slot}`;
|
|
1647
1881
|
}
|
|
1648
1882
|
/** True for the hand slots that hold a wielded weapon. */
|
|
1649
1883
|
static isWeaponSlot(group, slot) {
|
|
@@ -1667,6 +1901,23 @@ export class Player extends AbstractPlayer {
|
|
|
1667
1901
|
}
|
|
1668
1902
|
return `No equipped item matches "${input}".`;
|
|
1669
1903
|
}
|
|
1904
|
+
/**
|
|
1905
|
+
* The slot an ITEM wears in: the category decides (determineEquipmentSlot)
|
|
1906
|
+
* except where the name has to -- a trode net is Electronics like a
|
|
1907
|
+
* flashlight, and the flashlight has no slot. Kit devices (a deck, a
|
|
1908
|
+
* drone, uninstalled 'ware) answer null: they are carried, not worn.
|
|
1909
|
+
*/
|
|
1910
|
+
slotForItem(item) {
|
|
1911
|
+
if (Player.isTrodeNet(item))
|
|
1912
|
+
return { group: 'head', slot: 'trodes' };
|
|
1913
|
+
if (item.category === Category.Cyberdeck || item.category === Category.Drone || item.category === Category.Cyberware)
|
|
1914
|
+
return null;
|
|
1915
|
+
return this.determineEquipmentSlot(item.category);
|
|
1916
|
+
}
|
|
1917
|
+
/** Everything in the bag that has a slot and is not in it -- armor left in the pack (a real save: Armor Clothing carried, nothing worn). */
|
|
1918
|
+
wearablesInBag() {
|
|
1919
|
+
return this._inventory.getAllItems().filter(i => this.slotForItem(i) !== null);
|
|
1920
|
+
}
|
|
1670
1921
|
determineEquipmentSlot(input) {
|
|
1671
1922
|
const normalized = input.toLowerCase();
|
|
1672
1923
|
switch (normalized) {
|
|
@@ -1708,10 +1959,10 @@ export class Player extends AbstractPlayer {
|
|
|
1708
1959
|
return { group: 'torso', slot: 'tacticalRig' };
|
|
1709
1960
|
case 'backpack':
|
|
1710
1961
|
return { group: 'torso', slot: 'backpack' };
|
|
1962
|
+
case 'shoulder holster':
|
|
1711
1963
|
case 'left shoulder holster':
|
|
1712
|
-
return { group: 'torso', slot: 'leftShoulder' };
|
|
1713
1964
|
case 'right shoulder holster':
|
|
1714
|
-
return { group: 'torso', slot: '
|
|
1965
|
+
return { group: 'torso', slot: 'shoulder' };
|
|
1715
1966
|
case 'bandolier':
|
|
1716
1967
|
return { group: 'torso', slot: 'bandolier' };
|
|
1717
1968
|
// LEGS
|
|
@@ -1722,39 +1973,38 @@ export class Player extends AbstractPlayer {
|
|
|
1722
1973
|
case 'thigh rig':
|
|
1723
1974
|
case 'thigh holster':
|
|
1724
1975
|
return { group: 'legs', slot: 'thighRig' };
|
|
1976
|
+
case 'hip pouch':
|
|
1725
1977
|
case 'left hip pouch':
|
|
1726
|
-
return { group: 'legs', slot: 'leftHip' };
|
|
1727
1978
|
case 'right hip pouch':
|
|
1728
|
-
return { group: 'legs', slot: '
|
|
1979
|
+
return { group: 'legs', slot: 'hip' };
|
|
1729
1980
|
// FEET
|
|
1730
1981
|
case Category.Boots:
|
|
1731
1982
|
case 'combat boots':
|
|
1732
1983
|
case 'shoes':
|
|
1733
1984
|
return { group: 'feet', slot: 'boots' };
|
|
1985
|
+
case 'anklet':
|
|
1734
1986
|
case 'left anklet':
|
|
1735
|
-
return { group: 'feet', slot: 'leftAnkle' };
|
|
1736
1987
|
case 'right anklet':
|
|
1737
|
-
return { group: 'feet', slot: '
|
|
1988
|
+
return { group: 'feet', slot: 'ankle' };
|
|
1738
1989
|
// ARMS
|
|
1739
1990
|
case Category.ArmArmor:
|
|
1740
1991
|
case 'arm sleeve':
|
|
1992
|
+
case 'bracer':
|
|
1741
1993
|
case 'left forearm implant':
|
|
1742
|
-
return { group: 'arms', slot: 'leftForearm' };
|
|
1743
1994
|
case 'right forearm implant':
|
|
1744
|
-
return { group: 'arms', slot: '
|
|
1995
|
+
return { group: 'arms', slot: 'forearm' };
|
|
1996
|
+
case 'armband':
|
|
1745
1997
|
case 'left armband':
|
|
1746
|
-
return { group: 'arms', slot: 'leftBicept' };
|
|
1747
1998
|
case 'right armband':
|
|
1748
|
-
return { group: 'arms', slot: '
|
|
1999
|
+
return { group: 'arms', slot: 'bicep' };
|
|
1749
2000
|
case Category.Cyberware:
|
|
1750
2001
|
return { group: 'arms', slot: 'cyberware' };
|
|
1751
2002
|
// GLOVES / WRIST
|
|
1752
2003
|
case Category.Gloves:
|
|
1753
2004
|
case 'glove':
|
|
1754
2005
|
case 'left glove':
|
|
1755
|
-
return { group: 'gloves', slot: 'leftWrist' };
|
|
1756
2006
|
case 'right glove':
|
|
1757
|
-
return { group: 'gloves', slot: '
|
|
2007
|
+
return { group: 'gloves', slot: 'gloves' };
|
|
1758
2008
|
// WRIST-WORN gear -- a DocWagon band, a smartwatch, a med
|
|
1759
2009
|
// bracelet (player note 2026-08-25: "docwagon wristband should
|
|
1760
2010
|
// be wearable"). No new equipment GROUP was needed: the wrist
|
|
@@ -1767,9 +2017,9 @@ export class Player extends AbstractPlayer {
|
|
|
1767
2017
|
case 'wristband':
|
|
1768
2018
|
case 'bracelet':
|
|
1769
2019
|
case 'watch':
|
|
1770
|
-
|
|
2020
|
+
case 'left wrist':
|
|
1771
2021
|
case 'right wrist':
|
|
1772
|
-
return { group: 'gloves', slot: '
|
|
2022
|
+
return { group: 'gloves', slot: 'wrist' };
|
|
1773
2023
|
// LEFT HAND
|
|
1774
2024
|
case Category.Shield:
|
|
1775
2025
|
return { group: 'leftHand', slot: 'shield' };
|
|
@@ -1802,14 +2052,14 @@ export class Player extends AbstractPlayer {
|
|
|
1802
2052
|
}
|
|
1803
2053
|
const itemToUnequip = groupRef[slot];
|
|
1804
2054
|
if (!itemToUnequip) {
|
|
1805
|
-
return `Nothing
|
|
2055
|
+
return `Nothing on your ${Player.slotWord(group, slot)}.`;
|
|
1806
2056
|
}
|
|
1807
2057
|
this.addInventory(itemToUnequip);
|
|
1808
2058
|
groupRef[slot] = null;
|
|
1809
2059
|
// No weapon, nothing drawn -- keeps the flag honest for the next equip.
|
|
1810
2060
|
if (Player.isWeaponSlot(group, slot))
|
|
1811
2061
|
this.weaponDrawn = false;
|
|
1812
|
-
return `You
|
|
2062
|
+
return `You take off ${itemToUnequip.name} (${Player.slotWord(group, slot)}) and bag it.`;
|
|
1813
2063
|
}
|
|
1814
2064
|
hear(actor, message, opts) {
|
|
1815
2065
|
// Cross-plane silence: a Matrix persona's chatter never reaches meat
|
|
@@ -2107,25 +2357,51 @@ export class Player extends AbstractPlayer {
|
|
|
2107
2357
|
// point, and trim (boots, gloves, arm/leg pieces) is flavor, not
|
|
2108
2358
|
// dice -- stacking four rated accessories used to out-armor the
|
|
2109
2359
|
// jacket they hung off.
|
|
2110
|
-
|
|
2360
|
+
return this.armorBreakdown().total;
|
|
2361
|
+
}
|
|
2362
|
+
/**
|
|
2363
|
+
* ARMOR DOES NOT STACK (SR5 p.169, RAG-checked 2026-09-02): "only the
|
|
2364
|
+
* highest Armor rating applies; all other pieces only add bulk". A helmet
|
|
2365
|
+
* or shield is a "+" accessory and adds to that one piece (p.438). The
|
|
2366
|
+
* old sum let a vest under a coat out-armor either -- the same deviation
|
|
2367
|
+
* the trim rule below already closed for boots and gloves. `ignored` is
|
|
2368
|
+
* what the player is wearing for nothing, so the panel can say so.
|
|
2369
|
+
*/
|
|
2370
|
+
armorBreakdown() {
|
|
2371
|
+
let piece = null;
|
|
2372
|
+
let best = 0;
|
|
2373
|
+
const extras = [];
|
|
2374
|
+
const ignored = [];
|
|
2111
2375
|
for (const group of Object.values(this._equipment)) {
|
|
2112
2376
|
if (!group)
|
|
2113
2377
|
continue;
|
|
2114
2378
|
for (const item of Object.values(group)) {
|
|
2115
2379
|
if (!item || !Player.ARMOR_CATEGORIES.has(item.category))
|
|
2116
2380
|
continue;
|
|
2117
|
-
if (item.category === Category.Helmet)
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2381
|
+
if (item.category === Category.Helmet) {
|
|
2382
|
+
extras.push({ item, bonus: 1 });
|
|
2383
|
+
continue;
|
|
2384
|
+
}
|
|
2385
|
+
if (item.category === Category.Shield) {
|
|
2386
|
+
extras.push({ item, bonus: 2 });
|
|
2387
|
+
continue;
|
|
2388
|
+
}
|
|
2389
|
+
if (item.category === Category.Boots || item.category === Category.Gloves
|
|
2122
2390
|
|| item.category === Category.ArmArmor || item.category === Category.LegArmor)
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2391
|
+
continue;
|
|
2392
|
+
const value = 1 + item.qualityBonus + (item.isHeavyArmor ? 1 : 0);
|
|
2393
|
+
if (value > best) {
|
|
2394
|
+
if (piece)
|
|
2395
|
+
ignored.push(piece);
|
|
2396
|
+
piece = item;
|
|
2397
|
+
best = value;
|
|
2398
|
+
}
|
|
2399
|
+
else {
|
|
2400
|
+
ignored.push(item);
|
|
2401
|
+
}
|
|
2126
2402
|
}
|
|
2127
2403
|
}
|
|
2128
|
-
return total;
|
|
2404
|
+
return { total: best + extras.reduce((n, e) => n + e.bonus, 0), piece, extras, ignored };
|
|
2129
2405
|
}
|
|
2130
2406
|
getAttackPool() {
|
|
2131
2407
|
// Plane decides which attribute drives the fight: meat is muscle
|
|
@@ -2819,6 +3095,15 @@ export class Player extends AbstractPlayer {
|
|
|
2819
3095
|
get formDefenseBonus() {
|
|
2820
3096
|
return this.sustainedForms.some(f => f.key === 'static-veil') ? 2 : 0;
|
|
2821
3097
|
}
|
|
3098
|
+
/** The Force each sustained spell was cast at (cast.ts records it) -- Focused Concentration reads it. */
|
|
3099
|
+
sustainedForces = new Map();
|
|
3100
|
+
noteSustainedForce(kind, force) {
|
|
3101
|
+
this.sustainedForces.set(kind, force);
|
|
3102
|
+
}
|
|
3103
|
+
/** GREMLINS (p.81): the 1s a glitch needs, one fewer per rating, on external equipment -- the deck. */
|
|
3104
|
+
get deckGremlins() {
|
|
3105
|
+
return this.isTechnomancer() ? 0 : this.qualityRating('gremlins');
|
|
3106
|
+
}
|
|
2822
3107
|
get sustainingPenalty() {
|
|
2823
3108
|
// Each sustained effect costs -2 dice across the board: the armor
|
|
2824
3109
|
// spell, invisibility, hot reflexes, and open astral senses (see
|
|
@@ -2827,11 +3112,22 @@ export class Player extends AbstractPlayer {
|
|
|
2827
3112
|
// QUICKENING (metamagic, p.326 adapted): the armor spell anchors into
|
|
2828
3113
|
// the world and sustains itself -- its -2 simply never bills.
|
|
2829
3114
|
const armorCost = this._sustainedArmor > 0 && !this.hasMetamagic('quickening') ? 2 : 0;
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
3115
|
+
// FOCUSED CONCENTRATION (p.74, RAG-checked 2026-09-02): "sustain one
|
|
3116
|
+
// spell with a Force equal to or less than your rating without
|
|
3117
|
+
// penalty". One spell, whose Force the cast recorded; a spell with
|
|
3118
|
+
// no recorded Force is not eligible (honest, not generous).
|
|
3119
|
+
const spells = [
|
|
3120
|
+
{ cost: armorCost, force: this.sustainedForces.get('armor') },
|
|
3121
|
+
{ cost: this.sustainedInvisibility ? 2 : 0, force: this.sustainedForces.get('invisibility') },
|
|
3122
|
+
{ cost: this.sustainedReflexes ? 2 : 0, force: this.sustainedForces.get('reflexes') },
|
|
3123
|
+
{ cost: this.sustainedBarrier ? 2 : 0, force: this.sustainedBarrier?.force },
|
|
3124
|
+
{ cost: this.sustainedManaBarrier ? 2 : 0, force: this.sustainedManaBarrier?.force },
|
|
3125
|
+
];
|
|
3126
|
+
const focus = this.qualityRating('focused-concentration');
|
|
3127
|
+
const waived = focus > 0 ? spells.find(sp => sp.cost > 0 && sp.force !== undefined && sp.force <= focus) : undefined;
|
|
3128
|
+
if (waived)
|
|
3129
|
+
waived.cost = 0;
|
|
3130
|
+
return spells.reduce((n, sp) => n + sp.cost, 0)
|
|
2835
3131
|
+ (this.astralPerceiving ? 2 : 0)
|
|
2836
3132
|
+ (this.dumpshocked ? 2 : 0)
|
|
2837
3133
|
+ this.sustainedForms.length * 2;
|