@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.131.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
|
|
6
6
|
"description": "A command line tool for scaffolding Meteor 3.x applications using React.",
|
|
@@ -72,6 +72,15 @@ export class CallCommand extends Command {
|
|
|
72
72
|
// before NPC resolution ("taxi" is a service, not a person).
|
|
73
73
|
const spoken = rawWords.map(w => w.toLowerCase()).filter(w => w !== 'a' && w !== 'the');
|
|
74
74
|
if (this.game?.hasHub && (spoken[0] === 'taxi' || spoken[0] === 'cab')) {
|
|
75
|
+
// A CAB IS THE RUNNER'S CALL (rigger pass 2026-09-06: a deployed
|
|
76
|
+
// drone must never start a run). `call` itself stays open -- NPCs
|
|
77
|
+
// phone people (npc.ts advertises the verb) -- but the taxi branch
|
|
78
|
+
// launches the PLAYER's pending run off this.game, so only a
|
|
79
|
+
// person at the table may hail one. Latent until now because NPC
|
|
80
|
+
// command contexts carry no `game`; a rule now rather than luck.
|
|
81
|
+
if (!this.scene.isHumanControlled(this.actor)) {
|
|
82
|
+
return `The dispatcher's bored voice: "No fare on the books for ${this.actor.name}, chummer." A cab is the runner's call.`;
|
|
83
|
+
}
|
|
75
84
|
if (this.actor.plane !== 'meat') {
|
|
76
85
|
return this.actor.plane === 'matrix'
|
|
77
86
|
? `Cabs take bodies, not personas -- "jack out" first.`
|
|
@@ -777,15 +777,20 @@ export class CastCommand extends BypassCommand {
|
|
|
777
777
|
return error ?? `You don't see them here.`;
|
|
778
778
|
const self = target === this.actor;
|
|
779
779
|
// Sustained toggle per bearer: casting at them again unravels it, no
|
|
780
|
-
// drain to let go.
|
|
781
|
-
//
|
|
780
|
+
// drain to let go. The caster is the only one who can release it
|
|
781
|
+
// (p.282: the caster sustains) -- a weave somebody ELSE is holding
|
|
782
|
+
// on them is that caster's to drop, not yours to overwrite.
|
|
782
783
|
if (target.sustainedArmor > 0) {
|
|
784
|
+
const holder = target.wornSpellCaster('armor');
|
|
785
|
+
if (holder && holder !== this.actor) {
|
|
786
|
+
return `${holder.name} is holding an armor weave on ${self ? 'you' : target.name} already -- only ${holder.name} can let it go.`;
|
|
787
|
+
}
|
|
783
788
|
target.setSustainedArmor(0);
|
|
784
789
|
this.scene.updateStatus();
|
|
785
790
|
this.actor.performAction('releases armor spell', self ? '' : `on ${target.name}`);
|
|
786
791
|
return this.finishSpell([self
|
|
787
792
|
? `You let the armor spell go. The air stops humming, and the sustaining penalty lifts.`
|
|
788
|
-
: `You unravel the armor weave around ${target.name}. The shimmer fades, and
|
|
793
|
+
: `You unravel the armor weave around ${target.name}. The shimmer fades, and the burden of holding it lifts from you.`]);
|
|
789
794
|
}
|
|
790
795
|
if (target.isIncapacitated()) {
|
|
791
796
|
return `${target.name} is down -- armor won't help now.`;
|
|
@@ -795,19 +800,20 @@ export class CastCommand extends BypassCommand {
|
|
|
795
800
|
const focus = this.actor.getBestFocus();
|
|
796
801
|
const bonus = Math.ceil(force / 3) + (focus?.ratingValue ?? 0); // the focus's Force (catalog v13)
|
|
797
802
|
const spell = spellFor('armor');
|
|
798
|
-
// The
|
|
799
|
-
//
|
|
803
|
+
// The BEARER wears the weave (the soak is theirs); the CASTER
|
|
804
|
+
// sustains it and carries the -2 (p.282), on top of the drain.
|
|
800
805
|
target.setSustainedArmor(bonus);
|
|
801
806
|
target.noteSustainedForce('armor', force);
|
|
807
|
+
target.sustainedBy('armor', this.actor);
|
|
802
808
|
this.actor.performAction('casts armor', self ? '' : `on ${target.name}`);
|
|
803
809
|
this.scene.addWorldEvent(`${this.actor.name} wrapped ${self ? 'themselves' : target.name} in a shimmering armor spell.`);
|
|
804
810
|
this.scene.updateStatus();
|
|
805
|
-
// The burden is the
|
|
811
|
+
// The burden is the CASTER's ledger's, not a restated -2: a Focused
|
|
806
812
|
// Concentration holder is told "no penalty" the moment it applies.
|
|
807
|
-
const burden = sustainBurden(
|
|
813
|
+
const burden = sustainBurden(this.actor.sustainedLedger().find(w => w.key === 'armor' && (self ? w.bearer === undefined : w.bearer === target.name)), ' to everything else you do');
|
|
808
814
|
const lines = [
|
|
809
815
|
`${this.actor.name} casts armor at Force ${force}${self ? '' : ` on ${target.name}`}, ${this.castingLabel()}:`,
|
|
810
|
-
` The air hardens around ${self ? 'you' : target.name} -- +${bonus} soak dice while sustained, ${burden}.`,
|
|
816
|
+
` The air hardens around ${self ? 'you' : target.name} -- +${bonus} soak dice while sustained${self ? '' : '; you hold the weave up'}, ${burden}.`,
|
|
811
817
|
hint(` (Cast armor${self ? '' : ` on ${target.name}`} again to release it.)`),
|
|
812
818
|
...this.applyDrain(spell, force, overcast),
|
|
813
819
|
];
|
|
@@ -882,8 +888,8 @@ export class CastCommand extends BypassCommand {
|
|
|
882
888
|
}
|
|
883
889
|
/**
|
|
884
890
|
* The sustained utility pair (invisibility / increase reflexes), same
|
|
885
|
-
* toggle shape as the armor spell: the BEARER
|
|
886
|
-
*
|
|
891
|
+
* toggle shape as the armor spell: the BEARER wears the effect, the
|
|
892
|
+
* CASTER sustains it (-2, p.282) and pays the drain, and casting at
|
|
887
893
|
* the same bearer again releases it for free.
|
|
888
894
|
*/
|
|
889
895
|
async castSustainedUtility(spellKey, force, overcast, rest = []) {
|
|
@@ -894,12 +900,16 @@ export class CastCommand extends BypassCommand {
|
|
|
894
900
|
const flag = spellKey === 'invisibility' ? 'sustainedInvisibility' : 'sustainedReflexes';
|
|
895
901
|
const label = spellKey === 'invisibility' ? 'invisibility' : 'increase reflexes';
|
|
896
902
|
if (target[flag]) {
|
|
903
|
+
const holder = target.wornSpellCaster(spellKey);
|
|
904
|
+
if (holder && holder !== this.actor) {
|
|
905
|
+
return `${holder.name} is holding ${label} on ${self ? 'you' : target.name} already -- only ${holder.name} can let it go.`;
|
|
906
|
+
}
|
|
897
907
|
target[flag] = false;
|
|
898
908
|
this.scene.updateStatus();
|
|
899
909
|
this.actor.performAction(`releases ${label}`, self ? '' : `on ${target.name}`);
|
|
900
910
|
return this.finishSpell([spellKey === 'invisibility'
|
|
901
|
-
? `${self ? 'You shimmer back into the world' : `${target.name} shimmers back into the world`} -- the light remembers ${self ? 'you' : 'them'}, and the sustaining burden lifts.`
|
|
902
|
-
: `${self ? 'Your' : `${target.name}'s`} nerves cool back to human speed. The sustaining burden lifts.`]);
|
|
911
|
+
? `${self ? 'You shimmer back into the world' : `${target.name} shimmers back into the world`} -- the light remembers ${self ? 'you' : 'them'}, and the sustaining burden lifts${self ? '' : ' from you'}.`
|
|
912
|
+
: `${self ? 'Your' : `${target.name}'s`} nerves cool back to human speed. The sustaining burden lifts${self ? '' : ' from you'}.`]);
|
|
903
913
|
}
|
|
904
914
|
if (target.isIncapacitated()) {
|
|
905
915
|
return `${target.name} is down -- ${label} won't help now.`;
|
|
@@ -911,15 +921,18 @@ export class CastCommand extends BypassCommand {
|
|
|
911
921
|
const spell = spellFor(spellKey);
|
|
912
922
|
target[flag] = true;
|
|
913
923
|
target.noteSustainedForce(spellKey, force);
|
|
924
|
+
target.sustainedBy(spellKey, this.actor);
|
|
914
925
|
this.actor.performAction(`casts ${label}`, self ? '' : `on ${target.name}`);
|
|
915
926
|
this.scene.addWorldEvent(spellKey === 'invisibility'
|
|
916
927
|
? `${this.actor.name} wove an invisibility spell${self ? '' : ` over ${target.name}`}.`
|
|
917
928
|
: `${this.actor.name} cast increase reflexes${self ? '' : ` on ${target.name}`}.`);
|
|
918
929
|
this.scene.updateStatus();
|
|
919
|
-
|
|
930
|
+
// The CASTER's ledger: they hold it, they pay (p.282).
|
|
931
|
+
const burden = sustainBurden(this.actor.sustainedLedger().find(w => w.key === spellKey && (self ? w.bearer === undefined : w.bearer === target.name)), ' to everything else you do');
|
|
932
|
+
const holding = self ? '' : '; you hold it up';
|
|
920
933
|
const effectLine = spellKey === 'invisibility'
|
|
921
|
-
? ` The light slides around ${self ? 'you' : target.name} like water around a stone -- +3 dice on sneak contests while sustained, ${burden}. Loud acts still announce ${self ? 'you' : 'them'}.`
|
|
922
|
-
: ` ${self ? 'Your' : `${target.name}'s`} nerves run HOT -- +2 initiative while sustained, ${burden}.`;
|
|
934
|
+
? ` The light slides around ${self ? 'you' : target.name} like water around a stone -- +3 dice on sneak contests while sustained${holding}, ${burden}. Loud acts still announce ${self ? 'you' : 'them'}.`
|
|
935
|
+
: ` ${self ? 'Your' : `${target.name}'s`} nerves run HOT -- +2 initiative while sustained${holding}, ${burden}.`;
|
|
923
936
|
const lines = [
|
|
924
937
|
`${this.actor.name} casts ${label} at Force ${force}${self ? '' : ` on ${target.name}`}, ${this.castingLabel()}:`,
|
|
925
938
|
effectLine,
|
|
@@ -5,14 +5,18 @@ import { agentRatingFromName } from '../utilities/programs.js';
|
|
|
5
5
|
import { resonanceAction } from '../resonance-actions.js';
|
|
6
6
|
import { FIREARM_SKILLS } from '../models/player.js';
|
|
7
7
|
import { ownedDrones, droneListLines } from '../utilities/owned-drones.js';
|
|
8
|
+
import { droneLaunchAction, droneLaunchLine, droneRecallLine, droneWreckFragment } from '../utilities/drone-prose.js';
|
|
8
9
|
/**
|
|
9
10
|
* The rigger's autonomous frames (SR5e p.269-271): "deploy <drone>" puts
|
|
10
11
|
* a carried frame in the air under its own DOG-BRAIN -- no control rig
|
|
11
|
-
* needed, Pilot (2 + quality) standing
|
|
12
|
-
* It follows you, watches, and (armed
|
|
13
|
-
* you; damage lands on the HULL and a
|
|
14
|
-
* "recall" folds it back to your
|
|
15
|
-
* stick from the dog-brain
|
|
12
|
+
* needed, Pilot (the row's, else 2 + quality: Item.pilotRating) standing
|
|
13
|
+
* in for every attribute it lacks. It follows you, watches, and (armed
|
|
14
|
+
* frames only) shoots what threatens you; damage lands on the HULL and a
|
|
15
|
+
* wreck is a wreck (rest to repair). "recall" folds it back to your
|
|
16
|
+
* hand, and "jump" (jump.ts) seizes the stick from the dog-brain
|
|
17
|
+
* wherever the frame flies. Every word about the frame moving comes
|
|
18
|
+
* from utilities/drone-prose.ts, classed by how it actually moves: a
|
|
19
|
+
* Steel Lynx rolls, a Fly-Spy whines up off your palm.
|
|
16
20
|
*/
|
|
17
21
|
export class DeployCommand extends Command {
|
|
18
22
|
static verb = 'deploy';
|
|
@@ -45,23 +49,27 @@ export class DeployCommand extends Command {
|
|
|
45
49
|
if (!drone)
|
|
46
50
|
return [`Which frame? ("deploy <name>")`, ...droneListLines(owned)].join('\n');
|
|
47
51
|
if (drone.isWrecked)
|
|
48
|
-
return `The ${drone.name} is a wreck -- it deploys nowhere until you rest and
|
|
52
|
+
return `The ${drone.name} is a wreck -- ${droneWreckFragment(drone)} -- it deploys nowhere until you rest and rebuild it.`;
|
|
49
53
|
if (game.companions.some(c => c.boundDevice === drone)) {
|
|
50
54
|
return `The ${drone.name} is already in the air.`;
|
|
51
55
|
}
|
|
52
56
|
const shell = game.deployDroneShell(drone, actor.currentLocation);
|
|
53
57
|
if (!shell)
|
|
54
58
|
return `The autopilot stutters -- something already answers to that designation here.`;
|
|
55
|
-
const
|
|
56
|
-
actor.performAction('deploys',
|
|
59
|
+
const from = owned.find(d => d.item === drone)?.where === 'garage' ? 'garage' : 'hand';
|
|
60
|
+
actor.performAction('deploys', `${drone.name} -- ${droneLaunchAction(drone, from)}`);
|
|
57
61
|
this.logger.write(`DeployCommand: ${actor.name} deployed ${drone.name} on autopilot.`);
|
|
58
62
|
this.scene.updateStatus();
|
|
59
63
|
// ANY firearm skill, not one of them. A drone's mount is a gun; after
|
|
60
64
|
// the split there is no single `firearms` rating to ask for, and
|
|
61
65
|
// picking one would call a shell with a shotgun rating unarmed.
|
|
62
66
|
const armed = FIREARM_SKILLS.some(k => shell.skillRating(k) > 0);
|
|
67
|
+
// The Pilot printed is the Pilot the shell RUNS ON (deployDroneShell
|
|
68
|
+
// reads Item.pilotRating, the catalog row's): this line used to
|
|
69
|
+
// inline the price-tier fallback and disagreed with the HUD for
|
|
70
|
+
// every catalog frame.
|
|
63
71
|
return [
|
|
64
|
-
|
|
72
|
+
`${droneLaunchLine(drone, from)} -- dog-brain running, Pilot ${drone.pilotRating}.`,
|
|
65
73
|
` It follows where you walk${armed ? ` and its mount tracks what threatens you` : ` -- cameras only; it will not fight`}. Hull ${drone.droneSummary()}.${hint(` ("order ${drone.name.toLowerCase()} ..." to direct it, "recall" to bring it home, "jump" to take the stick yourself, wherever it flies.)`)}`,
|
|
66
74
|
].join('\n');
|
|
67
75
|
}
|
|
@@ -159,7 +167,7 @@ export class RecallCommand extends Command {
|
|
|
159
167
|
game.dismissCompanion(entry, 'recalled');
|
|
160
168
|
this.actor.performAction('recalls', name);
|
|
161
169
|
this.scene.updateStatus();
|
|
162
|
-
return
|
|
170
|
+
return entry.boundDevice ? droneRecallLine(entry.boundDevice) : `The ${name} comes back to hand.`;
|
|
163
171
|
}
|
|
164
172
|
/**
|
|
165
173
|
* CALL/DISMISS SPRITE (SR5 p.250, Simple Action), on this verb rather
|
|
@@ -2,6 +2,7 @@ import { Command } from './command.js';
|
|
|
2
2
|
import { Door } from '../models/door.js';
|
|
3
3
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
4
4
|
import { ownedDrones } from '../utilities/owned-drones.js';
|
|
5
|
+
import { droneReconLaunchLine, droneReconNowhereLine, droneReconReturnLine, droneSlipsVents } from '../utilities/drone-prose.js';
|
|
5
6
|
/**
|
|
6
7
|
* The rigger's mechanic: launch a carried surveillance drone for a live
|
|
7
8
|
* sweep of every room adjacent to this one -- who's there, what's locked,
|
|
@@ -32,7 +33,7 @@ export class DroneCommand extends Command {
|
|
|
32
33
|
const room = actor.currentLocation;
|
|
33
34
|
const exits = Array.from(room.exits.entries());
|
|
34
35
|
if (exits.length === 0) {
|
|
35
|
-
return
|
|
36
|
+
return droneReconNowhereLine(drone);
|
|
36
37
|
}
|
|
37
38
|
actor.performAction('launches a drone', drone.name);
|
|
38
39
|
this.logger.write(`${actor.name} launched ${drone.name} from ${room.name}.`);
|
|
@@ -43,18 +44,26 @@ export class DroneCommand extends Command {
|
|
|
43
44
|
const roll = rollPool(Math.max(1, actor.reaction + (piloting > 0 ? piloting : -1) + actor.bonus('piloting') + actor.woundModifier - actor.sustainingPenalty));
|
|
44
45
|
if (roll.hits === 0) {
|
|
45
46
|
return [
|
|
46
|
-
|
|
47
|
+
droneReconLaunchLine(drone, formatRoll(roll), false),
|
|
47
48
|
` It clips a railing, spins, and the feed dissolves into static. It limps home with nothing.`,
|
|
48
49
|
].join('\n');
|
|
49
50
|
}
|
|
50
|
-
const lines = [
|
|
51
|
+
const lines = [droneReconLaunchLine(drone, formatRoll(roll), true)];
|
|
51
52
|
for (const [direction, exit] of exits) {
|
|
52
53
|
let target;
|
|
53
54
|
let lockNote = '';
|
|
54
55
|
if (exit instanceof Door) {
|
|
55
56
|
target = exit.getOtherSide(room);
|
|
56
|
-
if (exit.checkIfLocked())
|
|
57
|
+
if (exit.checkIfLocked()) {
|
|
58
|
+
// Only a palm-sized flier slips a vent (drone-prose.ts): a
|
|
59
|
+
// locked door stops a Roto-Drone or a Steel Lynx like anyone,
|
|
60
|
+
// and the sweep says so instead of reading through it.
|
|
61
|
+
if (!droneSlipsVents(drone)) {
|
|
62
|
+
lines.push(` • ${direction}: ${target?.name ?? '?'} [door LOCKED -- no way through for a ${drone.name}]`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
57
65
|
lockNote = ' [door LOCKED -- it slipped a vent]';
|
|
66
|
+
}
|
|
58
67
|
}
|
|
59
68
|
else if (typeof exit === 'string') {
|
|
60
69
|
target = this.rooms?.[exit];
|
|
@@ -68,7 +77,7 @@ export class DroneCommand extends Command {
|
|
|
68
77
|
const node = target.hasNode ? ' -- a Matrix host node hums here' : '';
|
|
69
78
|
lines.push(` • ${direction}: ${target.name}${lockNote} -- ${who}${node}`);
|
|
70
79
|
}
|
|
71
|
-
lines.push(
|
|
80
|
+
lines.push(droneReconReturnLine(drone));
|
|
72
81
|
return lines.join('\n');
|
|
73
82
|
}
|
|
74
83
|
}
|
|
@@ -16,6 +16,12 @@ import { meetLocationFor } from '../utilities/meet-location.js';
|
|
|
16
16
|
* with AI off it resolves immediately and the click is instant).
|
|
17
17
|
*/
|
|
18
18
|
class JobOfferCommand extends Command {
|
|
19
|
+
// THE JOB IS THE PLAYER'S TO TAKE OR REFUSE (rigger pass 2026-09-06).
|
|
20
|
+
// Both verbs mutate this.game -- the player's run -- not the actor,
|
|
21
|
+
// so a companion shell ordered to "accept" would lock in the player's
|
|
22
|
+
// job. Declared once here: statics inherit, and Command.isHumanOnly()
|
|
23
|
+
// reads it off the subclass constructor (command-registry.ts).
|
|
24
|
+
static humanOnly = true;
|
|
19
25
|
/** Bare "accept"/"decline" with NO job on the table but a PARTY
|
|
20
26
|
* INVITE waiting obviously means the invite (real session typed
|
|
21
27
|
* "accept" first and got the job-offer refusal) -- route it to the
|
|
@@ -3,6 +3,14 @@ import { hint } from '../utilities/hints.js';
|
|
|
3
3
|
import { optionMark } from '../utilities/log-style.js';
|
|
4
4
|
export class JobsCommand extends Command {
|
|
5
5
|
static verb = 'jobs';
|
|
6
|
+
// THE JOB IS THE PLAYER'S TO TAKE (rigger pass 2026-09-06: "can a
|
|
7
|
+
// deployed drone take a job on its own? I don't think it should").
|
|
8
|
+
// "jobs take" acts on this.game -- the player's run -- not on the
|
|
9
|
+
// actor, so an ordered or AI-driven companion shell running it would
|
|
10
|
+
// put a job on the player's table. Latent until now only because NPC
|
|
11
|
+
// command contexts carry no `game`; now a rule, enforced and logged
|
|
12
|
+
// at command-registry.ts.
|
|
13
|
+
static humanOnly = true;
|
|
6
14
|
static description = 'The job board: what\'s on offer, CREW vs LOCAL. "jobs take <n>" puts one on the table, "jobs dismiss <n>" scraps it.';
|
|
7
15
|
async execute(args = []) {
|
|
8
16
|
const game = this.game;
|
|
@@ -3,6 +3,7 @@ import { hint } from '../utilities/hints.js';
|
|
|
3
3
|
import { enterDrone, leaveDrone } from '../utilities/planes.js';
|
|
4
4
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
5
5
|
import { ownedDrones, droneListLines } from '../utilities/owned-drones.js';
|
|
6
|
+
import { droneWreckFragment } from '../utilities/drone-prose.js';
|
|
6
7
|
/**
|
|
7
8
|
* Jumped-in rigging (SR5e p.266): "jump" pours the rigger into a carried
|
|
8
9
|
* drone -- full VR through an IMPLANTED CONTROL RIG (the book's hard
|
|
@@ -15,7 +16,7 @@ import { ownedDrones, droneListLines } from '../utilities/owned-drones.js';
|
|
|
15
16
|
*/
|
|
16
17
|
export class JumpCommand extends Command {
|
|
17
18
|
static verb = 'jump';
|
|
18
|
-
static description = 'Jump INTO a drone (control rig required): fly it, see through it, fight with its mount. A deployed frame is seized wherever it flies. "jump hot" for hot-sim; "jump" again returns to your body.';
|
|
19
|
+
static description = 'Jump INTO a drone (control rig required): fly it, see through it, fight with its mount. A deployed frame is seized wherever it flies. "jump hot" for hot-sim; "jump" again returns to your body and leaves the frame out on its dog-brain where you left it ("recall" brings it home).';
|
|
19
20
|
async execute(args = []) {
|
|
20
21
|
const actor = this.actor;
|
|
21
22
|
// Already riding: this is the jump OUT.
|
|
@@ -58,7 +59,7 @@ export class JumpCommand extends Command {
|
|
|
58
59
|
return [`Which airframe? ("jump <name>", add "hot" for hot-sim)`, ...droneListLines(owned)].join('\n');
|
|
59
60
|
}
|
|
60
61
|
if (drone.isWrecked) {
|
|
61
|
-
return `The ${drone.name} is a wreck --
|
|
62
|
+
return `The ${drone.name} is a wreck -- ${droneWreckFragment(drone)} -- and a wreck jumps nowhere. It repairs while you rest somewhere safe.`;
|
|
62
63
|
}
|
|
63
64
|
// Jumping into a DEPLOYED frame seizes it WHEREVER it flies (player
|
|
64
65
|
// request -- jumping in rides the link, not your hands): the
|
|
@@ -66,15 +67,25 @@ export class JumpCommand extends Command {
|
|
|
66
67
|
// position, not your body's. Only a frame mid-exchange refuses --
|
|
67
68
|
// combat holds live references to the shell.
|
|
68
69
|
let seizedRoom;
|
|
70
|
+
let seizedSpot;
|
|
71
|
+
let seizedCell;
|
|
69
72
|
const flying = this.game?.companions.find(c => c.boundDevice === drone);
|
|
70
73
|
if (flying) {
|
|
71
74
|
if (flying.npc.inExchange) {
|
|
72
75
|
return `The ${drone.name} is fighting for its life -- the link can't seize a bucking frame mid-exchange. Let it settle first.`;
|
|
73
76
|
}
|
|
77
|
+
// The frame's CELL comes with its room (rigger pass 2026-09-06):
|
|
78
|
+
// seizing used to carry the room and drop the cell, so the rigger
|
|
79
|
+
// snapped into the frame's room at their body's cell -- across the
|
|
80
|
+
// room from where the frame actually hovered.
|
|
74
81
|
seizedRoom = flying.npc.currentLocation;
|
|
82
|
+
seizedSpot = flying.npc.atSpot;
|
|
83
|
+
seizedCell = flying.npc.atCell;
|
|
75
84
|
this.game.dismissCompanion(flying, 'jumped into -- dog-brain yields the stick');
|
|
76
85
|
}
|
|
77
86
|
this.logger.write(`JumpCommand: ${actor.name} jumping into ${drone.name} (${mode}-sim)${seizedRoom ? ` -- seizing the deployed frame in ${seizedRoom.name}` : ''}.`);
|
|
87
|
+
// enterDrone FIRST: it sets the body's spot and cell aside from the
|
|
88
|
+
// actor's current values, which must still be the body's.
|
|
78
89
|
const lines = enterDrone(this.scene, actor, drone, mode);
|
|
79
90
|
if (seizedRoom && seizedRoom !== actor.currentLocation) {
|
|
80
91
|
actor.currentLocation = seizedRoom;
|
|
@@ -83,6 +94,10 @@ export class JumpCommand extends Command {
|
|
|
83
94
|
else if (flying) {
|
|
84
95
|
lines.push(`The dog-brain stands down -- the stick is yours.`);
|
|
85
96
|
}
|
|
97
|
+
if (flying) {
|
|
98
|
+
actor.atSpot = seizedSpot;
|
|
99
|
+
actor.atCell = seizedCell;
|
|
100
|
+
}
|
|
86
101
|
this.scene.updateStatus();
|
|
87
102
|
return lines.join('\n');
|
|
88
103
|
}
|
|
@@ -2,6 +2,7 @@ import { Command } from './command.js';
|
|
|
2
2
|
import { hint } from '../utilities/hints.js';
|
|
3
3
|
import { fuzzyPickName, significantWords, normalizeLoose, wordsAreCloseEnough } from '../utilities/fuzzy-match.js';
|
|
4
4
|
import { AI } from '../../../../tools/ai/ai.class.js';
|
|
5
|
+
import { DRONE_COLOUR, DRONE_ICON, droneRecallLine } from '../utilities/drone-prose.js';
|
|
5
6
|
/**
|
|
6
7
|
* What "you have nobody" says, one phrase per COMPANION KIND.
|
|
7
8
|
*
|
|
@@ -93,7 +94,7 @@ export class OrderCommand extends Command {
|
|
|
93
94
|
const cmd = commandWords.join(' ');
|
|
94
95
|
// The feed wears its plane's colors (player request: what the
|
|
95
96
|
// companion SEES should read as coming through its senses) -- the
|
|
96
|
-
// same visual language the HUD already speaks:
|
|
97
|
+
// same visual language the HUD already speaks: amber ✈ for a
|
|
97
98
|
// drone's sensor feed, magenta ✧ for a spirit's otherworldly report,
|
|
98
99
|
// cyan ▣ for an agent on the grid.
|
|
99
100
|
const feed = OrderCommand.FEED[entry.kind] ?? OrderCommand.FEED.ally;
|
|
@@ -137,7 +138,10 @@ export class OrderCommand extends Command {
|
|
|
137
138
|
const foldLine = entry.kind === 'spirit'
|
|
138
139
|
? `${feed.icon} You speak the release. The spirit inclines its head -- the bargain closes${(entry.services ?? 0) > 0 ? `, ${entry.services} unspent service${entry.services === 1 ? '' : 's'} forfeit` : ''} -- and it thins back to its own plane.`
|
|
139
140
|
: entry.kind === 'drone'
|
|
140
|
-
|
|
141
|
+
// The same words "recall" speaks (drone-prose.ts), classed by
|
|
142
|
+
// how the frame moves -- this line used to be a verbatim copy
|
|
143
|
+
// of companions.ts's, and both said rotors to a Steel Lynx.
|
|
144
|
+
? `${feed.icon} ${entry.boundDevice ? droneRecallLine(entry.boundDevice) : `The ${entry.npc.name} comes back to hand.`}`
|
|
141
145
|
: this.actor.plane === 'matrix'
|
|
142
146
|
? `${feed.icon} Your agent's icon collapses back into your persona -- folded into the deck.`
|
|
143
147
|
: `${feed.icon} The deck spins its agent down -- the DECK IN AR light dies as the icon folds home.`;
|
|
@@ -211,7 +215,9 @@ export class OrderCommand extends Command {
|
|
|
211
215
|
// kind fails the build here.
|
|
212
216
|
static FEED = {
|
|
213
217
|
spirit: { color: 'magenta', icon: '✧' },
|
|
214
|
-
drone:
|
|
218
|
+
// The rigger's amber (drone-prose.ts): the same constant the HUD
|
|
219
|
+
// and the room map paint with.
|
|
220
|
+
drone: { color: DRONE_COLOUR, icon: DRONE_ICON },
|
|
215
221
|
agent: { color: 'cyan', icon: '▣' },
|
|
216
222
|
// A sprite is a Matrix construct and reads cyan like the agent.
|
|
217
223
|
// Without a row of its own it fell through to `ally` and rendered
|
|
@@ -173,7 +173,7 @@ export class SheetCommand extends Command {
|
|
|
173
173
|
...(a.initiationGrade > 0 ? [row(a.isTechnomancer() ? 'Submersion' : 'Initiation', `grade ${a.initiationGrade} -- ${catalogMetamagics().filter(m => a.hasMetamagic(m.key)).map(m => m.name).join(', ')}`)] : []),
|
|
174
174
|
...(a.lockedFatigue > 0 ? [row('Fatigue', `${a.lockedFatigue} stun LOCKED until the need is met`)] : []),
|
|
175
175
|
...(a.woundModifier < 0 ? [row('Wounds', `${a.woundModifier} dice on everything`)] : []),
|
|
176
|
-
...(a.sustainedArmor > 0 ? [row('Sustained', `armor +${a.sustainedArmor} soak (${sustainBurden(a.sustainedLedger().find(w => w.key === 'armor'))})`)] : []),
|
|
176
|
+
...(a.sustainedArmor > 0 ? [row('Sustained', `armor +${a.sustainedArmor} soak (${a.wornSpellCaster('armor') ? `held by ${a.wornSpellCaster('armor').name}` : sustainBurden(a.sustainedLedger().find(w => w.key === 'armor' && w.bearer === undefined))})`)] : []),
|
|
177
177
|
];
|
|
178
178
|
return lines.join('\n');
|
|
179
179
|
}
|
|
@@ -70,14 +70,25 @@ export class SpellsCommand extends Command {
|
|
|
70
70
|
// The cost per working is the engine's (Player.sustainedLedger): a
|
|
71
71
|
// Focused Concentration holder reads "no penalty" here, not a
|
|
72
72
|
// restated -2.
|
|
73
|
+
// The CASTER pays (p.282): a spell somebody else holds on you shows
|
|
74
|
+
// as worn, with the holder named; what you hold on others is listed
|
|
75
|
+
// after, with its bearer.
|
|
73
76
|
const ledger = actor.sustainedLedger();
|
|
74
|
-
const burden = (key) =>
|
|
77
|
+
const burden = (key) => {
|
|
78
|
+
const holder = actor.wornSpellCaster(key);
|
|
79
|
+
return holder ? `held by ${holder.name} -- their burden` : sustainBurden(ledger.find(w => w.key === key && w.bearer === undefined), ' to everything else');
|
|
80
|
+
};
|
|
75
81
|
if (actor.sustainedArmor > 0) {
|
|
76
82
|
lines.push(`Sustained: armor spell, +${actor.sustainedArmor} soak (${burden('armor')}).${hint(' Cast armor again to release.')}`);
|
|
77
83
|
}
|
|
78
84
|
if (actor.sustainedInvisibility) {
|
|
79
85
|
lines.push(`Sustained: invisibility, +3 on sneak contests (${burden('invisibility')}).${hint(' Cast invisibility again to release.')}`);
|
|
80
86
|
}
|
|
87
|
+
for (const w of ledger) {
|
|
88
|
+
if (w.bearer === undefined)
|
|
89
|
+
continue;
|
|
90
|
+
lines.push(`Sustaining on ${w.bearer}: ${w.key === 'reflexes' ? 'increase reflexes' : w.key} (${sustainBurden(w, ' to everything else')}).${hint(` Cast ${w.key} on ${w.bearer} again to release.`)}`);
|
|
91
|
+
}
|
|
81
92
|
if (actor.sustainedBarrier) {
|
|
82
93
|
lines.push(`Sustained: force barrier sealing ${actor.sustainedBarrier.direction} in ${actor.sustainedBarrier.roomName}, Force ${actor.sustainedBarrier.force} (${burden('barrier')}).${hint(' Cast barrier again to release.')}`);
|
|
83
94
|
}
|
|
@@ -321,5 +321,20 @@
|
|
|
321
321
|
// blocks and is blocked like anyone, and the rigger's body keeps its own
|
|
322
322
|
// spot for jump-out. Two tables on different versions disagree about
|
|
323
323
|
// where a drone stands.
|
|
324
|
-
|
|
324
|
+
// 1.36.0 (2026-09-06): JUMP-OUT LEAVES THE FRAME DEPLOYED. COMMAND SEMANTICS
|
|
325
|
+
// in shared scenes: "jump" (out) no longer autopilots the frame home -- it
|
|
326
|
+
// re-shells on its dog-brain where it was (SR5 p.266/p.268-269: the drone
|
|
327
|
+
// reverts to Pilot on jump-out; nothing flies it back), at the frame's
|
|
328
|
+
// cell, whether it was deployed first or jumped from the hand; "recall"
|
|
329
|
+
// brings it home. A WRECKED frame (forced dump) does not re-shell.
|
|
330
|
+
// Jumping into a deployed frame now lands you at the frame's cell, not
|
|
331
|
+
// your body's. The room map draws a jumped-in rigger as an amber ✈ at the
|
|
332
|
+
// frame and the slumped body as 👤; drone feed/HUD lines are amber
|
|
333
|
+
// (#ffbf00). "jobs", "accept", "decline" and "call taxi" refuse a
|
|
334
|
+
// non-human actor. Deploy reports the frame's real Pilot; very-large
|
|
335
|
+
// frames get their cargo cap; a frame you cannot carry follows the cab
|
|
336
|
+
// under its own power; a locked door stops every frame but a palm-flier
|
|
337
|
+
// on the recon sweep. Two tables on different versions disagree about
|
|
338
|
+
// where a frame is after a jump-out.
|
|
339
|
+
export const ENGINE_VERSION = '1.36.0';
|
|
325
340
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -182,6 +182,7 @@ import { CALL_ICON, CALL_COLOR, END_CALL_SENTINEL } from './utilities/comm-style
|
|
|
182
182
|
import { AI } from '../../../tools/ai/ai.class.js';
|
|
183
183
|
import { BULLET } from './utilities/log-style.js';
|
|
184
184
|
import { spotOf, resolveSpot } from './utilities/spots.js';
|
|
185
|
+
import { DRONE_COLOUR, DRONE_ICON, droneFoldForTravelLine, droneUnfoldAtCurbLine } from './utilities/drone-prose.js';
|
|
185
186
|
export default class Game {
|
|
186
187
|
initialized;
|
|
187
188
|
static instance;
|
|
@@ -1413,13 +1414,18 @@ export default class Game {
|
|
|
1413
1414
|
}
|
|
1414
1415
|
/**
|
|
1415
1416
|
* The autonomous-drone shell, shared by the deploy command and
|
|
1416
|
-
* arrival re-shelling: the frame's dog-brain (Pilot
|
|
1417
|
-
* stands in for every attribute it
|
|
1417
|
+
* arrival re-shelling: the frame's dog-brain (Pilot: the row's, else
|
|
1418
|
+
* 2 + quality -- Item.pilotRating) stands in for every attribute it
|
|
1419
|
+
* lacks (RAW p.269-271).
|
|
1418
1420
|
*/
|
|
1419
1421
|
// Cargo sling by frame class (player request: capacity enforced -- a
|
|
1420
|
-
// palm-sized Fly-Spy is not a mule). Keyed by the frame Item's size
|
|
1422
|
+
// palm-sized Fly-Spy is not a mule). Keyed by the frame Item's size --
|
|
1423
|
+
// the enum VALUE, and by the enum itself so a size the table forgets
|
|
1424
|
+
// fails the build: this was a string map keyed `huge`, which is not a
|
|
1425
|
+
// Size, while `'very large'` was missing, so every VeryLarge frame
|
|
1426
|
+
// (a 400 kg Kodiak) fell to the 2 kg default -- less than a Medium.
|
|
1421
1427
|
static DRONE_CARGO_KG = {
|
|
1422
|
-
|
|
1428
|
+
[Size.Tiny]: 0.5, [Size.Small]: 2, [Size.Medium]: 8, [Size.Large]: 20, [Size.VeryLarge]: 40,
|
|
1423
1429
|
};
|
|
1424
1430
|
deployDroneShell(item, room) {
|
|
1425
1431
|
// The drone's own Pilot, Body and mount (catalog v13 vehicle block; the
|
|
@@ -1436,7 +1442,7 @@ export default class Game {
|
|
|
1436
1442
|
// Take-command weight checks (Player.addInventory) enforce this cap
|
|
1437
1443
|
// for the shell exactly as they do for a runner.
|
|
1438
1444
|
if (shell)
|
|
1439
|
-
shell.maxCarryingWeight = Game.DRONE_CARGO_KG[
|
|
1445
|
+
shell.maxCarryingWeight = Game.DRONE_CARGO_KG[item.size] ?? 2;
|
|
1440
1446
|
return shell;
|
|
1441
1447
|
}
|
|
1442
1448
|
/** Drops a companion's shell and its roster entry. */
|
|
@@ -1622,7 +1628,12 @@ export default class Game {
|
|
|
1622
1628
|
}
|
|
1623
1629
|
}
|
|
1624
1630
|
else if (entry.kind === 'drone') {
|
|
1625
|
-
|
|
1631
|
+
// A frame you can carry rides with you; one you cannot (a 120 kg
|
|
1632
|
+
// Steel Lynx, a 400 kg Kodiak) follows the cab on its own
|
|
1633
|
+
// dog-brain and meets you at the curb (user ruling 2026-09-06;
|
|
1634
|
+
// SR5 p.269 the Pilot drives). Same stash either way -- the
|
|
1635
|
+
// shell re-lands at the arrival room -- only the words differ.
|
|
1636
|
+
lines.push(entry.boundDevice ? droneFoldForTravelLine(entry.boundDevice) : `The drone folds for the ride.`);
|
|
1626
1637
|
if (entry.boundDevice)
|
|
1627
1638
|
this._pendingRedeploys.push(entry.boundDevice);
|
|
1628
1639
|
}
|
|
@@ -1644,7 +1655,7 @@ export default class Game {
|
|
|
1644
1655
|
continue;
|
|
1645
1656
|
const shell = this.deployDroneShell(item, this.player.currentLocation);
|
|
1646
1657
|
if (shell)
|
|
1647
|
-
lines.push(
|
|
1658
|
+
lines.push(droneUnfoldAtCurbLine(item));
|
|
1648
1659
|
}
|
|
1649
1660
|
// EVERY registered sprite re-shells from the PAN (register.ts).
|
|
1650
1661
|
// splice(0) drains the stash exactly once, the way the drone loop
|
|
@@ -3609,6 +3620,10 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3609
3620
|
endPersona(this.player);
|
|
3610
3621
|
this.player.astralTicks = 0;
|
|
3611
3622
|
this.player.activeDeck = undefined;
|
|
3623
|
+
// A frame ridden into the cab is an Item in the pack again, not a
|
|
3624
|
+
// companion (foldCompanionsForTravel ran first and never saw it), so
|
|
3625
|
+
// unlike planes.leaveDrone this path re-shells nothing: the curb
|
|
3626
|
+
// loop only re-lands what was folded.
|
|
3612
3627
|
this.player.riggedDrone = undefined;
|
|
3613
3628
|
this.player.astralPerceiving = false;
|
|
3614
3629
|
this.player.sneaking = false;
|
|
@@ -5281,9 +5296,16 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5281
5296
|
// literal -2 until 2026-09-06, while sustainingPenalty was already
|
|
5282
5297
|
// waiving a Focused Concentration holder's spell -- the HUD told the
|
|
5283
5298
|
// player they were paying for what the pools were not charging.
|
|
5299
|
+
//
|
|
5300
|
+
// THE CASTER PAYS (p.282, 2026-09-06). A spell somebody else holds
|
|
5301
|
+
// on this character is on their sheet as WORN -- soak and all --
|
|
5302
|
+
// with the holder named and no cost; what this character holds on
|
|
5303
|
+
// others rides the ledger below with its bearer named.
|
|
5284
5304
|
const ledger = player.sustainedLedger();
|
|
5305
|
+
const heldBy = (key) => player.wornSpellCaster(key)?.name;
|
|
5285
5306
|
if (player.sustainedArmor > 0) {
|
|
5286
|
-
|
|
5307
|
+
const holder = heldBy('armor');
|
|
5308
|
+
lines.push(`{magenta-fg}Sustained: armor +${player.sustainedArmor} soak (${holder ? `held by ${holder} -- their burden` : sustainBurden(ledger.find(w => w.key === 'armor' && w.bearer === undefined), ' to everything else')}){/magenta-fg}`);
|
|
5287
5309
|
}
|
|
5288
5310
|
// THE MAGE'S HALF OF THE SAME QUESTION (TNf9WrZGxCmCzqA4L, scope
|
|
5289
5311
|
// completed by ruling).
|
|
@@ -5309,10 +5331,11 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5309
5331
|
// Naming it belongs with the condition monitors if it belongs
|
|
5310
5332
|
// anywhere; that is a separate call and not this item's.
|
|
5311
5333
|
const heldSpells = [];
|
|
5334
|
+
const worn = (label, key) => heldSpells.push(heldBy(key) ? `${label} (held by ${heldBy(key)})` : label);
|
|
5312
5335
|
if (player.sustainedInvisibility)
|
|
5313
|
-
|
|
5336
|
+
worn('invisibility', 'invisibility');
|
|
5314
5337
|
if (player.sustainedReflexes)
|
|
5315
|
-
|
|
5338
|
+
worn('increased reflexes', 'reflexes');
|
|
5316
5339
|
// The barriers say WHERE, because a wall you forgot you put up is
|
|
5317
5340
|
// the one that strands the team -- and both of them seal an exit
|
|
5318
5341
|
// that the caster cannot pass either.
|
|
@@ -5322,8 +5345,14 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5322
5345
|
if (player.sustainedManaBarrier) {
|
|
5323
5346
|
heldSpells.push(`mana barrier (${player.sustainedManaBarrier.roomName} ${player.sustainedManaBarrier.direction})`);
|
|
5324
5347
|
}
|
|
5348
|
+
// What this character holds on OTHERS -- the caster's burden, named
|
|
5349
|
+
// by bearer so the mage knows whose armor is costing them two dice.
|
|
5350
|
+
for (const w of ledger) {
|
|
5351
|
+
if (w.bearer !== undefined)
|
|
5352
|
+
heldSpells.push(`${w.key === 'reflexes' ? 'increased reflexes' : w.key} on ${w.bearer}`);
|
|
5353
|
+
}
|
|
5325
5354
|
if (heldSpells.length > 0) {
|
|
5326
|
-
const these = ledger.filter(w => w.kind === 'spell' && w.key
|
|
5355
|
+
const these = ledger.filter(w => w.kind === 'spell' && !(w.key === 'armor' && w.bearer === undefined));
|
|
5327
5356
|
lines.push(`{magenta-fg}✴ Sustained: ${heldSpells.join(', ')} (${sustainTotal(these)}){/magenta-fg}`);
|
|
5328
5357
|
}
|
|
5329
5358
|
// ACTIVE THREADS, WITHOUT ASKING (TNf9WrZGxCmCzqA4L: "instead of
|
|
@@ -5429,7 +5458,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5429
5458
|
const drone = player.riggedDrone;
|
|
5430
5459
|
const hull = drone ? ` Hull ${drone.droneSummary()}` : '';
|
|
5431
5460
|
const mount = player.droneArmed ? ' ⌖ armed' : ' (recon -- no mounts)';
|
|
5432
|
-
|
|
5461
|
+
// Amber, the rigger's colour (drone-prose.ts): the same hex the
|
|
5462
|
+
// room map paints the jumped-in icon with.
|
|
5463
|
+
lines.push(`{${DRONE_COLOUR}-fg}${DRONE_ICON} JUMPED IN: ${drone?.name ?? 'drone'} ${(player.simMode ?? 'cold').toUpperCase()}-SIM${mount}${hull} -- body at ${player.bodyRoom?.name ?? '???'}{/${DRONE_COLOUR}-fg}`);
|
|
5433
5464
|
}
|
|
5434
5465
|
return lines;
|
|
5435
5466
|
}
|
|
@@ -10,6 +10,7 @@ import { spotsActive, spotOf, describeSpotRoster, spotDistanceMeters, spotCoverB
|
|
|
10
10
|
import { classifyWeapon, rangePenalty, rangeBracketName } from '../utilities/range.js';
|
|
11
11
|
import { priceOf } from '../utilities/commerce.js';
|
|
12
12
|
import { catalogGear, ROLE_DOMAINS } from '../utilities/catalog.js';
|
|
13
|
+
import { DRONE_COLOUR, DRONE_ICON } from '../utilities/drone-prose.js';
|
|
13
14
|
/**
|
|
14
15
|
* BARRIERS AT YOUR POST (a real session: a beaten sentry announced he
|
|
15
16
|
* was "unlocking" a spell-sealed hatch he could never open -- his AI had
|
|
@@ -703,7 +704,9 @@ export class NPC extends Player {
|
|
|
703
704
|
// HUD and order.ts speak.
|
|
704
705
|
static LINK_STYLE = {
|
|
705
706
|
spirit: { color: 'magenta', icon: '✧' },
|
|
706
|
-
drone:
|
|
707
|
+
// The rigger's amber (drone-prose.ts): one constant for the feed,
|
|
708
|
+
// the HUD and the room map, so "amber" means one thing everywhere.
|
|
709
|
+
drone: { color: DRONE_COLOUR, icon: DRONE_ICON },
|
|
707
710
|
agent: { color: 'cyan', icon: '▣' },
|
|
708
711
|
// A sprite is a Matrix construct and relays cyan like the agent.
|
|
709
712
|
// With no arm of its own it fell through to `ally` and a sprite
|
|
@@ -824,8 +827,8 @@ Available in-game commands (use with [COMMAND] prefix):
|
|
|
824
827
|
"manabolt" (combat damage resisted by body, not armor), "heal [on <ally>]" (heal
|
|
825
828
|
physical wounds -- your own by default, or an ally's in your room; healing a
|
|
826
829
|
wounded ally is a strong support move), "armor [on <ally>]" (sustained protection
|
|
827
|
-
on yourself or an ally; its bearer gets +soak
|
|
828
|
-
again to release). EVERY cast costs drain -- stun damage to YOU the caster; fill
|
|
830
|
+
on yourself or an ally; its bearer gets +soak, and YOU carry -2 dice to all else
|
|
831
|
+
while you hold it; cast at them again to release). EVERY cast costs drain -- stun damage to YOU the caster; fill
|
|
829
832
|
your stun track and you're out cold. Never cast if you're mundane with no magic gear.
|
|
830
833
|
- cast — bare "cast" attempts to magically bypass the ward/barrier in this room
|
|
831
834
|
without knowing the answer (requires the same talent or gear; failing burns the
|