@maka/maka-cli 5.118.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/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/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/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/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/hack.js +16 -3
- 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.js +4 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js.map +1 -1
- 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/mark.js +12 -2
- 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 +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +2 -2
- 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/tap.js +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.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/game.d.ts +18 -0
- package/bundle/typescript/src/commands/game/sideQuest/game.js +92 -16
- package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js.map +1 -1
- 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 +88 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +302 -50
- package/bundle/typescript/src/commands/game/sideQuest/models/player.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 +12 -1
- 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 +31 -6
- 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 +48 -1
- 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/grids.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grids.js.map +1 -1
- 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.js +8 -0
- 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/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.js +16 -5
- 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,7 +12,13 @@ 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';
|
|
@@ -576,7 +583,29 @@ export class Player extends AbstractPlayer {
|
|
|
576
583
|
return true;
|
|
577
584
|
if (this.hasAug('datajack'))
|
|
578
585
|
return true;
|
|
579
|
-
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;
|
|
580
609
|
}
|
|
581
610
|
/**
|
|
582
611
|
* THE PRICE OF SILENCE (canon p.235-236 "Running Silent",
|
|
@@ -1189,7 +1218,23 @@ export class Player extends AbstractPlayer {
|
|
|
1189
1218
|
* the only quality that edits an attribute (RAW raises the Edge ceiling;
|
|
1190
1219
|
* with no attribute point-buy here, the point lands directly).
|
|
1191
1220
|
*/
|
|
1192
|
-
|
|
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
|
+
}
|
|
1193
1238
|
if (this.qualities.has(key))
|
|
1194
1239
|
return;
|
|
1195
1240
|
this.qualities.add(key);
|
|
@@ -1415,7 +1460,8 @@ export class Player extends AbstractPlayer {
|
|
|
1415
1460
|
ears: null,
|
|
1416
1461
|
mouth: null,
|
|
1417
1462
|
nose: null,
|
|
1418
|
-
commlink: null
|
|
1463
|
+
commlink: null,
|
|
1464
|
+
trodes: null
|
|
1419
1465
|
},
|
|
1420
1466
|
torso: {
|
|
1421
1467
|
clothing: null,
|
|
@@ -1423,31 +1469,26 @@ export class Player extends AbstractPlayer {
|
|
|
1423
1469
|
armorCoat: null,
|
|
1424
1470
|
backpack: null,
|
|
1425
1471
|
tacticalRig: null,
|
|
1426
|
-
|
|
1427
|
-
rightShoulder: null,
|
|
1472
|
+
shoulder: null,
|
|
1428
1473
|
bandolier: null
|
|
1429
1474
|
},
|
|
1430
1475
|
legs: {
|
|
1431
1476
|
pants: null,
|
|
1432
1477
|
thighRig: null,
|
|
1433
|
-
|
|
1434
|
-
rightHip: null
|
|
1478
|
+
hip: null
|
|
1435
1479
|
},
|
|
1436
1480
|
feet: {
|
|
1437
1481
|
boots: null,
|
|
1438
|
-
|
|
1439
|
-
rightAnkle: null
|
|
1482
|
+
ankle: null
|
|
1440
1483
|
},
|
|
1441
1484
|
arms: {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
leftForearm: null,
|
|
1445
|
-
rightForearm: null,
|
|
1485
|
+
bicep: null,
|
|
1486
|
+
forearm: null,
|
|
1446
1487
|
cyberware: null
|
|
1447
1488
|
},
|
|
1448
1489
|
gloves: {
|
|
1449
|
-
|
|
1450
|
-
|
|
1490
|
+
gloves: null,
|
|
1491
|
+
wrist: null
|
|
1451
1492
|
},
|
|
1452
1493
|
leftHand: {
|
|
1453
1494
|
weapon: null,
|
|
@@ -1642,8 +1683,143 @@ export class Player extends AbstractPlayer {
|
|
|
1642
1683
|
this.logger.write("Failed to add item to inventory due to weight limits.");
|
|
1643
1684
|
throw new Error("The item is too heavy to add to your inventory.");
|
|
1644
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
|
+
}
|
|
1645
1709
|
this._inventory.addItem(item);
|
|
1646
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;
|
|
1647
1823
|
}
|
|
1648
1824
|
removeInventory(item) {
|
|
1649
1825
|
if (this._inventory.removeItem(item.name)) {
|
|
@@ -1660,7 +1836,7 @@ export class Player extends AbstractPlayer {
|
|
|
1660
1836
|
if (!this.canEquip(item)) {
|
|
1661
1837
|
return `You can't equip ${item.name}.`;
|
|
1662
1838
|
}
|
|
1663
|
-
const slotInfo = this.
|
|
1839
|
+
const slotInfo = this.slotForItem(item);
|
|
1664
1840
|
if (!slotInfo) {
|
|
1665
1841
|
return `${item.name} doesn't fit any known equipment slot.`;
|
|
1666
1842
|
}
|
|
@@ -1673,7 +1849,7 @@ export class Player extends AbstractPlayer {
|
|
|
1673
1849
|
let swapNotice = '';
|
|
1674
1850
|
if (previousItem) {
|
|
1675
1851
|
if (previousItem.name === item.name) {
|
|
1676
|
-
return `You already have ${item.name}
|
|
1852
|
+
return `You already have ${item.name} on -- ${Player.slotWord(group, slot)}.`;
|
|
1677
1853
|
}
|
|
1678
1854
|
this.unequipBySlot(group, slot);
|
|
1679
1855
|
swapNotice = ` (unequipped ${previousItem.name})`;
|
|
@@ -1687,7 +1863,21 @@ export class Player extends AbstractPlayer {
|
|
|
1687
1863
|
this.weaponDrawn = false;
|
|
1688
1864
|
return `You slide ${item.name} into your holster.${swapNotice}${hint(` "brandish" when you want it in hand.`)}`;
|
|
1689
1865
|
}
|
|
1690
|
-
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}`;
|
|
1691
1881
|
}
|
|
1692
1882
|
/** True for the hand slots that hold a wielded weapon. */
|
|
1693
1883
|
static isWeaponSlot(group, slot) {
|
|
@@ -1711,6 +1901,23 @@ export class Player extends AbstractPlayer {
|
|
|
1711
1901
|
}
|
|
1712
1902
|
return `No equipped item matches "${input}".`;
|
|
1713
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
|
+
}
|
|
1714
1921
|
determineEquipmentSlot(input) {
|
|
1715
1922
|
const normalized = input.toLowerCase();
|
|
1716
1923
|
switch (normalized) {
|
|
@@ -1752,10 +1959,10 @@ export class Player extends AbstractPlayer {
|
|
|
1752
1959
|
return { group: 'torso', slot: 'tacticalRig' };
|
|
1753
1960
|
case 'backpack':
|
|
1754
1961
|
return { group: 'torso', slot: 'backpack' };
|
|
1962
|
+
case 'shoulder holster':
|
|
1755
1963
|
case 'left shoulder holster':
|
|
1756
|
-
return { group: 'torso', slot: 'leftShoulder' };
|
|
1757
1964
|
case 'right shoulder holster':
|
|
1758
|
-
return { group: 'torso', slot: '
|
|
1965
|
+
return { group: 'torso', slot: 'shoulder' };
|
|
1759
1966
|
case 'bandolier':
|
|
1760
1967
|
return { group: 'torso', slot: 'bandolier' };
|
|
1761
1968
|
// LEGS
|
|
@@ -1766,39 +1973,38 @@ export class Player extends AbstractPlayer {
|
|
|
1766
1973
|
case 'thigh rig':
|
|
1767
1974
|
case 'thigh holster':
|
|
1768
1975
|
return { group: 'legs', slot: 'thighRig' };
|
|
1976
|
+
case 'hip pouch':
|
|
1769
1977
|
case 'left hip pouch':
|
|
1770
|
-
return { group: 'legs', slot: 'leftHip' };
|
|
1771
1978
|
case 'right hip pouch':
|
|
1772
|
-
return { group: 'legs', slot: '
|
|
1979
|
+
return { group: 'legs', slot: 'hip' };
|
|
1773
1980
|
// FEET
|
|
1774
1981
|
case Category.Boots:
|
|
1775
1982
|
case 'combat boots':
|
|
1776
1983
|
case 'shoes':
|
|
1777
1984
|
return { group: 'feet', slot: 'boots' };
|
|
1985
|
+
case 'anklet':
|
|
1778
1986
|
case 'left anklet':
|
|
1779
|
-
return { group: 'feet', slot: 'leftAnkle' };
|
|
1780
1987
|
case 'right anklet':
|
|
1781
|
-
return { group: 'feet', slot: '
|
|
1988
|
+
return { group: 'feet', slot: 'ankle' };
|
|
1782
1989
|
// ARMS
|
|
1783
1990
|
case Category.ArmArmor:
|
|
1784
1991
|
case 'arm sleeve':
|
|
1992
|
+
case 'bracer':
|
|
1785
1993
|
case 'left forearm implant':
|
|
1786
|
-
return { group: 'arms', slot: 'leftForearm' };
|
|
1787
1994
|
case 'right forearm implant':
|
|
1788
|
-
return { group: 'arms', slot: '
|
|
1995
|
+
return { group: 'arms', slot: 'forearm' };
|
|
1996
|
+
case 'armband':
|
|
1789
1997
|
case 'left armband':
|
|
1790
|
-
return { group: 'arms', slot: 'leftBicept' };
|
|
1791
1998
|
case 'right armband':
|
|
1792
|
-
return { group: 'arms', slot: '
|
|
1999
|
+
return { group: 'arms', slot: 'bicep' };
|
|
1793
2000
|
case Category.Cyberware:
|
|
1794
2001
|
return { group: 'arms', slot: 'cyberware' };
|
|
1795
2002
|
// GLOVES / WRIST
|
|
1796
2003
|
case Category.Gloves:
|
|
1797
2004
|
case 'glove':
|
|
1798
2005
|
case 'left glove':
|
|
1799
|
-
return { group: 'gloves', slot: 'leftWrist' };
|
|
1800
2006
|
case 'right glove':
|
|
1801
|
-
return { group: 'gloves', slot: '
|
|
2007
|
+
return { group: 'gloves', slot: 'gloves' };
|
|
1802
2008
|
// WRIST-WORN gear -- a DocWagon band, a smartwatch, a med
|
|
1803
2009
|
// bracelet (player note 2026-08-25: "docwagon wristband should
|
|
1804
2010
|
// be wearable"). No new equipment GROUP was needed: the wrist
|
|
@@ -1811,9 +2017,9 @@ export class Player extends AbstractPlayer {
|
|
|
1811
2017
|
case 'wristband':
|
|
1812
2018
|
case 'bracelet':
|
|
1813
2019
|
case 'watch':
|
|
1814
|
-
|
|
2020
|
+
case 'left wrist':
|
|
1815
2021
|
case 'right wrist':
|
|
1816
|
-
return { group: 'gloves', slot: '
|
|
2022
|
+
return { group: 'gloves', slot: 'wrist' };
|
|
1817
2023
|
// LEFT HAND
|
|
1818
2024
|
case Category.Shield:
|
|
1819
2025
|
return { group: 'leftHand', slot: 'shield' };
|
|
@@ -1846,14 +2052,14 @@ export class Player extends AbstractPlayer {
|
|
|
1846
2052
|
}
|
|
1847
2053
|
const itemToUnequip = groupRef[slot];
|
|
1848
2054
|
if (!itemToUnequip) {
|
|
1849
|
-
return `Nothing
|
|
2055
|
+
return `Nothing on your ${Player.slotWord(group, slot)}.`;
|
|
1850
2056
|
}
|
|
1851
2057
|
this.addInventory(itemToUnequip);
|
|
1852
2058
|
groupRef[slot] = null;
|
|
1853
2059
|
// No weapon, nothing drawn -- keeps the flag honest for the next equip.
|
|
1854
2060
|
if (Player.isWeaponSlot(group, slot))
|
|
1855
2061
|
this.weaponDrawn = false;
|
|
1856
|
-
return `You
|
|
2062
|
+
return `You take off ${itemToUnequip.name} (${Player.slotWord(group, slot)}) and bag it.`;
|
|
1857
2063
|
}
|
|
1858
2064
|
hear(actor, message, opts) {
|
|
1859
2065
|
// Cross-plane silence: a Matrix persona's chatter never reaches meat
|
|
@@ -2151,25 +2357,51 @@ export class Player extends AbstractPlayer {
|
|
|
2151
2357
|
// point, and trim (boots, gloves, arm/leg pieces) is flavor, not
|
|
2152
2358
|
// dice -- stacking four rated accessories used to out-armor the
|
|
2153
2359
|
// jacket they hung off.
|
|
2154
|
-
|
|
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 = [];
|
|
2155
2375
|
for (const group of Object.values(this._equipment)) {
|
|
2156
2376
|
if (!group)
|
|
2157
2377
|
continue;
|
|
2158
2378
|
for (const item of Object.values(group)) {
|
|
2159
2379
|
if (!item || !Player.ARMOR_CATEGORIES.has(item.category))
|
|
2160
2380
|
continue;
|
|
2161
|
-
if (item.category === Category.Helmet)
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
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
|
|
2166
2390
|
|| item.category === Category.ArmArmor || item.category === Category.LegArmor)
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
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
|
+
}
|
|
2170
2402
|
}
|
|
2171
2403
|
}
|
|
2172
|
-
return total;
|
|
2404
|
+
return { total: best + extras.reduce((n, e) => n + e.bonus, 0), piece, extras, ignored };
|
|
2173
2405
|
}
|
|
2174
2406
|
getAttackPool() {
|
|
2175
2407
|
// Plane decides which attribute drives the fight: meat is muscle
|
|
@@ -2863,6 +3095,15 @@ export class Player extends AbstractPlayer {
|
|
|
2863
3095
|
get formDefenseBonus() {
|
|
2864
3096
|
return this.sustainedForms.some(f => f.key === 'static-veil') ? 2 : 0;
|
|
2865
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
|
+
}
|
|
2866
3107
|
get sustainingPenalty() {
|
|
2867
3108
|
// Each sustained effect costs -2 dice across the board: the armor
|
|
2868
3109
|
// spell, invisibility, hot reflexes, and open astral senses (see
|
|
@@ -2871,11 +3112,22 @@ export class Player extends AbstractPlayer {
|
|
|
2871
3112
|
// QUICKENING (metamagic, p.326 adapted): the armor spell anchors into
|
|
2872
3113
|
// the world and sustains itself -- its -2 simply never bills.
|
|
2873
3114
|
const armorCost = this._sustainedArmor > 0 && !this.hasMetamagic('quickening') ? 2 : 0;
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
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)
|
|
2879
3131
|
+ (this.astralPerceiving ? 2 : 0)
|
|
2880
3132
|
+ (this.dumpshocked ? 2 : 0)
|
|
2881
3133
|
+ this.sustainedForms.length * 2;
|