@maka/maka-cli 5.129.0 → 5.131.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/call.js +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +28 -15
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js +18 -10
- package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +14 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/job-offer.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/jobs.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/jump.js +17 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +9 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/spells.js +12 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +16 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +43 -12
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +6 -3
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +106 -13
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +15 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +6 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/drone-prose.js +289 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +33 -6
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +44 -4
- package/package.json +1 -1
|
@@ -3631,7 +3631,7 @@ export class Player extends AbstractPlayer {
|
|
|
3631
3631
|
return;
|
|
3632
3632
|
this._sustainedArmor = next;
|
|
3633
3633
|
if (next === 0)
|
|
3634
|
-
this.
|
|
3634
|
+
this.onSustainDropped('armor');
|
|
3635
3635
|
// SUSTAINS BEAT ON FLIP (web report 2026-09-02: "I cast armor on myself
|
|
3636
3636
|
// and don't see it in the sustained list"): the sheet only caught up on
|
|
3637
3637
|
// the next unrelated beat. Every sustain mutator fires the hook now.
|
|
@@ -3682,8 +3682,27 @@ export class Player extends AbstractPlayer {
|
|
|
3682
3682
|
* spells in `sustainedForces`.
|
|
3683
3683
|
*/
|
|
3684
3684
|
sustainedWorkings() {
|
|
3685
|
+
const mine = this.wornWorkings().filter(w => w.caster === undefined).map(({ caster: _c, ...w }) => w);
|
|
3686
|
+
const onOthers = [];
|
|
3687
|
+
for (const bearer of this.sustainedBearers) {
|
|
3688
|
+
const held = bearer.wornWorkings().filter(w => w.caster === this);
|
|
3689
|
+
if (held.length === 0) {
|
|
3690
|
+
this.sustainedBearers.delete(bearer);
|
|
3691
|
+
continue;
|
|
3692
|
+
}
|
|
3693
|
+
for (const { caster: _c, ...w } of held)
|
|
3694
|
+
onOthers.push({ ...w, bearer: bearer.name });
|
|
3695
|
+
}
|
|
3696
|
+
return [...mine, ...onOthers];
|
|
3697
|
+
}
|
|
3698
|
+
/**
|
|
3699
|
+
* Every working whose STATE is on this character, with who holds it
|
|
3700
|
+
* up when that is somebody else. The bearer's view; sustainedWorkings
|
|
3701
|
+
* is the holder's.
|
|
3702
|
+
*/
|
|
3703
|
+
wornWorkings() {
|
|
3685
3704
|
const spell = (key, force) => ({
|
|
3686
|
-
key, kind: 'spell', force, category: spellFor(key)?.category.toLowerCase(),
|
|
3705
|
+
key, kind: 'spell', force, category: spellFor(key)?.category.toLowerCase(), caster: this.sustainedCasters.get(key),
|
|
3687
3706
|
});
|
|
3688
3707
|
return [
|
|
3689
3708
|
...this.sustainedForms.map((f) => ({ key: f.key, kind: 'form', force: f.level })),
|
|
@@ -3749,15 +3768,22 @@ export class Player extends AbstractPlayer {
|
|
|
3749
3768
|
}
|
|
3750
3769
|
}
|
|
3751
3770
|
releaseSustainedWorkings() {
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
this
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3771
|
+
// What this character HOLDS -- their own, and what they hold on
|
|
3772
|
+
// others (which ends on the bearer). A spell somebody else holds on
|
|
3773
|
+
// this character is that caster's concentration, and sleeping in
|
|
3774
|
+
// it does not break it.
|
|
3775
|
+
const held = this.sustainedWorkings();
|
|
3776
|
+
for (const w of held) {
|
|
3777
|
+
if (w.bearer === undefined) {
|
|
3778
|
+
this.dropSustainedWorking(w.key);
|
|
3779
|
+
continue;
|
|
3780
|
+
}
|
|
3781
|
+
for (const bearer of this.sustainedBearers) {
|
|
3782
|
+
if (bearer.name === w.bearer)
|
|
3783
|
+
bearer.dropSustainedWorking(w.key);
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
return held.length;
|
|
3761
3787
|
}
|
|
3762
3788
|
// The other sustained spells (RAW gear/spell pass): invisibility bends
|
|
3763
3789
|
// light around the bearer (+3 on sneak contests, npc.ts), increase
|
|
@@ -3770,7 +3796,7 @@ export class Player extends AbstractPlayer {
|
|
|
3770
3796
|
return;
|
|
3771
3797
|
this._sustainedInvisibility = v;
|
|
3772
3798
|
if (!v)
|
|
3773
|
-
this.
|
|
3799
|
+
this.onSustainDropped('invisibility');
|
|
3774
3800
|
this.noteConditionChanged();
|
|
3775
3801
|
}
|
|
3776
3802
|
_sustainedReflexes = false;
|
|
@@ -3780,9 +3806,72 @@ export class Player extends AbstractPlayer {
|
|
|
3780
3806
|
return;
|
|
3781
3807
|
this._sustainedReflexes = v;
|
|
3782
3808
|
if (!v)
|
|
3783
|
-
this.
|
|
3809
|
+
this.onSustainDropped('reflexes');
|
|
3784
3810
|
this.noteConditionChanged();
|
|
3785
3811
|
}
|
|
3812
|
+
/**
|
|
3813
|
+
* WHO HOLDS A SPELL THAT IS ON THIS CHARACTER, when it is not them
|
|
3814
|
+
* (2026-09-06: "bill the caster, not the bearer"). SR5 p.282 puts the
|
|
3815
|
+
* -2 on whoever SUSTAINS the spell; the subject only wears it. Until
|
|
3816
|
+
* this the engine taxed the bearer -- a mundane street sam with an
|
|
3817
|
+
* ally's armor on them lost two dice off every pool, and the mage
|
|
3818
|
+
* holding it up paid nothing.
|
|
3819
|
+
*
|
|
3820
|
+
* The effect state stays on the bearer (that is what grants the
|
|
3821
|
+
* soak); this map is the other half, and the caster's ledger reads
|
|
3822
|
+
* it through `sustainedBearers`. Session-only, like every sustain.
|
|
3823
|
+
*/
|
|
3824
|
+
sustainedCasters = new Map();
|
|
3825
|
+
/** The characters this one holds a spell on. Pruned at read time. */
|
|
3826
|
+
sustainedBearers = new Set();
|
|
3827
|
+
/** Record that `caster` is the one holding `key` up on this character. */
|
|
3828
|
+
sustainedBy(key, caster) {
|
|
3829
|
+
if (caster === this) {
|
|
3830
|
+
this.sustainedCasters.delete(key);
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
this.sustainedCasters.set(key, caster);
|
|
3834
|
+
caster.sustainedBearers.add(this);
|
|
3835
|
+
caster.noteConditionChanged();
|
|
3836
|
+
}
|
|
3837
|
+
/** The caster holding `key` on this character, when it is somebody else. */
|
|
3838
|
+
wornSpellCaster(key) {
|
|
3839
|
+
return this.sustainedCasters.get(key);
|
|
3840
|
+
}
|
|
3841
|
+
/** Bookkeeping when a spell on this character ends, whoever ended it. */
|
|
3842
|
+
onSustainDropped(key) {
|
|
3843
|
+
this.sustainedForces.delete(key);
|
|
3844
|
+
const caster = this.sustainedCasters.get(key);
|
|
3845
|
+
if (caster) {
|
|
3846
|
+
this.sustainedCasters.delete(key);
|
|
3847
|
+
caster.noteConditionChanged();
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
/** End one working by key, whatever kind it is. */
|
|
3851
|
+
dropSustainedWorking(key) {
|
|
3852
|
+
switch (key) {
|
|
3853
|
+
case 'armor':
|
|
3854
|
+
this.setSustainedArmor(0);
|
|
3855
|
+
return;
|
|
3856
|
+
case 'invisibility':
|
|
3857
|
+
this.sustainedInvisibility = false;
|
|
3858
|
+
return;
|
|
3859
|
+
case 'reflexes':
|
|
3860
|
+
this.sustainedReflexes = false;
|
|
3861
|
+
return;
|
|
3862
|
+
case 'barrier':
|
|
3863
|
+
this.sustainedBarrier = undefined;
|
|
3864
|
+
return;
|
|
3865
|
+
case 'manabarrier':
|
|
3866
|
+
this.sustainedManaBarrier = undefined;
|
|
3867
|
+
return;
|
|
3868
|
+
default: {
|
|
3869
|
+
const i = this.sustainedForms.findIndex(f => f.key === key);
|
|
3870
|
+
if (i >= 0)
|
|
3871
|
+
this.sustainedForms.splice(i, 1);
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3786
3875
|
/** The sustained PHYSICAL BARRIER spell (ruling 2026-08-24): seals
|
|
3787
3876
|
* one exit of one room while held -- go.ts and move refuse passage
|
|
3788
3877
|
* for everyone (the caster included; walls don't play favorites).
|
|
@@ -3793,6 +3882,8 @@ export class Player extends AbstractPlayer {
|
|
|
3793
3882
|
if (this._sustainedBarrier === v)
|
|
3794
3883
|
return;
|
|
3795
3884
|
this._sustainedBarrier = v;
|
|
3885
|
+
if (!v)
|
|
3886
|
+
this.onSustainDropped('barrier');
|
|
3796
3887
|
this.noteConditionChanged();
|
|
3797
3888
|
}
|
|
3798
3889
|
/** The MANA BARRIER twin (canon p.294, RAG-checked): blocks SPIRITS
|
|
@@ -3804,6 +3895,8 @@ export class Player extends AbstractPlayer {
|
|
|
3804
3895
|
if (this._sustainedManaBarrier === v)
|
|
3805
3896
|
return;
|
|
3806
3897
|
this._sustainedManaBarrier = v;
|
|
3898
|
+
if (!v)
|
|
3899
|
+
this.onSustainDropped('manabarrier');
|
|
3807
3900
|
this.noteConditionChanged();
|
|
3808
3901
|
}
|
|
3809
3902
|
// THREADED COMPLEX FORMS (M4.15, see commands/thread.ts): the
|
|
@@ -105,7 +105,18 @@ export function parseCatalogVersion(raw) {
|
|
|
105
105
|
const n = Number(raw);
|
|
106
106
|
return Number.isFinite(n) ? n : undefined;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Set by resetCatalogForTest and never cleared: a suite that has reset
|
|
110
|
+
* the catalog wants ONLY what it applies. Found 2026-09-06, the day the
|
|
111
|
+
* developer's cache moved to v17 -- every fixture pinned at 16 lost the
|
|
112
|
+
* "highest version wins" contest to a file in the home directory, and
|
|
113
|
+
* eleven accessor tests went red on one machine and green on every
|
|
114
|
+
* other. Exactly the failure the note above says a test must not have.
|
|
115
|
+
*/
|
|
116
|
+
let diskCacheDisabled = false;
|
|
108
117
|
function readCache() {
|
|
118
|
+
if (diskCacheDisabled)
|
|
119
|
+
return undefined;
|
|
109
120
|
try {
|
|
110
121
|
const parsed = JSON.parse(fs.readFileSync(cachePath(), 'utf8'));
|
|
111
122
|
if (Array.isArray(parsed?.items) && parsed.items.length > 0)
|
|
@@ -571,6 +582,10 @@ export function resetCatalogForTest() {
|
|
|
571
582
|
liveSource = undefined;
|
|
572
583
|
resolved = undefined;
|
|
573
584
|
fetchAttempted = false;
|
|
585
|
+
// The machine's cache is out of the contest from here on: a fixture
|
|
586
|
+
// applied after this is the whole catalog, whatever version it says.
|
|
587
|
+
// A test that wants a cache passes its own reader (CacheReader).
|
|
588
|
+
diskCacheDisabled = true;
|
|
574
589
|
resetCatalogIndexes();
|
|
575
590
|
}
|
|
576
591
|
/**
|
|
@@ -3,6 +3,7 @@ import { crashIC } from './ic-actors.js';
|
|
|
3
3
|
import { samePlace } from './matrix-roster.js';
|
|
4
4
|
import { NPC } from '../models/npc.js';
|
|
5
5
|
import { rollPool, formatRoll, rollInitiativeScore, formatInitiative, INITIATIVE_PASS_DROP } from './dice.js';
|
|
6
|
+
import { droneComesApartLine } from './drone-prose.js';
|
|
6
7
|
import { leaveMatrix, leaveDrone } from './planes.js';
|
|
7
8
|
import { spotOf, spotCell, seatingIn, actorCell, OPEN_FLOOR, isInCover, coverAvailableFor, takeCoverHere } from './spots.js';
|
|
8
9
|
import { heldBy } from './grapple.js';
|
|
@@ -619,7 +620,7 @@ export class CombatExchange {
|
|
|
619
620
|
lines.push(` ${defender.name} takes ${dealt} box${dealt === 1 ? '' : 'es'} to the airframe: ${device.droneSummary()}`);
|
|
620
621
|
if (device.isWrecked) {
|
|
621
622
|
defender.takeDamage(defender.maxConditionBoxes);
|
|
622
|
-
lines.push(`
|
|
623
|
+
lines.push(` ${droneComesApartLine(device)}`);
|
|
623
624
|
}
|
|
624
625
|
return { world: [...lines, ...dryWarning], meta };
|
|
625
626
|
}
|
|
@@ -660,7 +661,7 @@ export class CombatExchange {
|
|
|
660
661
|
}
|
|
661
662
|
}
|
|
662
663
|
if (drone.isWrecked) {
|
|
663
|
-
lines.push(` ${drone
|
|
664
|
+
lines.push(` ${droneComesApartLine(drone)}`);
|
|
664
665
|
lines.push(...leaveDrone(this.scene, defender, { forced: true, reason: `The airframe dies around you.` }));
|
|
665
666
|
}
|
|
666
667
|
return { world: [...lines, ...dryWarning], meta };
|
|
@@ -830,7 +831,9 @@ export class CombatExchange {
|
|
|
830
831
|
: companionKind === 'agent'
|
|
831
832
|
? `${fallen.name} DEREZZES -- its icon shears into static and rains out of the grid, gone with the deck that ran it.`
|
|
832
833
|
: companionKind === 'drone'
|
|
833
|
-
?
|
|
834
|
+
? (fallen instanceof NPC && fallen.boundDevice
|
|
835
|
+
? droneComesApartLine(fallen.boundDevice, room.name)
|
|
836
|
+
: `${fallen.name} comes apart -- plating and sparks skidding across ${room.name}. The frame is wrecked.`)
|
|
834
837
|
: (opts?.fatal ?? true)
|
|
835
838
|
? `${fallen.name} goes down -- dead before they hit the floor. The body lies where it fell.`
|
|
836
839
|
: `${fallen.name} collapses, out cold -- down for the count, but still breathing.`;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { Size } from '../types/shared/item-enum.js';
|
|
2
|
+
import { hint } from './hints.js';
|
|
3
|
+
/**
|
|
4
|
+
* ONE VOICE FOR EVERY FRAME (rigger pass, 2026-09-06). Every drone verb
|
|
5
|
+
* used to speak rotorcraft: a 120 kg wheeled Steel Lynx "lifts off your
|
|
6
|
+
* hand and settles into a watchful hover", a 400 kg Kodiak "autopilots
|
|
7
|
+
* home to your hand for the ride" and "folds back into your kit". The
|
|
8
|
+
* catalog knows better -- every drone row carries `shape`, `size`,
|
|
9
|
+
* `weight` and a vehicle block -- so the words come from here, classed
|
|
10
|
+
* by how the frame actually moves, and every verb that says anything
|
|
11
|
+
* about a frame moving (deploy, recall, order home, travel fold and
|
|
12
|
+
* curb, jump in, jump out, the recon sweep, a wreck) says it through
|
|
13
|
+
* one of these. The same shape as comm-style.ts: icon, colour, and the
|
|
14
|
+
* wording, in one place.
|
|
15
|
+
*
|
|
16
|
+
* AMBER IS THE RIGGER'S COLOUR (player ruling 2026-09-06: "when jumped
|
|
17
|
+
* into the drone, I'd like the player icon to change to amber to match
|
|
18
|
+
* the color scheme"). The drone rows of the HUD and the order feed used
|
|
19
|
+
* plain yellow; this is the first named colour in the game tree, a hex
|
|
20
|
+
* blessed accepts in a tag ({#ffbf00-fg}, the same way logo-mark.ts
|
|
21
|
+
* paints the mark), so "amber" means one thing on the map, the HUD and
|
|
22
|
+
* the feed. ✈ is the drone icon the feed already spoke; it is a BMP
|
|
23
|
+
* dingbat, so unlike 👤 it actually takes a foreground colour.
|
|
24
|
+
*/
|
|
25
|
+
export const DRONE_COLOUR = '#ffbf00';
|
|
26
|
+
export const DRONE_ICON = '✈';
|
|
27
|
+
export function droneLocomotion(item) {
|
|
28
|
+
const shape = (item.row?.shape ?? item.shape ?? '').toLowerCase();
|
|
29
|
+
if (shape === 'insect' || shape === 'disc')
|
|
30
|
+
return 'flier-palm';
|
|
31
|
+
if (shape === 'rotary' || shape === 'vtol' || shape === 'vstol')
|
|
32
|
+
return 'flier';
|
|
33
|
+
if (shape === 'missile')
|
|
34
|
+
return 'missile';
|
|
35
|
+
if (shape === 'walker')
|
|
36
|
+
return 'walker';
|
|
37
|
+
if (shape === 'anthroform' || item.vehicle?.anthro)
|
|
38
|
+
return 'anthro';
|
|
39
|
+
if (shape === 'wheeled' || shape === 'tracked' || shape === 'hull')
|
|
40
|
+
return 'ground';
|
|
41
|
+
if (item.size === Size.Tiny)
|
|
42
|
+
return 'flier-palm';
|
|
43
|
+
if (item.size === Size.Small && item.weight <= 5)
|
|
44
|
+
return 'flier';
|
|
45
|
+
return 'ground';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether the frame rides in your hands (or pocket, or pack) when you
|
|
49
|
+
* travel. Everything Large or bigger answers no, whatever its shape;
|
|
50
|
+
* fliers, walkers and the palm-sized answer yes; an anthroform or a
|
|
51
|
+
* missile only when it is Tiny/Small; a ground frame never. The rest
|
|
52
|
+
* follow a cab under their own dog-brain (SR5 p.269: the Pilot program
|
|
53
|
+
* drives; Rigger 5.0 p.174: the autopilot obeys traffic law) and meet
|
|
54
|
+
* you at the curb -- which is what foldCompanionsForTravel always did,
|
|
55
|
+
* it just used to claim a Kodiak fit in your hand.
|
|
56
|
+
*/
|
|
57
|
+
export function droneHandHeld(item) {
|
|
58
|
+
if (item.size === Size.Large || item.size === Size.VeryLarge)
|
|
59
|
+
return false;
|
|
60
|
+
const loco = droneLocomotion(item);
|
|
61
|
+
if (loco === 'flier-palm' || loco === 'flier' || loco === 'walker')
|
|
62
|
+
return true;
|
|
63
|
+
if (loco === 'ground')
|
|
64
|
+
return false;
|
|
65
|
+
return item.size === Size.Tiny || item.size === Size.Small;
|
|
66
|
+
}
|
|
67
|
+
const big = (item) => item.size === Size.Large || item.size === Size.VeryLarge;
|
|
68
|
+
const pick = (item, lines) => lines[droneLocomotion(item)];
|
|
69
|
+
/** The third-person fragment after "<name> --" in the deploy broadcast. */
|
|
70
|
+
export function droneLaunchAction(item, from) {
|
|
71
|
+
if (from === 'garage') {
|
|
72
|
+
return pick(item, {
|
|
73
|
+
'flier-palm': 'whirring up off the bench and out of the bay',
|
|
74
|
+
flier: 'lifting off the bench and out of the bay',
|
|
75
|
+
ground: 'rolling out of the garage under its own power',
|
|
76
|
+
walker: 'walking out of the bay on its own legs',
|
|
77
|
+
anthro: 'stepping out of the bay',
|
|
78
|
+
missile: 'kicking off its rack and circling the bay',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return pick(item, {
|
|
82
|
+
'flier-palm': 'micro-rotors whining up off their palm',
|
|
83
|
+
flier: 'rotors spinning up to a hover',
|
|
84
|
+
ground: 'rolling off under its own power',
|
|
85
|
+
walker: 'unfolding its legs',
|
|
86
|
+
anthro: 'standing up and squaring its shoulders',
|
|
87
|
+
missile: 'kicking off its rail',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** The deploy line itself, second person; the caller adds the Pilot. */
|
|
91
|
+
export function droneLaunchLine(item, from) {
|
|
92
|
+
const name = item.name;
|
|
93
|
+
if (from === 'garage') {
|
|
94
|
+
return pick(item, {
|
|
95
|
+
'flier-palm': `The ${name} whirs up off the bench and out of the bay to hang at eye level`,
|
|
96
|
+
flier: `The ${name} lifts off the bench and out of the bay into a watchful hover`,
|
|
97
|
+
ground: `The ${name} wakes in the bay and rolls out under its own power, idling beside you`,
|
|
98
|
+
walker: `The ${name} walks out of the bay on its own legs and settles at your feet`,
|
|
99
|
+
anthro: `The ${name} steps out of the bay, squares its shoulders, and waits`,
|
|
100
|
+
missile: `The ${name} kicks off its rack and holds a tight circle over the bay`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return pick(item, {
|
|
104
|
+
'flier-palm': `The ${name} whines up off your palm and hangs at eye level`,
|
|
105
|
+
flier: `The ${name} lifts off your hand and settles into a watchful hover`,
|
|
106
|
+
ground: big(item)
|
|
107
|
+
? `The ${name} wakes where it sits and rolls forward under its own power, idling beside you`
|
|
108
|
+
: `You set the ${name} down; it wakes and idles at your feet, motors ticking`,
|
|
109
|
+
walker: `The ${name} unfolds its legs off your hand and picks its way to the floor`,
|
|
110
|
+
anthro: big(item) || item.size === Size.Medium
|
|
111
|
+
? `The ${name} stands up, squares its shoulders, and waits`
|
|
112
|
+
: `The ${name} climbs down off your pack and stands`,
|
|
113
|
+
missile: `The ${name} kicks off its rail and holds a tight circle overhead`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** Recall / "order <drone> home": the frame comes back to you. */
|
|
117
|
+
export function droneRecallLine(item) {
|
|
118
|
+
const name = item.name;
|
|
119
|
+
const back = pick(item, {
|
|
120
|
+
'flier-palm': `The ${name} loops back and settles onto your palm, rotors stilling.`,
|
|
121
|
+
flier: `The ${name} banks once and settles back onto your hand, rotors folding.`,
|
|
122
|
+
ground: `The ${name} rolls back to your side and powers down.`,
|
|
123
|
+
walker: `The ${name} picks its way back and folds its legs into your hand.`,
|
|
124
|
+
anthro: `The ${name} walks back and powers down at your side.`,
|
|
125
|
+
missile: `The ${name} comes around and settles onto its rail, turbine spooling down.`,
|
|
126
|
+
});
|
|
127
|
+
const dents = item.droneDamage > 0
|
|
128
|
+
? ` The frame carries its dents (${item.droneSummary()})${hint(' -- rest repairs it')}.`
|
|
129
|
+
: '';
|
|
130
|
+
return `${back}${dents}`;
|
|
131
|
+
}
|
|
132
|
+
/** Pre-travel: how the frame gets to the cab, or does not need one. */
|
|
133
|
+
export function droneFoldForTravelLine(item) {
|
|
134
|
+
const name = item.name;
|
|
135
|
+
if (droneHandHeld(item)) {
|
|
136
|
+
return pick(item, {
|
|
137
|
+
'flier-palm': `The ${name} tucks into your pocket for the ride.`,
|
|
138
|
+
flier: `The ${name} autopilots home to your hand for the ride.`,
|
|
139
|
+
ground: `The ${name} rides in the pack.`,
|
|
140
|
+
walker: `The ${name} folds its legs and rides in the pack.`,
|
|
141
|
+
anthro: `The ${name} climbs into the pack for the ride.`,
|
|
142
|
+
missile: `The ${name} rides racked for the trip.`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return pick(item, {
|
|
146
|
+
'flier-palm': `The ${name} flies alongside the cab -- it will meet you at the curb.`,
|
|
147
|
+
flier: `The ${name} flies alongside the cab on its own dog-brain -- it will meet you at the curb.`,
|
|
148
|
+
ground: `The ${name} falls in behind the cab on its own dog-brain -- it will meet you at the curb.`,
|
|
149
|
+
walker: `The ${name} takes its own way there on its own legs -- it will meet you at the curb.`,
|
|
150
|
+
anthro: `The ${name} takes its own way there on its own two legs -- it will meet you at the curb.`,
|
|
151
|
+
missile: `The ${name} shadows the cab from altitude -- it will meet you at the curb.`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** Post-arrival: the frame re-shells at the curb. */
|
|
155
|
+
export function droneUnfoldAtCurbLine(item) {
|
|
156
|
+
const name = item.name;
|
|
157
|
+
if (droneHandHeld(item)) {
|
|
158
|
+
return pick(item, {
|
|
159
|
+
'flier-palm': `The ${name} whirs up off your palm at the curb.`,
|
|
160
|
+
flier: `The ${name}'s rotors spin back up at the curb.`,
|
|
161
|
+
ground: `The ${name} drops off its sling and idles at the curb.`,
|
|
162
|
+
walker: `The ${name} unfolds its legs at the curb.`,
|
|
163
|
+
anthro: `The ${name} hops down at the curb and stands.`,
|
|
164
|
+
missile: `The ${name} kicks off its rail at the curb.`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return pick(item, {
|
|
168
|
+
'flier-palm': `The ${name} is already hanging at the curb when you step out.`,
|
|
169
|
+
flier: `The ${name} is already hovering at the curb when you step out.`,
|
|
170
|
+
ground: `The ${name} pulls up at the curb behind the cab, motors ticking.`,
|
|
171
|
+
walker: `The ${name} is already waiting at the curb, legs folded under it.`,
|
|
172
|
+
anthro: `The ${name} is already waiting at the curb.`,
|
|
173
|
+
missile: `The ${name} drops out of its circle and picks up station over the curb.`,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** The "jumps into" broadcast fragment after the verb. */
|
|
177
|
+
export function droneJumpInAction(item, mode) {
|
|
178
|
+
const wake = pick(item, {
|
|
179
|
+
'flier-palm': 'the micro-rotors whine up',
|
|
180
|
+
flier: 'the rotors spin up',
|
|
181
|
+
ground: 'the drive motors wake',
|
|
182
|
+
walker: 'the legs take the weight',
|
|
183
|
+
anthro: 'the frame\'s limbs answer theirs',
|
|
184
|
+
missile: 'the turbine spools',
|
|
185
|
+
});
|
|
186
|
+
return `${item.name} -- body going slack as ${wake} (${mode}-sim)`;
|
|
187
|
+
}
|
|
188
|
+
/** The sensor-sight slot in the jump-in line ("...you ARE the X now, <this>, your body slumped..."). */
|
|
189
|
+
export function droneJumpInFragment(item) {
|
|
190
|
+
return pick(item, {
|
|
191
|
+
'flier-palm': 'micro-rotors whining',
|
|
192
|
+
flier: 'rotors humming',
|
|
193
|
+
ground: 'drive motors humming under you',
|
|
194
|
+
walker: 'legs ticking under you',
|
|
195
|
+
anthro: 'servos whining in your limbs',
|
|
196
|
+
missile: 'turbine screaming',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Jump-out: the frame STAYS where it is on its dog-brain (SR5 p.266,
|
|
201
|
+
* p.268-269: the drone reverts to its Pilot program when the rigger
|
|
202
|
+
* leaves; nothing flies it home). Same room or another, the line says
|
|
203
|
+
* where it was left.
|
|
204
|
+
*/
|
|
205
|
+
export function droneJumpOutLine(item, frameRoom, bodyRoom) {
|
|
206
|
+
const holds = pick(item, {
|
|
207
|
+
'flier-palm': 'holds its hover',
|
|
208
|
+
flier: 'holds its hover',
|
|
209
|
+
ground: 'sits where it stopped, motors idling',
|
|
210
|
+
walker: 'stands where it is',
|
|
211
|
+
anthro: 'stands where it is, holding the pose',
|
|
212
|
+
missile: 'holds its circle overhead',
|
|
213
|
+
});
|
|
214
|
+
const where = frameRoom === bodyRoom ? 'where you left it' : `in ${frameRoom.name}`;
|
|
215
|
+
return `You're back in your body in ${bodyRoom.name}; the ${item.name} ${holds} ${where} -- dog-brain on the stick.${hint(' ("recall" brings it home.)')}`;
|
|
216
|
+
}
|
|
217
|
+
/** The recon sweep ("drone"): nowhere to go, launch, and return. */
|
|
218
|
+
export function droneReconNowhereLine(item) {
|
|
219
|
+
return pick(item, {
|
|
220
|
+
'flier-palm': `The ${item.name} lifts, circles once -- nowhere to go from here.`,
|
|
221
|
+
flier: `The ${item.name} lifts, circles once -- nowhere to go from here.`,
|
|
222
|
+
ground: `The ${item.name} rolls a slow loop -- nowhere to go from here.`,
|
|
223
|
+
walker: `The ${item.name} picks a circle around your feet -- nowhere to go from here.`,
|
|
224
|
+
anthro: `The ${item.name} looks around -- nowhere to go from here.`,
|
|
225
|
+
missile: `The ${item.name} climbs, circles once -- nowhere to go from here.`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
export function droneReconLaunchLine(item, rollText, ok) {
|
|
229
|
+
const off = pick(item, {
|
|
230
|
+
'flier-palm': `The ${item.name} whirs off your palm`,
|
|
231
|
+
flier: `The ${item.name} lifts off your hand`,
|
|
232
|
+
ground: `The ${item.name} rolls off`,
|
|
233
|
+
walker: `The ${item.name} scuttles off`,
|
|
234
|
+
anthro: `The ${item.name} walks off`,
|
|
235
|
+
missile: `The ${item.name} kicks off its rail`,
|
|
236
|
+
});
|
|
237
|
+
if (!ok)
|
|
238
|
+
return `${off} -- Piloting: ${rollText}`;
|
|
239
|
+
const threads = pick(item, {
|
|
240
|
+
'flier-palm': 'threads the gaps',
|
|
241
|
+
flier: 'threads the gaps',
|
|
242
|
+
ground: 'noses through the doorways',
|
|
243
|
+
walker: 'picks through the gaps',
|
|
244
|
+
anthro: 'goes to look',
|
|
245
|
+
missile: 'makes a fast pass',
|
|
246
|
+
});
|
|
247
|
+
return `${off} and ${threads} -- Piloting: ${rollText}. Feed incoming:`;
|
|
248
|
+
}
|
|
249
|
+
export function droneReconReturnLine(item) {
|
|
250
|
+
return pick(item, {
|
|
251
|
+
'flier-palm': `The ${item.name} loops home and folds back into your kit.`,
|
|
252
|
+
flier: `The ${item.name} banks home and folds onto your hand.`,
|
|
253
|
+
ground: `The ${item.name} rolls back and powers down at your feet.`,
|
|
254
|
+
walker: `The ${item.name} picks its way back into your hand.`,
|
|
255
|
+
anthro: `The ${item.name} walks back and powers down.`,
|
|
256
|
+
missile: `The ${item.name} comes around and settles onto its rail.`,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/** Can this frame get past a locked door on the recon sweep? Only the
|
|
260
|
+
* palm-sized fliers slip a vent; everything else is stopped by a door
|
|
261
|
+
* like anyone. */
|
|
262
|
+
export function droneSlipsVents(item) {
|
|
263
|
+
return droneLocomotion(item) === 'flier-palm';
|
|
264
|
+
}
|
|
265
|
+
/** What a wreck of this frame looks like: "bent rotors, dead boards". */
|
|
266
|
+
export function droneWreckFragment(item) {
|
|
267
|
+
return pick(item, {
|
|
268
|
+
'flier-palm': 'crushed rotors, dead boards',
|
|
269
|
+
flier: 'bent rotors, dead boards',
|
|
270
|
+
ground: 'buckled frame, dead boards',
|
|
271
|
+
walker: 'snapped legs, dead boards',
|
|
272
|
+
anthro: 'dead servos, dead boards',
|
|
273
|
+
missile: 'torn fins, dead boards',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/** The moment of wrecking, in a fight. */
|
|
277
|
+
export function droneComesApartLine(item, roomName) {
|
|
278
|
+
const bits = pick(item, {
|
|
279
|
+
'flier-palm': 'rotors, casing, sparks',
|
|
280
|
+
flier: 'rotors, casing, sparks',
|
|
281
|
+
ground: 'plating, wheels, sparks',
|
|
282
|
+
walker: 'legs, casing, sparks',
|
|
283
|
+
anthro: 'limbs, plating, sparks',
|
|
284
|
+
missile: 'fins, casing, fire',
|
|
285
|
+
});
|
|
286
|
+
const where = roomName ? ` skidding across ${roomName}` : '';
|
|
287
|
+
return `${item.name} comes apart -- ${bits}${where}. The frame is wrecked.`;
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=drone-prose.js.map
|
|
@@ -7,6 +7,7 @@ import { fuzzyPickName } from './fuzzy-match.js';
|
|
|
7
7
|
import { resetOverwatch } from './overwatch.js';
|
|
8
8
|
import { rebootWipe } from './marks.js';
|
|
9
9
|
import { hint } from './hints.js';
|
|
10
|
+
import { droneJumpInAction, droneJumpInFragment, droneJumpOutLine, droneWreckFragment } from './drone-prose.js';
|
|
10
11
|
/**
|
|
11
12
|
* Plane transitions and their tolls -- the one place that moves an actor
|
|
12
13
|
* between meat, Matrix, and astral (see Player.plane for the model).
|
|
@@ -288,7 +289,7 @@ export function enterDrone(scene, actor, drone, mode) {
|
|
|
288
289
|
actor.simMode = mode;
|
|
289
290
|
actor.riggedDrone = drone;
|
|
290
291
|
actor.sneaking = false;
|
|
291
|
-
actor.performAction('jumps into',
|
|
292
|
+
actor.performAction('jumps into', droneJumpInAction(drone, mode));
|
|
292
293
|
scene.addWorldEvent(`${actor.name} jumped into ${drone.name} in ${actor.bodyRoom.name}.`);
|
|
293
294
|
Logger.getInstance().write(`${actor.name} jumped into ${drone.name} (${mode}-sim, hull ${drone.droneSummary()}) from ${actor.bodyRoom.name}.`);
|
|
294
295
|
const hullNote = drone.droneDamage > 0 ? ` The airframe is already scarred: ${drone.droneSummary()}.` : '';
|
|
@@ -296,8 +297,8 @@ export function enterDrone(scene, actor, drone, mode) {
|
|
|
296
297
|
? ` Hot-sim: the machine answers like your own skin -- and every hit it takes bleeds PHYSICAL into yours.`
|
|
297
298
|
: ` Cold-sim: hits on the hull sting through the link as stun, half strength.`;
|
|
298
299
|
return [
|
|
299
|
-
`The rig takes hold and the world snaps to sensor-sight -- you ARE the ${drone.name} now,
|
|
300
|
-
hint(`Move with "go" (you fly the meat world; locked doors still stop an airframe), "look" through the sensors, "jump" again to drop back into your body.`),
|
|
300
|
+
`The rig takes hold and the world snaps to sensor-sight -- you ARE the ${drone.name} now, ${droneJumpInFragment(drone)}, your body slumped and empty in ${actor.bodyRoom.name}.${hotWarning}${hullNote}`,
|
|
301
|
+
hint(`Move with "go" (you fly the meat world; locked doors still stop an airframe), "look" through the sensors, "jump" again to drop back into your body -- the frame stays out on its dog-brain where you leave it; "recall" brings it home.`),
|
|
301
302
|
].filter(l => l.length > 0);
|
|
302
303
|
}
|
|
303
304
|
/**
|
|
@@ -308,11 +309,27 @@ export function enterDrone(scene, actor, drone, mode) {
|
|
|
308
309
|
* Willpower-only resist -- the same softening the Matrix side had, minus
|
|
309
310
|
* the Firewall term, plus the only correct half either copy had (the
|
|
310
311
|
* p.229 disorientation, which the Matrix side was missing entirely).
|
|
312
|
+
*
|
|
313
|
+
* THE FRAME STAYS WHERE IT IS (user ruling 2026-09-06, and canon: SR5
|
|
314
|
+
* p.266 / p.268-269 -- when the rigger leaves, the drone reverts to its
|
|
315
|
+
* Pilot program at the next Combat Turn; nothing flies it home). This
|
|
316
|
+
* used to say the frame "autopilots home to your hand", which was both
|
|
317
|
+
* a deviation and a lie to a rigger who deployed a frame, seized it two
|
|
318
|
+
* rooms away and let go: the frame vanished. Now it re-shells on its
|
|
319
|
+
* dog-brain in the frame's room, at the frame's cell, whether you
|
|
320
|
+
* deployed it first or jumped straight from your hand, and "recall"
|
|
321
|
+
* brings it home. A WRECKED frame (the forced dump) never re-shells.
|
|
322
|
+
* Ordered AFTER the body is put back so seatingIn sees the rigger on
|
|
323
|
+
* bodyCell before the shell claims frameCell.
|
|
311
324
|
*/
|
|
312
325
|
export function leaveDrone(scene, actor, opts) {
|
|
313
326
|
const lines = [];
|
|
314
327
|
const body = actor.bodyRoom;
|
|
315
328
|
const drone = actor.riggedDrone;
|
|
329
|
+
// Where the FRAME is, captured before the body restore overwrites it.
|
|
330
|
+
const frameRoom = actor.currentLocation;
|
|
331
|
+
const frameSpot = actor.atSpot;
|
|
332
|
+
const frameCell = actor.atCell;
|
|
316
333
|
if (opts?.forced) {
|
|
317
334
|
lines.push(...applyDumpshock(actor, opts.reason ?? `The link dies with the airframe.`));
|
|
318
335
|
}
|
|
@@ -320,7 +337,7 @@ export function leaveDrone(scene, actor, opts) {
|
|
|
320
337
|
lines.push(`You let go of the machine and drop back down the link.`);
|
|
321
338
|
}
|
|
322
339
|
if (drone?.isWrecked) {
|
|
323
|
-
lines.push(`The ${drone.name} is WRECKED --
|
|
340
|
+
lines.push(`The ${drone.name} is WRECKED -- ${droneWreckFragment(drone)}. It repairs while you rest somewhere safe.`);
|
|
324
341
|
}
|
|
325
342
|
actor.plane = 'meat';
|
|
326
343
|
actor.simMode = undefined;
|
|
@@ -336,8 +353,18 @@ export function leaveDrone(scene, actor, opts) {
|
|
|
336
353
|
actor.atCell = actor.bodyCell;
|
|
337
354
|
actor.bodySpot = undefined;
|
|
338
355
|
actor.bodyCell = undefined;
|
|
339
|
-
|
|
340
|
-
|
|
356
|
+
// The dog-brain takes the stick where the frame is (see above).
|
|
357
|
+
const game = scene.ownerGame;
|
|
358
|
+
const shell = drone && !drone.isWrecked && game && frameRoom
|
|
359
|
+
? game.deployDroneShell(drone, frameRoom)
|
|
360
|
+
: null;
|
|
361
|
+
if (shell) {
|
|
362
|
+
shell.atSpot = frameSpot;
|
|
363
|
+
shell.atCell = frameCell;
|
|
364
|
+
Logger.getInstance().write(`${drone.name} re-shelled on its dog-brain in ${frameRoom.name} after ${actor.name} jumped out.`);
|
|
365
|
+
}
|
|
366
|
+
lines.push(shell && drone
|
|
367
|
+
? droneJumpOutLine(drone, frameRoom, body)
|
|
341
368
|
: `You're back in your body in ${body.name}.`);
|
|
342
369
|
}
|
|
343
370
|
actor.performAction('jumps out', 'stirring awake as the rig releases');
|